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
17package android.media;
18
19import android.annotation.IntDef;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.media.AudioTrack;
23import android.media.PlaybackParams;
24import android.os.Handler;
25import android.os.Looper;
26import android.os.Message;
27import android.util.Log;
28import android.view.Surface;
29
30import java.lang.annotation.Retention;
31import java.lang.annotation.RetentionPolicy;
32import java.nio.ByteBuffer;
33import java.util.concurrent.TimeUnit;
34import java.util.LinkedList;
35import java.util.List;
36
37/**
38 * MediaSync class can be used to synchronously play 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 */
116public 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         */
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         */
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
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     */
210    public MediaSync() {
211        native_setup();
212    }
213
214    private native final void native_setup();
215
216    @Override
217    protected void finalize() {
218        native_finalize();
219    }
220
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     */
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
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     */
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     */
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     */
317    public void setSurface(@Nullable Surface surface) {
318        native_setSurface(surface);
319    }
320
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     */
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
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
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     */
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
393    public native PlaybackParams getPlaybackParams();
394
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     */
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
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
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     */
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
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
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
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     */
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.
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
574    private native final void native_updateQueuedAudioData(
575            int sizeInBytes, long presentationTimeUs);
576
577    private native final long native_getPlayTimeForPendingAudioFrames();
578
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
605    private final void returnAudioBuffers() {
606        synchronized(mAudioLock) {
607            for (AudioBuffer audioBuffer: mAudioBuffers) {
608                postReturnByteBuffer(audioBuffer);
609            }
610            mAudioBuffers.clear();
611        }
612    }
613
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");
639        native_init();
640    }
641
642    private static native final void native_init();
643}
644