diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 84bb49a639..f059768ed3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,7 @@ deprecated `DynamicConcatenatingMediaSource`. * Allow clipping of child media sources where the period and window have a non-zero offset with `ClippingMediaSource`. +* Audio: Factor out `AudioTrack` position tracking from `DefaultAudioSink`. ### 2.7.1 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java new file mode 100644 index 0000000000..1eb203bc64 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -0,0 +1,609 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.audio; + +import android.annotation.TargetApi; +import android.media.AudioTimestamp; +import android.media.AudioTrack; +import android.os.SystemClock; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; + +/** + * Wraps an {@link AudioTrack}, exposing a position based on {@link + * AudioTrack#getPlaybackHeadPosition()} and {@link AudioTrack#getTimestamp(AudioTimestamp)}. + * + *

Call {@link #setAudioTrack(AudioTrack, int, int, int)} to set the audio track to wrap. Call + * {@link #mayHandleBuffer(long)} if there is input data to write to the track. If it returns false, + * the audio track position is stabilizing and no data may be written. Call {@link #start()} + * immediately before calling {@link AudioTrack#play()}. Call {@link #pause()} when pausing the + * track. Call {@link #handleEndOfStream(long)} when no more data will be written to the track. When + * the audio track will no longer be used, call {@link #reset()}. + */ +/* package */ final class AudioTrackPositionTracker { + + /** Listener for position tracker events. */ + public interface Listener { + + /** + * Called when the frame position is too far from the expected frame position. + * + * @param audioTimestampPositionFrames The frame position of the last known audio track + * timestamp. + * @param audioTimestampSystemTimeUs The system time associated with the last known audio track + * timestamp, in microseconds. + * @param systemTimeUs The current time. + * @param playbackPositionUs The current playback head position in microseconds. + */ + void onPositionFramesMismatch( + long audioTimestampPositionFrames, + long audioTimestampSystemTimeUs, + long systemTimeUs, + long playbackPositionUs); + + /** + * Called when the system time associated with the last known audio track timestamp is + * unexpectedly far from the current time. + * + * @param audioTimestampPositionFrames The frame position of the last known audio track + * timestamp. + * @param audioTimestampSystemTimeUs The system time associated with the last known audio track + * timestamp, in microseconds. + * @param systemTimeUs The current time. + * @param playbackPositionUs The current playback head position in microseconds. + */ + void onSystemTimeUsMismatch( + long audioTimestampPositionFrames, + long audioTimestampSystemTimeUs, + long systemTimeUs, + long playbackPositionUs); + + /** + * Called when the audio track has provided an invalid latency. + * + * @param latencyUs The reported latency in microseconds. + */ + void onInvalidLatency(long latencyUs); + + /** + * Called when the audio track runs out of data to play. + * + * @param bufferSize The size of the sink's buffer, in bytes. + * @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for + * PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the + * buffered media can have a variable bitrate so the duration may be unknown. + */ + void onUnderrun(int bufferSize, long bufferSizeMs); + } + + /** {@link AudioTrack} playback states. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({PLAYSTATE_STOPPED, PLAYSTATE_PAUSED, PLAYSTATE_PLAYING}) + private @interface PlayState {} + /** @see AudioTrack#PLAYSTATE_STOPPED */ + private static final int PLAYSTATE_STOPPED = AudioTrack.PLAYSTATE_STOPPED; + /** @see AudioTrack#PLAYSTATE_PAUSED */ + private static final int PLAYSTATE_PAUSED = AudioTrack.PLAYSTATE_PAUSED; + /** @see AudioTrack#PLAYSTATE_PLAYING */ + private static final int PLAYSTATE_PLAYING = AudioTrack.PLAYSTATE_PLAYING; + + /** + * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more than + * this amount. + * + *

This is a fail safe that should not be required on correctly functioning devices. + */ + private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND; + + /** + * AudioTrack latencies are deemed impossibly large if they are greater than this amount. + * + *

This is a fail safe that should not be required on correctly functioning devices. + */ + private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND; + + private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200; + + private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10; + private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; + private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; + private static final int MIN_LATENCY_SAMPLE_INTERVAL_US = 500000; + + private final Listener listener; + private final long[] playheadOffsets; + private AudioTrack audioTrack; + private int outputSampleRate; + private int bufferSize; + private long bufferSizeUs; + + private long smoothedPlayheadOffsetUs; + private long lastPlayheadSampleTimeUs; + private boolean audioTimestampSet; + private long lastTimestampSampleTimeUs; + + private Method getLatencyMethod; + private int outputPcmFrameSize; + private long resumeSystemTimeUs; + private long latencyUs; + private boolean hasData; + + private boolean needsPassthroughWorkarounds; + private @Nullable AudioTimestampV19 audioTimestamp; + private boolean shouldSampleLatency; + private long lastLatencySampleTimeUs; + private long lastRawPlaybackHeadPosition; + private long rawPlaybackHeadWrapCount; + private long passthroughWorkaroundPauseOffset; + private int nextPlayheadOffsetIndex; + private int playheadOffsetCount; + private long stopTimestampUs; + private long forceResetWorkaroundTimeMs; + private long stopPlaybackHeadPosition; + private long endPlaybackHeadPosition; + + /** + * Creates a new audio track position tracker. + * + * @param listener A listener for position tracking events. + */ + public AudioTrackPositionTracker(Listener listener) { + this.listener = Assertions.checkNotNull(listener); + if (Util.SDK_INT >= 18) { + try { + getLatencyMethod = AudioTrack.class.getMethod("getLatency", (Class[]) null); + } catch (NoSuchMethodException e) { + // There's no guarantee this method exists. Do nothing. + } + } + playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; + } + + /** + * Sets the {@link AudioTrack} to wrap. Subsequent method calls on this instance relate to this + * track's position, until the next call to {@link #reset()}. + * + * @param audioTrack The audio track to wrap. + * @param outputEncoding The encoding of the audio track. + * @param outputPcmFrameSize For PCM output encodings, the frame size. The value is ignored + * otherwise. + * @param bufferSize The audio track buffer size in bytes. + */ + public void setAudioTrack( + AudioTrack audioTrack, + @C.Encoding int outputEncoding, + int outputPcmFrameSize, + int bufferSize) { + this.audioTrack = audioTrack; + this.bufferSize = bufferSize; + this.outputPcmFrameSize = outputPcmFrameSize; + outputSampleRate = audioTrack.getSampleRate(); + needsPassthroughWorkarounds = needsPassthroughWorkarounds(outputEncoding); + boolean isOutputPcm = Util.isEncodingPcm(outputEncoding); + bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET; + if (Util.SDK_INT >= 19) { + audioTimestamp = new AudioTimestampV19(audioTrack); + } + shouldSampleLatency = isOutputPcm && getLatencyMethod != null; + lastRawPlaybackHeadPosition = 0; + rawPlaybackHeadWrapCount = 0; + passthroughWorkaroundPauseOffset = 0; + hasData = false; + stopTimestampUs = C.TIME_UNSET; + forceResetWorkaroundTimeMs = C.TIME_UNSET; + latencyUs = 0; + } + + public long getCurrentPositionUs(boolean sourceEnded) { + if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) { + maybeSampleSyncParams(); + } + + // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp. + // Otherwise, derive a smoothed position by sampling the track's frame position. + long systemTimeUs = System.nanoTime() / 1000; + long positionUs; + if (audioTimestamp != null && audioTimestampSet) { + // Calculate the speed-adjusted position using the timestamp (which may be in the future). + long elapsedSinceTimestampUs = systemTimeUs - audioTimestamp.getTimestampSystemTimeUs(); + long elapsedSinceTimestampFrames = durationUsToFrames(elapsedSinceTimestampUs); + long elapsedFrames = + audioTimestamp.getTimestampPositionFrames() + elapsedSinceTimestampFrames; + positionUs = framesToDurationUs(elapsedFrames); + } else { + if (playheadOffsetCount == 0) { + // The AudioTrack has started, but we don't have any samples to compute a smoothed position. + positionUs = getPlaybackHeadPositionUs(); + } else { + // getPlaybackHeadPositionUs() only has a granularity of ~20 ms, so we base the position off + // the system clock (and a smoothed offset between it and the playhead position) so as to + // prevent jitter in the reported positions. + positionUs = systemTimeUs + smoothedPlayheadOffsetUs; + } + if (!sourceEnded) { + positionUs -= latencyUs; + } + } + return positionUs; + } + + /** Starts position tracking. Must be called immediately before {@link AudioTrack#play()}. */ + public void start() { + resumeSystemTimeUs = System.nanoTime() / 1000; + } + + /** Returns whether the audio track is in the playing state. */ + public boolean isPlaying() { + return audioTrack.getPlayState() == PLAYSTATE_PLAYING; + } + + /** + * Checks the state of the audio track and returns whether the caller can write data to the track. + * Notifies {@link Listener#onUnderrun(int, long)} if the track has underrun. + * + * @param writtenFrames The number of frames that have been written. + * @return Whether the caller can write data to the track. + */ + public boolean mayHandleBuffer(long writtenFrames) { + @PlayState int playState = audioTrack.getPlayState(); + if (needsPassthroughWorkarounds) { + // An AC-3 audio track continues to play data written while it is paused. Stop writing so its + // buffer empties. See [Internal: b/18899620]. + if (playState == PLAYSTATE_PAUSED) { + // We force an underrun to pause the track, so don't notify the listener in this case. + hasData = false; + return false; + } + + // A new AC-3 audio track's playback position continues to increase from the old track's + // position for a short time after is has been released. Avoid writing data until the playback + // head position actually returns to zero. + if (playState == PLAYSTATE_STOPPED && getPlaybackHeadPosition() == 0) { + return false; + } + } + + boolean hadData = hasData; + hasData = hasPendingData(writtenFrames); + if (hadData && !hasData && playState != PLAYSTATE_STOPPED && listener != null) { + listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs)); + } + + 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 + && writtenFrames > 0 + && SystemClock.elapsedRealtime() - forceResetWorkaroundTimeMs + >= FORCE_RESET_WORKAROUND_TIMEOUT_MS; + } + + /** + * Records the writing position at which the stream ended, so that the reported position can + * continue to increment while remaining data is played out. + * + * @param writtenFrames The number of frames that have been written. + */ + public void handleEndOfStream(long writtenFrames) { + stopPlaybackHeadPosition = getPlaybackHeadPosition(); + stopTimestampUs = SystemClock.elapsedRealtime() * 1000; + endPlaybackHeadPosition = writtenFrames; + } + + /** + * Returns whether the audio track has any pending data to play out at its current position. + * + * @param writtenFrames The number of frames written to the audio track. + * @return Whether the audio track has any pending data to play out. + */ + public boolean hasPendingData(long writtenFrames) { + return writtenFrames > getPlaybackHeadPosition() + || forceHasPendingData(); + } + + /** + * Pauses the audio track position tracker, returning whether the audio track needs to be paused + * to cause playback to pause. If {@code false} is returned the audio track will pause without + * further interaction, as the end of stream has been handled. + */ + public boolean pause() { + resetSyncParams(); + return stopTimestampUs == C.TIME_UNSET; + } + + /** + * Resets the position tracker. Should be called when the audio track previous passed to {@link + * #setAudioTrack(AudioTrack, int, int, int)} is no longer in use. + */ + public void reset() { + resetSyncParams(); + audioTrack = null; + audioTimestamp = null; + } + + private void maybeSampleSyncParams() { + long playbackPositionUs = getPlaybackHeadPositionUs(); + if (playbackPositionUs == 0) { + // The AudioTrack hasn't output anything yet. + return; + } + long systemTimeUs = System.nanoTime() / 1000; + if (systemTimeUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) { + // Take a new sample and update the smoothed offset between the system clock and the playhead. + playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemTimeUs; + nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT; + if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) { + playheadOffsetCount++; + } + lastPlayheadSampleTimeUs = systemTimeUs; + smoothedPlayheadOffsetUs = 0; + for (int i = 0; i < playheadOffsetCount; i++) { + smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount; + } + } + + if (needsPassthroughWorkarounds) { + // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on + // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353]. + return; + } + maybeUpdateAudioTimestamp(systemTimeUs, playbackPositionUs); + maybeUpdateLatency(systemTimeUs); + } + + private void maybeUpdateAudioTimestamp(long systemTimeUs, long playbackPositionUs) { + if (audioTimestamp != null + && systemTimeUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { + audioTimestampSet = audioTimestamp.maybeUpdateTimestamp(); + if (audioTimestampSet) { + // Perform sanity checks on the timestamp. + long audioTimestampSystemTimeUs = audioTimestamp.getTimestampSystemTimeUs(); + long audioTimestampPositionFrames = audioTimestamp.getTimestampPositionFrames(); + if (audioTimestampSystemTimeUs < resumeSystemTimeUs) { + // The timestamp corresponds to a time before the track was most recently resumed. + audioTimestampSet = false; + } else if (Math.abs(audioTimestampSystemTimeUs - systemTimeUs) + > MAX_AUDIO_TIMESTAMP_OFFSET_US) { + listener.onSystemTimeUsMismatch( + audioTimestampPositionFrames, + audioTimestampSystemTimeUs, + systemTimeUs, + playbackPositionUs); + audioTimestampSet = false; + } else if (Math.abs(framesToDurationUs(audioTimestampPositionFrames) - playbackPositionUs) + > MAX_AUDIO_TIMESTAMP_OFFSET_US) { + listener.onPositionFramesMismatch( + audioTimestampPositionFrames, + audioTimestampSystemTimeUs, + systemTimeUs, + playbackPositionUs); + audioTimestampSet = false; + } + } + lastTimestampSampleTimeUs = systemTimeUs; + } + } + + private void maybeUpdateLatency(long systemTimeUs) { + if (shouldSampleLatency + && systemTimeUs - lastLatencySampleTimeUs >= MIN_LATENCY_SAMPLE_INTERVAL_US) { + try { + // Compute the audio track latency, excluding the latency due to the buffer (leaving + // latency due to the mixer and audio hardware driver). + latencyUs = + (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - bufferSizeUs; + // Sanity check that the latency is non-negative. + latencyUs = Math.max(latencyUs, 0); + // Sanity check that the latency isn't too large. + if (latencyUs > MAX_LATENCY_US) { + listener.onInvalidLatency(latencyUs); + latencyUs = 0; + } + } catch (Exception e) { + // The method existed, but doesn't work. Don't try again. + getLatencyMethod = null; + } + lastLatencySampleTimeUs = systemTimeUs; + } + } + + private long framesToDurationUs(long frameCount) { + return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate; + } + + private long durationUsToFrames(long durationUs) { + return (durationUs * outputSampleRate) / C.MICROS_PER_SECOND; + } + + private void resetSyncParams() { + smoothedPlayheadOffsetUs = 0; + playheadOffsetCount = 0; + nextPlayheadOffsetIndex = 0; + lastPlayheadSampleTimeUs = 0; + audioTimestampSet = false; + lastTimestampSampleTimeUs = 0; + } + + /** + * If passthrough workarounds are enabled, pausing is implemented by forcing the AudioTrack to + * underrun. In this case, still behave as if we have pending data, otherwise writing won't + * resume. + */ + private boolean forceHasPendingData() { + return needsPassthroughWorkarounds + && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PAUSED + && getPlaybackHeadPosition() == 0; + } + + /** + * Returns whether to work around problems with passthrough audio tracks. See [Internal: + * b/18899620, b/19187573, b/21145353]. + */ + private static boolean needsPassthroughWorkarounds(@C.Encoding int outputEncoding) { + return Util.SDK_INT < 23 + && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3); + } + + private long getPlaybackHeadPositionUs() { + return framesToDurationUs(getPlaybackHeadPosition()); + } + + /** + * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as an + * unsigned 32 bit integer, which also wraps around periodically. This method returns the playback + * head position as a long that will only wrap around if the value exceeds {@link Long#MAX_VALUE} + * (which in practice will never happen). + * + * @return The playback head position, in frames. + */ + private long getPlaybackHeadPosition() { + if (stopTimestampUs != C.TIME_UNSET) { + // Simulate the playback head position up to the total number of frames submitted. + long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs; + long framesSinceStop = (elapsedTimeSinceStopUs * outputSampleRate) / C.MICROS_PER_SECOND; + return Math.min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop); + } + + int state = audioTrack.getPlayState(); + if (state == PLAYSTATE_STOPPED) { + // The audio track hasn't been started. + return 0; + } + + long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); + if (needsPassthroughWorkarounds) { + // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22 + // where the playback head position jumps back to zero on paused passthrough/direct audio + // tracks. See [Internal: b/19187573]. + if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) { + passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; + } + rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; + } + + if (Util.SDK_INT <= 28) { + if (rawPlaybackHeadPosition == 0 + && lastRawPlaybackHeadPosition > 0 + && state == PLAYSTATE_PLAYING) { + // If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state + // where its Java API is in the playing state, but the native track is stopped. When this + // happens the playback head position gets stuck at zero. In this case, return the old + // playback head position and force the track to be reset after + // {@link #FORCE_RESET_WORKAROUND_TIMEOUT_MS} has elapsed. + if (forceResetWorkaroundTimeMs == C.TIME_UNSET) { + forceResetWorkaroundTimeMs = SystemClock.elapsedRealtime(); + } + return lastRawPlaybackHeadPosition; + } else { + forceResetWorkaroundTimeMs = C.TIME_UNSET; + } + } + + if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) { + // The value must have wrapped around. + rawPlaybackHeadWrapCount++; + } + lastRawPlaybackHeadPosition = rawPlaybackHeadPosition; + return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32); + } + + @TargetApi(19) + private static final class AudioTimestampV19 { + + private final AudioTrack audioTrack; + private final AudioTimestamp audioTimestamp; + + private long rawTimestampFramePositionWrapCount; + private long lastTimestampRawPositionFrames; + private long lastTimestampPositionFrames; + + /** + * Creates a new {@link AudioTimestamp} wrapper. + * + * @param audioTrack The audio track that will provide timestamps. + */ + public AudioTimestampV19(AudioTrack audioTrack) { + this.audioTrack = audioTrack; + audioTimestamp = new AudioTimestamp(); + } + + /** + * Attempts to update the audio track timestamp. Returns {@code true} if the timestamp was + * updated, in which case the updated timestamp system time and position can be accessed with + * {@link #getTimestampSystemTimeUs()} and {@link #getTimestampPositionFrames()}. Returns {@code + * false} if no timestamp is available, in which case those methods should not be called. + */ + public boolean maybeUpdateTimestamp() { + boolean updated = audioTrack.getTimestamp(audioTimestamp); + if (updated) { + long rawPositionFrames = audioTimestamp.framePosition; + if (lastTimestampRawPositionFrames > rawPositionFrames) { + // The value must have wrapped around. + rawTimestampFramePositionWrapCount++; + } + lastTimestampRawPositionFrames = rawPositionFrames; + lastTimestampPositionFrames = + rawPositionFrames + (rawTimestampFramePositionWrapCount << 32); + } + return updated; + } + + /** + * Returns the {@link android.media.AudioTimestamp#nanoTime} obtained during the most recent + * call to {@link #maybeUpdateTimestamp()} that returned true. + * + * @return The nanoTime obtained during the most recent call to {@link #maybeUpdateTimestamp()} + * that returned true. + */ + public long getTimestampSystemTimeUs() { + return audioTimestamp.nanoTime / 1000; + } + + /** + * Returns the {@link android.media.AudioTimestamp#framePosition} obtained during the most + * recent call to {@link #maybeUpdateTimestamp()} that returned true. The value is adjusted so + * that wrap around only occurs if the value exceeds {@link Long#MAX_VALUE} (which in practice + * will never happen). + * + * @return The framePosition obtained during the most recent call to {@link + * #maybeUpdateTimestamp()} that returned true. + */ + public long getTimestampPositionFrames() { + return lastTimestampPositionFrames; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 9689b6da8f..12c1011cbc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -19,7 +19,6 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.AudioFormat; import android.media.AudioManager; -import android.media.AudioTimestamp; import android.media.AudioTrack; import android.os.ConditionVariable; import android.os.SystemClock; @@ -32,7 +31,6 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayDeque; @@ -50,8 +48,8 @@ import java.util.ArrayList; public final class DefaultAudioSink implements AudioSink { /** - * Thrown when {@link AudioTrack#getTimestamp} returns a spurious timestamp, if - * {@link #failOnSpuriousAudioTimestamp} is set. + * Thrown when the audio track has provided a spurious timestamp, if {@link + * #failOnSpuriousAudioTimestamp} is set. */ public static final class InvalidAudioTrackTimestampException extends RuntimeException { @@ -84,22 +82,6 @@ public final class DefaultAudioSink implements AudioSink { */ private static final int BUFFER_MULTIPLICATION_FACTOR = 4; - /** {@link AudioTrack} playback states. */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({PLAYSTATE_STOPPED, PLAYSTATE_PAUSED, PLAYSTATE_PLAYING}) - private @interface PlayState {} - /** - * @see AudioTrack#PLAYSTATE_STOPPED - */ - private static final int PLAYSTATE_STOPPED = AudioTrack.PLAYSTATE_STOPPED; - /** - * @see AudioTrack#PLAYSTATE_PAUSED - */ - private static final int PLAYSTATE_PAUSED = AudioTrack.PLAYSTATE_PAUSED; - /** - * @see AudioTrack#PLAYSTATE_PLAYING - */ - private static final int PLAYSTATE_PLAYING = AudioTrack.PLAYSTATE_PLAYING; /** * @see AudioTrack#ERROR_BAD_VALUE */ @@ -124,21 +106,6 @@ public final class DefaultAudioSink implements AudioSink { private static final String TAG = "AudioTrack"; - /** - * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more - * than this amount. - *

- * This is a fail safe that should not be required on correctly functioning devices. - */ - private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND; - - /** - * AudioTrack latencies are deemed impossibly large if they are greater than this amount. - *

- * This is a fail safe that should not be required on correctly functioning devices. - */ - private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND; - /** * Represents states of the {@link #startMediaTimeUs} value. */ @@ -149,10 +116,6 @@ public final class DefaultAudioSink implements AudioSink { private static final int START_IN_SYNC = 1; private static final int START_NEED_SYNC = 2; - private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10; - private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; - private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; - /** * Whether to enable a workaround for an issue where an audio effect does not keep its session * active across releasing/initializing a new audio track, on platform builds where @@ -179,9 +142,7 @@ public final class DefaultAudioSink implements AudioSink { private final AudioProcessor[] toIntPcmAvailableAudioProcessors; private final AudioProcessor[] toFloatPcmAvailableAudioProcessors; private final ConditionVariable releasingConditionVariable; - private final long[] playheadOffsets; - private final AudioTrackUtil audioTrackUtil; - private final AudioTrackPositionErrorListener audioTrackPositionErrorListener; + private final AudioTrackPositionTracker audioTrackPositionTracker; private final ArrayDeque playbackParametersCheckpoints; @Nullable private Listener listener; @@ -200,7 +161,6 @@ public final class DefaultAudioSink implements AudioSink { private boolean processingEnabled; private boolean canApplyPlaybackParameters; private int bufferSize; - private long bufferSizeUs; private PlaybackParameters drainingPlaybackParameters; private PlaybackParameters playbackParameters; @@ -210,14 +170,6 @@ public final class DefaultAudioSink implements AudioSink { private ByteBuffer avSyncHeader; private int bytesUntilNextAvSync; - private int nextPlayheadOffsetIndex; - private int playheadOffsetCount; - private long smoothedPlayheadOffsetUs; - private long lastPlayheadSampleTimeUs; - private boolean audioTimestampSet; - private long lastTimestampSampleTimeUs; - - private Method getLatencyMethod; private int pcmFrameSize; private long submittedPcmBytes; private long submittedEncodedFrames; @@ -227,8 +179,6 @@ public final class DefaultAudioSink implements AudioSink { private int framesPerEncodedSample; private @StartMediaTimeState int startMediaTimeState; private long startMediaTimeUs; - private long resumeSystemTimeUs; - private long latencyUs; private float volume; private AudioProcessor[] audioProcessors; @@ -243,7 +193,6 @@ public final class DefaultAudioSink implements AudioSink { private boolean playing; private int audioSessionId; private boolean tunneling; - private boolean hasData; private long lastFeedElapsedRealtimeMs; /** @@ -274,19 +223,7 @@ public final class DefaultAudioSink implements AudioSink { this.audioCapabilities = audioCapabilities; this.enableConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat; releasingConditionVariable = new ConditionVariable(true); - if (Util.SDK_INT >= 18) { - try { - getLatencyMethod = AudioTrack.class.getMethod("getLatency", (Class[]) null); - } catch (NoSuchMethodException e) { - // There's no guarantee this method exists. Do nothing. - } - } - if (Util.SDK_INT >= 19) { - audioTrackUtil = new AudioTrackUtilV19(); - } else { - audioTrackUtil = new AudioTrackUtil(); - } - audioTrackPositionErrorListener = new DefaultAudioTrackPositionErrorListener(); + audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); trimmingAudioProcessor = new TrimmingAudioProcessor(); sonicAudioProcessor = new SonicAudioProcessor(); @@ -298,7 +235,6 @@ public final class DefaultAudioSink implements AudioSink { audioProcessors, 0, toIntPcmAvailableAudioProcessors, 3, audioProcessors.length); toIntPcmAvailableAudioProcessors[3 + audioProcessors.length] = sonicAudioProcessor; toFloatPcmAvailableAudioProcessors = new AudioProcessor[] {new FloatResamplingAudioProcessor()}; - playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; audioAttributes = AudioAttributes.DEFAULT; @@ -331,40 +267,10 @@ public final class DefaultAudioSink implements AudioSink { @Override public long getCurrentPositionUs(boolean sourceEnded) { - if (!hasCurrentPositionUs()) { + if (!isInitialized() || startMediaTimeState == START_NOT_SET) { return CURRENT_POSITION_NOT_SET; } - - if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) { - maybeSampleSyncParams(); - } - - // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp. - // Otherwise, derive a smoothed position by sampling the track's frame position. - long systemClockUs = System.nanoTime() / 1000; - long positionUs; - if (audioTimestampSet) { - // Calculate the speed-adjusted position using the timestamp (which may be in the future). - long elapsedSinceTimestampUs = systemClockUs - audioTrackUtil.getTimestampSystemTimeUs(); - long elapsedSinceTimestampFrames = durationUsToFrames(elapsedSinceTimestampUs); - long elapsedFrames = - audioTrackUtil.getTimestampPositionFrames() + elapsedSinceTimestampFrames; - positionUs = framesToDurationUs(elapsedFrames); - } else { - if (playheadOffsetCount == 0) { - // The AudioTrack has started, but we don't have any samples to compute a smoothed position. - positionUs = audioTrackUtil.getPositionUs(); - } else { - // getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the - // system clock (and a smoothed offset between it and the playhead position) so as to - // prevent jitter in the reported positions. - positionUs = systemClockUs + smoothedPlayheadOffsetUs; - } - if (!sourceEnded) { - positionUs -= latencyUs; - } - } - + long positionUs = audioTrackPositionTracker.getCurrentPositionUs(sourceEnded); positionUs = Math.min(positionUs, framesToDurationUs(getWrittenFrames())); return startMediaTimeUs + applySpeedup(positionUs); } @@ -497,8 +403,6 @@ public final class DefaultAudioSink implements AudioSink { bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 6 * 1024 / C.MICROS_PER_SECOND); } } - bufferSizeUs = - isInputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET; } private void resetAudioProcessors() { @@ -557,17 +461,16 @@ public final class DefaultAudioSink implements AudioSink { } } - audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds()); + audioTrackPositionTracker.setAudioTrack( + audioTrack, outputEncoding, outputPcmFrameSize, bufferSize); setVolumeInternal(); - hasData = false; - latencyUs = 0; } @Override public void play() { playing = true; if (isInitialized()) { - resumeSystemTimeUs = System.nanoTime() / 1000; + audioTrackPositionTracker.start(); audioTrack.play(); } } @@ -592,28 +495,8 @@ public final class DefaultAudioSink implements AudioSink { } } - if (needsPassthroughWorkarounds()) { - // An AC-3 audio track continues to play data written while it is paused. Stop writing so its - // buffer empties. See [Internal: b/18899620]. - if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) { - // We force an underrun to pause the track, so don't notify the listener in this case. - hasData = false; - return false; - } - - // A new AC-3 audio track's playback position continues to increase from the old track's - // position for a short time after is has been released. Avoid writing data until the playback - // head position actually returns to zero. - if (audioTrack.getPlayState() == PLAYSTATE_STOPPED - && audioTrack.getPlaybackHeadPosition() != 0) { - return false; - } - } - - boolean hadData = hasData; - hasData = hasPendingData(); - if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED && listener != null) { - audioTrackPositionErrorListener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs)); + if (!audioTrackPositionTracker.mayHandleBuffer(getWrittenFrames())) { + return false; } if (inputBuffer == null) { @@ -694,7 +577,7 @@ public final class DefaultAudioSink implements AudioSink { return true; } - if (audioTrackUtil.needsReset(getWrittenFrames())) { + if (audioTrackPositionTracker.isStalled(getWrittenFrames())) { Log.w(TAG, "Resetting stalled audio track"); reset(); return true; @@ -757,9 +640,7 @@ public final class DefaultAudioSink implements AudioSink { int bytesWritten = 0; if (Util.SDK_INT < 21) { // isInputPcm == true // Work out how many bytes we can write without the risk of blocking. - int bytesPending = - (int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize)); - int bytesToWrite = bufferSize - bytesPending; + int bytesToWrite = audioTrackPositionTracker.getAvailableBufferSize(writtenPcmBytes); if (bytesToWrite > 0) { bytesToWrite = Math.min(bytesRemaining, bytesToWrite); bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); @@ -801,7 +682,8 @@ public final class DefaultAudioSink implements AudioSink { if (drainAudioProcessorsToEndOfStream()) { // The audio processors have drained, so drain the underlying audio track. - audioTrackUtil.handleEndOfStream(getWrittenFrames()); + audioTrackPositionTracker.handleEndOfStream(getWrittenFrames()); + audioTrack.stop(); bytesUntilNextAvSync = 0; handledEndOfStream = true; } @@ -844,9 +726,7 @@ public final class DefaultAudioSink implements AudioSink { @Override public boolean hasPendingData() { - return isInitialized() - && (getWrittenFrames() > audioTrackUtil.getPlaybackHeadPosition() - || overrideHasPendingData()); + return isInitialized() && audioTrackPositionTracker.hasPendingData(getWrittenFrames()); } @Override @@ -942,9 +822,8 @@ public final class DefaultAudioSink implements AudioSink { @Override public void pause() { playing = false; - if (isInitialized()) { - resetSyncParams(); - audioTrackUtil.pause(); + if (isInitialized() && audioTrackPositionTracker.pause()) { + audioTrack.pause(); } } @@ -977,14 +856,13 @@ public final class DefaultAudioSink implements AudioSink { avSyncHeader = null; bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; - resetSyncParams(); - if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) { + if (audioTrackPositionTracker.isPlaying()) { audioTrack.pause(); } // AudioTrack.release can take some time, so we call it on a background thread. final AudioTrack toRelease = audioTrack; audioTrack = null; - audioTrackUtil.reconfigure(null, false); + audioTrackPositionTracker.reset(); releasingConditionVariable.close(); new Thread() { @Override @@ -1033,13 +911,6 @@ public final class DefaultAudioSink implements AudioSink { }.start(); } - /** - * Returns whether {@link #getCurrentPositionUs} can return the current playback position. - */ - private boolean hasCurrentPositionUs() { - return isInitialized() && startMediaTimeState != START_NOT_SET; - } - /** * Returns the underlying audio track {@code positionUs} with any applicable speedup applied. */ @@ -1067,85 +938,6 @@ public final class DefaultAudioSink implements AudioSink { positionUs - playbackParametersPositionUs, playbackParameters.speed); } - /** - * Updates the audio track latency and playback position parameters. - */ - private void maybeSampleSyncParams() { - long playbackPositionUs = audioTrackUtil.getPositionUs(); - if (playbackPositionUs == 0) { - // The AudioTrack hasn't output anything yet. - return; - } - long systemTimeUs = System.nanoTime() / 1000; - if (systemTimeUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) { - // Take a new sample and update the smoothed offset between the system clock and the playhead. - playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemTimeUs; - nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT; - if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) { - playheadOffsetCount++; - } - lastPlayheadSampleTimeUs = systemTimeUs; - smoothedPlayheadOffsetUs = 0; - for (int i = 0; i < playheadOffsetCount; i++) { - smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount; - } - } - - if (needsPassthroughWorkarounds()) { - // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on - // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353]. - return; - } - - if (systemTimeUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { - audioTimestampSet = audioTrackUtil.updateTimestamp(); - if (audioTimestampSet) { - // Perform sanity checks on the timestamp. - long audioTimestampSystemTimeUs = audioTrackUtil.getTimestampSystemTimeUs(); - long audioTimestampPositionFrames = audioTrackUtil.getTimestampPositionFrames(); - if (audioTimestampSystemTimeUs < resumeSystemTimeUs) { - // The timestamp corresponds to a time before the track was most recently resumed. - audioTimestampSet = false; - } else if (Math.abs(audioTimestampSystemTimeUs - systemTimeUs) - > MAX_AUDIO_TIMESTAMP_OFFSET_US) { - audioTrackPositionErrorListener.onSystemTimeUsMismatch( - audioTimestampPositionFrames, - audioTimestampSystemTimeUs, - systemTimeUs, - playbackPositionUs); - audioTimestampSet = false; - } else if (Math.abs(framesToDurationUs(audioTimestampPositionFrames) - playbackPositionUs) - > MAX_AUDIO_TIMESTAMP_OFFSET_US) { - audioTrackPositionErrorListener.onPositionFramesMismatch( - audioTimestampPositionFrames, - audioTimestampSystemTimeUs, - systemTimeUs, - playbackPositionUs); - audioTimestampSet = false; - } - } - if (getLatencyMethod != null && isInputPcm) { - try { - // Compute the audio track latency, excluding the latency due to the buffer (leaving - // latency due to the mixer and audio hardware driver). - latencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - - bufferSizeUs; - // Sanity check that the latency is non-negative. - latencyUs = Math.max(latencyUs, 0); - // Sanity check that the latency isn't too large. - if (latencyUs > MAX_LATENCY_US) { - audioTrackPositionErrorListener.onInvalidLatency(latencyUs); - latencyUs = 0; - } - } catch (Exception e) { - // The method existed, but doesn't work. Don't try again. - getLatencyMethod = null; - } - } - lastTimestampSampleTimeUs = systemTimeUs; - } - } - private boolean isInitialized() { return audioTrack != null; } @@ -1170,36 +962,6 @@ public final class DefaultAudioSink implements AudioSink { return isInputPcm ? (writtenPcmBytes / outputPcmFrameSize) : writtenEncodedFrames; } - private void resetSyncParams() { - smoothedPlayheadOffsetUs = 0; - playheadOffsetCount = 0; - nextPlayheadOffsetIndex = 0; - lastPlayheadSampleTimeUs = 0; - audioTimestampSet = false; - lastTimestampSampleTimeUs = 0; - } - - /** - * Returns whether to work around problems with passthrough audio tracks. - * See [Internal: b/18899620, b/19187573, b/21145353]. - */ - private boolean needsPassthroughWorkarounds() { - return Util.SDK_INT < 23 - && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3); - } - - /** - * Returns whether the audio track should behave as though it has pending data. This is to work - * around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we - * empty their buffers when paused. In this case, they should still behave as if they have - * pending data, otherwise writing will never resume. - */ - private boolean overrideHasPendingData() { - return needsPassthroughWorkarounds() - && audioTrack.getPlayState() == PLAYSTATE_PAUSED - && audioTrack.getPlaybackHeadPosition() == 0; - } - private AudioTrack initializeAudioTrack() throws InitializationException { AudioTrack audioTrack; if (Util.SDK_INT >= 21) { @@ -1349,239 +1111,6 @@ public final class DefaultAudioSink implements AudioSink { audioTrack.setStereoVolume(volume, volume); } - /** - * Wraps an {@link AudioTrack} to expose useful utility methods. - */ - private static class AudioTrackUtil { - - private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200; - - protected AudioTrack audioTrack; - private boolean needsPassthroughWorkaround; - private int sampleRate; - private long lastRawPlaybackHeadPosition; - private long rawPlaybackHeadWrapCount; - private long passthroughWorkaroundPauseOffset; - - private long stopTimestampUs; - private long forceResetWorkaroundTimeMs; - private long stopPlaybackHeadPosition; - private long endPlaybackHeadPosition; - - /** - * Reconfigures the audio track utility helper to use the specified {@code audioTrack}. - * - * @param audioTrack The audio track to wrap. - * @param needsPassthroughWorkaround Whether to workaround issues with pausing AC-3 passthrough - * audio tracks on platform API version 21/22. - */ - public void reconfigure(AudioTrack audioTrack, boolean needsPassthroughWorkaround) { - this.audioTrack = audioTrack; - this.needsPassthroughWorkaround = needsPassthroughWorkaround; - stopTimestampUs = C.TIME_UNSET; - forceResetWorkaroundTimeMs = C.TIME_UNSET; - lastRawPlaybackHeadPosition = 0; - rawPlaybackHeadWrapCount = 0; - passthroughWorkaroundPauseOffset = 0; - if (audioTrack != null) { - sampleRate = audioTrack.getSampleRate(); - } - } - - /** - * Stops the audio track in a way that ensures media written to it is played out in full, and - * that {@link #getPlaybackHeadPosition()} and {@link #getPositionUs()} continue to increment as - * the remaining media is played out. - * - * @param writtenFrames The total number of frames that have been written. - */ - public void handleEndOfStream(long writtenFrames) { - stopPlaybackHeadPosition = getPlaybackHeadPosition(); - stopTimestampUs = SystemClock.elapsedRealtime() * 1000; - endPlaybackHeadPosition = writtenFrames; - audioTrack.stop(); - } - - /** - * Pauses the audio track unless the end of the stream has been handled, in which case calling - * this method does nothing. - */ - public void pause() { - if (stopTimestampUs != C.TIME_UNSET) { - // We don't want to knock the audio track back into the paused state. - return; - } - audioTrack.pause(); - } - - /** - * Returns whether the track is in an invalid state and must be reset. - * - * @see #getPlaybackHeadPosition() - */ - public boolean needsReset(long writtenFrames) { - return forceResetWorkaroundTimeMs != C.TIME_UNSET && writtenFrames > 0 - && SystemClock.elapsedRealtime() - forceResetWorkaroundTimeMs - >= FORCE_RESET_WORKAROUND_TIMEOUT_MS; - } - - /** - * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as an - * unsigned 32 bit integer, which also wraps around periodically. This method returns the - * playback head position as a long that will only wrap around if the value exceeds - * {@link Long#MAX_VALUE} (which in practice will never happen). - * - * @return The playback head position, in frames. - */ - public long getPlaybackHeadPosition() { - if (stopTimestampUs != C.TIME_UNSET) { - // Simulate the playback head position up to the total number of frames submitted. - long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs; - long framesSinceStop = (elapsedTimeSinceStopUs * sampleRate) / C.MICROS_PER_SECOND; - return Math.min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop); - } - - @PlayState int playState = audioTrack.getPlayState(); - if (playState == PLAYSTATE_STOPPED) { - // The audio track hasn't been started. - return 0; - } - - long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); - if (needsPassthroughWorkaround) { - // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22 - // where the playback head position jumps back to zero on paused passthrough/direct audio - // tracks. See [Internal: b/19187573]. - if (playState == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) { - passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; - } - rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; - } - - if (Util.SDK_INT <= 28) { - if (rawPlaybackHeadPosition == 0 - && lastRawPlaybackHeadPosition > 0 - && playState == PLAYSTATE_PLAYING) { - // If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state - // where its Java API is in the playing state, but the native track is stopped. When this - // happens the playback head position gets stuck at zero. In this case, return the old - // playback head position and force the track to be reset after - // {@link #FORCE_RESET_WORKAROUND_TIMEOUT_MS} has elapsed. - if (forceResetWorkaroundTimeMs == C.TIME_UNSET) { - forceResetWorkaroundTimeMs = SystemClock.elapsedRealtime(); - } - return lastRawPlaybackHeadPosition; - } else { - forceResetWorkaroundTimeMs = C.TIME_UNSET; - } - } - - if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) { - // The value must have wrapped around. - rawPlaybackHeadWrapCount++; - } - lastRawPlaybackHeadPosition = rawPlaybackHeadPosition; - return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32); - } - - /** - * Returns the duration of played media since reconfiguration, in microseconds. - */ - public long getPositionUs() { - return (getPlaybackHeadPosition() * C.MICROS_PER_SECOND) / sampleRate; - } - - /** - * Updates the values returned by {@link #getTimestampSystemTimeUs()} and {@link - * #getTimestampPositionFrames()}. - * - * @return Whether the timestamp values were updated. - */ - public boolean updateTimestamp() { - return false; - } - - /** - * Returns the system time in microseconds of the last {@link AudioTimestamp}, obtained during - * the most recent call to {@link #updateTimestamp()} that returned true. - * - * @return The system time in microseconds of the last {@link AudioTimestamp}, obtained during - * the most recent call to {@link #updateTimestamp()} that returned true. - * @throws UnsupportedOperationException If the implementation does not support audio timestamp - * queries. {@link #updateTimestamp()} will always return false in this case. - */ - public long getTimestampSystemTimeUs() { - // Should never be called if updateTimestamp() returned false. - throw new UnsupportedOperationException(); - } - - /** - * Returns the position in frames of the last {@link AudioTimestamp}, obtained during the most - * recent call to {@link #updateTimestamp()} that returned true. The value is adjusted so that - * wrap around only occurs if the value exceeds {@link Long#MAX_VALUE} (which in practice will - * never happen). - * - * @return The position in frames of the last {@link AudioTimestamp}, obtained during the most - * recent call to {@link #updateTimestamp()} that returned true. - * @throws UnsupportedOperationException If the implementation does not support audio timestamp - * queries. {@link #updateTimestamp()} will always return false in this case. - */ - public long getTimestampPositionFrames() { - // Should never be called if updateTimestamp() returned false. - throw new UnsupportedOperationException(); - } - - } - - @TargetApi(19) - private static class AudioTrackUtilV19 extends AudioTrackUtil { - - private final AudioTimestamp audioTimestamp; - - private long rawTimestampPositionFramesWrapCount; - private long lastRawTimestampPositionFrames; - private long lastTimestampPositionFrames; - - public AudioTrackUtilV19() { - audioTimestamp = new AudioTimestamp(); - } - - @Override - public void reconfigure(AudioTrack audioTrack, boolean needsPassthroughWorkaround) { - super.reconfigure(audioTrack, needsPassthroughWorkaround); - rawTimestampPositionFramesWrapCount = 0; - lastRawTimestampPositionFrames = 0; - lastTimestampPositionFrames = 0; - } - - @Override - public boolean updateTimestamp() { - boolean updated = audioTrack.getTimestamp(audioTimestamp); - if (updated) { - long rawPositionFrames = audioTimestamp.framePosition; - if (lastRawTimestampPositionFrames > rawPositionFrames) { - // The value must have wrapped around. - rawTimestampPositionFramesWrapCount++; - } - lastRawTimestampPositionFrames = rawPositionFrames; - lastTimestampPositionFrames = - rawPositionFrames + (rawTimestampPositionFramesWrapCount << 32); - } - return updated; - } - - @Override - public long getTimestampSystemTimeUs() { - return audioTimestamp.nanoTime / 1000; - } - - @Override - public long getTimestampPositionFrames() { - return lastTimestampPositionFrames; - } - - } - /** * Stores playback parameters with the position and media time at which they apply. */ @@ -1600,64 +1129,7 @@ public final class DefaultAudioSink implements AudioSink { } - // TODO(b/74325207): Factor out position tracking functionality into a separate class. - - /** Interface for errors relating to the audio track position. */ - private interface AudioTrackPositionErrorListener { - - /** - * Called when the frame position is too far from the expected frame position. - * - * @param audioTimestampPositionFrames The frame position of the last known audio track - * timestamp. - * @param audioTimestampSystemTimeUs The system time associated with the last known audio track - * timestamp, in microseconds. - * @param systemTimeUs The current time. - * @param playbackPositionUs The current playback head position in microseconds. - */ - void onPositionFramesMismatch( - long audioTimestampPositionFrames, - long audioTimestampSystemTimeUs, - long systemTimeUs, - long playbackPositionUs); - - /** - * Called when the system time associated with the last known audio track timestamp is - * unexpectedly far from the current time. - * - * @param audioTimestampPositionFrames The frame position of the last known audio track - * timestamp. - * @param audioTimestampSystemTimeUs The system time associated with the last known audio track - * timestamp, in microseconds. - * @param systemTimeUs The current time. - * @param playbackPositionUs The current playback head position in microseconds. - */ - void onSystemTimeUsMismatch( - long audioTimestampPositionFrames, - long audioTimestampSystemTimeUs, - long systemTimeUs, - long playbackPositionUs); - - /** - * Called when the audio track has provided an invalid latency. - * - * @param latencyUs The reported latency in microseconds. - */ - void onInvalidLatency(long latencyUs); - - /** - * Called if the audio track runs out of data to play. - * - * @param bufferSize The size of the sink's buffer, in bytes. - * @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for - * PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the - * buffered media can have a variable bitrate so the duration may be unknown. - */ - void onUnderrun(int bufferSize, long bufferSizeMs); - } - - private final class DefaultAudioTrackPositionErrorListener - implements AudioTrackPositionErrorListener { + private final class PositionTrackerListener implements AudioTrackPositionTracker.Listener { @Override public void onPositionFramesMismatch(