ffplay 音视频同步的源码分析

ffplay帧同步原理

每个音频/视频帧都有pts(presentation timestamp for the frame)的概念。这个pts可以理解为什么时间点去显示这一帧。无论是音频帧还是视频帧,这个pts都是递增的(随时间流逝,依次播放一帧一帧数据)。把每一帧的pts串起来,就成了一个时间轴,那么分别看待音频和视频,则有两个相互独立的时间轴。

音频同步的用于无非是要保证声音和画面一致,防止出现看电影时音不对嘴型的情况(看着就难受)。为了时间统一,要选择一个作为时间同步的标准。其它时间轴都要与该时间轴作为对比。本文以音频帧的时间轴作为基准时间(对应av_sync_type == AV_SYNC_AUDIO_MASTER)进行分析。当前视频帧时间点若晚于音频帧,则应当让视频帧立即显示或者延迟更少的时间进行显示;反之则应当延迟显示当前视频帧。

有两个核心过程需要先解释清楚:

  1. 视频帧播放时刻的估算(视频帧播放的理论时间点)。
  2. 同步音频时间轴:根据音频时间轴,对视频帧的估算时间点进行修正,得到真实时间点。

1、视频帧播放时刻的估算
代码中is->frame_timer体现了Video的真实播放的时间轴(相比上图中的都是根据pts画出的理论时间轴),播放第一帧时,该变量就赋值为当前系统时间戳,随后该变量则记录每个将要播放帧的真实时间,当系统真实时间超过is->frame_timer,当前帧应该要被播放。我们假定T1帧此时已经播放,T2帧正要播放。那么此时T1就等于is->frame_timer,接下来要根据is->frame_timer估算T2的时间点。

解码时每一帧都带有pts,T2帧的pts减去T1帧的pts就代表T2帧理论上应该播放的时间点。这个过程对应的代码last_duration = vp_duration(is, lastvp, vp);,这个last_duration就表示T2帧与T1帧的理论时间差,那么估算出T2的时间点就应该是T1+last_duration = is->frame_timer + last_duration。上述过程是单纯建立在Video时间轴上的,last_duration这个时间差还需要对比Audio时间轴进行修正。修正之后就能得出真实的时间点。

CSDN站内私信我,领取最新最全C++音视频学习提升资料,内容包括(C/C++Linux 服务器开发,FFmpeg webRTC rtmp hls rtsp ffplay srs

2、同步音频时间轴
获取到估算时间点之后,还需要对比Video和Audio的时间轴(都是pts描述的时间轴)。若Video播放比Audio快,则基于last_duration值再加大,加大后新的值为delay,那么修正后的is->frame_timer + delay就比is->frame_timer + last_duration值要大,也意味着当前帧要更晚播放。反之则减少last_duration值,减小后的新值为delay,那么当前帧就需要更早播放。

上面Video与Audio播放对比,是通过is->vidclk和is->audclk的pts进行对比。clk中的pts值更新:每播放一帧数据,对应的clk的pts就被更新为已经播放的帧的pts。对应的代码delay = compute_target_delay(last_duration, is);

上述整个过程对应的代码如下:

/* called to display each frame */
static void video_refresh(void *opaque, double *remaining_time)
{
	........
	
    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;
            
            .........
            
            //暂停/播放新视频等操作时,serial才会改变,此时视频时间轴要重新计算。
            if (lastvp->serial != vp->serial)
                is->frame_timer = av_gettime_relative() / 1000000.0;

            if (is->paused)
                goto display;

            /* compute nominal last_duration */
            last_duration = vp_duration(is, lastvp, vp); //计算上图中T2与T1的理论时间差。
            delay = compute_target_delay(last_duration, is); //对比Audio时间轴,对T2-T1进行修正。

            time= av_gettime_relative()/1000000.0; //当前真实时间值。
            if (time < is->frame_timer + delay) {
            	//当前帧的播放时间未到,设置睡眠时间。睡眠操作在该函数返回后执行。
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }

			//更新frame_timer值。
            is->frame_timer += delay;
            if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
                is->frame_timer = time;

			//更新Video clk的pts。
            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;
                }
            }
            
            ..........
}

Serial字段说明

我们先梳理下这个serial的关系。一切线从源头开始看起,源头就是这个packet queue的serial。

    static VideoState *stream_open(const char *filename, AVInputFormat *iformat)
    {
		........
		
        /* start video display */
        if (frame_queue_init(&is->pictq, &is->videoq, VIDEO_PICTURE_QUEUE_SIZE, 1) < 0)
            goto fail;
        if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)
            goto fail;
        if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)
            goto fail;
    
        if (packet_queue_init(&is->videoq) < 0 ||
            packet_queue_init(&is->audioq) < 0 ||
            packet_queue_init(&is->subtitleq) < 0)
            goto fail;
        
    	........
    	
        init_clock(&is->vidclk, &is->videoq.serial);
        init_clock(&is->audclk, &is->audioq.serial);
        init_clock(&is->extclk, &is->extclk.serial);
       	
       	........
    }

1、由frame_queue_init函数可知FrameQueue中的pktq指针分别指向对应的PacketQueue,那么代码中FrameQueue->pkgq->serial其实就是PacketQueue的serial。

2、由packet_queue_init函数可知,各个PacketQueue的serial初始值均为0.

3、由init_clock函数可知,各个Clock结构的queue_serial指针,指向的也是PacketQueue中的serial。

总结下,现在看到的结构中的serial其实都是对应的PacketQueue中的serial。除了上述结构外,Decoder结构和Frame结构体也包含serial字段,这些结构的serial字段的指向其实也是PacketQueue中的serial。
先看Decoder结构的

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
	........
	
	do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            if (d->packet_pending) {
                av_packet_move_ref(&pkt, &d->pkt);
                d->packet_pending = 0;
            } else {
            	//此处传递的是decoder->pkt_serial的地址。
                if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
                    return -1;
            }
        } while (d->queue->serial != d->pkt_serial);
	
	........
}

static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
{
	........
	
    for (;;) {
        if (q->abort_request) {
            ret = -1;
            break;
        }

        pkt1 = q->first_pkt;
        if (pkt1) {
            q->first_pkt = pkt1->next;
            if (!q->first_pkt)
                q->last_pkt = NULL;
            q->nb_packets--;
            q->size -= pkt1->pkt.size + sizeof(*pkt1);
            q->duration -= pkt1->pkt.duration;
            *pkt = pkt1->pkt;
            //此处对decoder->pkt_serial进行赋值
            if (serial)
                *serial = pkt1->serial;
            av_free(pkt1);
            ret = 1;
            break;
        }
        ........
    }
	........
}

Frame中的serial则是使用decoder的pkg_serial进行赋值。综上所述,serial的源头就是PacketQueue中的serial。那么这个源头是如何初始化并且改变的呢?

   static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
    {
    	........
        if (pkt == &flush_pkt)
            q->serial++;
        pkt1->serial = q->serial;
        ........
    }

从上面代码可知,仅当PacketQueue中含有flush_pkt时,才会导致serial递增,第一次播放时,各个PacketQueue中初始值便为flush_pkt,因此第一次执行上述代码时,q->serial = 1(执行了自加操作q->serial++),随后在暂停、或者切换播放另一个视频时,都会填充flush_pkt包,从而导致serial递增。

这个值主要用于干什么呢?我们继续看下面代码,当切换视频流之后,d->pkt_serial就会递增,此时在暂停前就已经进入PacketQueue的packet,会继续出队列,但由于此时老旧的包的d->queue->serial与d->pkt_serial不同,从而可以直接跳过这些老旧的包,不在进行解码。

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
	........

        do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            if (d->packet_pending) {
                av_packet_move_ref(&pkt, &d->pkt);
                d->packet_pending = 0;
            } else {
                if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
                    return -1;
            }
        } while (d->queue->serial != d->pkt_serial);

	........
}

猜你喜欢

转载自blog.csdn.net/m0_60259116/article/details/124989085