• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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