ffplay源码分析4-音视频同步

本文为作者原创,转载请注明出处:https://blog.csdn.net/leisure_chn/article/details/87949741

ffplay是FFmpeg工程自带的简单播放器,使用FFmpeg提供的解码器和SDL库进行视频播放。本文基于FFmpeg工程4.1版本进行分析,其中ffplay源码清单如下:
https://github.com/FFmpeg/FFmpeg/blob/n4.1/fftools/ffplay.c

在尝试分析源码前,可先阅读如下参考文章作为铺垫:
[1]. 雷霄骅,视音频编解码技术零基础学习方法
[2]. 视频编解码基础概念
[3]. 色彩空间与像素格式
[4]. 音频参数解析
[5]. FFmpeg基础概念

“ffplay源码分析”系列文章如下:
[1]. ffplay源码分析1-概述
[2]. ffplay源码分析2-数据结构
[3]. ffplay源码分析3-代码框架
[4]. ffplay源码分析4-音视频同步
[5]. ffplay源码分析5-图像格式转换
[6]. ffplay源码分析6-音频重采样
[7]. ffplay源码分析7-播放控制

4. 音视频同步

音视频同步的目的是为了使播放的声音和显示的画面保持一致。视频按帧播放,图像显示设备每次显示一帧画面,视频播放速度由帧率确定,帧率指示每秒显示多少帧;音频按采样点播放,声音播放设备每次播放一个采样点,声音播放速度由采样率确定,采样率指示每秒播放多少个采样点。如果仅仅是视频按帧率播放,音频按采样率播放,二者没有同步机制,即使最初音视频是基本同步的,随着时间的流逝,音视频会逐渐失去同步,并且不同步的现象会越来越严重。这是因为:一、播放时间难以精确控制,二、异常及误差会随时间累积。所以,必须要采用一定的同步策略,不断对音视频的时间差作校正,使图像显示与声音播放总体保持一致。

我们以一个44.1KHz的AAC音频流和25FPS的H264视频流为例,来看一下理想情况下音视频的同步过程:
一个AAC音频frame每个声道包含1024个采样点(也可能是2048,参[13][14]),则一个frame的播放时长(duration)为:(1024/44100)×1000ms = 23.22ms
一个H264视频frame播放时长(duration)为:1000ms/25 = 40ms
声卡虽然是以音频采样点为播放单位,但通常我们每次往声卡缓冲区送一个音频frame,每送一个音频frame更新一下音频的播放时刻,即每隔一个音频frame时长更新一下音频时钟,实际上ffplay就是这么做的。
我们暂且把一个音频时钟更新点记作其播放点,理想情况下,音视频完全同步,音视频播放过程如下图所示:

音视频同步理想情况

音视频同步的方式基本是确定一个时钟(音频时钟、视频时钟、外部时钟)作为主时钟,非主时钟的音频或视频时钟为从时钟。在播放过程中,主时钟作为同步基准,不断判断从时钟与主时钟的差异,调节从时钟,使从时钟追赶(落后时)或等待(超前时)主时钟。按照主时钟的不同种类,可以将音视频同步模式分为如下三种:
音频同步到视频,视频时钟作为主时钟。
视频同步到音频,音频时钟作为主时钟。
音视频同步到外部时钟,外部时钟作为主时钟。
ffplay中同步模式的定义如下:

enum {
   
    AV_SYNC_AUDIO_MASTER, /* default choice */
    AV_SYNC_VIDEO_MASTER,
    AV_SYNC_EXTERNAL_CLOCK, /* synchronize to an external clock */
};

4.1 time_base

time_base是PTS和DTS的时间单位,也称时间基。
不同的封装格式time_base不一样,转码过程中的不同阶段time_base也不一样。
以mpegts封装格式为例,假设视频帧率25FPS为。编码数据包packet(数据结构AVPacket)对应的time_base为AVRational{1,90000}。原始数据帧frame(数据结构AVFrame)对应的time_base为AVRational{1,25}。在解码或播放过程中,我们关注的是frame的time_base,定义在AVStream结构体中,其表示形式AVRational{1,25}是一个分数,值为1/25,单位是秒。在旧的FFmpeg版本中,AVStream中的time_base成员有如下注释:

For fixed-fps content, time base should be 1/framerate and timestamp increments should be 1.

当前新版本中已无此条注释。

typedef struct AVStream {
   
    ......
    
    /**
     * This is the fundamental unit of time (in seconds) in terms
     * of which frame timestamps are represented.
     *
     * decoding: set by libavformat
     * encoding: May be set by the caller before avformat_write_header() to
     *           provide a hint to the muxer about the desired timebase. In
     *           avformat_write_header(), the muxer will overwrite this field
     *           with the timebase that will actually be used for the timestamps
     *           written into the file (which may or may not be related to the
     *           user-provided one, depending on the format).
     */
    AVRational time_base;
    
    ......
}

/**
 * Rational number (pair of numerator and denominator).
 */
typedef struct AVRational{
   
    int num; ///< Numerator
    int den; ///< Denominator
} AVRational;

time_base是一个分数,av_q2d(time_base)则可将分数转换为对应的double类型数。因此有如下计算:

AVStream *st;
double duration_of_stream = st->duration * av_q2d(st->time_base);   // 视频流播放时长
double pts_of_frame = frame->pts * av_q2d(st->time_base);           // 视频帧显示时间戳

4.2 PTS/DTS/解码过程

DTS(Decoding Time Stamp, 解码时间戳),表示packet的解码时间。
PTS(Presentation Time Stamp, 显示时间戳),表示packet解码后数据的显示时间。
音频中DTS和PTS是相同的。视频中由于B帧需要双向预测,B帧依赖于其前和其后的帧,因此含B帧的视频解码顺序与显示顺序不同,即DTS与PTS不同。当然,不含B帧的视频,其DTS和PTS是相同的。

下图以一个开放式GOP示意图为例,说明视频流的解码顺序和显示顺序

解码和显示顺序

采集顺序指图像传感器采集原始信号得到图像帧的顺序。
编码顺序指编码器编码后图像帧的顺序。存储到磁盘的本地视频文件中图像帧的顺序与编码顺序相同。
传输顺序指编码后的流在网络中传输过程中图像帧的顺序。
解码顺序指解码器解码图像帧的顺序。
显示顺序指图像帧在显示器上显示的顺序。
采集顺序与显示顺序相同。编码顺序、传输顺序和解码顺序相同。
图中“B[1]”帧依赖于“I[0]”帧和“P[3]”帧,因此“P[3]”帧必须比“B[1]”帧先解码。这就导致了解码顺序和显示顺序的不一致,后显示的帧需要先解码。

上述内容可参考“视频编解码基础概念”。

理解了含B帧视频流解码顺序与显示顺序的不同,才容易理解解码函数decoder_decode_frame()中对视频解码的处理:
avcodec_send_packet()按解码顺序发送packet。
avcodec_receive_frame()按显示顺序输出frame。
这个过程由解码器处理,不需要用户程序费心。
decoder_decode_frame()是非常核心的一个函数,代码本身并不难理解。decoder_decode_frame()是一个通用函数,可以解码音频帧、视频帧和字幕帧,本节着重关注视频帧解码过程。音频帧解码过程在注释中。

// 从packet_queue中取一个packet,解码生成frame
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
   
    int ret = AVERROR(EAGAIN);

    for (;;) {
   
        AVPacket pkt;

        // 本函数被各解码线程(音频、视频、字幕)首次调用时,d->pkt_serial等于-1,d->queue->serial等于1
        if (d->queue->serial == d->pkt_serial) {
   
            do {
   
                if (d->queue->abort_request)
                    return -1;

                // 3. 从解码器接收frame
                switch (d->avctx->codec_type) {
   
                    case AVMEDIA_TYPE_VIDEO:
                        // 3.1 一个视频packet含一个视频frame
                        //     解码器缓存一定数量的packet后,才有解码后的frame输出
                        //     frame输出顺序是按pts的顺序,如IBBPBBP
                        //     frame->pkt_pos变量是此frame对应的packet在视频文件中的偏移地址,值同pkt.pos
                        ret = avcodec_receive_frame(d->avctx, frame);
                        if (ret 
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值