在上一篇文章中我們知道了如何将FFmpeg4.0.2源碼編譯成so庫,并且如何在Android Studio中配置并使用so庫,那麼這篇文章我們将介紹如何使用FFmpeg在Android ndk中界面視訊檔案并繪制到螢幕上。
我們先來看下效果一睹為快。
總體流程
下面是整個解碼并播放的主要流程,無論是我們解碼視訊還是解碼音頻基本都遵照這個流程進行操作。
具體步驟
- 注冊所有元件
// 注冊所有元件,例如初始化一些全局的變量、初始化網絡等等
av_register_all();
在FFmpeg 4.0.2中這個方法已經被标注為過時,忽略調用該方法也是可行的。
- 打開視訊檔案
// 封裝格式上下文,統領全局的結構體,儲存了視訊檔案封裝格式的相關資訊
AVFormatContext* avFormatContext = avformat_alloc_context();
// 打開輸入視訊檔案
if (avformat_open_input(&avFormatContext, input, NULL, NULL) != 0) {
LOGE("%s", "無法打開輸入視訊檔案");
return;
}
- 擷取視訊檔案資訊
// 3.擷取視訊檔案資訊
if (avformat_find_stream_info(avFormatContext, NULL) < 0) {
LOGE("%s", "無法擷取視訊檔案資訊");
return;
}
- 查找解碼器
// 擷取視訊流的索引位置
int video_stream_idx = -1;
for (int i = 0; i < avFormatContext->nb_streams; i++) {
//流的類型
if (avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_idx = i;
break;
}
}
if (video_stream_idx == -1) {
LOGE("%s", "找不到視訊流\n");
return;
}
// 根據編解碼上下文中的編碼id查找對應的解碼器
// 隻有知道視訊的編碼方式,才能夠根據編碼方式去找到解碼器
// avFormatContext->streams[video_stream_idx]->codec已經過時了,這裡用codecpar代替
AVCodecParameters* avCodecParameters = avFormatContext->streams[video_stream_idx]->codecpar;
AVCodec* avCodec = avcodec_find_decoder(avCodecParameters->codec_id);
if (avCodec == NULL) {
LOGE("%s", "找不到解碼器,或者視訊已加密\n");
return;
}
- 打開解碼器
AVCodecContext* avCodecContext = avcodec_alloc_context3(avCodec);
avcodec_parameters_to_context(avCodecContext, avCodecParameters);
if (avcodec_open2(avCodecContext, avCodec, NULL) < 0) {
LOGE("%s", "解碼器無法打開\n");
return;
}
- 解碼并繪制
// 準備讀取
// AVPacket用于存儲一幀一幀的壓縮資料(H264)
AVPacket* avPacket = av_packet_alloc();
// AVFrame用于存儲解碼後的像素資料(YUV)
AVFrame *yuvFrame = av_frame_alloc();
AVFrame *rgbFrame = av_frame_alloc();
int frame_count = 0;
int width = avCodecContext->width;
int height = avCodecContext->height;
// 窗體
ANativeWindow* nativeWindow = ANativeWindow_fromSurface(env, surface);
// 繪制時的緩沖區
ANativeWindow_Buffer out_buffer;
// 6.一幀一幀的讀取壓縮資料
while (av_read_frame(avFormatContext, avPacket) >= 0) {
// 隻要視訊壓縮資料(根據流的索引位置判斷)
if (avPacket->stream_index == video_stream_idx) {
// 7.解碼一幀視訊壓縮資料,得到視訊像素資料
if(avcodec_send_packet(avCodecContext, avPacket) == 0){
// 一個avPacket可能包含多幀資料,是以需要使用while循環一直讀取
while (avcodec_receive_frame(avCodecContext, yuvFrame) == 0) {
// 1.lock window
// 設定緩沖區的屬性:寬高、像素格式(需要與Java層的格式一緻)
ANativeWindow_setBuffersGeometry(nativeWindow, width, height,
WINDOW_FORMAT_RGBA_8888);
ANativeWindow_lock(nativeWindow, &out_buffer, NULL);
// 2.fix buffer
// 初始化緩沖區
// 設定屬性,像素格式、寬高
av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize,
(const uint8_t *) out_buffer.bits,
AV_PIX_FMT_RGBA, width, height, 1);
// YUV格式的資料轉換成RGBA 8888格式的資料
libyuv::I420ToARGB(yuvFrame->data[0], yuvFrame->linesize[0],
yuvFrame->data[2], yuvFrame->linesize[2],
yuvFrame->data[1], yuvFrame->linesize[1],
rgbFrame->data[0], rgbFrame->linesize[0],
width, height);
// 3.unlock window
ANativeWindow_unlockAndPost(nativeWindow);
frame_count++;
LOGI("解碼繪制第%d幀", frame_count);
// 每繪制一幀便休眠16毫秒,避免繪制過快導緻播放的視訊速度加快
usleep(1000 * 16);
}
}
}
av_packet_unref(avPacket);
}
這裡需要注意的是,我們解碼得到的一幀資料格式是YUV格式的,我們需要将yuv格式的資料轉換成RGB的格式,然後進行繪制,是以這裡我們使用libyuv的庫來進行格式轉換。libyuv的網址被牆了,我這裡用v.p.n也沒有下下來,找了很久然後在github上找到一個libyuv非官方庫裡面已經有編譯的腳本了,編譯方法和我們之前編譯FFmpeg類似,編譯完成後在as裡面的配置也和FFmpeg的類似,這裡就不細說了,還有問題的參考我最後上傳的完整項目工程。
- 釋放資源
av_frame_free(&yuvFrame);
av_frame_free(&rgbFrame);
avcodec_close(avCodecContext);
avformat_free_context(avFormatContext);
ANativeWindow_release(nativeWindow);
env->ReleaseStringUTFChars(input_, input);
總結
講解在Android ndk中使用FFmpeg解碼視訊的文章也比較多了,但是大多版本都比較老,而在新版本4.0的API較之前都有了很大改變,許多方法過時需要用新的方法替代,是以這就是我寫這篇文章的目的,串連整個編譯到解碼再到播放的流程。
源碼