ffplay播放控制代码分析

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

* 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如何实现各种操作,对播放器有了更全面的理解了。


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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ITRonnie

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值