1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.exoplayer2.audio; 17 18 import android.annotation.TargetApi; 19 import android.media.AudioTimestamp; 20 import android.media.AudioTrack; 21 import androidx.annotation.IntDef; 22 import androidx.annotation.Nullable; 23 import androidx.annotation.RequiresApi; 24 import com.google.android.exoplayer2.C; 25 import com.google.android.exoplayer2.util.Util; 26 import java.lang.annotation.Documented; 27 import java.lang.annotation.Retention; 28 import java.lang.annotation.RetentionPolicy; 29 30 /** 31 * Polls the {@link AudioTrack} timestamp, if the platform supports it, taking care of polling at 32 * the appropriate rate to detect when the timestamp starts to advance. 33 * 34 * <p>When the audio track isn't paused, call {@link #maybePollTimestamp(long)} regularly to check 35 * for timestamp updates. If it returns {@code true}, call {@link #getTimestampPositionFrames()} and 36 * {@link #getTimestampSystemTimeUs()} to access the updated timestamp, then call {@link 37 * #acceptTimestamp()} or {@link #rejectTimestamp()} to accept or reject it. 38 * 39 * <p>If {@link #hasTimestamp()} returns {@code true}, call {@link #getTimestampSystemTimeUs()} to 40 * get the system time at which the latest timestamp was sampled and {@link 41 * #getTimestampPositionFrames()} to get its position in frames. If {@link #isTimestampAdvancing()} 42 * returns {@code true}, the caller should assume that the timestamp has been increasing in real 43 * time since it was sampled. Otherwise, it may be stationary. 44 * 45 * <p>Call {@link #reset()} when pausing or resuming the track. 46 */ 47 /* package */ final class AudioTimestampPoller { 48 49 /** Timestamp polling states. */ 50 @Documented 51 @Retention(RetentionPolicy.SOURCE) 52 @IntDef({ 53 STATE_INITIALIZING, 54 STATE_TIMESTAMP, 55 STATE_TIMESTAMP_ADVANCING, 56 STATE_NO_TIMESTAMP, 57 STATE_ERROR 58 }) 59 private @interface State {} 60 /** State when first initializing. */ 61 private static final int STATE_INITIALIZING = 0; 62 /** State when we have a timestamp and we don't know if it's advancing. */ 63 private static final int STATE_TIMESTAMP = 1; 64 /** State when we have a timestamp and we know it is advancing. */ 65 private static final int STATE_TIMESTAMP_ADVANCING = 2; 66 /** State when the no timestamp is available. */ 67 private static final int STATE_NO_TIMESTAMP = 3; 68 /** State when the last timestamp was rejected as invalid. */ 69 private static final int STATE_ERROR = 4; 70 71 /** The polling interval for {@link #STATE_INITIALIZING} and {@link #STATE_TIMESTAMP}. */ 72 private static final int FAST_POLL_INTERVAL_US = 5_000; 73 /** 74 * The polling interval for {@link #STATE_TIMESTAMP_ADVANCING} and {@link #STATE_NO_TIMESTAMP}. 75 */ 76 private static final int SLOW_POLL_INTERVAL_US = 10_000_000; 77 /** The polling interval for {@link #STATE_ERROR}. */ 78 private static final int ERROR_POLL_INTERVAL_US = 500_000; 79 80 /** 81 * The minimum duration to remain in {@link #STATE_INITIALIZING} if no timestamps are being 82 * returned before transitioning to {@link #STATE_NO_TIMESTAMP}. 83 */ 84 private static final int INITIALIZING_DURATION_US = 500_000; 85 86 @Nullable private final AudioTimestampV19 audioTimestamp; 87 88 private @State int state; 89 private long initializeSystemTimeUs; 90 private long sampleIntervalUs; 91 private long lastTimestampSampleTimeUs; 92 private long initialTimestampPositionFrames; 93 94 /** 95 * Creates a new audio timestamp poller. 96 * 97 * @param audioTrack The audio track that will provide timestamps, if the platform supports it. 98 */ AudioTimestampPoller(AudioTrack audioTrack)99 public AudioTimestampPoller(AudioTrack audioTrack) { 100 if (Util.SDK_INT >= 19) { 101 audioTimestamp = new AudioTimestampV19(audioTrack); 102 reset(); 103 } else { 104 audioTimestamp = null; 105 updateState(STATE_NO_TIMESTAMP); 106 } 107 } 108 109 /** 110 * Polls the timestamp if required and returns whether it was updated. If {@code true}, the latest 111 * timestamp is available via {@link #getTimestampSystemTimeUs()} and {@link 112 * #getTimestampPositionFrames()}, and the caller should call {@link #acceptTimestamp()} if the 113 * timestamp was valid, or {@link #rejectTimestamp()} otherwise. The values returned by {@link 114 * #hasTimestamp()} and {@link #isTimestampAdvancing()} may be updated. 115 * 116 * @param systemTimeUs The current system time, in microseconds. 117 * @return Whether the timestamp was updated. 118 */ 119 @TargetApi(19) // audioTimestamp will be null if Util.SDK_INT < 19. maybePollTimestamp(long systemTimeUs)120 public boolean maybePollTimestamp(long systemTimeUs) { 121 if (audioTimestamp == null || (systemTimeUs - lastTimestampSampleTimeUs) < sampleIntervalUs) { 122 return false; 123 } 124 lastTimestampSampleTimeUs = systemTimeUs; 125 boolean updatedTimestamp = audioTimestamp.maybeUpdateTimestamp(); 126 switch (state) { 127 case STATE_INITIALIZING: 128 if (updatedTimestamp) { 129 if (audioTimestamp.getTimestampSystemTimeUs() >= initializeSystemTimeUs) { 130 // We have an initial timestamp, but don't know if it's advancing yet. 131 initialTimestampPositionFrames = audioTimestamp.getTimestampPositionFrames(); 132 updateState(STATE_TIMESTAMP); 133 } else { 134 // Drop the timestamp, as it was sampled before the last reset. 135 updatedTimestamp = false; 136 } 137 } else if (systemTimeUs - initializeSystemTimeUs > INITIALIZING_DURATION_US) { 138 // We haven't received a timestamp for a while, so they probably aren't available for the 139 // current audio route. Poll infrequently in case the route changes later. 140 // TODO: Ideally we should listen for audio route changes in order to detect when a 141 // timestamp becomes available again. 142 updateState(STATE_NO_TIMESTAMP); 143 } 144 break; 145 case STATE_TIMESTAMP: 146 if (updatedTimestamp) { 147 long timestampPositionFrames = audioTimestamp.getTimestampPositionFrames(); 148 if (timestampPositionFrames > initialTimestampPositionFrames) { 149 updateState(STATE_TIMESTAMP_ADVANCING); 150 } 151 } else { 152 reset(); 153 } 154 break; 155 case STATE_TIMESTAMP_ADVANCING: 156 if (!updatedTimestamp) { 157 // The audio route may have changed, so reset polling. 158 reset(); 159 } 160 break; 161 case STATE_NO_TIMESTAMP: 162 if (updatedTimestamp) { 163 // The audio route may have changed, so reset polling. 164 reset(); 165 } 166 break; 167 case STATE_ERROR: 168 // Do nothing. If the caller accepts any new timestamp we'll reset polling. 169 break; 170 default: 171 throw new IllegalStateException(); 172 } 173 return updatedTimestamp; 174 } 175 176 /** 177 * Rejects the timestamp last polled in {@link #maybePollTimestamp(long)}. The instance will enter 178 * the error state and poll timestamps infrequently until the next call to {@link 179 * #acceptTimestamp()}. 180 */ rejectTimestamp()181 public void rejectTimestamp() { 182 updateState(STATE_ERROR); 183 } 184 185 /** 186 * Accepts the timestamp last polled in {@link #maybePollTimestamp(long)}. If the instance is in 187 * the error state, it will begin to poll timestamps frequently again. 188 */ acceptTimestamp()189 public void acceptTimestamp() { 190 if (state == STATE_ERROR) { 191 reset(); 192 } 193 } 194 195 /** 196 * Returns whether this instance has a timestamp that can be used to calculate the audio track 197 * position. If {@code true}, call {@link #getTimestampSystemTimeUs()} and {@link 198 * #getTimestampSystemTimeUs()} to access the timestamp. 199 */ hasTimestamp()200 public boolean hasTimestamp() { 201 return state == STATE_TIMESTAMP || state == STATE_TIMESTAMP_ADVANCING; 202 } 203 204 /** 205 * Returns whether the timestamp appears to be advancing. If {@code true}, call {@link 206 * #getTimestampSystemTimeUs()} and {@link #getTimestampSystemTimeUs()} to access the timestamp. A 207 * current position for the track can be extrapolated based on elapsed real time since the system 208 * time at which the timestamp was sampled. 209 */ isTimestampAdvancing()210 public boolean isTimestampAdvancing() { 211 return state == STATE_TIMESTAMP_ADVANCING; 212 } 213 214 /** Resets polling. Should be called whenever the audio track is paused or resumed. */ reset()215 public void reset() { 216 if (audioTimestamp != null) { 217 updateState(STATE_INITIALIZING); 218 } 219 } 220 221 /** 222 * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns 223 * the system time at which the latest timestamp was sampled, in microseconds. 224 */ 225 @TargetApi(19) // audioTimestamp will be null if Util.SDK_INT < 19. getTimestampSystemTimeUs()226 public long getTimestampSystemTimeUs() { 227 return audioTimestamp != null ? audioTimestamp.getTimestampSystemTimeUs() : C.TIME_UNSET; 228 } 229 230 /** 231 * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns 232 * the latest timestamp's position in frames. 233 */ 234 @TargetApi(19) // audioTimestamp will be null if Util.SDK_INT < 19. getTimestampPositionFrames()235 public long getTimestampPositionFrames() { 236 return audioTimestamp != null ? audioTimestamp.getTimestampPositionFrames() : C.POSITION_UNSET; 237 } 238 updateState(@tate int state)239 private void updateState(@State int state) { 240 this.state = state; 241 switch (state) { 242 case STATE_INITIALIZING: 243 // Force polling a timestamp immediately, and poll quickly. 244 lastTimestampSampleTimeUs = 0; 245 initialTimestampPositionFrames = C.POSITION_UNSET; 246 initializeSystemTimeUs = System.nanoTime() / 1000; 247 sampleIntervalUs = FAST_POLL_INTERVAL_US; 248 break; 249 case STATE_TIMESTAMP: 250 sampleIntervalUs = FAST_POLL_INTERVAL_US; 251 break; 252 case STATE_TIMESTAMP_ADVANCING: 253 case STATE_NO_TIMESTAMP: 254 sampleIntervalUs = SLOW_POLL_INTERVAL_US; 255 break; 256 case STATE_ERROR: 257 sampleIntervalUs = ERROR_POLL_INTERVAL_US; 258 break; 259 default: 260 throw new IllegalStateException(); 261 } 262 } 263 264 @RequiresApi(19) 265 private static final class AudioTimestampV19 { 266 267 private final AudioTrack audioTrack; 268 private final AudioTimestamp audioTimestamp; 269 270 private long rawTimestampFramePositionWrapCount; 271 private long lastTimestampRawPositionFrames; 272 private long lastTimestampPositionFrames; 273 274 /** 275 * Creates a new {@link AudioTimestamp} wrapper. 276 * 277 * @param audioTrack The audio track that will provide timestamps. 278 */ AudioTimestampV19(AudioTrack audioTrack)279 public AudioTimestampV19(AudioTrack audioTrack) { 280 this.audioTrack = audioTrack; 281 audioTimestamp = new AudioTimestamp(); 282 } 283 284 /** 285 * Attempts to update the audio track timestamp. Returns {@code true} if the timestamp was 286 * updated, in which case the updated timestamp system time and position can be accessed with 287 * {@link #getTimestampSystemTimeUs()} and {@link #getTimestampPositionFrames()}. Returns {@code 288 * false} if no timestamp is available, in which case those methods should not be called. 289 */ maybeUpdateTimestamp()290 public boolean maybeUpdateTimestamp() { 291 boolean updated = audioTrack.getTimestamp(audioTimestamp); 292 if (updated) { 293 long rawPositionFrames = audioTimestamp.framePosition; 294 if (lastTimestampRawPositionFrames > rawPositionFrames) { 295 // The value must have wrapped around. 296 rawTimestampFramePositionWrapCount++; 297 } 298 lastTimestampRawPositionFrames = rawPositionFrames; 299 lastTimestampPositionFrames = 300 rawPositionFrames + (rawTimestampFramePositionWrapCount << 32); 301 } 302 return updated; 303 } 304 getTimestampSystemTimeUs()305 public long getTimestampSystemTimeUs() { 306 return audioTimestamp.nanoTime / 1000; 307 } 308 getTimestampPositionFrames()309 public long getTimestampPositionFrames() { 310 return lastTimestampPositionFrames; 311 } 312 } 313 } 314