一、播放器原理
播放一个媒体文件一般需要 5
个基本模块,按层级顺序:
1、文件读取模块(Source)
2、解复用模块(Demuxer)
3、音视频解码模块(Decoder)
4、色彩空间转换模块(Color Space Converter)
5、音视频渲染模块(Render)

1、
文件读取模块(
Source
)
的作用是为下级解复用模块(
Demuxer
)以包的形式源源不断的提供数据流,
对于下一级的
Demuxer
来说,本地文件和网络数据是一样的。在
ffmpeg 框架中,文件读取模块可分为 3
层:
协议层: pipe
,
tcp
,
udp
,
http
等这些具体的本地文件或网络协议
抽象层:URLContext
结构来统一表示底层具体的本地文件或网络协议
接口层用:AVIOContext
结构来扩展
URLProtocol
结构成内部有缓冲机制的广泛意义上的文件,并且仅仅由最上层用 AVIOContext
对模块外提供服务,实现读媒体文件功能。
2、
解复用模块(
Demuxer
):作用是识别文件类型,媒体类型,分离出音频、视频、字幕原始数据流,打上时间戳信息后传给下级的视频频解码模块(Decoder)。可以简单的分为两层,底层是 AVIContext,TCPContext,UDPContext 等等,这些具体媒体的解复用结构和相关的基础程序,上层是 AVInputFormat 结构和相关的程序。上下层之间由AVInputFormat 相对应的AVFormatContext 结构的 priv_data 字段关联 AVIContext 或TCPContext 或 UDPContext 等等具体的文件格式。AVInputFormat 和具体的音视频编码算法格式由 AVFormatContext 结构的streams 字段关联媒体格式,streams 相当于Demuxer 的 output pin,
解复用模块分离音视频裸数据通过 streams 传递给下级音视频解码器。
3、
视频频解码模块(
Decoder
)
的作用就是解码数据包,并且把同步时钟信息传递下去。
4、
色彩空间转换模块(
Color Space Converter
)颜色空间转换过滤器的作用是把
视频解码器解码出来的数据转换成当前显示系统支持的颜色格式。
5、
音视频渲染模块(
Render
)的作用就是在适当的时间渲染相应的媒体,对视频媒体就是直接显示图像,对音频就是播放声音。
二、main()函数解析
1、FFplay的主要流程
调用如下函数:
av_register_all
()
:注册所有编码器和解码器。
show_banner
()
:打印输出
FFmpeg
版本信息(编译时间,编译选项,类库信息等)。
parse_options
()
:解析输入的命令。
SDL_Init
()
:
SDL
初始化。
stream_open
()
:打开输入媒体。
event_loop
()
:处理各种消息,不停地循环下去。
2、FFplay代码的总体结构
1)parse_options()
parse_options() 解析全部输入选项。 即将输入命令“ffplay -f h264 test.264”
中的
“-f”
这样的命令解析出来。 需要注意的是 ,FFplay
( ffplay.c) 的
parse_options()
和
FFmpeg
( ffmpeg.c) 中 的 parse_options()实际上是一样的。
2)SDL_Init()
SDL_Init()用于初始化
SDL
。
FFplay 中视频的显示和声音的播放都用到了
SDL
。
3)stream_open()
stream_open()的作用是
打开输入的媒体
。
这个函数还是比较复杂的,包含了 FFplay
中各种线程的创建。
stream_open()调用了如下函数:
packet_queue_init():初始化各个 PacketQueue(视频/音频/字幕)
read_thread()
:读取媒体信息线程。
read_thread()
调用了如下函数:
avformat_open_input():打开媒体。
avformat_find_stream_info():获得媒体信息。
av_dump_format():输出媒体信息到控制台。
stream_component_open()
:分别打开视频/音频/字幕解码线程。
refresh_thread()
:视频刷新线程。
av_read_frame():获取一帧压缩编码数据(即一个 AVPacket)。
packet_queue_put():根据压缩编码数据类型的不同(视频/音频/字幕),放到不同的 PacketQueue 中。
4)refresh_thread()
refresh_thread()调用了如下函数:
SDL_PushEvent(FF_REFRESH_EVENT):发送 FF_REFRESH_EVENT 的 SDL_Event
av_usleep():每两次发送之间,间隔一段时间。
5)stream_component_open()
stream_component_open()用于打开视频/音频/字幕解码的线程。 其函数调用关系如下所示。
avcodec_find_decoder():获得解码器。
avcodec_open2():打开解码器。
audio_open()
:打开音频解码。
SDL_PauseAudio():SDL 中播放音频的函数。
video_thread()
:创建视频解码线程。
subtitle_thread()
:创建字幕解码线程。
packet_queue_start():初始化 PacketQueue。
6)audio_open()
audio_open()调用了如下函数
SDL_OpenAudio():SDL 中打开音频设备的函数。
注意它是根据 SDL_AudioSpec 参数打开音频设备。
SDL_AudioSpec 中的 callback 字段指定了音频播放的回调函数 sdl_audio_callback()。当音频设备需要更多数据的时候,会调用该回调函数。因此该函数是会被反复调用的。
sdl_audio_callback()调用了如下函数
audio_decode_frame():解码音频数据。
update_sample_display():当不显示视频图像,而是显示音频波形的时候,调用此函数。
audio_decode_frame()调用了如下函数
packet_queue_get():获取音频压缩编码数据(一个 AVPacket)。
avcodec_decode_audio4():解码音频压缩编码数据(得到一个 AVFrame)。
swr_init():初始化 libswresample 中的 SwrContext。libswresample 用于音频采样采样数据(PCM)的转换。
swr_convert():转换音频采样率到适合系统播放的格式。
swr_free():释放 SwrContext。
7)video_thread()
video_thread()调用了如下函数
avcodec_alloc_frame():初始化一个 AVFrame。
get_video_frame()
:获取一个存储解码后数据的 AVFrame。
queue_picture():
get_video_frame()
调用了如下函数
packet_queue_get():获取视频压缩编码数据(一个 AVPacket)。
avcodec_decode_video2():解码视频压缩编码数据(得到一个 AVFrame)。
queue_picture()
调用了如下函数
SDL_LockYUVOverlay():锁定一个 SDL_Overlay。
sws_getCachedContext():初始化 libswscale 中的 SwsContext。Libswscale 用于图像的 Raw 格式数据(YUV,RGB)之间的转换。注意 sws_getCachedContext()和 sws_getContext()
功能是一致的。
sws_scale():转换图像数据到适合系统播放的格式。
SDL_UnlockYUVOverlay():解锁一个 SDL_Overlay。
8)subtitle_thread()
subtitle_thread()调用了如下函数
packet_queue_get():获取字幕压缩编码数据(一个 AVPacket)。
avcodec_decode_subtitle2():解码字幕压缩编码数据。
9)event_loop()
FFplay 再打开媒体之后,便会进入 event_loop()函数,永远不停的循环下去。
该函数用于接收并处理各种各样的消息。
有点像 Windows 的消息循环机制。
PS:该循环确实是无止尽的,其形
式为如下
SDL_Event event;
for (;;) {
SDL_WaitEvent(&event);
switch (event.type) {
case SDLK_ESCAPE:
case SDLK_q:
do_exit(cur_stream);
break;
case SDLK_f:
…
…
}
}
event_loop()函数调用关系:
根据 event_loop()
中
SDL_WaitEvent()
接收到的
SDL_Event
类型的不同,会调用不同的函
数进行处理(从编程的角度来说就是一个
switch()
语法)。
仅仅列举了几个例子:
SDLK_ESCAPE(按下
“ESC”
键):
do_exit()
。退出程序。
SDLK_f(按下
“f”
键):
toggle_full_screen()
。切换全屏显示。
SDLK_SPACE(按下
“
空格
”
键):
toggle_pause()
。切换
“
暂停
”
。
SDLK_DOWN(按下鼠标键):
stream_seek()
。跳转到指定的时间点播放。
SDL_VIDEORESIZE(窗口大小发生变化):
SDL_SetVideoMode()
。重新设置宽高。
FF_REFRESH_EVENT(视频刷新事件(自定义事件)):
video_refresh()
。刷新视频。
10)do_exit()
下面分析一下 do_exit()
函数。该函数用于退出程序。
函数的调用关系如下所示。
do_exit()
函数调用了以下函数
stream_close()
:关闭打开的媒体。
SDL_Quit()
:关闭
SDL
。
11)stream_close()
stream_close()函数调用了以下函数
packet_queue_destroy()
:释放
PacketQueue
。
SDL_FreeYUVOverlay()
:释放
SDL_Overlay
。
sws_freeContext()
:释放
SwsContext
。
12)video_refresh()
下面重点分析
video_refresh()
函数。
该函数用于
将图像显示到显示器上。
video_refresh()函数调用了以下函数
video_display()
:显示像素数据到屏幕上。
show_status
:这算不上是一个函数,但是是一个独立的功能模块,因此列了出来。该部分打印输出播放的状态至屏幕上。如下图所示。
video_display()函数调用了以下函数
video_open()
:初始化的时候调用,打开播放窗口。
video_audio_display()
:显示音频波形图(或者频谱图)的时候调用。里面包含了不少画
图操作。
video_image_display()
:显示视频画面的时候调用。
video_open()函数调用了以下函数
SDL_SetVideoMode()
:设置
SDL_Surface
(即
SDL
最基础的黑色的框)的大小等信息。
SDL_WM_SetCaption()
:设置
SDL_Surface
对应窗口的标题文字。
13)音视频同步
视频帧的播放时间其实依赖
pts
字段的,音频和视频都有自己单独的 pts。
但
pts 究竟是如何生成的呢
,假如音视频不同步时,pts 是否需要动态调整,以保证音
视频的同步?
下面先来分析,如何控制视频帧的显示时间的:
static void video_refresh(void *opaque){
//根据索引获取当前需要显示的
VideoPicture VideoPicture *vp = &is->pictq[is->pictq_rindex];
if (is->paused)
goto display; //只有在 paused 的情况下,才播放图像
// 将当前帧的 pts 减去上一帧的 pts,得到中间时间差
last_duration = vp->pts - is->frame_last_pts;
//检查差值是否在合理范围内,因为两个连续帧 pts 的时间差,不应该太大或太小
if (last_duration > 0 && last_duration < 10.0) {
/* if duration of the last frame was sane, update last_duration in video state */
is->frame_last_duration = last_duration;
}
//既然要音视频同步,肯定要以视频或音频为参考标准,然后控制延时来保证音视频的同步,
//这个函数就做这个事情了,下面会有分析,具体是如何做到的。
delay = compute_target_delay(is->frame_last_duration, is);
//获取当前时间
time= av_gettime()/1000000.0;
//假如当前时间小于 frame_timer + delay,也就是这帧改显示的时间超前,还没到,就直接返回
if (time < is->frame_timer + delay)
return;
//根据音频时钟,只要需要延时,即 delay 大于 0,就需要更新累加到 frame_timer 当中。
if (delay > 0)
//更新 frame_timer,frame_time 是 delay 的累加值
is->frame_timer += delay * FFMAX(1, floor((time-is->frame_timer) / delay));
SDL_LockMutex(is->pictq_mutex);
//更新 is 当中当前帧的 pts,比如 video_current_pts、video_current_pos 等变量
update_video_pts(is, vp->pts, vp->pos);
SDL_UnlockMutex(is->pictq_mutex);
display:
/* display picture */
if (!display_disable)
video_display(is);
}
函数 compute_target_delay 根据音频的时钟信号,重新计算了延时,从而达到了根据
音频
来调整视频的显示时间,从而实现音视频同步的效果。
static double compute_target_delay(double delay, VideoState *is) {
double sync_threshold, diff;
//因为音频是采样数据,有固定的采用周期并且依赖于主系统时钟,要调整音频的延时播放较难控制。 所以实际场合中视频同步音频相比音频同步视频实现起来更容易。
if (((is->av_sync_type == AV_SYNC_AUDIO_MASTER && is->audio_st) ||
is->av_sync_type == AV_SYNC_EXTERNAL_CLOCK)) {
//获取当前视频帧播放的时间,与系统主时钟时间相减得到差值
diff = get_video_clock(is) - get_master_clock(is);
sync_threshold = FFMAX(AV_SYNC_THRESHOLD, delay);
//假如当前帧的播放时间,也就是 pts,滞后于主时钟
if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
if (diff <= -sync_threshold)
delay = 0;
//假如当前帧的播放时间,也就是 pts,超前于主时钟,那就需要加大延时
else if (diff >= sync_threshold)
delay = 2 * delay;
}
}
return delay;
}
14)如何控制视频的播放和暂停?
static void stream_toggle_pause(VideoState *is) { if (is->paused) {
//由于 frame_timer 记下来视频从开始播放到当前帧播放的时间,所以暂停后,必须要将暂停的时间( is->video_current_pts_drift - is->video_current_pts)一起累加起来,并加上 drift 时间。
is->frame_timer += av_gettime() / 1000000.0 + is->video_current_pts_drift - is->video_current_pts;
if (is->read_pause_return != AVERROR(ENOSYS)) {
//并更新 video_current_pts
is->video_current_pts = is->video_current_pts_drift + av_gettime() / 1000000.0;
}
//drift 其实就是当前帧的 pts 和当前时间的时间差
is->video_current_pts_drift = is->video_current_pts - av_gettime() / 1000000.0;
}
//paused 取反,paused 标志位也会控制到图像帧的展示,按一次空格键实现暂停,再按一次就实现播 放了。
is->paused = !is->paused;
}
特别说明:paused 标志位控制着视频是否播放,当需要继续播放的时候,一定要重新更新当前所需要播放 帧的 pts 时间,因为这里面要加上已经暂停的时间。
15)逐帧播放是如何做的?
在视频解码线程中,不断通过
stream_toggle_paused
,控制对视频的暂停和显示,从而实现逐帧播放:
static void step_to_next_frame(VideoState *is) {
//逐帧播放时,一定要先继续播放,然后再设置 step 变量,控制逐帧播放
if (is->paused)
stream_toggle_pause(is);//会不断将 paused 进行取反
is->step = 1;
}
其原理就是不断的播放,然后暂停,从而实现逐帧播放:
static int video_thread(void *arg) {
if (is->step)
stream_toggle_pause(is);
……………………
if (is->paused)
goto display;//显示视频
}
}