/* * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; import android.media.AudioTimestamp; import android.media.AudioTrack; import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; 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. */ @Documented @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_LATENCY_SAMPLE_INTERVAL_US = 500000; private final Listener listener; private final long[] playheadOffsets; @Nullable private AudioTrack audioTrack; private int outputPcmFrameSize; private int bufferSize; @Nullable private AudioTimestampPoller audioTimestampPoller; private int outputSampleRate; private boolean needsPassthroughWorkarounds; private long bufferSizeUs; private long smoothedPlayheadOffsetUs; private long lastPlayheadSampleTimeUs; @Nullable private Method getLatencyMethod; private long latencyUs; private boolean hasData; private boolean isOutputPcm; 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.outputPcmFrameSize = outputPcmFrameSize; this.bufferSize = bufferSize; audioTimestampPoller = new AudioTimestampPoller(audioTrack); outputSampleRate = audioTrack.getSampleRate(); needsPassthroughWorkarounds = needsPassthroughWorkarounds(outputEncoding); isOutputPcm = Util.isEncodingLinearPcm(outputEncoding); bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET; lastRawPlaybackHeadPosition = 0; rawPlaybackHeadWrapCount = 0; passthroughWorkaroundPauseOffset = 0; hasData = false; stopTimestampUs = C.TIME_UNSET; forceResetWorkaroundTimeMs = C.TIME_UNSET; latencyUs = 0; } public long getCurrentPositionUs(boolean sourceEnded) { if (Assertions.checkNotNull(this.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; AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller); if (audioTimestampPoller.hasTimestamp()) { // Calculate the speed-adjusted position using the timestamp (which may be in the future). long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); long timestampPositionUs = framesToDurationUs(timestampPositionFrames); if (!audioTimestampPoller.isTimestampAdvancing()) { return timestampPositionUs; } long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs(); return timestampPositionUs + elapsedSinceTimestampUs; } else { long positionUs; 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() { Assertions.checkNotNull(audioTimestampPoller).reset(); } /** Returns whether the audio track is in the playing state. */ public boolean isPlaying() { return Assertions.checkNotNull(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 = Assertions.checkNotNull(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(); if (stopTimestampUs == C.TIME_UNSET) { // The audio track is going to be paused, so reset the timestamp poller to ensure it doesn't // supply an advancing position. Assertions.checkNotNull(audioTimestampPoller).reset(); return true; } // We've handled the end of the stream already, so there's no need to pause the track. return false; } /** * 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; audioTimestampPoller = 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; } maybePollAndCheckTimestamp(systemTimeUs, playbackPositionUs); maybeUpdateLatency(systemTimeUs); } private void maybePollAndCheckTimestamp(long systemTimeUs, long playbackPositionUs) { AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller); if (!audioTimestampPoller.maybePollTimestamp(systemTimeUs)) { return; } // Perform sanity checks on the timestamp and accept/reject it. long audioTimestampSystemTimeUs = audioTimestampPoller.getTimestampSystemTimeUs(); long audioTimestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); if (Math.abs(audioTimestampSystemTimeUs - systemTimeUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { listener.onSystemTimeUsMismatch( audioTimestampPositionFrames, audioTimestampSystemTimeUs, systemTimeUs, playbackPositionUs); audioTimestampPoller.rejectTimestamp(); } else if (Math.abs(framesToDurationUs(audioTimestampPositionFrames) - playbackPositionUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { listener.onPositionFramesMismatch( audioTimestampPositionFrames, audioTimestampSystemTimeUs, systemTimeUs, playbackPositionUs); audioTimestampPoller.rejectTimestamp(); } else { audioTimestampPoller.acceptTimestamp(); } } private void maybeUpdateLatency(long systemTimeUs) { if (isOutputPcm && getLatencyMethod != null && 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 = castNonNull((Integer) getLatencyMethod.invoke(Assertions.checkNotNull(audioTrack))) * 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 void resetSyncParams() { smoothedPlayheadOffsetUs = 0; playheadOffsetCount = 0; nextPlayheadOffsetIndex = 0; lastPlayheadSampleTimeUs = 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 && Assertions.checkNotNull(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() { AudioTrack audioTrack = Assertions.checkNotNull(this.audioTrack); 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 <= 29) { 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); } }