From 48bb06c9cdeac5f6698b6322683af38653458626 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:49:56 +0000 Subject: [PATCH 1/6] Initial plan From 69c9a692157c92520ef45279b266e49d42ba319f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:54:38 +0000 Subject: [PATCH 2/6] Always seek to live stream when resuming radio playback Co-authored-by: TheJoeFin <7809853+TheJoeFin@users.noreply.github.com> --- Trdo/Services/RadioPlayerService.cs | 85 +++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/Trdo/Services/RadioPlayerService.cs b/Trdo/Services/RadioPlayerService.cs index ca8ab21..a1bb9c0 100644 --- a/Trdo/Services/RadioPlayerService.cs +++ b/Trdo/Services/RadioPlayerService.cs @@ -170,8 +170,21 @@ private RadioPlayerService() Debug.WriteLine("[RadioPlayerService] External state change detected (likely hardware button)"); if (currentState == MediaPlaybackState.Playing) { - _watchdog.NotifyUserIntentionToPlay(); - Debug.WriteLine("[RadioPlayerService] Notified watchdog of user intention to play (hardware button)"); + // For live radio streams, when resuming via hardware buttons we need to + // refresh the stream to get the live position instead of resuming from + // the buffered position. Queue a refresh on the UI thread. + Debug.WriteLine("[RadioPlayerService] External play detected - will refresh stream for live playback"); + TryEnqueueOnUi(() => + { + try + { + RefreshStreamForLivePlayback(); + } + catch (Exception ex) + { + Debug.WriteLine($"[RadioPlayerService] Failed to refresh stream: {ex.Message}"); + } + }); } else if (currentState == MediaPlaybackState.Paused) { @@ -328,19 +341,21 @@ public void Play() try { - // Ensure we have a fresh media source - if (_player.Source == null) + // Always create a fresh media source to ensure we're streaming live content. + // For live radio streams, resuming from a paused position causes issues + // if there's been a long gap, so we always seek to the live stream. + if (_player.Source is MediaSource oldMedia) { - Debug.WriteLine("[RadioPlayerService] Player.Source is null, creating new MediaSource"); - Uri uri = new(_streamUrl); - _player.Source = MediaSource.CreateFromUri(uri); - Debug.WriteLine($"[RadioPlayerService] Created new MediaSource from URL: {_streamUrl}"); - } - else - { - Debug.WriteLine($"[RadioPlayerService] Player.Source exists, current state: {(_player.Source as MediaSource)?.State}"); + Debug.WriteLine("[RadioPlayerService] Disposing old MediaSource to ensure live stream"); + oldMedia.Reset(); + oldMedia.Dispose(); } + Debug.WriteLine("[RadioPlayerService] Creating fresh MediaSource for live stream"); + Uri uri = new(_streamUrl); + _player.Source = MediaSource.CreateFromUri(uri); + Debug.WriteLine($"[RadioPlayerService] Created new MediaSource from URL: {_streamUrl}"); + Debug.WriteLine("[RadioPlayerService] Calling _player.Play()..."); _isInternalStateChange = true; _player.Play(); @@ -382,6 +397,52 @@ public void Play() Debug.WriteLine($"=== Play END ==="); } + /// + /// Refreshes the stream to ensure live playback. + /// Used when external controls (like hardware buttons) resume playback, + /// to ensure we're always streaming live content instead of resuming from a buffered position. + /// + private void RefreshStreamForLivePlayback() + { + Debug.WriteLine("=== RefreshStreamForLivePlayback START ==="); + + if (string.IsNullOrWhiteSpace(_streamUrl)) + { + Debug.WriteLine("[RadioPlayerService] No stream URL set, skipping refresh"); + Debug.WriteLine("=== RefreshStreamForLivePlayback END ==="); + return; + } + + try + { + // Create a fresh media source to get the live stream position + if (_player.Source is MediaSource oldMedia) + { + Debug.WriteLine("[RadioPlayerService] Disposing old MediaSource for live stream refresh"); + oldMedia.Reset(); + oldMedia.Dispose(); + } + + Debug.WriteLine("[RadioPlayerService] Creating fresh MediaSource for live stream"); + Uri uri = new(_streamUrl); + _isInternalStateChange = true; + _player.Source = MediaSource.CreateFromUri(uri); + _player.Play(); + _isInternalStateChange = false; + Debug.WriteLine($"[RadioPlayerService] Stream refreshed for live playback: {_streamUrl}"); + + _watchdog.NotifyUserIntentionToPlay(); + Debug.WriteLine("[RadioPlayerService] Notified watchdog of user intention to play"); + } + catch (Exception ex) + { + _isInternalStateChange = false; + Debug.WriteLine($"[RadioPlayerService] EXCEPTION in RefreshStreamForLivePlayback: {ex.Message}"); + } + + Debug.WriteLine("=== RefreshStreamForLivePlayback END ==="); + } + /// /// Stop playback and clean up resources /// From 9a4acde0927f97034b36c879f38a567b606dbbc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:56:01 +0000 Subject: [PATCH 3/6] Refactor: Eliminate code duplication in RefreshStreamForLivePlayback Co-authored-by: TheJoeFin <7809853+TheJoeFin@users.noreply.github.com> --- Trdo/Services/RadioPlayerService.cs | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/Trdo/Services/RadioPlayerService.cs b/Trdo/Services/RadioPlayerService.cs index a1bb9c0..703f87b 100644 --- a/Trdo/Services/RadioPlayerService.cs +++ b/Trdo/Services/RadioPlayerService.cs @@ -413,30 +413,15 @@ private void RefreshStreamForLivePlayback() return; } + // Play() already creates a fresh MediaSource to ensure live streaming, + // so we just delegate to it to avoid code duplication. try { - // Create a fresh media source to get the live stream position - if (_player.Source is MediaSource oldMedia) - { - Debug.WriteLine("[RadioPlayerService] Disposing old MediaSource for live stream refresh"); - oldMedia.Reset(); - oldMedia.Dispose(); - } - - Debug.WriteLine("[RadioPlayerService] Creating fresh MediaSource for live stream"); - Uri uri = new(_streamUrl); - _isInternalStateChange = true; - _player.Source = MediaSource.CreateFromUri(uri); - _player.Play(); - _isInternalStateChange = false; - Debug.WriteLine($"[RadioPlayerService] Stream refreshed for live playback: {_streamUrl}"); - - _watchdog.NotifyUserIntentionToPlay(); - Debug.WriteLine("[RadioPlayerService] Notified watchdog of user intention to play"); + Play(); + Debug.WriteLine("[RadioPlayerService] Stream refreshed for live playback via Play()"); } catch (Exception ex) { - _isInternalStateChange = false; Debug.WriteLine($"[RadioPlayerService] EXCEPTION in RefreshStreamForLivePlayback: {ex.Message}"); } From e6cbcd69ab811823dd5118fad9311ce1906fd6a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:59:22 +0000 Subject: [PATCH 4/6] Improve exception handling documentation in RefreshStreamForLivePlayback Co-authored-by: TheJoeFin <7809853+TheJoeFin@users.noreply.github.com> --- Trdo/Services/RadioPlayerService.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Trdo/Services/RadioPlayerService.cs b/Trdo/Services/RadioPlayerService.cs index 703f87b..4d7bf98 100644 --- a/Trdo/Services/RadioPlayerService.cs +++ b/Trdo/Services/RadioPlayerService.cs @@ -415,6 +415,9 @@ private void RefreshStreamForLivePlayback() // Play() already creates a fresh MediaSource to ensure live streaming, // so we just delegate to it to avoid code duplication. + // Exceptions are caught here because this method is called from event handlers + // where unhandled exceptions could crash the application. The watchdog service + // will attempt recovery if the stream fails to start. try { Play(); @@ -423,6 +426,7 @@ private void RefreshStreamForLivePlayback() catch (Exception ex) { Debug.WriteLine($"[RadioPlayerService] EXCEPTION in RefreshStreamForLivePlayback: {ex.Message}"); + Debug.WriteLine($"[RadioPlayerService] The watchdog service will attempt recovery if enabled"); } Debug.WriteLine("=== RefreshStreamForLivePlayback END ==="); From c274943fa9a7149fd9f04267defb7156b9a72720 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 05:02:53 +0000 Subject: [PATCH 5/6] Use SeekToLive instead of disposing MediaSource on resume Co-authored-by: TheJoeFin <7809853+TheJoeFin@users.noreply.github.com> --- Trdo/Services/RadioPlayerService.cs | 100 +++++++++++++++++----------- 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/Trdo/Services/RadioPlayerService.cs b/Trdo/Services/RadioPlayerService.cs index 4d7bf98..7fe0ad6 100644 --- a/Trdo/Services/RadioPlayerService.cs +++ b/Trdo/Services/RadioPlayerService.cs @@ -171,18 +171,17 @@ private RadioPlayerService() if (currentState == MediaPlaybackState.Playing) { // For live radio streams, when resuming via hardware buttons we need to - // refresh the stream to get the live position instead of resuming from - // the buffered position. Queue a refresh on the UI thread. - Debug.WriteLine("[RadioPlayerService] External play detected - will refresh stream for live playback"); + // seek to the live position instead of resuming from the buffered position. + Debug.WriteLine("[RadioPlayerService] External play detected - will seek to live position"); TryEnqueueOnUi(() => { try { - RefreshStreamForLivePlayback(); + SeekToLiveOnResume(); } catch (Exception ex) { - Debug.WriteLine($"[RadioPlayerService] Failed to refresh stream: {ex.Message}"); + Debug.WriteLine($"[RadioPlayerService] Failed to seek to live: {ex.Message}"); } }); } @@ -341,20 +340,18 @@ public void Play() try { - // Always create a fresh media source to ensure we're streaming live content. - // For live radio streams, resuming from a paused position causes issues - // if there's been a long gap, so we always seek to the live stream. - if (_player.Source is MediaSource oldMedia) + // Ensure we have a media source + if (_player.Source == null) { - Debug.WriteLine("[RadioPlayerService] Disposing old MediaSource to ensure live stream"); - oldMedia.Reset(); - oldMedia.Dispose(); + Debug.WriteLine("[RadioPlayerService] Player.Source is null, creating new MediaSource"); + Uri uri = new(_streamUrl); + _player.Source = MediaSource.CreateFromUri(uri); + Debug.WriteLine($"[RadioPlayerService] Created new MediaSource from URL: {_streamUrl}"); + } + else + { + Debug.WriteLine($"[RadioPlayerService] Player.Source exists, current state: {(_player.Source as MediaSource)?.State}"); } - - Debug.WriteLine("[RadioPlayerService] Creating fresh MediaSource for live stream"); - Uri uri = new(_streamUrl); - _player.Source = MediaSource.CreateFromUri(uri); - Debug.WriteLine($"[RadioPlayerService] Created new MediaSource from URL: {_streamUrl}"); Debug.WriteLine("[RadioPlayerService] Calling _player.Play()..."); _isInternalStateChange = true; @@ -362,6 +359,9 @@ public void Play() _isInternalStateChange = false; Debug.WriteLine("[RadioPlayerService] _player.Play() called successfully"); + // For live streams, seek to the live position to avoid playing from buffered content + SeekToLive(); + _watchdog.NotifyUserIntentionToPlay(); Debug.WriteLine("[RadioPlayerService] Notified watchdog of user intention to play"); } @@ -383,6 +383,8 @@ public void Play() _isInternalStateChange = false; Debug.WriteLine("[RadioPlayerService] _player.Play() called successfully (retry)"); + SeekToLive(); + _watchdog.NotifyUserIntentionToPlay(); Debug.WriteLine("[RadioPlayerService] Notified watchdog of user intention to play"); } @@ -398,38 +400,58 @@ public void Play() } /// - /// Refreshes the stream to ensure live playback. + /// Seeks the player to the live position. + /// For live streams, this moves the playback position to the end of the seekable range, + /// ensuring we're streaming live content rather than buffered content from an earlier time. + /// + private void SeekToLive() + { + try + { + MediaPlaybackSession session = _player.PlaybackSession; + + // Check if there's a seekable range (for live streams, this represents the buffer) + if (session.IsSeekable && session.NaturalDuration.TotalSeconds > 0) + { + // Seek to the end of the buffer (live edge) + TimeSpan livePosition = session.NaturalDuration; + Debug.WriteLine($"[RadioPlayerService] Seeking to live position: {livePosition}"); + session.Position = livePosition; + } + else + { + Debug.WriteLine("[RadioPlayerService] Stream is not seekable or has no duration, skipping seek to live"); + } + } + catch (Exception ex) + { + // Seeking to live is a best-effort operation - don't fail playback if it doesn't work + Debug.WriteLine($"[RadioPlayerService] Failed to seek to live: {ex.Message}"); + } + } + + /// + /// Seeks the player to the live position when resuming from external controls. /// Used when external controls (like hardware buttons) resume playback, - /// to ensure we're always streaming live content instead of resuming from a buffered position. + /// to ensure we're streaming live content instead of resuming from a buffered position. /// - private void RefreshStreamForLivePlayback() + private void SeekToLiveOnResume() { - Debug.WriteLine("=== RefreshStreamForLivePlayback START ==="); + Debug.WriteLine("=== SeekToLiveOnResume START ==="); if (string.IsNullOrWhiteSpace(_streamUrl)) { - Debug.WriteLine("[RadioPlayerService] No stream URL set, skipping refresh"); - Debug.WriteLine("=== RefreshStreamForLivePlayback END ==="); + Debug.WriteLine("[RadioPlayerService] No stream URL set, skipping seek to live"); + Debug.WriteLine("=== SeekToLiveOnResume END ==="); return; } - // Play() already creates a fresh MediaSource to ensure live streaming, - // so we just delegate to it to avoid code duplication. - // Exceptions are caught here because this method is called from event handlers - // where unhandled exceptions could crash the application. The watchdog service - // will attempt recovery if the stream fails to start. - try - { - Play(); - Debug.WriteLine("[RadioPlayerService] Stream refreshed for live playback via Play()"); - } - catch (Exception ex) - { - Debug.WriteLine($"[RadioPlayerService] EXCEPTION in RefreshStreamForLivePlayback: {ex.Message}"); - Debug.WriteLine($"[RadioPlayerService] The watchdog service will attempt recovery if enabled"); - } + // Just seek to the live position - no need to recreate the MediaSource + SeekToLive(); + _watchdog.NotifyUserIntentionToPlay(); + Debug.WriteLine("[RadioPlayerService] Notified watchdog of user intention to play"); - Debug.WriteLine("=== RefreshStreamForLivePlayback END ==="); + Debug.WriteLine("=== SeekToLiveOnResume END ==="); } /// From d2ce2b50ad285eb132e3e702958783bd17e15979 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Tue, 25 Nov 2025 15:29:52 -0600 Subject: [PATCH 6/6] Fix the SeekToLive position setting logic getting called, but it still doesn't work --- Trdo/Services/RadioPlayerService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Trdo/Services/RadioPlayerService.cs b/Trdo/Services/RadioPlayerService.cs index 7fe0ad6..b582a48 100644 --- a/Trdo/Services/RadioPlayerService.cs +++ b/Trdo/Services/RadioPlayerService.cs @@ -411,7 +411,7 @@ private void SeekToLive() MediaPlaybackSession session = _player.PlaybackSession; // Check if there's a seekable range (for live streams, this represents the buffer) - if (session.IsSeekable && session.NaturalDuration.TotalSeconds > 0) + if (session.CanSeek) { // Seek to the end of the buffer (live edge) TimeSpan livePosition = session.NaturalDuration;