Streaming Bug Fixes
This commit is contained in:
@@ -42,7 +42,7 @@ else
|
||||
Step="0.1"
|
||||
Value="@CurrentTime"
|
||||
ValueChanged="@OnSeek"
|
||||
Disabled="!IsLoaded"/>
|
||||
Disabled="@(!IsLoaded || IsStreamingMode)"/>
|
||||
</div>
|
||||
|
||||
<div class="volume-right">
|
||||
@@ -77,7 +77,7 @@ else
|
||||
Step="0.1"
|
||||
Value="@CurrentTime"
|
||||
ValueChanged="@OnSeek"
|
||||
Disabled="!IsLoaded"/>
|
||||
Disabled="@(!IsLoaded || IsStreamingMode)"/>
|
||||
</div>
|
||||
|
||||
@* Control Buttons - positioned absolutely like original *@
|
||||
|
||||
@@ -13,6 +13,7 @@ public partial class AudioPlayerBar : ComponentBase
|
||||
private bool IsLoaded => PlayerService.IsLoaded;
|
||||
private bool IsLoading => PlayerService.IsLoading;
|
||||
private bool IsStreaming => PlayerService.CanStartStreaming;
|
||||
private bool IsStreamingMode => PlayerService.IsStreamingMode;
|
||||
private bool IsPlaying => PlayerService.IsPlaying;
|
||||
private bool IsPaused => PlayerService.IsPaused;
|
||||
private double CurrentTime => PlayerService.CurrentTime;
|
||||
@@ -26,6 +27,21 @@ public partial class AudioPlayerBar : ComponentBase
|
||||
await base.OnInitializedAsync();
|
||||
// Set up EventCallback for track selection
|
||||
PlayerService.OnTrackSelected = new EventCallback(this, Expand);
|
||||
|
||||
// Store the original OnStateChanged callback set by the provider
|
||||
var originalOnStateChanged = PlayerService.OnStateChanged;
|
||||
|
||||
// Set up a wrapper that calls both the original callback and our StateHasChanged
|
||||
PlayerService.OnStateChanged = new EventCallback(this, async () =>
|
||||
{
|
||||
// Invoke the original callback (AudioPlayerProvider's StateHasChanged)
|
||||
if (originalOnStateChanged.HasValue)
|
||||
{
|
||||
await originalOnStateChanged.Value.InvokeAsync();
|
||||
}
|
||||
// Also trigger our own re-render
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task Expand()
|
||||
|
||||
@@ -19,9 +19,10 @@ public partial class AudioPlayerProvider : ComponentBase
|
||||
{
|
||||
// Create the service immediately (but don't initialize yet)
|
||||
_audioPlayerService = new StreamingAudioPlayerService(AudioInterop, TrackMediaClient, Logger);
|
||||
|
||||
|
||||
// Set up EventCallback to properly marshal UI updates back to UI thread
|
||||
_audioPlayerService.OnStateChanged = new EventCallback(this, StateHasChanged);
|
||||
// Use InvokeAsync to ensure proper Blazor render cycle triggering
|
||||
_audioPlayerService.OnStateChanged = new EventCallback(this, () => InvokeAsync(StateHasChanged));
|
||||
// OnTrackSelected will be set by individual child components that need it
|
||||
}
|
||||
|
||||
|
||||
@@ -230,6 +230,7 @@ public class StreamingResult : AudioOperationResult
|
||||
public bool CanStartStreaming { get; set; }
|
||||
public bool HeaderParsed { get; set; }
|
||||
public int BufferCount { get; set; }
|
||||
public double? Duration { get; set; } // Duration in seconds calculated from WAV header
|
||||
}
|
||||
|
||||
public class AudioPlayerState
|
||||
|
||||
@@ -238,7 +238,7 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Stop()
|
||||
public virtual async Task Stop()
|
||||
{
|
||||
if (!IsLoaded) return;
|
||||
|
||||
|
||||
@@ -60,40 +60,24 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
|
||||
private async Task LoadTrackStreaming(TrackEntity track)
|
||||
{
|
||||
// Cancel and replace any previous streaming operation atomically
|
||||
var oldCancellation = _streamingCancellation;
|
||||
// Always reset to clean state before loading new track
|
||||
await ResetToIdle();
|
||||
|
||||
// Create new cancellation token for this streaming operation
|
||||
_streamingCancellation = new CancellationTokenSource();
|
||||
|
||||
// Cancel the old operation after we've replaced it
|
||||
oldCancellation?.Cancel();
|
||||
oldCancellation?.Dispose();
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
// No need to check IsLoading - we cancel previous operations
|
||||
|
||||
if (IsPlaying || IsPaused)
|
||||
{
|
||||
await Unload();
|
||||
}
|
||||
|
||||
// Reset state to indicate streaming has started
|
||||
// Set state to indicate loading has started
|
||||
ErrorMessage = null;
|
||||
LoadProgress = 0;
|
||||
IsLoaded = false;
|
||||
IsLoading = true;
|
||||
IsStreamingMode = true;
|
||||
CanStartStreaming = false;
|
||||
HeaderParsed = false;
|
||||
BufferedChunks = 0;
|
||||
_streamingPlaybackStarted = false;
|
||||
Duration = null;
|
||||
CurrentTime = 0;
|
||||
|
||||
|
||||
// Reset adaptive buffer sizing
|
||||
_currentBufferSize = DefaultBufferSize;
|
||||
_consecutiveSlowReads = 0;
|
||||
|
||||
|
||||
await NotifyStateChanged();
|
||||
|
||||
var mediaResult = await _trackMediaClient.GetTrackMedia(track.EntryKey);
|
||||
@@ -190,6 +174,13 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
CanStartStreaming = chunkResult.CanStartStreaming;
|
||||
HeaderParsed = chunkResult.HeaderParsed;
|
||||
BufferedChunks = chunkResult.BufferCount;
|
||||
|
||||
// Set duration from WAV header when available (only set once)
|
||||
if (chunkResult.Duration.HasValue && Duration == null)
|
||||
{
|
||||
Duration = chunkResult.Duration.Value;
|
||||
_logger.LogInformation("Duration set from WAV header: {Duration:F2} seconds", Duration);
|
||||
}
|
||||
|
||||
// Start playback as soon as we can
|
||||
if (!_streamingPlaybackStarted && CanStartStreaming)
|
||||
@@ -245,20 +236,63 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In streaming mode, Stop fully resets to Idle state since audio data is consumed.
|
||||
/// This is equivalent to Unload for streaming playback.
|
||||
/// </summary>
|
||||
public override async Task Stop()
|
||||
{
|
||||
// In streaming mode, Stop = Unload (data is consumed, can't replay)
|
||||
await ResetToIdle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fully resets the player to Idle state, ready for a new track.
|
||||
/// </summary>
|
||||
public override async Task Unload()
|
||||
{
|
||||
// Cancel any ongoing streaming operation
|
||||
await ResetToIdle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single method to reset all state - called by both Stop and Unload.
|
||||
/// </summary>
|
||||
private async Task ResetToIdle()
|
||||
{
|
||||
// 1. Cancel any ongoing streaming operation
|
||||
_streamingCancellation?.Cancel();
|
||||
_streamingCancellation?.Dispose();
|
||||
_streamingCancellation = null;
|
||||
|
||||
|
||||
// 2. Tell JS to stop and unload
|
||||
try
|
||||
{
|
||||
await _audioInterop.StopAsync(PlayerId);
|
||||
await _audioInterop.UnloadAsync(PlayerId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore JS errors during cleanup
|
||||
}
|
||||
|
||||
// 3. Reset ALL state to Idle
|
||||
IsPlaying = false;
|
||||
IsPaused = false;
|
||||
IsLoaded = false;
|
||||
IsLoading = false;
|
||||
CurrentTime = 0;
|
||||
Duration = null;
|
||||
LoadProgress = 0;
|
||||
ErrorMessage = null;
|
||||
|
||||
// 4. Reset streaming-specific state
|
||||
IsStreamingMode = false;
|
||||
CanStartStreaming = false;
|
||||
HeaderParsed = false;
|
||||
BufferedChunks = 0;
|
||||
_streamingPlaybackStarted = false;
|
||||
|
||||
await base.Unload();
|
||||
|
||||
await NotifyStateChanged();
|
||||
}
|
||||
|
||||
private async Task ThrottledNotifyStateChanged()
|
||||
|
||||
Reference in New Issue
Block a user