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