ffplay播放控制代码分析

原文地址:https://blog.csdn.net/lrzkd/article/details/78841677

*****************************************************************************

* ffplay系列博客: *

* ffplay播放器原理剖析 *

* ffplay播放器音视频同步原理 *

* ffplay播放控制代码分析 *

* 视频主观质量对比工具(Visual comparision tool based on ffplay) *

*****************************************************************************

ffplay可以响应各种控制请求,比如快进快退,暂停,单帧播放等,具体是如何做的呢?

ffplay通过获取键盘和鼠标事情来执行各种控制逻辑,函数调用关系如下:

main() -> event_loop() --> refresh_loop_wait_event(cur_stream, &event)

|–> case key_p or SPACE: toggle_pause()

|–> case key_s: step_to_next_frame()

|–> case key_left, right, up, down: stream_seek()

main()函数完成初始化工作后,进入了事件队列的循环,循环中首先调用refresh_loop_wait_event(),该函数先获取事件队列中的事件,没有任何事件输入时,则进行video frame的渲染。如果有事件输入,则响应。

暂停

当收到按下键盘p或者空格键的事件时,调用 toggle_pause(),进入暂停或者恢复播放。

先看下时钟的定义,其中pts表示video或者audio的时间戳,关键要理解pts_drift:pts_drift计算公式为pts - time,其中time为 真实时间, 所以pts_drift代表了时间戳与真实时间的差别。当视频正常播放时,时间戳pts和真实时间time都是按相同速度在走的,所以pts_drift保持恒定;如果暂停,那时间戳pts就停止更新了,但是真实时间不会因为暂停操作就不停止,所以pts_drift就会变小。pts_drift + 当前time 可以得到当前正在播放的video的时间戳。

typedef struct Clock {
    double pts;           /* clock base */
    double pts_drift;     /* clock base minus time at which we updated the clock */
    double last_updated;
    double speed;
    int serial;           /* clock is based on a packet with this serial */
    int paused;
    int *queue_serial;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

pause的相关代码和注释如下:

 static void toggle_pause(VideoState *is)
    {
        stream_toggle_pause(is);
        is->step = 0;
    }
    
    static void stream_toggle_pause(VideoState *is)
    {
        if (is->paused) { //如果当前状态就是暂停,则接下来进入播放状态,需要更新vidclk
            is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated;
            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; // pause状态反转
    }
    
    static double get_clock(Clock *c)
    {
        if (*c->queue_serial != c->serial)
            return NAN;
        if (c->paused) { //如果当前是暂停状态,则返回最新的pts即可,因为暂停时时间没走
            return c->pts;
        } else { // 如果当前正处在播放状态,则返回的时间为最新的pts + 更新pts之后流逝的时间
            double time = av_gettime_relative() / 1000000.0;
            //这里返回的时间实际为 c->pts + time - c->last_updated		
            return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed); //这里正常速度播放,speed=1.0
        }
    }
    
    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;
    }

分析从播放进入暂停状态播放器的逻辑:stream_toggle_pause其实就是设置了标志位而已VideoState->paused,设置了paused状态后,对read_thread, video_thread, audio_thread, 渲染线程,音频回调函数sdl_audio_callback 都会产生影响。

对read_thread的影响:由于解码线程不从packet queue中拿packet了,所以packet queue中已经有足够的packets,read_thread不再读取新的包。暂停状态下的read_thread如下:

// 暂停状态下的read_thread
static int read_thread(void *arg)
{
    ......
    for (;;) {
        ......
        /* if the queue are full, no need to read more */
        // 暂停后,这个if条件满足,packet queue中有足够的包,将不再继续读包了
        if (infinite_buffer<1 &&
              (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
            || (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
                stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
                stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
            /* wait 10 ms */
            SDL_LockMutex(wait_mutex);
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
            SDL_UnlockMutex(wait_mutex);
            continue;
        }	
        ......
        ret = av_read_frame(ic, pkt);
        ......
    }
}

对sdl_audio_callbakc的影响:sdl_audio_callback不再从audio sample queue中拿解码后的音频数据了,而是直接播放静音。

 // 暂停状态下的audio_decode_frame
    static int audio_decode_frame(VideoState *is)
    {
        int data_size, resampled_data_size;
        int64_t dec_channel_layout;
        av_unused double audio_clock0;
        int wanted_nb_samples;
        Frame *af;
    
        if (is->paused) // pause状态下直接返回-1,不从audio sample queue拿audio sample了
            return -1;
        ......
    }
    // 暂停状态下的sdl_audio_callback
    static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
    {
        ......
    	while (len > 0) {
            if (is->audio_buf_index >= is->audio_buf_size) {
               audio_size = audio_decode_frame(is); //此时函数返回-1
               if (audio_size < 0) { // 直接输入静音
                    /* if error, just output silence */
                   is->audio_buf = NULL;
                   is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
               } else {
                   if (is->show_mode != SHOW_MODE_VIDEO)
                       update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
                   is->audio_buf_size = audio_size;
               }
               is->audio_buf_index = 0;
            }
            len1 = is->audio_buf_size - is->audio_buf_index;
            if (len1 > len)
                len1 = len;
            if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
                memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
            else { //暂停状态下直接往stream中填充0,静音
                memset(stream, 0, len1);
                if (!is->muted && is->audio_buf)
                    SDL_MixAudio(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1, is->audio_volume);
            }
            len -= len1;
            stream += len1;
            is->audio_buf_index += len1;
        }
        ......
    }

对解码线程video_thread和audio_thread的影响:video_thread和audio_thread解码需要往frame queue或者sample queue中写解码后的数据,暂停后frame queue和sample queue已经满了,所以video_thread和audio_thread就会等待,直到frame queue或sample queue中有空余。

 static Frame *frame_queue_peek_writable(FrameQueue *f)
    {
        /* wait until we have space to put a new frame */
        SDL_LockMutex(f->mutex);
        while (f->size >= f->max_size &&
               !f->pktq->abort_request) {
            printf("frame queue peek writable: f->size %d >= f->max_size %d\n", f->size, f->max_size);
            SDL_CondWait(f->cond, f->mutex);
        }
        SDL_UnlockMutex(f->mutex);
    
        if (f->pktq->abort_request)
            return NULL;
    
        return &f->queue[f->windex];
    }

video渲染也会停止:

    static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
        double remaining_time = 0.0;
        SDL_PumpEvents();
        while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_ALLEVENTS)) {
            if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
                SDL_ShowCursor(0);
                cursor_hidden = 1;
            }
            if (remaining_time > 0.0)
                av_usleep((int64_t)(remaining_time * 1000000.0));
            remaining_time = REFRESH_RATE;
            if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh)) // pause状态不再刷新video frame
                video_refresh(is, &remaining_time);
            SDL_PumpEvents();
        }
    }

快进快退

seek操作只影响read_thread线程,先seek到指定位置,然后将packet queue清空,接着从新的位置开始读取packet,这样就实现了seek操作。

   /* seek in the stream */
    static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
    {
        if (!is->seek_req) {
            is->seek_pos = pos;
            is->seek_rel = rel;
            is->seek_flags &= ~AVSEEK_FLAG_BYTE;
            if (seek_by_bytes)
                is->seek_flags |= AVSEEK_FLAG_BYTE;
            is->seek_req = 1; // 设置seek request = 1
            SDL_CondSignal(is->continue_read_thread);
        }
    }

seek对read\_thread的影响:

    static int read_thread(void *arg)
    {
        VideoState *is = arg;
        ......
        for (;;) {
            ......
            if (is->seek_req) {  //执行seek请求
                int64_t seek_target = is->seek_pos;
                int64_t seek_min    = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
                int64_t seek_max    = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
    
                ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
                if (ret < 0) {
                    av_log(NULL, AV_LOG_ERROR,
                           "%s: error while seeking\n", is->ic->filename);
                } else {
                    if (is->audio_stream >= 0) {
                        packet_queue_flush(&is->audioq); // 将audio packet queue清空
                        packet_queue_put(&is->audioq, &flush_pkt);
                    }
                    if (is->subtitle_stream >= 0) {
                        packet_queue_flush(&is->subtitleq);
                        packet_queue_put(&is->subtitleq, &flush_pkt);
                    }
                    if (is->video_stream >= 0) {
                        packet_queue_flush(&is->videoq); // 将video packet queue清空
                        packet_queue_put(&is->videoq, &flush_pkt);
                    }
                    if (is->seek_flags & AVSEEK_FLAG_BYTE) {
                       set_clock(&is->extclk, NAN, 0);
                    } else {
                       set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
                    }
                }
                is->seek_req = 0;
                is->queue_attachments_req = 1;
                is->eof = 0;
                if (is->paused)
                    step_to_next_frame(is);
            }
            ......
            ret = av_read_frame(ic, pkt);  // 从源文件中读取内容到pkt结构中
            /* check if packet is in play range specified by user, then queue, otherwise discard */
            stream_start_time = ic->streams[pkt->stream_index]->start_time;
            // 下面的duration是通过命令传递给ffplay的指定播放时长的参数,所以判断pkt的时间戳是否在duration内
            pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
            pkt_in_play_range = duration == AV_NOPTS_VALUE ||
                    (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
                    av_q2d(ic->streams[pkt->stream_index]->time_base) -
                    (double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
                    <= ((double)duration / 1000000);
            if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
                packet_queue_put(&is->audioq, pkt);      // 读到的pkt为audio,放入audio queue(is->audioq)
            } else if (pkt->stream_index == is->video_stream && pkt_in_play_range
                       && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
                packet_queue_put(&is->videoq, pkt);      // 读到的pkt为video,放入video queue(is->videoq)
            } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
                packet_queue_put(&is->subtitleq, pkt);   // 读到的pkt为subtitle,放到subtitile queue中
            } else {
                av_packet_unref(pkt);
            }
        }
        ......
    }
    

单帧播放

单帧播放的原理为:在视频的渲染线程中,在显示一帧的时候,同时将播放器置为pause状态。这样播放器的表现为视频走了一帧之后播放器进入了暂停状态。具体来说,调用了step_to_next_frame()函数设置标志位,在渲染函数中,渲染一帧视频,同时播放器进入暂停状态。代码如下:

   static void step_to_next_frame(VideoState *is)
    {
        /* if the stream is paused unpause it, then step */
        if (is->paused) // 如果本来是pause状态,则先进入播放状态
            stream_toggle_pause(is);
        is->step = 1; //设置标志位
    }

  

    /* called to display each frame */
    static void video_refresh(void *opaque, double *remaining_time)
    {
        VideoState *is = opaque;
        double time;
    
        Frame *sp, *sp2;
    
        if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
            check_external_clock_speed(is);
    
        ......
    
        if (is->video_st) {
    retry:
            if (frame_queue_nb_remaining(&is->pictq) == 0) {
                // nothing to do, no picture to display in the queue
            } else {
                double last_duration, duration, delay;
                Frame *vp, *lastvp;
    
                /* dequeue the picture */
                lastvp = frame_queue_peek_last(&is->pictq);   //取Video Frame Queue上一帧图像
                vp = frame_queue_peek(&is->pictq);            //取Video Frame Queue当前帧图像
    
                ......
    
                if (is->paused)
                    goto display;
    
                /* compute nominal last_duration */
                last_duration = vp_duration(is, lastvp, vp);     //计算两帧之间的时间间隔
                delay = compute_target_delay(last_duration, is); //计算当前帧与上一帧渲染的时间差
    
                time= av_gettime_relative()/1000000.0;
                //is->frame_timer + delay是当前帧渲染的时刻,如果当前时间还没到帧渲染的时刻,那就要sleep了
                if (time < is->frame_timer + delay) { // remaining_time为需要sleep的时间
                    *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time); 
                    goto display;
                }
    
                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);
                SDL_UnlockMutex(is->pictq.mutex);
    
                if (frame_queue_nb_remaining(&is->pictq) > 1) {
                    Frame *nextvp = frame_queue_peek_next(&is->pictq);
                    duration = vp_duration(is, vp, nextvp);
                    // 如果当前帧显示时刻早于实际时刻,说明解码慢了,帧到的晚了,需要丢弃不能用于显示了,不然音视频不同步了。
                    if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
                        is->frame_drops_late++;
                        frame_queue_next(&is->pictq);
                        goto retry;
                    }
                }
    
                ......
                frame_queue_next(&is->pictq); 
                is->force_refresh = 1;        //显示当前帧
    
                if (is->step && !is->paused)  //如果当前是单帧播放模式,渲染当前帧之后,马上进入暂停状态,且is->step置为0
                    stream_toggle_pause(is);
            }
    display:
            /* display picture */
            if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
                video_display(is);
        }
        is->force_refresh = 0;
        ......
    }

结语

经过上面的分析,可以了解ffplay如何实现各种操作,对播放器有了更全面的理解了。

版权声明:本文为博主原创文章,未经博主允许请勿转载。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值