Android使用FFmpeg 解碼H264並播放(二)
阿新 • • 發佈:2019-02-06
上一節記錄了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.c 和 h264_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 解碼就完成了。