/*
* Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com
*
* This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/)
*
* Spydroid is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This source code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this source code; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package net.majorkernelpanic.streaming.hw;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import net.majorkernelpanic.streaming.hw.CodecManager.Codec;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log;
/**
*
* The purpose of this class is to detect and by-pass some bugs (or underspecified configuration) that
* encoders available through the MediaCodec API may have. <br />
* Feeding the encoder with a surface is not tested here.
* Some bugs you may have encountered:<br />
* <ul>
* <li>U and V panes reversed</li>
* <li>Some padding is needed after the Y pane</li>
* <li>stride!=width or slice-height!=height</li>
* </ul>
*/
@SuppressLint("NewApi")
public class EncoderDebugger {
public final static String TAG = "EncoderDebugger";
/** Prefix that will be used for all shared preferences saved by libstreaming. */
private static final String PREF_PREFIX = "libstreaming-";
/**
* If this is set to false the test will be run only once and the result
* will be saved in the shared preferences.
*/
private static final boolean DEBUG = false;
/** Set this to true to see more logs. */
private static final boolean VERBOSE = false;
/** Will be incremented every time this test is modified. */
private static final int VERSION = 3;
/** Bitrate that will be used with the encoder. */
private final static int BITRATE = 1000000;
/** Framerate that will be used to test the encoder. */
private final static int FRAMERATE = 20;
private final static String MIME_TYPE = "video/avc";
private final static int NB_DECODED = 34;
private final static int NB_ENCODED = 50;
private int mDecoderColorFormat, mEncoderColorFormat;
private String mDecoderName, mEncoderName, mErrorLog;
private MediaCodec mEncoder, mDecoder;
private int mWidth, mHeight, mSize;
private byte[] mSPS, mPPS;
private byte[] mData, mInitialImage;
private MediaFormat mDecOutputFormat;
private NV21Convertor mNV21;
private SharedPreferences mPreferences;
private byte[][] mVideo, mDecodedVideo;
private String mB64PPS, mB64SPS;
public synchronized static void asyncDebug(final Context context, final int width, final int height) {
new Thread(new Runnable() {
@Override
public void run() {
try {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
debug(prefs, width, height);
} catch (Exception e) {}
}
}).start();
}
public synchronized static EncoderDebugger debug(Context context, int width, int height) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return debug(prefs, width, height);
}
public synchronized static EncoderDebugger debug(SharedPreferences prefs, int width, int height) {
EncoderDebugger debugger = new EncoderDebugger(prefs, width, height);
debugger.debug();
return debugger;
}
public String getB64PPS() {
return mB64PPS;
}
public String getB64SPS() {
return mB64SPS;
}
public String getEncoderName() {
return mEncoderName;
}
public int getEncoderColorFormat() {
return mEncoderColorFormat;
}
/** This {@link NV21Convertor} will do the necessary work to feed properly the encoder. */
public NV21Convertor getNV21Convertor() {
return mNV21;
}
/** A log of all the errors that occured during the test. */
public String getErrorLog() {
return mErrorLog;
}
private EncoderDebugger(SharedPreferences prefs, int width, int height) {
mPreferences = prefs;
mWidth = width;
mHeight = height;
mSize = width*height;
reset();
}
private void reset() {
mNV21 = new NV21Convertor();
mVideo = new byte[NB_ENCODED][];
mDecodedVideo = new byte[NB_DECODED][];
mErrorLog = "";
mPPS = null;
mSPS = null;
}
private void debug() {
// If testing the phone again is not needed,
// we just restore the result from the shared preferences
if (!checkTestNeeded()) {
String resolution = mWidth+"x"+mHeight+"-";
boolean success = mPreferences.getBoolean(PREF_PREFIX+resolution+"success",false);
if (!success) {
throw new RuntimeException("Phone not supported with this resolution ("+mWidth+"x"+mHeight+")");
}
mNV21.setSize(mWidth, mHeight);
mNV21.setSliceHeigth(mPreferences.getInt(PREF_PREFIX+resolution+"sliceHeight", 0));
mNV21.setStride(mPreferences.getInt(PREF_PREFIX+resolution+"stride", 0));
mNV21.setYPadding(mPreferences.getInt(PREF_PREFIX+resolution+"padding", 0));
mNV21.setPlanar(mPreferences.getBoolean(PREF_PREFIX+resolution+"planar", false));
mNV21.setColorPanesReversed(mPreferences.getBoolean(PREF_PREFIX+resolution+"reversed", false));
mEncoderName = mPreferences.getString(PREF_PREFIX+resolution+"encoderName", "");
mEncoderColorFormat = mPreferences.getInt(PREF_PREFIX+resolution+"colorFormat", 0);
mB64PPS = mPreferences.getString(PREF_PREFIX+resolution+"pps", "");
mB64SPS = mPreferences.getString(PREF_PREFIX+resolution+"sps", "");
return;
}
if (VERBOSE) Log.d(TAG, ">>>> Testing the phone for resolution "+mWidth+"x"+mHeight);
// Builds a list of available encoders and decoders we may be able to use
// because they support some nice color formats
Codec[] encoders = CodecManager.findEncodersForMimeType(MIME_TYPE);
Codec[] decoders = CodecManager.findDecodersForMimeType(MIME_TYPE);
int count = 0, n = 1;
for (int i=0;i<encoders.length;i++) {
count += encoders[i].formats.length;
}
// Tries available encoders
for (int i=0;i<encoders.length;i++) {
for (int j=0;j<encoders[i].formats.length;j++) {
reset();
mEncoderName = encoders[i].name;
mEncoderColorFormat = encoders[i].formats[j];
if (VERBOSE) Log.v(TAG, ">> Test "+(n++)+"/"+count+": "+mEncoderName+" with color format "+mEncoderColorFormat+" at "+mWidth+"x"+mHeight);
// Converts from NV21 to YUV420 with the specified parameters
mNV21.setSize(mWidth, mHeight);
mNV21.setSliceHeigth(mHeight);
mNV21.setStride(mWidth);
mNV21.setYPadding(0);
mNV21.setEncoderColorFormat(mEncoderColorFormat);
// /!\ NV21Convertor can directly modify the input
createTestImage();
mData = mNV21.convert(mInitialImage);
try {
// Starts the encoder
configureEncoder();
searchSPSandPPS();
if (VERBOSE) Log.v(TAG, "SPS and PPS in b64: SPS="+mB64SPS+", PPS="+mB64PPS);
// Feeds the encoder with an image repeatidly to produce some NAL units
encode();
// We now try to decode the NALs with decoders available on the phone
boolean decoded = false;
for (int k=0;k<decoders.length && !decoded;k++) {
for (int l=0;l<decoders[k].formats.length && !decoded;l++) {
mDecoderName = decoders[k].name;
mDecoderColorFormat = decoders[k].formats[l];
try {
configureDecoder();
} catch (Exception e) {
if (VERBOSE) Log.d(TAG, mDecoderName+" can't be used with "+mDecoderColorFormat+" at "+mWidth+"x"+mHeight);
releaseDecoder();
break;
}
try {
decode(true);
if (VERBOSE) Log.d(TAG, mDecoderName+" successfully decoded the NALs (color format "+mDecoderColorFormat+")");
decoded = true;
} catch (Exception e) {
if (VERBOSE) Log.e(TAG, mDecoderName+" failed to decode the NALs");
e.printStackTrace();
} finally {
releaseDecoder();
}
}
}
if (!decoded) throw new RuntimeException("Failed to decode NALs from the encoder.");
// Compares the image before and after
if (!compareLumaPanes()) {
// TODO: try again with a different stride
// TODO: try again with the "stride" param
throw new RuntimeException("It is likely that stride!=width");
}
int padding;
if ((padding = checkPaddingNeeded())>0) {
if (padding<4096) {
if (VERBOSE) Log.d(TAG, "Some padding is needed: "+padding);
mNV21.setYPadding(padding);
createTestImage();
mData = mNV21.convert(mInitialImage);
encodeDecode();
} else {
// TODO: try again with a different sliceHeight
// TODO: try again with the "slice-height" param
throw new RuntimeException("It is likely that sliceHeight!=height");
}
}
createTestImage();
if (!compareChromaPanes(false)) {
if (compareChromaPanes(true)) {
mNV21.setColorPanesReversed(true);
if (VERBOSE) Log.d(TAG, "U and V pane are reversed");
} else {
throw new RuntimeException("Incorrect U or V pane...");
}
}
saveTestResult(true);
Log.v(TAG, "The encoder "+mEncoderName+" is usable with resolution "+mWidth+"x"+mHeight);
return;
} catch (Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw);
String stack = sw.toString();
String str = "Encoder "+mEncoderName+" cannot be used with color format "+mEncoderColorFormat;
if (VERBOSE) Log.e(TAG, str, e);
mErrorLog += str + "\n" + stack;
e.printStackTrace();
} finally {
releaseEncoder();
}
}
}
saveTestResult(false);
Log.e(TAG,"No usable encoder were found on the phone for resolution "+mWidth+"x"+mHeight);
throw new RuntimeException("No usable encoder were found on the phone for resolution "+mWidth+"x"+mHeight);
}
private boolean checkTestNeeded() {
String resolution = mWidth+"x"+mHeight+"-";
// Forces the test
if (DEBUG || mPreferences==null) return true;
// If the sdk has changed on the phone, or the version of the test
// it has to be run again
if (mPreferences.contains(PREF_PREFIX+resolution+"lastSdk")) {
int lastSdk = mPreferences.getInt(PREF_PREFIX+resolution+"lastSdk", 0);
int lastVersion = mPreferences.getInt(PREF_PREFIX+resolution+"lastVersion", 0);
if (Build.VERSION.SDK_INT>lastSdk || VERSION>lastVersion) {
return true;
}
} else {
return true;
}
return false;
}
/**
* Saves the result of the test in the shared preferences,
* we will run it again only if the SDK has changed on the phone,
* or if this test has been modified.
*/
private void saveTestResult(boolean success) {
String resolution = mWidth+"x"+mHeight+"-";
Editor editor = mPreferences.edit();
editor.putBoolean(PREF_PREFIX+resolution+"success", success);
if (success) {
editor.putInt(PREF_PREFIX+resolution+"lastSdk", Build.VERSION.SDK_INT);
editor.putInt(PREF_PREFIX+resolution+"lastVersion", VERSION);
editor.putInt(PREF_PREFIX+resolution+"sliceHeight", mNV21.getSliceHeigth());
editor.putInt(PREF_PREFIX+resolution+"stride", mNV21.getStride());
editor.putInt(PREF_PREFIX+resolution+"padding", mNV21.getYPadding());
editor.putBoolean(PREF_PREFIX+resolution+"planar", mNV21.getPlanar());
editor.putBoolean(PREF_PREFIX+resolution+"reversed", mNV21.getUVPanesReversed());
editor.putString(PREF_PREFIX+resolution+"encoderName", mEncoderName);
editor.putInt(PREF_PREFIX+resolution+"colorFormat", mEncoderColorFormat);
editor.putString(PREF_PREFIX+resolution+"encoderName", mEncoderName);
editor.putString(PREF_PREFIX+resolution+"pps", mB64PPS);
editor.putString(PREF_PREFIX+resolution+"sps", mB64SPS);
}
editor.commit();
}
/**
* Creates the test image that will be used to feed the encoder.
*/
private void createTestImage() {
mInitialImage = new byte[3*mSize/2];
for (int i=0;i<mSize;i++) {
mInitialImage[i] = (byte) (40+i%199);
}
for (int i=mSize;i<3*mSize/2;i+=2) {
mInitialImage[i] = (byte) (40+i%200);
mInitialImage[i+1] = (byte) (40+(i+99)%200);
}
}
/**
* Compares the Y pane of the initial image, and the Y pane
* after having encoded & decoded the image.
*/
private boolean compareLumaPanes() {
int d, e, f = 0;
for (int j=0;j<NB_DECODED;j++) {
for (int i=0;i<mSize;i+=10) {
d = (mInitialImage[i]&0xFF) - (mDecodedVideo[j][i]&0xFF);
e = (mInitialImage[i+1]&0xFF) - (mDecodedVideo[j][i+1]&0xFF);
d = d<0 ? -d : d;
e = e<0 ? -e : e;
if (d>50 && e>50) {
mDecodedVideo[j] = null;
f++;
break;
}
}
}
return f<=NB_DECODED/2;
}
private int checkPaddingNeeded() {
int i = 0, j = 3*mSize/2-1, max = 0;
int[] r = new int[NB_DECODED];
for (int k=0;k<NB_DECODED;k++) {
if (mDecodedVideo[k] != null) {
i = 0;
while (i<j && (mDecodedVideo[k][j-i]&0xFF)<50) i+=2;
if (i>0) {
r[k] = ((i>>6)<<6);
max = r[k]>max ? r[k] : max;
if (VERBOSE) Log.e(TAG,"Padding needed: "+r[k]);
} else {
if (VERBOSE) Log.v(TAG,"No padding needed.");
}
}
}
return ((max>>6)<<6);
}
/**
* Compares the U or V pane of the initial image, and the U or V pane
* after having encoded & decoded the image.
*/
private boolean compareChromaPanes(boolean crossed) {
int d, f = 0;
for (int j=0;j<NB_DECODED;j++) {
if (mDecodedVideo[j] != null) {
// We compare the U and V pane before and after
if (!crossed) {
for (int i=mSize;i<3*mSize/2;i+=1) {
d = (mInitialImage[i]&0xFF) - (mDecodedVideo[j][i]&0xFF);
d = d<0 ? -d : d;
if (d>50) {
//if (VERBOSE) Log.e(TAG,"BUG "+(i-mSize)+" d "+d);
f++;
break;
}
}
// We compare the V pane before with the U pane after
} else {
for (int i=mSize;i<3*mSize/2;i+=2) {
d = (mInitialImage[i]&0xFF) - (mDecodedVideo[j][i+1]&0xFF);
d = d<0 ? -d : d;
if (d>50) {
f++;
}
}
}
}
}
return f<=NB_DECODED/2;
}
/**
* Converts the image obtained from the decoder to NV21.
*/
private void convertToNV21(int k) {
byte[] buffer = new byte[3*mSize/2];
int stride = mWidth, sliceHeight = mHeight;
int colorFormat = mDecoderColorFormat;
boolean planar = false;
if (mDecOutputFormat != null) {
MediaFormat format = mDecOutputFormat;
if (format != null) {
if (format.containsKey("slice-height")) {
sliceHeight = format.getInteger("slice-height");
if (sliceHeight<mHeight) sliceHeight = mHeight;
}
if (format.containsKey("stride")) {
stride = format.getInteger("stride");
if (stride<mWidth) stride = mWidth;
}
if (format.containsKey(MediaFormat.KEY_COLOR_FORMAT)) {
if (format.getInteger(MediaFormat.KEY_COLOR_FORMAT)>0) {
colorFormat = format.getInteger(MediaFormat.KEY_COLOR_FORMAT);
}
}
}
}
switch (colorFormat) {
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
planar = false;
break;
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
planar = true;
break;
}
for (int i=0;i<mSize;i++) {
if (i%mWidth==0) i+=stride-mWidth;
buffer[i] = mDecodedVideo[k][i];
}
if (!planar) {
for (int i=0,j=0;j<mSize/4;i+=1,j+=1) {
if (i%mWidth/2==0) i+=(stride-mWidth)/2;
buffer[mSize+2*j+1] = mDecodedVideo[k][stride*sliceHeight+2*i];
buffer[mSize+2*j] = mDecodedVideo[k][stride*sliceHeight+2*i+1];
}
} else {
for (int i=0,j=0;j<mSize/4;i+=1,j+=1) {
if (i%mWidth/2==0) i+=(stride-mWidth)/2;
buffer[mSize+2*j+1] = mDecodedVideo[k][stride*sliceHeight+i];
buffer[mSize+2*j] = mDecodedVideo[k][stride*sliceHeight*5/4+i];
}
}
mDecodedVideo[k] = buffer;
}
/**
* Instantiates and starts the encoder.
*/
private void configureEncoder() {
mEncoder = MediaCodec.createByCodecName(mEncoderName);
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BITRATE);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAMERATE);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mEncoderColorFormat);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
mEncoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mEncoder.start();
}
private void releaseEncoder() {
if (mEncoder != null) {
try {
mEncoder.stop();
} catch (Exception ignore) {}
try {
mEncoder.release();
} catch (Exception ignore) {}
}
}
/**
* Instantiates and starts the decoder.
*/
private void configureDecoder() {
byte[] prefix = new byte[] {0x00,0x00,0x00,0x01};
ByteBuffer csd0 = ByteBuffer.allocate(4+mSPS.length+4+mPPS.length);
csd0.put(new byte[] {0x00,0x00,0x00,0x01});
csd0.put(mSPS);
csd0.put(new byte[] {0x00,0x00,0x00,0x01});
csd0.put(mPPS);
mDecoder = MediaCodec.createByCodecName(mDecoderName);
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
mediaFormat.setByteBuffer("csd-0", csd0);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mDecoderColorFormat);
mDecoder.configure(mediaFormat, null, null, 0);
mDecoder.start();
ByteBuffer[] decInputBuffers = mDecoder.getInputBuffers();
int decInputIndex = mDecoder.dequeueInputBuffer(1000000/FRAMERATE);
if (decInputIndex>=0) {
decInputBuffers[decInputIndex].clear();
decInputBuffers[decInputIndex].put(prefix);
decInputBuffers[decInputIndex].put(mSPS);
mDecoder.queueInputBuffer(decInputIndex, 0, decInputBuffers[decInputIndex].position(), timestamp(), 0);
} else {
if (VERBOSE) Log.e(TAG,"No buffer available !");
}
decInputIndex = mDecoder.dequeueInputBuffer(1000000/FRAMERATE);
if (decInputIndex>=0) {
decInputBuffers[decInputIndex].clear();
decInputBuffers[decInputIndex].put(prefix);
decInputBuffers[decInputIndex].put(mPPS);
mDecoder.queueInputBuffer(decInputIndex, 0, decInputBuffers[decInputIndex].position(), timestamp(), 0);
} else {
if (VERBOSE) Log.e(TAG,"No buffer available !");
}
}
private void releaseDecoder() {
if (mDecoder != null) {
try {
mDecoder.stop();
} catch (Exception ignore) {}
try {
mDecoder.release();
} catch (Exception ignore) {}
}
}
/**
* Tries to obtain the SPS and the PPS for the encoder.
*/
private long searchSPSandPPS() {
ByteBuffer[] inputBuffers = mEncoder.getInputBuffers();
ByteBuffer[] outputBuffers = mEncoder.getOutputBuffers();
BufferInfo info = new BufferInfo();
byte[] csd = new byte[128];
int len = 0, p = 4, q = 4;
long elapsed = 0, now = timestamp();
while (elapsed<3000000 && (mSPS==null || mPPS==null)) {
// Some encoders won't give us the SPS and PPS unless they receive something to encode first...
int bufferIndex = mEncoder.dequeueInputBuffer(1000000/FRAMERATE);
if (bufferIndex>=0) {
check(inputBuffers[bufferIndex].capacity()>=mData.length, "The input buffer is not big enough.");
inputBuffers[bufferIndex].clear();
inputBuffers[bufferIndex].put(mData, 0, mData.length);
mEncoder.queueInputBuffer(bufferIndex, 0, mData.length, timestamp(), 0);
} else {
if (VERBOSE) Log.e(TAG,"No buffer available !");
}
// We are looking for the SPS and the PPS here. As always, Android is very inconsistent, I have observed that some
// encoders will give those parameters through the MediaFormat object (that is the normal behaviour).
// But some other will not, in that case we try to find a NAL unit of type 7 or 8 in the byte stream outputed by the encoder...
int index = mEncoder.dequeueOutputBuffer(info, 1000000/FRAMERATE);
if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// The PPS and PPS shoud be there
MediaFormat format = mEncoder.getOutputFormat();
ByteBuffer spsb = format.getByteBuffer("csd-0");
ByteBuffer ppsb = format.getByteBuffer("csd-1");
mSPS = new byte[spsb.capacity()-4];
spsb.position(4);
spsb.get(mSPS,0,mSPS.length);
mPPS = new byte[ppsb.capacity()-4];
ppsb.position(4);
ppsb.get(mPPS,0,mPPS.length);
break;
} else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = mEncoder.getOutputBuffers();
} else if (index>=0) {
len = info.size;
if (len<128) {
outputBuffers[index].get(csd,0,len);
if (len>0 && csd[0]==0 && csd[1]==0 && csd[2]==0 && csd[3]==1) {
// Parses the SPS and PPS, they could be in two different packets and in a different order
//depending on the phone so we don't make any assumption about that
while (p<len) {
while (!(csd[p+0]==0 && csd[p+1]==0 && csd[p+2]==0 && csd[p+3]==1) && p+3<len) p++;
if (p+3>=len) p=len;
if ((csd[q]&0x1F)==7) {
mSPS = new byte[p-q];
System.arraycopy(csd, q, mSPS, 0, p-q);
} else {
mPPS = new byte[p-q];
System.arraycopy(csd, q, mPPS, 0, p-q);
}
p += 4;
q = p;
}
}
}
mEncoder.releaseOutputBuffer(index, false);
}
elapsed = timestamp() - now;
}
check(mPPS != null & mSPS != null, "Could not determine the SPS & PPS.");
mB64PPS = Base64.encodeToString(mPPS, 0, mPPS.length, Base64.NO_WRAP);
mB64SPS = Base64.encodeToString(mSPS, 0, mSPS.length, Base64.NO_WRAP);
return elapsed;
}
private long encode() {
int n = 0;
long elapsed = 0, now = timestamp();
int encOutputIndex = 0, encInputIndex = 0;
BufferInfo info = new BufferInfo();
ByteBuffer[] encInputBuffers = mEncoder.getInputBuffers();
ByteBuffer[] encOutputBuffers = mEncoder.getOutputBuffers();
while (elapsed<5000000) {
// Feeds the encoder with an image
encInputIndex = mEncoder.dequeueInputBuffer(1000000/FRAMERATE);
if (encInputIndex>=0) {
check(encInputBuffers[encInputIndex].capacity()>=mData.length, "The input buffer is not big enough.");
encInputBuffers[encInputIndex].clear();
encInputBuffers[encInputIndex].put(mData, 0, mData.length);
mEncoder.queueInputBuffer(encInputIndex, 0, mData.length, timestamp(), 0);
} else {
if (VERBOSE) Log.d(TAG,"No buffer available !");
}
// Tries to get a NAL unit
encOutputIndex = mEncoder.dequeueOutputBuffer(info, 1000000/FRAMERATE);
if (encOutputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
encOutputBuffers = mEncoder.getOutputBuffers();
} else if (encOutputIndex>=0) {
mVideo[n] = new byte[info.size];
encOutputBuffers[encOutputIndex].clear();
encOutputBuffers[encOutputIndex].get(mVideo[n++], 0, info.size);
mEncoder.releaseOutputBuffer(encOutputIndex, false);
if (n>=NB_ENCODED) {
flushMediaCodec(mEncoder);
return elapsed;
}
}
elapsed = timestamp() - now;
}
throw new RuntimeException("The encoder is too slow.");
}
/**
* @param withPrefix If set to true, the decoder will be fed with NALs preceeded with 0x00000001.
* @return How long it took to decode all the NALs
*/
private long decode(boolean withPrefix) {
int n = 0, i = 0, j = 0;
long elapsed = 0, now = timestamp();
int decInputIndex = 0, decOutputIndex = 0;
ByteBuffer[] decInputBuffers = mDecoder.getInputBuffers();
ByteBuffer[] decOutputBuffers = mDecoder.getOutputBuffers();
BufferInfo info = new BufferInfo();
while (elapsed<3000000) {
// Feeds the decoder with a NAL unit
if (i<NB_ENCODED) {
decInputIndex = mDecoder.dequeueInputBuffer(1000000/FRAMERATE);
if (decInputIndex>=0) {
int l1 = decInputBuffers[decInputIndex].capacity();
int l2 = mVideo[i].length;
decInputBuffers[decInputIndex].clear();
if ((withPrefix && hasPrefix(mVideo[i])) || (!withPrefix && !hasPrefix(mVideo[i]))) {
check(l1>=l2, "The decoder input buffer is not big enough (nal="+l2+", capacity="+l1+").");
decInputBuffers[decInputIndex].put(mVideo[i],0,mVideo[i].length);
} else if (withPrefix && !hasPrefix(mVideo[i])) {
check(l1>=l2+4, "The decoder input buffer is not big enough (nal="+(l2+4)+", capacity="+l1+").");
decInputBuffers[decInputIndex].put(new byte[] {0,0,0,1});
decInputBuffers[decInputIndex].put(mVideo[i],0,mVideo[i].length);
} else if (!withPrefix && hasPrefix(mVideo[i])) {
check(l1>=l2-4, "The decoder input buffer is not big enough (nal="+(l2-4)+", capacity="+l1+").");
decInputBuffers[decInputIndex].put(mVideo[i],4,mVideo[i].length-4);
}
mDecoder.queueInputBuffer(decInputIndex, 0, l2, timestamp(), 0);
i++;
} else {
if (VERBOSE) Log.d(TAG,"No buffer available !");
}
}
// Tries to get a decoded image
decOutputIndex = mDecoder.dequeueOutputBuffer(info, 1000000/FRAMERATE);
if (decOutputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
decOutputBuffers = mDecoder.getOutputBuffers();
} else if (decOutputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
mDecOutputFormat = mDecoder.getOutputFormat();
} else if (decOutputIndex>=0) {
if (n>2) {
// We have successfully encoded and decoded an image !
int length = info.size;
mDecodedVideo[j] = new byte[length];
decOutputBuffers[decOutputIndex].clear();
decOutputBuffers[decOutputIndex].get(mDecodedVideo[j], 0, length);
// Converts the decoded frame to NV21
convertToNV21(j);
if (j>=NB_DECODED-1) {
flushMediaCodec(mDecoder);
if (VERBOSE) Log.v(TAG, "Decoding "+n+" frames took "+elapsed/1000+" ms");
return elapsed;
}
j++;
}
mDecoder.releaseOutputBuffer(decOutputIndex, false);
n++;
}
elapsed = timestamp() - now;
}
throw new RuntimeException("The decoder did not decode anything.");
}
/**
* Makes sure the NAL has a header or not.
* @param withPrefix If set to true, the NAL will be preceeded with 0x00000001.
*/
private boolean hasPrefix(byte[] nal) {
if (nal[0] == 0 && nal[1] == 0 && nal[2] == 0 && nal[3] == 0x01)
return true;
else
return false;
}
private void encodeDecode() {
encode();
try {
configureDecoder();
decode(true);
} finally {
releaseDecoder();
}
}
private void flushMediaCodec(MediaCodec mc) {
int index = 0;
BufferInfo info = new BufferInfo();
while (index != MediaCodec.INFO_TRY_AGAIN_LATER) {
index = mc.dequeueOutputBuffer(info, 1000000/FRAMERATE);
if (index>=0) {
mc.releaseOutputBuffer(index, false);
}
}
}
private void check(boolean cond, String message) {
if (!cond) {
if (VERBOSE) Log.e(TAG,message);
throw new IllegalStateException(message);
}
}
private long timestamp() {
return System.nanoTime()/1000;
}
}