基本原理

音频和视频流内部有信息来控制播放的时机以及速度,音频内部有采样率,视频有帧率,表明每秒播放帧数,但是呢这两个数值很明显是不同的,比如音频一般是44100HZ,而fps一般标准是60,那么要想让音视频同步,就需要使音频流和视频流往同一个标准时间上靠近。这里我们选择音频流的时间作为标准。
如何往音频流时间靠,需要一些比较细的计算方式,一般采用PTS和DTS,也就是展示时间戳和解码时间戳(也就是每一帧对应的展示序号以及解码序号)。
视频的播放可以简单的简单的看成一帧帧的图片快速的呈现的过程,但是由于成本的原因我们需要对每一帧图片进行适当的压缩,现在视频的压缩根据帧分类有三种情况,I帧,P帧,B帧,I帧也可以叫做关键帧,一般在视频有明显转场的时候需要有一张关键帧作为这一节视频的起点,I帧的压缩率比较低,一般只有帧内压缩,也就是图片的压缩,P帧又叫做前向预测帧,可以看作是根据与I帧或者前一个P帧之间的差值压缩的帧,而B帧的压缩率更高,是根据前后帧来压缩的帧。具体的话有几种不同的策略。一般来说网络中传递的帧的排列方式是按照DTS也就是解码时间戳排序的,但是由于压缩策略导致视频帧的传输需要先将B帧所依赖的位于B帧之后展示的帧先传输,所以会造成展示时间戳与解码时间戳不一致。举个简单的例子:
Stream顺序: I P B B
DTS: 1 2 3 4
PTS: 1 4 2 3 //先展示I帧,然后展示两个B帧,最后才展示P帧
所以为了正确展示视频,我们需要的就是最新解码好的原始帧的 PTS。啰嗦那么多,其实实现上解码完成以后使用pFrame->best_effort_timestamp这个属性就完事了。FFmpeg都帮我们封装好了。
音频流由于不存在B帧,所以它的DTS和PTS是一致的。

具体同步流程

音频时钟

既然是往音频时间靠,那么首先就需要添加一个音频时钟audio_clock,可以把这个状态放到VideoState结构体中,毕竟基准时钟也是当前音视频播放状态的一个属性。
在音频解码的函数中就可以计算当前音频的播放时钟:

if (packet_queue_get(&is->audioq, thisPkt, 1) < 0) {
     return -1;
} else {
     avcodec_send_packet(aCodecCtx, thisPkt);
}

     is->audio_pkt_data = thisPkt->data;
     is->audio_pkt_size = thisPkt->size;
     if (thisPkt->pts != AV_NOPTS_VALUE) {
        is->audio_clock = av_q2d(is->audio_st->time_base) * thisPkt->pts;
}

解释一下:is->audio_st->time_base指的就是sample_rate的倒数,也可以认为是两个音频帧之间的时间间隔,av_q2d(is->audio_st->time_base) 就是把这个ffmpeg中的时间间隔表示成double值,由于音频的压缩帧中不存在什么B帧所以他的pts和dts是一个值,因此可以将这个时候的时钟设置为 is->audio_clock = av_q2d(is->audio_st->time_base) * thisPkt->pts;
由于音频压缩帧中可能解压出多个AVFrame,所以对于每一个解压帧,需要根据具体数值调整它对应的音频时钟:这里其实也只是一个估算,把 每一帧的数据/(采样率*2个字节)=此帧所需要的播放时间 作为调整的值,实际上由于多线程的原因肯定也会浪费一点点时间的。

while (0 == avcodec_receive_frame(aCodecCtx, &frame)) {
      uint8_t *audio_buf = is->audio_buf;
      int out_samples = swr_convert(resample_ctx, &audio_buf, frame.nb_samples, (const uint8_t **)frame.data, frame.nb_samples);
      if(out_samples > 0){
           resampled_data_size =  av_samples_get_buffer_size(NULL,output_channels ,out_samples, output_sample_fmt, 1);//out_samples*output_channels*av_get_bytes_per_sample(output_sample_fmt);
      } else {
           return -1;
      }
      swr_free(&resample_ctx);
            
      int n = 2 * output_channels;
      is->audio_clock += (double) resampled_data_size / (double) (n * output_rate);
      return resampled_data_size;
}

最后还有一个需要注意的点,将视频时钟往音频时钟靠,需要获取音频时钟,但是获取音频时钟的时候,当前音频缓冲区还有没有播放完的数据,所以还需要减去缓冲区中的数据的播放时间。

double get_audio_clock(VideoState *is) {
    double pts;
    int hw_buf_size, bytes_per_sec, n;
    
    pts = is->audio_clock;
    hw_buf_size = is->audio_buf_size - is->audio_buf_index;
    bytes_per_sec = 0;
 
    n = is->audio_st->codec->channels * 2;//2是指量化精度,一般是16bit = 2 B;
 
    if (is->audio_st) {
        bytes_per_sec = is->audio_st->codec->sample_rate * n;
    }
    if (bytes_per_sec) {
        pts -= (double) hw_buf_size / bytes_per_sec;
    }
    return pts;
}
视频时钟

往VideoState结构体中增加video_clock时钟

avcodec_decode_video2(is->video_st->codec, pFrame, &frameFinished, packet);
if (pFrame->best_effort_timestamp == AV_NOPTS_VALUE ){
       pts = 0;
} else {
       pts = pFrame->best_effort_timestamp;
}
       pts *= av_q2d(is->video_st->time_base);
        
// Did we get a video frame?
if (frameFinished) {	
		pts = synchronize_video(is, pFrame, pts);
 		if (queue_picture(is, pFrame,pts) < 0){
                break;
        }
}
av_packet_unref(packet);

double synchronize_video(VideoState* is, AVFrame *src_frame, double pts) {
    double frame_delay;
    if (pts!=0){
        is->video_clock = pts;
    } else {
        pts = is->video_clock;
    }
    
    frame_delay = av_q2d(is->video_st->codec->time_base);
    frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
    //这个地方实际上是因为有些视频帧需要重复,在对应属性中注释了/**
     * When decoding, this signals how much the picture must be delayed.
     * extra_delay = repeat_pict / (2*fps)
     */
    int repeat_pict;
    is->video_clock += frame_delay;
    return pts;
}

获取当前视频帧的PTS(best_effort_timestamp)然后乘上av_q2d(is->video_st->time_base)得到视频时钟。

音视频时钟同步

有了video_clock和audio_clock我们的目标就比较明确了,把video_clock尽可能的往audio_clock上靠,如果当前的video_clock>audio_clock说明我们需要调慢视频的播放速度,否则就应该加快视频的播放速度了,这里定一个策略,如果视频播放速度慢了,就立即播放下一帧视频,如果快了就推迟下一帧视频的播放。

void video_refresh_timer(void *userdata) {

    VideoState *is = (VideoState *)userdata;
    VideoPicture *vp;
    
    double actual_delay,delay,sync_threshold,ref_clock,diff;

    if (is->video_st) {
        if (is->pictq_size == 0) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.001 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                video_refresh_timer(is);
            });
        } else {
            vp = &is->pictq[is->pictq_rindex];
            //这里的vp->pts以及is->frame_last_pts都是指具体的展示时间而不是时间戳了
            delay = vp->pts - is->frame_last_pts;
            if (delay <= 0 || delay >= 1.0) {
                delay = is->frame_last_delay;
            }
          //更新last_delay以及last_pts
            is->frame_last_delay = delay;
            is->frame_last_pts = vp->pts;
            
            //获取音频时钟
            ref_clock = get_audio_clock(is);
            diff = vp->pts - ref_clock;//获取差值
            
            sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
            if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
                if (diff <= -sync_threshold) {
                    delay = 0;
                } else if (diff >= sync_threshold) {
                    delay = 2 * delay;
                }
            }
            is->frame_timer += delay;//is->frame_timer累加delay值获取实际帧的播放时间
            actual_delay = is->frame_timer - (av_gettime() / 1000000.0);//获取主runloop的时间差值,让后续dispatch_after的时间相对准确
            if (actual_delay < 0.010) {
                actual_delay = 0.010;
            }
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(actual_delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                video_refresh_timer(is);
            });
 
            video_display(is);
 
            if (++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
                is->pictq_rindex = 0;
            }
            [is->pictq_cond lock];
            is->pictq_size--;
            [is->pictq_cond signal];
            [is->pictq_cond unlock];
        }
    } else {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            video_refresh_timer(is);
        });
    }
}

以上就是同步音视频的核心代码,详细代码参照
音视频同步


如果觉得我的文章对你有帮助,希望能点个赞,您的“点赞”将是我最大的写作动力,如果觉得有什么问题也可以直接指出。