blob: e6bc10dce4e16bf345c6e2c269ad7072b82c6d1d [file] [log] [blame]
/*
* 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.media.AudioTrack;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.Surface;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;
/**
* MediaSync class can be used to synchronously playback 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.configureSurface(surface);
* Surface inputSurface = sync.createInputSurface();
* ...
* // MediaCodec videoDecoder = ...;
* videoDecoder.configure(format, inputSurface, ...);
* ...
* sync.configureAudioTrack(audioTrack);
* sync.setCallback(new MediaSync.Callback() {
* {@literal @Override}
* public void onReturnAudioBuffer(MediaSync sync, ByteBuffer audioBuffer, int bufferIndex) {
* ...
* }
* }, null);
* // This needs to be done since sync is paused on creation.
* sync.setPlaybackRate(1.0f, MediaSync.PLAYBACK_RATE_AUDIO_MODE_RESAMPLE);
*
* for (;;) {
* ...
* // send video frames to surface for rendering, e.g., call
* // videoDecoder.releaseOutputBuffer(videoOutputBufferIx, videoPresentationTimeNs);
* // More details are available as below.
* ...
* sync.queueAudio(audioByteBuffer, bufferIndex, size, audioPresentationTimeUs); // non-blocking.
* // The audioByteBuffer and bufferIndex will be returned via callback.
* // More details are available as below.
* ...
* ...
* }
* sync.setPlaybackRate(0.0f, MediaSync.PLAYBACK_RATE_AUDIO_MODE_RESAMPLE);
* 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 bufferIndex, BufferInfo info) {
* // ...
* if (codec == videoDecoder) {
* // surface timestamp must contain media presentation time in nanoseconds.
* codec.releaseOutputBuffer(bufferIndex, 1000 * info.presentationTime);
* } else {
* ByteBuffer audioByteBuffer = codec.getOutputBuffer(bufferIndex);
* sync.queueByteBuffer(audioByteBuffer, bufferIndex, info.size, info.presentationTime);
* }
* // ...
* }
*
* // This is the callback from MediaSync.
* onReturnAudioBuffer(MediaSync sync, ByteBuffer buffer, int bufferIndex) {
* // ...
* audioDecoder.releaseBuffer(bufferIndex, false);
* // ...
* }
*
* </pre>
*
* The client needs to configure corresponding sink (i.e., Surface and 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#onReturnAudioBuffer}
* 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>
*/
final public 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.
*/
public abstract void onReturnAudioBuffer(
MediaSync sync, ByteBuffer audioBuffer, int bufferIndex);
}
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;
public int mSizeInBytes;
long mPresentationTimeUs;
public AudioBuffer(ByteBuffer byteBuffer, int bufferIndex,
int sizeInBytes, long presentationTimeUs) {
mByteBuffer = byteBuffer;
mBufferIndex = bufferIndex;
mSizeInBytes = sizeInBytes;
mPresentationTimeUs = presentationTimeUs;
}
}
private final Object mCallbackLock = new Object();
private Handler mCallbackHandler = null;
private MediaSync.Callback mCallback = 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>();
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.
* It shouldn't be called inside callback.
*
* @param cb The callback that will run.
* @param handler The Handler that will run the callback. Using null means to use MediaSync's
* internal handler if it exists.
*/
public void setCallback(/* MediaSync. */ Callback cb, 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;
}
}
/**
* Configures the output surface for MediaSync.
*
* @param surface Specify a surface on which to render the video data.
* @throws IllegalArgumentException if the surface has been released, or is invalid.
* or can not be connected.
* @throws IllegalStateException if not in the Initialized state, or another surface
* has already been configured.
*/
public void configureSurface(Surface surface) {
native_configureSurface(surface);
}
private native final void native_configureSurface(Surface surface);
/**
* Configures the audio track for MediaSync.
*
* @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 not in the Initialized state, or another audio track
* has already been configured.
*/
public void configureAudioTrack(AudioTrack audioTrack) {
// AudioTrack has sanity check for configured sample rate.
int nativeSampleRateInHz = (audioTrack == null ? 0 : audioTrack.getSampleRate());
native_configureAudioTrack(audioTrack, nativeSampleRateInHz);
mAudioTrack = audioTrack;
if (audioTrack != null && mAudioThread == null) {
createAudioThread();
}
}
private native final void native_configureAudioTrack(
AudioTrack audioTrack, int nativeSampleRateInHz);
/**
* Requests a Surface to use as the input. This may only be called after
* {@link #configureSurface}.
* <p>
* The application is responsible for calling release() on the Surface when
* done.
* @throws IllegalStateException if not configured, or another input surface has
* already been created.
*/
public native final Surface createInputSurface();
/**
* Specifies resampling as audio mode for variable rate playback, i.e.,
* resample the waveform based on the requested playback rate to get
* a new waveform, and play back the new waveform at the original sampling
* frequency.
* When rate is larger than 1.0, pitch becomes higher.
* When rate is smaller than 1.0, pitch becomes lower.
*/
public static final int PLAYBACK_RATE_AUDIO_MODE_RESAMPLE = 0;
/**
* Specifies time stretching as audio mode for variable rate playback.
* Time stretching changes the duration of the audio samples without
* affecting its pitch.
* FIXME: implement time strectching.
* @hide
*/
public static final int PLAYBACK_RATE_AUDIO_MODE_STRETCH = 1;
/** @hide */
@IntDef(
value = {
PLAYBACK_RATE_AUDIO_MODE_RESAMPLE,
PLAYBACK_RATE_AUDIO_MODE_STRETCH })
@Retention(RetentionPolicy.SOURCE)
public @interface PlaybackRateAudioMode {}
/**
* Sets playback rate. It does same as {@link #setPlaybackRate(float, int)},
* except that it always uses {@link #PLAYBACK_RATE_AUDIO_MODE_STRETCH} for audioMode.
*
* @param rate the ratio between desired playback rate and normal one. 1.0 means normal
* playback speed. 0.0 means stop or pause. Value larger than 1.0 means faster playback,
* while value between 0.0 and 1.0 for slower playback.
*
* @throws IllegalStateException if the internal sync engine or the audio track has not
* been initialized.
* TODO: unhide when PLAYBACK_RATE_AUDIO_MODE_STRETCH is supported.
* @hide
*/
public void setPlaybackRate(float rate) {
setPlaybackRate(rate, PLAYBACK_RATE_AUDIO_MODE_STRETCH);
}
/**
* Sets playback rate and audio mode.
*
* <p> The supported audio modes are:
* <ul>
* <li> {@link #PLAYBACK_RATE_AUDIO_MODE_RESAMPLE}
* </ul>
*
* @param rate the ratio between desired playback rate and normal one. 1.0 means normal
* playback speed. 0.0 means stop or pause. Value larger than 1.0 means faster playback,
* while value between 0.0 and 1.0 for slower playback.
* @param audioMode audio playback mode. Must be one of the supported
* audio modes.
*
* @throws IllegalStateException if the internal sync engine or the audio track has not
* been initialized.
* @throws IllegalArgumentException if audioMode is not supported.
*/
public void setPlaybackRate(float rate, @PlaybackRateAudioMode int audioMode) {
if (!isAudioPlaybackModeSupported(audioMode)) {
final String msg = "Audio playback mode " + audioMode + " is not supported";
throw new IllegalArgumentException(msg);
}
int status = AudioTrack.SUCCESS;
if (mAudioTrack != null) {
int nativeSampleRateInHz = mAudioTrack.getSampleRate();
int playbackSampleRate = (int)(rate * nativeSampleRateInHz + 0.5);
rate = playbackSampleRate / (float)nativeSampleRateInHz;
try {
if (rate == 0.0) {
mAudioTrack.pause();
} else {
status = mAudioTrack.setPlaybackRate(playbackSampleRate);
mAudioTrack.play();
}
} catch (IllegalStateException e) {
throw e;
}
}
if (status != AudioTrack.SUCCESS) {
throw new IllegalArgumentException("Fail to set playback rate in audio track");
}
synchronized(mAudioLock) {
mPlaybackRate = rate;
}
if (mPlaybackRate != 0.0 && mAudioThread != null) {
postRenderAudio(0);
}
native_setPlaybackRate(mPlaybackRate);
}
private native final void native_setPlaybackRate(float rate);
/*
* Test whether a given audio playback mode is supported.
* TODO query supported AudioPlaybackMode from audio track.
*/
private boolean isAudioPlaybackModeSupported(int mode) {
return (mode == PLAYBACK_RATE_AUDIO_MODE_RESAMPLE);
}
/**
* Get current playback position.
* <p>
* The MediaTimestamp represents a clock ticking during media playback. It's represented
* by an anchor frame ({@link MediaTimestamp#mediaTimeUs} and {@link MediaTimestamp#nanoTime})
* and clock speed ({@link MediaTimestamp#clockRate}). For continous playback with
* constant speed, its anchor frame doesn't change that often. Thereafter, it's recommended
* to not call this method often.
* <p>
* To help users to get current playback position, this method always returns the timestamp of
* just-rendered frame, i.e., {@link System#nanoTime} and its corresponding media time. They
* can be used as current playback position.
*
* @param timestamp a reference to a non-null MediaTimestamp instance allocated
* and owned by caller.
* @return true if a timestamp is available, or false if no timestamp is available.
* If a timestamp if available, the MediaTimestamp instance is filled in with
* playback rate, together with the current media timestamp and the system nanoTime
* corresponding to the measured media timestamp.
* In the case that no timestamp is available, any supplied instance is left unaltered.
*/
public boolean getTimestamp(@NonNull MediaTimestamp timestamp)
{
if (timestamp == null) {
throw new IllegalArgumentException();
}
return native_getTimestamp(timestamp);
}
private native final boolean native_getTimestamp(MediaTimestamp timestamp);
/**
* Queues the audio data asynchronously for playback (AudioTrack must be in streaming mode).
* @param audioData the buffer that holds the data to play. This buffer will be returned
* to the client via registered callback.
* @param bufferIndex the buffer index used to identify audioData. It will be returned to
* the client along with audioData. This helps applications to keep track of audioData.
* @param sizeInBytes number of bytes to queue.
* @param presentationTimeUs the presentation timestamp in microseconds for the first frame
* in the buffer.
* @throws IllegalStateException if audio track is not configured or internal configureation
* has not been done correctly.
*/
public void queueAudio(
ByteBuffer audioData, int bufferIndex, int sizeInBytes, long presentationTimeUs) {
if (mAudioTrack == null || mAudioThread == null) {
throw new IllegalStateException(
"AudioTrack is NOT configured or audio thread is not created");
}
synchronized(mAudioLock) {
mAudioBuffers.add(new AudioBuffer(
audioData, bufferIndex, sizeInBytes, 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 sizeWritten = mAudioTrack.write(
audioBuffer.mByteBuffer,
audioBuffer.mSizeInBytes,
AudioTrack.WRITE_NON_BLOCKING);
if (sizeWritten > 0) {
if (audioBuffer.mPresentationTimeUs != -1) {
native_updateQueuedAudioData(
audioBuffer.mSizeInBytes, audioBuffer.mPresentationTimeUs);
audioBuffer.mPresentationTimeUs = -1;
}
if (sizeWritten == audioBuffer.mSizeInBytes) {
postReturnByteBuffer(audioBuffer);
mAudioBuffers.remove(0);
if (!mAudioBuffers.isEmpty()) {
postRenderAudio(0);
}
return;
}
audioBuffer.mSizeInBytes -= sizeWritten;
}
// TODO: wait time depends on fullness of audio track.
postRenderAudio(10);
}
}
}, delayMillis);
}
private native final void native_updateQueuedAudioData(
int sizeInBytes, long presentationTimeUs);
private final void postReturnByteBuffer(final AudioBuffer audioBuffer) {
synchronized(mCallbackLock) {
if (mCallbackHandler != null) {
final MediaSync sync = this;
mCallbackHandler.post(new Runnable() {
public void run() {
synchronized(mCallbackLock) {
if (mCallbackHandler == null
|| mCallbackHandler.getLooper().getThread()
!= Thread.currentThread()) {
// callback handler has been changed.
return;
}
if (mCallback != null) {
mCallback.onReturnAudioBuffer(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();
}