前言
暂停功能是播放器的一个重要功能,分为触发暂停和恢复播放两部分。
一、暂停
1、触发暂停的位置
/* handle an event sent by the GUI */
static void event_loop(VideoState *cur_stream)
{
SDL_Event event;
double incr, pos, frac;
for (;;) {
double x;
refresh_loop_wait_event(cur_stream, &event);
switch (event.type) {
case SDL_KEYDOWN:
switch (event.key.keysym.sym) {
case SDLK_p:
case SDLK_SPACE:
toggle_pause(cur_stream);
break;
default:
break;
}
break;
default:
break;
}//end case SDL_KEYDOWN
}//end for
}//end event_loop
当按下键盘的空格键或P键时,产生键盘按下事件SDL_KEYDOWN,执行toggle_pause()。
toggle_pause()的代码如下,内部调用了stream_toggle_pause(),并且退出逐帧模式。
static void toggle_pause(VideoState *is)
{
stream_toggle_pause(is);
is->step = 0;
}
stream_toggle_pause()代码如下:
/* pause or resume the video */
static void stream_toggle_pause(VideoState *is)
{
if (is->paused) {
is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated;
printf("is->read_pause_return=%d AVERROR(ENOSYS)=%d\n", is->read_pause_return, AVERROR(ENOSYS));
if (is->read_pause_return != AVERROR(ENOSYS)) {
is->vidclk.paused = 0;
}
set_clock(&is->vidclk, get_clock(&is->vidclk), is->vidclk.serial);
}
set_clock(&is->extclk, get_clock(&is->extclk), is->extclk.serial);
is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
}
刚开始暂停时,is->paused标志为0,没有执行if语句,if语句恢复播放时执行
is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
把is->paused和三个时钟的paused标志置为1,表示此时是暂停状态。暂停后,受影响的代码有读取线程read_thread,音频解码线程audio_thread、视频解码线程video_thread,音频播放线程sdl_audio_callback,视频播放refresh_loop_wait_event()。先看下视频播放暂停
2、视频播放暂停
视频播放的流程为:event_loop()-->refresh_loop_wait_event()-->video_refresh()-->video_display(),当is->paused标志为1时,如果强制刷新标志is->force_refresh为0,程序不执行video_refresh(),如下所示

没有其他事件时,程序会在refresh_loop_wait_event()的while循环中空转
static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
double remaining_time = 0.0;/* 休眠等待,remaining_time的计算在video_refresh中 */
/* 调用SDL_PeepEvents前先调用SDL_PumpEvents,将输入设备的事件抽到事件队列中 */
SDL_PumpEvents();
while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {
if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY){
SDL_ShowCursor(0);
cursor_hidden = 1;
}
if (remaining_time > 0.0) //sleep控制画面输出的时机
av_usleep((int64_t)(remaining_time * 1000000.0)); // remaining_time <= REFRESH_RATE
remaining_time = REFRESH_RATE;
//显示模式不等于SHOW_MODE_NONE && 非暂停状态 || 强制刷新状态
if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
video_refresh(is, &remaining_time);
SDL_PumpEvents();
}
}
如果暂停状态下,窗口大小变了或全屏了等,触发is->force_refresh为1,此时会执行video_refresh()

并触发执行video_display()

通过calculate_display_rect()重新计算SDL窗口的大小,并继续渲染最后一帧。因为vp帧的数据已经拷贝到纹理了,此时vp->uploaded为1不会再调用upload_texture()。
无论窗口变化与否,程序都不会执行frame_queue_next(),也就是不会消费FrameQueue的视频数据了。
3、音频播放静音数据
音频回调函数是sdl_audio_callback(),暂停状态下,sdl需要填充数据时程序运行到audio_decode_frame()后立刻返回-1,也就是说SDL取不到解码后的音频数据了

由于audio_decode_frame()返回-1,所以运行下面的if语句
audio_size = audio_decode_frame(is);
if (audio_size < 0) {
/* if error, just output silence */
is->audio_buf = NULL;
//确保is->audio_buf_size是is->audio_tgt.frame_size的整数倍。
is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
}
下面这一句的意思是确保is->audio_buf_size是is->audio_tgt.frame_size的整数倍。
is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
由于is->audio_buf为NULL,所以此时填充的都是静音数据

4、read_thread读取线程
在read_thread线程中搜索paused关键字,发现有如下代码
if (is->paused != is->last_paused) {
is->last_paused = is->paused;
if (is->paused)
{
is->read_pause_return = av_read_pause(ic);
printf("is->read_pause_return=%d\n", is->read_pause_return);
}
else
{
av_read_play(ic);
printf("is->av_read_play\n");
}
}
is->paused和is->last_paused开始时都为0,暂停后is->paused为1, 执行av_read_pause()暂停读取网络数据,恢复播放后调用av_read_play()继续读取网络数据,本地文件不受av_read_pause()和av_read_play()的影响,所以读取线程会一直读取AVPacket数据。
5、音频和视频解码暂停
解码函数decoder_decode_frame()没有paused关键字,说明解码没有立即暂停。读取线程一直在读取数据,音频播放和视频播放不消费FrameQueue数据了,解码线程一直解码的话,会导致音频和视频的FrameQueue数据积压,直到满为止,所以音频和视频解码线程会阻塞在frame_queue_peek_writable()-->SDL_CondWait()中,如下图所示:

音视频解码线程阻塞后,不会调用packet_queue_get()从packetQueue中消费数据了。
再回到read_thread线程中,由于解码线程不再从PacketQueue中get数据解码,read_thread一直读取数据的话,会导致PacketQueue越来越大,所以read_thread线程会在以下代码处休眠并等待超时,
SDL_CondWaitTimeout()在10毫秒后,如果没有收到其他线程发送信号,会自动返回继续continue循环等待10毫秒。如果收到信号,SDL_CondWaitTimeout()会立即返回读取数据,此时说明解码线程没有数据可解码了,解码线程发送信号在以下代码处:

如果解码线程没有数据可解码了,会发送信号立刻唤醒read_thead线程读取数据。
6、时钟暂停
在暂停状态下,当调用get_clock()获取时钟时,直接返回最后一次的pts。

视频时钟的更新是在video_refresh()里,暂停时不会运行update_video_pts()更新视频时钟,所以视频的pts一直不变。
音频时钟的更新是在sdl_audio_callback()中,暂停时sdl_audio_callback()仍然在运行,所以会调用set_clock_at()更新音频时钟
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);
因为获取不到解码数据了, 所以is->audio_clock不会继续更新,is->audio_clock保留的是暂停前播放的最后一帧音频的结束时间,is->audio_hw_buf_size为固定值8192,is->audio_write_buf_size为0,所以计算的pts不变
is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size)
即音频时钟的pts字段不更新,只更新音频时钟的last_updated和pts_drift字段。
注:音频每次播放2048个样本,对于44100的采用率,SDL需要取21.53(44100/2048)次数据。
SDL每次取数据的间隔为46.44毫秒(1000/21.53),即0.046秒
7、总结:
暂停后,音频和视频都暂停播放了,FrameQueue的数据积压满。
解码线程阻塞在frame_queue_peek_writable()中,直到收到信号为止。
read_thread线程会超时等待10毫秒判断一次。
二、恢复播放
1、触发恢复播放
再次按下P键或空格键时,恢复成播放状态,运行toggle_pause()。

在toggle_pause()中,恢复播放状态后is->step=0,说明取消单帧播放模式。即无论暂停时还是恢复播放时,都要取消单帧播放模式。

再来看stream_toggle_pause()内部,由于此时is->paused标志为1,运行if语句,如下所示

1)修改is->frame_timer的值(当前帧的开始显示时间)
is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated;
is->frame_timer是暂停前最后一帧的开始显示时间(系统时间)
is->vidclk.last_updated:暂停前最后一帧更新pts的系统时间,
// 走到这一步,说明已经到了或过了该显示的时间,待显示帧vp的状态变更为当前要显示的帧
is->frame_timer += delay; // 更新当前帧播放的时间
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
{
is->frame_timer = time; //如果和系统时间差距太大,就纠正为系统时间
}
SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts))
update_video_pts(is, vp->pts, vp->pos, vp->serial); // 更新video时钟
该时间是最后一帧的实际显示时间,以如下图所示

t1是执行is->frame_timer += delay的时间,t2是执行update_video_pts()的时间,t3是暂停时间,t4是恢复播放时间。
t1和t2代码非常接近,可认为是同一时间,av_gettime_relative()也区分不了这个差异。
t2到t3是该帧开始显示到暂停的时间。有可能刚显示就暂停了,也有可能快显示结束才暂停,所以t2到t3介于[0,一帧的时长]的区间,t3到t4是暂停经过的时间,取决于人工操作,可能暂停1秒,可能暂停10秒。那么
av_gettime_relative() / 1000000.0 - is->vidclk.last_updated:相当于t4-t2,该帧开始显示到恢复播放经过的时长

is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated值的含义:
is->frame_timer的值记作t0,此时的is->frame_timer是加上delay后的值,代表当前帧的计算结束时间,真正的结束时间是t1时刻,由于t1略大于t0,所以t0大概在t1前一点位置(两者可能相差几微妙)。
那么is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated后的值如下图红色大括号所示的区域

所以is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated后的值非常接近当前时间t4,两者相差t2到t0的时间。因为t1和t2相等,所以差值t1-t0是实际的帧结束时间和计算的帧结束时间的差值。如果这个值忽略不记的话,is->frame_timer直接赋值为当前时间比较好理解。即is->frame_timer = av_gettime_relative() / 1000000.0。
2)视频时钟恢复
此时的时钟保存的last_updated是暂停前的最后一次更新的系统时间,pts_drift是根据last_updated和pts计算的。如果再用get_clock()获取视频时钟时,计算的值是错误的。比如视频在10:00暂停了,过了10秒恢复播放,那么恢复播放1秒后,用get_clock()获取的时钟是pts+11秒,实际视频才播放了一秒,视频时钟应该是pts+1。所以视频时钟的last_updated应该修改为恢复播放时的系统时间,下面代码的作用就是如此。
set_clock(&is->vidclk, get_clock(&is->vidclk), is->vidclk.serial);
这句代码主要作用是就是更新视频时钟的last_updated和pts_drift字段。
第二个参数通过get_clock()获取的是视频的pts(不是c->pts_drift + time,因为此时paused还为1)。
在set_clock()的内部结构中,通过av_gettime_relative()获取当前时间后,调用set_clock_at()更新last_updated和pts_drift字段。这样再次用get_clock()获取时钟时,就能计算出正确的时钟了。
static void set_clock(Clock *c, double pts, int serial)
{
double time = av_gettime_relative() / 1000000.0;
set_clock_at(c, pts, serial, time);
}
static void set_clock_at(Clock *c, double pts, int serial, double time)
{
c->pts = pts;
c->last_updated = time;
c->pts_drift = c->pts - time;
c->serial = serial;
}
3)音频时钟恢复
在sdl_audio_callback()中,音频时钟的last_updated字段一直在更新(每隔0.046秒更新一次)。所以c->pts_drift也一直在更新,这里也没有更新音频时钟的代码。
4)暂停标志位恢复
is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
取反重新把paused标志位置为0,此时音视频时钟又继续跑起来了。
2、视频恢复播放
paused标志位恢复成0后,refresh_loop_wait_event()继续调用video_refresh()播放视频。再次根据时钟差以及is->frame_timer(该帧的显示时间),计算当前显示帧的播放时长,恢复播放。当该帧显示完毕后,调用frame_queue_next(),通过发送信号给视频解码线程,通知解码线程立即恢复工作。

3、音频播放恢复sdl_audio_callback()
在sdl_audio_callback()中,音频暂停时运行到if(is->paused)处会立即返回,此时向SDL填充的都是静音数据

paused恢复标志位为0后,会继续向下运行,继续从FrameQueue中读取解码后的音频数据向SDL填充,此时音频恢复播放。
同时在frame_queue_next()中同视频一样,通过调用SDL_CondSignal()发送信号给音频解码线程audio_thread,唤醒audio_thread继续解码。
注:音频FrameQueue和视频FrameQueue中的条件变量和锁都是独立的,在初始化FrameQueue的函数frame_queue_init()中,各个队列创建自己的条件变量和锁。

4、解码线程恢复
音频解码线程和视频解码线程都阻塞在frame_queue_peek_writable()中,若消费线程的frame_queue_next()不发送信号的话,会一直阻塞,直到收到信号为止

1)视频播放线程调用frame_queue_next()-->SDL_CondSignal()发送信号后,frame_queue_peek_writable()-->SDL_CondWait()被唤醒继续向下运行,开始消费视频PacketQueue的数据。

2) 音频消费线程sdl_audio_callback()调用audio_decode_frame-->frame_queue_next()发送信号后唤醒audio_thread,开始消费音频PacketQueue继续解码。
5、读线程read_thread
音视频解码线程恢复工作后,packetQueue的数据正常消费了,读线程继续调用av_read_frame()读取AVPacket数据。
注:在读线程read_thread中,用SDL_CondWaitTimeout()超时返回的机制,相当于不断的检测,不像解码线程那样阻塞在SDL_CondWait()中。
6、总结
1) 恢复播放后,首先是更新is->frame_timer,也就是修改当前显示帧的开始播放时间,然后更新视频时钟的last_updated和pts_drift,并把paused标志位恢复成0。
2) paused标志位恢复后,当前帧根据时钟差计算显示时长,并根据修改后的is->frame_timer计算下一帧的播放时间。音频不再播放静音数据,开始播放解码后的数据,同时音频消费线程和视频消费线程通过frame_queue_next()唤醒解码线程。
3)音频和视频解码线程被消费线程唤醒后,继续工作解码数据。
4)read_thread线程继续用超时检测机制,从文件中读取AVPacket数据。

746

被折叠的 条评论
为什么被折叠?



