http.ServeFile gestisce effettivamente le range request correttamente per i file statici. Il problema sorge quando cerchi di costruire qualcosa di piu sofisticato: transcodifica al volo, live streaming, o segmentazione HLS. Questo articolo copre ogni scenario con codice funzionante, e spiega cosa gestisce e cosa non gestisce http.ServeFile.
Come funzionano i video player#
Un video player non richiede l’intero file in una singola chiamata HTTP. Invia una richiesta Range:
GET /video.mp4 HTTP/1.1
Range: bytes=0-1048575Il server risponde con 206 Partial Content e l’intervallo di byte richiesto:
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1048575/52428800
Accept-Ranges: bytes
Content-Length: 1048576Quando l’utente cerca una posizione diversa, il player invia una nuova range request per l’offset di byte corrispondente a quel timestamp. Senza supporto delle range request, il seeking azzera dall’inizio.
Servire un file video statico con supporto range#
Per un file statico su disco, http.ServeFile gestisce automaticamente le range request. Questa e l’implementazione corretta e completa:
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/video/", func(w http.ResponseWriter, r *http.Request) {
// ServeFile gestisce header Range, risposte 206, ETag e If-Modified-Since.
// Il path dopo /video/ mappa al filesystem.
http.ServeFile(w, r, "videos/"+r.URL.Path[len("/video/"):])
})
log.Println("in ascolto su :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}http.ServeFile e appropriato per i file statici. Gestisce gli header Range, ETag, If-Modified-Since, If-None-Match e If-Range. Usalo per la distribuzione di video statici. Il resto di questo articolo copre scenari in cui hai bisogno di maggiore controllo.
Implementare le range request manualmente#
Quando servi byte da qualcosa di diverso da un file (un buffer, uno stream, un output transcodificato), devi implementare la gestione range tu stesso:
package rangeserver
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
)
// ServeRange serve il file in path con pieno supporto delle range request.
func ServeRange(w http.ResponseWriter, r *http.Request, path string) {
f, err := os.Open(path)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
defer f.Close()
info, err := f.Stat()
if err != nil {
http.Error(w, "stat error", http.StatusInternalServerError)
return
}
totalSize := info.Size()
w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Type", "video/mp4")
rangeHeader := r.Header.Get("Range")
if rangeHeader == "" {
w.Header().Set("Content-Length", strconv.FormatInt(totalSize, 10))
w.WriteHeader(http.StatusOK)
io.Copy(w, f)
return
}
// Parsa "bytes=start-end"
start, end, err := parseRange(rangeHeader, totalSize)
if err != nil {
http.Error(w, "invalid range", http.StatusRequestedRangeNotSatisfiable)
return
}
length := end - start + 1
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, totalSize))
w.Header().Set("Content-Length", strconv.FormatInt(length, 10))
w.WriteHeader(http.StatusPartialContent)
f.Seek(start, io.SeekStart)
io.CopyN(w, f, length)
}
func parseRange(header string, total int64) (int64, int64, error) {
if !strings.HasPrefix(header, "bytes=") {
return 0, 0, fmt.Errorf("unsupported range unit")
}
parts := strings.Split(header[6:], "-")
if len(parts) != 2 {
return 0, 0, fmt.Errorf("malformed range")
}
start, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return 0, 0, err
}
end := total - 1
if parts[1] != "" {
end, err = strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return 0, 0, err
}
}
if start < 0 || end >= total || start > end {
return 0, 0, fmt.Errorf("range out of bounds")
}
return start, end, nil
}Segmentazione HLS con FFmpeg#
HLS (HTTP Live Streaming) divide il video in piccoli segmenti .ts e genera un manifest .m3u8. I player scaricano il manifest, poi recuperano i segmenti sequenzialmente. Questo abilita lo streaming a bitrate adattivo e il seeking affidabile.
Usa exec.Command per eseguire FFmpeg come sottoprocesso:
package hls
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
)
// Segment transcodifica inputPath in segmenti HLS in outputDir.
// Blocca finche FFmpeg non finisce (caso d'uso VOD).
func Segment(inputPath, outputDir string) error {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("create output dir: %w", err)
}
manifestPath := filepath.Join(outputDir, "index.m3u8")
segmentPattern := filepath.Join(outputDir, "segment%03d.ts")
cmd := exec.Command(
"ffmpeg",
"-i", inputPath,
"-codec:", "copy", // Copia gli stream senza ri-encodare (veloce, nessuna perdita di qualita)
"-start_number", "0",
"-hls_time", "6", // Durata target del segmento in secondi
"-hls_list_size", "0", // 0 = mantieni tutti i segmenti nel manifest (VOD)
"-hls_segment_filename", segmentPattern,
"-f", "hls",
manifestPath,
)
// Cattura stderr per il debug. FFmpeg scrive il suo log li.
cmd.Stderr = os.Stderr
log.Printf("segmentazione %s -> %s", inputPath, outputDir)
if err := cmd.Run(); err != nil {
return fmt.Errorf("ffmpeg: %w", err)
}
log.Printf("segmentazione completata: %s", manifestPath)
return nil
}Usare -codec: copy copia gli stream senza ri-encodare. E veloce e senza perdite ma richiede che l’input sia gia in un codec compatibile con il container (video H.264, audio AAC per HLS). Se hai bisogno di transcodificare, sostituisci con -c:v libx264 -c:a aac e aggiungi flag per il bitrate.
Servire HLS da Go#
Servi il manifest .m3u8 e i segmenti .ts con i tipi MIME corretti e gli header CORS:
package hls
import (
"net/http"
"path/filepath"
"strings"
)
// Handler serve file HLS da dir.
func Handler(dir string) http.Handler {
fs := http.FileServer(http.Dir(dir))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Gli header CORS sono necessari per la riproduzione HLS da un'origine diversa
// (es. un player video.js su una porta diversa).
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Range")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
// Imposta i tipi MIME corretti.
ext := strings.ToLower(filepath.Ext(r.URL.Path))
switch ext {
case ".m3u8":
w.Header().Set("Content-Type", "application/vnd.apple.mpegurl")
w.Header().Set("Cache-Control", "no-cache") // Il manifest cambia durante il live
case ".ts":
w.Header().Set("Content-Type", "video/mp2t")
w.Header().Set("Cache-Control", "public, max-age=3600") // I segmenti sono immutabili
}
fs.ServeHTTP(w, r)
})
}Collegalo:
package main
import (
"flag"
"log"
"net/http"
"github.com/yourname/video/hls"
)
func main() {
segDir := flag.String("dir", "./segments", "directory segmenti HLS")
addr := flag.String("addr", ":8080", "indirizzo di ascolto")
flag.Parse()
mux := http.NewServeMux()
mux.Handle("/hls/", http.StripPrefix("/hls/", hls.Handler(*segDir)))
log.Printf("HLS server su %s, servendo da %s", *addr, *segDir)
log.Fatal(http.ListenAndServe(*addr, mux))
}I player accedono a http://localhost:8080/hls/index.m3u8.
Live streaming con FFmpeg#
Per il live streaming da una webcam o da una sorgente RTMP, FFmpeg scrive segmenti continuamente in una directory. Il server Go li serve. Il manifest usa un hls_list_size finito e il player lo recupera ripetutamente:
package hls
import (
"context"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
)
// StartLive avvia uno stream HLS live da inputSource in outputDir.
// Rispetta ctx per la cancellazione graceful.
func StartLive(ctx context.Context, inputSource, outputDir string) error {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("create output dir: %w", err)
}
manifestPath := filepath.Join(outputDir, "live.m3u8")
segmentPattern := filepath.Join(outputDir, "live%05d.ts")
cmd := exec.CommandContext(ctx,
"ffmpeg",
"-re", // Leggi l'input al framerate nativo (per input da file)
"-i", inputSource, // es. "rtmp://localhost/live/stream" o "/dev/video0"
"-c:v", "libx264",
"-preset", "veryfast", // Bassa CPU, bassa latenza
"-tune", "zerolatency",
"-c:a", "aac",
"-hls_time", "2", // Segmenti brevi per bassa latenza
"-hls_list_size", "5", // Mantieni 5 segmenti nel manifest (sliding window)
"-hls_flags", "delete_segments+append_list",
"-hls_segment_filename", segmentPattern,
"-f", "hls",
manifestPath,
)
cmd.Stderr = os.Stderr
log.Printf("avvio live stream: %s -> %s", inputSource, outputDir)
if err := cmd.Start(); err != nil {
return fmt.Errorf("ffmpeg start: %w", err)
}
// Attendi che FFmpeg termini (la cancellazione del context lo ferma via CommandContext).
if err := cmd.Wait(); err != nil && ctx.Err() == nil {
return fmt.Errorf("ffmpeg exited: %w", err)
}
return nil
}-hls_flags delete_segments rimuove i vecchi file .ts dal disco. Questo previene la crescita illimitata del disco durante stream live lunghi, ma significa che i segmenti non piu nel manifest daranno 404 se un player lento li richiede. Calibra hls_list_size e la durata dei segmenti in base al tuo trade-off latenza/buffer.
Progressive download vs HLS vs DASH#
Come funziona: Singolo file MP4 servito con supporto range request.
Pro: Semplice. Un file. Funziona ovunque.
Contro: Nessun bitrate adattivo. Il seeking richiede il download fino a quell’offset di byte se il file non e gia bufferizzato. Non adatto per stream live.
Quando usarlo: Clip brevi, download, embed video semplici.
http.HandleFunc("/video/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./videos/"+r.URL.Path[len("/video/"):])
})Come funziona: Video diviso in segmenti .ts, manifest .m3u8. Il player recupera il manifest, poi i segmenti.
Pro: Supporto nativo su iOS e Safari. Bitrate adattivo (varianti di qualita multiple). Funziona per il live. Seekable senza download completo.
Contro: Latenza piu alta rispetto al progressive download (minimo ~2x la durata del segmento). Richiede FFmpeg o simile per la segmentazione.
Quando usarlo: Video mobile, live streaming, qualsiasi cosa che necessiti di bitrate adattivo.
Come funziona: Simile a HLS ma usa manifest XML .mpd e segmenti MP4 frammentati .m4s.
Pro: Standard aperto. Miglior supporto browser su Android. Struttura dei segmenti piu flessibile.
Contro: Nessun supporto nativo iOS/Safari senza una libreria player (dash.js, Shaka Player). Formato manifest piu complesso.
Quando usarlo: Quando hai bisogno di bitrate adattivo cross-platform senza l’overhead nativo HLS di Safari.
Produzione: usa un CDN per la distribuzione dei segmenti#
flowchart LR
Source["Sorgente\n(camera / file)"] --> FFmpeg["FFmpeg\n(sottoprocesso Go)"]
FFmpeg --> Segments["Segmenti su disco\n(file .m3u8 + .ts)"]
Segments --> GoServer["Go HTTP Server\n(origin)"]
GoServer --> CDN["CDN\n(CloudFront / CloudFlare)"]
CDN --> Players["Video Player\n(milioni)"]
Il server Go in questo articolo gestisce la generazione dei segmenti e funge da server origin. Per qualsiasi traffico reale, metti un CDN davanti. CloudFront puo fare cache dei segmenti .ts (sono immutabili una volta scritti) con un TTL lungo. Il manifest (.m3u8) dovrebbe essere servito con Cache-Control: no-cache o un TTL molto breve in modo che i player ottengano sempre la lista corrente dei segmenti.
Go e per generare segmenti. Il CDN e per distribuirli in scala. Non servire segmenti video direttamente dal tuo processo Go a qualsiasi livello di traffico significativo.
Se vuoi approfondire questi argomenti, offro sessioni di coaching 1:1 per ingegneri che lavorano su integrazione AI, architettura cloud e platform engineering. Prenota una sessione (50 EUR / 60 min) o scrivimi a manuel.fedele+website@gmail.com.