diff --git a/cmd/mxl-player/main.go b/cmd/mxl-player/main.go index d578858..27b8c01 100644 --- a/cmd/mxl-player/main.go +++ b/cmd/mxl-player/main.go @@ -8,6 +8,7 @@ import ( "log" mxladapter "mxl-player/internal/adapter/mxl" "mxl-player/internal/imgui" + "mxl-player/internal/output" "mxl-player/internal/playback" "mxl-player/internal/renderer" "mxl-player/internal/sdl" @@ -149,6 +150,7 @@ func main() { } // path selection useVideoSlot := args.AudioFlowId == "" + useAudioSlot := args.AudioFlowId != "" && args.VideoFlowId == "" runtime.LockOSThread() if err := sdl.Load(); err != nil { @@ -303,25 +305,7 @@ func main() { case args.VideoFlowId != "": // VideoSlot owns opening and closing the video reader. case args.AudioFlowId != "": - audioSrc, err = source.OpenAudio(args.Domain, args.AudioFlowId) - if err != nil { - log.Fatalf("audio source: %v", err) - } - aChans = audioSrc.Channels() - audioBatch = uint64(audioSrc.Rate().Num) / (100 * uint64(audioSrc.Rate().Den)) - if audioBatch == 0 { - audioBatch = 1 - } - audioStream = sdl.OpenAudioDeviceStream(sdlAudioDevice, sdl.AudioSpec{ - Format: sdl.AudioF32, - Channels: int32(aChans), - Freq: int32(audioSrc.Rate().Num / audioSrc.Rate().Den), - }) - if audioStream == 0 { - log.Fatalf("audio: %s", sdl.GetError()) - } - sdl.ResumeAudioStreamDevice(audioStream) - fmt.Printf("audio: %dch %d/%d Hz\n", aChans, audioSrc.Rate().Num, audioSrc.Rate().Den) + // AudioSlot owns opening and closing the audio reader. default: // No configured feeds. Renderer and GUI use the placeholder. } @@ -390,6 +374,7 @@ func main() { showStats bool = true ) videoActive := args.VideoFlowId != "" + audioActive := useAudioSlot ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -437,6 +422,44 @@ func main() { } videoCommands := make(chan playback.FeedConfig, 1) + audioOutput := output.NewSDLAudioSink(sdlAudioDevice) + defer audioOutput.Close() + audioWorker, err := playback.NewAudioWorker( + mxladapter.AudioFactory{}, + audioOutput, + retryPolicy, + mxladapter.ShouldRetry, + func(status playback.Status) { + statusStore.Observe(status) + + if status.Err != nil { + log.Printf( + "audio: state=%v attempt=%d failed=%d: %v", + status.State, + status.Attempt, + status.FailedAttempts, + status.Err, + ) + return + } + + log.Printf( + "audio: state=%v attempt=%d failed=%d", + status.State, + status.Attempt, + status.FailedAttempts, + ) + }, + ) + if err != nil { + panic(err) + } + audioSlot, err := playback.NewAudioSlot(audioWorker) + if err != nil { + panic(err) + } + audioCommands := make(chan playback.FeedConfig, 1) + reopen := func(params reconnectParams) error { // Close current sources if syncSrc != nil { @@ -499,6 +522,17 @@ func main() { default: } } + enqueueAudioConfig := func(config playback.FeedConfig) { + select { + case <-audioCommands: + default: + } + + select { + case audioCommands <- config: + default: + } + } doReconnect := func() { if useVideoSlot { @@ -516,6 +550,22 @@ func main() { enqueueVideoConfig(config) return } + if useAudioSlot { + audioActive = audioStr != "" + + config := playback.FeedConfig{} + if audioStr != "" { + config = playback.FeedConfig{ + Domain: domainStr, + UUID: audioStr, + Active: true, + } + } + + enqueueAudioConfig(config) + return + } + // legacy select { case <-control: default: @@ -542,6 +592,21 @@ func main() { } return } + if useAudioSlot { + err := audioSlot.Run( + ctx, + playback.FeedConfig{ + Domain: args.Domain, + UUID: args.AudioFlowId, + Active: args.AudioFlowId != "", + }, + audioCommands, + ) + if err != nil && !errors.Is(err, context.Canceled) { + log.Printf("audio slot: %v", err) + } + return + } // Audio-only mode: independent loop. if audioSrc != nil && syncSrc == nil && videoSrc == nil { @@ -929,6 +994,71 @@ func main() { cimgui.Text("Video actual: not started") } } + if useAudioSlot && audioActive { + if cimgui.Button("Stop audio") { + audioActive = false + enqueueAudioConfig(playback.FeedConfig{ + Domain: domainStr, + UUID: audioStr, + Active: false, + }) + } + } + + if useAudioSlot && !audioActive && audioStr != "" { + if cimgui.Button("Resume audio") { + audioActive = true + enqueueAudioConfig(playback.FeedConfig{ + Domain: domainStr, + UUID: audioStr, + Active: true, + }) + } + } + + if useAudioSlot && audioStr != "" { + if cimgui.Button("Remove audio") { + audioActive = false + audioStr = "" + enqueueAudioConfig(playback.FeedConfig{}) + } + } + + if useAudioSlot { + if audioActive { + cimgui.Text("Audio desired: active") + } else if audioStr != "" { + cimgui.Text("Audio desired: stopped") + } else { + cimgui.Text("Audio desired: not configured") + } + + if status, ok := statusStore.Snapshot(playback.UnitAudio); ok { + cimgui.Text(fmt.Sprintf( + "Audio actual: %s", + status.State, + )) + cimgui.Text(fmt.Sprintf( + "Attempt: %d, failed: %d", + status.Attempt, + status.FailedAttempts, + )) + + if status.RetryIn > 0 { + cimgui.Text(fmt.Sprintf( + "Retry in: %s", + status.RetryIn.Round(time.Millisecond), + )) + } + + if status.Err != nil { + cimgui.TextWrapped(status.Err.Error()) + } + } else { + cimgui.Text("Audio actual: not started") + } + } + cimgui.End() gui.EndFrame() lastFrame = time.Now() diff --git a/imgui.ini b/imgui.ini index 8609d51..97d736a 100644 --- a/imgui.ini +++ b/imgui.ini @@ -14,7 +14,7 @@ Size=200,200 Collapsed=0 [Window][Connection] -Pos=979,700 -Size=695,233 +Pos=174,363 +Size=986,237 Collapsed=0