Replaces the broken anacrolix WebTorrent path with a custom WebRTC peer that the browser drives directly. Architecture matches plan/clever- weaving-dove.md (Fase 2 + 3 + 6 of the streaming pivot). - engine/wire: shared 12-byte binary frame format (Hello / RangeReq / RangeData / RangeEnd / Cancel / Ping / Pong / SeekHint). Roundtrip + oversized-frame rejection tests. - agent/signal_client: SSE consumer + POST sender for SDP/ICE relay through /api/internal/stream/signal/<id>; auto-reconnects. - engine/webrtc_stream: pion v4 PeerConnection + DataChannel pump. Reads file via os.ReadAt, chunks RangeData at 16 KiB, honours app- level backpressure with SetBufferedAmountLowThreshold. - cmd/daemon dispatcher learns mode webrtc_stream + new webrtcSessionRegistry tracks per-session cancel funcs for clean shutdown. - engine/probe + hwaccel + transcoder: foundation for Fase 2.5 (codec detection, NVENC/QSV/VAAPI/VideoToolbox autodetection, ffmpeg pipe wrapper to fragmented MP4). Integration into webrtc_stream still pending. - pion/webrtc/v4 promoted from indirect to direct dep. End-to-end against unarr-dev confirms a 122 MB 1080p H.264 / AAC MP4 plays in Chrome with the new pipeline.
193 lines
5.3 KiB
Go
193 lines
5.3 KiB
Go
package wire
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
)
|
|
|
|
func TestHeaderRoundtrip(t *testing.T) {
|
|
cases := []Header{
|
|
{Type: FrameHello, Flags: FlagSeekable, StreamID: 0, Length: 32},
|
|
{Type: FrameRangeReq, Flags: 0, StreamID: 7, Length: 16},
|
|
{Type: FrameRangeData, Flags: FlagLastChunk, StreamID: 4242, Length: 16380},
|
|
{Type: FrameRangeEnd, Flags: 0, StreamID: 1, Length: 4},
|
|
{Type: FrameCancel, Flags: 0, StreamID: 9, Length: 0},
|
|
{Type: FramePing, Flags: 0, StreamID: 0, Length: 0},
|
|
}
|
|
for _, want := range cases {
|
|
buf := make([]byte, HeaderSize)
|
|
EncodeHeader(buf, want)
|
|
got, err := DecodeHeader(buf)
|
|
if err != nil {
|
|
t.Fatalf("decode: %v (want %+v)", err, want)
|
|
}
|
|
if got != want {
|
|
t.Errorf("roundtrip mismatch: got %+v want %+v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecodeHeaderShort(t *testing.T) {
|
|
if _, err := DecodeHeader([]byte{0, 0, 0}); err == nil {
|
|
t.Fatal("expected error on short header")
|
|
}
|
|
}
|
|
|
|
func TestDecodeHeaderRejectsHugeLength(t *testing.T) {
|
|
// Synthesize a header with payload length above MaxFrameSize.
|
|
buf := make([]byte, HeaderSize)
|
|
buf[0] = byte(FrameRangeData)
|
|
buf[8] = 0xff
|
|
buf[9] = 0xff
|
|
buf[10] = 0xff
|
|
buf[11] = 0xff
|
|
if _, err := DecodeHeader(buf); err == nil {
|
|
t.Fatal("expected error on oversized payload length")
|
|
}
|
|
}
|
|
|
|
func TestEncodeFramePanicsOnLengthMismatch(t *testing.T) {
|
|
defer func() {
|
|
if r := recover(); r == nil {
|
|
t.Fatal("expected panic on header length / payload mismatch")
|
|
}
|
|
}()
|
|
EncodeFrame(Header{Type: FrameRangeData, Length: 5}, []byte{1, 2, 3})
|
|
}
|
|
|
|
func TestReadFrameRoundtrip(t *testing.T) {
|
|
want := Header{Type: FrameRangeData, Flags: FlagLastChunk, StreamID: 99, Length: 5}
|
|
payload := []byte{0xde, 0xad, 0xbe, 0xef, 0x42}
|
|
frame := EncodeFrame(want, payload)
|
|
|
|
r := bytes.NewReader(frame)
|
|
got, gotPayload, err := ReadFrame(r)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if got != want {
|
|
t.Errorf("header mismatch: %+v want %+v", got, want)
|
|
}
|
|
if !bytes.Equal(gotPayload, payload) {
|
|
t.Errorf("payload mismatch: %x want %x", gotPayload, payload)
|
|
}
|
|
}
|
|
|
|
func TestReadFrameZeroPayload(t *testing.T) {
|
|
want := Header{Type: FrameCancel, StreamID: 7}
|
|
frame := EncodeFrame(want, nil)
|
|
got, payload, err := ReadFrame(bytes.NewReader(frame))
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if got != want {
|
|
t.Errorf("header mismatch: %+v want %+v", got, want)
|
|
}
|
|
if len(payload) != 0 {
|
|
t.Errorf("expected empty payload, got %d bytes", len(payload))
|
|
}
|
|
}
|
|
|
|
func TestHelloRoundtrip(t *testing.T) {
|
|
want := HelloPayload{
|
|
FileSize: 1<<32 + 12345,
|
|
Transcoding: false,
|
|
Seekable: true,
|
|
FileName: "Tangled.Ever.After.2025.1080p.WEB-DL.h264.mp4",
|
|
}
|
|
flags := HelloFlags(want.Transcoding, want.Seekable)
|
|
payload := EncodeHello(want)
|
|
got, err := DecodeHello(payload, flags)
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if got != want {
|
|
t.Errorf("hello mismatch: %+v want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestHelloRejectsTruncatedPayload(t *testing.T) {
|
|
if _, err := DecodeHello([]byte{1, 2, 3}, 0); err == nil {
|
|
t.Fatal("expected error on truncated hello")
|
|
}
|
|
}
|
|
|
|
func TestHelloRejectsNameLenOverrun(t *testing.T) {
|
|
// file_size + name_len=999 but no name bytes → should fail.
|
|
buf := make([]byte, 12)
|
|
buf[8], buf[9], buf[10], buf[11] = 0, 0, 0x03, 0xe7 // 999
|
|
if _, err := DecodeHello(buf, 0); err == nil {
|
|
t.Fatal("expected error on name_len overrun")
|
|
}
|
|
}
|
|
|
|
func TestRangeReqRoundtrip(t *testing.T) {
|
|
want := RangeReqPayload{Offset: 1 << 30, Length: 1 << 20}
|
|
got, err := DecodeRangeReq(EncodeRangeReq(want))
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if got != want {
|
|
t.Errorf("range_req mismatch: %+v want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRangeReqRejectsWrongLength(t *testing.T) {
|
|
if _, err := DecodeRangeReq(make([]byte, 15)); err == nil {
|
|
t.Fatal("expected error on 15-byte payload")
|
|
}
|
|
if _, err := DecodeRangeReq(make([]byte, 17)); err == nil {
|
|
t.Fatal("expected error on 17-byte payload")
|
|
}
|
|
}
|
|
|
|
func TestRangeEndRoundtrip(t *testing.T) {
|
|
want := RangeEndPayload{Status: 42}
|
|
got, err := DecodeRangeEnd(EncodeRangeEnd(want))
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if got != want {
|
|
t.Errorf("range_end mismatch: %+v want %+v", got, want)
|
|
}
|
|
if _, err := DecodeRangeEnd(make([]byte, 3)); err == nil {
|
|
t.Fatal("expected error on short range_end payload")
|
|
}
|
|
}
|
|
|
|
func TestSeekHintRoundtrip(t *testing.T) {
|
|
want := SeekHintPayload{TimestampMs: 123_456}
|
|
got, err := DecodeSeekHint(EncodeSeekHint(want))
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if got != want {
|
|
t.Errorf("seek_hint mismatch: %+v want %+v", got, want)
|
|
}
|
|
if _, err := DecodeSeekHint(make([]byte, 7)); err == nil {
|
|
t.Fatal("expected error on short seek_hint payload")
|
|
}
|
|
}
|
|
|
|
func TestHelloFlagsHelper(t *testing.T) {
|
|
if HelloFlags(false, false) != 0 {
|
|
t.Error("expected 0 for both false")
|
|
}
|
|
if HelloFlags(true, false) != FlagTranscoding {
|
|
t.Error("expected FlagTranscoding only")
|
|
}
|
|
if HelloFlags(false, true) != FlagSeekable {
|
|
t.Error("expected FlagSeekable only")
|
|
}
|
|
if HelloFlags(true, true) != (FlagTranscoding | FlagSeekable) {
|
|
t.Error("expected both flags")
|
|
}
|
|
}
|
|
|
|
// Sanity check that MaxChunkPayload + HeaderSize fits inside MaxFrameSize so
|
|
// callers can rely on the chunk cap without their own bookkeeping.
|
|
func TestMaxChunkFitsInMaxFrame(t *testing.T) {
|
|
if MaxChunkPayload+HeaderSize > MaxFrameSize {
|
|
t.Fatalf("chunk %d + hdr %d > max frame %d", MaxChunkPayload, HeaderSize, MaxFrameSize)
|
|
}
|
|
}
|