ffplay源码分析___暂停功能

前言

暂停功能是播放器的一个重要功能,分为触发暂停和恢复播放两部分。

一、暂停

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数据。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值