Synced to commit df5e5013422b81b4fd05c0ac9fd964b13624847a. Includes new samples for Android Auto. Change-Id: I3fec46e2a6b3f196682a92f1afd91eb682dc2dc1
388 lines
15 KiB
Java
388 lines
15 KiB
Java
/*
|
|
* Copyright (C) 2013 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 com.example.android.common.media;
|
|
|
|
import android.media.*;
|
|
import android.os.Handler;
|
|
import android.os.Looper;
|
|
import android.view.Surface;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.ArrayDeque;
|
|
import java.util.Queue;
|
|
|
|
/**
|
|
* Simplifies the MediaCodec interface by wrapping around the buffer processing operations.
|
|
*/
|
|
public class MediaCodecWrapper {
|
|
|
|
// Handler to use for {@code OutputSampleListener} and {code OutputFormatChangedListener}
|
|
// callbacks
|
|
private Handler mHandler;
|
|
|
|
|
|
// Callback when media output format changes.
|
|
public interface OutputFormatChangedListener {
|
|
void outputFormatChanged(MediaCodecWrapper sender, MediaFormat newFormat);
|
|
}
|
|
|
|
private OutputFormatChangedListener mOutputFormatChangedListener = null;
|
|
|
|
/**
|
|
* Callback for decodes frames. Observers can register a listener for optional stream
|
|
* of decoded data
|
|
*/
|
|
public interface OutputSampleListener {
|
|
void outputSample(MediaCodecWrapper sender, MediaCodec.BufferInfo info, ByteBuffer buffer);
|
|
}
|
|
|
|
/**
|
|
* The {@link MediaCodec} that is managed by this class.
|
|
*/
|
|
private MediaCodec mDecoder;
|
|
|
|
// References to the internal buffers managed by the codec. The codec
|
|
// refers to these buffers by index, never by reference so it's up to us
|
|
// to keep track of which buffer is which.
|
|
private ByteBuffer[] mInputBuffers;
|
|
private ByteBuffer[] mOutputBuffers;
|
|
|
|
// Indices of the input buffers that are currently available for writing. We'll
|
|
// consume these in the order they were dequeued from the codec.
|
|
private Queue<Integer> mAvailableInputBuffers;
|
|
|
|
// Indices of the output buffers that currently hold valid data, in the order
|
|
// they were produced by the codec.
|
|
private Queue<Integer> mAvailableOutputBuffers;
|
|
|
|
// Information about each output buffer, by index. Each entry in this array
|
|
// is valid if and only if its index is currently contained in mAvailableOutputBuffers.
|
|
private MediaCodec.BufferInfo[] mOutputBufferInfo;
|
|
|
|
// An (optional) stream that will receive decoded data.
|
|
private OutputSampleListener mOutputSampleListener;
|
|
|
|
private MediaCodecWrapper(MediaCodec codec) {
|
|
mDecoder = codec;
|
|
codec.start();
|
|
mInputBuffers = codec.getInputBuffers();
|
|
mOutputBuffers = codec.getOutputBuffers();
|
|
mOutputBufferInfo = new MediaCodec.BufferInfo[mOutputBuffers.length];
|
|
mAvailableInputBuffers = new ArrayDeque<Integer>(mOutputBuffers.length);
|
|
mAvailableOutputBuffers = new ArrayDeque<Integer>(mInputBuffers.length);
|
|
}
|
|
|
|
/**
|
|
* Releases resources and ends the encoding/decoding session.
|
|
*/
|
|
public void stopAndRelease() {
|
|
mDecoder.stop();
|
|
mDecoder.release();
|
|
mDecoder = null;
|
|
mHandler = null;
|
|
}
|
|
|
|
/**
|
|
* Getter for the registered {@link OutputFormatChangedListener}
|
|
*/
|
|
public OutputFormatChangedListener getOutputFormatChangedListener() {
|
|
return mOutputFormatChangedListener;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param outputFormatChangedListener the listener for callback.
|
|
* @param handler message handler for posting the callback.
|
|
*/
|
|
public void setOutputFormatChangedListener(final OutputFormatChangedListener
|
|
outputFormatChangedListener, Handler handler) {
|
|
mOutputFormatChangedListener = outputFormatChangedListener;
|
|
|
|
// Making sure we don't block ourselves due to a bad implementation of the callback by
|
|
// using a handler provided by client.
|
|
Looper looper;
|
|
mHandler = handler;
|
|
if (outputFormatChangedListener != null && mHandler == null) {
|
|
if ((looper = Looper.myLooper()) != null) {
|
|
mHandler = new Handler();
|
|
} else {
|
|
throw new IllegalArgumentException(
|
|
"Looper doesn't exist in the calling thread");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructs the {@link MediaCodecWrapper} wrapper object around the video codec.
|
|
* The codec is created using the encapsulated information in the
|
|
* {@link MediaFormat} object.
|
|
*
|
|
* @param trackFormat The format of the media object to be decoded.
|
|
* @param surface Surface to render the decoded frames.
|
|
* @return
|
|
*/
|
|
public static MediaCodecWrapper fromVideoFormat(final MediaFormat trackFormat,
|
|
Surface surface) throws IOException {
|
|
MediaCodecWrapper result = null;
|
|
MediaCodec videoCodec = null;
|
|
|
|
// BEGIN_INCLUDE(create_codec)
|
|
final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
|
|
|
|
// Check to see if this is actually a video mime type. If it is, then create
|
|
// a codec that can decode this mime type.
|
|
if (mimeType.contains("video/")) {
|
|
videoCodec = MediaCodec.createDecoderByType(mimeType);
|
|
videoCodec.configure(trackFormat, surface, null, 0);
|
|
|
|
}
|
|
|
|
// If codec creation was successful, then create a wrapper object around the
|
|
// newly created codec.
|
|
if (videoCodec != null) {
|
|
result = new MediaCodecWrapper(videoCodec);
|
|
}
|
|
// END_INCLUDE(create_codec)
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Write a media sample to the decoder.
|
|
*
|
|
* A "sample" here refers to a single atomic access unit in the media stream. The definition
|
|
* of "access unit" is dependent on the type of encoding used, but it typically refers to
|
|
* a single frame of video or a few seconds of audio. {@link android.media.MediaExtractor}
|
|
* extracts data from a stream one sample at a time.
|
|
*
|
|
* @param input A ByteBuffer containing the input data for one sample. The buffer must be set
|
|
* up for reading, with its position set to the beginning of the sample data and its limit
|
|
* set to the end of the sample data.
|
|
*
|
|
* @param presentationTimeUs The time, relative to the beginning of the media stream,
|
|
* at which this buffer should be rendered.
|
|
*
|
|
* @param flags Flags to pass to the decoder. See {@link MediaCodec#queueInputBuffer(int,
|
|
* int, int, long, int)}
|
|
*
|
|
* @throws MediaCodec.CryptoException
|
|
*/
|
|
public boolean writeSample(final ByteBuffer input,
|
|
final MediaCodec.CryptoInfo crypto,
|
|
final long presentationTimeUs,
|
|
final int flags) throws MediaCodec.CryptoException, WriteException {
|
|
boolean result = false;
|
|
int size = input.remaining();
|
|
|
|
// check if we have dequed input buffers available from the codec
|
|
if (size > 0 && !mAvailableInputBuffers.isEmpty()) {
|
|
int index = mAvailableInputBuffers.remove();
|
|
ByteBuffer buffer = mInputBuffers[index];
|
|
|
|
// we can't write our sample to a lesser capacity input buffer.
|
|
if (size > buffer.capacity()) {
|
|
throw new MediaCodecWrapper.WriteException(String.format(
|
|
"Insufficient capacity in MediaCodec buffer: "
|
|
+ "tried to write %d, buffer capacity is %d.",
|
|
input.remaining(),
|
|
buffer.capacity()));
|
|
}
|
|
|
|
buffer.clear();
|
|
buffer.put(input);
|
|
|
|
// Submit the buffer to the codec for decoding. The presentationTimeUs
|
|
// indicates the position (play time) for the current sample.
|
|
if (crypto == null) {
|
|
mDecoder.queueInputBuffer(index, 0, size, presentationTimeUs, flags);
|
|
} else {
|
|
mDecoder.queueSecureInputBuffer(index, 0, crypto, presentationTimeUs, flags);
|
|
}
|
|
result = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static MediaCodec.CryptoInfo cryptoInfo= new MediaCodec.CryptoInfo();
|
|
|
|
/**
|
|
* Write a media sample to the decoder.
|
|
*
|
|
* A "sample" here refers to a single atomic access unit in the media stream. The definition
|
|
* of "access unit" is dependent on the type of encoding used, but it typically refers to
|
|
* a single frame of video or a few seconds of audio. {@link android.media.MediaExtractor}
|
|
* extracts data from a stream one sample at a time.
|
|
*
|
|
* @param extractor Instance of {@link android.media.MediaExtractor} wrapping the media.
|
|
*
|
|
* @param presentationTimeUs The time, relative to the beginning of the media stream,
|
|
* at which this buffer should be rendered.
|
|
*
|
|
* @param flags Flags to pass to the decoder. See {@link MediaCodec#queueInputBuffer(int,
|
|
* int, int, long, int)}
|
|
*
|
|
* @throws MediaCodec.CryptoException
|
|
*/
|
|
public boolean writeSample(final MediaExtractor extractor,
|
|
final boolean isSecure,
|
|
final long presentationTimeUs,
|
|
int flags) {
|
|
boolean result = false;
|
|
boolean isEos = false;
|
|
|
|
if (!mAvailableInputBuffers.isEmpty()) {
|
|
int index = mAvailableInputBuffers.remove();
|
|
ByteBuffer buffer = mInputBuffers[index];
|
|
|
|
// reads the sample from the file using extractor into the buffer
|
|
int size = extractor.readSampleData(buffer, 0);
|
|
if (size <= 0) {
|
|
flags |= MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
|
}
|
|
|
|
// Submit the buffer to the codec for decoding. The presentationTimeUs
|
|
// indicates the position (play time) for the current sample.
|
|
if (!isSecure) {
|
|
mDecoder.queueInputBuffer(index, 0, size, presentationTimeUs, flags);
|
|
} else {
|
|
extractor.getSampleCryptoInfo(cryptoInfo);
|
|
mDecoder.queueSecureInputBuffer(index, 0, cryptoInfo, presentationTimeUs, flags);
|
|
}
|
|
|
|
result = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Performs a peek() operation in the queue to extract media info for the buffer ready to be
|
|
* released i.e. the head element of the queue.
|
|
*
|
|
* @param out_bufferInfo An output var to hold the buffer info.
|
|
*
|
|
* @return True, if the peek was successful.
|
|
*/
|
|
public boolean peekSample(MediaCodec.BufferInfo out_bufferInfo) {
|
|
// dequeue available buffers and synchronize our data structures with the codec.
|
|
update();
|
|
boolean result = false;
|
|
if (!mAvailableOutputBuffers.isEmpty()) {
|
|
int index = mAvailableOutputBuffers.peek();
|
|
MediaCodec.BufferInfo info = mOutputBufferInfo[index];
|
|
// metadata of the sample
|
|
out_bufferInfo.set(
|
|
info.offset,
|
|
info.size,
|
|
info.presentationTimeUs,
|
|
info.flags);
|
|
result = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Processes, releases and optionally renders the output buffer available at the head of the
|
|
* queue. All observers are notified with a callback. See {@link
|
|
* OutputSampleListener#outputSample(MediaCodecWrapper, android.media.MediaCodec.BufferInfo,
|
|
* java.nio.ByteBuffer)}
|
|
*
|
|
* @param render True, if the buffer is to be rendered on the {@link Surface} configured
|
|
*
|
|
*/
|
|
public void popSample(boolean render) {
|
|
// dequeue available buffers and synchronize our data structures with the codec.
|
|
update();
|
|
if (!mAvailableOutputBuffers.isEmpty()) {
|
|
int index = mAvailableOutputBuffers.remove();
|
|
|
|
if (render && mOutputSampleListener != null) {
|
|
ByteBuffer buffer = mOutputBuffers[index];
|
|
MediaCodec.BufferInfo info = mOutputBufferInfo[index];
|
|
mOutputSampleListener.outputSample(this, info, buffer);
|
|
}
|
|
|
|
// releases the buffer back to the codec
|
|
mDecoder.releaseOutputBuffer(index, render);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Synchronize this object's state with the internal state of the wrapped
|
|
* MediaCodec.
|
|
*/
|
|
private void update() {
|
|
// BEGIN_INCLUDE(update_codec_state)
|
|
int index;
|
|
|
|
// Get valid input buffers from the codec to fill later in the same order they were
|
|
// made available by the codec.
|
|
while ((index = mDecoder.dequeueInputBuffer(0)) != MediaCodec.INFO_TRY_AGAIN_LATER) {
|
|
mAvailableInputBuffers.add(index);
|
|
}
|
|
|
|
|
|
// Likewise with output buffers. If the output buffers have changed, start using the
|
|
// new set of output buffers. If the output format has changed, notify listeners.
|
|
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
|
|
while ((index = mDecoder.dequeueOutputBuffer(info, 0)) != MediaCodec.INFO_TRY_AGAIN_LATER) {
|
|
switch (index) {
|
|
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
|
mOutputBuffers = mDecoder.getOutputBuffers();
|
|
mOutputBufferInfo = new MediaCodec.BufferInfo[mOutputBuffers.length];
|
|
mAvailableOutputBuffers.clear();
|
|
break;
|
|
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
|
if (mOutputFormatChangedListener != null) {
|
|
mHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
mOutputFormatChangedListener
|
|
.outputFormatChanged(MediaCodecWrapper.this,
|
|
mDecoder.getOutputFormat());
|
|
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
default:
|
|
// Making sure the index is valid before adding to output buffers. We've already
|
|
// handled INFO_TRY_AGAIN_LATER, INFO_OUTPUT_FORMAT_CHANGED &
|
|
// INFO_OUTPUT_BUFFERS_CHANGED i.e all the other possible return codes but
|
|
// asserting index value anyways for future-proofing the code.
|
|
if(index >= 0) {
|
|
mOutputBufferInfo[index] = info;
|
|
mAvailableOutputBuffers.add(index);
|
|
} else {
|
|
throw new IllegalStateException("Unknown status from dequeueOutputBuffer");
|
|
}
|
|
break;
|
|
}
|
|
|
|
}
|
|
// END_INCLUDE(update_codec_state)
|
|
|
|
}
|
|
|
|
private class WriteException extends Throwable {
|
|
private WriteException(final String detailMessage) {
|
|
super(detailMessage);
|
|
}
|
|
}
|
|
}
|