1. 程式人生 > >FFmpeg 4.0.2解碼並播放視訊

FFmpeg 4.0.2解碼並播放視訊

上一篇文章中我們知道了如何將FFmpeg4.0.2原始碼編譯成so庫,並且如何在Android Studio中配置並使用so庫,那麼這篇文章我們將介紹如何使用FFmpeg在Android ndk中介面視訊檔案並繪製到螢幕上。 我們先來看下效果一睹為快。 在這裡插入圖片描述

總體流程

下面是整個解碼並播放的主要流程,無論是我們解碼視訊還是解碼音訊基本都遵照這個流程進行操作。 在這裡插入圖片描述

具體步驟

  1. 註冊所有元件
	// 註冊所有元件,例如初始化一些全域性的變數、初始化網路等等
	av_register_all();

在FFmpeg 4.0.2中這個方法已經被標註為過時,忽略呼叫該方法也是可行的。

  1. 開啟視訊檔案
	// 封裝格式上下文,統領全域性的結構體,儲存了視訊檔案封裝格式的相關資訊
	AVFormatContext* avFormatContext = avformat_alloc_context();
	
	// 開啟輸入視訊檔案
	if (avformat_open_input(&avFormatContext, input, NULL, NULL) != 0) {
		LOGE("%s", "無法開啟輸入視訊檔案");
		return;
	}
  1. 獲取視訊檔案資訊
	// 3.獲取視訊檔案資訊
	if (avformat_find_stream_info(avFormatContext, NULL) < 0) {
		LOGE("%s", "無法獲取視訊檔案資訊");
		return;
	}
  1. 查詢解碼器
	// 獲取視訊流的索引位置
    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;
	}
  1. 開啟解碼器
AVCodecContext* avCodecContext = avcodec_alloc_context3(avCodec);
avcodec_parameters_to_context(avCodecContext, avCodecParameters);
if (avcodec_open2(avCodecContext, avCodec, NULL) < 0) {
	LOGE("%s", "解碼器無法開啟\n");
	return;
}
  1. 解碼並繪製
	// 準備讀取
    // 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的類似,這裡就不細說了,還有問題的參考我最後上傳的完整專案工程。

  1. 釋放資源
	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正在稽核中。