Refactoring #3
@@ -14,7 +14,7 @@ Size=200,200
|
||||
Collapsed=0
|
||||
|
||||
[Window][Connection]
|
||||
Pos=911,549
|
||||
Pos=250,275
|
||||
Size=640,352
|
||||
Collapsed=0
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package mxladapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"mxl-player/internal/playback"
|
||||
"mxl-player/internal/source"
|
||||
|
||||
mxl "github.com/qvest-digital/go-mxl/mxl"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultSyncReadTimeout = 200 * time.Millisecond
|
||||
DefaultSyncBatchDuration = 10 * time.Millisecond
|
||||
)
|
||||
|
||||
var ErrNativeSyncDifferentDomains = errors.New(
|
||||
"native MXL synchronization requires matching domains",
|
||||
)
|
||||
|
||||
type SyncFactory struct {
|
||||
ReadTimeout time.Duration
|
||||
BatchDuration time.Duration
|
||||
open func(string, string, string) (localSyncSource, error)
|
||||
}
|
||||
|
||||
type localSyncSource interface {
|
||||
NextSync(
|
||||
context.Context,
|
||||
uint64,
|
||||
time.Duration,
|
||||
) (source.Frame, source.AudioFrame, error)
|
||||
|
||||
AudioRate() mxl.Rational
|
||||
Close() error
|
||||
}
|
||||
|
||||
type syncReader struct {
|
||||
source localSyncSource
|
||||
readTimeout time.Duration
|
||||
audioBatch uint64
|
||||
rateNumerator int64
|
||||
rateDenominator int64
|
||||
}
|
||||
|
||||
var _ playback.SyncReaderFactory = SyncFactory{}
|
||||
var _ playback.SyncReader = (*syncReader)(nil)
|
||||
|
||||
func (f SyncFactory) OpenSync(
|
||||
ctx context.Context,
|
||||
videoConfig playback.FeedConfig,
|
||||
audioConfig playback.FeedConfig,
|
||||
) (playback.SyncReader, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := videoConfig.Validate(); err != nil {
|
||||
return nil, &source.SourceError{
|
||||
Op: "validate sync video feed",
|
||||
Kind: source.ErrorKindInvalidConfig,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if err := audioConfig.Validate(); err != nil {
|
||||
return nil, &source.SourceError{
|
||||
Op: "validate sync audio feed",
|
||||
Kind: source.ErrorKindInvalidConfig,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if !videoConfig.IsConfigured() {
|
||||
return nil, &source.SourceError{
|
||||
Op: "validate sync video feed",
|
||||
Kind: source.ErrorKindInvalidConfig,
|
||||
Err: errors.New("sync video feed is not configured"),
|
||||
}
|
||||
}
|
||||
if !audioConfig.IsConfigured() {
|
||||
return nil, &source.SourceError{
|
||||
Op: "validate sync audio feed",
|
||||
Kind: source.ErrorKindInvalidConfig,
|
||||
Err: errors.New("sync audio feed is not configured"),
|
||||
}
|
||||
}
|
||||
|
||||
if videoConfig.Domain != audioConfig.Domain {
|
||||
return nil, &source.SourceError{
|
||||
Op: "validate native MXL sync group",
|
||||
Kind: source.ErrorKindInvalidConfig,
|
||||
Err: ErrNativeSyncDifferentDomains,
|
||||
}
|
||||
}
|
||||
|
||||
open := f.open
|
||||
if open == nil {
|
||||
open = func(domain, videoUUID, audioUUID string) (localSyncSource, error) {
|
||||
return source.OpenSameDomainSync(domain, videoUUID, audioUUID)
|
||||
}
|
||||
}
|
||||
src, err := open(
|
||||
videoConfig.Domain,
|
||||
videoConfig.UUID,
|
||||
audioConfig.UUID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open native MXL sync group: %w", err)
|
||||
}
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
_ = src.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
readTimeout := f.ReadTimeout
|
||||
if readTimeout <= 0 {
|
||||
readTimeout = DefaultSyncReadTimeout
|
||||
}
|
||||
batchDuration := f.BatchDuration
|
||||
if batchDuration <= 0 {
|
||||
batchDuration = DefaultSyncBatchDuration
|
||||
}
|
||||
audioRate := src.AudioRate()
|
||||
batch, err := audioBatchSize(audioRate.Num, audioRate.Den, batchDuration)
|
||||
if err != nil {
|
||||
_ = src.Close()
|
||||
return nil, &source.SourceError{
|
||||
Op: "calculate sync audio batch",
|
||||
Kind: source.ErrorKindInvalidConfig,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return &syncReader{
|
||||
source: src,
|
||||
readTimeout: readTimeout,
|
||||
audioBatch: batch,
|
||||
rateNumerator: audioRate.Num,
|
||||
rateDenominator: audioRate.Den,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *syncReader) ReadSync(
|
||||
ctx context.Context,
|
||||
) (playback.SyncFrame, error) {
|
||||
video, audio, err := r.source.NextSync(ctx, r.audioBatch, r.readTimeout)
|
||||
if err != nil {
|
||||
return playback.SyncFrame{}, err
|
||||
}
|
||||
return playback.SyncFrame{
|
||||
Video: playback.VideoFrame{
|
||||
Index: video.Index,
|
||||
Width: video.Width,
|
||||
Height: video.Height,
|
||||
Stride: video.Stride,
|
||||
Size: video.Size,
|
||||
Invalid: video.Invalid,
|
||||
Payload: video.Payload,
|
||||
},
|
||||
Audio: playback.AudioFrame{
|
||||
Index: audio.Index,
|
||||
SampleCount: audio.SampleCount,
|
||||
Channels: audio.Channels,
|
||||
SampleRateNumerator: r.rateNumerator,
|
||||
SampleRateDenominator: r.rateDenominator,
|
||||
Samples: audio.Samples,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *syncReader) Close() error {
|
||||
return r.source.Close()
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package mxladapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"mxl-player/internal/playback"
|
||||
"mxl-player/internal/source"
|
||||
|
||||
mxl "github.com/qvest-digital/go-mxl/mxl"
|
||||
)
|
||||
|
||||
type fakeLocalSyncSource struct {
|
||||
video source.Frame
|
||||
audio source.AudioFrame
|
||||
readErr error
|
||||
rate mxl.Rational
|
||||
batch uint64
|
||||
timeout time.Duration
|
||||
closed bool
|
||||
closeError error
|
||||
}
|
||||
|
||||
func (s *fakeLocalSyncSource) NextSync(
|
||||
_ context.Context,
|
||||
batch uint64,
|
||||
timeout time.Duration,
|
||||
) (source.Frame, source.AudioFrame, error) {
|
||||
s.batch = batch
|
||||
s.timeout = timeout
|
||||
return s.video, s.audio, s.readErr
|
||||
}
|
||||
|
||||
func (s *fakeLocalSyncSource) AudioRate() mxl.Rational { return s.rate }
|
||||
func (s *fakeLocalSyncSource) Close() error {
|
||||
s.closed = true
|
||||
return s.closeError
|
||||
}
|
||||
|
||||
func syncFeedConfigs() (playback.FeedConfig, playback.FeedConfig) {
|
||||
return playback.FeedConfig{Domain: "/mxl", UUID: "video", Active: true},
|
||||
playback.FeedConfig{Domain: "/mxl", UUID: "audio", Active: true}
|
||||
}
|
||||
|
||||
func TestSyncFactoryRejectsDifferentDomains(t *testing.T) {
|
||||
video, audio := syncFeedConfigs()
|
||||
audio.Domain = "/other"
|
||||
reader, err := (SyncFactory{}).OpenSync(context.Background(), video, audio)
|
||||
if reader != nil {
|
||||
t.Fatal("OpenSync() reader is not nil")
|
||||
}
|
||||
if !errors.Is(err, ErrNativeSyncDifferentDomains) {
|
||||
t.Fatalf("OpenSync() error = %v, want %v", err, ErrNativeSyncDifferentDomains)
|
||||
}
|
||||
if source.KindOf(err) != source.ErrorKindInvalidConfig {
|
||||
t.Fatalf("error kind = %v, want invalid config", source.KindOf(err))
|
||||
}
|
||||
if ShouldRetry(err) {
|
||||
t.Fatal("ShouldRetry() = true for different domains")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncFactoryUsesDefaultsAndForwardsFeeds(t *testing.T) {
|
||||
fake := &fakeLocalSyncSource{rate: mxl.Rational{Num: 48_000, Den: 1}}
|
||||
var domain, videoUUID, audioUUID string
|
||||
factory := SyncFactory{open: func(d, v, a string) (localSyncSource, error) {
|
||||
domain, videoUUID, audioUUID = d, v, a
|
||||
return fake, nil
|
||||
}}
|
||||
video, audio := syncFeedConfigs()
|
||||
|
||||
reader, err := factory.OpenSync(context.Background(), video, audio)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenSync() error = %v", err)
|
||||
}
|
||||
if domain != video.Domain || videoUUID != video.UUID || audioUUID != audio.UUID {
|
||||
t.Fatalf("open args = %q %q %q", domain, videoUUID, audioUUID)
|
||||
}
|
||||
got := reader.(*syncReader)
|
||||
if got.readTimeout != DefaultSyncReadTimeout || got.audioBatch != 480 {
|
||||
t.Fatalf("reader timeout=%s batch=%d, want %s and 480", got.readTimeout, got.audioBatch, DefaultSyncReadTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncFactoryClosesSourceForInvalidAudioRate(t *testing.T) {
|
||||
fake := &fakeLocalSyncSource{rate: mxl.Rational{}}
|
||||
factory := SyncFactory{open: func(string, string, string) (localSyncSource, error) {
|
||||
return fake, nil
|
||||
}}
|
||||
video, audio := syncFeedConfigs()
|
||||
reader, err := factory.OpenSync(context.Background(), video, audio)
|
||||
if reader != nil {
|
||||
t.Fatal("OpenSync() reader is not nil")
|
||||
}
|
||||
if !errors.Is(err, ErrInvalidAudioBatch) || !fake.closed {
|
||||
t.Fatalf("OpenSync() error=%v closed=%t", err, fake.closed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncReaderConvertsPairWithoutCopying(t *testing.T) {
|
||||
videoPayload := []byte{1, 2, 3}
|
||||
audioSamples := [][]byte{{4, 5, 6, 7}}
|
||||
fake := &fakeLocalSyncSource{
|
||||
video: source.Frame{Index: 10, Width: 20, Height: 30, Payload: videoPayload},
|
||||
audio: source.AudioFrame{Index: 40, SampleCount: 1, Channels: 1, Samples: audioSamples},
|
||||
rate: mxl.Rational{Num: 48_000, Den: 1},
|
||||
}
|
||||
reader := &syncReader{
|
||||
source: fake, readTimeout: 7 * time.Millisecond, audioBatch: 12,
|
||||
rateNumerator: 48_000, rateDenominator: 1,
|
||||
}
|
||||
|
||||
frame, err := reader.ReadSync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fake.batch != 12 || fake.timeout != 7*time.Millisecond {
|
||||
t.Fatalf("NextSync() batch=%d timeout=%s", fake.batch, fake.timeout)
|
||||
}
|
||||
if frame.Video.Index != 10 || frame.Audio.Index != 40 || frame.Audio.SampleRateNumerator != 48_000 {
|
||||
t.Fatalf("frame = %+v", frame)
|
||||
}
|
||||
if &frame.Video.Payload[0] != &videoPayload[0] || &frame.Audio.Samples[0][0] != &audioSamples[0][0] {
|
||||
t.Fatal("sync payload was copied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncFactoryReturnsPreCanceledContextWithoutOpening(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
opened := false
|
||||
factory := SyncFactory{open: func(string, string, string) (localSyncSource, error) {
|
||||
opened = true
|
||||
return nil, nil
|
||||
}}
|
||||
video, audio := syncFeedConfigs()
|
||||
reader, err := factory.OpenSync(ctx, video, audio)
|
||||
if reader != nil || !errors.Is(err, context.Canceled) || opened {
|
||||
t.Fatalf("reader=%v error=%v opened=%t", reader, err, opened)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user