FFmpeg封装格式处理3-复用例程

作者:叶余

来源:https://www.cnblogs.com/leisure_chn/p/10506653.html


FFmpeg 封装格式处理相关内容分为如下几篇文章:

[1]. FFmpeg 封装格式处理-简介

[2]. FFmpeg 封装格式处理-解复用例程

[3]. FFmpeg 封装格式处理-复用例程

[4]. FFmpeg 封装格式处理-转封装例程

4. 复用例程

复用(mux),是 multiplex 的缩写,表示将多路流(视频、音频、字幕等)混入一路输出中(普通文件、流等)。

本例实现,提取第一路输入文件中的视频流和第二路输入文件中的音频流,将这两路流混合,输出到一路输出文件中。

FFmpeg封装格式处理3-复用例程

本例不支持裸流输入,是因为裸流不包含时间戳信息(时间戳信息一般由容器提供),为裸流生成时间戳信息会增加示例代码的复杂性。因此输入文件有特定要求,第一路输入文件应包含至少一路视频流,第二路输入文件应包含至少一路音频流,且输入文件必须包含封装格式,以便能取得时间戳信息,从而可根据时间戳信息对音视频帧排序;另外,为了观测输出文件的音画效果,第一路输入中的视频和第二路输入中的音频最好有一定的关系关系,本例中即是先从一个电影片段中分离出视频和音频,用作测试输入。

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

FFmpeg封装格式处理3-复用例程

先看一下测试用资源文件的格式:

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 播放不正常,这和预期是相符的。


「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。

FFmpeg封装格式处理3-复用例程

上一篇:FFmpeg编解码处理2-编解码API详解


下一篇:FFmpeg封装格式处理2-解复用例程