最近在做一个本地播放器,这个播放器是从ffplay.c改编而来的,对源代码进行重构,并添加QT界面。其中的核心文件是videoctl,这篇文章目的在于细致的分析这个文件,进而理解播放器的工作流程。至于播放器的外围部分这里不做详细讨论,仅在第一节中进行阐述

undefined

播放器线程分配。

播放器组成

  • 播放器前端是基于QT的,核心媒体处理分三个部分,一个是数据包的格式规定,一个是音视频的渲染,另一个则是音视频的控制。用户界面做了解耦处理,将信号和槽做多级触发。项目中的核心结构体是CurVideoState,在对这个结构体进行操作时要加线程锁,保证线程安全,例如执行全屏操作时,一方面是QT计算屏幕大小,一方面是SDL实现视频的缩放,刷新等等。这其中就多次涉及CurVideoState状态的改变,每次改变时都要注意为结构体加入线程锁。
  • 注意关于界面的一些操作设置,例如单击鼠标,双击标题栏,拖动界面。通过注册表来实现软件启动时的默认设置,例如初始文件等。
  • 全屏播放应该有两种方式,包括整体填充屏幕,和show组件填充屏幕
  • 下面是ffmpeg解码的流程,结构体分析参考另一篇文章simplest——ffmpeg_player源码阅读

核心媒体处理

  • datactl.h : 媒体数据控制器,负责处理媒体数据流
  • ffplay_renderer.h/c: 基于FFmpeg的渲染器,处理音视频解码和渲染
  • videoctl.h/cpp: 视频控制器,管理视频播放、暂停等核心功能

用户界面组件

  • mainwindow.h/cpp: 主窗口,应用程序的入口和主界面
  • ctlbar.h/cpp: 控制栏,提供播放、暂停、音量等控制按钮
  • title.h/cpp: 标题栏组件
  • show.h/cpp: 显示区域,负责视频内容的显示
  • playlist.h/cpp: 播放列表管理
  • medialist.h/cpp**: 媒体列表组件
  • customslider.h/cpp: 自定义滑块控件,可能用于进度条或音量控制

核心结构体

  • VideoState
    其中包括,指向解复用器的指针,线程句柄,音视频、外部时钟。强制刷新变量。视频状态变量等等多个变量的存储,

  • 时钟类

  • MyAVpacketList
    其中存放packet的链表结构,并且每次拖动进度条会更新其中的serial字段。

  • PacketQueue
    定义Packet 队列自身属性,包括serial字段,包数量字段,队列所有元素数据大小字段等等

  • packet_qeue_start
    其中有packet_queue_put_private(q,&flush_pkt);这里传入了一个flush_pkt是为了触发PacketQueue中对应的serial。使其+1.并触发解码器清空自身的缓存,重新开始解码

  • FrameQueue
    帧队列,其中包含多个接口;frame_peek_writable获取一个可写的frame,可以以阻塞或者非阻塞方式进行。

  • 线程列表

媒体处理分析

以下是videoctl.h分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
#ifndef VIDEOCTL_H
#define VIDEOCTL_H

#include <QObject>
#include <QThread>
#include <QString>
#include "datactl.h"
#include "globalhelper.h"

class VideoCtl : public QObject
{
Q_OBJECT

public:
/*
需要注意的是,这里创建了一个单例对象,具体做法就是将构造函数,析构函数,拷贝构造方法和赋值构造函数都放入Private中,禁止外部构造对象
在类里会设计一个获取实例的静态函数,可以全局访问
单例设计模式分为懒汉式和饿汉式,具体见文章,https://juejin.cn/post/6844903928497176584
*/
static VideoCtl *GetInstance();
~VideoCtl();

static int read_thread_wrapper(void *arg);//读取线程包装器
static int audio_thread_wrapper(void *arg);//音频线程包装器
static int video_thread_wrapper(void *arg);//视频线程包装器
static int subtitle_thread_wrapper(void *arg);//字幕线程包装器
/*
首先确保其他播放线程结束,避免资源冲突,使用m_playLoopIndex字段。接下里发送信号给标题栏完成播放的初始化
接下来通过对预置选项的判断,完成标志位初始化,完成SDL初始化。并配置硬件加速,以及渲染。
创建LoopThread线程
*/
void StartPlay(QString strFileName, WId widPlayWid);//开始播放

void StopPlay();//停止播放
/*
创建SDL事件,
判断双击事件,显示全屏,通过检测鼠标单击事件的时间差实现。
鼠标隐藏事件
*/
void LoopThread(VideoState* CurStream);//循环线程
/*
这个函数是设备初始化的核心,负责配置和打开SDL音频设备。完成音频格式协商,设备参数设置,错误恢复等复杂流程
创建音频结构体,其中包括期望的音频参数,和实际的音频参数。
定义备选声道数和采样率数组,用于降级策略。
成功打开设备后,会返回spec结构体,其中包括
spec.freq: 实际的采样率(可能与请求不同)
spec.channels: 实际的声道数
spec.format: 实际的音频格式
spec.samples: 音频缓冲区大小
spec.size: 音频缓冲区的总字节数
*/
int audio_open(void *opaque, AVChannelLayout *wanted_channel_layout,
int wanted_sample_rate, struct AudioParams *audio_hw_params);//音频打开
/*
double pts; // 当前媒体的时间戳
double pts_drift; // 与系统时钟的漂移(pts - gettime)
double last_updated; // 最后更新时间(系统时间)
double speed; // 播放速率(支持变速)
int serial; // 用于检测流是否连续的序列号
int paused; // 暂停状态
int *queue_serial; // 关联队列版本号(跨线程同步)
av_gettime_relative()获取自程序启动的相对时间,单位us

`set_clock_at`: 设置时钟的基础函数,直接设置时钟的时间戳、序列号和更新时间点
- `pts`: 当前媒体的时间戳(秒)
- `serial`: 用于检测流是否连续的序列号
- `time`: 系统当前时间(秒)
- `pts_drift`: 计算媒体时间与系统时间的偏移量

`set_clock`: 简化版的时钟设置函数,自动获取当前系统时间并调用`set_clock_at`

`get_clock`: 获取时钟当前时间,考虑了暂停状态和播放速度
- 如果序列号不匹配(可能发生了seek操作),返回NaN
- 如果暂停,直接返回保存的pts
- 如果正常播放,返回考虑了时间漂移和播放速度的实际时间点

`sync_clock_to_slave`: 将一个时钟同步到另一个时钟(主从同步)
- 当两个时钟差距超过阈值(AV_NOSYNC_THRESHOLD)时进行同步
- 通常用于将视频时钟或外部时钟同步到音频时钟

这些函数在音视频同步中的应用:
- 音频、视频和外部时钟各有一个Clock实例(audclk, vidclk, extclk)
- 通过`get_master_sync_type`决定主时钟(通常是音频时钟)
- 其他时钟会通过`sync_clock_to_slave`与主时钟同步
- 视频帧显示时会根据与主时钟的差异决定是加速、减速还是丢帧

这种设计确保了即使在网络延迟、解码速度不一致等情况下,音视频仍能保持同步播放。
*/
void set_clock_at(Clock *c, double pts, int serial, double time);//设置时钟
void sync_clock_to_slave(Clock *c, Clock *slave);//同步时钟
double get_clock(Clock *c);//获取时钟
void set_clock(Clock *c, double pts, int serial);//设置时钟
/*
维护一个sample_array,实现音频的定长缓存,缓存区为环形
*/
void update_sample_display(VideoState *is, short *samples, int samples_size);
/*
帧获取与序列检查:从音频帧队列获取下一帧,确保序列号匹配,避免处理过时的帧
音频同步:调用synchronize_audio计算需要的样本数,用于与主时钟同步
动态重采样:
检测音频格式变化(采样率、声道布局、样本格式)
动态创建和配置重采样器
处理音频格式转换,确保输出符合音频设备要求
时钟更新:根据帧的PTS和样本数更新音频时钟,这是音视频同步的关键
内存管理:动态分配和管理音频缓冲区,处理各种边缘情况
*/
int audio_decode_frame(VideoState *is);//音频解码
/*
通过动态调整音频样本数量来实现音频与主时钟的同步。
检查是否需要同步(音频不是主时钟时才需要)
计算音频时钟与主时钟的差异
如果差异在合理范围内,累积并计算平均差异
当积累足够样本且平均差异超过阈值时,调整音频样本数
返回调整后的样本数,供重采样器使用
*/
int synchronize_audio(VideoState *is, int nb_samples);//同步音频
double get_master_clock(VideoState *is);//获取主时钟
int get_master_sync_type(VideoState *is);//获取主同步类型
/*
创建线程锁
创建AVPacket
创建格式上下文
打开媒体文件并解析格式
获取流信息
seek机制
选择最佳流
处理附加信息
错误处理
*/
int read_thread(void *arg);//读取线程
/*
开启解码线程
*/
int decoder_start(Decoder *d, int (*fn)(void *), const char *thread_name, void *arg);//解码器启动
/*
用户界面调用OnPlaySeek(double dPercent)函数,该函数内部会计算实际时间戳并调用stream_seek(),并且重启线程
*/
void stream_seek(VideoState *is, int64_t pos, int64_t rel, int by_bytes);

void step_to_next_frame(VideoState *is);//步进到下一个帧,逐帧前进功能
void stream_toggle_pause(VideoState *is);//处理暂停到播放的状态转换
int stream_has_enough_packets(AVStream *st, int stream_id, PacketQueue *queue);//流是否有足够的包,在读取线程中控制读取数据的速率
/*
解码器初始化,
*/
int stream_component_open(VideoState *is, int stream_index);
/*
进行音频解码,decoder_decode_frame()函数
首先进行初始化,并进入解码循环
解码一帧
检查音频配置是否发生改变
音频过滤处理,实现混音,重采样等操作
帧处理和入队
检查包队列序列号和解码器序列号,判断是否发送seek操作
*/
int audio_thread(void *arg);


inline int cmp_audio_fmts(enum AVSampleFormat fmt1, int64_t channel_count1,
enum AVSampleFormat fmt2, int64_t channel_count2);//比较音频格式。用于配置编码器

int configure_audio_filters(VideoState *is, const char *afilters, int force_output_format);//配置音频过滤器
/*
初始化,
获取视频帧,不仅使用decoder_decode_frame,同时通过检测序列号和时间戳来进行丢帧,进行同步
检测视频格式变化
进行过滤处理
时间戳处理,需要计算过滤器的延时
队列管理
同步控制
*/
int video_thread(void *arg);//视频线程
/*
丢帧策略
在两种情况下考虑丢帧:
framedrop > 0: 强制丢帧模式
framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER: 开启丢帧且视频不是主时钟
通过判断fabs(diff)决定是否丢弃。
*/
int get_video_frame(VideoState *is, AVFrame *frame);//获取视频帧
int queue_picture(VideoState *is, AVFrame *src_frame,
double pts, double duration, int64_t pos, int serial);//队列图片
void set_default_window_size(int width, int height, AVRational sar);//设置默认窗口大小
void calculate_display_rect(SDL_Rect *rect,
int scr_xleft, int scr_ytop, int scr_width, int scr_height,
int pic_width, int pic_height, AVRational pic_sar);//计算显示矩形
/*
将视频帧转换成YUV格式数据
处理视频自动旋转
添加自定义滤镜:宏INSERT_FILT
*/
int configure_video_filters(AVFilterGraph *graph,
VideoState *is, const char *vfilters, AVFrame *frame);//配置视频过滤器
int configure_filtergraph(AVFilterGraph *graph, const char *filtergraph,
AVFilterContext *source_ctx, AVFilterContext *sink_ctx);//配置过滤器图
/*
字幕也是当作一个frame帧来进行处理的。
同样利用PTS来进行同步,
*/
int subtitle_thread(void *arg);//字幕线程

int filter_codec_opts(const AVDictionary *opts, enum AVCodecID codec_id,
AVFormatContext *s, AVStream *st, const AVCodec *codec,
AVDictionary **dst);//过滤器编码选项

int check_stream_specifier(AVFormatContext *s, AVStream *st, const char *spec);//检查流规范
int create_hwaccel(AVBufferRef **device_ctx);//创建硬件加速

void toggle_pause(VideoState *is);//切换暂停
void do_exit(VideoState *is);//退出

void stream_cycle_channel(int codec_type);//流循环通道
void toggle_audio_display();//切换音频显示

VideoState *m_CurVideoState = nullptr;//当前视频状态
bool m_playLoopIndex;//播放循环索引

private:
explicit VideoCtl(QObject *parent = nullptr);//构造函数

bool Init();//初始化
bool ConnectSignalSlots();//连接信号与槽

void UpdateVolume(int sign, double step);//更新音量

void refresh_loop_wait_event(VideoState* is, SDL_Event* event);//刷新循环等待事件

VideoState* stream_open(const char *filename, const AVInputFormat *iformat);//流打开
void stream_close(VideoState *is);//流关闭
void stream_component_close(int stream_index);//流组件关闭

int video_open();

void seek_chapter(int incr);//章节跳转

void toggle_full_screen();//切换全屏
void toggle_mute();//切换静音

void init_clock(Clock *c, int *queue_serial);//初始化时钟

// double get_master_clock();
// int get_master_sync_type();
void check_external_clock_speed();//检查外部时钟速度
void set_clock_speed(Clock *c, double speed);//设置时钟速度

void update_volume(int sign, double step);//更新音量
void update_video_pts(double pts, int serial);//更新视频时间戳

double compute_target_delay(double delay);//计算目标延迟
double vp_duration(Frame *vp, Frame *nextvp);//计算视频持续时间

void video_refresh(double *remaining_time);//视频刷新
void video_display();//视频显示
void video_audio_display();//视频音频显示
void video_image_display();//视频图像显示
int upload_texture(SDL_Texture **tex, AVFrame *frame);//上传纹理
int realloc_texture(SDL_Texture **texture, Uint32 new_format,
int new_width, int new_height, SDL_BlendMode blendmode, int init_texture);//重新分配纹理
void get_sdl_pix_fmt_and_blendmode(int format, Uint32 *sdl_pix_fmt,
SDL_BlendMode *sdl_blendmode);//获取SDL像素格式和混合模式
void set_sdl_yuv_conversion_mode(AVFrame *frame);//设置SDL YUV转换模式
inline void fill_rectangle(int x, int y, int w, int h);//填充矩形
inline int compute_mod(int a, int b);//计算模数


static VideoCtl* m_pInstance;//单例实例

bool m_initIndex;//初始化索引

SDL_Window *window;//窗口
SDL_Renderer *renderer;//渲染器
SDL_RendererInfo renderer_info = { 0 };//渲染器信息
SDL_AudioDeviceID audio_dev;//音频设备
WId play_wid; // 播放窗口

int screen_width;//屏幕宽度
int screen_height;//屏幕高度
int startup_volume;//启动音量

//播放刷新循环线程
std::thread m_tPlayLoopThread;//播放刷新循环线程

int m_frameW;//帧宽度
int m_frameH;//帧高度

signals:
void SigPlayMsg(QString strMsg); //< 错误信息
void SigFrameDimensionsChanged(int nFrameWidth, int nFrameHeight); //<视频宽高发生变化

void SigVideoTotalSeconds(int nSeconds);//视频总秒数
void SigVideoPlaySeconds(int nSeconds);//视频播放秒数

void SigVideoVolume(double dPercent);//视频音量
void SigPauseStat(bool bPaused);//暂停状态

void SigStop();//停止
void SigStopAndNext();//停止并下一个

void SigStopFinished(); // 停止播放完成

void SigStartPlay(QString strFileName);

public slots:
void OnPlaySeek(double dPercent);//播放进度
void OnPlayVolume(double dPercent);//播放音量
void OnPause();//暂停
void OnStop();//停止
void OnSeekForward();//向前搜索
void OnSeekBack();//向后搜索
void OnAddVolume();//增加音量
void OnSubVolume();//减少音量
};

#endif

本站由 Edison.Chen 创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

undefined