FFplay源码分析-streams_open - 弦外之音

/ 0评 / 0

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

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


如下图所示,本文主要讲解 streams_open() 函数的内部逻辑。

如上图所示,streams_open() 里面比较重要的函数就是 初始化 frame_queue 跟 packet_queue,我画一个数据结构图会更容易理解。

数据结构图,我画着画着,不小心画出了整体的数据关系图,不过大家暂时只需要关注 Frame 那块就行。

如上图所示,我先简单讲解一下上面全局的数据关系图。

可以看到 struct VideoState 统领了全局。每一个数据流,音频流,视频流,数据流,都有各自的 struct FrameQueuestruct PacketQueuestruct Decoder

有一点需要注意的是 FrameQueue 是一个静态数组,PacketQueue 是一个动态链表。 FrameQueue 相对复杂一点,后面会详细讲解。



先介绍一下 struct Frame 结构体里面各个成员的作用。


接着介绍一下 struct FrameQueue 结构体里面各个成员的作用。

FrameQueue 里面的 keep_lastrindex_shown 这两个变量比较难懂,我仔细讲解一下。keep_last 其实是一个临时变量,他的作用是控制 rindex_shown 变成 1。rindex_shown 这个变量名我个人认为起的不太好,我一眼看这个变量没法看出他是做什么的。或者说这个逻辑本身就复杂,跟变量命名无关。

keep_last 是如何控制 rindex_shown 变成 1 的呢,请看下面代码。

static void frame_queue_next(FrameQueue *f)
{
    if (f->keep_last && !f->rindex_shown) {
        f->rindex_shown = 1; //注意这行代码。
        return;
    }
    frame_queue_unref_item(&f->queue[f->rindex]);
    if (++f->rindex == f->max_size)  //注意这行代码。
        f->rindex = 0;
    SDL_LockMutex(f->mutex);
    f->size--;
    SDL_CondSignal(f->cond);
    SDL_UnlockMutex(f->mutex);
}

可以看到 f->keep_last && !f->rindex_shown 会导致 rindex_show 置为 1,然后就return 了,没有执行后面的 frame_queue_unref_item()++f->rindex 也没有执行,也就是说 rindex 读索引没有偏移。

到这里,读者可能会有疑惑,如果 rindex 读索引没有偏移,下次用 queue[rindex] 的方式读下一帧不就读不到了吗?

对,queue[rindex] 确实读不到下一帧,所以ffplay 读取队列帧用的是 queue[ rindex + rindex_shown ]

分析到这里,可能读者到这里还是不太理解,我举个例子。

假如现在播放线程从 FrameQueue 里面取第一帧出来播放,因为是第一帧,所以 rindex_shown 此时是 0,rindex 也是 0。queue[ rindex + rindex_shown ]

这样确实取出来的是 0 索引的Frame。播放完之后,就会调 frame_queue_next() 函数,因为是第一次调 frame_queue_next(),所以就跑进去 if (f->keep_last && !f->rindex_shown) 这个条件,然后return,第一帧的frame数据没有销毁, rindex 读索引也没有偏移,但是,但是 rindex_shown 变成 1了,queue[ rindex + rindex_shown ] 就能正确读到 第二帧了。

其实源代码读取播放帧 真正的用法 是 queue[ (rindex + rindex_shown) % f->max_size] ,里面还有 % f->max_size 操作,这是个回环处理。例如如果 rindex 已经递增成 16 ,max_size 是16,那 16%16 ,取余数。就是0,rindex再+1,17%16,余数就是1。

这个就是 keep_lastrindex_shown 的作用,保存播放的上一帧在队列里面。那保存在队列里面有什么用呢?用视频流举例,当SDL窗口变小的时候,ffplay 可以取上一帧frame,重新渲染 texture 来适应缩小后的窗口大小。

还有一个注意的地方是,字幕流没有用到 keep_last 跟 rindex_shown,音频流好像也是没用到 keep_last 跟 rindex_shown,虽然音频流初始化队列的时候把 keep_last 设置为1,我个人感觉好像只有视频流用到 keep_last 跟 rindex_shown。

下面是初始化 Frame 队列代码,注意看最后一个参数 keep_last 是1 还是 0.

if (frame_queue_init(&is->pictq, &is->videoq, VIDEO_PICTURE_QUEUE_SIZE, 1) < 0)
        goto fail;
if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)
        goto fail;
if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)
        goto fail;

好了,分析到这里,一部分的数据结构已经分析完毕,文章开头的 frame_queue_init() 自行看代码即可理解,就是一些变量赋值。

然后 stream_open() 后面的 init_clock() 初始化时钟,本文暂时不详细分析时钟,先简单介绍时钟的作用,clock 是用来控制音视频同步的,如果只播放视频,实际上clock这个结构体是没用的,只播放音频也是。视频就按照帧率播放,音频就按照采样率播放就行。但如果音视频同时播放,时间长了会产生不同步,至于为什么会不同步,自行百度,答案太多了,我不粘贴了。

stream_open() 函数的最后 调用了SDL_CreateThread 创建一个新的线程,read_thread,这是第二个线程。

is->read_tid     = SDL_CreateThread(read_thread, "read_thread", is);

read_thread() 函数下篇文章再继续分析。


ffplay 源码分析,stream_open() 分析完毕。

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

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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注