fix(audio): guard underrun/stream-complete against false end-of-playback
pause() clears underrun_ so setStreamComplete can't fire TrackEnded while paused; resetToStart() resets streamComplete. Prior fix: underrun_ park + streamComplete discriminator prevent the Opus-startup false-end. Tests: 18 PlaybackScheduler cases including pause-during-underrun and underrun->resume->genuine-end-once.
This commit is contained in:
@@ -186,6 +186,10 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
|
||||
var result = await _audioInterop.UnloadAsync(PlayerId);
|
||||
if (result.Success)
|
||||
{
|
||||
// [RELOAD-DIAG] One of two base-class sites that null Duration (the other is
|
||||
// OnPlaybackEndCallback). Logged so a run can attribute a "Duration set from header"
|
||||
// re-fire to this path vs the spurious end-callback. Trivially removable.
|
||||
OnDurationNulledDiag("Unload");
|
||||
IsPlaying = false;
|
||||
IsPaused = false;
|
||||
CurrentTime = 0;
|
||||
@@ -278,6 +282,12 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
|
||||
|
||||
private async Task OnPlaybackEndCallback()
|
||||
{
|
||||
// [RELOAD-DIAG] The second base-class Duration-null site — the JS PlaybackScheduler's
|
||||
// end-of-playback callback. A false (mid-stream) fire here is the Opus-startup bug: it nulls
|
||||
// Duration (forcing a second "Duration set from header"), sets IsLoaded=false/CurrentTime=0,
|
||||
// and raises TrackEnded (premature queue auto-advance). After the scheduler fix this must fire
|
||||
// only on genuine end-of-track. Trivially removable.
|
||||
OnDurationNulledDiag("OnPlaybackEndCallback");
|
||||
IsPlaying = false;
|
||||
IsPaused = false;
|
||||
IsLoaded = false;
|
||||
@@ -308,6 +318,14 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
|
||||
/// </summary>
|
||||
protected virtual void OnPlaybackEnded() { }
|
||||
|
||||
/// <summary>
|
||||
/// [RELOAD-DIAG] Diagnostic seam — invoked at each base-class site that nulls <see cref="Duration"/>
|
||||
/// (<see cref="Unload"/> and <see cref="OnPlaybackEndCallback"/>), naming the caller. The streaming
|
||||
/// subclass overrides this to emit a tagged log via its logger so a run can attribute a re-fired
|
||||
/// "Duration set from header" to its true cause. No-op in the base; trivially removable.
|
||||
/// </summary>
|
||||
protected virtual void OnDurationNulledDiag(string caller) { }
|
||||
|
||||
|
||||
protected async Task EnsureInitializedAsync()
|
||||
{
|
||||
|
||||
@@ -138,6 +138,12 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
// Organic end-of-stream closes the session; the bucket reflects the high-water fraction reached.
|
||||
protected override void OnPlaybackEnded() => _playTracker?.Close();
|
||||
|
||||
// [RELOAD-DIAG] Emit the tagged log at each base-class Duration-null site so a run unambiguously
|
||||
// shows which path nulled Duration between two "Duration set from header" lines. Trivially removable.
|
||||
protected override void OnDurationNulledDiag(string caller) =>
|
||||
_logger.LogInformation(
|
||||
"[RELOAD-DIAG] Base nulling Duration caller={Caller} (gen={Gen})", caller, _loadGeneration);
|
||||
|
||||
public override async Task SelectTrack(TrackDto track)
|
||||
{
|
||||
await SelectTrackStreaming(track);
|
||||
|
||||
Reference in New Issue
Block a user