1 /*
2  * Copyright (C) 2015 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 
17 package android.media;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.media.AudioTrack;
23 import android.media.PlaybackParams;
24 import android.os.Handler;
25 import android.os.Looper;
26 import android.os.Message;
27 import android.util.Log;
28 import android.view.Surface;
29 
30 import java.lang.annotation.Retention;
31 import java.lang.annotation.RetentionPolicy;
32 import java.nio.ByteBuffer;
33 import java.util.concurrent.TimeUnit;
34 import java.util.LinkedList;
35 import java.util.List;
36 
37 /**
38  * MediaSync class can be used to synchronously playback audio and video streams.
39  * It can be used to play audio-only or video-only stream, too.
40  *
41  * <p>MediaSync is generally used like this:
42  * <pre>
43  * MediaSync sync = new MediaSync();
44  * sync.setSurface(surface);
45  * Surface inputSurface = sync.createInputSurface();
46  * ...
47  * // MediaCodec videoDecoder = ...;
48  * videoDecoder.configure(format, inputSurface, ...);
49  * ...
50  * sync.setAudioTrack(audioTrack);
51  * sync.setCallback(new MediaSync.Callback() {
52  *     {@literal @Override}
53  *     public void onAudioBufferConsumed(MediaSync sync, ByteBuffer audioBuffer, int bufferId) {
54  *         ...
55  *     }
56  * }, null);
57  * // This needs to be done since sync is paused on creation.
58  * sync.setPlaybackParams(new PlaybackParams().setSpeed(1.f));
59  *
60  * for (;;) {
61  *   ...
62  *   // send video frames to surface for rendering, e.g., call
63  *   // videoDecoder.releaseOutputBuffer(videoOutputBufferIx, videoPresentationTimeNs);
64  *   // More details are available as below.
65  *   ...
66  *   sync.queueAudio(audioByteBuffer, bufferId, audioPresentationTimeUs); // non-blocking.
67  *   // The audioByteBuffer and bufferId will be returned via callback.
68  *   // More details are available as below.
69  *   ...
70  *     ...
71  * }
72  * sync.setPlaybackParams(new PlaybackParams().setSpeed(0.f));
73  * sync.release();
74  * sync = null;
75  *
76  * // The following code snippet illustrates how video/audio raw frames are created by
77  * // MediaCodec's, how they are fed to MediaSync and how they are returned by MediaSync.
78  * // This is the callback from MediaCodec.
79  * onOutputBufferAvailable(MediaCodec codec, int bufferId, BufferInfo info) {
80  *     // ...
81  *     if (codec == videoDecoder) {
82  *         // surface timestamp must contain media presentation time in nanoseconds.
83  *         codec.releaseOutputBuffer(bufferId, 1000 * info.presentationTime);
84  *     } else {
85  *         ByteBuffer audioByteBuffer = codec.getOutputBuffer(bufferId);
86  *         sync.queueAudio(audioByteBuffer, bufferId, info.presentationTime);
87  *     }
88  *     // ...
89  * }
90  *
91  * // This is the callback from MediaSync.
92  * onAudioBufferConsumed(MediaSync sync, ByteBuffer buffer, int bufferId) {
93  *     // ...
94  *     audioDecoder.releaseBuffer(bufferId, false);
95  *     // ...
96  * }
97  *
98  * </pre>
99  *
100  * The client needs to configure corresponding sink by setting the Surface and/or AudioTrack
101  * based on the stream type it will play.
102  * <p>
103  * For video, the client needs to call {@link #createInputSurface} to obtain a surface on
104  * which it will render video frames.
105  * <p>
106  * For audio, the client needs to set up audio track correctly, e.g., using {@link
107  * AudioTrack#MODE_STREAM}. The audio buffers are sent to MediaSync directly via {@link
108  * #queueAudio}, and are returned to the client via {@link Callback#onAudioBufferConsumed}
109  * asynchronously. The client should not modify an audio buffer till it's returned.
110  * <p>
111  * The client can optionally pre-fill audio/video buffers by setting playback rate to 0.0,
112  * and then feed audio/video buffers to corresponding components. This can reduce possible
113  * initial underrun.
114  * <p>
115  */
116 public final class MediaSync {
117     /**
118      * MediaSync callback interface. Used to notify the user asynchronously
119      * of various MediaSync events.
120      */
121     public static abstract class Callback {
122         /**
123          * Called when returning an audio buffer which has been consumed.
124          *
125          * @param sync The MediaSync object.
126          * @param audioBuffer The returned audio buffer.
127          * @param bufferId The ID associated with audioBuffer as passed into
128          *     {@link MediaSync#queueAudio}.
129          */
onAudioBufferConsumed( @onNull MediaSync sync, @NonNull ByteBuffer audioBuffer, int bufferId)130         public abstract void onAudioBufferConsumed(
131                 @NonNull MediaSync sync, @NonNull ByteBuffer audioBuffer, int bufferId);
132     }
133 
134     /** Audio track failed.
135      * @see android.media.MediaSync.OnErrorListener
136      */
137     public static final int MEDIASYNC_ERROR_AUDIOTRACK_FAIL = 1;
138 
139     /** The surface failed to handle video buffers.
140      * @see android.media.MediaSync.OnErrorListener
141      */
142     public static final int MEDIASYNC_ERROR_SURFACE_FAIL = 2;
143 
144     /**
145      * Interface definition of a callback to be invoked when there
146      * has been an error during an asynchronous operation (other errors
147      * will throw exceptions at method call time).
148      */
149     public interface OnErrorListener {
150         /**
151          * Called to indicate an error.
152          *
153          * @param sync The MediaSync the error pertains to
154          * @param what The type of error that has occurred:
155          * <ul>
156          * <li>{@link #MEDIASYNC_ERROR_AUDIOTRACK_FAIL}
157          * <li>{@link #MEDIASYNC_ERROR_SURFACE_FAIL}
158          * </ul>
159          * @param extra an extra code, specific to the error. Typically
160          * implementation dependent.
161          */
onError(@onNull MediaSync sync, int what, int extra)162         void onError(@NonNull MediaSync sync, int what, int extra);
163     }
164 
165     private static final String TAG = "MediaSync";
166 
167     private static final int EVENT_CALLBACK = 1;
168     private static final int EVENT_SET_CALLBACK = 2;
169 
170     private static final int CB_RETURN_AUDIO_BUFFER = 1;
171 
172     private static class AudioBuffer {
173         public ByteBuffer mByteBuffer;
174         public int mBufferIndex;
175         long mPresentationTimeUs;
176 
AudioBuffer(@onNull ByteBuffer byteBuffer, int bufferId, long presentationTimeUs)177         public AudioBuffer(@NonNull ByteBuffer byteBuffer, int bufferId,
178                            long presentationTimeUs) {
179             mByteBuffer = byteBuffer;
180             mBufferIndex = bufferId;
181             mPresentationTimeUs = presentationTimeUs;
182         }
183     }
184 
185     private final Object mCallbackLock = new Object();
186     private Handler mCallbackHandler = null;
187     private MediaSync.Callback mCallback = null;
188 
189     private final Object mOnErrorListenerLock = new Object();
190     private Handler mOnErrorListenerHandler = null;
191     private MediaSync.OnErrorListener mOnErrorListener = null;
192 
193     private Thread mAudioThread = null;
194     // Created on mAudioThread when mAudioThread is started. When used on user thread, they should
195     // be guarded by checking mAudioThread.
196     private Handler mAudioHandler = null;
197     private Looper mAudioLooper = null;
198 
199     private final Object mAudioLock = new Object();
200     private AudioTrack mAudioTrack = null;
201     private List<AudioBuffer> mAudioBuffers = new LinkedList<AudioBuffer>();
202     // this is only used for paused/running decisions, so it is not affected by clock drift
203     private float mPlaybackRate = 0.0f;
204 
205     private long mNativeContext;
206 
207     /**
208      * Class constructor. On creation, MediaSync is paused, i.e., playback rate is 0.0f.
209      */
MediaSync()210     public MediaSync() {
211         native_setup();
212     }
213 
native_setup()214     private native final void native_setup();
215 
216     @Override
finalize()217     protected void finalize() {
218         native_finalize();
219     }
220 
native_finalize()221     private native final void native_finalize();
222 
223     /**
224      * Make sure you call this when you're done to free up any opened
225      * component instance instead of relying on the garbage collector
226      * to do this for you at some point in the future.
227      */
release()228     public final void release() {
229         returnAudioBuffers();
230         if (mAudioThread != null) {
231             if (mAudioLooper != null) {
232                 mAudioLooper.quit();
233             }
234         }
235         setCallback(null, null);
236         native_release();
237     }
238 
native_release()239     private native final void native_release();
240 
241     /**
242      * Sets an asynchronous callback for actionable MediaSync events.
243      * <p>
244      * This method can be called multiple times to update a previously set callback. If the
245      * handler is changed, undelivered notifications scheduled for the old handler may be dropped.
246      * <p>
247      * <b>Do not call this inside callback.</b>
248      *
249      * @param cb The callback that will run. Use {@code null} to stop receiving callbacks.
250      * @param handler The Handler that will run the callback. Use {@code null} to use MediaSync's
251      *     internal handler if it exists.
252      */
setCallback(@ullable Callback cb, @Nullable Handler handler)253     public void setCallback(@Nullable /* MediaSync. */ Callback cb, @Nullable Handler handler) {
254         synchronized(mCallbackLock) {
255             if (handler != null) {
256                 mCallbackHandler = handler;
257             } else {
258                 Looper looper;
259                 if ((looper = Looper.myLooper()) == null) {
260                     looper = Looper.getMainLooper();
261                 }
262                 if (looper == null) {
263                     mCallbackHandler = null;
264                 } else {
265                     mCallbackHandler = new Handler(looper);
266                 }
267             }
268 
269             mCallback = cb;
270         }
271     }
272 
273     /**
274      * Sets an asynchronous callback for error events.
275      * <p>
276      * This method can be called multiple times to update a previously set listener. If the
277      * handler is changed, undelivered notifications scheduled for the old handler may be dropped.
278      * <p>
279      * <b>Do not call this inside callback.</b>
280      *
281      * @param listener The callback that will run. Use {@code null} to stop receiving callbacks.
282      * @param handler The Handler that will run the callback. Use {@code null} to use MediaSync's
283      *     internal handler if it exists.
284      */
setOnErrorListener(@ullable OnErrorListener listener, @Nullable Handler handler)285     public void setOnErrorListener(@Nullable /* MediaSync. */ OnErrorListener listener,
286             @Nullable Handler handler) {
287         synchronized(mOnErrorListenerLock) {
288             if (handler != null) {
289                 mOnErrorListenerHandler = handler;
290             } else {
291                 Looper looper;
292                 if ((looper = Looper.myLooper()) == null) {
293                     looper = Looper.getMainLooper();
294                 }
295                 if (looper == null) {
296                     mOnErrorListenerHandler = null;
297                 } else {
298                     mOnErrorListenerHandler = new Handler(looper);
299                 }
300             }
301 
302             mOnErrorListener = listener;
303         }
304     }
305 
306     /**
307      * Sets the output surface for MediaSync.
308      * <p>
309      * Currently, this is only supported in the Initialized state.
310      *
311      * @param surface Specify a surface on which to render the video data.
312      * @throws IllegalArgumentException if the surface has been released, is invalid,
313      *     or can not be connected.
314      * @throws IllegalStateException if setting the surface is not supported, e.g.
315      *     not in the Initialized state, or another surface has already been set.
316      */
setSurface(@ullable Surface surface)317     public void setSurface(@Nullable Surface surface) {
318         native_setSurface(surface);
319     }
320 
native_setSurface(@ullable Surface surface)321     private native final void native_setSurface(@Nullable Surface surface);
322 
323     /**
324      * Sets the audio track for MediaSync.
325      * <p>
326      * Currently, this is only supported in the Initialized state.
327      *
328      * @param audioTrack Specify an AudioTrack through which to render the audio data.
329      * @throws IllegalArgumentException if the audioTrack has been released, or is invalid.
330      * @throws IllegalStateException if setting the audio track is not supported, e.g.
331      *     not in the Initialized state, or another audio track has already been set.
332      */
setAudioTrack(@ullable AudioTrack audioTrack)333     public void setAudioTrack(@Nullable AudioTrack audioTrack) {
334         native_setAudioTrack(audioTrack);
335         mAudioTrack = audioTrack;
336         if (audioTrack != null && mAudioThread == null) {
337             createAudioThread();
338         }
339     }
340 
native_setAudioTrack(@ullable AudioTrack audioTrack)341     private native final void native_setAudioTrack(@Nullable AudioTrack audioTrack);
342 
343     /**
344      * Requests a Surface to use as the input. This may only be called after
345      * {@link #setSurface}.
346      * <p>
347      * The application is responsible for calling release() on the Surface when
348      * done.
349      * @throws IllegalStateException if not set, or another input surface has
350      *     already been created.
351      */
352     @NonNull
createInputSurface()353     public native final Surface createInputSurface();
354 
355     /**
356      * Sets playback rate using {@link PlaybackParams}.
357      * <p>
358      * When using MediaSync with {@link AudioTrack}, set playback params using this
359      * call instead of calling it directly on the track, so that the sync is aware of
360      * the params change.
361      * <p>
362      * This call also works if there is no audio track.
363      *
364      * @param params the playback params to use. {@link PlaybackParams#getSpeed
365      *     Speed} is the ratio between desired playback rate and normal one. 1.0 means
366      *     normal playback speed. 0.0 means pause. Value larger than 1.0 means faster playback,
367      *     while value between 0.0 and 1.0 for slower playback. <b>Note:</b> the normal rate
368      *     does not change as a result of this call. To restore the original rate at any time,
369      *     use speed of 1.0.
370      *
371      * @throws IllegalStateException if the internal sync engine or the audio track has not
372      *     been initialized.
373      * @throws IllegalArgumentException if the params are not supported.
374      */
setPlaybackParams(@onNull PlaybackParams params)375     public void setPlaybackParams(@NonNull PlaybackParams params) {
376         synchronized(mAudioLock) {
377             mPlaybackRate = native_setPlaybackParams(params);;
378         }
379         if (mPlaybackRate != 0.0 && mAudioThread != null) {
380             postRenderAudio(0);
381         }
382     }
383 
384     /**
385      * Gets the playback rate using {@link PlaybackParams}.
386      *
387      * @return the playback rate being used.
388      *
389      * @throws IllegalStateException if the internal sync engine or the audio track has not
390      *     been initialized.
391      */
392     @NonNull
getPlaybackParams()393     public native PlaybackParams getPlaybackParams();
394 
native_setPlaybackParams(@onNull PlaybackParams params)395     private native float native_setPlaybackParams(@NonNull PlaybackParams params);
396 
397     /**
398      * Sets A/V sync mode.
399      *
400      * @param params the A/V sync params to apply
401      *
402      * @throws IllegalStateException if the internal player engine has not been
403      * initialized.
404      * @throws IllegalArgumentException if params are not supported.
405      */
setSyncParams(@onNull SyncParams params)406     public void setSyncParams(@NonNull SyncParams params) {
407         synchronized(mAudioLock) {
408             mPlaybackRate = native_setSyncParams(params);;
409         }
410         if (mPlaybackRate != 0.0 && mAudioThread != null) {
411             postRenderAudio(0);
412         }
413     }
414 
native_setSyncParams(@onNull SyncParams params)415     private native float native_setSyncParams(@NonNull SyncParams params);
416 
417     /**
418      * Gets the A/V sync mode.
419      *
420      * @return the A/V sync params
421      *
422      * @throws IllegalStateException if the internal player engine has not been
423      * initialized.
424      */
425     @NonNull
getSyncParams()426     public native SyncParams getSyncParams();
427 
428     /**
429      * Flushes all buffers from the sync object.
430      * <p>
431      * All pending unprocessed audio and video buffers are discarded. If an audio track was
432      * configured, it is flushed and stopped. If a video output surface was configured, the
433      * last frame queued to it is left on the frame. Queue a blank video frame to clear the
434      * surface,
435      * <p>
436      * No callbacks are received for the flushed buffers.
437      *
438      * @throws IllegalStateException if the internal player engine has not been
439      * initialized.
440      */
flush()441     public void flush() {
442         synchronized(mAudioLock) {
443             mAudioBuffers.clear();
444             mCallbackHandler.removeCallbacksAndMessages(null);
445         }
446         if (mAudioTrack != null) {
447             mAudioTrack.pause();
448             mAudioTrack.flush();
449             // Call stop() to signal to the AudioSink to completely fill the
450             // internal buffer before resuming playback.
451             mAudioTrack.stop();
452         }
453         native_flush();
454     }
455 
native_flush()456     private native final void native_flush();
457 
458     /**
459      * Get current playback position.
460      * <p>
461      * The MediaTimestamp represents how the media time correlates to the system time in
462      * a linear fashion using an anchor and a clock rate. During regular playback, the media
463      * time moves fairly constantly (though the anchor frame may be rebased to a current
464      * system time, the linear correlation stays steady). Therefore, this method does not
465      * need to be called often.
466      * <p>
467      * To help users get current playback position, this method always anchors the timestamp
468      * to the current {@link System#nanoTime system time}, so
469      * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position.
470      *
471      * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp
472      *         is available, e.g. because the media player has not been initialized.
473      *
474      * @see MediaTimestamp
475      */
476     @Nullable
getTimestamp()477     public MediaTimestamp getTimestamp()
478     {
479         try {
480             // TODO: create the timestamp in native
481             MediaTimestamp timestamp = new MediaTimestamp();
482             if (native_getTimestamp(timestamp)) {
483                 return timestamp;
484             } else {
485                 return null;
486             }
487         } catch (IllegalStateException e) {
488             return null;
489         }
490     }
491 
native_getTimestamp(@onNull MediaTimestamp timestamp)492     private native final boolean native_getTimestamp(@NonNull MediaTimestamp timestamp);
493 
494     /**
495      * Queues the audio data asynchronously for playback (AudioTrack must be in streaming mode).
496      * If the audio track was flushed as a result of {@link #flush}, it will be restarted.
497      * @param audioData the buffer that holds the data to play. This buffer will be returned
498      *     to the client via registered callback.
499      * @param bufferId an integer used to identify audioData. It will be returned to
500      *     the client along with audioData. This helps applications to keep track of audioData,
501      *     e.g., it can be used to store the output buffer index used by the audio codec.
502      * @param presentationTimeUs the presentation timestamp in microseconds for the first frame
503      *     in the buffer.
504      * @throws IllegalStateException if audio track is not set or internal configureation
505      *     has not been done correctly.
506      */
queueAudio( @onNull ByteBuffer audioData, int bufferId, long presentationTimeUs)507     public void queueAudio(
508             @NonNull ByteBuffer audioData, int bufferId, long presentationTimeUs) {
509         if (mAudioTrack == null || mAudioThread == null) {
510             throw new IllegalStateException(
511                     "AudioTrack is NOT set or audio thread is not created");
512         }
513 
514         synchronized(mAudioLock) {
515             mAudioBuffers.add(new AudioBuffer(audioData, bufferId, presentationTimeUs));
516         }
517 
518         if (mPlaybackRate != 0.0) {
519             postRenderAudio(0);
520         }
521     }
522 
523     // When called on user thread, make sure to check mAudioThread != null.
postRenderAudio(long delayMillis)524     private void postRenderAudio(long delayMillis) {
525         mAudioHandler.postDelayed(new Runnable() {
526             public void run() {
527                 synchronized(mAudioLock) {
528                     if (mPlaybackRate == 0.0) {
529                         return;
530                     }
531 
532                     if (mAudioBuffers.isEmpty()) {
533                         return;
534                     }
535 
536                     AudioBuffer audioBuffer = mAudioBuffers.get(0);
537                     int size = audioBuffer.mByteBuffer.remaining();
538                     // restart audio track after flush
539                     if (size > 0 && mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
540                         try {
541                             mAudioTrack.play();
542                         } catch (IllegalStateException e) {
543                             Log.w(TAG, "could not start audio track");
544                         }
545                     }
546                     int sizeWritten = mAudioTrack.write(
547                             audioBuffer.mByteBuffer,
548                             size,
549                             AudioTrack.WRITE_NON_BLOCKING);
550                     if (sizeWritten > 0) {
551                         if (audioBuffer.mPresentationTimeUs != -1) {
552                             native_updateQueuedAudioData(
553                                     size, audioBuffer.mPresentationTimeUs);
554                             audioBuffer.mPresentationTimeUs = -1;
555                         }
556 
557                         if (sizeWritten == size) {
558                             postReturnByteBuffer(audioBuffer);
559                             mAudioBuffers.remove(0);
560                             if (!mAudioBuffers.isEmpty()) {
561                                 postRenderAudio(0);
562                             }
563                             return;
564                         }
565                     }
566                     long pendingTimeMs = TimeUnit.MICROSECONDS.toMillis(
567                             native_getPlayTimeForPendingAudioFrames());
568                     postRenderAudio(pendingTimeMs / 2);
569                 }
570             }
571         }, delayMillis);
572     }
573 
native_updateQueuedAudioData( int sizeInBytes, long presentationTimeUs)574     private native final void native_updateQueuedAudioData(
575             int sizeInBytes, long presentationTimeUs);
576 
native_getPlayTimeForPendingAudioFrames()577     private native final long native_getPlayTimeForPendingAudioFrames();
578 
postReturnByteBuffer(@onNull final AudioBuffer audioBuffer)579     private final void postReturnByteBuffer(@NonNull final AudioBuffer audioBuffer) {
580         synchronized(mCallbackLock) {
581             if (mCallbackHandler != null) {
582                 final MediaSync sync = this;
583                 mCallbackHandler.post(new Runnable() {
584                     public void run() {
585                         Callback callback;
586                         synchronized(mCallbackLock) {
587                             callback = mCallback;
588                             if (mCallbackHandler == null
589                                     || mCallbackHandler.getLooper().getThread()
590                                             != Thread.currentThread()) {
591                                 // callback handler has been changed.
592                                 return;
593                             }
594                         }
595                         if (callback != null) {
596                             callback.onAudioBufferConsumed(sync, audioBuffer.mByteBuffer,
597                                     audioBuffer.mBufferIndex);
598                         }
599                     }
600                 });
601             }
602         }
603     }
604 
returnAudioBuffers()605     private final void returnAudioBuffers() {
606         synchronized(mAudioLock) {
607             for (AudioBuffer audioBuffer: mAudioBuffers) {
608                 postReturnByteBuffer(audioBuffer);
609             }
610             mAudioBuffers.clear();
611         }
612     }
613 
createAudioThread()614     private void createAudioThread() {
615         mAudioThread = new Thread() {
616             @Override
617             public void run() {
618                 Looper.prepare();
619                 synchronized(mAudioLock) {
620                     mAudioLooper = Looper.myLooper();
621                     mAudioHandler = new Handler();
622                     mAudioLock.notify();
623                 }
624                 Looper.loop();
625             }
626         };
627         mAudioThread.start();
628 
629         synchronized(mAudioLock) {
630             try {
631                 mAudioLock.wait();
632             } catch(InterruptedException e) {
633             }
634         }
635     }
636 
637     static {
638         System.loadLibrary("media_jni");
native_init()639         native_init();
640     }
641 
native_init()642     private static native final void native_init();
643 }
644