FFmpeg是一个很不错的开源的音视频编解码库,其编解码器几乎涵盖所有格式的音视频。但是它是利用CPU来编解码的,在PC等设备上面解码能力还能满足需求,但是在移动设备上面解码720p及其以上的视频时就显得很尴尬了,解码速度不够导致解码视频帧的速度太慢,造成播放卡顿并且耗电也快。如果能用移动设备上的GPU来解码视频帧的话,那效率将会提高很多倍的,这就需要用到硬解码器MediaCodec了。
FFmpeg解码出AVpacket的速度是完全够的,因此我们就会想如果我们能用MediaCodec来解码AVpacket包里面的视频原始压缩数据的话,那就能播放高清视频了并且还不会太耗电。幸好,经过测试,这种方式是完全可行的。
那么开始我们的MediaCodec解码AVpacket之旅吧。还是先看效果:都为720p的视频(源码下载wlplayer)
一、Mediacodec解码过程
1.1、首先的配置MediaFormat,来告诉MediaCodec解码的视频时怎样的,有哪些信息,如下代码:
public void mediacodecInit(int mimetype, int width, int height, byte[] csd0, byte[] csd1)
{
if(surface != null)
{
try {
wlGlSurfaceView.setCodecType(1);
String mtype = getMimeType(mimetype);
mediaFormat = MediaFormat.createVideoFormat(mtype, width, height);
mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width);
mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height);
mediaFormat.setLong(MediaFormat.KEY_MAX_INPUT_SIZE, width * height);
mediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(csd0));
mediaFormat.setByteBuffer("csd-1", ByteBuffer.wrap(csd1));
Log.d("ywl5320", mediaFormat.toString());
mediaCodec = MediaCodec.createDecoderByType(mtype);
if(surface != null)
{
mediaCodec.configure(mediaFormat, surface, null, 0);
mediaCodec.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
else
{
if(wlOnErrorListener != null)
{
wlOnErrorListener.onError(WlStatus.WL_STATUS_SURFACE_NULL, "surface is null");
}
}
}
参数mimeType:是告诉MediaCodec要解码的视频的编码格式,如:video/avc、video/hevc等。
width和height:表示视频的宽和长。
csd0和csd1:都对应于AVCodecContext里面的extradata字段。
这样就配置好了MediaFormat。
1.2、MediaCodec解码AVpacket:
MediaCodec解码视频的过程为:其里面有2个Buffer队列,一个是InputBuffer队列,负责把视频压缩数据送给MediaCodec解码器解码,然后清空数据并以此循环指定结束;另一个是OutputBuffer队列,负责把MediaCodec解码后的数据给surface渲染,然后清空数据并以此循环指定结束;说白了就是一个负责往MediaCodec喂数据,一个负责把MediaCodec(排出的数据)送给surface渲染,循环这个过程,就能播放视频了。
了解了MediaCodec的解码过程,我们就知道从何入手了,就在喂数据(InputBuffer)那里开刀,获取MediaCodec的InputBuffer,然后把AVpacket里面的视频压缩数据添加到里面,并用queueInputBuffer方法送给MediaCodec,这样MediaCodec就有了解码的原始数据,那么代码怎么写呢:
public void mediacodecDecode(byte[] bytes, int size, int pts)
{
if(bytes != null && mediaCodec != null && info != null)
{
try
{
int inputBufferIndex = mediaCodec.dequeueInputBuffer(10000);
if(inputBufferIndex >= 0)
{
ByteBuffer byteBuffer = mediaCodec.getInputBuffers()[inputBufferIndex];
byteBuffer.clear();
byteBuffer.put(bytes);
mediaCodec.queueInputBuffer(inputBufferIndex, 0, size, pts, 0);
}
int index = mediaCodec.dequeueOutputBuffer(info, 10000);
if (index >= 0) {
//ByteBuffer buffer = mediaCodec.getOutputBuffers()[index];
//buffer.position(info.offset);
//buffer.limit(info.offset + info.size);
mediaCodec.releaseOutputBuffer(index, true);
}
}catch (Exception e)
{
e.printStackTrace();
}
}
}
上面代码参数byte[]就是从C++传过来的AVpacket里面的视频原始压缩数据,然后获取InputBuffer并把byte数据添加到里面,最好送给MediaCodec。
解码过程没什么变化,和官方过程一样。
二、C++提供AVpacket数据:
2.1、封装调用Java的方法:
void WlJavaCall::onDecMediacodec(int type, int size, uint8_t *packet_data, int pts) {
if(type == WL_THREAD_CHILD)
{
JNIEnv *jniEnv;
if(javaVM->AttachCurrentThread(&jniEnv, 0) != JNI_OK)
{
// LOGE("%s: AttachCurrentThread() failed", __FUNCTION__);
return;
}
jbyteArray data = jniEnv->NewByteArray(size);
jniEnv->SetByteArrayRegion(data, 0, size, (jbyte*)packet_data);
jniEnv->CallVoidMethod(jobj, jmid_dec_mediacodec, data, size, pts);
jniEnv->DeleteLocalRef(data);
javaVM->DetachCurrentThread();
}
else
{
jbyteArray data = jniEnv->NewByteArray(size);
jniEnv->SetByteArrayRegion(data, 0, size, (jbyte*)data);
jniEnv->CallVoidMethod(jobj, jmid_dec_mediacodec, data, size, pts);
jniEnv->DeleteLocalRef(data);
}
}
这里分了主线程和子线程,不过解码是在子线程的,所以不会用到主线程的。
2.2、添加数据头:
因为AVpacket里面的压缩数据是很纯粹的,这种数据MediaCodec是不能解码或者解码出来也不能播放的,因此需要将AVpacket添加相应的数据头,这就要用到FFmpeg的av_bitstream_filter_filter方法,如:
mimType = av_bitstream_filter_init("h264_mp4toannexb");
if(mimType != NULL && !isavi)
{
uint8_t *data;
av_bitstream_filter_filter(mimType, pFormatCtx->streams[wlVideo->streamIndex]->codec, NULL, &data, &packet->size, packet->data, packet->size, 0);
uint8_t *tdata = NULL;
tdata = packet->data;
packet->data = data;
if(tdata != NULL)
{
av_free(tdata);
}
}
注:这里会导致内存泄漏,经过av_bitstream_filter_filter处理的AVpacket的data的地址和原来的是不一样的,不释放原来的地址就会造成内存泄漏。
2.3、传递AVpacket数据到MediaCodec播放:
wljavaCall->onDecMediacodec(WL_THREAD_CHILD, packet->size, packet->data, clock);
直接把AVpacket的size和data传给MediaCodec就可以了。
2.4、释放AVpacket
由于我们在解复用时对AVpacket的data进行了操作,如果直接av_packet_free的话,会报释放地址错误,所以这里就单独释放AVpacket里面的指针就行了:
av_free(packet->data);
av_free(packet->buf);
av_free(packet->side_data);
packet = NULL;
完整实例可参考:wlplayer
OK,就这样了:多捣鼓总会成功的!