diff --git a/internal/engine/stream_growing_test.go b/internal/engine/stream_growing_test.go index cb16c1f..39cdae4 100644 --- a/internal/engine/stream_growing_test.go +++ b/internal/engine/stream_growing_test.go @@ -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{} diff --git a/internal/engine/stream_server.go b/internal/engine/stream_server.go index 2d972ad..e536416 100644 --- a/internal/engine/stream_server.go +++ b/internal/engine/stream_server.go @@ -1467,25 +1467,38 @@ func (ss *StreamServer) serveGrowing(w http.ResponseWriter, r *http.Request, src w.Header().Set("Content-Type", "video/mp4") w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", src.FileName())) - // Total to advertise: exact when ffmpeg has exited, else the estimate. - total := src.EstimatedSize() - if src.Final() { - total = src.Size() + // The instance length is KNOWN only once ffmpeg has exited. While the remux + // is still growing, the final size is genuinely unknown — the source MKV + // size is NOT it (the audio re-encode to AAC + fMP4 fragmentation change the + // byte count). Advertising that wrong total made the native