FFmpeg视频播放(音视频同步)

1,804 阅读2分钟

继上一讲音视频解码,接下来分析音视频同步,可以参照 FFmpeg源码中FFplay中的音视频同步的具体代码。

音视频同步方式:

  1. 将视频根据音频同步(以音频为主)
  2. 以视频为主
  3. 以一个外部时间进度为主

这里采用第一种方式完成音视频同步。

获取一个音频frame的播放时间,通过 frame->pts拿到 pts用来度量音频帧什么时候被播放出来。time_base是pts转成秒的单位,获取time_base, 传给VideoChannel,AudioChannel。

 //每个画面 刷新的间隔 单位:秒
 double frame_delays = 1.0 / fps;
 //单位
 AVRational time_base = stream->time_base;
        
//在getPCM()中拿到播放 一个音声frame的时间

//获取一个frame的一个相对播放时间
//获得播放这段数据的秒速(时间机), clock的时间单位是秒
double clock =  frame->pts * av_q2d(time_base);

//视频通过best_effort_timestamp,而不是pts
//获取当前 一个画面播放的时间
double clock = frame->best_effort_timestamp * av_q2d(time_base);

//拿到两个的时间戳后,需要两者的clock进行比较,比较的场所在Video中需要把AudioChannel传入到videoChannel中。

//比较音频与视频
double audioClock = audioChannel->clock;
//间隔 音视频相差的间隔
double diff = clock - audioClock;
if (diff > 0) {
  //大于0 表示视频比较快
  //LOGE("视频快了:%lf", diff);
  av_usleep((delays + diff) * 1000000);
} else if (diff < 0) {
  //不睡了,快点赶上音频
  //LOGE("音频快了:%lf",diff);
  // 视频包积压的太多了 (丢包)
  if (fabs(diff) >= 0.05) {
    releaseAvFrame(&frame);
    //丢包
    frames.sync();
    continue;//每次丢一帧,循环丢,重新计算延迟时间。
  }else{
    //不睡了 快点赶上 音频
  }
}

frame本身自己有个延迟时间

/**
 * When decoding, this signals how much the picture must be delayed.
 * extra_delay = repeat_pict / (2*fps)
*/

int repeat_pict;

double extra_delay = frame->repeat_pict / (2 * fps);
double delays = extra_delay + frame_delays;

//所以真正需要延迟的是 delays 这个时间。

当视频落下太多的时候,需要丢包。Video 中有packets跟Frames两个queue,所以这里可以选择丢弃packets或者Frames。 根据H.264原则,Frame分为 I,B, P帧,这里不能丢弃I帧,因为与之相关的B帧,P帧没法获取其相关联的数据。

//丢包, 因为其他地方需要去拿,所以要同步操作,加锁
frames.sync();

void sync() {
  pthread_mutex_lock(&mutex);
  //同步代码
  syncHandle(q);
  pthread_mutex_unlock(&mutex);
}

void setSyncHandle(SyncHandle s) {
   syncHandle = s;
}

VideoChannel::VideoChannel(int id, AVCodecContext *avCodecContext, AVRational time_base, int fps)
        : BaseChannel(id, avCodecContext, time_base) {
    this->fps = fps;
    //用于同步操作队列的一个函数指针
    //packets.setSyncHandle(dropAvPacket);
    //丢画面
    frames.setSyncHandle(dropAvFramev);
}


//丢包,知道下一个关键帧
void dropAvPacket(queue<AVPacket *> &q) {
    while (!q.empty()) {
        AVPacket *packet = q.front();
        if (packet->flags != AV_PKT_FLAG_KEY) {//还没有解码,需要判断它不是I帧,否则会影响后续的B帧,P帧的解码过程
            BaseChannel::releaseAvPacket(&packet);
            q.pop();
        } else {
            break;
        }
    }
}

//丢帧
void dropAvFramev(queue<AVFrame *> &q) {
    if (!q.empty()) {
        AVFrame *frame = q.front();
        BaseChannel::releaseAvFrame(&frame);
        q.pop();
    }
}

至此音视频同步的整个过程也讲完了。需要源码的,可前往GitHub获取, 顺便点个star呗