/* * 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 mAvailableInputBuffers; // Indices of the output buffers that currently hold valid data, in the order // they were produced by the codec. private Queue 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(mOutputBuffers.length); mAvailableOutputBuffers = new ArrayDeque(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); } } }