EasyPlayerPro基於FFMPEG實現播放同時進行錄像的功能

之前有博客專門介紹了EasyPlayer的本地錄像的功能,簡單來說,EasyPlayer是一款RTSP播放器,它將RTSP流裏的音視頻媒體幀解析出來,並用安卓系統提供的MediaMuxer類進行錄像.那EasyPlayerPro可以這樣實現嗎?答案是不太現實,因爲Pro支持絕大多數的流媒體協議,並不單單是RTSP協議,包括hTTP\RTSP\RTMP\HLS\FILE等格式都支持.要將這些數據分別解析出來並拋給上層,並不合適.EasyPlayerPro最終是基於FFMPEG來做的媒體流的解析(demux),既然用FFMPEG來demux,那同樣也可以基於FFMPEG來實現錄像.錄像在FFMPEG裏面,就是mux的過程.

作者參考了ffmpeg mux的相關代碼,在Pro上成功實現了播放同時的錄像功能.現在尚處於測試階段,此文亦起到一個記錄與總結的目的.

EasyPlayerPro是參考ffplay的實現,開啓專門的接收線程來接收和解析音視頻媒體幀.我們可以在收到媒體幀後進行MUX操作.

Created with Raphaël 2.1.0av_read_frameav_read_frameav_interleaved_write_frameav_interleaved_write_frameMUX

在接收線程中,我們可以設置一個錄像標誌位:recording.我們設定該值爲0的話表示未在錄像;1表示要啓動錄像;2表示錄像正在啓動,等待關鍵幀;大於2表示正在錄像正在啓動中.這裏不再多說了,結合代碼來做敘述.
請看相關代碼以及註釋:

接收線程讀取媒體幀.

ret = av_read_frame(ic, pkt);
if (ret < 0) {
    .. // error handle
}

錄像開始

外部線程更改read thread的recording狀態量來控制錄像的狀態.錄像開始時,將狀態位設置爲1,這時read thread會進行錄像的一些初始化操作:

START_RECORDING:
    if (is->recording == 1){    // 錄像即將啓動
        do
        {
            // 首先我們判斷一下,當前如果是視頻的話,需要爲關鍵幀.
            av_log(NULL, AV_LOG_INFO, "try start record:%s", is->record_filename);
            AVStream *ins = ic->streams[pkt->stream_index];
            if (ins->codec->codec_type == AVMEDIA_TYPE_VIDEO ){
                av_log(NULL, AV_LOG_DEBUG, "check video  key frame.");
                if (!(pkt->flags & AV_PKT_FLAG_KEY)){   // 不是關鍵幀,跳出錄像塊.下次繼續嘗試.
                    av_log(NULL,AV_LOG_WARNING,"waiting for key frame of video stream:%d.", pkt->stream_index); 
                    break;
                }
                is->recording++;
            }
            // 至此,已經找到了首個關鍵幀,錄像可以啓動了.
            av_log(NULL, AV_LOG_INFO, "start record:%s", is->record_filename);
            // 創建AVFormatContext,作爲錄像的Context
            avformat_alloc_output_context2(&o_fmt_ctx, NULL, NULL, is->record_filename);
            if (!o_fmt_ctx){    // 創建失敗.
                is->recording = 0;
                av_log(NULL, AV_LOG_WARNING, "avformat_alloc_output_context2 error");
                o_fmt_ctx = is->oc = NULL;
                goto START_RECORDING;
            }
            ofmt = o_fmt_ctx->oformat;
            // 在這裏遍歷所有的媒體流
            for (i = 0; i < ic->nb_streams; i++) {  
                // 目前MP4Muxer支持的AV_CODEC類型,我們在這裏判斷下,加一些打印日誌.這些代碼摘自FFMPEG的muxer部分
                AVStream *in_stream = ic->streams[i];  
                AVCodecParameters *par = in_stream->codecpar;
                unsigned int tag = 0;            
                if      (par->codec_id == AV_CODEC_ID_H264)      tag = MKTAG('a','v','c','1');
                else if (par->codec_id == AV_CODEC_ID_HEVC)      tag = MKTAG('h','e','v','1');
                else if (par->codec_id == AV_CODEC_ID_VP9)       tag = MKTAG('v','p','0','9');
                else if (par->codec_id == AV_CODEC_ID_AC3)       tag = MKTAG('a','c','-','3');
                else if (par->codec_id == AV_CODEC_ID_EAC3)      tag = MKTAG('e','c','-','3');
                else if (par->codec_id == AV_CODEC_ID_DIRAC)     tag = MKTAG('d','r','a','c');
                else if (par->codec_id == AV_CODEC_ID_MOV_TEXT)  tag = MKTAG('t','x','3','g');
                else if (par->codec_id == AV_CODEC_ID_VC1)       tag = MKTAG('v','c','-','1');
                else if (par->codec_id == AV_CODEC_ID_DVD_SUBTITLE)  tag = MKTAG('m','p','4','s');
                av_log(NULL, AV_LOG_INFO, "par->codec_id:%d, tag:%d\n", par->codec_id, tag);
                if (tag == 0) {
                    // 這個CODEC不支持,打印下.
                    av_log(NULL, AV_LOG_WARNING, "unsupported codec codec_id:%d\n", par->codec_id);
                    // continue;
                }
            // av_log(NULL, AV_LOG_INFO, "-ffplay : %d", __LINE__);
                //Create output AVStream according to input AVStream  
                if(ic->streams[i]->codec->codec_type ==AVMEDIA_TYPE_VIDEO){     // 這個是視頻幀
                    // 我們在這裏檢查一下寬\高是否合法.
                    if ((par->width <= 0 || par->height <= 0) &&
                        !(ofmt->flags & AVFMT_NODIMENSIONS)) {
                        av_log(NULL, AV_LOG_ERROR, "dimensions not set\n");
                        continue;
                    }
                    // 加入視頻流至Muxer.
                    AVStream *out_stream = avformat_new_stream(o_fmt_ctx, in_stream->codec->codec);
                    // 將視頻流的一些參數拷貝到muxer.
                    if (avcodec_copy_context(out_stream->codec, in_stream->codec) < 0) {
                        // 失敗了,做一些錯誤處理\釋放.
                        // printf( "Failed to copy context from input to output stream codec context\n");  
                        av_log(NULL, AV_LOG_WARNING,
                            "Failed to copy context from input to output stream codec context\n");
                        is->recording = 0;
                        avformat_free_context(o_fmt_ctx);
                        o_fmt_ctx = is->oc = NULL;
                        goto START_RECORDING;  
                    }
                    // av_log(NULL, AV_LOG_INFO, "-ffplay:%d out_stream:%p, in_stream:%p", __LINE__, out_stream->codec, in_stream->codec);
                    out_stream->codec->codec_tag = 0;  
                    if (o_fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)  
                        out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;  
                    av_log(NULL, AV_LOG_INFO, "-ffplay : %d video added", __LINE__);
                }else if(ic->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO){  //    這個是音頻幀
                    av_log(NULL, AV_LOG_INFO, "-ffplay : %d", __LINE__);
                    // 我們檢查下採樣率是否合法?
                    if (par->sample_rate <= 0) {
                        av_log(NULL, AV_LOG_ERROR, "sample rate not set\n");
                        continue;
                    }
                    // 加入音頻流至Muxer.
                    AVStream *out_stream = avformat_new_stream(o_fmt_ctx, in_stream->codec->codec);  
                    // 將音頻流的一些參數拷貝到muxer.
                    if (avcodec_copy_context(out_stream->codec, in_stream->codec) < 0) {
                        // 失敗了,做一些錯誤處理\釋放.
                        av_log(NULL, AV_LOG_WARNING,
                            "Failed to copy context from input to output stream codec context 2\n"); 
                        is->recording = 0;
                        avformat_free_context(o_fmt_ctx);
                        o_fmt_ctx = is->oc = NULL;
                        goto START_RECORDING;
                    }  
                    out_stream->codec->codec_tag = 0;  
                    if (o_fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)  
                        out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
                    av_log(NULL, AV_LOG_INFO, "-ffplay : %d audio added", __LINE__);
                }    
            }  
            // 至此,AVFormatContext裏面應該至少含有一條流了.否則錄像也算沒有啓動.
            if (o_fmt_ctx->nb_streams < 1){ 
                av_log(NULL, AV_LOG_WARNING,
                    "NO available stream found in muxer \n"); 
                is->recording = 0;
                avformat_free_context(o_fmt_ctx);
                o_fmt_ctx = is->oc = NULL;
                goto START_RECORDING;
            }
            // 下面開始創建文件
            if (!(ofmt->flags & AVFMT_NOFILE)){
                av_log(NULL, AV_LOG_INFO, "-ffplay : %d AVFMT_NOFILE", __LINE__);
                if (avio_open(&o_fmt_ctx->pb, is->record_filename, AVIO_FLAG_WRITE) < 0) {  
                    // 出錯了,錯誤處理.
                    av_log(NULL,AV_LOG_WARNING, "Could not open output file '%s'", is->record_filename);
                    is->recording = 0;
                    avformat_free_context(o_fmt_ctx);
                    o_fmt_ctx = is->oc = NULL;
                    goto START_RECORDING;
                }  
            } 
            // 下面寫入header. Allocate the stream private data and write the stream header to an output media file.
            int r = avformat_write_header(o_fmt_ctx, NULL);
            if (r < 0) {    // error handle    
                av_log(NULL,AV_LOG_WARNING, "Error occurred when opening output file:%d\n",r);
                is->recording = 0;
                avformat_free_context(o_fmt_ctx);
                o_fmt_ctx = is->oc = NULL;
                goto START_RECORDING;
            }  
            // 輸出一下OUTPUT格式.
            av_dump_format(o_fmt_ctx, 0, is->record_filename, 1);
            // 將標誌位改爲2,表示錄像已經啓動.開始等待關鍵幀.
            is->recording = 2;
        }
        while(0);            
    }

錄像中

當錄像初始化完成後,狀態量會變爲2,狀態改爲錄像中:

    do{
        if (is->recording >= 2){
            // 忽略未被加入的stream
            if (pkt->stream_index >= o_fmt_ctx->nb_streams)
            {
                av_log(NULL,AV_LOG_WARNING,"stream_index large than nb_streams %d:%d\n", pkt->stream_index,  o_fmt_ctx->nb_streams); 
                break; 
            }

            AVStream *ins = ic->streams[pkt->stream_index];
            av_log(NULL,AV_LOG_DEBUG,"before write frame.stream index:%d, codec:%d,type:%d\n", ins->index, ins->codec->codec_id,  ins->codec->codec_type); 
            if(is->recording == 2)  // 等待關鍵幀..
            if (ins->codec->codec_type == AVMEDIA_TYPE_VIDEO ){
                av_log(NULL, AV_LOG_DEBUG, "check video  key frame.");
                if (!(pkt->flags & AV_PKT_FLAG_KEY)){
                    av_log(NULL,AV_LOG_WARNING,"waiting for key frame of video stream:%d.", pkt->stream_index); 
                    break;
                }
                // 表示關鍵幀得到了.
                is->recording++;
            }
            // 將接收到的AVPacket拷貝一份
            AVPacket *newPkt = av_packet_clone(pkt);
            AVStream *in_stream, *out_stream; 
            in_stream  = ic->streams[newPkt->stream_index];
            out_stream = o_fmt_ctx->streams[newPkt->stream_index];
            // 下面轉換一下PTSDTS.這幾句的意思大概是,將原先以輸入的time_base爲基準的時間戳轉換爲以輸出的time_base爲基準的時間戳,
            // 要把這幾句加上,最終錄像時間戳才正常..
            //Convert PTS/DTS  
            newPkt->pts = av_rescale_q_rnd(newPkt->pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
            newPkt->dts = av_rescale_q_rnd(newPkt->dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
            newPkt->duration = av_rescale_q(newPkt->duration, in_stream->time_base, out_stream->time_base);  
            // 下面開始寫入AVPacket.
            int r;
            if (o_fmt_ctx->nb_streams < 2){ // 如果只有視頻或只有音頻,那直接寫入
                r = av_write_frame(o_fmt_ctx, newPkt); 
            }else {                         // 多條流的情況下,調用av_interleaved_write_frame,內部會做一些排序,再寫入.
                r = av_interleaved_write_frame(o_fmt_ctx, newPkt);
            }
            if (r < 0) {
                printf( "Error muxing packet\n");  
                break;
            }
        }
    }while(0);

停止錄像

外部控制recording狀態量,置爲0時,表示要停止錄像了.這時候做一些反初始化的工作:

    if (is->recording == 0){    // 要停止錄像了.
        if (o_fmt_ctx != NULL){
            av_log(NULL, AV_LOG_INFO, "stop record~~~~~~~");
            // 一定要先寫入trailer
            av_write_trailer(o_fmt_ctx);
            // 釋放context
            avformat_free_context(o_fmt_ctx);
            o_fmt_ctx = is->oc = NULL;
        }
    }

EasyPlayerPro下載地址:https://fir.im/EasyPlayerPro

相關介紹見:http://www.easydarwin.org/article/news/117.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章