FFmpeg封裝格式處理3-復用例程


本文為作者原創,轉載請注明出處:https://www.cnblogs.com/leisure_chn/p/10506653.html

FFmpeg封裝格式處理相關內容分為如下幾篇文章:
[1]. FFmpeg封裝格式處理-簡介
[2]. FFmpeg封裝格式處理-解復用例程
[3]. FFmpeg封裝格式處理-復用例程
[4]. FFmpeg封裝格式處理-轉封裝例程

4. 復用例程

復用(mux),是multiplex的縮寫,表示將多路流(視頻、音頻、字幕等)混入一路輸出中(普通文件、流等)。

本例實現,提取第一路輸入文件中的視頻流和第二路輸入文件中的音頻流,將這兩路流混合,輸出到一路輸出文件中。

muxing

本例不支持裸流輸入,是因為裸流不包含時間戳信息(時間戳信息一般由容器提供),為裸流生成時間戳信息會增加示例代碼的復雜性。因此輸入文件有特定要求,第一路輸入文件應包含至少一路視頻流,第二路輸入文件應包含至少一路音頻流,且輸入文件必須包含封裝格式,以便能取得時間戳信息,從而可根據時間戳信息對音視頻幀排序;另外,為了觀測輸出文件的音畫效果,第一路輸入中的視頻和第二路輸入中的音頻最好有一定的關系關系,本例中即是先從一個電影片段中分離出視頻和音頻,用作測試輸入。

4.1 源碼

源碼實現步驟如注釋所述。

#include <stdbool.h>
#include <libavformat/avformat.h>

/*
ffmpeg -i tnmil.flv -c:v copy -an tnmil_v.flv
ffmpeg -i tnmil.flv -c:a copy -vn tnmil_a.flv
./muxing tnmil_v.flv tnmil_a.flv tnmil_av.flv
*/

int main (int argc, char **argv)
{
    if (argc != 4)
    {
        fprintf(stderr, "usage: %s test.h264 test.aac test.ts\n", argv[0]);
        exit(1);
    }

    const char *input_v_fname = argv[1];
    const char *input_a_fname = argv[2];
    const char *output_fname = argv[3];
    int ret = 0;

    // 1 打開兩路輸入
    // 1.1 打開第一路輸入,並找到一路視頻流
    AVFormatContext *v_ifmt_ctx = NULL;
    ret = avformat_open_input(&v_ifmt_ctx, input_v_fname, NULL, NULL);
    ret = avformat_find_stream_info(v_ifmt_ctx, NULL);
    int video_idx = av_find_best_stream(v_ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    AVStream *in_v_stream = v_ifmt_ctx->streams[video_idx];
    // 1.2 打開第二路輸入,並找到一路音頻流
    AVFormatContext *a_ifmt_ctx = NULL;
    ret = avformat_open_input(&a_ifmt_ctx, input_a_fname, NULL, NULL);
    ret = avformat_find_stream_info(a_ifmt_ctx, NULL);
    int audio_idx = av_find_best_stream(a_ifmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    AVStream *in_a_stream = a_ifmt_ctx->streams[audio_idx];
    
    av_dump_format(v_ifmt_ctx, 0, input_v_fname, 0);
    av_dump_format(a_ifmt_ctx, 1, input_a_fname, 0);

    if (video_idx < 0 || audio_idx < 0)
    {
        printf("find stream failed: %d %d\n", video_idx, audio_idx);
        return -1;
    }

    
    // 2 打開輸出,並向輸出中添加兩路流,一路用於存儲視頻,一路用於存儲音頻
    AVFormatContext *ofmt_ctx = NULL;
    ret = avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, output_fname);
    
    AVStream *out_v_stream = avformat_new_stream(ofmt_ctx, NULL);
    ret = avcodec_parameters_copy(out_v_stream->codecpar, in_v_stream->codecpar);

    AVStream *out_a_stream = avformat_new_stream(ofmt_ctx, NULL);
    ret = avcodec_parameters_copy(out_a_stream->codecpar, in_a_stream->codecpar);
    
    if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) // TODO: 研究AVFMT_NOFILE標志
    { 
        ret = avio_open(&ofmt_ctx->pb, output_fname, AVIO_FLAG_WRITE);
    }

    av_dump_format(ofmt_ctx, 0, output_fname, 1);

    // 3 寫輸入文件頭
    ret = avformat_write_header(ofmt_ctx, NULL);

    AVPacket vpkt;
    av_init_packet(&vpkt);
    vpkt.data = NULL;
    vpkt.size = 0;

    AVPacket apkt;
    av_init_packet(&apkt);
    apkt.data = NULL;
    apkt.size = 0;

    AVPacket *p_pkt = NULL;

    int64_t vdts = 0;
    int64_t adts = 0;
    
    bool video_finished = false;
    bool audio_finished = false;
    bool v_or_a = false;

    // 4 從兩路輸入依次取得packet,交織存入輸出中
    printf("V/A\tPTS\tDTS\tSIZE\n");
    while (1)
    {
        if (vpkt.data == NULL && (!video_finished))
        {
            while (1)   // 取出一個video packet,退出循環
            {
                ret = av_read_frame(v_ifmt_ctx, &vpkt);
                if ((ret == AVERROR_EOF) || avio_feof(v_ifmt_ctx->pb))
                {
                    printf("video finished\n");
                    video_finished = true;
                    vdts = AV_NOPTS_VALUE;
                    break;
                }
                else if (ret < 0)
                {
                    printf("video read error\n");
                    goto end;
                }
                
                if (vpkt.stream_index == video_idx)
                {
                    // 更新packet中的pts和dts。關於AVStream.time_base的說明:
                    // 輸入:輸入流中含有time_base,在avformat_find_stream_info()中可取到每個流中的time_base
                    // 輸出:avformat_write_header()會根據輸出的封裝格式確定每個流的time_base並寫入文件中
                    // AVPacket.pts和AVPacket.dts的單位是AVStream.time_base,不同的封裝格式其AVStream.time_base不同
                    // 所以輸出文件中,每個packet需要根據輸出封裝格式重新計算pts和dts
                    av_packet_rescale_ts(&vpkt, in_v_stream->time_base, out_v_stream->time_base);
                    vpkt.pos = -1;      // 讓muxer根據重新將packet在輸出容器中排序
                    vpkt.stream_index = 0;
                    vdts = vpkt.dts;
                    break;
                }
                av_packet_unref(&vpkt);
            }

        }
        
        if (apkt.data == NULL && (!audio_finished))
        {
            while (1)   // 取出一個audio packet,退出循環
            {
                ret = av_read_frame(a_ifmt_ctx, &apkt);
                if ((ret == AVERROR_EOF) || avio_feof(a_ifmt_ctx->pb))
                {
                    printf("audio finished\n");
                    audio_finished = true;
                    adts = AV_NOPTS_VALUE;
                    break;
                }
                else if (ret < 0)
                {
                    printf("audio read error\n");
                    goto end;
                }
                
                if (apkt.stream_index == audio_idx)
                {
                    ret = av_compare_ts(vdts, out_v_stream->time_base, adts, out_a_stream->time_base);
                    apkt.pos = -1;
                    apkt.stream_index = 1;
                    adts = apkt.dts;
                    break;
                }
                av_packet_unref(&apkt);
            }

        }

        if (video_finished && audio_finished)
        {
            printf("all read finished. flushing queue.\n");
            //av_interleaved_write_frame(ofmt_ctx, NULL); // 沖洗交織隊列
            break;
        }
        else                            // 音頻或視頻未讀完
        {
            if (video_finished)         // 視頻讀完,音頻未讀完
            {
                v_or_a = false;
            }
            else if (audio_finished)    // 音頻讀完,視頻未讀完
            {
                v_or_a = true;
            }
            else                        // 音頻視頻都未讀完
            {
                // video pakect is before audio packet?
                ret = av_compare_ts(vdts, in_v_stream->time_base, adts, in_a_stream->time_base);
                v_or_a = (ret <= 0);
            }

            
            p_pkt = v_or_a ? &vpkt : &apkt;
            printf("%s\t%3"PRId64"\t%3"PRId64"\t%-5d\n", v_or_a ? "vp" : "ap", 
                   p_pkt->pts, p_pkt->dts, p_pkt->size);
            //ret = av_interleaved_write_frame(ofmt_ctx, p_pkt);
            ret = av_write_frame(ofmt_ctx, p_pkt);
            
            if (p_pkt->data != NULL)
            {
                av_packet_unref(p_pkt);
            }
        }
    }

    // 5 寫輸出文件尾
    av_write_trailer(ofmt_ctx);

    printf("Muxing succeeded.\n");

end:
    avformat_close_input(&v_ifmt_ctx);
    avformat_close_input(&a_ifmt_ctx);
    avformat_free_context(ofmt_ctx);
    return 0;
}

注意兩點:

4.1.1 音視頻幀交織問題

音頻流視頻流混合進輸出媒體時,需要確保音頻幀和視頻幀按照dts遞增的順序交錯排列,這就是交織(interleaved)問題。如果我們使用av_interleaved_write_frame(),這個函數會緩存一定數量的幀,將將緩存的幀按照dts遞增的順序寫入輸出媒體,用戶(調用者)不必關注交織問題(當然,因為緩存幀數量有限,用戶不可能完全不關注交織問題,小范圍的dts順序錯誤問題這個函數可以修正)。如果我們使用av_write_frame(),這個函數會直接將幀寫入輸出媒體,用戶(必須)自行處理交織問題,確保寫幀的順序嚴格按照dts遞增的順序。

代碼中,通過av_compare_ts()比較視頻幀dts和音頻幀dts哪值小,將值小的幀調用av_write_frame()先輸出。

運行測試命令(詳細測試方法在4.3節描述):

./muxing tnmil_v.flv tnmil_a.flv tnmil_av.flv

抓取一段打印看一下:

V/A     PTS     DTS     SIZE
vp       80       0     12840
ap        0       0     368  
ap       23      23     364  
vp      240      40     4346 
ap       46      46     365  
ap       70      70     365  
vp      160      80     1257 
ap       93      93     368  
ap      116     116     367  
vp      120     120     626  
ap      139     139     367  
vp      200     160     738  
ap      163     163     367  
ap      186     186     367  
vp      400     200     4938 

可以看到,第三列DTS,數值逐行遞增。

4.1.2 時間域轉換問題

在代碼中,讀取音頻幀或視頻幀后,調用了av_packet_rescale_ts()將幀中的時間相關值(pts、dts、duration)進行了時基轉換,從輸入流的時基轉換為輸出流的時間基(time_base)。pts/dts的單位是time_base,pts/dts的值乘以time_base表示時刻值。不同的封裝格式,其時間基(time_base)不同,所以需要進行轉換。當然,如果輸出封裝格式和輸入封裝格式相同,那不調用av_packet_rescale_ts()也可以。

封裝格式中的時間基就是流中的時間基AVStream.time_base,關於AVStream.time_base的說明:
輸入:輸入流中含有time_base,在avformat_find_stream_info()中可取到每個流中的time_base
輸出:avformat_write_header()會根據輸出的封裝格式確定每個流的time_base並寫入文件中

我們對比看一下,ts封裝格式和flv封裝格式的不同,運行測試命令(詳細測試方法在4.3節描述):

./muxing tnmil_v.flv tnmil_a.flv tnmil_av.ts

看一下前15幀的打印信息:

V/A     PTS     DTS     SIZE
vp      7200      0     12840
ap        0       0     368  
ap      2070    2070    364  
vp      21600   3600    4346 
ap      4140    4140    365  
ap      6300    6300    365  
vp      14400   7200    1257 
ap      8370    8370    368  
ap      10440   10440   367  
vp      10800   10800   626  
ap      12510   12510   367  
vp      18000   14400   738  
ap      14670   14670   367  
ap      16740   16740   367  
vp      36000   18000   4938 

和上一節flv封裝格式打印信息對比一下,不同封裝格式中同樣的一幀數據,其解碼時刻和播放時刻肯定是一樣的,但其PTS/DTS值是不同的,說明它們的時間單位不同。

4.2 編譯

源文件為muxing.c,在SHELL中執行如下編譯命令:

gcc -o muxing muxing.c -lavformat -lavcodec -lavutil -g

生成可執行文件muxing

4.3 驗證

測試文件下載:tnmil.flv
迷龍
先看一下測試用資源文件的格式:

think@opensuse> ffprobe tnmil.flv 
ffprobe version 4.1 Copyright (c) 2007-2018 the FFmpeg developers
Input #0, flv, from 'tnmil.flv':
  Metadata:
    encoder         : Lavf58.20.100
  Duration: 00:00:54.52, start: 0.000000, bitrate: 611 kb/s
    Stream #0:0: Video: h264 (High), yuv420p(progressive), 784x480, 25 fps, 25 tbr, 1k tbn, 50 tbc
    Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp

可以看到視頻文件'tnmil.flv'封裝格式為flv,包含一路h264編碼的視頻流和一路aac編碼的音頻流。

運行如下兩條命令,處理一下,生成只含一路視頻流的文件,和只含一路音頻流的文件,文件封裝格式均為FLV。這兩個文件用於下一步的測試。

ffmpeg -i tnmil.flv -c:v copy -an tnmil_v.flv
ffmpeg -i tnmil.flv -c:a copy -vn tnmil_a.flv

不輸出裸流,而輸出帶封裝格式的流,就是為了利用封裝格式中攜帶的時間戳信息,簡化本例程。

運行如下命令進行測試:

./muxing tnmil_v.flv tnmil_a.flv tnmil_av.flv

使用ffprobe檢測輸出文件正常。使用ffplay播放輸出文件正常,播放效果和原始的測試文件一致。

輸出另外一路封裝格式的文件再測試一下,運行如下命令:

./muxing tnmil_v.flv tnmil_a.flv tnmil_av.ts

使用ffprobe檢測輸出文件正常。使用ffplay播放輸出文件正常,播放效果和原始的測試文件一致。

如果我們改一下代碼,將av_packet_rescale_ts()注釋掉,再測上述兩條指令,發現tnmil_av.flv播放正常,tnmil_av.ts播放不正常,這和預期是相符的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM