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
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)
+9 -4
View File
@@ -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
}