1. 程式人生 > >(四) FFmpeg軟硬解碼和多執行緒解碼(C++ NDK)

(四) FFmpeg軟硬解碼和多執行緒解碼(C++ NDK)

dts(Decoding Time Stamp):即解碼時間戳,這個時間戳的意義在於告訴播放器該在什麼時候解碼這一幀的資料。
pts(Presentation Time Stamp):即顯示時間戳,這個時間戳用來告訴播放器該在什麼時候顯示這一幀的資料。

AVFrame的linesize成員:
這裡寫圖片描述

程式碼

#include "common.hpp"

#ifdef __cplusplus // ffmpeg是基於c語言開發的庫,所有標頭檔案引入的時候需要 extern "C"
extern "C" {
#endif

    #include "libavcodec/avcodec.h"
    #include "libavformat/avformat.h"
    #include "libavcodec/jni.h"

    JNIEXPORT jint JNI_OnLoad(JavaVM * vm, void * res) // jni 初始化時會呼叫此函式
    {
        // ffmpeg要使用硬解碼,需要將java虛擬機器環境傳給ffmpeg,ffmpeg中通過vm來呼叫android中mediacodec硬解碼的java介面 , 第二個函式為日誌,不需要,傳0
        av_jni_set_java_vm(vm, 0);
        return JNI_VERSION_1_4; // 選用jni 1.4 版本
    }

#ifdef __cplusplus
}
#endif

// 當前時間戳
static long long getNowMs()
{
    /*
        struct timeval tv1,tv2;
        unsigned long long timeconsumed = 0;
        // 獲取當前時間:
        gettimeofday(&tv1,NULL);
        ...
        gettimeofday(&tv2,NULL);
        // 時間統計:
        timeconsumed = tv2.tv_sec-tv1.tv_sec +(tv2.tv_usev-tv1.tv_usec)/1000000; // 以秒為單位
     */
    struct timeval tv; // timeval結構體的兩個成員為 秒 與 微秒
    gettimeofday(&tv, NULL); // 獲取系統當前時間, 1970到系統時間的秒數? signed long值裝不下
    int sec = tv.tv_sec%360000; // 100個小時
    long long time = sec*1000 + tv.tv_usec/1000; // 將 timeval 時間單位轉換為 毫秒
    return time;
}

JNIEXPORT void JNICALL Java_hankin_hjmedia_mpeg_Mp5_11Activity_decode(JNIEnv *env, jobject instance, jstring url_, jobject handle)
{
    const char * path = env->GetStringUTFChars(url_, NULL); // jstring 2 char*
    if (path[0]=='\0')
    {
        LOGE("path is empty.");
        return;
    }

    av_register_all();
    int netInit = avformat_network_init();
    LOGD("netInit=%d", netInit);

    avcodec_register_all(); // 在4.0 已過時,Register all the codecs, parsers and bitstream filters which were enabled at configuration time. 註釋此程式碼無影響

    AVFormatContext * ic = NULL;
    int openRet = avformat_open_input(&ic, path, NULL, NULL); // 開啟媒體檔案
    if (openRet!=0 || ic==NULL)
    {
        LOGE("avformat_open_input is failed : %s", av_err2str(openRet));
        return;
    }
    int findStream = avformat_find_stream_info(ic, NULL); // 查詢媒體資訊
    if (findStream!=0)
    {
        LOGE("avformat_find_stream_info is failed");
        return;
    }
    LOGD("duration=%lld, nb_streams=%d, bit_rate=%lld", ic->duration, ic->nb_streams, ic->bit_rate);
    int videoStream = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); // 查詢視訊流在媒體流中的下標,方式一
    int audioStream = -1;
    for (int i = 0; i < ic->nb_streams; ++i)
    {
        AVStream * as = ic->streams[i];
        if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) // 音訊流
        {
            audioStream = i; // 查詢音訊流在媒體流中的下標,方式二,遍歷
            break;
        }
    }
    if (videoStream==AVERROR_STREAM_NOT_FOUND || audioStream==-1)
    {
        LOGE("沒有找到視訊流或音訊流 : videoStream=%d, audioStream=%d", videoStream, audioStream);
        return;
    }
    LOGD("videoStream=%d, audioStream=%d", videoStream, audioStream);
    AVPacket * pkt = av_packet_alloc(); // 建立AVPacket

    /////////////////// 視訊解碼器配置資訊 ///////////////////   開啟解碼器最好與解封裝程式碼分開
    AVCodec * videoCodec = avcodec_find_decoder(ic->streams[videoStream]->codecpar->codec_id); // 查詢軟解碼器,AVCodec存放解碼器的配置資訊,並不是解碼資訊
    if (!videoCodec) LOGE("查詢視訊軟解碼器失敗.");
    // 通過名字查詢解碼器,之前編譯ffmpeg的時候將硬解碼編譯了進去,所以這裡name可以傳 "h264_mediacodec" 表示硬解,
    //      mediacodec是android arm晶片自帶的硬解碼韌體,這裡ffmpeg函式內部呼叫了android的java介面,因為android系統的mediacodec硬解只提供了java介面
    videoCodec = avcodec_find_decoder_by_name("h264_mediacodec"); // 硬解碼器
    if (!videoCodec)
    {
        LOGE("查詢視訊硬解碼器失敗.");
        return; // 不要return,還需要在後面釋放相應記憶體
    }
    // 建立視訊解碼器上下文
    AVCodecContext * videoContext = avcodec_alloc_context3(videoCodec); // 建立編解碼器(ffmpeg中編碼解碼都是用的AVCodec),codec引數傳NULL也可以,但是不會建立編、解碼器
    if (!videoContext)
    {
        LOGE("建立視訊解碼器上下文失敗.");
        return;
    }
    int cvRet = avcodec_parameters_to_context(videoContext, ic->streams[videoStream]->codecpar); // 將AVStream中的引數直接複製到AVCodec中
    if (cvRet!=0) LOGE("視訊 avcodec_parameters_to_context 錯誤.");
    videoContext->thread_count = 8; // 設定軟解碼執行緒數量,硬解碼時次引數無用
    /*
        int avcodec_open2(
            AVCodecContext *avctx, // 編解碼器上下文
            const AVCodec *codec, // 如果在建立AVCodecContext的時候沒有傳codec,那麼這裡就要傳,建立時傳了,這裡就可以傳NULL,總之,這兩個函式中只要傳一個codec
            AVDictionary **options // 字典的陣列,key-value , 參看 ffmpeg原始碼 ./libavformat/options_table.h
        );
     */
    int vret = avcodec_open2(videoContext, 0, 0); // 開啟視訊解碼器
    if (vret!=0)
    {
        LOGE("開啟視訊解碼器失敗");
        return;
    }
    /////////////////// 音訊解碼器,同視訊解碼器一樣 ///////////////////
    AVCodec * audioCodec = avcodec_find_decoder(ic->streams[audioStream]->codecpar->codec_id); // 音訊沒有硬解碼器?
    if (!audioCodec)
    {
        LOGE("查詢音訊軟解碼器失敗.");
        return;
    }
    AVCodecContext * audioContext = avcodec_alloc_context3(audioCodec);
    if (!audioContext)
    {
        LOGE("建立音訊解碼器上下文失敗.");
        return;
    }
    int caRet = avcodec_parameters_to_context(audioContext, ic->streams[audioStream]->codecpar);
    if (caRet!=0) LOGE("音訊 avcodec_parameters_to_context 錯誤.");
    audioContext->thread_count = 8;
    int aret = avcodec_open2(audioContext, 0, 0);
    if (aret!=0)
    {
        LOGE("開啟音訊解碼器失敗");
        return;
    }
    AVFrame * frame = av_frame_alloc(); // 建立AVFrame
    /////////////////// end ///////////////////

    long long start = getNowMs(); // 其實時間
    int frameCount = 0;
    while (true)
    {
        if (getNowMs() - start >= 3000) // 3秒內
        {
            // 軟解碼單執行緒時,手機每秒50幀左右(非neon、mediacodec的so庫效能差百分之三四十),模擬器差不多快一倍。手機cpu佔用率大概在16%
            // 8執行緒時效能幾乎快一倍,但是cpu佔用率有百分之七八十
            // 硬解碼時,使用的是非cpu gpu的韌體,解位元速率理論上是固定的
            LOGW("3秒內平均每秒解碼視訊幀數 : %d", frameCount/3);
            start = getNowMs();
            frameCount = 0;
        }

        int ret = av_read_frame(ic, pkt); // 讀取視訊流和音訊流的每一幀資料,返回非0表示出錯或者讀到了檔案末尾
        if (ret!=0)
        {
            LOGD("讀到結尾處了");
            int pos = 20 * (double) ic->streams[videoStream]->time_base.num / (double) ic->streams[videoStream]->time_base.den;
            av_seek_frame(ic, videoStream, pos, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME); // seek到在pos位置往前找到關鍵幀的地方
            break;
        }
        LOGV("stream=%d, size=%d, pts=%lld, flag=%d", pkt->stream_index, pkt->size, pkt->pts, pkt->flags);

        // 開始視訊、音訊解碼
        AVCodecContext * acc = videoContext;
        if (pkt->stream_index==audioStream) acc = audioContext;
        /*
            int avcodec_send_packet( // 早前ffmpeg解碼音視訊是分兩個不同介面,現在是多執行緒解碼,需要先將AVPacket發到解碼佇列中去(子執行緒中)
                AVCodecContext *avctx, // 解碼上下文
                const AVPacket *avpkt // 呼叫此函式會淺拷貝一份AVPacket,avpkt內部data引用計數會加1,所以呼叫此函式後可以放心 av_packet_unref與釋放avpkt
            ); // return 0 on success, otherwise negative error code:
         */
        int rr = avcodec_send_packet(acc, pkt); // 最後一幀的處理 pkt 要傳 NULL ?
        if (pkt->stream_index==videoStream) LOGI("%s send rr=%d", "video", rr);
            else LOGD("%s send rr=%d", "audio", rr);
        /*
            AVFrame                                                     結構體,存放每幀解碼後的資料,對應著AVPacket,不過比AVPacket的記憶體開銷更大
                uint8_t *data[AV_NUM_DATA_POINTERS];                        指標陣列,如果是視訊表示每行的資料,如果是音訊表示每個通道的資料。 這樣存的原因是因為資料的存放方式(有交叉存放有平面存放)
                int linesize[AV_NUM_DATA_POINTERS];                         與data成對的資訊,如果是視訊表示一行資料的大小,如果是音訊表示一個通道的資料的大小
                int width;                                                  視訊的寬高,只有視訊有
                int height;
                int nb_samples;                                             單通道的樣本數量,只有音訊有
                int64_t pts;                                                這一幀的pts
                int64_t pkt_pts;                                            對應AVPacket中的pts
                int64_t pkt_dts;                                            對應AVPacket中的dts
                uint64_t channel_layout;                                    有音訊有,通道型別,Channel layout of the audio data.
                int channels;                                               聲道數,只用於音訊
                int sample_rate;                                            取樣率,只用於音訊
                int format;                                                 畫素格式(列舉AVPixelFormat) 或 音訊的樣本格式(比如16bit、32bit等)(列舉AVSampleFormat)

                AVFrame *av_frame_alloc(void);                              建立AVFrame
                void av_frame_free(AVFrame **frame);                        釋放AVFrame本身的記憶體
                AVFrame *av_frame_clone(const AVFrame *src);                同AVPacket的操作類似,淺拷貝,src內部data的引用計數加1
                int av_frame_ref(AVFrame *dst, const AVFrame *src);         同AVPacket的操作類似,src淺拷貝到dst,同時src內部data的引用計數加1
                void av_frame_unref(AVFrame *frame);                        同AVPacket的操作類似,將frame內部data的引用計數減1 ,為0時釋放data記憶體

            int avcodec_receive_frame( // 從解碼佇列中已成功解碼的資料的快取中取出幀資料,一次函式呼叫有可能接受到多幀資料,也有可能接受不到
                AVCodecContext *avctx, // 解碼上下文
                AVFrame *frame // 返回的接受到的每幀的資料
            ); // return 0 on success, otherwise negative error code:
         */
        while (true) // 迴圈接受解碼佇列中成功解碼的幀資料,直到該時刻沒有成功解碼的幀資料為止
        {
            rr = avcodec_receive_frame(acc, frame);
            if (pkt->stream_index==videoStream) LOGI("%s receive rr=%d", "video", rr);
                else LOGD("%s receive rr=%d", "audio", rr);
            if (rr==0)
            {
                if (pkt->stream_index==videoStream) LOGI("%s frame->pts=%lld", "video", frame->pts);
                    else LOGD("%s frame->pts=%lld", "audio", frame->pts);
            }
            else break; // 沒有接收到的時候,break,這個時候是不會為frame內部的data申請記憶體的
            av_frame_unref(frame); // 這句程式碼不加也不會記憶體溢位。。 avcodec_receive_frame 並不會為 frame 的data重複申請記憶體?
            if (pkt->stream_index==videoStream && rr==0) frameCount++; // 讀取到的視訊幀數 ++
        }

        av_packet_unref(pkt); // av_read_frame讀取每幀資料的時候會為AVPacket的data指標申請記憶體,這裡將其引用計數減1,以釋放記憶體
    }
    avcodec_free_context(&videoContext); // 清理AVCodecContext記憶體空間,如果不及時釋放會造成應用記憶體崩潰
    avcodec_free_context(&audioContext);

    av_packet_free(&pkt); // 釋放AVPacket本身佔用的記憶體
    avformat_close_input(&ic); // 關閉開啟的媒體流,釋放記憶體

    env->ReleaseStringUTFChars(url_, path); // java的String引用計數減1,釋放path記憶體
}