iOS/WebKit abre todo <video src> con una sonda "bytes=0-1" y se niega a reproducir si el 206 no trae una longitud concreta en Content-Range — "/*" (total desconocido, el fix anterior del loop de re-seek) le hacía abortar y re-bootstrapear la sesión sin parar. Vuelve a anunciar siempre un total numérico (exacto si ffmpeg terminó, el estimado mientras crece). El loop de re-seek real no era el total anunciado sino el init segment malformado, ya arreglado con +delay_moov en buildFFmpegArgs. Test nuevo: la sonda 0-1 debe llevar total concreto.
222 lines
7.1 KiB
Go
222 lines
7.1 KiB
Go
package engine
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// fakeGrowing is a GrowingSource backed by a fixed byte slice. When final is
|
|
// true it behaves like a completed remux (ReadAt returns io.EOF at the end);
|
|
// est overrides the advertised estimate (0 = use len(data)).
|
|
type fakeGrowing struct {
|
|
data []byte
|
|
final bool
|
|
est int64
|
|
}
|
|
|
|
func (f *fakeGrowing) ReadAt(p []byte, off int64) (int, error) {
|
|
if off < 0 || off >= int64(len(f.data)) {
|
|
return 0, io.EOF
|
|
}
|
|
n := copy(p, f.data[off:])
|
|
if int(off)+n >= len(f.data) {
|
|
return n, io.EOF
|
|
}
|
|
return n, nil
|
|
}
|
|
func (f *fakeGrowing) Size() int64 { return int64(len(f.data)) }
|
|
func (f *fakeGrowing) Final() bool { return f.final }
|
|
func (f *fakeGrowing) EstimatedSize() int64 {
|
|
if f.est > 0 {
|
|
return f.est
|
|
}
|
|
return int64(len(f.data))
|
|
}
|
|
func (f *fakeGrowing) FileName() string { return "movie.mp4" }
|
|
func (f *fakeGrowing) Close() error { return nil }
|
|
|
|
func TestParseByteRange(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
start, end int64
|
|
}{
|
|
{"", 0, -1},
|
|
{"bytes=0-", 0, -1},
|
|
{"bytes=100-", 100, -1},
|
|
{"bytes=5-9", 5, 9},
|
|
{"bytes=0-0", 0, 0},
|
|
{"bytes=10-19,40-49", 10, 19}, // first range only
|
|
{"bytes=-500", 0, -1}, // suffix unsupported → open from 0
|
|
{"garbage", 0, -1},
|
|
{"bytes=", 0, -1},
|
|
}
|
|
for _, c := range cases {
|
|
s, e := parseByteRange(c.in)
|
|
if s != c.start || e != c.end {
|
|
t.Errorf("parseByteRange(%q) = (%d,%d), want (%d,%d)", c.in, s, e, c.start, c.end)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestServeGrowing_FinalFullRequest(t *testing.T) {
|
|
data := []byte("0123456789abcdef")
|
|
src := &fakeGrowing{data: data, final: true}
|
|
ss := &StreamServer{}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/stream", nil)
|
|
rec := httptest.NewRecorder()
|
|
ss.serveGrowing(rec, req, src)
|
|
|
|
res := rec.Result()
|
|
if res.StatusCode != http.StatusPartialContent {
|
|
t.Fatalf("status = %d, want 206", res.StatusCode)
|
|
}
|
|
if got := res.Header.Get("Content-Range"); got != "bytes 0-15/16" {
|
|
t.Errorf("Content-Range = %q, want bytes 0-15/16", got)
|
|
}
|
|
if got := res.Header.Get("Accept-Ranges"); got != "bytes" {
|
|
t.Errorf("Accept-Ranges = %q, want bytes", got)
|
|
}
|
|
if got := res.Header.Get("Content-Type"); got != "video/mp4" {
|
|
t.Errorf("Content-Type = %q, want video/mp4", got)
|
|
}
|
|
// Final + open-ended → exact Content-Length.
|
|
if got := res.Header.Get("Content-Length"); got != "16" {
|
|
t.Errorf("Content-Length = %q, want 16", got)
|
|
}
|
|
if body := rec.Body.String(); body != string(data) {
|
|
t.Errorf("body = %q, want %q", body, string(data))
|
|
}
|
|
}
|
|
|
|
func TestServeGrowing_OffsetRange(t *testing.T) {
|
|
data := []byte("0123456789abcdef")
|
|
src := &fakeGrowing{data: data, final: true}
|
|
ss := &StreamServer{}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/stream", nil)
|
|
req.Header.Set("Range", "bytes=10-")
|
|
rec := httptest.NewRecorder()
|
|
ss.serveGrowing(rec, req, src)
|
|
|
|
res := rec.Result()
|
|
if res.StatusCode != http.StatusPartialContent {
|
|
t.Fatalf("status = %d, want 206", res.StatusCode)
|
|
}
|
|
if got := res.Header.Get("Content-Range"); got != "bytes 10-15/16" {
|
|
t.Errorf("Content-Range = %q, want bytes 10-15/16", got)
|
|
}
|
|
if body := rec.Body.String(); body != "abcdef" {
|
|
t.Errorf("body = %q, want abcdef", body)
|
|
}
|
|
}
|
|
|
|
func TestServeGrowing_BoundedRange(t *testing.T) {
|
|
data := []byte("0123456789abcdef")
|
|
src := &fakeGrowing{data: data, final: true}
|
|
ss := &StreamServer{}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/stream", nil)
|
|
req.Header.Set("Range", "bytes=5-9")
|
|
rec := httptest.NewRecorder()
|
|
ss.serveGrowing(rec, req, src)
|
|
|
|
res := rec.Result()
|
|
if res.StatusCode != http.StatusPartialContent {
|
|
t.Fatalf("status = %d, want 206", res.StatusCode)
|
|
}
|
|
if got := res.Header.Get("Content-Range"); got != "bytes 5-9/16" {
|
|
t.Errorf("Content-Range = %q, want bytes 5-9/16", got)
|
|
}
|
|
if body := rec.Body.String(); body != "56789" {
|
|
t.Errorf("body = %q, want 56789 (exactly the requested 5 bytes)", body)
|
|
}
|
|
}
|
|
|
|
func TestServeGrowing_EstimateUsedWhileNotFinal(t *testing.T) {
|
|
// Not final: only 8 bytes produced, estimate says 100. We advertise the
|
|
// estimate as the total — iOS/WebKit refuses to play a <video src> whose
|
|
// "bytes=0-1" probe comes back without a concrete instance length, so "/*"
|
|
// (unknown total) is not an option. The estimate need not be byte-exact;
|
|
// the real re-seek loop was the malformed init segment (fixed by
|
|
// +delay_moov), not the advertised total. Body is what exists so far.
|
|
src := &fakeGrowing{data: []byte("01234567"), final: false, est: 100}
|
|
ss := &StreamServer{}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/stream", nil)
|
|
rec := httptest.NewRecorder()
|
|
ss.serveGrowing(rec, req, src)
|
|
|
|
res := rec.Result()
|
|
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)
|
|
}
|
|
// Not final → no exact Content-Length (chunked) so we never promise bytes
|
|
// a still-running remux might not produce.
|
|
if got := res.Header.Get("Content-Length"); got != "" {
|
|
t.Errorf("Content-Length = %q, want empty (chunked) while not final", got)
|
|
}
|
|
if body := rec.Body.String(); body != "01234567" {
|
|
t.Errorf("body = %q, want 01234567 (bytes produced so far)", body)
|
|
}
|
|
}
|
|
|
|
func TestServeGrowing_HeadProbe(t *testing.T) {
|
|
// HEAD: advertise the total (estimate while growing) so iOS gets the size
|
|
// it needs from its probe.
|
|
src := &fakeGrowing{data: make([]byte, 0), final: false, est: 4242}
|
|
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", got)
|
|
}
|
|
if rec.Body.Len() != 0 {
|
|
t.Errorf("HEAD body = %d bytes, want 0", rec.Body.Len())
|
|
}
|
|
}
|
|
|
|
func TestServeGrowing_ProbeRangeCarriesTotal(t *testing.T) {
|
|
// The iOS "bytes=0-1" probe MUST come back with a concrete instance length
|
|
// (bytes 0-1/<total>), or WebKit bails and re-bootstraps the session.
|
|
src := &fakeGrowing{data: []byte("0123456789"), final: false, est: 6685677633}
|
|
ss := &StreamServer{}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/stream", nil)
|
|
req.Header.Set("Range", "bytes=0-1")
|
|
rec := httptest.NewRecorder()
|
|
ss.serveGrowing(rec, req, src)
|
|
|
|
if got := rec.Result().Header.Get("Content-Range"); got != "bytes 0-1/6685677633" {
|
|
t.Errorf("Content-Range = %q, want bytes 0-1/6685677633 (concrete total for iOS)", got)
|
|
}
|
|
if body := rec.Body.String(); body != "01" {
|
|
t.Errorf("body = %q, want 01 (the 2 probed bytes)", body)
|
|
}
|
|
}
|
|
|
|
func TestServeGrowing_RangeBeyondTotal(t *testing.T) {
|
|
src := &fakeGrowing{data: []byte("0123456789"), final: true}
|
|
ss := &StreamServer{}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/stream", nil)
|
|
req.Header.Set("Range", "bytes=999-")
|
|
rec := httptest.NewRecorder()
|
|
ss.serveGrowing(rec, req, src)
|
|
|
|
if rec.Result().StatusCode != http.StatusRequestedRangeNotSatisfiable {
|
|
t.Errorf("status = %d, want 416", rec.Result().StatusCode)
|
|
}
|
|
}
|