关于ffmpeg:ffplay音视频同步

7次阅读

共计 9281 个字符,预计需要花费 24 分钟才能阅读完成。

前言

对于音视频同步是有三种计划的,一种是以内部时钟为基准,音频时钟和视频时钟在播放时都以内部时钟为参考系,谁快了就期待,慢了就丢帧;第二种是以视频时钟为基准,
音频时钟在播放的过程中参考视频时钟;第三种是以音频时钟为基准,视频时钟在播放的过程中参考音频时钟。

因为人体器官对视觉的敏感读没有听觉的灵敏度高,因而为了更好的体验,在音视频同步时个别都是以音频时钟为基准的计划。那是不是说其余两种计划没有用途呢?也不是的,比如说
一个只有视频没有音频的的视频文件,在播放的时候就须要以视频为基准了。

明天介绍的音视频同步计划也是最广泛的视频同步音频的计划。

Clock 时钟

咱们再来看看 Clock 这个构造体:

// 时钟 / 同步时钟
typedef struct Clock {
    double pts;       // 以后正在播放的帧的 pts    /* clock base */
    double pts_drift;   // 以后的 pts 与零碎工夫的差值  放弃设置 pts 时候的差值,前面就能够利用这个差值推算下一个 pts 播放的工夫点
    double last_updated; // 最初一次更新时钟的工夫,应该是一个零碎工夫吧?double speed;  // 播放速度管制
    int serial;     // 播放序列      /* clock is based on a packet with this serial */
    int paused;  // 是否暂停
    int *queue_serial;   // 队列的播放序列 PacketQueue 中的 serial /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

再联合对于时钟的几个函数看看:

// 次要由 set_clock 调用
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;
}

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 double get_clock(Clock *c)
{
    // 如果时钟的播放序列与待解码包队列的序列不始终了,返回 NAN,必定就是不同步或者须要丢帧了
    if (*c->queue_serial != c->serial)
        return NAN;
    if (c->paused) {
        // 暂停状态则返回原来的 pts
        return c->pts;
    } else {double time = av_gettime_relative() / 1000000.0;
        // speed 能够先疏忽播放速度管制
        // 如果是 1 倍播放速度,c->pts_drift + time
        return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
    }
}

音频和视频每次在播放新的一帧数据时都会调用函数 set_clock 更新音频时钟或视频时钟。通过函数 set_clock_at 咱们发现,就是更新了 Clock 构造体的四个变量。
其中 pts_drift 是以后帧的 pts 与零碎工夫的差值,有了这个差值在将来的某一刻就可能很不便地算出以后帧对于的时钟点。

大抵原理如图:

视频同步音频

视频同步到音频的根本办法是:如果视频超前音频,持续显示上一帧,以期待音频;如果视频落后音频,则显示下一帧,以追赶音频。

对于视频同步解决在 ffplay 有两处中央,一是在函数 get_video_frame 做了简略的丢帧解决,二是在函数 video_refresh 显示管制是做的同步解决。

对于函数 get_video_frame 丢帧解决的次要逻辑如下:

  // 同步时钟不以视频为基准时
        if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {if (frame->pts != AV_NOPTS_VALUE) {// 实践上如果须要间断接上播放的话  dpts + diff = get_master_clock(is)
                // 所以能够算出 diff  留神绝对值
                double diff = dpts - get_master_clock(is);
                if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD &&
                    diff - is->frame_last_filter_delay < 0 &&
                    is->viddec.pkt_serial == is->vidclk.serial &&
                    is->videoq.nb_packets) {
                    is->frame_drops_early++;
                    av_frame_unref(frame);
                    got_picture = 0;
                }
            }
        }

就是解码进去的帧曾经来不及显示了,间接抛弃。

在视频播放线程中,视频播放函数 video_refresh 实现了视频显示和同步控制,这个函数的调用过程如下:

main() –>
event_loop() –>
refresh_loop_wait_event() –>
video_refresh()

其中函数 video_refresh 具体如下:

/**
 * 显示视频
 * @param opaque
 * @param remaining_time
 */
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 (!display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {time = av_gettime_relative() / 1000000.0;
        if (is->force_refresh || is->last_vis_time + rdftspeed < time) {video_display(is);
            is->last_vis_time = time;
        }
        *remaining_time = FFMIN(*remaining_time, is->last_vis_time + rdftspeed - 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;

            /* dequeue the picture */
            // 正在显示的帧
            lastvp = frame_queue_peek_last(&is->pictq);
            // 将要显示的帧
            vp = frame_queue_peek(&is->pictq);

            if (vp->serial != is->videoq.serial) {
                // 不在同一个播放序列了,抛弃
                frame_queue_next(&is->pictq);
                goto retry;
            }

            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);
            // 上一帧通过校对后理论须要显示多长
            delay = compute_target_delay(last_duration, is);

            time= av_gettime_relative()/1000000.0;
            if (time < is->frame_timer + delay) {
                // 还没达到下一帧的显示工夫,持续显示上一帧
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }

            // 显示下一帧了
            // 更新播放工夫,与下面 time < is->frame_timer + delay 判断条件对应???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;
                }
            }

            if (is->subtitle_st) {while (frame_queue_nb_remaining(&is->subpq) > 0) {sp = frame_queue_peek(&is->subpq);

                    if (frame_queue_nb_remaining(&is->subpq) > 1)
                        sp2 = frame_queue_peek_next(&is->subpq);
                    else
                        sp2 = NULL;

                    if (sp->serial != is->subtitleq.serial
                            || (is->vidclk.pts > (sp->pts + ((float) sp->sub.end_display_time / 1000)))
                            || (sp2 && is->vidclk.pts > (sp2->pts + ((float) sp2->sub.start_display_time / 1000))))
                    {if (sp->uploaded) {
                            int i;
                            for (i = 0; i < sp->sub.num_rects; i++) {AVSubtitleRect *sub_rect = sp->sub.rects[i];
                                uint8_t *pixels;
                                int pitch, j;

                                if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)&pixels, &pitch)) {for (j = 0; j < sub_rect->h; j++, pixels += pitch)
                                        memset(pixels, 0, sub_rect->w << 2);
                                    SDL_UnlockTexture(is->sub_texture);
                                }
                            }
                        }
                        frame_queue_next(&is->subpq);
                    } else {break;}
                }
            }

            frame_queue_next(&is->pictq);
            is->force_refresh = 1;

            if (is->step && !is->paused)
                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;
    if (show_status) {
        AVBPrint buf;
        static int64_t last_time;
        int64_t cur_time;
        int aqsize, vqsize, sqsize;
        double av_diff;

        cur_time = av_gettime_relative();
        if (!last_time || (cur_time - last_time) >= 30000) {
            aqsize = 0;
            vqsize = 0;
            sqsize = 0;
            if (is->audio_st)
                aqsize = is->audioq.size;
            if (is->video_st)
                vqsize = is->videoq.size;
            if (is->subtitle_st)
                sqsize = is->subtitleq.size;
            av_diff = 0;
            if (is->audio_st && is->video_st)
                av_diff = get_clock(&is->audclk) - get_clock(&is->vidclk);
            else if (is->video_st)
                av_diff = get_master_clock(is) - get_clock(&is->vidclk);
            else if (is->audio_st)
                av_diff = get_master_clock(is) - get_clock(&is->audclk);

            av_bprint_init(&buf, 0, AV_BPRINT_SIZE_AUTOMATIC);
            av_bprintf(&buf,
                      "%7.2f %s:%7.3f fd=%4d aq=%5dKB vq=%5dKB sq=%5dB f=%"PRId64"/%"PRId64"\r",
                      get_master_clock(is),
                      (is->audio_st && is->video_st) ? "A-V" : (is->video_st ? "M-V" : (is->audio_st ? "M-A" : " ")),
                      av_diff,
                      is->frame_drops_early + is->frame_drops_late,
                      aqsize / 1024,
                      vqsize / 1024,
                      sqsize,
                      is->video_st ? is->viddec.avctx->pts_correction_num_faulty_dts : 0,
                      is->video_st ? is->viddec.avctx->pts_correction_num_faulty_pts : 0);

            if (show_status == 1 && AV_LOG_INFO > av_log_get_level())
                fprintf(stderr, "%s", buf.str);
            else
                av_log(NULL, AV_LOG_INFO, "%s", buf.str);

            fflush(stderr);
            av_bprint_finalize(&buf, NULL);

            last_time = cur_time;
        }
    }
}

这个函数的外围逻辑是:

1、获取正在播放的帧与下一帧,如果播放序列变了则重试,通过两帧计算出正在播放的帧现实状况下应该播放多久

   // 没有可读取的帧
        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);
            // 将要显示的帧
            vp = frame_queue_peek(&is->pictq);

            if (vp->serial != is->videoq.serial) {
                // 不在同一个播放序列了,抛弃
                frame_queue_next(&is->pictq);
                goto retry;
            }

            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);

函数 vp_duration 就是通过两帧的 pts 差值计算:

static double vp_duration(VideoState *is, Frame *vp, Frame *nextvp) {if (vp->serial == nextvp->serial) {
        double duration = nextvp->pts - vp->pts;
        if (isnan(duration) || duration <= 0 || duration > is->max_frame_duration)
            return vp->duration;
        else
            return duration;
    } else {return 0.0;}
}

2、通过函数 compute_target_delay 算出以后播放帧真正的播放工夫,外部做了工夫弥补,能够说这是音视频同步的外围


static double compute_target_delay(double delay, VideoState *is)
{
    double sync_threshold, diff = 0;

    /* update delay to follow master synchronisation source */
    if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
        /* if video is slave, we try to correct big delays by
           duplicating or deleting a frame */
        // 音频时钟和视频时钟的差距
        // 如果是以音频时钟为基准,那么 get_master_clock 拿到的就是音频时钟的 pts
        diff = get_clock(&is->vidclk) - get_master_clock(is);

        /* skip or repeat frame. We take into account the
           delay to compute the threshold. I still don't know
           if it is the best guess */

        sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
        // 须要做同步调整
        if (!isnan(diff) && fabs(diff) < is->max_frame_duration) {if (diff <= -sync_threshold)
                // 视频落后了,并且超过了同步阈值
                delay = FFMAX(0, delay + diff);
            else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
                // 视频超前了,切超过了同步阈值
                delay = delay + diff;
            else if (diff >= sync_threshold)
                // 视频超前了
                delay = 2 * delay;
        }
    }

    av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
            delay, -diff);

    return delay;
}

联合下面的 Clock 时钟的原理图,看懂这段代码应该不难,如果看不懂,可能就是下面对于 Clock 时钟的概念原理每说分明了。

3、通过零碎以后工夫与上一帧的播放工夫比照,看比照后果是持续显示以后帧呢还是更新显示下一帧:


            time= av_gettime_relative()/1000000.0;
            if (time < is->frame_timer + delay) {
                // 还没达到下一帧的显示工夫,持续显示上一帧
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }

            // 显示下一帧了
            // 更新播放工夫,与下面 time < is->frame_timer + delay 判断条件对应???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);

能够看到如果是到了显示下一帧的工夫,会调用函数 update_video_pts 更新视频时钟,用于下一次音视频同步。

4、frame_queue_next 更新读索引

好了,对于音视频同步的就介绍到这里,其实音视频同步说难不难,次要还是要了解好时钟的概念就相当于买通了任督二脉,就晓得怎么做的了。

举荐浏览

FFmpeg 连载 1 - 开发环境搭建
FFmpeg 连载 2 - 拆散视频和音频
FFmpeg 连载 3 - 视频解码
FFmpeg 连载 4 - 音频解码
FFmpeg 连载 5 - 音视频编码
FFmpeg 连载 6 - 音频重采样
FFmpeg 连载 8 - 视频合并以及替换视频背景音乐实战
ffplay 调试环境搭建
ffplay 整体框架
ffplay 数据读取线程
ffplay 音视频解码线程

关注我,一起提高,人生不止 coding!!!

正文完
 0