stats window

This commit is contained in:
Dmitry Sergeev
2026-09-01 22:59:09 +03:00
parent ca25bf88a7
commit a8d277ee3d
13 changed files with 524 additions and 80 deletions
+114 -17
View File
@@ -464,16 +464,18 @@ func main() {
displayedVideoWidth uint32 = placeholderWidth displayedVideoWidth uint32 = placeholderWidth
displayedVideoHeight uint32 = placeholderHeight displayedVideoHeight uint32 = placeholderHeight
displayedVideoStride uint32 = placeholderStride displayedVideoStride uint32 = placeholderStride
hasDisplayedVideo bool = false
fps float64 fps float64
lastIndex uint64 lastIndex uint64
dropped uint64 dropped uint64
frameCount uint64 droppedTotal uint64
lastReport time.Time frameCount uint64
lastFrame time.Time lastReport time.Time
lastFrame time.Time
renderLoopDT time.Duration
) )
lastFrame = time.Now() lastFrame = time.Now()
lastReport = lastFrame
// ImGui // ImGui
var ( var (
@@ -555,7 +557,6 @@ func main() {
displayedVideoWidth = pendingFrame.Frame.Width displayedVideoWidth = pendingFrame.Frame.Width
displayedVideoHeight = pendingFrame.Frame.Height displayedVideoHeight = pendingFrame.Frame.Height
displayedVideoStride = pendingFrame.Frame.Stride displayedVideoStride = pendingFrame.Frame.Stride
hasDisplayedVideo = true
hasFrame = true hasFrame = true
} else if frameErr != nil && } else if frameErr != nil &&
!errors.Is(frameErr, context.DeadlineExceeded) && !errors.Is(frameErr, context.DeadlineExceeded) &&
@@ -568,6 +569,7 @@ func main() {
if lastIndex != 0 && shownIndex > lastIndex { if lastIndex != 0 && shownIndex > lastIndex {
if g := shownIndex - lastIndex - 1; g > 0 { if g := shownIndex - lastIndex - 1; g > 0 {
dropped += g dropped += g
droppedTotal += g
} }
} }
lastIndex = shownIndex lastIndex = shownIndex
@@ -585,25 +587,119 @@ func main() {
// end of stats // end of stats
if r != nil { if r != nil {
gui.BeginFrame(time.Since(lastFrame), int32(r.Extent().Width), int32(r.Extent().Height)) gui.BeginFrame(time.Since(lastFrame), int32(r.Extent().Width), int32(r.Extent().Height))
snapshot, hasSnapshot := player.Controller.Snapshot()
if showStats { if showStats {
cimgui.SetNextWindowPos(cimgui.Vec2{X: 10, Y: 10}) 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.BeginV("Stats", &showStats,
cimgui.WindowFlagsNoTitleBar|cimgui.WindowFlagsNoResize|cimgui.WindowFlagsNoScrollbar) cimgui.WindowFlagsNoTitleBar|cimgui.WindowFlagsNoResize|cimgui.WindowFlagsNoScrollbar)
cimgui.Text(fmt.Sprintf("FPS: %.1f", fps)) mediaStats := player.MediaStats.Snapshot()
cimgui.Text(fmt.Sprintf("Dropped: %d", dropped)) cimgui.SeparatorText("Video")
cimgui.Text(fmt.Sprintf("Index: %d", shownIndex)) if mediaStats.Video.Available {
if hasDisplayedVideo { 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( cimgui.Text(fmt.Sprintf(
"Video: %dx%d", "Resolution: %dx%d (stride %d)",
displayedVideoWidth, mediaStats.Video.Width,
displayedVideoHeight, 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() cimgui.End()
} }
// settings & info window // settings & info window
snapshot, hasSnapshot := player.Controller.Snapshot()
videoConfigured := videoStr != "" videoConfigured := videoStr != ""
audioConfigured := audioStr != "" audioConfigured := audioStr != ""
if hasSnapshot { if hasSnapshot {
@@ -933,6 +1029,7 @@ func main() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
renderLoopDT = time.Since(frameStart)
} else { } else {
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
+9 -4
View File
@@ -20,6 +20,7 @@ type playerPlayback struct {
Commands chan playback.SessionCommand Commands chan playback.SessionCommand
Video *playback.VideoBridge Video *playback.VideoBridge
Status *playback.StatusStore Status *playback.StatusStore
MediaStats *playback.MediaStatsStore
Audio playerAudioSink Audio playerAudioSink
} }
@@ -29,7 +30,10 @@ func newPlayerPlayback(
) (*playerPlayback, error) { ) (*playerPlayback, error) {
videoBridge := playback.NewVideoBridge() videoBridge := playback.NewVideoBridge()
statusStore := playback.NewStatusStore() statusStore := playback.NewStatusStore()
mediaStats := playback.NewMediaStatsStore()
audioSink := output.NewSDLAudioSink(audioDevice) audioSink := output.NewSDLAudioSink(audioDevice)
videoSink := playback.VideoStatsSink{Stats: mediaStats, Sink: videoBridge}
observedAudioSink := playback.AudioStatsSink{Stats: mediaStats, Sink: audioSink}
observe := func(status playback.Status) { observe := func(status playback.Status) {
statusStore.Observe(status) statusStore.Observe(status)
@@ -57,7 +61,7 @@ func newPlayerPlayback(
videoWorker, err := playback.NewVideoWorker( videoWorker, err := playback.NewVideoWorker(
mxladapter.VideoFactory{}, mxladapter.VideoFactory{},
videoBridge, videoSink,
retry, retry,
mxladapter.ShouldRetry, mxladapter.ShouldRetry,
observe, observe,
@@ -74,7 +78,7 @@ func newPlayerPlayback(
audioWorker, err := playback.NewAudioWorker( audioWorker, err := playback.NewAudioWorker(
mxladapter.AudioFactory{}, mxladapter.AudioFactory{},
audioSink, observedAudioSink,
retry, retry,
mxladapter.ShouldRetry, mxladapter.ShouldRetry,
observe, observe,
@@ -91,8 +95,8 @@ func newPlayerPlayback(
syncWorker, err := playback.NewSyncWorker( syncWorker, err := playback.NewSyncWorker(
mxladapter.SyncFactory{}, mxladapter.SyncFactory{},
videoBridge, videoSink,
audioSink, observedAudioSink,
retry, retry,
mxladapter.ShouldRetry, mxladapter.ShouldRetry,
observe, observe,
@@ -125,6 +129,7 @@ func newPlayerPlayback(
Commands: make(chan playback.SessionCommand, 32), Commands: make(chan playback.SessionCommand, 32),
Video: videoBridge, Video: videoBridge,
Status: statusStore, Status: statusStore,
MediaStats: mediaStats,
Audio: audioSink, Audio: audioSink,
}, nil }, nil
} }
+1 -1
View File
@@ -10,7 +10,7 @@ Collapsed=0
[Window][Stats] [Window][Stats]
Pos=10,10 Pos=10,10
Size=200,200 Size=460,510
Collapsed=0 Collapsed=0
[Window][Connection] [Window][Connection]
+1
View File
@@ -170,6 +170,7 @@ func (r *audioReader) ReadAudio(ctx context.Context) (playback.AudioFrame, error
Index: frame.Index, Index: frame.Index,
SampleCount: frame.SampleCount, SampleCount: frame.SampleCount,
Channels: frame.Channels, Channels: frame.Channels,
Label: frame.Label,
SampleRateNumerator: r.rateNumerator, SampleRateNumerator: r.rateNumerator,
SampleRateDenominator: r.rateDenominator, SampleRateDenominator: r.rateDenominator,
Samples: frame.Samples, Samples: frame.Samples,
+11 -7
View File
@@ -133,18 +133,22 @@ func (r *syncReader) ReadSync(
} }
return playback.SyncFrame{ return playback.SyncFrame{
Video: playback.VideoFrame{ Video: playback.VideoFrame{
Index: video.Index, Index: video.Index,
Width: video.Width, Width: video.Width,
Height: video.Height, Height: video.Height,
Stride: video.Stride, Stride: video.Stride,
Size: video.Size, Size: video.Size,
Invalid: video.Invalid, Invalid: video.Invalid,
Payload: video.Payload, Label: video.Label,
FrameRateNumerator: video.FrameRateNumerator,
FrameRateDenominator: video.FrameRateDenominator,
Payload: video.Payload,
}, },
Audio: playback.AudioFrame{ Audio: playback.AudioFrame{
Index: audio.Index, Index: audio.Index,
SampleCount: audio.SampleCount, SampleCount: audio.SampleCount,
Channels: audio.Channels, Channels: audio.Channels,
Label: audio.Label,
SampleRateNumerator: r.rateNumerator, SampleRateNumerator: r.rateNumerator,
SampleRateDenominator: r.rateDenominator, SampleRateDenominator: r.rateDenominator,
Samples: audio.Samples, Samples: audio.Samples,
+10 -7
View File
@@ -113,13 +113,16 @@ func (r *videoReader) ReadVideo(
frame, err := r.source.ReadOnceCtx(ctx, r.readTimeout) frame, err := r.source.ReadOnceCtx(ctx, r.readTimeout)
if err == nil { if err == nil {
return playback.VideoFrame{ return playback.VideoFrame{
Index: frame.Index, Index: frame.Index,
Width: frame.Width, Width: frame.Width,
Height: frame.Height, Height: frame.Height,
Stride: frame.Stride, Stride: frame.Stride,
Size: frame.Size, Size: frame.Size,
Invalid: frame.Invalid, Invalid: frame.Invalid,
Payload: frame.Payload, Label: frame.Label,
FrameRateNumerator: frame.FrameRateNumerator,
FrameRateDenominator: frame.FrameRateDenominator,
Payload: frame.Payload,
}, nil }, nil
} }
if ctx.Err() != nil { if ctx.Err() != nil {
+1
View File
@@ -13,6 +13,7 @@ type AudioFrame struct {
Index uint64 Index uint64
SampleCount uint64 SampleCount uint64
Channels uint64 Channels uint64
Label string
SampleRateNumerator int64 SampleRateNumerator int64
SampleRateDenominator int64 SampleRateDenominator int64
+136
View File
@@ -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)
}
+116
View File
@@ -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)
}
}
+10 -7
View File
@@ -8,13 +8,16 @@ import "context"
// reader is closed. Consumers must finish reading Payload before returning // reader is closed. Consumers must finish reading Payload before returning
// control to the worker // control to the worker
type VideoFrame struct { type VideoFrame struct {
Index uint64 Index uint64
Width uint32 Width uint32
Height uint32 Height uint32
Stride uint32 Stride uint32
Size uint32 Size uint32
Invalid bool Invalid bool
Payload []byte Label string
FrameRateNumerator int64
FrameRateDenominator int64
Payload []byte
} }
// VideoReader reads frames from a video source. // VideoReader reads frames from a video source.
+68 -34
View File
@@ -11,6 +11,7 @@ import (
) )
type flowDef struct { type flowDef struct {
Label string `json:"label"`
FrameWidth int `json:"frame_width"` FrameWidth int `json:"frame_width"`
FrameHeight int `json:"frame_height"` FrameHeight int `json:"frame_height"`
MediaType string `json:"media_type"` MediaType string `json:"media_type"`
@@ -21,14 +22,31 @@ type flowDef struct {
} `json:"grain_rate"` } `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 { type Frame struct {
Index uint64 Index uint64
Width uint32 Width uint32
Height uint32 Height uint32
Stride uint32 Stride uint32
Size uint32 Size uint32
Invalid bool Invalid bool
Payload []byte Label string
FrameRateNumerator int64
FrameRateDenominator int64
Payload []byte
} }
type Source struct { type Source struct {
@@ -40,6 +58,7 @@ type Source struct {
stride uint32 stride uint32
width uint32 width uint32
height uint32 height uint32
label string
idx uint64 idx uint64
} }
@@ -109,6 +128,7 @@ func Open(domain, flowID string) (*Source, error) {
stride: info.Config.Discrete.SliceSizes[0], stride: info.Config.Discrete.SliceSizes[0],
width: uint32(fd.FrameWidth), width: uint32(fd.FrameWidth),
height: uint32(fd.FrameHeight), height: uint32(fd.FrameHeight),
label: fd.Label,
idx: idx, idx: idx,
}, nil }, nil
} }
@@ -157,13 +177,16 @@ func (s *Source) ReadOnceCtx(
switch { switch {
case err == nil: case err == nil:
frame := Frame{ frame := Frame{
Index: g.Index, Index: g.Index,
Width: s.width, Width: s.width,
Height: s.height, Height: s.height,
Stride: s.stride, Stride: s.stride,
Size: g.GrainSize, Size: g.GrainSize,
Invalid: g.Invalid(), Invalid: g.Invalid(),
Payload: g.Payload, Label: s.label,
FrameRateNumerator: s.rate.Num,
FrameRateDenominator: s.rate.Den,
Payload: g.Payload,
} }
s.idx++ s.idx++
return frame, nil return frame, nil
@@ -222,12 +245,14 @@ type AudioSource struct {
rate mxl.Rational rate mxl.Rational
chans uint64 chans uint64
idx uint64 idx uint64
label string
} }
type AudioFrame struct { type AudioFrame struct {
Index uint64 Index uint64
SampleCount uint64 SampleCount uint64
Channels uint64 Channels uint64
Label string
Samples [][]byte // per-channel byte slices (F32, deinterleaved) Samples [][]byte // per-channel byte slices (F32, deinterleaved)
} }
@@ -293,6 +318,7 @@ func OpenAudio(domain, flowID string) (*AudioSource, error) {
rate: rate, rate: rate,
chans: channels, chans: channels,
idx: idx, idx: idx,
label: flowLabel(inst, flowID),
}, nil }, nil
} }
@@ -326,6 +352,7 @@ func (s *AudioSource) ReadAudioOnceCtx(
Index: s.idx, Index: s.idx,
SampleCount: batch, SampleCount: batch,
Channels: s.chans, Channels: s.chans,
Label: s.label,
Samples: samples, Samples: samples,
} }
s.idx += batch s.idx += batch
@@ -401,15 +428,16 @@ func (s *AudioSource) Rate() mxl.Rational { return s.rate }
func (s *AudioSource) Channels() uint64 { return s.chans } func (s *AudioSource) Channels() uint64 { return s.chans }
type SyncSource struct { type SyncSource struct {
inst *mxl.Instance inst *mxl.Instance
vr *mxl.Reader vr *mxl.Reader
ar *mxl.Reader ar *mxl.Reader
group *mxl.SyncGroup group *mxl.SyncGroup
rate mxl.Rational // video rate rate mxl.Rational // video rate
aRate mxl.Rational aRate mxl.Rational
chans uint64 chans uint64
idx uint64 idx uint64
width, height, stride uint32 width, height, stride uint32
videoLabel, audioLabel string
} }
// OpenSameDomainSync opens a native MXL synchronization group. // OpenSameDomainSync opens a native MXL synchronization group.
@@ -577,17 +605,19 @@ func OpenSameDomainSync(domain, videoFlow, audioFlow string) (*SyncSource, error
} }
return &SyncSource{ return &SyncSource{
inst: inst, inst: inst,
vr: vr, vr: vr,
ar: ar, ar: ar,
group: group, group: group,
rate: vRate, rate: vRate,
aRate: aRate, aRate: aRate,
chans: channels, chans: channels,
idx: idx, idx: idx,
width: uint32(fd.FrameWidth), width: uint32(fd.FrameWidth),
height: uint32(fd.FrameHeight), height: uint32(fd.FrameHeight),
stride: vInfo.Config.Discrete.SliceSizes[0], stride: vInfo.Config.Discrete.SliceSizes[0],
videoLabel: fd.Label,
audioLabel: flowLabel(inst, audioFlow),
}, nil }, 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, Index: g.Index, Width: s.width, Height: s.height,
Stride: s.stride, Size: g.GrainSize, Stride: s.stride, Size: g.GrainSize,
Invalid: g.Invalid(), Payload: g.Payload, Invalid: g.Invalid(), Payload: g.Payload,
Label: s.videoLabel,
FrameRateNumerator: s.rate.Num,
FrameRateDenominator: s.rate.Den,
} }
s.idx++ s.idx++
@@ -680,6 +713,7 @@ func (s *SyncSource) NextSync(ctx context.Context, timeout time.Duration) (Frame
aFrame := AudioFrame{ aFrame := AudioFrame{
Index: aIdx, SampleCount: audioBatch, Index: aIdx, SampleCount: audioBatch,
Channels: s.chans, Samples: samples, Channels: s.chans, Samples: samples,
Label: s.audioLabel,
} }
return vFrame, aFrame, nil return vFrame, aFrame, nil
case errors.Is(err, mxl.ErrTimeout), errors.Is(err, mxl.ErrOutOfRangeEarly), errors.Is(err, mxl.ErrOutOfRangeLate): case errors.Is(err, mxl.ErrTimeout), errors.Is(err, mxl.ErrOutOfRangeEarly), errors.Is(err, mxl.ErrOutOfRangeLate):
+33
View File
@@ -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"
}
}
]
}
+14 -3
View File
@@ -15,19 +15,30 @@
"duration": "10s" "duration": "10s"
}, },
{ {
"name": "F1", "name": "F1 Highlights",
"video": { "video": {
"domain": "/dev/shm/mxl", "domain": "/dev/shm/mxl",
"uuid": "5fbec3b1-1b0f-417d-9059-8b94a47197ef" "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", "name": "Costa Rica",
"video": {
"domain": "/dev/shm/mxl",
"uuid": "2618979d-76a5-45e0-83cb-0f192978d1cd"
},
"audio": { "audio": {
"domain": "/dev/shm/mxl", "domain": "/dev/shm/mxl",
"uuid": "9d2a041b-01cf-4ee4-bffa-188fe093c99b" "uuid": "9d2a041b-01cf-4ee4-bffa-188fe093c99b"
} },
"sync": true,
"duration": "10s"
} }
] ]
} }