我的視頻課程(基礎):《(NDK)FFmpeg打造Android萬能音頻播放器》
我的視頻課程(進階):《(NDK)FFmpeg打造Android視頻播放器》
我的視頻課程(編碼直播推流):《Android視頻編碼和直播推流》
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,就這樣了:多搗鼓總會成功的!