diff --git a/cmd/mxl-player/main.go b/cmd/mxl-player/main.go index d6b7fad..bbf8e46 100644 --- a/cmd/mxl-player/main.go +++ b/cmd/mxl-player/main.go @@ -464,16 +464,18 @@ func main() { displayedVideoWidth uint32 = placeholderWidth displayedVideoHeight uint32 = placeholderHeight displayedVideoStride uint32 = placeholderStride - hasDisplayedVideo bool = false - fps float64 - lastIndex uint64 - dropped uint64 - frameCount uint64 - lastReport time.Time - lastFrame time.Time + fps float64 + lastIndex uint64 + dropped uint64 + droppedTotal uint64 + frameCount uint64 + lastReport time.Time + lastFrame time.Time + renderLoopDT time.Duration ) lastFrame = time.Now() + lastReport = lastFrame // ImGui var ( @@ -555,7 +557,6 @@ func main() { displayedVideoWidth = pendingFrame.Frame.Width displayedVideoHeight = pendingFrame.Frame.Height displayedVideoStride = pendingFrame.Frame.Stride - hasDisplayedVideo = true hasFrame = true } else if frameErr != nil && !errors.Is(frameErr, context.DeadlineExceeded) && @@ -568,6 +569,7 @@ func main() { if lastIndex != 0 && shownIndex > lastIndex { if g := shownIndex - lastIndex - 1; g > 0 { dropped += g + droppedTotal += g } } lastIndex = shownIndex @@ -585,25 +587,119 @@ func main() { // end of stats if r != nil { gui.BeginFrame(time.Since(lastFrame), int32(r.Extent().Width), int32(r.Extent().Height)) + snapshot, hasSnapshot := player.Controller.Snapshot() if showStats { cimgui.SetNextWindowPos(cimgui.Vec2{X: 10, Y: 10}) - cimgui.SetNextWindowSize(cimgui.Vec2{X: 200, Y: 200}) + cimgui.SetNextWindowSize(cimgui.Vec2{X: 460, Y: 510}) cimgui.BeginV("Stats", &showStats, cimgui.WindowFlagsNoTitleBar|cimgui.WindowFlagsNoResize|cimgui.WindowFlagsNoScrollbar) - cimgui.Text(fmt.Sprintf("FPS: %.1f", fps)) - cimgui.Text(fmt.Sprintf("Dropped: %d", dropped)) - cimgui.Text(fmt.Sprintf("Index: %d", shownIndex)) - if hasDisplayedVideo { + mediaStats := player.MediaStats.Snapshot() + cimgui.SeparatorText("Video") + if mediaStats.Video.Available { + label := mediaStats.Video.Label + if label == "" { + label = "(no label)" + } + cimgui.TextWrapped(fmt.Sprintf("Label: %s", label)) + if hasSnapshot && snapshot.Desired.Video.IsConfigured() { + cimgui.TextWrapped(fmt.Sprintf("Domain: %s", snapshot.Desired.Video.Domain)) + cimgui.TextWrapped(fmt.Sprintf("UUID: %s", snapshot.Desired.Video.UUID)) + } cimgui.Text(fmt.Sprintf( - "Video: %dx%d", - displayedVideoWidth, - displayedVideoHeight, + "Resolution: %dx%d (stride %d)", + mediaStats.Video.Width, + mediaStats.Video.Height, + mediaStats.Video.Stride, )) + cimgui.Text(fmt.Sprintf( + "FPS: flow %.3f | received %.1f | displayed %.1f", + mediaStats.Video.DeclaredFPS, + mediaStats.Video.ReceivedFPS, + fps, + )) + cimgui.Text(fmt.Sprintf( + "Frame dt: source %.3f ms | render loop %.3f ms", + float64(mediaStats.Video.FrameDT.Microseconds())/1000, + float64(renderLoopDT.Microseconds())/1000, + )) + cimgui.Text(fmt.Sprintf( + "Index: %d | payload: %d bytes", + mediaStats.Video.Index, + mediaStats.Video.PayloadSize, + )) + cimgui.Text(fmt.Sprintf( + "Dropped: %d | invalid: %d", + droppedTotal, + mediaStats.Video.Invalid, + )) + } else { + cimgui.Text("No video frames received") + } + + cimgui.SeparatorText("Audio") + if mediaStats.Audio.Available { + label := mediaStats.Audio.Label + if label == "" { + label = "(no label)" + } + cimgui.TextWrapped(fmt.Sprintf("Label: %s", label)) + if hasSnapshot && snapshot.Desired.Audio.IsConfigured() { + cimgui.TextWrapped(fmt.Sprintf("Domain: %s", snapshot.Desired.Audio.Domain)) + cimgui.TextWrapped(fmt.Sprintf("UUID: %s", snapshot.Desired.Audio.UUID)) + } + cimgui.Text(fmt.Sprintf( + "Format: %.3f kHz, %d channels", + mediaStats.Audio.SampleRateHz/1000, + mediaStats.Audio.Channels, + )) + cimgui.Text(fmt.Sprintf( + "Batch: %d samples (%.3f ms)", + mediaStats.Audio.SampleCount, + float64(mediaStats.Audio.BatchDuration.Microseconds())/1000, + )) + cimgui.Text(fmt.Sprintf("Index: %d", mediaStats.Audio.Index)) + } else { + cimgui.Text("No audio batches received") + } + + cimgui.SeparatorText("Runtime") + if hasSnapshot { + cimgui.Text(fmt.Sprintf( + "Topology: %s | generation %d", + snapshot.Plan.Topology, + snapshot.Generation, + )) + drawCompactStatus := func(label string, unit playback.Unit) { + status, ok := statusStore.Snapshot(unit) + if !ok { + cimgui.Text(fmt.Sprintf("%s: not started", label)) + return + } + cimgui.Text(fmt.Sprintf( + "%s: %s (attempt %d, failed %d)", + label, + status.State, + status.Attempt, + status.FailedAttempts, + )) + } + switch snapshot.Plan.Topology { + case playback.TopologySynchronized: + drawCompactStatus("Sync", playback.UnitSync) + case playback.TopologyIndependent: + if snapshot.Plan.Video.Active { + drawCompactStatus("Video", playback.UnitVideo) + } + if snapshot.Plan.Audio.Active { + drawCompactStatus("Audio", playback.UnitAudio) + } + } + } else { + cimgui.Text("Playback controller is starting") } cimgui.End() } // settings & info window - snapshot, hasSnapshot := player.Controller.Snapshot() videoConfigured := videoStr != "" audioConfigured := audioStr != "" if hasSnapshot { @@ -933,6 +1029,7 @@ func main() { if err != nil { panic(err) } + renderLoopDT = time.Since(frameStart) } else { time.Sleep(10 * time.Millisecond) diff --git a/cmd/mxl-player/playback_runtime.go b/cmd/mxl-player/playback_runtime.go index 151514f..d4deae8 100644 --- a/cmd/mxl-player/playback_runtime.go +++ b/cmd/mxl-player/playback_runtime.go @@ -20,6 +20,7 @@ type playerPlayback struct { Commands chan playback.SessionCommand Video *playback.VideoBridge Status *playback.StatusStore + MediaStats *playback.MediaStatsStore Audio playerAudioSink } @@ -29,7 +30,10 @@ func newPlayerPlayback( ) (*playerPlayback, error) { videoBridge := playback.NewVideoBridge() statusStore := playback.NewStatusStore() + mediaStats := playback.NewMediaStatsStore() audioSink := output.NewSDLAudioSink(audioDevice) + videoSink := playback.VideoStatsSink{Stats: mediaStats, Sink: videoBridge} + observedAudioSink := playback.AudioStatsSink{Stats: mediaStats, Sink: audioSink} observe := func(status playback.Status) { statusStore.Observe(status) @@ -57,7 +61,7 @@ func newPlayerPlayback( videoWorker, err := playback.NewVideoWorker( mxladapter.VideoFactory{}, - videoBridge, + videoSink, retry, mxladapter.ShouldRetry, observe, @@ -74,7 +78,7 @@ func newPlayerPlayback( audioWorker, err := playback.NewAudioWorker( mxladapter.AudioFactory{}, - audioSink, + observedAudioSink, retry, mxladapter.ShouldRetry, observe, @@ -91,8 +95,8 @@ func newPlayerPlayback( syncWorker, err := playback.NewSyncWorker( mxladapter.SyncFactory{}, - videoBridge, - audioSink, + videoSink, + observedAudioSink, retry, mxladapter.ShouldRetry, observe, @@ -125,6 +129,7 @@ func newPlayerPlayback( Commands: make(chan playback.SessionCommand, 32), Video: videoBridge, Status: statusStore, + MediaStats: mediaStats, Audio: audioSink, }, nil } diff --git a/imgui.ini b/imgui.ini index f8dd632..4255119 100644 --- a/imgui.ini +++ b/imgui.ini @@ -10,7 +10,7 @@ Collapsed=0 [Window][Stats] Pos=10,10 -Size=200,200 +Size=460,510 Collapsed=0 [Window][Connection] diff --git a/internal/adapter/mxl/audio.go b/internal/adapter/mxl/audio.go index 4373242..e5821b9 100644 --- a/internal/adapter/mxl/audio.go +++ b/internal/adapter/mxl/audio.go @@ -170,6 +170,7 @@ func (r *audioReader) ReadAudio(ctx context.Context) (playback.AudioFrame, error Index: frame.Index, SampleCount: frame.SampleCount, Channels: frame.Channels, + Label: frame.Label, SampleRateNumerator: r.rateNumerator, SampleRateDenominator: r.rateDenominator, Samples: frame.Samples, diff --git a/internal/adapter/mxl/sync.go b/internal/adapter/mxl/sync.go index c8194b2..a9431e9 100644 --- a/internal/adapter/mxl/sync.go +++ b/internal/adapter/mxl/sync.go @@ -133,18 +133,22 @@ func (r *syncReader) ReadSync( } 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, + Index: video.Index, + Width: video.Width, + Height: video.Height, + Stride: video.Stride, + Size: video.Size, + Invalid: video.Invalid, + Label: video.Label, + FrameRateNumerator: video.FrameRateNumerator, + FrameRateDenominator: video.FrameRateDenominator, + Payload: video.Payload, }, Audio: playback.AudioFrame{ Index: audio.Index, SampleCount: audio.SampleCount, Channels: audio.Channels, + Label: audio.Label, SampleRateNumerator: r.rateNumerator, SampleRateDenominator: r.rateDenominator, Samples: audio.Samples, diff --git a/internal/adapter/mxl/video.go b/internal/adapter/mxl/video.go index 26e662d..cb867a6 100644 --- a/internal/adapter/mxl/video.go +++ b/internal/adapter/mxl/video.go @@ -113,13 +113,16 @@ func (r *videoReader) ReadVideo( frame, err := r.source.ReadOnceCtx(ctx, r.readTimeout) if err == nil { return playback.VideoFrame{ - Index: frame.Index, - Width: frame.Width, - Height: frame.Height, - Stride: frame.Stride, - Size: frame.Size, - Invalid: frame.Invalid, - Payload: frame.Payload, + Index: frame.Index, + Width: frame.Width, + Height: frame.Height, + Stride: frame.Stride, + Size: frame.Size, + Invalid: frame.Invalid, + Label: frame.Label, + FrameRateNumerator: frame.FrameRateNumerator, + FrameRateDenominator: frame.FrameRateDenominator, + Payload: frame.Payload, }, nil } if ctx.Err() != nil { diff --git a/internal/playback/audio.go b/internal/playback/audio.go index 0d37fc5..3972edd 100644 --- a/internal/playback/audio.go +++ b/internal/playback/audio.go @@ -13,6 +13,7 @@ type AudioFrame struct { Index uint64 SampleCount uint64 Channels uint64 + Label string SampleRateNumerator int64 SampleRateDenominator int64 diff --git a/internal/playback/media_stats.go b/internal/playback/media_stats.go new file mode 100644 index 0000000..68c4215 --- /dev/null +++ b/internal/playback/media_stats.go @@ -0,0 +1,136 @@ +package playback + +import ( + "context" + "sync" + "time" +) + +type VideoMediaStats struct { + Available bool + Label string + Index uint64 + Width uint32 + Height uint32 + Stride uint32 + PayloadSize uint32 + DeclaredFPS float64 + ReceivedFPS float64 + FrameDT time.Duration + Invalid uint64 +} + +type AudioMediaStats struct { + Available bool + Label string + Index uint64 + SampleRateHz float64 + Channels uint64 + SampleCount uint64 + BatchDuration time.Duration +} + +type MediaStatsSnapshot struct { + Video VideoMediaStats + Audio AudioMediaStats +} + +type MediaStatsStore struct { + mu sync.RWMutex + snapshot MediaStatsSnapshot + now func() time.Time + + lastVideoAt time.Time + videoWindowAt time.Time + videoWindowCount uint64 +} + +func NewMediaStatsStore() *MediaStatsStore { + return &MediaStatsStore{now: time.Now} +} + +func (s *MediaStatsStore) ObserveVideo(frame VideoFrame) { + now := s.now() + s.mu.Lock() + defer s.mu.Unlock() + + stats := &s.snapshot.Video + stats.Available = true + stats.Label = frame.Label + stats.Index = frame.Index + stats.Width = frame.Width + stats.Height = frame.Height + stats.Stride = frame.Stride + stats.PayloadSize = frame.Size + if frame.FrameRateDenominator > 0 { + stats.DeclaredFPS = float64(frame.FrameRateNumerator) / + float64(frame.FrameRateDenominator) + } + if !s.lastVideoAt.IsZero() { + stats.FrameDT = now.Sub(s.lastVideoAt) + } + s.lastVideoAt = now + if frame.Invalid { + stats.Invalid++ + } + if s.videoWindowAt.IsZero() { + s.videoWindowAt = now + } + s.videoWindowCount++ + if elapsed := now.Sub(s.videoWindowAt); elapsed >= time.Second { + stats.ReceivedFPS = float64(s.videoWindowCount) / elapsed.Seconds() + s.videoWindowAt = now + s.videoWindowCount = 0 + } +} + +func (s *MediaStatsStore) ObserveAudio(frame AudioFrame) { + s.mu.Lock() + defer s.mu.Unlock() + + stats := &s.snapshot.Audio + stats.Available = true + stats.Label = frame.Label + stats.Index = frame.Index + stats.Channels = frame.Channels + stats.SampleCount = frame.SampleCount + if frame.SampleRateDenominator > 0 { + stats.SampleRateHz = float64(frame.SampleRateNumerator) / + float64(frame.SampleRateDenominator) + } + if stats.SampleRateHz > 0 { + stats.BatchDuration = time.Duration( + float64(time.Second) * float64(frame.SampleCount) / stats.SampleRateHz, + ) + } +} + +func (s *MediaStatsStore) Snapshot() MediaStatsSnapshot { + s.mu.RLock() + defer s.mu.RUnlock() + return s.snapshot +} + +type VideoStatsSink struct { + Stats *MediaStatsStore + Sink VideoSink +} + +func (s VideoStatsSink) ConsumeVideo(ctx context.Context, frame VideoFrame) error { + if s.Stats != nil { + s.Stats.ObserveVideo(frame) + } + return s.Sink.ConsumeVideo(ctx, frame) +} + +type AudioStatsSink struct { + Stats *MediaStatsStore + Sink AudioSink +} + +func (s AudioStatsSink) ConsumeAudio(ctx context.Context, frame AudioFrame) error { + if s.Stats != nil { + s.Stats.ObserveAudio(frame) + } + return s.Sink.ConsumeAudio(ctx, frame) +} diff --git a/internal/playback/media_stats_test.go b/internal/playback/media_stats_test.go new file mode 100644 index 0000000..f936898 --- /dev/null +++ b/internal/playback/media_stats_test.go @@ -0,0 +1,116 @@ +package playback + +import ( + "context" + "testing" + "time" +) + +func TestMediaStatsStoreObservesVideo(t *testing.T) { + store := NewMediaStatsStore() + now := time.Date(2026, time.September, 1, 12, 0, 0, 0, time.UTC) + store.now = func() time.Time { return now } + frame := VideoFrame{ + Index: 10, + Width: 1920, + Height: 1080, + Stride: 5120, + Size: 5_529_600, + Label: "Main video", + FrameRateNumerator: 30000, + FrameRateDenominator: 1001, + } + store.ObserveVideo(frame) + now = now.Add(40 * time.Millisecond) + frame.Index++ + frame.Invalid = true + store.ObserveVideo(frame) + + got := store.Snapshot().Video + if !got.Available || got.Label != "Main video" || got.Index != 11 { + t.Fatalf("video stats = %#v", got) + } + if got.Width != 1920 || got.Height != 1080 || got.Stride != 5120 { + t.Fatalf("video dimensions = %#v", got) + } + if got.DeclaredFPS < 29.96 || got.DeclaredFPS > 29.98 { + t.Fatalf("declared FPS = %v", got.DeclaredFPS) + } + if got.FrameDT != 40*time.Millisecond || got.Invalid != 1 { + t.Fatalf("video timing = %#v", got) + } +} + +func TestMediaStatsStoreCalculatesReceivedFPS(t *testing.T) { + store := NewMediaStatsStore() + now := time.Date(2026, time.September, 1, 12, 0, 0, 0, time.UTC) + store.now = func() time.Time { return now } + store.ObserveVideo(VideoFrame{}) + now = now.Add(500 * time.Millisecond) + store.ObserveVideo(VideoFrame{}) + now = now.Add(500 * time.Millisecond) + store.ObserveVideo(VideoFrame{}) + + if got := store.Snapshot().Video.ReceivedFPS; got != 3 { + t.Fatalf("received FPS = %v, want 3", got) + } +} + +func TestMediaStatsStoreObservesAudio(t *testing.T) { + store := NewMediaStatsStore() + store.ObserveAudio(AudioFrame{ + Index: 100, + SampleCount: 480, + Channels: 2, + Label: "Programme audio", + SampleRateNumerator: 48_000, + SampleRateDenominator: 1, + }) + + got := store.Snapshot().Audio + if !got.Available || got.Label != "Programme audio" || got.Index != 100 { + t.Fatalf("audio stats = %#v", got) + } + if got.SampleRateHz != 48_000 || got.Channels != 2 || got.SampleCount != 480 { + t.Fatalf("audio format = %#v", got) + } + if got.BatchDuration != 10*time.Millisecond { + t.Fatalf("batch duration = %v, want 10ms", got.BatchDuration) + } +} + +type recordingVideoStatsSink struct{ frame VideoFrame } + +func (s *recordingVideoStatsSink) ConsumeVideo(_ context.Context, frame VideoFrame) error { + s.frame = frame + return nil +} + +type recordingAudioStatsSink struct{ frame AudioFrame } + +func (s *recordingAudioStatsSink) ConsumeAudio(_ context.Context, frame AudioFrame) error { + s.frame = frame + return nil +} + +func TestStatsSinksObserveAndForwardFrames(t *testing.T) { + store := NewMediaStatsStore() + videoDownstream := &recordingVideoStatsSink{} + audioDownstream := &recordingAudioStatsSink{} + video := VideoFrame{Index: 7, Label: "video"} + audio := AudioFrame{Index: 8, Label: "audio"} + + if err := (VideoStatsSink{Stats: store, Sink: videoDownstream}).ConsumeVideo(context.Background(), video); err != nil { + t.Fatalf("ConsumeVideo() error = %v", err) + } + if err := (AudioStatsSink{Stats: store, Sink: audioDownstream}).ConsumeAudio(context.Background(), audio); err != nil { + t.Fatalf("ConsumeAudio() error = %v", err) + } + if videoDownstream.frame.Index != video.Index || audioDownstream.frame.Index != audio.Index { + t.Fatalf("forwarded frames = %#v, %#v", videoDownstream.frame, audioDownstream.frame) + } + snapshot := store.Snapshot() + if snapshot.Video.Label != "video" || snapshot.Audio.Label != "audio" { + t.Fatalf("stats snapshot = %#v", snapshot) + } +} diff --git a/internal/playback/video.go b/internal/playback/video.go index 16500b8..98271ee 100644 --- a/internal/playback/video.go +++ b/internal/playback/video.go @@ -8,13 +8,16 @@ import "context" // reader is closed. Consumers must finish reading Payload before returning // control to the worker type VideoFrame struct { - Index uint64 - Width uint32 - Height uint32 - Stride uint32 - Size uint32 - Invalid bool - Payload []byte + Index uint64 + Width uint32 + Height uint32 + Stride uint32 + Size uint32 + Invalid bool + Label string + FrameRateNumerator int64 + FrameRateDenominator int64 + Payload []byte } // VideoReader reads frames from a video source. diff --git a/internal/source/source.go b/internal/source/source.go index c1215c5..9670311 100644 --- a/internal/source/source.go +++ b/internal/source/source.go @@ -11,6 +11,7 @@ import ( ) type flowDef struct { + Label string `json:"label"` FrameWidth int `json:"frame_width"` FrameHeight int `json:"frame_height"` MediaType string `json:"media_type"` @@ -21,14 +22,31 @@ type flowDef struct { } `json:"grain_rate"` } +func flowLabel(inst *mxl.Instance, flowID string) string { + definition, err := inst.FlowDef(flowID) + if err != nil { + return "" + } + var metadata struct { + Label string `json:"label"` + } + if json.Unmarshal([]byte(definition), &metadata) != nil { + return "" + } + return metadata.Label +} + type Frame struct { - Index uint64 - Width uint32 - Height uint32 - Stride uint32 - Size uint32 - Invalid bool - Payload []byte + Index uint64 + Width uint32 + Height uint32 + Stride uint32 + Size uint32 + Invalid bool + Label string + FrameRateNumerator int64 + FrameRateDenominator int64 + Payload []byte } type Source struct { @@ -40,6 +58,7 @@ type Source struct { stride uint32 width uint32 height uint32 + label string idx uint64 } @@ -109,6 +128,7 @@ func Open(domain, flowID string) (*Source, error) { stride: info.Config.Discrete.SliceSizes[0], width: uint32(fd.FrameWidth), height: uint32(fd.FrameHeight), + label: fd.Label, idx: idx, }, nil } @@ -157,13 +177,16 @@ func (s *Source) ReadOnceCtx( switch { case err == nil: frame := Frame{ - Index: g.Index, - Width: s.width, - Height: s.height, - Stride: s.stride, - Size: g.GrainSize, - Invalid: g.Invalid(), - Payload: g.Payload, + Index: g.Index, + Width: s.width, + Height: s.height, + Stride: s.stride, + Size: g.GrainSize, + Invalid: g.Invalid(), + Label: s.label, + FrameRateNumerator: s.rate.Num, + FrameRateDenominator: s.rate.Den, + Payload: g.Payload, } s.idx++ return frame, nil @@ -222,12 +245,14 @@ type AudioSource struct { rate mxl.Rational chans uint64 idx uint64 + label string } type AudioFrame struct { Index uint64 SampleCount uint64 Channels uint64 + Label string Samples [][]byte // per-channel byte slices (F32, deinterleaved) } @@ -293,6 +318,7 @@ func OpenAudio(domain, flowID string) (*AudioSource, error) { rate: rate, chans: channels, idx: idx, + label: flowLabel(inst, flowID), }, nil } @@ -326,6 +352,7 @@ func (s *AudioSource) ReadAudioOnceCtx( Index: s.idx, SampleCount: batch, Channels: s.chans, + Label: s.label, Samples: samples, } s.idx += batch @@ -401,15 +428,16 @@ func (s *AudioSource) Rate() mxl.Rational { return s.rate } func (s *AudioSource) Channels() uint64 { return s.chans } type SyncSource struct { - inst *mxl.Instance - vr *mxl.Reader - ar *mxl.Reader - group *mxl.SyncGroup - rate mxl.Rational // video rate - aRate mxl.Rational - chans uint64 - idx uint64 - width, height, stride uint32 + inst *mxl.Instance + vr *mxl.Reader + ar *mxl.Reader + group *mxl.SyncGroup + rate mxl.Rational // video rate + aRate mxl.Rational + chans uint64 + idx uint64 + width, height, stride uint32 + videoLabel, audioLabel string } // OpenSameDomainSync opens a native MXL synchronization group. @@ -577,17 +605,19 @@ func OpenSameDomainSync(domain, videoFlow, audioFlow string) (*SyncSource, error } return &SyncSource{ - inst: inst, - vr: vr, - ar: ar, - group: group, - rate: vRate, - aRate: aRate, - chans: channels, - idx: idx, - width: uint32(fd.FrameWidth), - height: uint32(fd.FrameHeight), - stride: vInfo.Config.Discrete.SliceSizes[0], + inst: inst, + vr: vr, + ar: ar, + group: group, + rate: vRate, + aRate: aRate, + chans: channels, + idx: idx, + width: uint32(fd.FrameWidth), + height: uint32(fd.FrameHeight), + stride: vInfo.Config.Discrete.SliceSizes[0], + videoLabel: fd.Label, + audioLabel: flowLabel(inst, audioFlow), }, nil } @@ -665,6 +695,9 @@ func (s *SyncSource) NextSync(ctx context.Context, timeout time.Duration) (Frame Index: g.Index, Width: s.width, Height: s.height, Stride: s.stride, Size: g.GrainSize, Invalid: g.Invalid(), Payload: g.Payload, + Label: s.videoLabel, + FrameRateNumerator: s.rate.Num, + FrameRateDenominator: s.rate.Den, } s.idx++ @@ -680,6 +713,7 @@ func (s *SyncSource) NextSync(ctx context.Context, timeout time.Duration) (Frame aFrame := AudioFrame{ Index: aIdx, SampleCount: audioBatch, Channels: s.chans, Samples: samples, + Label: s.audioLabel, } return vFrame, aFrame, nil case errors.Is(err, mxl.ErrTimeout), errors.Is(err, mxl.ErrOutOfRangeEarly), errors.Is(err, mxl.ErrOutOfRangeLate): diff --git a/sample-list-all.json b/sample-list-all.json new file mode 100644 index 0000000..de250aa --- /dev/null +++ b/sample-list-all.json @@ -0,0 +1,33 @@ +{ + "loop": true, + "entries": [ + { + "name": "timelapse", + "video": { + "domain": "/dev/shm/mxl", + "uuid": "5fbec3b1-1b0f-417d-9059-8b94a47197ed" + }, + "audio": { + "domain": "/dev/shm/mxl", + "uuid": "5fbec3b1-1b0f-417d-9059-8b94a47197ec" + }, + "sync": true, + "duration": "10s" + }, + { + "name": "F1", + "video": { + "domain": "/dev/shm/mxl", + "uuid": "5fbec3b1-1b0f-417d-9059-8b94a47197ef" + }, + "duration": "15s" + }, + { + "name": "Costa Rica", + "audio": { + "domain": "/dev/shm/mxl", + "uuid": "9d2a041b-01cf-4ee4-bffa-188fe093c99b" + } + } + ] +} diff --git a/sample-list.json b/sample-list.json index de250aa..f6fd9e1 100644 --- a/sample-list.json +++ b/sample-list.json @@ -15,19 +15,30 @@ "duration": "10s" }, { - "name": "F1", + "name": "F1 Highlights", "video": { "domain": "/dev/shm/mxl", "uuid": "5fbec3b1-1b0f-417d-9059-8b94a47197ef" }, - "duration": "15s" + "audio": { + "domain": "/dev/shm/mxl", + "uuid": "5fbec3b1-1b0f-417d-9059-8b94a47197eb" + }, + "sync": true, + "duration": "10s" }, { "name": "Costa Rica", + "video": { + "domain": "/dev/shm/mxl", + "uuid": "2618979d-76a5-45e0-83cb-0f192978d1cd" + }, "audio": { "domain": "/dev/shm/mxl", "uuid": "9d2a041b-01cf-4ee4-bffa-188fe093c99b" - } + }, + "sync": true, + "duration": "10s" } ] }