1. 程式人生 > >Android使用FFmpeg+Opengles來解碼播放視訊(一)

Android使用FFmpeg+Opengles來解碼播放視訊(一)

前面已經介紹了FFmpeg解碼視訊的具體流程,現在使用FFmpeg解碼視訊然後用Opengles來渲染。
Demo地址:https://github.com/Huzhuwei1/ffmpegdecoder.git
注:這裡只是簡單的實現一下,程式碼寫的比較粗糙,不喜勿噴!

實現思路:

1.首先通過JNI將視訊地址傳給C層;
2.使用FFmpeg解碼視訊獲取到YUV資料;
3.將YUV資料通過回撥的方式傳到java層;
4.使用Opengles渲染yuv資料;
對於第2步,解碼要使用子執行緒,將YUV資料傳給java層也使用一個執行緒,所以還要使用到一個佇列。

好了,開始擼程式碼。。。

一、配置環境,編譯FFmpeg

編譯FFmpeg這裡就不介紹了
配置NDk環境,首先建立一個AS專案,注意勾選C++庫。專案構建完成後,需要下載CMake和NDK,下載NDK建議去官網下載
然後就是CMakeLists.txt,如下:

    cmake_minimum_required(VERSION 3.4.1)
    
    #新增依賴庫的標頭檔案目錄
    include_directories(src/main/cpp/include)

    add_library( #庫名
             native-lib
             #庫型別
             SHARED
             src/main/cpp/native-lib.cpp)

    #新增動態庫
    add_library( avcodec-57
             SHARED
             IMPORTED)
    #設定庫路徑,和前面對應
    set_target_properties( avcodec-57
                       PROPERTIES IMPORTED_LOCATION
                     ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libavcodec-57.so)
		....
	target_link_libraries( # 連線庫
                       native-lib
                       avcodec-57
                       avdevice-57
                       avfilter-6
                       avformat-57
                       avutil-55
                       swresample-2
                       swscale-4
                       postproc-54
                       OpenSLES
                       android
                       log
                        )

###二、定義native方法 和 解碼資料回撥方法###
public native void init(String url);
public native void start();
然後使用alt+回車自動生成.h標頭檔案
回撥方法:
這裡寫圖片描述

###三、開始寫C程式碼###
1.首先是初始化引數、執行緒、鎖物件和條件對像等
這裡寫圖片描述
這裡寫圖片描述

2.視訊解碼

	void* p_decode(void* data){
    //1.註冊所有元件
    av_register_all();

    //封裝格式上下文,統領全域性的結構體,儲存了視訊檔案封裝格式的相關資訊
    AVFormatContext *
pFormatCtx = avformat_alloc_context(); //2.開啟輸入視訊檔案 if (avformat_open_input(&pFormatCtx, url, NULL, NULL) != 0) { LOGD("%s","無法開啟輸入視訊檔案"); return 0; } //3.獲取視訊檔案資訊 if (avformat_find_stream_info(pFormatCtx,NULL) < 0) { LOGD("%s","無法獲取視訊檔案資訊"); return 0; } //獲取視訊流的索引位置 //遍歷所有型別的流(音訊流、視訊流、字幕流),找到視訊流 int v_stream_idx = -1; int i = 0; //number of streams for (; i < pFormatCtx->nb_streams; i++) { //流的型別 if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { v_stream_idx = i; break; } } if (v_stream_idx == -1) { LOGD("%s","找不到視訊流\n"); return 0; } //只有知道視訊的編碼方式,才能夠根據編碼方式去找到解碼器 //獲取視訊流中的編解碼上下文 AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec; //4.根據編解碼上下文中的編碼id查詢對應的解碼 AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id); if (pCodec == NULL) { LOGD("%s","找不到解碼器\n"); return 0; } //5.開啟解碼器 if (avcodec_open2(pCodecCtx,pCodec,NULL)<0) { LOGD("%s","解碼器無法開啟\n"); return 0; } //準備讀取 //AVPacket用於儲存一幀一幀的壓縮資料(H264) //緩衝區,開闢空間 AVPacket *packet = (AVPacket*)av_malloc(sizeof(AVPacket)); //AVFrame用於儲存解碼後的畫素資料(YUV) //記憶體分配 // AVFrame *pFrame = av_frame_alloc(); //YUV420 AVFrame *pFrameYUV = av_frame_alloc(); //只有指定了AVFrame的畫素格式、畫面大小才能真正分配記憶體 //緩衝區分配記憶體 uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height)); //初始化緩衝區 avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height); //用於轉碼(縮放)的引數,轉之前的寬高,轉之後的寬高,格式等 struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width,pCodecCtx->height,pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); int got_picture, ret; int frame_count = 0; //6.一幀一幀的讀取壓縮資料 while (av_read_frame(pFormatCtx, packet) >= 0) { //只要視訊壓縮資料(根據流的索引位置判斷) if (packet->stream_index == v_stream_idx) { pthread_mutex_lock(&pthread_mutex); //7.解碼一幀視訊壓縮資料,得到視訊畫素資料 AVFrame *pFrame = av_frame_alloc(); ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet); if (ret < 0) { LOGD("%s","解碼錯誤"); return 0; } //為0說明解碼完成,非0正在解碼 if (got_picture) { //AVFrame轉為畫素格式YUV420,寬高 //2 6輸入、輸出資料 //3 7輸入、輸出畫面一行的資料的大小 AVFrame 轉換是一行一行轉換的 //4 輸入資料第一列要轉碼的位置 從0開始 //5 輸入畫面的高度 sws_scale(sws_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); //將YUV資料回撥給java層 //data解碼後的影象畫素資料(音訊取樣資料) //Y 亮度 UV 色度(壓縮了) 人對亮度更加敏感 //U V 個數是Y的1/4 int width = pFrame -> width; int height = pFrame -> height; queue.push(pFrame); pthread_cond_signal(&pthread_cond); frame_count++; LOGD("解碼第%d幀\n",frame_count); } pthread_mutex_unlock(&pthread_mutex); } //釋放資源 av_free_packet(packet); } avcodec_close(pCodecCtx); avformat_close_input(&pFormatCtx); avformat_free_context(pFormatCtx); flag = false; pthread_exit(&decode_pthread); }

3.YUV資料回撥

    void* jCallback(void* data){
    while (flag){
        pthread_mutex_lock(&pthread_mutex);
        if (queue.size()>0){
            //回撥java層
            AVFrame* pFrame = queue.front();
            JNIEnv *env;
            jvm->AttachCurrentThread(&env, 0);
            int width = pFrame->width;
            int height = pFrame->height;
            jbyteArray y = env->NewByteArray(width * height);
            env->SetByteArrayRegion(y, 0, width * height, (jbyte*)pFrame->data[0]);
            jbyteArray u = env->NewByteArray(width * height / 4);
            env->SetByteArrayRegion(u, 0, width * height / 4, (jbyte*)pFrame->data[1]);
            jbyteArray v = env->NewByteArray(width * height / 4);
            env->SetByteArrayRegion(v, 0, width * height / 4, (jbyte*)pFrame->data[2]);
            env->CallVoidMethod(jobj, jmid_dec_callback, width, height, y, u, v);
            env->DeleteLocalRef(y);
            env->DeleteLocalRef(u);
            env->DeleteLocalRef(v);
            jvm->DetachCurrentThread();
            queue.pop();
            LOGD("nnnnnnnnnnnnnnnn");
            av_frame_free(&pFrame);
        } else {
            pthread_cond_wait(&pthread_cond,&pthread_mutex);
        }
        pthread_mutex_unlock(&pthread_mutex);
    }
    pthread_exit(&pthread);
	}

這裡要注意的是,子執行緒回撥需要用到JavaVM。我們需要實現JNI_OnLoad方法,來儲存jvm

    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm,void* reserved){
    JNIEnv *env;
    jvm = vm;
    if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)!=JNI_OK){
        return -1;
    }
    return JNI_VERSION_1_6;
    }

還有就是C呼叫java方法的方法:

env->CallVoidMethod(jobj, jmid_dec_callback, width, height, y, u, v);

注意:

1.這裡是子執行緒回撥,所以使用JNIEnv應該是子執行緒的

        JNIEnv *env;
        jvm->AttachCurrentThread(&env, 0);

2.callVoidMethod的第二個引數是methodId獲取方法如下:

   jmid_dec_callback = env->GetMethodID(jlz, "decCallBack", "(II[B[B[B)V");

呼叫JNIEnv的GetMethodID方法,第一個引數是jclass,第二個引數是java方法名,第三個引數是方法的簽名"(II[B[B[B)"是引數的簽名,"V"是返回值的簽名。

到這裡解碼部分基本上已經完成,使用Opengles渲染的部分,下一篇再實現