音视频开发—音视频同步

1、FFmpeg简易播放器流程图

FFmpeg简易播放器流程图

音视频同步的目的是为了使播放的声音和显示的画面保持一致。

视频按帧播放,图像显示设备每次显示一帧画面,视频播放速度由帧率确定,帧率指示每秒显示多少帧;

音频按采样点播放,声音播放设备每次播放一个采样点,声音播放速度由采样率确定,采样率指示每秒播放多少个采样点。

如果仅仅是视频按帧率播放,音频按采样率播放,二者没有同步机制,即使最初音视频是基本同步的,随着时间的流逝,音视频会逐渐失去同步,并且不同步的现象会越来越严重。

这是因为:一、播放时间难以精确控制,二、异常及误差会随时间累积。所以,必须要采用一定的同步策略,不断对音视频的时间差作校正,使图像显示与声音播放总体保持一致。

音视频同步的方式基本是确定一个时钟(音频时钟、视频时钟、外部时钟)作为主时钟,非主时钟的音频或视频时钟为从时钟。在播放过程中,主时钟作为同步基准,不断判断从时钟与主时钟的差异,调节从时钟,使从时钟追赶(落后时)或等待(超前时)主时钟。

按照主时钟的不同种类,可以将音视频同步模式分为如下三种:

  • 音频同步到视频,视频时钟作为主时钟;

  • 视频同步到音频,音频时钟作为主时钟;

  • 音视频同步到外部时钟,外部时钟作为主时钟;

ffplay默认的同步方式:视频同步到音频。

2、I帧/IDR帧/P帧/B帧

I帧:I帧(Intra-codedpicture,帧内编码帧,常称为关键帧)包含一幅完整的图像信息,属于帧内编码图像,不含运动矢量,在解码时不需要参考其他帧图像。因此在I帧图像处可以切换频道,而不会导致图像丢失或无法解码。I帧图像用于阻止误差的累积和扩散。在闭合式GOP中,每个GOP的第一个帧一定是I帧,且当前GOP的数据不会参考前后GOP的数据。

IDR帧:IDR帧(InstantaneousDecodingRefreshpicture,即时解码刷新帧)是一种特殊的I帧。当解码器解码到IDR帧时,会将DPB(DecodedPictureBuffer,指前后向参考帧列表)清空,将已解码的数据全部输出或抛弃,然后开始一次全新的解码序列。IDR帧之后的图像不会参考IDR帧之前的图像,因此IDR帧可以阻止视频流中的错误传播,同时IDR帧也是解码器、播放器的一个安全访问点。

P帧:P帧(Predictive-codedpicture,预测编码图像帧)是帧间编码帧,利用之前的I帧或P帧进行预测编码。

B帧:B帧(Bi-directionallypredictedpicture,双向预测编码图像帧)是帧间编码帧,利用之前和(或)之后的I帧或P帧进行双向预测编码。B帧不可以作为参考帧。B帧具有更高的压缩率,但需要更多的缓冲时间以及更高的CPU占用率,因此B帧适合本地存储以及视频点播,而不适用对实时性要求较高的直播系统。

3、GOP

GOP(Group Of Pictures,图像组)是一组连续的图像,由一个I帧和多个B/P帧组成,是编解码器存取的基本单位。GOP结构常用的两个参数M和N,M指定GOP中两个anchor frame(anchor frame指可被其他帧参考的帧,即I帧或P帧)之间的距离,N指定一个GOP的大小。例如M=3,N=15,GOP结构为:IBBPBBPBBPBBPBB

GOP有两种:闭合式GOP和开放式GOP。

  • 闭合式GOP:闭合式GOP只需要参考本GOP内的图像即可,不需参考前后GOP的数据。这种模式决定了,闭合式GOP的显示顺序总是以I帧开始,以P帧结束;

  • 开放式GOP:开放式GOP中的B帧解码时可能要用到其前一个GOP或后一个GOP的某些帧。码流里面包含B帧的时候才会出现开放式GOP。

在开放式GOP中,普通I帧和IDR帧功能是有差别的,需要明确区分两种帧类型。在闭合式GOP中,普通I帧和IDR帧功能没有差别,可以不作区分。

开放式GOP和闭合式GOP中I帧、P帧、B帧的依赖关系如下图所示: 图3 GOP模式

4、DTS和PTS

DTS(Decoding Time Stamp, 解码时间戳),表示压缩帧的解码时间。 PTS(Presentation Time Stamp, 显示时间戳),表示将压缩帧解码后得到的原始帧的显示时间。 音频中DTS和PTS是相同的。视频中由于B帧需要双向预测,B帧依赖于其前和其后的帧,因此含B帧的视频解码顺序与显示顺序不同,即DTS与PTS不同。当然,不含B帧的视频,其DTS和PTS是相同的。下图以一个开放式GOP示意图为例,说明视频流的解码顺序和显示顺序:

图4 解码和显示顺序

以图中“B[1]”帧为例进行说明,“B[1]”帧解码时需要参考“I[0]”帧和“P[3]”帧,因此“P[3]”帧必须比“B[1]”帧先解码。这就导致了解码顺序和显示顺序的不一致,后显示的帧需要先解码。

video_decode_frame() 函数:

本函数实现如下功能:

  1. 从视频 packet 队列中取一个 packet。

  2. 将取得的 packet 发送给解码器。

  3. 从解码器接收解码后的 frame,此 frame 作为函数的输出参数供上级函数处理。

注意如下几点:

  1. 含 B 帧的视频文件,其视频帧存储顺序与显示顺序不同。

  2. 解码器的输入是 packet 队列,视频帧解码顺序与存储顺序相同,是按 dts 递增的顺序。dts 是解码时间戳,因此存储顺序、解码顺序都是 dts 递增的顺序。avcodec_send_packet() 就是将视频文件中的 packet 序列依次发送给解码器。发送 packet 的顺序如 IPBBPBB。

  3. 解码器的输出是 frame 队列,frame 输出顺序是按 pts 递增的顺序。pts 是解码时间戳。pts 与 dts 不一致的问题由解码器进行了处理,用户程序不必关心。从解码器接收 frame 的顺序如 IBBPBBP。

  4. 解码器中会缓存一定数量的帧,一个新的解码动作启动后,向解码器送入好几个 packet 后解码器才会输出第一个 packet,这比较容易理解,因为解码时帧之间有信赖关系,例如 IPB 三个帧被送入解码器后,B 帧解码需要依赖 I 帧和 P 帧,所以在 B 帧输出前,I 帧和 P 帧必须存在于解码器中而不能删除。理解了这一点,后面视频 frame 队列中对视频帧的显示和删除机制才容易理解。

  5. 解码器中缓存的帧可以通过冲洗(flush)解码器取出。冲洗(flush)解码器的方法就是调用 avcodec_send_packet(…, NULL),然后多次调用 avcodec_receive_frame() 将缓存帧取尽。缓存帧取完后,avcodec_receive_frame() 返回 AVERROR_EOF。

如何确定解码器的输出 frame 与输入 packet 的对应关系呢?可以对比 frame->pkt_pos 和 pkt.pos 的值,这两个值表示 packet 在视频文件中的偏移地址,如果这两个变量值相等,表示此 frame 来自此 packet。调试跟踪这两个变量值,即能发现解码器输入帧与输出帧的关系。

5、视频同步音频

视频同步到音频是 ffplay 的默认同步方式,在视频播放线程中实现。其中,video_refresh()函数实现了视频播放(包含同步控制)核心步骤。

相关函数关系如下:

main() -->
player_running() -->
open_video() -->
open_video_playing() -->
SDL_CreateThread(video_playing_thread, ...) 创建视频播放线程
​
video_playing_thread() -->
video_refresh()

视频播放线程源码如下:

static int video_playing_thread(void *arg)
{
    player_stat_t *is = (player_stat_t *)arg;
    double remaining_time = 0.0;
​
    while (1)
    {
        if (remaining_time > 0.0)
        {
            av_usleep((unsigned)(remaining_time * 1000000.0));
        }
        remaining_time = REFRESH_RATE;
        // 立即显示当前帧,或延时remaining_time后再显示
        video_refresh(is, &remaining_time);
    }
    return 0;
}
video_refresh()函数源码如下:

/* called to display each frame */
static void video_refresh(void *opaque, double *remaining_time)
{
    player_stat_t *is = (player_stat_t *)opaque;
    double time;
    static bool first_frame = true;
​
retry:
    if (frame_queue_nb_remaining(&is->video_frm_queue) == 0)  // 所有帧已显示
    {    
        // nothing to do, no picture to display in the queue
        return;
    }
​
    double last_duration, duration, delay;
    frame_t *vp, *lastvp;
​
    /* dequeue the picture */
    lastvp = frame_queue_peek_last(&is->video_frm_queue);     // 上一帧:上次已显示的帧
    vp = frame_queue_peek(&is->video_frm_queue);              // 当前帧:当前待显示的帧
​
    // lastvp和vp不是同一播放序列(一个seek会开始一个新播放序列),将frame_timer更新为当前时间
    if (first_frame)
    {
        is->frame_timer = av_gettime_relative() / 1000000.0;
        first_frame = false;
    }
​
    // 暂停处理:不停播放上一帧图像
    if (is->paused)
        goto display;
​
    /* compute nominal last_duration */
    last_duration = vp_duration(is, lastvp, vp);        // 上一帧播放时长:vp->pts - lastvp->pts
    delay = compute_target_delay(last_duration, is);    // 根据视频时钟和同步时钟的差值,计算delay值
​
    time = av_gettime_relative()/1000000.0;
    // 当前帧播放时刻(is->frame_timer+delay)大于当前时刻(time),表示播放时刻未到
    if (time < is->frame_timer + delay)
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值