| /* |
| * Copyright (C) 2015 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 android.media; |
| |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.media.AudioTrack; |
| import android.media.PlaybackParams; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.util.Log; |
| import android.view.Surface; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.nio.ByteBuffer; |
| import java.util.concurrent.TimeUnit; |
| import java.util.LinkedList; |
| import java.util.List; |
| |
| /** |
| * MediaSync class can be used to synchronously play audio and video streams. |
| * It can be used to play audio-only or video-only stream, too. |
| * |
| * <p>MediaSync is generally used like this: |
| * <pre> |
| * MediaSync sync = new MediaSync(); |
| * sync.setSurface(surface); |
| * Surface inputSurface = sync.createInputSurface(); |
| * ... |
| * // MediaCodec videoDecoder = ...; |
| * videoDecoder.configure(format, inputSurface, ...); |
| * ... |
| * sync.setAudioTrack(audioTrack); |
| * sync.setCallback(new MediaSync.Callback() { |
| * {@literal @Override} |
| * public void onAudioBufferConsumed(MediaSync sync, ByteBuffer audioBuffer, int bufferId) { |
| * ... |
| * } |
| * }, null); |
| * // This needs to be done since sync is paused on creation. |
| * sync.setPlaybackParams(new PlaybackParams().setSpeed(1.f)); |
| * |
| * for (;;) { |
| * ... |
| * // send video frames to surface for rendering, e.g., call |
| * // videoDecoder.releaseOutputBuffer(videoOutputBufferIx, videoPresentationTimeNs); |
| * // More details are available as below. |
| * ... |
| * sync.queueAudio(audioByteBuffer, bufferId, audioPresentationTimeUs); // non-blocking. |
| * // The audioByteBuffer and bufferId will be returned via callback. |
| * // More details are available as below. |
| * ... |
| * ... |
| * } |
| * sync.setPlaybackParams(new PlaybackParams().setSpeed(0.f)); |
| * sync.release(); |
| * sync = null; |
| * |
| * // The following code snippet illustrates how video/audio raw frames are created by |
| * // MediaCodec's, how they are fed to MediaSync and how they are returned by MediaSync. |
| * // This is the callback from MediaCodec. |
| * onOutputBufferAvailable(MediaCodec codec, int bufferId, BufferInfo info) { |
| * // ... |
| * if (codec == videoDecoder) { |
| * // surface timestamp must contain media presentation time in nanoseconds. |
| * codec.releaseOutputBuffer(bufferId, 1000 * info.presentationTime); |
| * } else { |
| * ByteBuffer audioByteBuffer = codec.getOutputBuffer(bufferId); |
| * sync.queueAudio(audioByteBuffer, bufferId, info.presentationTime); |
| * } |
| * // ... |
| * } |
| * |
| * // This is the callback from MediaSync. |
| * onAudioBufferConsumed(MediaSync sync, ByteBuffer buffer, int bufferId) { |
| * // ... |
| * audioDecoder.releaseBuffer(bufferId, false); |
| * // ... |
| * } |
| * |
| * </pre> |
| * |
| * The client needs to configure corresponding sink by setting the Surface and/or AudioTrack |
| * based on the stream type it will play. |
| * <p> |
| * For video, the client needs to call {@link #createInputSurface} to obtain a surface on |
| * which it will render video frames. |
| * <p> |
| * For audio, the client needs to set up audio track correctly, e.g., using {@link |
| * AudioTrack#MODE_STREAM}. The audio buffers are sent to MediaSync directly via {@link |
| * #queueAudio}, and are returned to the client via {@link Callback#onAudioBufferConsumed} |
| * asynchronously. The client should not modify an audio buffer till it's returned. |
| * <p> |
| * The client can optionally pre-fill audio/video buffers by setting playback rate to 0.0, |
| * and then feed audio/video buffers to corresponding components. This can reduce possible |
| * initial underrun. |
| * <p> |
| */ |
| public final class MediaSync { |
| /** |
| * MediaSync callback interface. Used to notify the user asynchronously |
| * of various MediaSync events. |
| */ |
| public static abstract class Callback { |
| /** |
| * Called when returning an audio buffer which has been consumed. |
| * |
| * @param sync The MediaSync object. |
| * @param audioBuffer The returned audio buffer. |
| * @param bufferId The ID associated with audioBuffer as passed into |
| * {@link MediaSync#queueAudio}. |
| */ |
| public abstract void onAudioBufferConsumed( |
| @NonNull MediaSync sync, @NonNull ByteBuffer audioBuffer, int bufferId); |
| } |
| |
| /** Audio track failed. |
| * @see android.media.MediaSync.OnErrorListener |
| */ |
| public static final int MEDIASYNC_ERROR_AUDIOTRACK_FAIL = 1; |
| |
| /** The surface failed to handle video buffers. |
| * @see android.media.MediaSync.OnErrorListener |
| */ |
| public static final int MEDIASYNC_ERROR_SURFACE_FAIL = 2; |
| |
| /** |
| * Interface definition of a callback to be invoked when there |
| * has been an error during an asynchronous operation (other errors |
| * will throw exceptions at method call time). |
| */ |
| public interface OnErrorListener { |
| /** |
| * Called to indicate an error. |
| * |
| * @param sync The MediaSync the error pertains to |
| * @param what The type of error that has occurred: |
| * <ul> |
| * <li>{@link #MEDIASYNC_ERROR_AUDIOTRACK_FAIL} |
| * <li>{@link #MEDIASYNC_ERROR_SURFACE_FAIL} |
| * </ul> |
| * @param extra an extra code, specific to the error. Typically |
| * implementation dependent. |
| */ |
| void onError(@NonNull MediaSync sync, int what, int extra); |
| } |
| |
| private static final String TAG = "MediaSync"; |
| |
| private static final int EVENT_CALLBACK = 1; |
| private static final int EVENT_SET_CALLBACK = 2; |
| |
| private static final int CB_RETURN_AUDIO_BUFFER = 1; |
| |
| private static class AudioBuffer { |
| public ByteBuffer mByteBuffer; |
| public int mBufferIndex; |
| long mPresentationTimeUs; |
| |
| public AudioBuffer(@NonNull ByteBuffer byteBuffer, int bufferId, |
| long presentationTimeUs) { |
| mByteBuffer = byteBuffer; |
| mBufferIndex = bufferId; |
| mPresentationTimeUs = presentationTimeUs; |
| } |
| } |
| |
| private final Object mCallbackLock = new Object(); |
| private Handler mCallbackHandler = null; |
| private MediaSync.Callback mCallback = null; |
| |
| private final Object mOnErrorListenerLock = new Object(); |
| private Handler mOnErrorListenerHandler = null; |
| private MediaSync.OnErrorListener mOnErrorListener = null; |
| |
| private Thread mAudioThread = null; |
| // Created on mAudioThread when mAudioThread is started. When used on user thread, they should |
| // be guarded by checking mAudioThread. |
| private Handler mAudioHandler = null; |
| private Looper mAudioLooper = null; |
| |
| private final Object mAudioLock = new Object(); |
| private AudioTrack mAudioTrack = null; |
| private List<AudioBuffer> mAudioBuffers = new LinkedList<AudioBuffer>(); |
| // this is only used for paused/running decisions, so it is not affected by clock drift |
| private float mPlaybackRate = 0.0f; |
| |
| private long mNativeContext; |
| |
| /** |
| * Class constructor. On creation, MediaSync is paused, i.e., playback rate is 0.0f. |
| */ |
| public MediaSync() { |
| native_setup(); |
| } |
| |
| private native final void native_setup(); |
| |
| @Override |
| protected void finalize() { |
| native_finalize(); |
| } |
| |
| private native final void native_finalize(); |
| |
| /** |
| * Make sure you call this when you're done to free up any opened |
| * component instance instead of relying on the garbage collector |
| * to do this for you at some point in the future. |
| */ |
| public final void release() { |
| returnAudioBuffers(); |
| if (mAudioThread != null) { |
| if (mAudioLooper != null) { |
| mAudioLooper.quit(); |
| } |
| } |
| setCallback(null, null); |
| native_release(); |
| } |
| |
| private native final void native_release(); |
| |
| /** |
| * Sets an asynchronous callback for actionable MediaSync events. |
| * <p> |
| * This method can be called multiple times to update a previously set callback. If the |
| * handler is changed, undelivered notifications scheduled for the old handler may be dropped. |
| * <p> |
| * <b>Do not call this inside callback.</b> |
| * |
| * @param cb The callback that will run. Use {@code null} to stop receiving callbacks. |
| * @param handler The Handler that will run the callback. Use {@code null} to use MediaSync's |
| * internal handler if it exists. |
| */ |
| public void setCallback(@Nullable /* MediaSync. */ Callback cb, @Nullable Handler handler) { |
| synchronized(mCallbackLock) { |
| if (handler != null) { |
| mCallbackHandler = handler; |
| } else { |
| Looper looper; |
| if ((looper = Looper.myLooper()) == null) { |
| looper = Looper.getMainLooper(); |
| } |
| if (looper == null) { |
| mCallbackHandler = null; |
| } else { |
| mCallbackHandler = new Handler(looper); |
| } |
| } |
| |
| mCallback = cb; |
| } |
| } |
| |
| /** |
| * Sets an asynchronous callback for error events. |
| * <p> |
| * This method can be called multiple times to update a previously set listener. If the |
| * handler is changed, undelivered notifications scheduled for the old handler may be dropped. |
| * <p> |
| * <b>Do not call this inside callback.</b> |
| * |
| * @param listener The callback that will run. Use {@code null} to stop receiving callbacks. |
| * @param handler The Handler that will run the callback. Use {@code null} to use MediaSync's |
| * internal handler if it exists. |
| */ |
| public void setOnErrorListener(@Nullable /* MediaSync. */ OnErrorListener listener, |
| @Nullable Handler handler) { |
| synchronized(mOnErrorListenerLock) { |
| if (handler != null) { |
| mOnErrorListenerHandler = handler; |
| } else { |
| Looper looper; |
| if ((looper = Looper.myLooper()) == null) { |
| looper = Looper.getMainLooper(); |
| } |
| if (looper == null) { |
| mOnErrorListenerHandler = null; |
| } else { |
| mOnErrorListenerHandler = new Handler(looper); |
| } |
| } |
| |
| mOnErrorListener = listener; |
| } |
| } |
| |
| /** |
| * Sets the output surface for MediaSync. |
| * <p> |
| * Currently, this is only supported in the Initialized state. |
| * |
| * @param surface Specify a surface on which to render the video data. |
| * @throws IllegalArgumentException if the surface has been released, is invalid, |
| * or can not be connected. |
| * @throws IllegalStateException if setting the surface is not supported, e.g. |
| * not in the Initialized state, or another surface has already been set. |
| */ |
| public void setSurface(@Nullable Surface surface) { |
| native_setSurface(surface); |
| } |
| |
| private native final void native_setSurface(@Nullable Surface surface); |
| |
| /** |
| * Sets the audio track for MediaSync. |
| * <p> |
| * Currently, this is only supported in the Initialized state. |
| * |
| * @param audioTrack Specify an AudioTrack through which to render the audio data. |
| * @throws IllegalArgumentException if the audioTrack has been released, or is invalid. |
| * @throws IllegalStateException if setting the audio track is not supported, e.g. |
| * not in the Initialized state, or another audio track has already been set. |
| */ |
| public void setAudioTrack(@Nullable AudioTrack audioTrack) { |
| native_setAudioTrack(audioTrack); |
| mAudioTrack = audioTrack; |
| if (audioTrack != null && mAudioThread == null) { |
| createAudioThread(); |
| } |
| } |
| |
| private native final void native_setAudioTrack(@Nullable AudioTrack audioTrack); |
| |
| /** |
| * Requests a Surface to use as the input. This may only be called after |
| * {@link #setSurface}. |
| * <p> |
| * The application is responsible for calling release() on the Surface when |
| * done. |
| * @throws IllegalStateException if not set, or another input surface has |
| * already been created. |
| */ |
| @NonNull |
| public native final Surface createInputSurface(); |
| |
| /** |
| * Sets playback rate using {@link PlaybackParams}. |
| * <p> |
| * When using MediaSync with {@link AudioTrack}, set playback params using this |
| * call instead of calling it directly on the track, so that the sync is aware of |
| * the params change. |
| * <p> |
| * This call also works if there is no audio track. |
| * |
| * @param params the playback params to use. {@link PlaybackParams#getSpeed |
| * Speed} is the ratio between desired playback rate and normal one. 1.0 means |
| * normal playback speed. 0.0 means pause. Value larger than 1.0 means faster playback, |
| * while value between 0.0 and 1.0 for slower playback. <b>Note:</b> the normal rate |
| * does not change as a result of this call. To restore the original rate at any time, |
| * use speed of 1.0. |
| * |
| * @throws IllegalStateException if the internal sync engine or the audio track has not |
| * been initialized. |
| * @throws IllegalArgumentException if the params are not supported. |
| */ |
| public void setPlaybackParams(@NonNull PlaybackParams params) { |
| synchronized(mAudioLock) { |
| mPlaybackRate = native_setPlaybackParams(params);; |
| } |
| if (mPlaybackRate != 0.0 && mAudioThread != null) { |
| postRenderAudio(0); |
| } |
| } |
| |
| /** |
| * Gets the playback rate using {@link PlaybackParams}. |
| * |
| * @return the playback rate being used. |
| * |
| * @throws IllegalStateException if the internal sync engine or the audio track has not |
| * been initialized. |
| */ |
| @NonNull |
| public native PlaybackParams getPlaybackParams(); |
| |
| private native float native_setPlaybackParams(@NonNull PlaybackParams params); |
| |
| /** |
| * Sets A/V sync mode. |
| * |
| * @param params the A/V sync params to apply |
| * |
| * @throws IllegalStateException if the internal player engine has not been |
| * initialized. |
| * @throws IllegalArgumentException if params are not supported. |
| */ |
| public void setSyncParams(@NonNull SyncParams params) { |
| synchronized(mAudioLock) { |
| mPlaybackRate = native_setSyncParams(params);; |
| } |
| if (mPlaybackRate != 0.0 && mAudioThread != null) { |
| postRenderAudio(0); |
| } |
| } |
| |
| private native float native_setSyncParams(@NonNull SyncParams params); |
| |
| /** |
| * Gets the A/V sync mode. |
| * |
| * @return the A/V sync params |
| * |
| * @throws IllegalStateException if the internal player engine has not been |
| * initialized. |
| */ |
| @NonNull |
| public native SyncParams getSyncParams(); |
| |
| /** |
| * Flushes all buffers from the sync object. |
| * <p> |
| * All pending unprocessed audio and video buffers are discarded. If an audio track was |
| * configured, it is flushed and stopped. If a video output surface was configured, the |
| * last frame queued to it is left on the frame. Queue a blank video frame to clear the |
| * surface, |
| * <p> |
| * No callbacks are received for the flushed buffers. |
| * |
| * @throws IllegalStateException if the internal player engine has not been |
| * initialized. |
| */ |
| public void flush() { |
| synchronized(mAudioLock) { |
| mAudioBuffers.clear(); |
| mCallbackHandler.removeCallbacksAndMessages(null); |
| } |
| if (mAudioTrack != null) { |
| mAudioTrack.pause(); |
| mAudioTrack.flush(); |
| // Call stop() to signal to the AudioSink to completely fill the |
| // internal buffer before resuming playback. |
| mAudioTrack.stop(); |
| } |
| native_flush(); |
| } |
| |
| private native final void native_flush(); |
| |
| /** |
| * Get current playback position. |
| * <p> |
| * The MediaTimestamp represents how the media time correlates to the system time in |
| * a linear fashion using an anchor and a clock rate. During regular playback, the media |
| * time moves fairly constantly (though the anchor frame may be rebased to a current |
| * system time, the linear correlation stays steady). Therefore, this method does not |
| * need to be called often. |
| * <p> |
| * To help users get current playback position, this method always anchors the timestamp |
| * to the current {@link System#nanoTime system time}, so |
| * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position. |
| * |
| * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp |
| * is available, e.g. because the media player has not been initialized. |
| * |
| * @see MediaTimestamp |
| */ |
| @Nullable |
| public MediaTimestamp getTimestamp() |
| { |
| try { |
| // TODO: create the timestamp in native |
| MediaTimestamp timestamp = new MediaTimestamp(); |
| if (native_getTimestamp(timestamp)) { |
| return timestamp; |
| } else { |
| return null; |
| } |
| } catch (IllegalStateException e) { |
| return null; |
| } |
| } |
| |
| private native final boolean native_getTimestamp(@NonNull MediaTimestamp timestamp); |
| |
| /** |
| * Queues the audio data asynchronously for playback (AudioTrack must be in streaming mode). |
| * If the audio track was flushed as a result of {@link #flush}, it will be restarted. |
| * @param audioData the buffer that holds the data to play. This buffer will be returned |
| * to the client via registered callback. |
| * @param bufferId an integer used to identify audioData. It will be returned to |
| * the client along with audioData. This helps applications to keep track of audioData, |
| * e.g., it can be used to store the output buffer index used by the audio codec. |
| * @param presentationTimeUs the presentation timestamp in microseconds for the first frame |
| * in the buffer. |
| * @throws IllegalStateException if audio track is not set or internal configureation |
| * has not been done correctly. |
| */ |
| public void queueAudio( |
| @NonNull ByteBuffer audioData, int bufferId, long presentationTimeUs) { |
| if (mAudioTrack == null || mAudioThread == null) { |
| throw new IllegalStateException( |
| "AudioTrack is NOT set or audio thread is not created"); |
| } |
| |
| synchronized(mAudioLock) { |
| mAudioBuffers.add(new AudioBuffer(audioData, bufferId, presentationTimeUs)); |
| } |
| |
| if (mPlaybackRate != 0.0) { |
| postRenderAudio(0); |
| } |
| } |
| |
| // When called on user thread, make sure to check mAudioThread != null. |
| private void postRenderAudio(long delayMillis) { |
| mAudioHandler.postDelayed(new Runnable() { |
| public void run() { |
| synchronized(mAudioLock) { |
| if (mPlaybackRate == 0.0) { |
| return; |
| } |
| |
| if (mAudioBuffers.isEmpty()) { |
| return; |
| } |
| |
| AudioBuffer audioBuffer = mAudioBuffers.get(0); |
| int size = audioBuffer.mByteBuffer.remaining(); |
| // restart audio track after flush |
| if (size > 0 && mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) { |
| try { |
| mAudioTrack.play(); |
| } catch (IllegalStateException e) { |
| Log.w(TAG, "could not start audio track"); |
| } |
| } |
| int sizeWritten = mAudioTrack.write( |
| audioBuffer.mByteBuffer, |
| size, |
| AudioTrack.WRITE_NON_BLOCKING); |
| if (sizeWritten > 0) { |
| if (audioBuffer.mPresentationTimeUs != -1) { |
| native_updateQueuedAudioData( |
| size, audioBuffer.mPresentationTimeUs); |
| audioBuffer.mPresentationTimeUs = -1; |
| } |
| |
| if (sizeWritten == size) { |
| postReturnByteBuffer(audioBuffer); |
| mAudioBuffers.remove(0); |
| if (!mAudioBuffers.isEmpty()) { |
| postRenderAudio(0); |
| } |
| return; |
| } |
| } |
| long pendingTimeMs = TimeUnit.MICROSECONDS.toMillis( |
| native_getPlayTimeForPendingAudioFrames()); |
| postRenderAudio(pendingTimeMs / 2); |
| } |
| } |
| }, delayMillis); |
| } |
| |
| private native final void native_updateQueuedAudioData( |
| int sizeInBytes, long presentationTimeUs); |
| |
| private native final long native_getPlayTimeForPendingAudioFrames(); |
| |
| private final void postReturnByteBuffer(@NonNull final AudioBuffer audioBuffer) { |
| synchronized(mCallbackLock) { |
| if (mCallbackHandler != null) { |
| final MediaSync sync = this; |
| mCallbackHandler.post(new Runnable() { |
| public void run() { |
| Callback callback; |
| synchronized(mCallbackLock) { |
| callback = mCallback; |
| if (mCallbackHandler == null |
| || mCallbackHandler.getLooper().getThread() |
| != Thread.currentThread()) { |
| // callback handler has been changed. |
| return; |
| } |
| } |
| if (callback != null) { |
| callback.onAudioBufferConsumed(sync, audioBuffer.mByteBuffer, |
| audioBuffer.mBufferIndex); |
| } |
| } |
| }); |
| } |
| } |
| } |
| |
| private final void returnAudioBuffers() { |
| synchronized(mAudioLock) { |
| for (AudioBuffer audioBuffer: mAudioBuffers) { |
| postReturnByteBuffer(audioBuffer); |
| } |
| mAudioBuffers.clear(); |
| } |
| } |
| |
| private void createAudioThread() { |
| mAudioThread = new Thread() { |
| @Override |
| public void run() { |
| Looper.prepare(); |
| synchronized(mAudioLock) { |
| mAudioLooper = Looper.myLooper(); |
| mAudioHandler = new Handler(); |
| mAudioLock.notify(); |
| } |
| Looper.loop(); |
| } |
| }; |
| mAudioThread.start(); |
| |
| synchronized(mAudioLock) { |
| try { |
| mAudioLock.wait(); |
| } catch(InterruptedException e) { |
| } |
| } |
| } |
| |
| static { |
| System.loadLibrary("media_jni"); |
| native_init(); |
| } |
| |
| private static native final void native_init(); |
| } |