fix(stream): no anunciar un total falso mientras el remux crece (loop de re-seek)

serveGrowing anunciaba en Content-Range total = EstimatedSize() = el tamaño
del MKV fuente mientras ffmpeg aún corría. Pero el fMP4 resultante no mide
eso (el audio re-encodea a AAC y la fragmentación cambian el byte count), así
que el <video> nativo mapeaba su timeline sobre una longitud falsa, pedía
offsets que no cuadraban, re-seekeaba y reabría la conexión cientos de veces
por segundo (el loop de reproducción remux).

Mientras crece (!Final) la longitud real es DESCONOCIDA: ahora se sirve
Content-Range "bytes start-end/*" (RFC 7233 §4.2) sin Content-Length, y el
cliente lee secuencial en vez de re-seekear. Cuando ffmpeg termina, el tamaño
real se conoce y se anuncia como antes. El 416 y el Content-Length del HEAD
solo cuando el total es real (final).
This commit is contained in:
Deivid Soto 2026-06-10 19:42:37 +02:00
parent 9ab0763f8a
commit 5f2d1cdc70
2 changed files with 66 additions and 20 deletions

View file

@ -135,9 +135,12 @@ func TestServeGrowing_BoundedRange(t *testing.T) {
}
}
func TestServeGrowing_EstimateUsedWhileNotFinal(t *testing.T) {
// Not final: only 8 bytes produced, but estimate says 100. The advertised
// total is the estimate (scrubber timeline); body is what exists so far.
func TestServeGrowing_UnknownTotalWhileNotFinal(t *testing.T) {
// Not final: only 8 bytes produced, estimate says 100. The instance length
// is genuinely unknown while the remux grows, so we advertise "/*" (RFC 7233
// §4.2) instead of a total the native player would map its timeline onto and
// re-seek against (the playback loop). The estimate is only an upper-bound
// hint for `end`; body is what exists so far.
src := &fakeGrowing{data: []byte("01234567"), final: false, est: 100}
ss := &StreamServer{}
@ -149,8 +152,8 @@ func TestServeGrowing_EstimateUsedWhileNotFinal(t *testing.T) {
if res.StatusCode != http.StatusPartialContent {
t.Fatalf("status = %d, want 206", res.StatusCode)
}
if got := res.Header.Get("Content-Range"); got != "bytes 0-99/100" {
t.Errorf("Content-Range = %q, want bytes 0-99/100 (estimate)", got)
if got := res.Header.Get("Content-Range"); got != "bytes 0-99/*" {
t.Errorf("Content-Range = %q, want bytes 0-99/* (unknown total)", got)
}
// Not final → no exact Content-Length (chunked) so we never promise bytes
// a still-running remux might not produce.
@ -163,6 +166,8 @@ func TestServeGrowing_EstimateUsedWhileNotFinal(t *testing.T) {
}
func TestServeGrowing_HeadProbe(t *testing.T) {
// HEAD while growing: total is unknown, so no Content-Length is promised
// (advertising the estimate is the bug this fix removes).
src := &fakeGrowing{data: make([]byte, 0), final: false, est: 4242}
ss := &StreamServer{}
@ -174,14 +179,32 @@ func TestServeGrowing_HeadProbe(t *testing.T) {
if res.StatusCode != http.StatusOK {
t.Fatalf("HEAD status = %d, want 200", res.StatusCode)
}
if got := res.Header.Get("Content-Length"); got != "4242" {
t.Errorf("HEAD Content-Length = %q, want 4242", got)
if got := res.Header.Get("Content-Length"); got != "" {
t.Errorf("HEAD Content-Length = %q, want empty (unknown total while growing)", got)
}
if rec.Body.Len() != 0 {
t.Errorf("HEAD body = %d bytes, want 0", rec.Body.Len())
}
}
func TestServeGrowing_HeadProbeFinal(t *testing.T) {
// HEAD once final: the true total IS known, so advertise it.
src := &fakeGrowing{data: make([]byte, 4242), final: true}
ss := &StreamServer{}
req := httptest.NewRequest(http.MethodHead, "/stream", nil)
rec := httptest.NewRecorder()
ss.serveGrowing(rec, req, src)
res := rec.Result()
if res.StatusCode != http.StatusOK {
t.Fatalf("HEAD status = %d, want 200", res.StatusCode)
}
if got := res.Header.Get("Content-Length"); got != "4242" {
t.Errorf("HEAD Content-Length = %q, want 4242 (final size known)", got)
}
}
func TestServeGrowing_RangeBeyondTotal(t *testing.T) {
src := &fakeGrowing{data: []byte("0123456789"), final: true}
ss := &StreamServer{}