diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTimestampPoller.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTimestampPoller.java | 309 |
1 files changed, 309 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTimestampPoller.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTimestampPoller.java new file mode 100644 index 0000000000..153947fec0 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTimestampPoller.java @@ -0,0 +1,309 @@ +/* + * 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 android.annotation.TargetApi; +import android.media.AudioTimestamp; +import android.media.AudioTrack; +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.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Polls the {@link AudioTrack} timestamp, if the platform supports it, taking care of polling at + * the appropriate rate to detect when the timestamp starts to advance. + * + * <p>When the audio track isn't paused, call {@link #maybePollTimestamp(long)} regularly to check + * for timestamp updates. If it returns {@code true}, call {@link #getTimestampPositionFrames()} and + * {@link #getTimestampSystemTimeUs()} to access the updated timestamp, then call {@link + * #acceptTimestamp()} or {@link #rejectTimestamp()} to accept or reject it. + * + * <p>If {@link #hasTimestamp()} returns {@code true}, call {@link #getTimestampSystemTimeUs()} to + * get the system time at which the latest timestamp was sampled and {@link + * #getTimestampPositionFrames()} to get its position in frames. If {@link #isTimestampAdvancing()} + * returns {@code true}, the caller should assume that the timestamp has been increasing in real + * time since it was sampled. Otherwise, it may be stationary. + * + * <p>Call {@link #reset()} when pausing or resuming the track. + */ +/* package */ final class AudioTimestampPoller { + + /** Timestamp polling states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_INITIALIZING, + STATE_TIMESTAMP, + STATE_TIMESTAMP_ADVANCING, + STATE_NO_TIMESTAMP, + STATE_ERROR + }) + private @interface State {} + /** State when first initializing. */ + private static final int STATE_INITIALIZING = 0; + /** State when we have a timestamp and we don't know if it's advancing. */ + private static final int STATE_TIMESTAMP = 1; + /** State when we have a timestamp and we know it is advancing. */ + private static final int STATE_TIMESTAMP_ADVANCING = 2; + /** State when the no timestamp is available. */ + private static final int STATE_NO_TIMESTAMP = 3; + /** State when the last timestamp was rejected as invalid. */ + private static final int STATE_ERROR = 4; + + /** The polling interval for {@link #STATE_INITIALIZING} and {@link #STATE_TIMESTAMP}. */ + private static final int FAST_POLL_INTERVAL_US = 5_000; + /** + * The polling interval for {@link #STATE_TIMESTAMP_ADVANCING} and {@link #STATE_NO_TIMESTAMP}. + */ + private static final int SLOW_POLL_INTERVAL_US = 10_000_000; + /** The polling interval for {@link #STATE_ERROR}. */ + private static final int ERROR_POLL_INTERVAL_US = 500_000; + + /** + * The minimum duration to remain in {@link #STATE_INITIALIZING} if no timestamps are being + * returned before transitioning to {@link #STATE_NO_TIMESTAMP}. + */ + private static final int INITIALIZING_DURATION_US = 500_000; + + @Nullable private final AudioTimestampV19 audioTimestamp; + + private @State int state; + private long initializeSystemTimeUs; + private long sampleIntervalUs; + private long lastTimestampSampleTimeUs; + private long initialTimestampPositionFrames; + + /** + * Creates a new audio timestamp poller. + * + * @param audioTrack The audio track that will provide timestamps, if the platform supports it. + */ + public AudioTimestampPoller(AudioTrack audioTrack) { + if (Util.SDK_INT >= 19) { + audioTimestamp = new AudioTimestampV19(audioTrack); + reset(); + } else { + audioTimestamp = null; + updateState(STATE_NO_TIMESTAMP); + } + } + + /** + * Polls the timestamp if required and returns whether it was updated. If {@code true}, the latest + * timestamp is available via {@link #getTimestampSystemTimeUs()} and {@link + * #getTimestampPositionFrames()}, and the caller should call {@link #acceptTimestamp()} if the + * timestamp was valid, or {@link #rejectTimestamp()} otherwise. The values returned by {@link + * #hasTimestamp()} and {@link #isTimestampAdvancing()} may be updated. + * + * @param systemTimeUs The current system time, in microseconds. + * @return Whether the timestamp was updated. + */ + public boolean maybePollTimestamp(long systemTimeUs) { + if (audioTimestamp == null || (systemTimeUs - lastTimestampSampleTimeUs) < sampleIntervalUs) { + return false; + } + lastTimestampSampleTimeUs = systemTimeUs; + boolean updatedTimestamp = audioTimestamp.maybeUpdateTimestamp(); + switch (state) { + case STATE_INITIALIZING: + if (updatedTimestamp) { + if (audioTimestamp.getTimestampSystemTimeUs() >= initializeSystemTimeUs) { + // We have an initial timestamp, but don't know if it's advancing yet. + initialTimestampPositionFrames = audioTimestamp.getTimestampPositionFrames(); + updateState(STATE_TIMESTAMP); + } else { + // Drop the timestamp, as it was sampled before the last reset. + updatedTimestamp = false; + } + } else if (systemTimeUs - initializeSystemTimeUs > INITIALIZING_DURATION_US) { + // We haven't received a timestamp for a while, so they probably aren't available for the + // current audio route. Poll infrequently in case the route changes later. + // TODO: Ideally we should listen for audio route changes in order to detect when a + // timestamp becomes available again. + updateState(STATE_NO_TIMESTAMP); + } + break; + case STATE_TIMESTAMP: + if (updatedTimestamp) { + long timestampPositionFrames = audioTimestamp.getTimestampPositionFrames(); + if (timestampPositionFrames > initialTimestampPositionFrames) { + updateState(STATE_TIMESTAMP_ADVANCING); + } + } else { + reset(); + } + break; + case STATE_TIMESTAMP_ADVANCING: + if (!updatedTimestamp) { + // The audio route may have changed, so reset polling. + reset(); + } + break; + case STATE_NO_TIMESTAMP: + if (updatedTimestamp) { + // The audio route may have changed, so reset polling. + reset(); + } + break; + case STATE_ERROR: + // Do nothing. If the caller accepts any new timestamp we'll reset polling. + break; + default: + throw new IllegalStateException(); + } + return updatedTimestamp; + } + + /** + * Rejects the timestamp last polled in {@link #maybePollTimestamp(long)}. The instance will enter + * the error state and poll timestamps infrequently until the next call to {@link + * #acceptTimestamp()}. + */ + public void rejectTimestamp() { + updateState(STATE_ERROR); + } + + /** + * Accepts the timestamp last polled in {@link #maybePollTimestamp(long)}. If the instance is in + * the error state, it will begin to poll timestamps frequently again. + */ + public void acceptTimestamp() { + if (state == STATE_ERROR) { + reset(); + } + } + + /** + * Returns whether this instance has a timestamp that can be used to calculate the audio track + * position. If {@code true}, call {@link #getTimestampSystemTimeUs()} and {@link + * #getTimestampSystemTimeUs()} to access the timestamp. + */ + public boolean hasTimestamp() { + return state == STATE_TIMESTAMP || state == STATE_TIMESTAMP_ADVANCING; + } + + /** + * Returns whether the timestamp appears to be advancing. If {@code true}, call {@link + * #getTimestampSystemTimeUs()} and {@link #getTimestampSystemTimeUs()} to access the timestamp. A + * current position for the track can be extrapolated based on elapsed real time since the system + * time at which the timestamp was sampled. + */ + public boolean isTimestampAdvancing() { + return state == STATE_TIMESTAMP_ADVANCING; + } + + /** Resets polling. Should be called whenever the audio track is paused or resumed. */ + public void reset() { + if (audioTimestamp != null) { + updateState(STATE_INITIALIZING); + } + } + + /** + * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns + * the system time at which the latest timestamp was sampled, in microseconds. + */ + public long getTimestampSystemTimeUs() { + return audioTimestamp != null ? audioTimestamp.getTimestampSystemTimeUs() : C.TIME_UNSET; + } + + /** + * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns + * the latest timestamp's position in frames. + */ + public long getTimestampPositionFrames() { + return audioTimestamp != null ? audioTimestamp.getTimestampPositionFrames() : C.POSITION_UNSET; + } + + private void updateState(@State int state) { + this.state = state; + switch (state) { + case STATE_INITIALIZING: + // Force polling a timestamp immediately, and poll quickly. + lastTimestampSampleTimeUs = 0; + initialTimestampPositionFrames = C.POSITION_UNSET; + initializeSystemTimeUs = System.nanoTime() / 1000; + sampleIntervalUs = FAST_POLL_INTERVAL_US; + break; + case STATE_TIMESTAMP: + sampleIntervalUs = FAST_POLL_INTERVAL_US; + break; + case STATE_TIMESTAMP_ADVANCING: + case STATE_NO_TIMESTAMP: + sampleIntervalUs = SLOW_POLL_INTERVAL_US; + break; + case STATE_ERROR: + sampleIntervalUs = ERROR_POLL_INTERVAL_US; + break; + default: + throw new IllegalStateException(); + } + } + + @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; + } + + public long getTimestampSystemTimeUs() { + return audioTimestamp.nanoTime / 1000; + } + + public long getTimestampPositionFrames() { + return lastTimestampPositionFrames; + } + } +} |