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

本文爲做者原創,轉載請註明出處:http://www.javashuo.com/article/p-zgdqtllz-dc.htmlhtml

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

4. 複用例程

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

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

muxing

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

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()先輸出。code

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

./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播放不正常,這和預期是相符的。

相關文章
相關標籤/搜索