feat(v2): add reconnecting FFmpeg stream source

This commit is contained in:
ila
2026-07-22 21:15:25 +08:00
parent 70d9c5c73e
commit 3edda4fb62
5 changed files with 388 additions and 9 deletions
+92
View File
@@ -0,0 +1,92 @@
package source
import (
"context"
"errors"
"strings"
"sync/atomic"
"testing"
"time"
)
func TestPublishLatestDropsStaleFrame(t *testing.T) {
frames := make(chan Frame, 1)
publishLatest(frames, Frame{Sequence: 1})
publishLatest(frames, Frame{Sequence: 2})
if got := <-frames; got.Sequence != 2 {
t.Fatalf("sequence = %d, want 2", got.Sequence)
}
}
func TestRetryDelayIsBounded(t *testing.T) {
if got := retryDelay(0); got != time.Second {
t.Fatalf("first retry delay = %s", got)
}
if got := retryDelay(3); got != 8*time.Second {
t.Fatalf("fourth retry delay = %s", got)
}
if got := retryDelay(99); got != 8*time.Second {
t.Fatalf("bounded retry delay = %s", got)
}
}
func TestStreamRetriesAndPublishesLatestCompletedFrame(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var decodeCalls int32
decodedLatest := make(chan struct{})
stream := Start(ctx, Config{SourceURL: "rtsp://operator:secret@example/101"}, Dependencies{
Probe: func(context.Context, Config) (Metadata, error) {
return Metadata{Width: 2, Height: 1, FPS: 30}, nil
},
Decode: func(ctx context.Context, _ Config, _ Metadata, publish func([]byte)) error {
call := atomic.AddInt32(&decodeCalls, 1)
if call == 1 {
publish([]byte{1, 1, 1, 1, 1, 1})
return errors.New("temporary decoder failure")
}
publish([]byte{2, 2, 2, 2, 2, 2})
close(decodedLatest)
cancel()
return ctx.Err()
},
Wait: func(ctx context.Context, _ time.Duration) error { return ctx.Err() },
})
select {
case <-decodedLatest:
case <-time.After(time.Second):
t.Fatal("decoder did not publish the second frame")
}
select {
case got := <-stream.Frames():
if got.Sequence != 2 || got.BGR[0] != 2 {
t.Fatalf("frame = %#v", got)
}
case <-time.After(time.Second):
t.Fatal("did not receive latest decoded frame")
}
<-stream.Done()
}
func TestStreamNeverPublishesDecoderErrorDetails(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
stream := Start(ctx, Config{SourceURL: "rtsp://operator:secret@example/101"}, Dependencies{
Probe: func(context.Context, Config) (Metadata, error) {
return Metadata{}, errors.New("ffprobe rtsp://operator:secret@example/101 failed")
},
Decode: func(context.Context, Config, Metadata, func([]byte)) error { return nil },
Wait: func(ctx context.Context, _ time.Duration) error {
cancel()
return ctx.Err()
},
})
for update := range stream.Statuses() {
if strings.Contains(update.Message, "secret") || strings.Contains(update.Message, "rtsp://") {
t.Fatalf("status leaked source details: %#v", update)
}
}
}