ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

FFplay源码分析-音视频同步1

2022-02-11 09:30:17  阅读:242  来源: 互联网

标签:clock 音频 音视频 callback 源码 FFplay audio buf size


本系列 以 ffmpeg4.2 源码为准,下载地址:链接:百度网盘 提取码:g3k8

FFplay 源码分析系列以一条简单的命令开始,ffplay -i a.mp4。a.mp4下载链接:百度网盘,提取码:nl0s 。


之前的文章已经讲解完 3 个线程的内部逻辑。

  • read_thread(),packet 读取线程,不断往 PacketQueue 写数据,直至队列写满。
  • audio_thread(),音频解码线程,从 音频PacketQueue 拿数据,解码出Frame,不断往 FrameQueue写数据,直至队列写满。
  • video_thread(),视频解码线程,从 视频PacketQueue 拿数据,解码出Frame,不断往 FrameQueue写数据,直至队列写满。

3个线程的逻辑已经讲完,后续的文章会开始讲播放线程 跟 音视频同步算法。

本文先从 音频播放线程讲起,音视频同步方式是 AV_SYNC_AUDIO_MASTER,以音频为主时钟。

音频播放线程分析开始:

音频播放线程函数 是 sdl_audio_callback() ,在 audio_open() 的时候用 wanted_spec.callback 指定了 SDL 的音频回调函数。

wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / SDL_AUDIO_MAX_CALLBACKS_PER_SEC));
wanted_spec.callback = sdl_audio_callback; //指定回调函数。
wanted_spec.userdata = opaque;

ffpaly 限制了 sdl_audio_callbakc() 每秒回调次数不超过 SDL_AUDIO_MAX_CALLBACKS_PER_SEC,也就是不超过30次。

下面对 sdl_audio_callback() 的参数做一些讲解:

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len){...}
  • void *opaque ,调用层参数,ffpaly是传了一个 struct VideoState 进去。在 wanted_spec.userdata 里指定的。
  • Uint8 *stream,SDL 播放内存的指针,往这个指针写数据,SDL 就能播放数据。
  • int len ,需要写多少数据进去 stream 指针。

len的计算方式其实非常有趣,就是根据 wanted_spec.samples 计算来的。在本文命令里面,wanted_spec.samples 赋值为 2048,每次回调callback,需要往 stream 指针写 2048个采样。那2048个样本又是多少字节?

由于 audio_open() 打开音频设备的时候指定用 16位的格式 AUDIO_S16SYS 存储一个采样,所以一个采样是2个字节。然后因为文件音频是双声道的,每个声道都取2048个样本,那就是 2048 * 2 * 2 = 8196 字节。有兴趣可以打印一下 callback 里面的len变量,在本文命令下,一直都是8196字节。


sdl_audio_callback() 的流程图如下:

流程图非常简洁,但其实 sdl_audio_callback() 里的逻辑是比较复杂的,流程图我画不出来,只能简洁,用文字来补充。

在讲 callback 的内部逻辑之前,需要先介绍一下struct VideoState 里面音频相关的一些字段。

struct VideoState 音频播放线程重要字段变量:

1,audio_hw_buf_size:hw是硬件的意思,这个字段代表SDL 线程执行 sdl_audio_callback() 的时候,SDL 硬件内部还有多少字节音频的数据没有播放。没错,SDL 线程并不是没有音频数据可以播放了才调 sdl_audio_callback() 来拿数据,而是他内部还剩 audio_hw_buf_size 长度的数据就调 sdl_audio_callback() 来拿数据,是提前拿数据的,这个概念特别重要。这个 audio_hw_buf_size 其实是等于 sdl_audio_callback() 里面的len变量的,也是为什么后面set_clock_at() 设置音频时钟pts的时候会用 audio_hw_buf_size 乘以2 。画个图加深理解。

在这里插入图片描述

如上图所示,SDL 内部是有两块内存的,一块红色的是它内部未播放的音频数据,红色部分我们的程序是无法操作那部分内存的,绿色部分就是 回调函数里面的 stream 指针,绿色部分的数据我们程序可以操作。audio_hw_buf_size 是在 audio_open() 的时候赋值的,请看代码:

if ((ret = audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
	goto fail;
is->audio_hw_buf_size = ret; //注意这里

2,audio_buf :音频数据,从 AVframe 的 data[] 里面得到,为什么要创建这样一个字段,是因为,回调函数有时候并不需要把 AVframe 的 data[] 全部写入到 stream 指针。例如回调的时候 SDL 需要 1500 个样本,一个AVFrame里面有 1024 个样本,那第二个Frame只需要拿476个样本就行,剩下的样本会放在 audio_buf 里面等待下次回调再继续读取。

3,audio_buf1 : 重采样临时存储地址,重采样的时候使用,暂时不需要关注,因为本文命令没有跑进去重采样的逻辑。后续音频向视频同步会重采样,到时候再仔细讲解。

4,audio_buf_size:表明 audio_buf 里面有多少字节数据。

5,audio_buf_index:audio_buf 的数据已经使用了多少。

6,audio_write_buf_size:audio_buf 的数据还剩下多少。

struct VideoState 里面有 还有两个字段比较容易混淆。需要重点讲解以下。

1,audio_filter_src:这个字段存的是 刚从解码器出来的 AVFrame 的采样率,格式等数据。

2,audio_src :这个字段存的是 刚从 filter_graph 出来的 AVFrame 的采样率,格式等数据。因为经过filter转换之后,采样率跟格式可能会改变。

audio_src 是 在 audio_open() 之后进行第一次赋值,赋值为打开的音频设备的采样,格式。

 if ((ret = audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
 	goto fail;
 is->audio_hw_buf_size = ret;
 is->audio_src = is->audio_tgt; //注意这里

然后 audio_src 会跟 从 filter_graph 出来的 frame的采样率等做比较,如果不一致就会执行重采样,audio_src 第二次赋值为 刚从 filter_graph 出来的 AVFrame 的采样率,格式等。

在这里插入图片描述


然后因为 audio_decode_frame() 函数里面用了 AVFrame::extended_data ,所以仔细讲一下,我刚学ffmpeg的时候也很迷惑,网上有些代码读数据用 extended_data ,有些用 data。现在就来讲下 AVFrame 里面 extended_data 跟data的区别。

对于视频帧而言,extended_data 跟 data是完全一样的,没有区别。

extended_data 是为音频帧准备的,因为 AVFrame::data 是一个 8 长度的数组,只能存到 8个声道,如果音频流是9声道,10声道怎么办?这就是 extended_data的作用,存储更多的声道数据。


sdl_audio_callback() 的流程描述如下:

1,一开始就是一个 while循环 ,while (len > 0),回调的时候必须往 stream 写入 len 大小的音频数据。

2,调用 audio_decode_frame() 把音频数据转移到 is->audio_buf。audio_decode_frame() 函数的内部逻辑非常复杂,后面会仔细讲。

3,音频数据转移到 is->audio_buf 之后,程序就开始拷贝数据了,代码里有非常多的零碎逻辑,实际上做的事情就是,判断 audio_buf 的音频数据需不需全部拷贝给 SDL 的内存,如果需要,全部拷贝就完事了。如果只是拷贝一部分到 SDL 的内存就够用了,就拷贝一部分,剩下的还在 audio_buf。如果全部拷贝完 audio_buf 还拼不够 len 长度,就会继续调 audio_decode_frame() 读取下一帧音频数据转移到 is->audio_buf,继续搞。

4,拷贝完 len 长度的数据给 SDL 内存 stream 之后,就会调 set_clock_at() 设置当前音频数据播放到哪里了。这个 set_clock_at() 比较难懂,需要仔细讲解,请看代码。

ffplay.c 2481行
set_clock_at(&is->audclk,
   is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec
                  ,is->audio_clock_serial, audio_callback_time / 1000000.0);

这里 set_clock_at() 函数的第二个参数 pts 的计算非常复杂。

pts = is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec

首先,is->audio_clock 是从 audio_decode_frame() 里面计算得到的,因为音频是主时钟,is->audio_clock 的计算方式还没那么绕,后续讲到 视频是同步主时钟的时候,is->audio_clock 会更难理解。

ffplay.c 2426行
is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;

从上面的代码可以看到 is->audio_clock 赋值为当前帧的pts + duration,也就是说,当SDL里面 audio_hw_buf_size 长度的红色音频数据 跟 我们后续写入 stream 指针的绿色音频数据,再加上 is->audio_write_buf_size 的数据,这 3块音频数据都播放完的时候,音频时钟的 pts 就等于 当前帧的pts + duration,为什么要加上 audio_write_buf_size ,是因为当前帧的 数据不一定全部会拷贝到绿色内存,可能塞不下。

在这里插入图片描述

所以,简洁来说,audio_hw_buf_size + len + audio_write_buf_size 这3块数据都播放完,音频时钟的 pts 就等于 is->audio_clock了。但是,此时此刻,这3块数据还在内存里面,还没播放呢。

所以,此时此刻,音频时钟的pts 就等于 is->audio_clock 减去那3块数据的时间。因为 len 等于 audio_hw_buf_size,所以这条公式就出来了。

pts = is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec

还有 set_clock_at() 最后一个参数 audio_callback_time,这个参数也需要注意,因为 audio_callback_time 是在 入口就初始化了,为什么要入口初始化呢?

因为,sdl_audio_callback() 函数开始执行的时候,SDL 里面还有 audio_write_buf_size 长度的数据没播放,但是 sdl_audio_callback() 并不会阻塞SDL 的音频播放,sdl_audio_callback()里面是有重采样处理的,在重采样消耗时间的时候,红色内存的数据也在同时播放的。

所以 set_clock_at() 设置的是 在 sdl_audio_callback() 开始执行的那一刻,音频数据播放到哪里了。并不是 sdl_audio_callback() 快执行完了 ,音频数据播放到哪里了。


接下来讲解以下 clock 里面 的 serial 有何作用。

typedef struct Clock {
 	..省略代码..
    int serial;           /* clock is based on a packet with this serial */
    int *queue_serial;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

struct Clock 里面有两个序列号,serial 是每次从FrameQueue 里面拿frame的时候,就会把 serial 赋值为 frame 的序列号。

queue_serial 就是一直指向 PacketQueue 队列的序列号,之前说过,如果seek了,PacketQueue 队列的序列号 会 +1 ,这时候 Clock 的 serial 就会不等于 queue_serial,get_clock() 函数会返回 NAN 的时间。

static double get_clock(Clock *c)
{
    if (*c->queue_serial != c->serial)
        return NAN;
}
注意第二个参数,初始化 queue_serial 为 PacketQueue 队列的序列号
init_clock(&is->vidclk, &is->videoq.serial);
init_clock(&is->audclk, &is->audioq.serial);
init_clock(&is->extclk, &is->extclk.serial);

sdl_audio_callback() 里面还有一个重要函数没有讲解,那就是 audio_decode_frame()。

audio_decode_frame() 这个函数命名不是特别好,audio_decode_frame 里面其实并没有执行解码操作,解码操作都是在 audio_thread() 线程里面做的。

/**
 * Decode one audio frame and return its uncompressed size.
 *
 * The processed audio frame is decoded, converted if required, and
 * stored in is->audio_buf, with size in bytes given by the return
 * value.
 */
static int audio_decode_frame(VideoState *is){...}

audio_decode_frame() 函数上方的注释已经讲明了这个函数的功能,就是把音频数据放到 is->audio_buf,返回数据大小,让别人来取。

不过 audio_decode_frame() 内部实现还是非常复杂的,需要仔细讲解一下。

入口就是一个针对 32 位做的特殊处理。

#if defined(_WIN32)
        while (frame_queue_nb_remaining(&is->sampq) == 0) {
            if ((av_gettime_relative() - audio_callback_time) > 1000000LL * is->audio_hw_buf_size / is->audio_tgt.bytes_per_sec / 2)
                return -1;
            av_usleep (1000);
        }
#endif

这里为什么会针对 32 位的系统做sleep呢?我估计是因为,32位的系统普遍硬件比较差,解码速度比较慢。

1000000LL * is->audio_hw_buf_size / is->audio_tgt.bytes_per_sec / 2 其实等于 二分之一的 callback 时间,例如本文命令是 0.04 s 回调一次,那 1000000LL * is->audio_hw_buf_size / is->audio_tgt.bytes_per_sec / 2 就等于 0.02s。

等待1/2 的时间是为什么呢?我估计是让 硬件差的环境在临界时间点不要轻易返回静音数据,延迟音频帧的播放。

举个例子,先说下背景,播放的还是 a.mp4 ,每0.04s秒 调一次 audio_decode_frame() 函数。

在 14:00 的时候,下午2点的时候,SDL 回调了 audio_decode_frame() 函数,因为系统慢,此刻 FrameQueue 里面没有数据可以拿。14:00:005 的时候才解码出数据放进去 FrameQueue ,如果 不sleep,立即就返回了。因为下次回调要等 0.04s,就会导致音频设备多播放了 0.04s 的静音数据。如果sleep 就可以在 14:00:005 左右的时刻把数据写进 SDL 的内存,也就是说本来应该播放的数据,如果因为解码慢,不sleep,延迟了0.04s才播放。

所以ffplay针对 32位的系统做了优化,折中一下,如果没有解码出数据,等待 1/2 的回调时间,不至于延迟那么大。

重要知识点: 1000000LL 是一百万的意思,后面的LL是转换成长整形数据类型,Long Long

然后,因为本文命令没有跑进去重采样逻辑,暂时不讲解里面的重采样逻辑,后续音频向视频同步的时候,要重采样做样本补偿,统一讲解。

没有重采样逻辑之后,audio_decode_frame() 剩下的逻辑就非常简单了,后面几乎没什么逻辑,就两点。

  1. 把 AVFrame 的data 转移到 is->audio_buf。

    is->audio_buf = af->frame->data[0];
    
  2. 设置 is->audio_clock。

    is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;
    

ffplay 源码分析,sdl_audio_callback() 音频播放线程分析完毕。

©版权所属:知识星球:弦外之音,QQ:2338195090。

由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。

推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

标签:clock,音频,音视频,callback,源码,FFplay,audio,buf,size
来源: https://blog.csdn.net/u012117034/article/details/122873602

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有