Skip to content

Instantly share code, notes, and snippets.

@fabriziosalmi
Created November 21, 2025 16:30
Show Gist options
  • Select an option

  • Save fabriziosalmi/6bf2acea09181c0aaaf1c083acce0900 to your computer and use it in GitHub Desktop.

Select an option

Save fabriziosalmi/6bf2acea09181c0aaaf1c083acce0900 to your computer and use it in GitHub Desktop.

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.

  1. Avvia il server: code Bash

go run main.go

  1. 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

Il player si connette e aspetta i dati...

curl -N -v http://localhost:8080/play > ricevuto.mp4

Noterai che il comando rimane "appeso". È connesso e aspetta i chunk.

  1. Simula l'Encoder (Terminale B): Inviamo un file video locale (o qualsiasi file) simulando uno stream lento per vedere l'effetto "chunked". code Bash

Inviamo un file, limitando la velocità per simulare encoding live

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment