1. 程式人生 > >Android使用FFmpeg 解碼H264並播放(二)

Android使用FFmpeg 解碼H264並播放(二)

上一節記錄了Android使用FFmpeg環境搭建過程。這一節記錄視訊解碼過程。

問題描述

在開發中使用某攝像頭的SDK,只能獲取到一幀幀的 H264 視訊資料,不知道視訊流地址,需要自己解碼出影象並播放。

問題解決

編譯FFmpeg

點選檢視

開發環境配置

點選檢視

解碼H264

原始資料格式

首先看我們能獲取到資料格式

public class VideoStream{
    //video buffer
    byte[] streamBuffer;
    //pps
    byte[] ppsBuffer;
    //sps
    byte[] spsBuffer;
    //當前是I幀還是P幀
int frameType; }

Java層程式碼

我們需要將從Java層取到的原始資料通過JNI傳遞到C層,交給FFmpeg解析。 Java 類大致如下:

public class H264FrameRender {

    static {
        //載入自己的 so 庫
        System.loadLibrary("decoder");
    }
    //儲存C中的物件記憶體地址
    private long nativeObject;
        public H264FrameRender() {
        long address = this
._init(); if (address <= 0) { throw new IllegalStateException("init failed"); } this.nativeObject = address; } /** * 將當前的buffer寫入緩衝佇列,等待解析 */ public void write(VideoStream videoStream) { if (nativeObject <= 0) { throw new
IllegalStateException("H264FrameRender init failed,cannot decode "); } byte[] streamBuffer = videoStream.getmStreamBuffer(); byte[] spsBuffer = videoStream.getmSPSBuffer(); byte[] ppsBuffer = videoStream.getmPPSBuffer(); int spsLen = spsBuffer == null ? 0 : spsBuffer.length; int ppsLen = ppsBuffer == null ? 0 : ppsBuffer.length; boolean isIFrame = videoStream.getmType() == VideoStream.IFrameType; _write(nativeObject, streamBuffer, streamBuffer.length, spsBuffer, spsLen, ppsBuffer, ppsLen, isIFrame); } /** * 釋放記憶體,呼叫該方法後,將會釋放C分配的記憶體空間,並將該Java物件標記為不可用 */ public void release() { if (this.nativeObject > 0) { _release(this.nativeObject); this.nativeObject = 0; } } /** * 如果沒有主動release,C 申請的空間將在Java類銷燬時自動釋放 * * @throws Throwable */ @Override protected void finalize() throws Throwable { release(); super.finalize(); } /** * 初始化記憶體空間,會呼叫C申請一段記憶體,並返回記憶體地址 */ private native long _init(); /** * 將資料通過 JNI 交給 C 去解析 */ private native void _write(long nativeObject, byte[] streamBuffer, int length, byte[] spsBuffer, int spsLen, byte[] ppsBuffer, int ppsLen, boolean isIFrame); }

JNI 程式碼

新建 h264_render.ch264_render.h ,新增兩個JNI方法:

//h264_render.h
#ifndef LITTLELF_JNI_H264_RENDER_H
#define LITTLELF_JNI_H264_RENDER_H

#include <jni.h>

//Java: _init()
JNIEXPORT jlong JNICALL
Java_com_hencenx_littlelf_jni_H264FrameRender__1init(JNIEnv *env, jobject instance);

//Java: _write(long nativeObject, long nativeObject, byte[] streamBuffer, int length, byte[] spsBuffer,int spsLen, byte[] ppsBuffer, int ppsLen, boolean isIFrame)
JNIEXPORT void JNICALL
Java_com_hencenx_littlelf_jni_H264FrameRender__1write(JNIEnv *env, jobject instance,
                                                      jlong nativeObject, jbyteArray streamBuffer_,
                                                      jint length, jbyteArray spsBuffer_,
                                                      jint spsLen, jbyteArray ppsBuffer_,
                                                      jint ppsLen, jboolean isIFrame);
//Java: _release(long nativeObject)
JNIEXPORT void JNICALL
Java_com_hencenx_littlelf_jni_H264FrameRender__1release(JNIEnv *env, jobject instance,
                                                        jlong nativeObject);

#endif //LITTLELF_JNI_H264_RENDER_H

至此,我們可以在 C 層訪問到流的 buffer 了。

FFmpeg 解析視訊流

解碼基本流程

FFmpeg 提供了一個解碼和編碼的 demo,程式碼很簡單,精簡後的程式碼如下:

//1.註冊所有編解碼器,註冊後才能使用
avcodec_register_all();
//2.從註冊的解碼器裡找到H264解碼器
AVCodec * codec = avcodec_find_decoder(AV_CODEC_ID_H264);
//3. 初始化解碼的上下文,上下文很關鍵,包含了解碼所需要的資訊
AVCodecContext * ctx = avcodec_alloc_context3(codec);
//準備一個容器用來裝需要解碼的原始H264資料
AVPacket avpkt;
//4. 準備一個容器用來裝解碼後的資料,AVFrame既可以表示視訊資料,也可以表示音訊資料
AVFrame frame = av_frame_alloc();
//5. 初始化avpkt,並將H264資料放進去(此處程式碼省略)
//6. 初始化解碼上下文,設定視訊寬高等(因為可能是從I幀中獲取的,所以寫在這一步,此處程式碼省略)
//7. 根據解碼上下文開啟解碼器,這樣解碼器才算初始完畢,可以解碼了
avcodec_open(ctx, codec, NULL);
//8. 解碼 - 傳送需要解碼的資料給上下文
avcodec_send_packet(ctx, avpkt);
//9. 解碼 - 從上下文中獲取解碼後的frame,解碼完成
avcodec_receive_frame(ctx, frame);
利用 sps 和 pps 初始化 上下文

我們的處理流程和 demo 類似,但稍有不同。因為解碼H264必須提供視訊的寬高,否則解析過程就會報錯。 但是原始資料中並沒有提供視訊寬高。經過查閱得知,sps 和 pps 中就包含了視訊寬高和其他一些解碼必須的資料。我們需要將 sps 和 pps 放到AVCodecContext 的 extradata 中。
程式碼如下:

int extra_len = sps_len + pps_len;
ctx->extradata = av_malloc(
            sizeof(uint8_t) * (size_t) (extra_len + AV_INPUT_BUFFER_PADDING_SIZE));
ctx->extradata_size = extra_len;
memcpy(ctx->extradata, (uint8_t *) input_sps, (size_t) sps_len);
memcpy(ctx->extradata + (size_t) sps_len, (uint8_t *) input_pps, (size_t) pps_len);            

經過以上處理,H264 解碼就完成了。