ffplay播放控制代码分析

来源:互联网 发布:飞鱼网络直播 编辑:程序博客网 时间:2024/06/05 02:29

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

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


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

原创粉丝点击