diff --git a/Trdo/Controls/TutorialWindow.xaml b/Trdo/Controls/TutorialWindow.xaml
index fe4d4f2..66d58f3 100644
--- a/Trdo/Controls/TutorialWindow.xaml
+++ b/Trdo/Controls/TutorialWindow.xaml
@@ -30,7 +30,7 @@
-
+
-
+
+ Version="1.7.0.0" />
diff --git a/Trdo/Pages/AboutPage.xaml b/Trdo/Pages/AboutPage.xaml
index 8cc1ff6..8dcf6be 100644
--- a/Trdo/Pages/AboutPage.xaml
+++ b/Trdo/Pages/AboutPage.xaml
@@ -17,7 +17,7 @@
Text="About" />
-
+
-
@@ -183,103 +262,97 @@
TextWrapping="Wrap" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Trdo/Pages/AboutPage.xaml.cs b/Trdo/Pages/AboutPage.xaml.cs
index ea0e9b0..6558e63 100644
--- a/Trdo/Pages/AboutPage.xaml.cs
+++ b/Trdo/Pages/AboutPage.xaml.cs
@@ -31,6 +31,45 @@ private void ReviewButton_Click(object sender, RoutedEventArgs e)
ViewModel.OpenRatingWindow();
}
+ private void Star_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button button && button.Tag is string tagString && int.TryParse(tagString, out int rating))
+ {
+ ViewModel.SelectedRating = rating;
+ UpdateStarVisuals(rating);
+
+ // If 5 stars, immediately launch the store review
+ if (rating >= 4)
+ {
+ _ = ViewModel.OpenRatingWindow();
+ }
+ }
+ }
+
+ private void UpdateStarVisuals(int selectedRating)
+ {
+ // Update all star buttons to show filled or outline based on rating
+ UpdateStarButton("Star1", selectedRating >= 1);
+ UpdateStarButton("Star2", selectedRating >= 2);
+ UpdateStarButton("Star3", selectedRating >= 3);
+ UpdateStarButton("Star4", selectedRating >= 4);
+ UpdateStarButton("Star5", selectedRating >= 5);
+ }
+
+ private void UpdateStarButton(string buttonName, bool isFilled)
+ {
+ if (FindName(buttonName) is Button starButton && starButton.Content is FontIcon icon)
+ {
+ // E735 is filled star, E734 is outline star
+ icon.Glyph = isFilled ? "\uE735" : "\uE734";
+ }
+ }
+
+ private void ContactDeveloperButton_Click(object sender, RoutedEventArgs e)
+ {
+ _ = ViewModel.ContactDeveloper();
+ }
+
private void TutorialButton_Click(object sender, RoutedEventArgs e)
{
TutorialWindow tutorialWindow = new();
diff --git a/Trdo/Pages/AddStation.xaml b/Trdo/Pages/AddStation.xaml
index 799ddd1..05d25e5 100644
--- a/Trdo/Pages/AddStation.xaml
+++ b/Trdo/Pages/AddStation.xaml
@@ -17,46 +17,48 @@
Text="{x:Bind ViewModel.PageTitle, Mode=OneWay}" />
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
+
-
+
diff --git a/Trdo/Pages/FavoritesPage.xaml b/Trdo/Pages/FavoritesPage.xaml
index 3373011..591b8c1 100644
--- a/Trdo/Pages/FavoritesPage.xaml
+++ b/Trdo/Pages/FavoritesPage.xaml
@@ -21,7 +21,7 @@
Grid.Row="0"
FontSize="16"
FontWeight="SemiBold"
- Text="Favorites" />
+ Text="Favorite Songs" />
@@ -41,13 +41,13 @@
HorizontalAlignment="Center"
FontSize="16"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
- Text="No favorites yet"
+ Text="No favorite songs yet"
TextAlignment="Center" />
diff --git a/Trdo/Pages/NowPlayingPage.xaml b/Trdo/Pages/NowPlayingPage.xaml
index 7e483e7..ee57468 100644
--- a/Trdo/Pages/NowPlayingPage.xaml
+++ b/Trdo/Pages/NowPlayingPage.xaml
@@ -88,27 +88,27 @@
@@ -121,7 +121,7 @@
Background="Transparent"
BorderThickness="0"
Click="FavoriteCurrentTrack_Click"
- ToolTipService.ToolTip="Toggle favorite">
+ ToolTipService.ToolTip="Favorite Song">
+ ToolTipService.ToolTip="Favorite Song">
@@ -46,7 +47,7 @@
Background="Transparent"
BorderBrush="Transparent"
Click="FavoritesButton_Click"
- ToolTipService.ToolTip="Favorites">
+ ToolTipService.ToolTip="Favorite Songs">
@@ -91,6 +92,7 @@
CanReorderItems="True"
DragItemsCompleted="StationsListView_DragItemsCompleted"
ItemsSource="{x:Bind ViewModel.Stations, Mode=OneWay}"
+ SelectedItem="{x:Bind ViewModel.SelectedStation, Mode=TwoWay}"
SelectionChanged="StationsListView_SelectionChanged"
SelectionMode="Single">
@@ -266,6 +268,17 @@
FontSize="14"
Foreground="{ThemeResource AccentFillColorDefaultBrush}"
Glyph="" />
+
+ TextWrapping="NoWrap"
+ Visibility="Collapsed" />
+ ToolTipService.ToolTip="Favorite Song">
+ Severity="Error" />
-
+
@@ -91,12 +91,8 @@
Height="40"
VerticalAlignment="Top"
Visibility="{x:Bind Favicon, Converter={StaticResource NullToVisibilityConverter}, Mode=OneWay}">
-
-
+
+
@@ -142,17 +138,17 @@
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/Trdo/Pages/SettingsPage.xaml b/Trdo/Pages/SettingsPage.xaml
index 642b893..c26cf1a 100644
--- a/Trdo/Pages/SettingsPage.xaml
+++ b/Trdo/Pages/SettingsPage.xaml
@@ -16,7 +16,7 @@
Text="Settings" />
-
+
@@ -56,7 +56,57 @@
Text="{x:Bind ViewModel.WatchdogToggleText, Mode=OneWay}" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/Trdo/Services/RadioPlayerService.cs b/Trdo/Services/RadioPlayerService.cs
index afcb01c..a081d2e 100644
--- a/Trdo/Services/RadioPlayerService.cs
+++ b/Trdo/Services/RadioPlayerService.cs
@@ -1,7 +1,9 @@
using Microsoft.UI.Dispatching;
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
+using System.Threading;
using System.Threading.Tasks;
using Trdo.Models;
using Windows.Media;
@@ -35,6 +37,7 @@ public sealed partial class RadioPlayerService : IDisposable
private System.Threading.Timer? _internalStateChangeTimer;
private DateTime _lastExternalPauseRecovery = DateTime.MinValue;
private bool _hasPlayedOnce;
+ private bool _isManuallyBuffering;
public static RadioPlayerService Instance { get; } = new();
@@ -60,13 +63,14 @@ public bool IsBuffering
try
{
MediaPlaybackState state = _player.PlaybackSession.PlaybackState;
- bool isBuffering = state is MediaPlaybackState.Opening or MediaPlaybackState.Buffering;
- Debug.WriteLine($"[RadioPlayerService] IsBuffering getter: {isBuffering}, PlaybackState: {state}");
+ bool isPlayerBuffering = state is MediaPlaybackState.Opening or MediaPlaybackState.Buffering;
+ bool isBuffering = isPlayerBuffering || _isManuallyBuffering;
+ Debug.WriteLine($"[RadioPlayerService] IsBuffering getter: {isBuffering} (Player: {isPlayerBuffering}, Manual: {_isManuallyBuffering}), PlaybackState: {state}");
return isBuffering;
}
catch
{
- return false;
+ return _isManuallyBuffering;
}
}
}
@@ -118,6 +122,69 @@ public TimeSpan Position
}
}
+ ///
+ /// Gets the total buffered duration using MediaPlaybackSession.GetBufferedRanges.
+ /// Returns the sum of all buffered time ranges.
+ ///
+ public TimeSpan TotalBufferedDuration
+ {
+ get
+ {
+ try
+ {
+ var bufferedRanges = _player.PlaybackSession.GetBufferedRanges();
+ TimeSpan totalBuffered = TimeSpan.Zero;
+ foreach (var range in bufferedRanges)
+ {
+ totalBuffered += range.End - range.Start;
+ }
+ return totalBuffered;
+ }
+ catch
+ {
+ return TimeSpan.Zero;
+ }
+ }
+ }
+
+ ///
+ /// Gets the buffered ranges from the playback session.
+ /// Each MediaTimeRange contains Start and End times representing buffered content.
+ ///
+ public IReadOnlyList GetBufferedRanges()
+ {
+ try
+ {
+ return _player.PlaybackSession.GetBufferedRanges();
+ }
+ catch
+ {
+ return Array.Empty();
+ }
+ }
+
+ ///
+ /// Gets the minimum buffer duration required based on current buffer level setting.
+ ///
+ public TimeSpan RequiredBufferDuration => TimeSpan.FromMilliseconds(_watchdog.BufferDelayMs);
+
+ ///
+ /// Checks if sufficient buffer is available for smooth playback based on current buffer level.
+ ///
+ public bool HasSufficientBuffer
+ {
+ get
+ {
+ TimeSpan required = RequiredBufferDuration;
+ if (required == TimeSpan.Zero)
+ {
+ // Default level - no minimum buffer required
+ return true;
+ }
+ return TotalBufferedDuration >= required;
+ }
+ }
+
public StreamWatchdogService Watchdog => _watchdog;
///
@@ -429,7 +496,8 @@ public void SetStationFavicon(string? faviconUrl)
}
///
- /// Start playback of the current stream
+ /// Start playback of the current stream.
+ /// Always applies buffer settings based on current buffer level using GetBufferedRanges.
///
public void Play()
{
@@ -440,6 +508,7 @@ public void Play()
Debug.WriteLine($"[RadioPlayerService] Player.Source is null: {_player.Source == null}");
Debug.WriteLine($"[RadioPlayerService] Has played once: {_hasPlayedOnce}");
Debug.WriteLine($"[RadioPlayerService] Was external pause: {_wasExternalPause}");
+ Debug.WriteLine($"[RadioPlayerService] Required buffer: {RequiredBufferDuration.TotalMilliseconds}ms");
if (string.IsNullOrWhiteSpace(_streamUrl))
{
@@ -447,6 +516,20 @@ public void Play()
throw new InvalidOperationException("No stream URL set. Call SetStreamUrl first.");
}
+ // Always use buffer-aware playback to apply buffer settings
+ // Fire and forget - PlayWithBufferAsync handles everything including buffering
+ _ = PlayWithBufferInternalAsync();
+
+ Debug.WriteLine($"=== Play END ===");
+ }
+
+ ///
+ /// Internal method that handles buffered playback asynchronously.
+ /// This is called by Play() to apply buffer settings every time playback starts.
+ /// The stream is paused during buffering, then resumed once sufficient buffer is achieved.
+ ///
+ private async Task PlayWithBufferInternalAsync()
+ {
try
{
// Only recreate MediaSource if:
@@ -474,7 +557,7 @@ public void Play()
}
// Create fresh MediaSource
- Uri uri = new(_streamUrl);
+ Uri uri = new(_streamUrl!);
_player.Source = MediaSource.CreateFromUri(uri);
Debug.WriteLine($"[RadioPlayerService] Created new MediaSource from URL: {_streamUrl}");
@@ -485,10 +568,59 @@ public void Play()
Debug.WriteLine("[RadioPlayerService] Reusing existing MediaSource - resuming playback");
}
- Debug.WriteLine("[RadioPlayerService] Calling _player.Play()...");
- SetInternalStateChange(true);
- _player.Play();
- Debug.WriteLine("[RadioPlayerService] _player.Play() called successfully");
+ // Check if we need to buffer before playing
+ bool needsBuffering = RequiredBufferDuration > TimeSpan.Zero;
+
+ if (needsBuffering)
+ {
+ // Start playback briefly to initiate buffering
+ Debug.WriteLine("[RadioPlayerService] Starting playback to initiate buffering...");
+ SetInternalStateChange(true);
+ _player.Play();
+
+ // Small delay to ensure play command is processed before pausing
+ await Task.Delay(100);
+
+ // Pause to prevent audio from playing while we buffer
+ Debug.WriteLine("[RadioPlayerService] Pausing for buffering...");
+ _player.Pause();
+
+ // Set manual buffering state so UI shows buffering during user-configured delay
+ SetManualBuffering(true);
+ Debug.WriteLine("[RadioPlayerService] Manual buffering state set to true");
+
+ // Wait for the user-set buffer amount of time
+ Debug.WriteLine($"[RadioPlayerService] Waiting for buffer time: {RequiredBufferDuration.TotalMilliseconds}ms...");
+ await Task.Delay(RequiredBufferDuration);
+
+ // Check if buffer is complete using GetBufferedRanges
+ TimeSpan bufferedDuration = TotalBufferedDuration;
+ Debug.WriteLine($"[RadioPlayerService] After wait - Buffered: {bufferedDuration.TotalMilliseconds}ms, Required: {RequiredBufferDuration.TotalMilliseconds}ms");
+
+ // If buffer is not yet sufficient, wait a bit more
+ if (bufferedDuration < RequiredBufferDuration)
+ {
+ Debug.WriteLine("[RadioPlayerService] Buffer not yet complete, waiting more...");
+ await WaitForSufficientBufferAsync();
+ }
+
+ // Now resume playback
+ Debug.WriteLine("[RadioPlayerService] Buffer complete. Calling _player.Play()...");
+ _player.Play();
+
+ // Clear manual buffering state as playback is resuming
+ SetManualBuffering(false);
+ Debug.WriteLine("[RadioPlayerService] Manual buffering state cleared");
+ Debug.WriteLine("[RadioPlayerService] Playback resumed after buffering");
+ }
+ else
+ {
+ // No buffering needed - start playback immediately
+ Debug.WriteLine("[RadioPlayerService] No buffering required (default level). Starting playback...");
+ SetInternalStateChange(true);
+ _player.Play();
+ Debug.WriteLine("[RadioPlayerService] _player.Play() called successfully");
+ }
// Clear the external pause flag
_wasExternalPause = false;
@@ -498,25 +630,57 @@ public void Play()
Debug.WriteLine("[RadioPlayerService] Notified watchdog of user intention to play");
// Start metadata polling
- _metadataService.StartPolling(_streamUrl);
+ _metadataService.StartPolling(_streamUrl!);
Debug.WriteLine("[RadioPlayerService] Started metadata polling");
+
+ // Log final buffer state
+ LogBufferedRanges();
}
catch (Exception ex)
{
- Debug.WriteLine($"[RadioPlayerService] EXCEPTION in Play: {ex.Message}");
+ Debug.WriteLine($"[RadioPlayerService] EXCEPTION in PlayWithBufferInternalAsync: {ex.Message}");
Debug.WriteLine($"[RadioPlayerService] Exception details: {ex}");
+
+ // Clear manual buffering state on error
+ SetManualBuffering(false);
+ Debug.WriteLine("[RadioPlayerService] Cleared manual buffering state due to exception");
+
Debug.WriteLine("[RadioPlayerService] Re-creating media source and trying again...");
- // Re-create the media source and try again
+ // Re-create the media source and try again with same buffering approach
try
{
- Uri uri = new(_streamUrl);
+ Uri uri = new(_streamUrl!);
_player.Source = MediaSource.CreateFromUri(uri);
Debug.WriteLine($"[RadioPlayerService] Created new MediaSource from URL: {_streamUrl}");
- SetInternalStateChange(true);
- _player.Play();
- Debug.WriteLine("[RadioPlayerService] _player.Play() called successfully (retry)");
+ // Apply same buffering logic on retry
+ bool needsBuffering = RequiredBufferDuration > TimeSpan.Zero;
+
+ if (needsBuffering)
+ {
+ SetInternalStateChange(true);
+ _player.Play();
+ _player.Pause();
+
+ // Set manual buffering state for retry
+ SetManualBuffering(true);
+ Debug.WriteLine("[RadioPlayerService] Started and paused for buffering (retry), manual buffering set");
+
+ await Task.Delay(RequiredBufferDuration);
+
+ _player.Play();
+
+ // Clear manual buffering state after retry
+ SetManualBuffering(false);
+ Debug.WriteLine("[RadioPlayerService] Playback resumed after buffering (retry), manual buffering cleared");
+ }
+ else
+ {
+ SetInternalStateChange(true);
+ _player.Play();
+ Debug.WriteLine("[RadioPlayerService] _player.Play() called successfully (retry)");
+ }
_wasExternalPause = false;
_hasPlayedOnce = true;
@@ -526,24 +690,286 @@ public void Play()
Debug.WriteLine("[RadioPlayerService] Notified watchdog of user intention to play");
// Start metadata polling
- _metadataService.StartPolling(_streamUrl);
+ _metadataService.StartPolling(_streamUrl!);
Debug.WriteLine("[RadioPlayerService] Started metadata polling (retry)");
}
catch (Exception retryEx)
{
SetInternalStateChange(false);
+
+ // Clear manual buffering state on retry failure
+ SetManualBuffering(false);
+ Debug.WriteLine("[RadioPlayerService] Cleared manual buffering state due to retry exception");
+
Debug.WriteLine($"[RadioPlayerService] EXCEPTION on retry: {retryEx.Message}");
- throw;
+ // Log the error but don't throw - this is a fire-and-forget async method
+ // The user will see the playback failed in the UI
}
}
+ }
- Debug.WriteLine($"=== Play END ===");
+ ///
+ /// Waits for sufficient buffer based on current buffer level settings.
+ /// Uses MediaPlaybackSession.GetBufferedRanges to monitor buffering progress.
+ ///
+ /// Optional cancellation token
+ private async Task WaitForSufficientBufferAsync(CancellationToken cancellationToken = default)
+ {
+ // Calculate timeout based on required buffer (minimum 10s, max 30s)
+ int timeoutMs = Math.Clamp((int)RequiredBufferDuration.TotalMilliseconds * 3, 10000, 30000);
+ const int checkIntervalMs = 250; // Check every 250ms
+ int elapsed = 0;
+
+ Debug.WriteLine($"[RadioPlayerService] Waiting for {RequiredBufferDuration.TotalMilliseconds}ms of buffer (timeout: {timeoutMs}ms)...");
+
+ while (elapsed < timeoutMs)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ Debug.WriteLine("[RadioPlayerService] Buffer wait cancelled");
+ return;
+ }
+
+ // Check if stream is fully buffered (BufferingProgress >= 1.0)
+ if (BufferingProgress >= 1.0)
+ {
+ Debug.WriteLine($"[RadioPlayerService] Stream fully buffered (BufferingProgress: {BufferingProgress:P0})");
+ return;
+ }
+
+ // Check buffered ranges using GetBufferedRanges
+ TimeSpan bufferedDuration = TotalBufferedDuration;
+
+ if (bufferedDuration >= RequiredBufferDuration)
+ {
+ Debug.WriteLine($"[RadioPlayerService] Sufficient buffer achieved: {bufferedDuration.TotalMilliseconds}ms >= {RequiredBufferDuration.TotalMilliseconds}ms");
+ return;
+ }
+
+ try
+ {
+ await Task.Delay(checkIntervalMs, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ Debug.WriteLine("[RadioPlayerService] Buffer wait cancelled during delay");
+ return;
+ }
+ elapsed += checkIntervalMs;
+ }
+
+ Debug.WriteLine($"[RadioPlayerService] Buffer wait timeout after {timeoutMs}ms. Current buffer: {TotalBufferedDuration.TotalMilliseconds}ms");
}
///
- /// Stop playback and clean up resources
+ /// Starts playback and waits for sufficient buffer based on current buffer level setting.
+ /// Uses MediaPlaybackSession.GetBufferedRanges to monitor buffering progress.
+ /// The stream is paused during buffering, then resumed once sufficient buffer is achieved.
+ /// This method is primarily used by the watchdog for recovery scenarios that need cancellation support.
///
- public void Pause()
+ /// Cancellation token to cancel waiting
+ /// True if playback started with sufficient buffer, false if cancelled or timeout
+ public async Task PlayWithBufferAsync(CancellationToken cancellationToken = default)
+ {
+ Debug.WriteLine($"=== PlayWithBufferAsync START ===");
+ Debug.WriteLine($"[RadioPlayerService] Required buffer duration: {RequiredBufferDuration.TotalMilliseconds}ms");
+
+ if (string.IsNullOrWhiteSpace(_streamUrl))
+ {
+ Debug.WriteLine("[RadioPlayerService] ERROR: No stream URL set");
+ throw new InvalidOperationException("No stream URL set. Call SetStreamUrl first.");
+ }
+
+ // Check if we need to buffer before playing
+ bool needsBuffering = RequiredBufferDuration > TimeSpan.Zero;
+
+ try
+ {
+ // Start playback (this also initiates buffer monitoring)
+ // We need to do the core playback logic here to support cancellation
+ bool needsRecreation = (!_hasPlayedOnce && _player.Source == null) || _wasExternalPause;
+
+ if (needsRecreation)
+ {
+ if (_player.Source is MediaSource oldMedia)
+ {
+ oldMedia.Reset();
+ oldMedia.Dispose();
+ }
+
+ Uri uri = new(_streamUrl!);
+ _player.Source = MediaSource.CreateFromUri(uri);
+ _hasPlayedOnce = true;
+ }
+
+ if (needsBuffering)
+ {
+ // mute initially so there isn't a blip during the initiate step
+ double lastVol = _player.Volume;
+ _player.Volume = 0;
+
+ // Start playback briefly to initiate buffering
+ Debug.WriteLine("[RadioPlayerService] Starting playback to initiate buffering...");
+ SetInternalStateChange(true);
+ _player.Play();
+
+ // Small delay to ensure play command is processed before pausing
+ await Task.Delay(100, cancellationToken);
+
+ // Pause to prevent audio from playing while we buffer
+ Debug.WriteLine("[RadioPlayerService] Pausing for buffering...");
+ _player.Pause();
+ _player.Volume = lastVol;
+
+ // Set manual buffering state so UI shows buffering during user-configured delay
+ SetManualBuffering(true);
+ Debug.WriteLine("[RadioPlayerService] Manual buffering state set to true");
+
+ // Wait for the user-set buffer amount of time
+ Debug.WriteLine($"[RadioPlayerService] Waiting for buffer time: {RequiredBufferDuration.TotalMilliseconds}ms...");
+ try
+ {
+ await Task.Delay(RequiredBufferDuration, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ Debug.WriteLine("[RadioPlayerService] Buffer wait cancelled during initial delay");
+ SetManualBuffering(false);
+ return false;
+ }
+
+ // Check if buffer is complete using GetBufferedRanges
+ TimeSpan bufferedDuration = TotalBufferedDuration;
+ Debug.WriteLine($"[RadioPlayerService] After wait - Buffered: {bufferedDuration.TotalMilliseconds}ms, Required: {RequiredBufferDuration.TotalMilliseconds}ms");
+
+ // If buffer is not yet sufficient, wait more with timeout
+ if (bufferedDuration < RequiredBufferDuration)
+ {
+ Debug.WriteLine("[RadioPlayerService] Buffer not yet complete, waiting more...");
+ int additionalTimeoutMs = Math.Clamp((int)RequiredBufferDuration.TotalMilliseconds * 2, 5000, 20000);
+ const int checkIntervalMs = 250;
+ int elapsed = 0;
+
+ while (elapsed < additionalTimeoutMs)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ Debug.WriteLine("[RadioPlayerService] Buffer wait cancelled");
+ SetManualBuffering(false);
+ return false;
+ }
+
+ if (BufferingProgress >= 1.0)
+ {
+ Debug.WriteLine($"[RadioPlayerService] Stream fully buffered (BufferingProgress: {BufferingProgress:P0})");
+ break;
+ }
+
+ bufferedDuration = TotalBufferedDuration;
+ if (bufferedDuration >= RequiredBufferDuration)
+ {
+ Debug.WriteLine($"[RadioPlayerService] Sufficient buffer achieved: {bufferedDuration.TotalMilliseconds}ms >= {RequiredBufferDuration.TotalMilliseconds}ms");
+ break;
+ }
+
+ await Task.Delay(checkIntervalMs, cancellationToken);
+ elapsed += checkIntervalMs;
+ }
+ }
+
+ // Now resume playback
+ Debug.WriteLine("[RadioPlayerService] Buffer complete. Calling _player.Play()...");
+ _player.Play();
+
+ // Clear manual buffering state as playback is resuming
+ SetManualBuffering(false);
+ Debug.WriteLine("[RadioPlayerService] Manual buffering state cleared");
+ Debug.WriteLine("[RadioPlayerService] Playback resumed after buffering");
+ }
+ else
+ {
+ // No buffering needed - start playback immediately
+ Debug.WriteLine("[RadioPlayerService] No buffering required (default level). Starting playback...");
+ SetInternalStateChange(true);
+ _player.Play();
+ }
+
+ _wasExternalPause = false;
+ _watchdog.NotifyUserIntentionToPlay();
+ _metadataService.StartPolling(_streamUrl!);
+
+ LogBufferedRanges();
+ return true;
+ }
+ catch (OperationCanceledException)
+ {
+ Debug.WriteLine("[RadioPlayerService] PlayWithBufferAsync cancelled");
+ SetManualBuffering(false);
+ return false;
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"[RadioPlayerService] EXCEPTION in PlayWithBufferAsync: {ex.Message}");
+ SetManualBuffering(false);
+ throw;
+ }
+ finally
+ {
+ Debug.WriteLine($"=== PlayWithBufferAsync END ===");
+ }
+ }
+
+ ///
+ /// Logs the current buffered ranges for debugging purposes.
+ ///
+ private void LogBufferedRanges()
+ {
+ try
+ {
+ var bufferedRanges = _player.PlaybackSession.GetBufferedRanges();
+ if (bufferedRanges.Count == 0)
+ {
+ Debug.WriteLine("[RadioPlayerService] No buffered ranges available");
+ return;
+ }
+
+ Debug.WriteLine($"[RadioPlayerService] Buffered ranges ({bufferedRanges.Count}):");
+ TimeSpan totalBuffered = TimeSpan.Zero;
+ foreach (var range in bufferedRanges)
+ {
+ var duration = range.End - range.Start;
+ totalBuffered += duration;
+ Debug.WriteLine($" - {range.Start.TotalSeconds:F2}s to {range.End.TotalSeconds:F2}s (duration: {duration.TotalMilliseconds:F0}ms)");
+ }
+ Debug.WriteLine($"[RadioPlayerService] Total buffered: {totalBuffered.TotalMilliseconds:F0}ms");
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"[RadioPlayerService] Error logging buffered ranges: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Sets the manual buffering state and notifies listeners.
+ ///
+ private void SetManualBuffering(bool isBuffering)
+ {
+ if (_isManuallyBuffering == isBuffering) return;
+
+ Debug.WriteLine($"[RadioPlayerService] Manual buffering state changing from {_isManuallyBuffering} to {isBuffering}");
+ _isManuallyBuffering = isBuffering;
+
+ // Notify on UI thread
+ TryEnqueueOnUi(() =>
+ {
+ BufferingStateChanged?.Invoke(this, IsBuffering);
+ });
+ }
+
+ ///
+ /// Stop playback and clean up resources
+ ///
+ public void Pause()
{
Debug.WriteLine($"=== Pause START ===");
Debug.WriteLine($"[RadioPlayerService] Pause called");
@@ -564,6 +990,10 @@ public void Pause()
_player.Pause();
Debug.WriteLine("[RadioPlayerService] _player.Pause() called successfully");
+ // Clear manual buffering state when pausing
+ SetManualBuffering(false);
+ Debug.WriteLine("[RadioPlayerService] Cleared manual buffering state");
+
// Mark that pause occurred - next play should recreate MediaSource to ensure live position
_wasExternalPause = true;
Debug.WriteLine("[RadioPlayerService] Marked for MediaSource recreation on next play (ensures live position)");
diff --git a/Trdo/Services/StreamWatchdogService.cs b/Trdo/Services/StreamWatchdogService.cs
index fca538d..31c5614 100644
--- a/Trdo/Services/StreamWatchdogService.cs
+++ b/Trdo/Services/StreamWatchdogService.cs
@@ -1,13 +1,16 @@
using Microsoft.UI.Dispatching;
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
+using Windows.Storage;
namespace Trdo.Services;
///
/// Monitors the radio stream and automatically resumes playback when the stream stops unexpectedly.
+/// Also tracks stutter patterns and can automatically increase buffer when stuttering is detected.
///
public sealed class StreamWatchdogService : IDisposable
{
@@ -24,6 +27,13 @@ public sealed class StreamWatchdogService : IDisposable
private double _lastBufferingProgress;
private int _consecutiveSilentChecks;
+ // Stutter detection tracking
+ private readonly Queue _recoveryAttempts = new();
+ private bool _autoBufferIncreaseEnabled;
+ private double _currentBufferLevel;
+ private const string AutoBufferIncreaseKey = "AutoBufferIncreaseEnabled";
+ private const string BufferLevelKey = "BufferLevel";
+
// Configuration
private readonly TimeSpan _checkInterval = TimeSpan.FromSeconds(5);
private readonly TimeSpan _recoveryDelay = TimeSpan.FromSeconds(3);
@@ -32,7 +42,16 @@ public sealed class StreamWatchdogService : IDisposable
private readonly TimeSpan _silenceDetectionThreshold = TimeSpan.FromSeconds(10);
private readonly int _maxConsecutiveSilentChecks = 2; // 2 checks * 5 seconds = 10 seconds
+ // Stutter detection configuration
+ private const int StutterThreshold = 3; // Number of recovery attempts to trigger stutter detection
+ private readonly TimeSpan _stutterWindow = TimeSpan.FromMinutes(2); // Time window to count recovery attempts
+ private const double MaxBufferLevel = 3.0; // Maximum buffer level (0=default, 1=medium, 2=large, 3=extra large)
+ private const bool DefaultAutoBufferIncreaseEnabled = true; // Auto-buffer increase is enabled by default for better user experience
+ private const double DefaultBufferLevel = 0.0; // Start with default (no extra delay) buffer level
+
public event EventHandler? StreamStatusChanged;
+ public event EventHandler? StutterDetected;
+ public event EventHandler? BufferLevelChanged;
public bool IsEnabled
{
@@ -49,6 +68,79 @@ public bool IsEnabled
}
}
+ ///
+ /// Gets or sets whether auto-buffer increase is enabled.
+ /// When enabled, the buffer level automatically increases when stutter is detected.
+ ///
+ public bool AutoBufferIncreaseEnabled
+ {
+ get => _autoBufferIncreaseEnabled;
+ set
+ {
+ if (_autoBufferIncreaseEnabled == value) return;
+ _autoBufferIncreaseEnabled = value;
+ SaveAutoBufferSettings();
+ Debug.WriteLine($"[Watchdog] Auto-buffer increase set to: {value}");
+ }
+ }
+
+ ///
+ /// Gets or sets the current buffer level (0-3).
+ /// 0 = Default, 1 = Medium, 2 = Large, 3 = Extra Large
+ ///
+ public double BufferLevel
+ {
+ get => _currentBufferLevel;
+ set
+ {
+ double clampedValue = Math.Clamp(value, 0, MaxBufferLevel);
+ if (Math.Abs(_currentBufferLevel - clampedValue) < 0.0001) return;
+ double oldValue = _currentBufferLevel;
+ _currentBufferLevel = clampedValue;
+ SaveAutoBufferSettings();
+ Debug.WriteLine($"[Watchdog] Buffer level changed from {oldValue} to {_currentBufferLevel}");
+ RaiseBufferLevelChanged(clampedValue);
+ }
+ }
+
+ ///
+ /// Gets the buffer delay in milliseconds based on current buffer level.
+ ///
+ public int BufferDelayMs
+ {
+ get
+ {
+ // Linear interpolation between buffer levels
+ // Level 0 = 0ms, Level 1 = 2000ms, Level 2 = 4000ms, Level 3 = 8000ms
+ if (_currentBufferLevel <= 0) return 0;
+ if (_currentBufferLevel >= 3) return 8000;
+ if (_currentBufferLevel <= 1) return (int)(_currentBufferLevel * 2000);
+ if (_currentBufferLevel <= 2) return (int)(2000 + (_currentBufferLevel - 1) * 2000);
+ return (int)(4000 + (_currentBufferLevel - 2) * 4000);
+ }
+ }
+
+ ///
+ /// Gets a human-readable description of the current buffer level.
+ ///
+ public string BufferLevelDescription
+ {
+ get
+ {
+ return _currentBufferLevel switch
+ {
+ 0 => "Default",
+ 1 => "Medium",
+ 2 => "Large",
+ 3 => "Extra Large",
+ _ when _currentBufferLevel < 0.5 => "Default",
+ _ when _currentBufferLevel < 1.5 => "Medium",
+ _ when _currentBufferLevel < 2.5 => "Large",
+ _ => "Extra Large"
+ };
+ }
+ }
+
public StreamWatchdogService(RadioPlayerService playerService)
{
_playerService = playerService ?? throw new ArgumentNullException(nameof(playerService));
@@ -59,6 +151,66 @@ public StreamWatchdogService(RadioPlayerService playerService)
_lastPositionChangeTime = DateTime.UtcNow;
_lastBufferingProgress = 0;
_consecutiveSilentChecks = 0;
+
+ // Load auto-buffer settings
+ LoadAutoBufferSettings();
+ }
+
+ private void LoadAutoBufferSettings()
+ {
+ try
+ {
+ if (ApplicationData.Current.LocalSettings.Values.TryGetValue(AutoBufferIncreaseKey, out object? autoBufferValue))
+ {
+ _autoBufferIncreaseEnabled = autoBufferValue switch
+ {
+ bool b => b,
+ string s when bool.TryParse(s, out bool b2) => b2,
+ _ => DefaultAutoBufferIncreaseEnabled
+ };
+ }
+ else
+ {
+ _autoBufferIncreaseEnabled = DefaultAutoBufferIncreaseEnabled;
+ }
+
+ if (ApplicationData.Current.LocalSettings.Values.TryGetValue(BufferLevelKey, out object? bufferLevelValue))
+ {
+ _currentBufferLevel = bufferLevelValue switch
+ {
+ double d => Math.Clamp(d, 0, MaxBufferLevel),
+ int i => Math.Clamp((double)i, 0, MaxBufferLevel),
+ string s when double.TryParse(s, out double d2) => Math.Clamp(d2, 0, MaxBufferLevel),
+ _ => DefaultBufferLevel
+ };
+ }
+ else
+ {
+ _currentBufferLevel = DefaultBufferLevel;
+ }
+
+ Debug.WriteLine($"[Watchdog] Loaded settings - AutoBufferIncrease: {_autoBufferIncreaseEnabled}, BufferLevel: {_currentBufferLevel}");
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"[Watchdog] Error loading auto-buffer settings: {ex.Message}");
+ _autoBufferIncreaseEnabled = DefaultAutoBufferIncreaseEnabled;
+ _currentBufferLevel = DefaultBufferLevel;
+ }
+ }
+
+ private void SaveAutoBufferSettings()
+ {
+ try
+ {
+ ApplicationData.Current.LocalSettings.Values[AutoBufferIncreaseKey] = _autoBufferIncreaseEnabled;
+ ApplicationData.Current.LocalSettings.Values[BufferLevelKey] = _currentBufferLevel;
+ Debug.WriteLine($"[Watchdog] Saved settings - AutoBufferIncrease: {_autoBufferIncreaseEnabled}, BufferLevel: {_currentBufferLevel}");
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"[Watchdog] Error saving auto-buffer settings: {ex.Message}");
+ }
}
///
@@ -273,13 +425,21 @@ private async Task AttemptRecoveryAsync(CancellationToken cancellationToken)
{
Debug.WriteLine("[Watchdog] Attempting to resume stream...");
+ // Track this recovery attempt for stutter detection
+ TrackRecoveryAttempt();
+
// Wait a bit before attempting recovery
+ Debug.WriteLine($"[Watchdog] Waiting {_recoveryDelay.TotalMilliseconds}ms before recovery");
await Task.Delay(_recoveryDelay, cancellationToken);
if (cancellationToken.IsCancellationRequested)
return;
// Attempt to restart playback on UI thread
+ // Use PlayWithBufferAsync to ensure sufficient buffer is accumulated using GetBufferedRanges
+ bool playbackStarted = false;
+ Exception? playbackException = null;
+
await RunOnUiThreadAsync(() =>
{
try
@@ -289,20 +449,37 @@ await RunOnUiThreadAsync(() =>
{
// Reinitialize the stream
_playerService.SetStreamUrl(streamUrl);
-
- // Resume playback
- _playerService.Play();
-
- Debug.WriteLine("[Watchdog] Stream recovery initiated");
- RaiseStatusChanged("Stream resumed", StreamWatchdogStatus.Recovering);
+ Debug.WriteLine("[Watchdog] Stream URL set, starting buffered playback");
}
}
catch (Exception ex)
{
- Debug.WriteLine($"[Watchdog] Failed to resume stream: {ex.Message}");
- RaiseStatusChanged($"Recovery failed: {ex.Message}", StreamWatchdogStatus.Error);
+ playbackException = ex;
+ Debug.WriteLine($"[Watchdog] Failed to set stream URL: {ex.Message}");
}
});
+
+ if (playbackException != null)
+ {
+ RaiseStatusChanged($"Recovery failed: {playbackException.Message}", StreamWatchdogStatus.Error);
+ return;
+ }
+
+ // Use PlayWithBufferAsync to wait for sufficient buffer based on GetBufferedRanges
+ // This ensures smooth playback by checking buffered content before audio starts
+ Debug.WriteLine($"[Watchdog] Starting playback with buffer monitoring (required: {_playerService.RequiredBufferDuration.TotalMilliseconds}ms)");
+ playbackStarted = await _playerService.PlayWithBufferAsync(cancellationToken);
+
+ if (playbackStarted)
+ {
+ Debug.WriteLine($"[Watchdog] Stream recovery successful with buffer: {_playerService.TotalBufferedDuration.TotalMilliseconds}ms");
+ RaiseStatusChanged("Stream resumed with buffer", StreamWatchdogStatus.Recovering);
+ }
+ else
+ {
+ Debug.WriteLine("[Watchdog] Playback start was cancelled");
+ RaiseStatusChanged("Recovery cancelled", StreamWatchdogStatus.Error);
+ }
}
catch (OperationCanceledException)
{
@@ -315,6 +492,64 @@ await RunOnUiThreadAsync(() =>
}
}
+ ///
+ /// Tracks a recovery attempt and checks for stutter pattern.
+ /// If stuttering is detected and auto-buffer increase is enabled, increases the buffer level.
+ ///
+ private void TrackRecoveryAttempt()
+ {
+ DateTime now = DateTime.UtcNow;
+ _recoveryAttempts.Enqueue(now);
+
+ // Remove old attempts outside the stutter window
+ while (_recoveryAttempts.Count > 0 &&
+ now - _recoveryAttempts.Peek() > _stutterWindow)
+ {
+ _recoveryAttempts.Dequeue();
+ }
+
+ Debug.WriteLine($"[Watchdog] Recovery attempts in last {_stutterWindow.TotalMinutes}min: {_recoveryAttempts.Count}");
+
+ // Check if we've hit the stutter threshold
+ if (_recoveryAttempts.Count >= StutterThreshold)
+ {
+ int recoveryCount = _recoveryAttempts.Count;
+ Debug.WriteLine($"[Watchdog] STUTTER DETECTED - {recoveryCount} recoveries in {_stutterWindow.TotalMinutes}min window");
+
+ double previousLevel = _currentBufferLevel;
+
+ // Auto-increase buffer if enabled and not at max
+ if (_autoBufferIncreaseEnabled && _currentBufferLevel < MaxBufferLevel)
+ {
+ BufferLevel = _currentBufferLevel + 1;
+ Debug.WriteLine($"[Watchdog] Auto-increased buffer level from {previousLevel} to {_currentBufferLevel} ({BufferLevelDescription})");
+
+ // Clear recovery attempts after buffer increase to give the new level a chance
+ _recoveryAttempts.Clear();
+ }
+
+ // Raise the stutter detected event
+ RaiseStutterDetected(new StutterDetectedEventArgs
+ {
+ RecoveryAttemptCount = recoveryCount,
+ TimeWindow = _stutterWindow,
+ PreviousBufferLevel = previousLevel,
+ NewBufferLevel = _currentBufferLevel,
+ BufferWasIncreased = Math.Abs(previousLevel - _currentBufferLevel) > 0.0001
+ });
+ }
+ }
+
+ ///
+ /// Resets the buffer level to default. Call this when the user manually changes stations or wants to reset.
+ ///
+ public void ResetBufferLevel()
+ {
+ _recoveryAttempts.Clear();
+ BufferLevel = 0;
+ Debug.WriteLine("[Watchdog] Buffer level reset to default");
+ }
+
private Task RunOnUiThreadAsync(Action action)
{
TaskCompletionSource tcs = new();
@@ -367,6 +602,40 @@ private void RaiseStatusChanged(string message, StreamWatchdogStatus status)
}
}
+ private void RaiseStutterDetected(StutterDetectedEventArgs args)
+ {
+ Debug.WriteLine($"[Watchdog] Raising StutterDetected event - BufferIncreased: {args.BufferWasIncreased}, NewLevel: {args.NewBufferLevel}");
+
+ if (_uiQueue is null || _uiQueue.HasThreadAccess)
+ {
+ StutterDetected?.Invoke(this, args);
+ }
+ else
+ {
+ _uiQueue.TryEnqueue(() =>
+ {
+ StutterDetected?.Invoke(this, args);
+ });
+ }
+ }
+
+ private void RaiseBufferLevelChanged(double newLevel)
+ {
+ Debug.WriteLine($"[Watchdog] Raising BufferLevelChanged event - NewLevel: {newLevel}");
+
+ if (_uiQueue is null || _uiQueue.HasThreadAccess)
+ {
+ BufferLevelChanged?.Invoke(this, newLevel);
+ }
+ else
+ {
+ _uiQueue.TryEnqueue(() =>
+ {
+ BufferLevelChanged?.Invoke(this, newLevel);
+ });
+ }
+ }
+
public void Dispose()
{
Stop();
@@ -402,3 +671,39 @@ public enum StreamWatchdogStatus
BackingOff,
Error
}
+
+///
+/// Event arguments for stutter detection events.
+///
+public class StutterDetectedEventArgs : EventArgs
+{
+ ///
+ /// Number of recovery attempts within the time window that triggered stutter detection.
+ ///
+ public int RecoveryAttemptCount { get; init; }
+
+ ///
+ /// The time window used for stutter detection.
+ ///
+ public TimeSpan TimeWindow { get; init; }
+
+ ///
+ /// The buffer level before auto-increase was applied.
+ ///
+ public double PreviousBufferLevel { get; init; }
+
+ ///
+ /// The buffer level after auto-increase was applied (may be same as previous if at max or disabled).
+ ///
+ public double NewBufferLevel { get; init; }
+
+ ///
+ /// Whether the buffer level was actually increased.
+ ///
+ public bool BufferWasIncreased { get; init; }
+
+ ///
+ /// Timestamp when the stutter was detected.
+ ///
+ public DateTime Timestamp { get; } = DateTime.UtcNow;
+}
diff --git a/Trdo/Trdo.csproj b/Trdo/Trdo.csproj
index 8167a96..326d4c2 100644
--- a/Trdo/Trdo.csproj
+++ b/Trdo/Trdo.csproj
@@ -54,6 +54,7 @@
+
diff --git a/Trdo/ViewModels/AboutViewModel.cs b/Trdo/ViewModels/AboutViewModel.cs
index 663a472..2a7f15f 100644
--- a/Trdo/ViewModels/AboutViewModel.cs
+++ b/Trdo/ViewModels/AboutViewModel.cs
@@ -11,6 +11,23 @@ public partial class AboutViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
+ private int _selectedRating;
+ public int SelectedRating
+ {
+ get => _selectedRating;
+ set
+ {
+ if (_selectedRating != value)
+ {
+ _selectedRating = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(ShowContactDeveloper));
+ }
+ }
+ }
+
+ public bool ShowContactDeveloper => SelectedRating > 0 && SelectedRating <= 3;
+
public string AppName => "Trdo";
public string AppDescription => "A simple, elegant internet radio player for Windows";
public string Version
@@ -54,6 +71,13 @@ public async Task OpenRatingWindow()
_ = await Launcher.LaunchUriAsync(new Uri("ms-windows-store://review/?ProductId=9NXT4TGJVHVV"));
}
+ public async Task ContactDeveloper()
+ {
+ string subject = Uri.EscapeDataString($"Trdo Feedback - {Version}");
+ string mailtoUri = $"mailto:joe@joefinapps.com?subject={subject}";
+ await Launcher.LaunchUriAsync(new Uri(mailtoUri));
+ }
+
public async Task OpenRadioBrowser()
{
await Launcher.LaunchUriAsync(new Uri(RadioBrowserUrl));
diff --git a/Trdo/ViewModels/PlayerViewModel.cs b/Trdo/ViewModels/PlayerViewModel.cs
index edd193b..466242a 100644
--- a/Trdo/ViewModels/PlayerViewModel.cs
+++ b/Trdo/ViewModels/PlayerViewModel.cs
@@ -54,6 +54,14 @@ public PlayerViewModel()
Debug.WriteLine($"[PlayerViewModel] Watchdog status: {WatchdogStatus}");
};
+ // Subscribe to buffer level changes (for auto-buffer increase)
+ _player.Watchdog.BufferLevelChanged += (_, newLevel) =>
+ {
+ Debug.WriteLine($"[PlayerViewModel] BufferLevelChanged event fired. NewLevel={newLevel}");
+ OnPropertyChanged(nameof(BufferLevel));
+ OnPropertyChanged(nameof(BufferLevelDescription));
+ };
+
// Subscribe to stream metadata changes
_player.StreamMetadataChanged += (_, metadata) =>
{
@@ -256,6 +264,44 @@ public bool WatchdogEnabled
}
}
+ ///
+ /// Gets or sets whether auto-buffer increase is enabled.
+ /// When enabled, the buffer level automatically increases when stutter is detected.
+ ///
+ public bool AutoBufferIncreaseEnabled
+ {
+ get => _player.Watchdog.AutoBufferIncreaseEnabled;
+ set
+ {
+ if (value == _player.Watchdog.AutoBufferIncreaseEnabled) return;
+ Debug.WriteLine($"[PlayerViewModel] Setting AutoBufferIncreaseEnabled to {value}");
+ _player.Watchdog.AutoBufferIncreaseEnabled = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets the current buffer level (0-3).
+ /// 0 = Default, 1 = Medium, 2 = Large, 3 = Extra Large
+ ///
+ public double BufferLevel
+ {
+ get => _player.Watchdog.BufferLevel;
+ set
+ {
+ if (Math.Abs(value - _player.Watchdog.BufferLevel) < 0.0001) return;
+ Debug.WriteLine($"[PlayerViewModel] Setting BufferLevel to {value}");
+ _player.Watchdog.BufferLevel = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(BufferLevelDescription));
+ }
+ }
+
+ ///
+ /// Gets a human-readable description of the current buffer level.
+ ///
+ public string BufferLevelDescription => _player.Watchdog.BufferLevelDescription;
+
public string WatchdogStatus
{
get => _watchdogStatus;
diff --git a/Trdo/ViewModels/SettingsViewModel.cs b/Trdo/ViewModels/SettingsViewModel.cs
index 9eb9f0f..a424963 100644
--- a/Trdo/ViewModels/SettingsViewModel.cs
+++ b/Trdo/ViewModels/SettingsViewModel.cs
@@ -13,6 +13,7 @@ public class SettingsViewModel : INotifyPropertyChanged
private bool _isStartupToggleEnabled = true;
private string _startupToggleText = "Off";
private string _watchdogToggleText = "Off";
+ private string _autoBufferToggleText = "Off";
private StartupTask? _startupTask;
private bool _initDone;
@@ -28,17 +29,30 @@ public SettingsViewModel()
if (args.PropertyName == nameof(PlayerViewModel.WatchdogEnabled))
{
OnPropertyChanged(nameof(IsWatchdogEnabled));
- WatchdogToggleText = _playerViewModel.WatchdogEnabled ? "On" : "Off";
+ WatchdogToggleText = GetToggleText(_playerViewModel.WatchdogEnabled);
+ }
+ else if (args.PropertyName == nameof(PlayerViewModel.AutoBufferIncreaseEnabled))
+ {
+ OnPropertyChanged(nameof(IsAutoBufferIncreaseEnabled));
+ AutoBufferToggleText = GetToggleText(_playerViewModel.AutoBufferIncreaseEnabled);
+ }
+ else if (args.PropertyName == nameof(PlayerViewModel.BufferLevel))
+ {
+ OnPropertyChanged(nameof(BufferLevel));
+ OnPropertyChanged(nameof(BufferLevelDescription));
}
};
- // Initialize watchdog toggle text
- WatchdogToggleText = _playerViewModel.WatchdogEnabled ? "On" : "Off";
+ // Initialize toggle text
+ WatchdogToggleText = GetToggleText(_playerViewModel.WatchdogEnabled);
+ AutoBufferToggleText = GetToggleText(_playerViewModel.AutoBufferIncreaseEnabled);
// Initialize startup task
_ = InitializeStartupTaskAsync();
}
+ private static string GetToggleText(bool enabled) => enabled ? "On" : "Off";
+
public bool IsStartupEnabled
{
get => _isStartupEnabled;
@@ -47,7 +61,7 @@ public bool IsStartupEnabled
if (value == _isStartupEnabled) return;
_isStartupEnabled = value;
OnPropertyChanged();
- StartupToggleText = value ? "On" : "Off";
+ StartupToggleText = GetToggleText(value);
// Apply the change
_ = ApplyStartupStateAsync(value);
@@ -84,7 +98,7 @@ public bool IsWatchdogEnabled
if (value == _playerViewModel.WatchdogEnabled) return;
_playerViewModel.WatchdogEnabled = value;
OnPropertyChanged();
- WatchdogToggleText = value ? "On" : "Off";
+ WatchdogToggleText = GetToggleText(value);
}
}
@@ -99,6 +113,54 @@ public string WatchdogToggleText
}
}
+ ///
+ /// Gets or sets whether auto-buffer increase is enabled.
+ /// When enabled, the buffer level automatically increases when stutter is detected.
+ ///
+ public bool IsAutoBufferIncreaseEnabled
+ {
+ get => _playerViewModel.AutoBufferIncreaseEnabled;
+ set
+ {
+ if (value == _playerViewModel.AutoBufferIncreaseEnabled) return;
+ _playerViewModel.AutoBufferIncreaseEnabled = value;
+ OnPropertyChanged();
+ AutoBufferToggleText = GetToggleText(value);
+ }
+ }
+
+ public string AutoBufferToggleText
+ {
+ get => _autoBufferToggleText;
+ set
+ {
+ if (value == _autoBufferToggleText) return;
+ _autoBufferToggleText = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets the current buffer level (0-3).
+ /// 0 = Default, 1 = Medium, 2 = Large, 3 = Extra Large
+ ///
+ public double BufferLevel
+ {
+ get => _playerViewModel.BufferLevel;
+ set
+ {
+ if (Math.Abs(value - _playerViewModel.BufferLevel) < 0.0001) return;
+ _playerViewModel.BufferLevel = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(BufferLevelDescription));
+ }
+ }
+
+ ///
+ /// Gets a human-readable description of the current buffer level.
+ ///
+ public string BufferLevelDescription => _playerViewModel.BufferLevelDescription;
+
private async Task InitializeStartupTaskAsync()
{
try
@@ -125,13 +187,13 @@ private void UpdateStartupStateFromTask()
IsStartupToggleEnabled = true;
_isStartupEnabled = true;
OnPropertyChanged(nameof(IsStartupEnabled));
- StartupToggleText = "On";
+ StartupToggleText = GetToggleText(true);
break;
case StartupTaskState.Disabled:
IsStartupToggleEnabled = true;
_isStartupEnabled = false;
OnPropertyChanged(nameof(IsStartupEnabled));
- StartupToggleText = "Off";
+ StartupToggleText = GetToggleText(false);
break;
case StartupTaskState.DisabledByUser:
case StartupTaskState.DisabledByPolicy:
@@ -139,7 +201,7 @@ private void UpdateStartupStateFromTask()
IsStartupToggleEnabled = false;
_isStartupEnabled = false;
OnPropertyChanged(nameof(IsStartupEnabled));
- StartupToggleText = "Off";
+ StartupToggleText = GetToggleText(false);
break;
}
}