Ecco una soluzione completa e funzionante scritta in Go (Golang).
Questa applicazione implementa un LL-DASH Origin Server minimale. Il concetto chiave qui è l'interfaccia http.Flusher di Go, che ci permette di inviare i dati al client mentre li stiamo ancora ricevendo o generando, senza aspettare la fine della richiesta. Architettura del Codice
Il Broker (Pub/Sub): Gestisce la memoria. Quando l'encoder invia dati, il Broker li distribuisce a tutti i player connessi in quel momento.
Endpoint Ingest (POST): Simula l'ingresso dell'encoder (es. FFmpeg che fa una PUT/POST dei chunk).
Endpoint Player (GET): Il player si connette. Il server imposta Transfer-Encoding: chunked (implicito in Go quando non c'è Content-Length) e usa Flush() dopo ogni scrittura.
Il Codice (main.go)
Salva questo codice in un file chiamato main.go. code Go
package main
import ( "fmt" "io" "log" "net/http" "sync" )
// --- 1. IL BROKER (Gestione Memoria Pub/Sub) --- // Questo sistema permette di ricevere dati dall'encoder e distribuirli // a tutti i client connessi in tempo reale.
type Broker struct { clients map[chan []byte]bool // Mappa dei canali dei client attivi lock sync.Mutex // Mutex per gestire la concorrenza }
var broker = &Broker{ clients: make(map[chan []byte]bool), }
// Aggiunge un nuovo player (client) alla lista di chi deve ricevere dati func (b *Broker) Subscribe() chan []byte { b.lock.Lock() defer b.lock.Unlock()
ch := make(chan []byte)
b.clients[ch] = true
log.Println("Nuovo Player connesso. Totale:", len(b.clients))
return ch
}
// Rimuove un player quando si disconnette func (b *Broker) Unsubscribe(ch chan []byte) { b.lock.Lock() defer b.lock.Unlock()
if _, ok := b.clients[ch]; ok {
delete(b.clients, ch)
close(ch)
log.Println("Player disconnesso. Totale:", len(b.clients))
}
}
// Quando arrivano dati dall'encoder, li invia a tutti i player func (b *Broker) Broadcast(data []byte) { b.lock.Lock() defer b.lock.Unlock()
for ch := range b.clients {
// Invio non bloccante (semplificato per l'esempio)
select {
case ch <- data:
default:
// Se il client è lento, saltiamo il frame per non bloccare tutto
}
}
}
// --- 2. ENDPOINT PLAYER (GET) - Low Latency Output ---
func playHandler(w http.ResponseWriter, r *http.Request) { // 1. Controlla se il writer supporta il flushing (fondamentale per LL-DASH) flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "Streaming not supported", http.StatusInternalServerError) return }
// 2. Header essenziali per lo streaming
w.Header().Set("Content-Type", "video/mp4") // o application/dash+xml
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS per i player web
// Nota: Go imposterà automaticamente "Transfer-Encoding: chunked"
// perché non stiamo impostando "Content-Length".
// 3. Iscrizione al flusso dati
clientChan := broker.Subscribe()
defer broker.Unsubscribe(clientChan)
// 4. Ascolta quando il client chiude la connessione
notify := r.Context().Done()
// 5. Loop di Streaming (Pipe)
for {
select {
case <-notify:
// Il client ha chiuso la finestra o stoppato il video
return
case chunk := <-clientChan:
// Riceviamo un pezzetto di dati dall'encoder...
// ... Lo scriviamo nella risposta HTTP
_, err := w.Write(chunk)
if err != nil {
return
}
// ... E FORZIAMO L'INVIO IMMEDIATO (Flush)
// Questa è la magia del Low Latency. Non aspettiamo il buffer.
flusher.Flush()
}
}
}
// --- 3. ENDPOINT ENCODER (POST) - Input ---
func ingestHandler(w http.ResponseWriter, r *http.Request) { log.Println("Encoder ha iniziato a inviare dati...")
// Leggiamo il body della richiesta a pezzi (chunks)
buffer := make([]byte, 4096) // Buffer di lettura (es. 4KB)
for {
n, err := r.Body.Read(buffer)
if n > 0 {
// Copiamo i dati letti
data := make([]byte, n)
copy(data, buffer[:n])
// Li mandiamo al Broker per distribuirli ai player
broker.Broadcast(data)
}
if err == io.EOF {
// L'encoder ha finito di inviare
break
}
if err != nil {
log.Printf("Errore lettura encoder: %v", err)
break
}
}
log.Println("Encoder ha finito.")
w.WriteHeader(http.StatusOK)
}
// --- MAIN ---
func main() { http.HandleFunc("/play", playHandler) // Endpoint per il Player (GET) http.HandleFunc("/ingest", ingestHandler) // Endpoint per l'Encoder (POST)
fmt.Println("Server LL-DASH Custom avviato su :8080")
fmt.Println("1. Invia dati (Encoder): curl -X POST -T video.mp4 http://localhost:8080/ingest --no-buffer")
fmt.Println("2. Ricevi dati (Player): curl -N http://localhost:8080/play > output.mp4")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Come testarlo (Simulazione)
Hai bisogno di due terminali aperti.
- Avvia il server: code Bash
go run main.go
- Simula il Player (Terminale A): Usiamo curl con il flag -N (no buffer) per vedere i dati arrivare live. Salveremo l'output su un file per verificare che arrivi. code Bash
curl -N -v http://localhost:8080/play > ricevuto.mp4
Noterai che il comando rimane "appeso". È connesso e aspetta i chunk.
- Simula l'Encoder (Terminale B): Inviamo un file video locale (o qualsiasi file) simulando uno stream lento per vedere l'effetto "chunked". code Bash
curl -X POST -T mio_video.mp4 http://localhost:8080/ingest --limit-rate 100k
Analisi tecnica della soluzione
w.(http.Flusher): Questa è la parte più importante. Normalmente, un server web accumula 4KB o 8KB di dati prima di inviarli per efficienza di rete. In LL-DASH, se hai un chunk CMAF di soli 500 byte, vuoi che parta subito. flusher.Flush() forza Nginx/Go a svuotare il buffer TCP verso il client.
Nessun Content-Length: Nota che in playHandler non impostiamo mai la lunghezza del file. Questo dice al browser/player: "Non so quanto sarà lungo, ti mando i pezzi man mano che arrivano".
Memoria (Canali): L'uso dei canali Go (chan []byte) crea una "pipe" naturale. Non stiamo scrivendo su disco. I dati passano dalla RAM dell'Ingest alla RAM del Player in microsecondi.
Integrazione Reale (CMAF)
In un caso reale con FFmpeg per LL-DASH, useresti un comando simile a questo per inviare i dati all'endpoint /ingest: code Bash
ffmpeg -re -i input.mp4
-c:v libx264 -b:v 1000k
-f mp4 -movflags empty_moov+frag_keyframe+default_base_moof
http://localhost:8080/ingest
-movflags ...: Questi flag dicono a FFmpeg di creare un file MP4 frammentato (CMAF compliant) adatto allo streaming pezzo per pezzo.
Questa soluzione in Go è estremamente performante e costituisce la base di server moderni come gacc, OvenMediaEngine o soluzioni custom interne di Netflix/Twitch.