diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9d6bf34b6e..05ba312629 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -38,6 +38,8 @@ * Use `AudioTrack#getUnderrunCount()` in `AudioTrackPositionTracker` to detect underruns in `DefaultAudioSink` instead of best-effort estimation. + * Improve audio timestamp smoothing for unexpected position drift from the + audio output device. * Video: * Add experimental `ExoPlayer` API to include the `MediaCodec.BUFFER_FLAG_DECODE_ONLY` flag when queuing decode-only input diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioTrackPositionTracker.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioTrackPositionTracker.java index 6fe09a0b85..14857d8669 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioTrackPositionTracker.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioTrackPositionTracker.java @@ -19,6 +19,7 @@ import static android.os.Build.VERSION.SDK_INT; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.common.util.Util.durationUsToSampleCount; +import static androidx.media3.common.util.Util.getMediaDurationForPlayoutDuration; import static androidx.media3.common.util.Util.msToUs; import static androidx.media3.common.util.Util.sampleCountToDurationUs; import static java.lang.Math.max; @@ -144,8 +145,14 @@ import java.lang.reflect.Method; */ private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND; - /** The duration of time used to smooth over an adjustment between position sampling modes. */ - private static final long MODE_SWITCH_SMOOTHING_DURATION_US = C.MICROS_PER_SECOND; + /** + * The maximum offset between the expected position and the reported position to attempt + * smoothing. + */ + private static final long MAX_POSITION_DRIFT_FOR_SMOOTHING_US = C.MICROS_PER_SECOND; + + /** The maximum allowed speed change to smooth out position drift in percent. */ + private static final int MAX_POSITION_SMOOTHING_SPEED_CHANGE_PERCENT = 10; /** Minimum update interval for getting the raw playback head position, in milliseconds. */ private static final long RAW_PLAYBACK_HEAD_POSITION_UPDATE_INTERVAL_MS = 5; @@ -160,7 +167,6 @@ import java.lang.reflect.Method; private final long[] playheadOffsets; @Nullable private AudioTrack audioTrack; - private int outputPcmFrameSize; private int bufferSize; @Nullable private AudioTimestampPoller audioTimestampPoller; private int outputSampleRate; @@ -193,11 +199,6 @@ import java.lang.reflect.Method; // Results from the previous call to getCurrentPositionUs. private long lastPositionUs; private long lastSystemTimeUs; - private boolean lastSampleUsedGetTimestampMode; - - // Results from the last call to getCurrentPositionUs that used a different sample mode. - private long previousModePositionUs; - private long previousModeSystemTimeUs; /** * Whether to expect a raw playback head reset. @@ -225,6 +226,8 @@ import java.lang.reflect.Method; // There's no guarantee this method exists. Do nothing. } playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; + lastSystemTimeUs = C.TIME_UNSET; + lastPositionUs = C.TIME_UNSET; clock = Clock.DEFAULT; } @@ -246,7 +249,6 @@ import java.lang.reflect.Method; int outputPcmFrameSize, int bufferSize) { this.audioTrack = audioTrack; - this.outputPcmFrameSize = outputPcmFrameSize; this.bufferSize = bufferSize; audioTimestampPoller = new AudioTimestampPoller(audioTrack, listener); outputSampleRate = audioTrack.getSampleRate(); @@ -296,42 +298,45 @@ import java.lang.reflect.Method; ? audioTimestampPoller.getTimestampPositionUs(systemTimeUs, audioTrackPlaybackSpeed) : getPlaybackHeadPositionEstimateUs(systemTimeUs); - if (lastSampleUsedGetTimestampMode != useGetTimestampMode) { - // We've switched sampling mode. - previousModeSystemTimeUs = lastSystemTimeUs; - previousModePositionUs = lastPositionUs; - } - long elapsedSincePreviousModeUs = systemTimeUs - previousModeSystemTimeUs; - if (elapsedSincePreviousModeUs < MODE_SWITCH_SMOOTHING_DURATION_US) { - // Use a ramp to smooth between the old mode and the new one to avoid introducing a sudden - // jump if the two modes disagree. - long previousModeProjectedPositionUs = - previousModePositionUs - + Util.getMediaDurationForPlayoutDuration( - elapsedSincePreviousModeUs, audioTrackPlaybackSpeed); - // A ramp consisting of 1000 points distributed over MODE_SWITCH_SMOOTHING_DURATION_US. - long rampPoint = (elapsedSincePreviousModeUs * 1000) / MODE_SWITCH_SMOOTHING_DURATION_US; - positionUs *= rampPoint; - positionUs += (1000 - rampPoint) * previousModeProjectedPositionUs; - positionUs /= 1000; - } + if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) { + if (lastSystemTimeUs != C.TIME_UNSET) { + // Only try to smooth if actively playing and having a previous sample to compare with. + long elapsedSystemTimeUs = systemTimeUs - lastSystemTimeUs; + long positionDiffUs = positionUs - lastPositionUs; + long expectedPositionDiffUs = + getMediaDurationForPlayoutDuration(elapsedSystemTimeUs, audioTrackPlaybackSpeed); + long expectedPositionUs = lastPositionUs + expectedPositionDiffUs; + long positionDriftUs = Math.abs(expectedPositionUs - positionUs); + if (positionDiffUs != 0 && positionDriftUs < MAX_POSITION_DRIFT_FOR_SMOOTHING_US) { + // Ignore updates without moving position (e.g. stuck audio, not yet started audio). Also + // ignore updates where the smoothing would take too long and it's preferable to jump to + // the new timestamp immediately. + long maxAllowedDriftUs = + expectedPositionDiffUs * MAX_POSITION_SMOOTHING_SPEED_CHANGE_PERCENT / 100; + positionUs = + Util.constrainValue( + positionUs, + expectedPositionUs - maxAllowedDriftUs, + expectedPositionUs + maxAllowedDriftUs); + } + } - if (!notifiedPositionIncreasing - && positionUs > lastPositionUs - && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { - notifiedPositionIncreasing = true; - long mediaDurationSinceLastPositionUs = Util.usToMs(positionUs - lastPositionUs); - long playoutDurationSinceLastPositionUs = - Util.getPlayoutDurationForMediaDuration( - mediaDurationSinceLastPositionUs, audioTrackPlaybackSpeed); - long playoutStartSystemTimeMs = - clock.currentTimeMillis() - Util.usToMs(playoutDurationSinceLastPositionUs); - listener.onPositionAdvancing(playoutStartSystemTimeMs); - } + if (!notifiedPositionIncreasing + && lastPositionUs != C.TIME_UNSET + && positionUs > lastPositionUs) { + notifiedPositionIncreasing = true; + long mediaDurationSinceLastPositionUs = Util.usToMs(positionUs - lastPositionUs); + long playoutDurationSinceLastPositionUs = + Util.getPlayoutDurationForMediaDuration( + mediaDurationSinceLastPositionUs, audioTrackPlaybackSpeed); + long playoutStartSystemTimeMs = + clock.currentTimeMillis() - Util.usToMs(playoutDurationSinceLastPositionUs); + listener.onPositionAdvancing(playoutStartSystemTimeMs); + } - lastSystemTimeUs = systemTimeUs; - lastPositionUs = positionUs; - lastSampleUsedGetTimestampMode = useGetTimestampMode; + lastSystemTimeUs = systemTimeUs; + lastPositionUs = positionUs; + } return positionUs; } @@ -392,20 +397,6 @@ import java.lang.reflect.Method; return true; } - /** - * Returns an estimate of the number of additional bytes that can be written to the audio track's - * buffer without running out of space. - * - *

May only be called if the output encoding is one of the PCM encodings. - * - * @param writtenBytes The number of bytes written to the audio track so far. - * @return An estimate of the number of bytes that can be written. - */ - public int getAvailableBufferSize(long writtenBytes) { - int bytesPending = (int) (writtenBytes - (getPlaybackHeadPosition() * outputPcmFrameSize)); - return bufferSize - bytesPending; - } - /** Returns whether the track is in an invalid state and must be recreated. */ public boolean isStalled(long writtenFrames) { return forceResetWorkaroundTimeMs != C.TIME_UNSET @@ -592,8 +583,8 @@ import java.lang.reflect.Method; playheadOffsetCount = 0; nextPlayheadOffsetIndex = 0; lastPlayheadSampleTimeUs = 0; - lastSystemTimeUs = 0; - previousModeSystemTimeUs = 0; + lastPositionUs = C.TIME_UNSET; + lastSystemTimeUs = C.TIME_UNSET; notifiedPositionIncreasing = false; }