(四) FFmpeg軟硬解碼和多執行緒解碼(C++ NDK)
阿新 • • 發佈:2019-02-19
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記憶體 }