FFmpeg 4.0.2解碼並播放視訊
阿新 • • 發佈:2018-12-14
在上一篇文章中我們知道了如何將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較之前都有了很大改變,許多方法過時需要用新的方法替代,所以這就是我寫這篇文章的目的,串連整個編譯到解碼再到播放的流程。
原始碼,已上傳csdn正在稽核中。