ffplay代码分析(1):主体框架、主要数据结构
1. 介绍
ffplay,作为FFmpeg项目的组成部分,是一个轻量级的命令行多媒体播放器。ffplay不仅支持广泛的音视频格式,还经常被用作验证多媒体文件的工具。分析其代码能够更好地理解音视频播放和流处理的复杂性。分析代码还为定制或进一步开发多媒体应用提供参考。
2. 主体框架
为了提高性能和响应速度,ffplay使用多线程技术来并行处理音频、视频和用户输入。
- 解码线程:此线程专门处理音频和视频流的解码工作。它从文件或网络源中读取数据,然后解码为可以播放的原始帧。
- 音频播放线程:此线程负责将解码后的音频数据送入音频输出设备。
- 视频播放线程:与音频线程类似,但这个线程负责渲染视频帧到显示设备。
2.1. 初始化过程
在ffplay启动时,首先进行的是初始化过程。这包括:
- ffplay初始化流程概览:在这一阶段,ffplay进行预设值的配置,预加载必要的库和组件,并为后续的解析和播放做好准备。
- 解析命令行参数:根据用户的输入,ffplay会解析命令行参数,确定播放文件、输出格式、解码选项等。
- 资源分配和初始化:为视频和音频流分配必要的内存和资源,初始化解码器和渲染器。
2.2. 主线程循环
一旦初始化完成,ffplay进入主循环,持续进行以下操作直至播放结束或用户中断:
- 事件处理 :
- 键盘输入:响应用户的播放控制,如暂停、跳转和停止等。
- 鼠标事件:允许用户点击和拖拽进度条、调整音量等。
- 音视频同步机制:确保音频和视频流同步播放,避免延迟或跳帧,为用户提供流畅的观看体验。
2.3. 清理和退出
播放完毕或收到停止命令后,ffplay开始清理过程:
- 释放资源:关闭打开的文件和网络流,释放内存和其他资源。
- 错误处理和日志记录:确保任何未处理的错误得到记录,同时将运行日志保存,以供后续分析或故障排查。
3. 主要数据结构分析
3.1 struct VideoState
struct VideoState
是在 FFplay 中使用的一个核心数据结构,用于跟踪和管理播放器的各种状态。这是播放器的心脏,其中包含了关于正在播放的媒体文件的所有重要信息。
主要属性:
- format_ctx: 与输入文件相关的上下文,它包括了文件的所有流信息。
- audio_stream, video_stream, subtitle_stream: 指向当前文件中的音频、视频和字幕流的指针。
- audio_clock, video_clock: 音频和视频的时钟值,用于同步。
- audioq, videoq, subtitleq: 音频、视频和字幕的数据包队列。
- av_sync_type: 表示音频和视频同步的方式。
- read_tid: 用于读取数据包的线程的线程ID。
- audio_hw_buf_size: 音频硬件缓冲区大小。
- audio_volume, muted: 音量控制和静音状态。
- frame_timer, frame_last_delay: 用于视频同步的计时器。
- frame_last_pts: 最后一个视频帧的PTS。
- texture: 视频帧的纹理(如果使用图形硬件加速)。
- paused, last_paused, queue_attachments_req: 播放控制标志,例如暂停状态。
- step, seek_req, seek_flags, seek_rel: 与播放控制和搜索有关的标志。
- read_pause_return: 用于管理读取操作的返回值。
- continue_read_thread: 线程管理的条件变量。
- video_stream_idx, audio_stream_idx, subtitle_stream_idx : 这些索引值确定音频、视频和字幕流在
format_ctx
中的位置。 - pictq, sampq: 图像和音频样本的队列。
- pictq_size, pictq_rindex, pictq_windex: 图像队列的大小及其读写索引。
- audio_buf, audio_buf1: 存放解码后的音频数据的缓冲区。
- audio_buf_size, audio_buf_index: 音频缓冲区的大小和索引。
- av_sync_threshold: 同步阈值,当超出此阈值时,音频和视频会尝试重新同步。
- video_current_pts_time: 当前视频帧的时间戳。
- frame_drops_early, frame_drops_late: 早期和晚期丢弃的帧数统计。
- realtime: 标志,指示是否是实时流。
- eof: 文件结束标志。
- last_video_stream, last_audio_stream, last_subtitle_stream: 上次选择的音频、视频和字幕流的索引。
- filename: 正在播放的文件名。
- width, height: 视频的宽度和高度。
3.2 struct Clock
struct Clock
是 FFplay 中用于管理和跟踪音视频同步的关键数据结构。当播放多媒体文件时,音频和视频流需要协同工作,以确保在播放过程中它们保持同步。这就是 struct Clock
发挥作用的地方,它帮助确保音视频之间的时间戳匹配并同步播放。
主要属性:
- pts: 当前的显示时间戳(Presentation Timestamp)。这是最近一次更新的时间戳,通常用于音视频同步。
- pts_drift :
pts
与实际时间的偏差,用于调整时间戳。 - last_updated: 上次更新时间戳的时间。
- speed: 播放速度。正常速度为1.0,但用户可以选择加速或减速播放。
- serial: 用于区分数据包序列的序列号。在流切换时,它有助于避免使用过时的数据。
- paused: 表示时钟是否已暂停。
这个数据结构为FFplay提供了一个在音视频同步过程中使用的中央时钟概念。通过使用和更新这个结构,FFplay可以确保音频和视频帧在正确的时间播放,从而为用户提供无缝的观看体验。
3.3 struct MyAVPacketList
与PacketQueue
struct MyAVPacketList
struct MyAVPacketList
是一个用于在内部队列中存储 AVPacket的数据结构。当 FFplay 需要从输入源读取多媒体数据(如视频或音频流)时,它会将这些数据读入 AVPacket 。而 struct MyAVPacketList
则用于为这些数据包提供一个链表结构,以便按顺序存储和访问它们。
主要属性:
- pkt: 存储的 AVPacket。
- next : 指向链表中的下一个
MyAVPacketList
的指针。 - serial: 播放序列。
PacketQueue
PacketQueue
是用于存储和管理多个 MyAVPacketList
(或 AVPacket)的数据结构。这是一个队列,它保证数据包的有序存储和逐个处理,这对于多媒体播放器来说非常重要,因为需要确保按正确的顺序处理每个音频和视频数据包。
主要属性:
- first_pkt, last_pkt : 这是队列的首尾指针,指向链表的开始和结束。当新的
MyAVPacketList
被添加到队列时,它将被添加到last_pkt
所指向的位置,并更新last_pkt
指针。当数据包从队列中被取出时,first_pkt
将指向下一个MyAVPacketList
。 - nb_packets: 当前队列中的数据包数量。这是一个计数器,每当新的数据包被添加到队列时,它增加,每当数据包从队列中被取出时,它减少。
- size: 队列的总大小(字节)。这是队列中所有数据包的总大小。
- mutex: 用于同步的互斥锁。当有多个线程尝试访问或修改队列时,它确保队列的完整性和同步。
- cond: 用于等待条件的条件变量。当队列为空且线程正在等待新数据包时,它可能会使用这个条件变量。
使用:
PacketQueue
提供了一个高效、线程安全的方式来存储和管理MyAVPacketList
。其设计确保了多媒体数据包的有序和同步处理。
-
线程安全 :
mutex
和cond
使得PacketQueue
是线程安全的。当一个线程正在读取或写入队列时,其他线程会被阻塞,直到第一个线程完成其操作。 -
队列管理 :通过
first_pkt
和last_pkt
,队列可以快速地进行数据包的插入和删除操作。 -
大小和数量 :
nb_packets
和size
提供了关于队列状态的实时信息,如队列中有多少数据包,以及它们占用了多少内存。 -
3.3.1
packet_queue_init(PacketQueue *q)
: 初始化用于初始化一个新的
PacketQueue
结构体。它确保队列的属性被正确设置,以便于后续的数据包添加、删除和处理操作。
参数说明:
PacketQueue *q
: 一个指向PacketQueue
结构体的指针,该结构体将被初始化。结构体属性:
- first_pkt, last_pkt : 队列的首尾指针,初始化时为
NULL
。- nb_packets : 当前队列中的数据包数量,初始化为
0
。- size : 队列的总大小(字节),初始化为
0
。- mutex: 用于同步的互斥锁,初始化时为未锁状态。
- cond: 用于等待条件的条件变量,初始化时为默认状态。
- 3.3.2
packet_queue_destroy(PacketQueue *q)
: 销毁
用于销毁一个已经初始化的PacketQueue
结构体。它会释放与结构体关联的所有资源,包括数据包内存、互斥锁和条件变量。在调用此函数后,队列不应再被使用。
参数说明:
PacketQueue *q
: 一个指向PacketQueue
结构体的指针,该结构体将被销毁。结构体属性:
- first_pkt, last_pkt: 队列的首尾指针,销毁时会释放所有关联的数据包内存。
- mutex: 用于同步的互斥锁,销毁时会被释放。
- cond: 用于等待条件的条件变量,销毁时会被释放。
-
3.3.3
packet_queue_start(PacketQueue *q)
: 开始启动数据包队列,准备接收和处理数据包。
-
3.3.4
packet_queue_abort(PacketQueue *q)
: 中止中止数据包队列的操作,停止队列的所有活动。
-
3.3.5
packet_queue_put(PacketQueue *q, AVPacket *pkt)
: 输入将一个数据包放入队列。
-
3.3.6
packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
: 获取从队列中获取一个数据包。
参数 :
PacketQueue *q
- 指向要从中获取数据包的PacketQueue
结构体的指针。
AVPacket *pkt
- 用于存放从队列中获取的数据包的指针。
int block
- 指定操作是否应阻塞直到数据包可用。 -
3.3.7
packet_queue_put_nullpacket(PacketQueue *q, int stream_index)
: 输入空包在队列中放入一个空数据包,通常用于表示流的结束。
参数 :
PacketQueue *q
- 指向要添加空数据包的PacketQueue
结构体的指针。
int stream_index
- 空数据包的流索引。 -
3.3.8
packet_queue_flush(PacketQueue *q)
: 清空清空数据包队列,删除所有包含的数据包。
3.4 struct Frame
与FrameQueue
struct Frame
是一个数据结构,用于存储解码后的视频或音频帧。
属性:
AVFrame *frame
:存储实际帧数据的指针。int width, height, format
:帧的宽度、高度和格式。int uploaded
:指示帧是否已上传到GPU(如果使用硬件加速)。double pts, duration
:帧的显示时间戳和持续时间。
作用:存储从数据包中解码出来的原始帧数据以及与该帧相关的信息。
FrameQueue
是一个数据结构,用于存储多个Frame
结构体。这个队列通常用于缓存解码后的帧,以便后续的播放和显示。
属性:
Frame array[MAX_FRAMES]
:存储帧的数组,MAX_FRAMES
是队列可以容纳的最大帧数。int rindex, windex
:读和写索引,指示从哪里读取帧和向哪里写入新的帧。int size, max_size
:队列中的当前帧数和队列的最大帧数。SDL_mutex *mutex
:用于同步访问队列的互斥锁。SDL_cond *cond
:与互斥锁配合使用的条件变量。
作用:
- 存储解码后的帧,提供帧的连续播放。
- 确保帧的正确排序和同步播放。
- 通过缓存帧,实现流畅的播放,尤其是在解码速度变化或网络波动时。
这两个结构体是音视频播放和处理中的核心部分,它们确保音视频数据得到正确、有效和同步的处理。
-
3.4.1
frame_queue_init(FrameQueue *fq)
为
FrameQueue
分配所需的内存,并初始化相关的属性和同步机制。 -
3.4.2
frame_queue_destory(FrameQueue *fq)
: 销毁释放
FrameQueue
使用的所有资源,并清空队列。 -
3.4.3
frame_queue_peek_writable(FrameQueue *fq)
: 查看可写Frame检查队列中是否有空间容纳新的帧,如果有,返回一个可以写入的
Frame
的引用。 -
3.4.4
frame_queue_push(FrameQueue *fq, Frame *frame)
: 压入将解码后的帧压入队列中。
-
3.4.5
frame_queue_peek_readable(FrameQueue *fq)
: 查看可读Frame检查队列中是否有可读取的帧,如果有,返回一个可以读取的
Frame
的引用。 -
3.4.6
frame_queue_next(FrameQueue *fq)
: 下一个移动队列的读索引到下一个帧,并返回该帧的引用。
-
3.4.7
frame_queue_nb_remaining(FrameQueue *fq)
: 剩余的size返回队列中尚未读取的帧的数量。
-
3.4.8
frame_queue_peek(FrameQueue *fq)
: 查看当前帧返回队列中的当前帧,但不移动读取指针。这使得用户可以再次查看当前帧,而无需实际移动到下一帧。
-
3.4.9
frame_queue_peek_next(FrameQueue *fq)
: 查看下一帧预览队列中的下一个帧,但不移动读取指针。这允许用户查看接下来的帧,以确定是否需要进行其他操作,例如跳过或解码。
-
3.4.10
frame_queue_peek_last(FrameQueue *fq)
: 查看上一帧返回队列中上一个被读取的帧的引用。这使得用户可以回退并再次处理或显示之前的帧,而不会对队列中的其他帧产生任何影响。