mirror of
https://github.com/androidx/media.git
synced 2025-05-17 20:49:53 +08:00
Improve timestamp smoothing in AudioTrackPositionTracker
The position tracker has two different position sources, getPlaybackHeadPosition and getTimestamp. Whenever we switch between these sources, we smooth out the position differences over a period of one second. This has two problems: 1. The smoothing duration is 1 sec regardless of the actual position difference. So for small differences, assuming the new timestamp is more correct, we needlessly keep the tracker with a position offset for longer. For large differences, the smoothing may result in an extremely large speedup (up to 5x in theory, for a max allowed diff of 5 seconds smoothed over a 1 second real time period). The solution to this issue is to adjust the smoothing period to the actual difference by using a maximum speedup/slowdown set to 10% at the moment. Smaller differences are corrected faster and larger differences are corrected in a slightly smoother way without speeding up drastically. We still need an upper bound though (set to 1 second difference) where plainly jumping to the correct position is likely a better user experience than having a lenghty smoothing. 2. The smoothing is only applied when switching between position sources. That means any position drift or jump coming from the same source is always taken as it is without any smoothing. This is problematic for the getTimstamp-based position in particular as it is only sampled every 10 seconds. The solution to this problem is to entirely remove the condition that smoothing only happens when switching between position sources. Instead we can always check whether the position drift compared to the last known position is more than the maximum allowed speedup/slowdown of 10% and if so, start applying smoothing. This helps to smooth out the position progress at the start of playback and after resumption when we switch between the position sources and both sources are not super reliable yet and it also helps for unexpected jumps in the position of getTimestamp later on during playback. PiperOrigin-RevId: 755348271
This commit is contained in:
parent
07b278a884
commit
0dc5ed19e2
@ -38,6 +38,8 @@
|
|||||||
* Use `AudioTrack#getUnderrunCount()` in `AudioTrackPositionTracker` to
|
* Use `AudioTrack#getUnderrunCount()` in `AudioTrackPositionTracker` to
|
||||||
detect underruns in `DefaultAudioSink` instead of best-effort
|
detect underruns in `DefaultAudioSink` instead of best-effort
|
||||||
estimation.
|
estimation.
|
||||||
|
* Improve audio timestamp smoothing for unexpected position drift from the
|
||||||
|
audio output device.
|
||||||
* Video:
|
* Video:
|
||||||
* Add experimental `ExoPlayer` API to include the
|
* Add experimental `ExoPlayer` API to include the
|
||||||
`MediaCodec.BUFFER_FLAG_DECODE_ONLY` flag when queuing decode-only input
|
`MediaCodec.BUFFER_FLAG_DECODE_ONLY` flag when queuing decode-only input
|
||||||
|
@ -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.Assertions.checkNotNull;
|
||||||
import static androidx.media3.common.util.Util.castNonNull;
|
import static androidx.media3.common.util.Util.castNonNull;
|
||||||
import static androidx.media3.common.util.Util.durationUsToSampleCount;
|
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.msToUs;
|
||||||
import static androidx.media3.common.util.Util.sampleCountToDurationUs;
|
import static androidx.media3.common.util.Util.sampleCountToDurationUs;
|
||||||
import static java.lang.Math.max;
|
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;
|
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. */
|
/** Minimum update interval for getting the raw playback head position, in milliseconds. */
|
||||||
private static final long RAW_PLAYBACK_HEAD_POSITION_UPDATE_INTERVAL_MS = 5;
|
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;
|
private final long[] playheadOffsets;
|
||||||
|
|
||||||
@Nullable private AudioTrack audioTrack;
|
@Nullable private AudioTrack audioTrack;
|
||||||
private int outputPcmFrameSize;
|
|
||||||
private int bufferSize;
|
private int bufferSize;
|
||||||
@Nullable private AudioTimestampPoller audioTimestampPoller;
|
@Nullable private AudioTimestampPoller audioTimestampPoller;
|
||||||
private int outputSampleRate;
|
private int outputSampleRate;
|
||||||
@ -193,11 +199,6 @@ import java.lang.reflect.Method;
|
|||||||
// Results from the previous call to getCurrentPositionUs.
|
// Results from the previous call to getCurrentPositionUs.
|
||||||
private long lastPositionUs;
|
private long lastPositionUs;
|
||||||
private long lastSystemTimeUs;
|
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.
|
* 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.
|
// There's no guarantee this method exists. Do nothing.
|
||||||
}
|
}
|
||||||
playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
|
playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
|
||||||
|
lastSystemTimeUs = C.TIME_UNSET;
|
||||||
|
lastPositionUs = C.TIME_UNSET;
|
||||||
clock = Clock.DEFAULT;
|
clock = Clock.DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,7 +249,6 @@ import java.lang.reflect.Method;
|
|||||||
int outputPcmFrameSize,
|
int outputPcmFrameSize,
|
||||||
int bufferSize) {
|
int bufferSize) {
|
||||||
this.audioTrack = audioTrack;
|
this.audioTrack = audioTrack;
|
||||||
this.outputPcmFrameSize = outputPcmFrameSize;
|
|
||||||
this.bufferSize = bufferSize;
|
this.bufferSize = bufferSize;
|
||||||
audioTimestampPoller = new AudioTimestampPoller(audioTrack, listener);
|
audioTimestampPoller = new AudioTimestampPoller(audioTrack, listener);
|
||||||
outputSampleRate = audioTrack.getSampleRate();
|
outputSampleRate = audioTrack.getSampleRate();
|
||||||
@ -296,29 +298,32 @@ import java.lang.reflect.Method;
|
|||||||
? audioTimestampPoller.getTimestampPositionUs(systemTimeUs, audioTrackPlaybackSpeed)
|
? audioTimestampPoller.getTimestampPositionUs(systemTimeUs, audioTrackPlaybackSpeed)
|
||||||
: getPlaybackHeadPositionEstimateUs(systemTimeUs);
|
: getPlaybackHeadPositionEstimateUs(systemTimeUs);
|
||||||
|
|
||||||
if (lastSampleUsedGetTimestampMode != useGetTimestampMode) {
|
if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) {
|
||||||
// We've switched sampling mode.
|
if (lastSystemTimeUs != C.TIME_UNSET) {
|
||||||
previousModeSystemTimeUs = lastSystemTimeUs;
|
// Only try to smooth if actively playing and having a previous sample to compare with.
|
||||||
previousModePositionUs = lastPositionUs;
|
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);
|
||||||
}
|
}
|
||||||
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 (!notifiedPositionIncreasing
|
if (!notifiedPositionIncreasing
|
||||||
&& positionUs > lastPositionUs
|
&& lastPositionUs != C.TIME_UNSET
|
||||||
&& audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
|
&& positionUs > lastPositionUs) {
|
||||||
notifiedPositionIncreasing = true;
|
notifiedPositionIncreasing = true;
|
||||||
long mediaDurationSinceLastPositionUs = Util.usToMs(positionUs - lastPositionUs);
|
long mediaDurationSinceLastPositionUs = Util.usToMs(positionUs - lastPositionUs);
|
||||||
long playoutDurationSinceLastPositionUs =
|
long playoutDurationSinceLastPositionUs =
|
||||||
@ -331,7 +336,7 @@ import java.lang.reflect.Method;
|
|||||||
|
|
||||||
lastSystemTimeUs = systemTimeUs;
|
lastSystemTimeUs = systemTimeUs;
|
||||||
lastPositionUs = positionUs;
|
lastPositionUs = positionUs;
|
||||||
lastSampleUsedGetTimestampMode = useGetTimestampMode;
|
}
|
||||||
|
|
||||||
return positionUs;
|
return positionUs;
|
||||||
}
|
}
|
||||||
@ -392,20 +397,6 @@ import java.lang.reflect.Method;
|
|||||||
return true;
|
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.
|
|
||||||
*
|
|
||||||
* <p>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. */
|
/** Returns whether the track is in an invalid state and must be recreated. */
|
||||||
public boolean isStalled(long writtenFrames) {
|
public boolean isStalled(long writtenFrames) {
|
||||||
return forceResetWorkaroundTimeMs != C.TIME_UNSET
|
return forceResetWorkaroundTimeMs != C.TIME_UNSET
|
||||||
@ -592,8 +583,8 @@ import java.lang.reflect.Method;
|
|||||||
playheadOffsetCount = 0;
|
playheadOffsetCount = 0;
|
||||||
nextPlayheadOffsetIndex = 0;
|
nextPlayheadOffsetIndex = 0;
|
||||||
lastPlayheadSampleTimeUs = 0;
|
lastPlayheadSampleTimeUs = 0;
|
||||||
lastSystemTimeUs = 0;
|
lastPositionUs = C.TIME_UNSET;
|
||||||
previousModeSystemTimeUs = 0;
|
lastSystemTimeUs = C.TIME_UNSET;
|
||||||
notifiedPositionIncreasing = false;
|
notifiedPositionIncreasing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user