本文從簡書遷移,原文地址:www.jianshu.com/p/1ff123409…html
由於項目中須要對解碼後的 YUV420P 格式數據作一些處理,在以前是使用 ffmpeg 軟解的方式獲得 YUV420P,但隨着圖像像素的提高,ffmpeg 的效率已經影響到軟件的體驗了,故使用 Android 上 MediaCodec 硬解的方式提升效率。java
參考 MediaCodec 的官方文檔:android
In broad terms, a codec processes input data to generate output data. It processes data asynchronously and uses a set of input and output buffers. At a simplistic level, you request (or receive) an empty input buffer, fill it up with data and send it to the codec for processing. The codec uses up the data and transforms it into one of its empty output buffers. Finally, you request (or receive) a filled output buffer, consume its contents and release it back to the codec.數組
意思是,MediaCodec 採用異步的方式處理數據,並使用一組輸入和輸出緩衝區。開發者在使用的時候經過請求一個空的輸入緩衝區,往其中填充數據以後放回編解碼器中,編解碼器處理完輸入數據後將處理結果輸出到一個空的輸出緩衝區中。開發者經過請求輸出緩存區使用完其內容後,將其釋放回編解碼器:緩存
private static final long DEFAULT_TIMEOUT_US = 1000 * 10;
private static final String MIME_TYPE = "video/avc";
private static final int VIDEO_WIDTH = 1520;
private static final int VIDEO_HEIGHT = 1520;
private MediaCodec mCodec;
private MediaCodec.BufferInfo bufferInfo;
public void initCodec() {
try {
mCodec = MediaCodec.createDecoderByType(MIME_TYPE);
} catch (IOException e) {
e.printStackTrace();
}
bufferInfo = new MediaCodec.BufferInfo();
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, VIDEO_WIDTH, VIDEO_HEIGHT);
mCodec.configure(mediaFormat, null, null, 0);
mCodec.start();
}
public void release() {
if (null != mCodec) {
mCodec.stop();
mCodec.release();
mCodec = null;
}
}
複製代碼
public void decode(byte[] h264Data) {
int inputBufferIndex = mCodec.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
inputBuffer = mCodec.getInputBuffer(inputBufferIndex);
} else {
inputBuffer = mCodec.getInputBuffers()[inputBufferIndex];
}
if (inputBuffer != null) {
inputBuffer.clear();
inputBuffer.put(h264Data, 0, h264Data.length);
mCodec.queueInputBuffer(inputBufferIndex, 0, h264Data.length, 0, 0);
}
}
int outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, DEFAULT_TIMEOUT_US);
ByteBuffer outputBuffer;
while (outputBufferIndex > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
outputBuffer = mCodec.getOutputBuffer(outputBufferIndex);
} else {
outputBuffer = mCodec.getOutputBuffers()[outputBufferIndex];
}
if (outputBuffer != null) {
outputBuffer.position(0);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
byte[] yuvData = new byte[outputBuffer.remaining()];
outputBuffer.get(yuvData);
if (null!=onDecodeCallback) {
onDecodeCallback.onFrame(yuvData);
}
mCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBuffer.clear();
}
outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, DEFAULT_TIMEOUT_US);
}
}
複製代碼
public interface OnDecoderCallback {
void onFrame(byte[] yuvData);
}
複製代碼
有幾個要注意的地方:異步
在實際的測試過程當中發現各家廠商的 Android 設備 MediaCodec 解碼獲得的 YUV 數據格式不盡相同,例如在個人測試機(某一不知名品牌的平板)上解碼獲得的是標準的 YUV420P 格式,而在另外一臺測試機(華爲榮耀note8)上解碼獲得的倒是 NV12 格式:async
參考 Android: MediaCodec視頻文件硬件解碼,高效率獲得YUV格式幀,快速保存JPEG圖片 得知 API 21 新加入了MediaCodec的全部硬件解碼都支持的 COLOR_FormatYUV420Flexible
格式。它並非一種肯定的 YUV420 格式,而是包含了 COLOR_FormatYUV411Planar
、COLOR_FormatYUV411PackedPlanar
、COLOR_FormatYUV420Planar
、COLOR_FormatYUV420PackedPlanar
,COLOR_FormatYUV420SemiPlanar
和 COLOR_FormatYUV420PackedSemiPlanar
這幾種,因此只能確保解碼後的幀格式是這幾種中的其中一種。MediaCodecInfo 源碼中能夠看到,在API 21引入 YUV420Flexible 的同時,它所包含的這些格式都 deprecated 掉了:ide
指定幀格式只須要在配置 MediaCodec 以前指定就能夠了,在上面的 initCodec 中更新以下:測試
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
mCodec.configure(mediaFormat, null, null, 0);
mCodec.start();
複製代碼
可能因爲我這邊測試機較少的問題,我在使用的時候不指定幀格式也能達到一樣的效果。ui
既然將解碼後的幀格式鎖定爲上面說到的幾種,那離獲得標準的 YUV420P 格式幀就只有一步之遙了。
能夠經過 mCodec.getOutputFormat().getInteger(MediaFormat.KEY_COLOR_FORMAT)
獲得解碼獲得的幀格式,這裏獲得的就是 COLOR_FORMATYUV411PLANAR
、COLOR_FORMATYUV411PACKEDPLANAR
、COLOR_FORMATYUV420PLANAR
、COLOR_FORMATYUV420PACKEDPLANAR
,COLOR_FORMATYUV420SEMIPLANAR
和 COLOR_FORMATYUV420PACKEDSEMIPLANAR
中的其中一個,接下來只須要把對應的類型轉化成標準的 YUV420P 數據就 OK 了:
MediaFormat mediaFormat = mCodec.getOutputFormat();
switch (mediaFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT)) {
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411Planar:
break;
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411PackedPlanar:
break;
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
break;
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
yuvData = yuv420spToYuv420P(yuvData, mediaFormat.getInteger(MediaFormat.KEY_WIDTH), mediaFormat.getInteger(MediaFormat.KEY_HEIGHT));
break;
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
break;
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
default:
break;
}
複製代碼
附上 yuv420sp 轉 Yuv420P 的方法:
private static byte[] yuv420spToYuv420P(byte[] yuv420spData, int width, int height) {
byte[] yuv420pData = new byte[width * height * 3 / 2];
int ySize = width * height;
System.arraycopy(yuv420spData, 0, yuv420pData, 0, ySize); //拷貝 Y 份量
for (int j = 0, i = 0; j < ySize / 2; j += 2, i++) {
yuv420pData[ySize + i] = yuv420spData[ySize + j]; //U 份量
yuv420pData[ySize * 5 / 4 + i] = yuv420spData[ySize + j + 1]; //V 份量
}
return yuv420pData;
}
複製代碼
本文給出 java 層面轉換思路,但實際使用的時候建議在 native 層轉換,或者使用時直接兼容不一樣 YUV 格式,畢竟多這一步轉換,對效率仍是會有比較大的影響的。
使用 MediaCodec 以後解碼速度確實快了許多,但在差一些的設備(例如我那不知名品牌的平板)上面,硬解的表現明顯的低於軟解。目前來看,網上衆多評價說的硬解有坑的說法仍是有道理的,但即使有坑,這解碼速度仍是讓我欲罷不能啊~