Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 97 additions & 2 deletions src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,32 @@ internal static bool AudioWriteCanAnchor(bool writeAccepted, int serial, int cur
return writeAccepted && serial == currentSerial;
}

internal static bool AudioWriteFailureIsDeviceFailure(
bool writeAccepted,
bool closing,
int serial,
int currentSerial,
int requestedSerial)
{
return !writeAccepted &&
!closing &&
serial == currentSerial &&
serial == requestedSerial;
}

internal static double AudioClockFailoverPosition(
double audioAnchorPts,
int audioAnchorSerial,
int serial,
double playedSeconds,
double audioSpeed,
double wallClockPosition)
{
return !double.IsNaN(audioAnchorPts) && audioAnchorSerial == serial
? audioAnchorPts + playedSeconds * audioSpeed
: wallClockPosition;
}

private static double TimestampToSeconds(long timestamp, AVRational timeBase)
{
return timestamp == ffmpeg.AV_NOPTS_VALUE ? double.NaN : timestamp * ffmpeg.av_q2d(timeBase);
Expand Down Expand Up @@ -469,6 +495,7 @@ private sealed class Session : IDisposable
private double _audioAnchorPts = double.NaN; // media time of the first sample written since the last sink reset
private int _audioAnchorSerial = -1;
private double _audioSpeed = 1.0; // speed the audio currently queued was resampled for
private bool _audioSinkFailed;

// Restart tracking (see IVideoPlayer.HasPlaybackRestartedSince).
private long _lastRestartTimestamp;
Expand Down Expand Up @@ -767,7 +794,9 @@ private double Clock()
{
lock (_seekLock)
{
if (!double.IsNaN(_audioAnchorPts) && _audioAnchorSerial == _currentSerial)
if (!_audioSinkFailed &&
!double.IsNaN(_audioAnchorPts) &&
_audioAnchorSerial == _currentSerial)
{
return _audioAnchorPts + _audioSink.PlayedSeconds * _audioSpeed;
}
Expand Down Expand Up @@ -891,6 +920,7 @@ private void PerformSeek(double target, int serial)
_audioAnchorPts = double.NaN;
_audioAnchorSerial = -1;
_audioSpeed = _speed;
_audioSinkFailed = false;
_wallClockBase = target;
if (_playing)
{
Expand Down Expand Up @@ -1385,6 +1415,16 @@ private void AudioLoop()
}

var packet = entry.Packet;
if (ShouldDrainAudioAfterSinkFailure())
{
if (packet != null)
{
ffmpeg.av_packet_free(&packet);
}

continue;
}

if (packet != null && packet->stream_index != codecStreamIndex)
{
if (codec != null)
Expand Down Expand Up @@ -1552,7 +1592,11 @@ private void AudioLoop()
var writeAccepted = _audioSink.Write(new ReadOnlySpan<byte>(pcm, 0, bytes), serial);
if (!writeAccepted)
{
break; // reset, device failure, seek or close while waiting for room
// A seek/reset/close also rejects writes and is a normal interruption.
// Only a rejection for the still-current serial means the device path
// itself failed; switch the master clock to wall time in that case.
TryFailOverAudioClock(serial);
break;
}

if (!anchored)
Expand Down Expand Up @@ -1608,6 +1652,57 @@ private void AudioLoop()
}
}

private bool ShouldDrainAudioAfterSinkFailure()
{
lock (_seekLock)
{
return _audioSinkFailed;
}
}

private bool TryFailOverAudioClock(int serial)
{
lock (_seekLock)
{
if (!AudioWriteFailureIsDeviceFailure(
writeAccepted: false,
_closing,
serial,
_currentSerial,
_requestedSerial))
{
return false;
}

var wallClockPosition = _wallClockBase + _wallClock.Elapsed.TotalSeconds * _speed;
var position = AudioClockFailoverPosition(
_audioAnchorPts,
_audioAnchorSerial,
serial,
_audioSink.PlayedSeconds,
_audioSpeed,
wallClockPosition);

_audioSinkFailed = true;
_audioAnchorPts = double.NaN;
_audioAnchorSerial = -1;
_wallClockBase = position;
if (_playing)
{
_wallClock.Restart();
}
else
{
_pausedPosition = position;
_wallClock.Reset();
}
}

Se.LogError("ffmpeg player: audio output failed, continuing with wall-clock timing");
_presentWake.Set();
return true;
}

private static void ApplyGain(byte[] pcm, int bytes, float gain)
{
if (Math.Abs(gain - 1f) < 0.001f)
Expand Down
50 changes: 50 additions & 0 deletions tests/UI/Logic/FfmpegPlayerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,56 @@ public void AudioWriteCanAnchor_RequiresAcceptedCurrentSerial()
Assert.False(FfmpegPlayer.AudioWriteCanAnchor(writeAccepted: true, serial: 4, currentSerial: 5));
}

[Theory]
[InlineData(false, false, 4, 4, 4, true)]
[InlineData(true, false, 4, 4, 4, false)]
[InlineData(false, true, 4, 4, 4, false)]
[InlineData(false, false, 4, 5, 5, false)]
[InlineData(false, false, 4, 4, 5, false)]
public void AudioWriteFailureIsDeviceFailure_RequiresRejectedCurrentSerialWithoutClose(
bool writeAccepted,
bool closing,
int serial,
int currentSerial,
int requestedSerial,
bool expected)
{
Assert.Equal(
expected,
FfmpegPlayer.AudioWriteFailureIsDeviceFailure(
writeAccepted,
closing,
serial,
currentSerial,
requestedSerial));
}

[Fact]
public void AudioClockFailoverPosition_PreservesLastPlayedAudioPosition()
{
Assert.Equal(
12.5,
FfmpegPlayer.AudioClockFailoverPosition(
audioAnchorPts: 10,
audioAnchorSerial: 7,
serial: 7,
playedSeconds: 1.25,
audioSpeed: 2,
wallClockPosition: 99),
precision: 6);

Assert.Equal(
99,
FfmpegPlayer.AudioClockFailoverPosition(
audioAnchorPts: double.NaN,
audioAnchorSerial: -1,
serial: 7,
playedSeconds: 1.25,
audioSpeed: 2,
wallClockPosition: 99),
precision: 6);
}

[Fact]
public void AudioQueueStartFence_FailsClosedOnAnyCoreAudioStartError()
{
Expand Down
Loading