ffplay播放器剖析(6)----音视频同步分析

news2024/11/25 10:31:29

文章目录

  • 1. 音视频同步基础
    • 1.1 音视频同步策略
    • 1.2 音视频同步概念
    • 1.3 FFmpeg中的时间单位
    • 1.4 不同结构体的time_base/duration分析
    • 1.5 不同结构体的pts/dts分析
    • 1.6 ffplay中Frame结构体分析
    • 1.7 Vidoe Frame PTS获取及矫正
    • 1.8 Audio Frame PTS的获取
  • 2.以音频为基准
  • 3.以视频为基准
    • 3.1 音频主流程
  • 4.以外部时钟为基准
  • 总结

1. 音视频同步基础

视频和音频是不同的线程,并且也不会同时解出同一个pts的音视频帧,因此需要音视频同步;

1.1 音视频同步策略

  1. 以音频为基准
    • 视频慢了则丢掉部分视频帧(视觉感受就是掉帧)
    • 视频快了就继续渲染上一帧
  2. 以视频为基准
    • 音频慢了则加速播放(或者丢帧,丢帧会断音,体验感特别差)
    • 音频快了就放慢数据点(或者重复上一帧)
    • 改变音频速度时涉及到重采样
  3. 以外部时钟为基准
    • 综合1 2,根据外部时钟改变播放速度
  4. 视频和音频各种输出,不做同步处理(一般不会这样)

由于人对听觉变化比对视觉变化更加强烈,因此一般选择同步策略时经常采用以音频为基准!

1.2 音视频同步概念

  1. DTS(Decoding Time Stamp):解码时间戳,是告诉播放器什么时候解码这一帧数据的
  2. PTS(Presentation Time Stamp):显示时间戳,是告诉播放器什么时候播放这一帧数据
  3. time_base时基:就是FFmpeg采用的单位而已

如果视频中没有B帧的话,那么DTS和PTS顺序就一样

time_base 结构为:

typedef struct AVRational
    int num; ///< 分子
    int den; ///< 分母
} AVRational;

计算时间戳:

timestame(秒)=pts*av_qtd(st->time_base)

计算帧时长:

time(秒)=duration*av_qtd(st->time_base)

不同时间基转换:

int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq)

音视频同步的时候需要一个"时钟"的概念,音频,视频,外部时钟都有自己的时钟,各种set自己的时钟,以谁为基准,get时钟的时候就是谁

typedef struct Clock {
    double	pts;            // 时钟基础, 当前帧(待播放)显示时间戳,播放后,当前帧变成上一帧
    // 当前pts与当前系统时钟的差值, audio、video对于该值是独立的
    double	pts_drift;      // clock base minus time at which we updated the clock
    // 当前时钟(如视频时钟)最后一次更新时间,也可称当前时钟时间
    double	last_updated;   // 最后一次更新的系统时钟
    double	speed;          // 时钟速度控制,用于控制播放速度
    // 播放序列,所谓播放序列就是一段连续的播放动作,一个seek操作会启动一段新的播放序列
    int	serial;             // clock is based on a packet with this serial
    int	paused;             // = 1 说明是暂停状态
    // 指向packet_serial
    int *queue_serial;      //指向当前数据包队列串行的指针,用于过时时钟检测
} Clock;

时钟的工作原理:

  1. 需要不停地调用set_clock_at进行"对时",需要pts,serial,time(系统时间)

  2. 获取的时间也是一个估算值,估算是通过"对时"的pts_drift估算的.

图中的时间轴是按照time递增的,我们调用set_Clock进行一次对时间,假设pts是落后于time的那么pts_drift=pts-time,计算出pts和time的差值

过一会需要get_clock来获取pts,那么就可以通过刚刚的差值pts_drift和time推算出来,pts=time+pts_drift,time可以通过ffmpeg提供的av_gettime_relative函数来获取

1.3 FFmpeg中的时间单位

AV_TIME_BASE

  • #define AV_TIME_BASE 1000000
  • FFmpeg内部计时单位

AV_TIME_BASE_Q

  • #define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
  • FFmpeg内部时间基的分数表示,就是AV_TIME_BASE的倒数

时间基转换公式

  • timestamp(FFmpeg内部时间戳)=AV_TIME_BASE*time(秒)
  • time(秒)=timestamp*AV_TIME_BASE_Q

1.4 不同结构体的time_base/duration分析

FFmpeg存在很多时间基,对应着不同的结构体,每一个时间基的值都不一样

  • AVFormatContext

    • duration:代表着整个码流的时间长,获取正常时长时需要除以AV_TIME_BASE,获得结果为秒
  • AVStream

    • time_base:单位为秒,比如AAC音频可能是{1,44100},TS流按{1,90KHZ}
    • duration:表示数据流的时长,单位为AVStream->time_base
  • AVStream的time_base是在dumuxer或者muxer中设置的

    TS

    • avpriv_set_pts_info(st,33,1,90000)

    FLV

    • avpriv_set_pts_info(st,32,1,10000)

    MP4

    • avpriv_set_pts_info(st,64,1,sc->time_scale)
    • avpriv_set_pts_info(st,64,1,track->timescale)

1.5 不同结构体的pts/dts分析

AVPacket和AVFrame时间上都是来自于AVStream->time_base

这里重点说一下编码时各个流的time_base是avformat_write_header后设置的,具体源码暂不分析,但是各位别陷入编码时不知道time_base是怎么来的!

1.6 ffplay中Frame结构体分析

typedef struct Frame {
    AVFrame		*frame;         // 指向数据帧
    AVSubtitle	sub;            // 用于字幕
    int		serial;             // 帧序列,在seek的操作时serial会变化
    double		pts;            // 时间戳,单位为秒
    double		duration;       // 该帧持续时间,单位为秒
    int64_t		pos;            // 该帧在输入文件中的字节位置
    int		width;              // 图像宽度
    int		height;             // 图像高读
    int		format;             // 对于图像为(enum AVPixelFormat),
    // 对于声音则为(enum AVSampleFormat)
    AVRational	sar;            // 图像的宽高比(16:9,4:3...),如果未知或未指定则为0/1
    int		uploaded;           // 用来记录该帧是否已经显示过?
    int		flip_v;             // =1则垂直翻转, = 0则正常播放
} Frame;

为什么Frame结构体中的pts和duration是double类型?

因为我们在写入帧的时候对pts和duration进行了转换,具体实现在queue_picture函数中,因此此时的pts和duration单位应该为!

1.7 Vidoe Frame PTS获取及矫正

pts矫正

frame->pts=frame->best_effort_timestamp;

/**
*使用各种启发式估计的帧时间戳,以流时基为单位
*-编码:未使用
*-解码:由libavcodec设置,由用户读取。
*/
int64_t best_effort_timestamp;

对frame的pts进行矫正,具体矫正算法由libavcodec设置,帧时间戳,基本与 pts 相同,如果当前 pts 存在不合理值,会尝试进行一系列校准来得到这个更合理的值.

1.8 Audio Frame PTS的获取

ffplay有3次pts转换

  1. 由AVStream->time_base转为{1,采样率}

    我认为应该是重采样导致采样率改变,如果还使用AVStream->time_base就会出错!

    frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
    
  2. 将采样率转化为秒,就是queue_picture放入帧队列时需要

    af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
    
  3. 根据拷贝给SDL的数据量进行估算调整

    set_clock_at(&is->audclk, is->audio_clock -
    			(double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size)
    			/ is->audio_tgt.bytes_per_sec,
    			is->audio_clock_serial,
    			audio_callback_time / 1000000.0);
    

2.以音频为基准

ffplay默认采用音频同步模式,而音频同步模式的时钟设置设置在sdl_audio_callback中

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
  
    audio_callback_time = av_gettime_relative(); // while可能产生延迟
    
       .............................................
    
    is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
    /* Let's assume the audio driver that is used by SDL has two periods. */
    if (!isnan(is->audio_clock)) {
        set_clock_at(&is->audclk, is->audio_clock -
                                      (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size)
                                          / is->audio_tgt.bytes_per_sec,
                     is->audio_clock_serial,
                     audio_callback_time / 1000000.0);
        sync_clock_to_slave(&is->extclk, &is->audclk);
    }
}

这块和我之前分析的视频播放线程里面是一样的可以看看

下面的流程是视频播放的主流程,也可以看看之前的讲解

]

重点就是计算上一帧显示时长比较重要,看一下代码:

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 */
            // 从队列取出上一个Frame
            lastvp = frame_queue_peek_last(&is->pictq);//读取上一帧
            vp = frame_queue_peek(&is->pictq);  // 读取待显示帧
            // lastvp 上一帧(正在显示的帧)
            // vp 等待显示的帧

            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;
                printf("视频暂停is->paused");
            }
            /* compute nominal last_duration */
            //lastvp上一帧,vp当前帧 ,nextvp下一帧
            //last_duration 计算上一帧应显示的时长
            last_duration = vp_duration(is, lastvp, vp);

            // 经过compute_target_delay方法,计算出待显示帧vp需要等待的时间
            // 如果以video同步,则delay直接等于last_duration。
            // 如果以audio或外部时钟同步,则需要比对主时钟调整待显示帧vp要等待的时间。
            delay = compute_target_delay(last_duration, is); // 上一帧需要维持的时间
            time= av_gettime_relative()/1000000.0;
            // is->frame_timer 实际上就是上一帧lastvp的播放时间,
            // is->frame_timer + delay 是待显示帧vp该播放的时间
            if (time < is->frame_timer + delay) { //判断是否继续显示上一帧
                // 当前系统时刻还未到达上一帧的结束时刻,那么还应该继续显示上一帧。
                // 计算出最小等待时间
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }

            // 走到这一步,说明已经到了或过了该显示的时间,待显示帧vp的状态变更为当前要显示的帧

            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); // 更新video时钟
            SDL_UnlockMutex(is->pictq.mutex);
            //丢帧逻辑
            if (frame_queue_nb_remaining(&is->pictq) > 1) {//有nextvp才会检测是否该丢帧
                Frame *nextvp = frame_queue_peek_next(&is->pictq);
                duration = vp_duration(is, vp, nextvp);
                if(!is->step        // 非逐帧模式才检测是否需要丢帧 is->step==1 为逐帧播放
                    && (framedrop>0 ||      // cpu解帧过慢
                        (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) // 非视频同步方式
                    && time > is->frame_timer + duration // 确实落后了一帧数据
                    ) {
                    printf("%s(%d) dif:%lfs, drop frame\n", __FUNCTION__, __LINE__,
                           (is->frame_timer + duration) - time);
                    is->frame_drops_late++;             // 统计丢帧情况
                    frame_queue_next(&is->pictq);       // 这里实现真正的丢帧
                    //(这里不能直接while丢帧,因为很可能audio clock重新对时了,这样delay值需要重新计算)
                    goto retry; //回到函数开始位置,继续重试
                }
            }
........
}

重点看一下delay = compute_target_delay(last_duration, is);

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

    /* update delay to follow master synchronisation source */
    /* 如果发现当前主Clock源不是video,则计算当前视频时钟与主时钟的差值 */
    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
           通过重复帧或者删除帧来纠正延迟*/
        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) { // diff在最大帧duration内
            if (diff <= -sync_threshold) {      // 视频已经落后了
                delay = FFMAX(0, delay + diff); // 上一帧持续的时间往小的方向去调整
            }
            else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) {
                //  delay = 0.2秒
                // diff  = 1秒
                // delay = 0.2 + 1 = 1.2
                // 视频超前
                //AV_SYNC_FRAMEDUP_THRESHOLD是0.1,此时如果delay>0.1, 如果2*delay时间就有点久
                delay = delay + diff; // 上一帧持续时间往大的方向去调整
                av_log(NULL, AV_LOG_INFO, "video: delay=%0.3f A-V=%f\n",
                       delay, -diff);
            }
            else if (diff >= sync_threshold) {
                // 上一帧持续时间往大的方向去调整
                // delay = 0.2 *2 = 0.4
                delay = 2 * delay; // 保持在 2 * AV_SYNC_FRAMEDUP_THRESHOLD内, 即是2*0.1 = 0.2秒内
                //                delay = delay + diff; // 上一帧持续时间往大的方向去调整
            } else {
                // 音视频同步精度在 -sync_threshold ~ +sync_threshold
                // 其他条件就是 delay = delay; 维持原来的delay, 依靠frame_timer+duration和当前时间进行对比
            }
        }
    } else {
        // 如果是以video为同步,则直接返回last_duration
    }

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

    return delay;
}

这个函数通过视频时钟和音频时钟计算的差值和delay进行比较来确定delay的值为多少

步骤:

  1. 计算diff,就是用视频的时钟-音频的时钟的值

  2. sync_threshold是delay在AV_SYNC_THRESHOLD_MIN和AV_SYNC_THRESHOLD_MIN范围类的值,计算方法为:

     sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN,
                                   FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
    
  3. 如果diff这个差值比设置的max_frame_duration还大的话,就直接 delay不需要做调整了,直接 返回出去有外部决定丢帧还是继续上一帧

  4. diff小于max_frame_duration,那么我们就要对delay作出调整,因为播放时间不能总固定为duration,而是更具实际误差来决定的,比如说音频比视频快半帧,那么视频这个播放时间就不能太久,得缩短,如果慢半帧的话,视频播放时间得延长

  5. if (diff <= -sync_threshold) {      // 视频已经落后了
                    delay = FFMAX(0, delay + diff); // 上一帧持续的时间往小的方向去调整
                }
                else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) {
                    //  delay = 0.2秒
                    // diff  = 1秒
                    // delay = 0.2 + 1 = 1.2
                    // 视频超前
                    //AV_SYNC_FRAMEDUP_THRESHOLD是0.1,此时如果delay>0.1, 如果2*delay时间就有点久
                    delay = delay + diff; // 上一帧持续时间往大的方向去调整
                    av_log(NULL, AV_LOG_INFO, "video: delay=%0.3f A-V=%f\n",
                           delay, -diff);
                }
                else if (diff >= sync_threshold) {
                    // 上一帧持续时间往大的方向去调整
                    // delay = 0.2 *2 = 0.4
                    delay = 2 * delay; // 保持在 2 * AV_SYNC_FRAMEDUP_THRESHOLD内, 即是2*0.1 = 0.2秒内
                    //                delay = delay + diff; // 上一帧持续时间往大的方向去调整
                } else {
                    // 音视频同步精度在 -sync_threshold ~ +sync_threshold
                    // 其他条件就是 delay = delay; 维持原来的delay, 依靠frame_timer+duration和当前时间进行对比
                }
    
    • 第一个if (diff <= -sync_threshold)

      是说视频落后了,这个时候的diff是负数,那么delay就应该变小,采取的是delay = FFMAX(0, delay + diff);毕竟不能为0

    • 第二个if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)

      是说视频快于音频了,并且显示时间大于AV_SYNC_FRAMEDUP_THRESHOLD,那么采取的措施是延长delay = delay + diff;

    • 第三个if (diff >= sync_threshold)就是快于音频,并且显示时间小于AV_SYNC_FRAMEDUP_THRESHOLD,采取的措施就是 delay = 2 * delay

3.以视频为基准

媒体流里面只有视频成分时,才会使用以视频为基准!

在以音频为基准时采用的是丢帧或者是等待的策略,而以视频为基准时就不能简单的丢帧处理了,因为人对声音非常敏感,一旦断音了非常容易感觉的到!

3.1 音频主流程

在这里插入图片描述

音频同步策略就是重采样,改变一帧的样本数从而实现变速效果,如果音频慢了就减少样本数,如果音频快了就增加样本数

具体实现看audio_decode_frame:

static 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)
        return -1;

    do {
        // 若队列头部可读,则由af指向可读帧
        if (!(af = frame_queue_peek_readable(&is->sampq)))
            return -1;
        frame_queue_next(&is->sampq);
    } while (af->serial != is->audioq.serial);

    // 根据frame中指定的音频参数获取缓冲区的大小 af->frame->channels * af->frame->nb_samples * 2
    data_size = av_samples_get_buffer_size(NULL,
                                           af->frame->channels,
                                           af->frame->nb_samples,
                                           af->frame->format, 1);
    // 获取声道布局
    dec_channel_layout =
        (af->frame->channel_layout &&
         af->frame->channels == av_get_channel_layout_nb_channels(af->frame->channel_layout)) ?
            af->frame->channel_layout : av_get_default_channel_layout(af->frame->channels);
    // 获取样本数校正值:若同步时钟是音频,则不调整样本数;否则根据同步需要调整样本数
    wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);
    // is->audio_tgt是SDL可接受的音频帧数,是audio_open()中取得的参数
    // 在audio_open()函数中又有"is->audio_src = is->audio_tgt""
    // 此处表示:如果frame中的音频参数 == is->audio_src == is->audio_tgt,
    // 那音频重采样的过程就免了(因此时is->swr_ctr是NULL)
    // 否则使用frame(源)和is->audio_tgt(目标)中的音频参数来设置is->swr_ctx,
    // 并使用frame中的音频参数来赋值is->audio_src
    if (af->frame->format           != is->audio_src.fmt            || // 采样格式
        dec_channel_layout      != is->audio_src.channel_layout || // 通道布局
        af->frame->sample_rate  != is->audio_src.freq           || // 采样率
        // 第4个条件, 要改变样本数量, 那就是需要初始化重采样
        (wanted_nb_samples      != af->frame->nb_samples && !is->swr_ctx) // samples不同且swr_ctx没有初始化
        ) {
        swr_free(&is->swr_ctx);
        is->swr_ctx = swr_alloc_set_opts(NULL,
                                         is->audio_tgt.channel_layout,  // 目标输出
                                         is->audio_tgt.fmt,
                                         is->audio_tgt.freq,
                                         dec_channel_layout,            // 数据源
                                         af->frame->format,
                                         af->frame->sample_rate,
                                         0, NULL);
        if (!is->swr_ctx || swr_init(is->swr_ctx) < 0) {
            av_log(NULL, AV_LOG_ERROR,
                   "Cannot create sample rate converter for conversion of %d Hz %s %d channels to %d Hz %s %d channels!\n",
                   af->frame->sample_rate, av_get_sample_fmt_name(af->frame->format), af->frame->channels,
                   is->audio_tgt.freq, av_get_sample_fmt_name(is->audio_tgt.fmt), is->audio_tgt.channels);
            swr_free(&is->swr_ctx);
            return -1;
        }
        is->audio_src.channel_layout = dec_channel_layout;
        is->audio_src.channels       = af->frame->channels;
        is->audio_src.freq = af->frame->sample_rate;
        is->audio_src.fmt = af->frame->format;
    }

    if (is->swr_ctx) {
        // 重采样输入参数1:输入音频样本数是af->frame->nb_samples
        // 重采样输入参数2:输入音频缓冲区
        const uint8_t **in = (const uint8_t **)af->frame->extended_data; // data[0] data[1]

        // 重采样输出参数1:输出音频缓冲区尺寸
        uint8_t **out = &is->audio_buf1; //真正分配缓存audio_buf1,指向是用audio_buf
        // 重采样输出参数2:输出音频缓冲区
        int out_count = (int64_t)wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate
                        + 256;

        int out_size  = av_samples_get_buffer_size(NULL, is->audio_tgt.channels,
                                                  out_count, is->audio_tgt.fmt, 0);
        int len2;
        if (out_size < 0) {
            av_log(NULL, AV_LOG_ERROR, "av_samples_get_buffer_size() failed\n");
            return -1;
        }
        // 如果frame中的样本数经过校正,则条件成立
        if (wanted_nb_samples != af->frame->nb_samples) {
            int sample_delta = (wanted_nb_samples - af->frame->nb_samples) * is->audio_tgt.freq
                               / af->frame->sample_rate;
            int compensation_distance = wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate;
            // swr_set_compensation
            if (swr_set_compensation(is->swr_ctx,
                                     sample_delta,
                                     compensation_distance) < 0) {
                av_log(NULL, AV_LOG_ERROR, "swr_set_compensation() failed\n");
                return -1;
            }
        }
        av_fast_malloc(&is->audio_buf1, &is->audio_buf1_size, out_size);
        if (!is->audio_buf1)
            return AVERROR(ENOMEM);
        // 音频重采样:返回值是重采样后得到的音频数据中单个声道的样本数
        len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);
        if (len2 < 0) {
            av_log(NULL, AV_LOG_ERROR, "swr_convert() failed\n");
            return -1;
        }
        if (len2 == out_count) {
            av_log(NULL, AV_LOG_WARNING, "audio buffer is probably too small\n");
            if (swr_init(is->swr_ctx) < 0)
                swr_free(&is->swr_ctx);
        }
        // 重采样返回的一帧音频数据大小(以字节为单位)
        is->audio_buf = is->audio_buf1;
        resampled_data_size = len2 * is->audio_tgt.channels * av_get_bytes_per_sample(is->audio_tgt.fmt);
    } else {
        // 未经重采样,则将指针指向frame中的音频数据
        is->audio_buf = af->frame->data[0]; // s16交错模式data[0], fltp data[0] data[1]
        resampled_data_size = data_size;
    }

    audio_clock0 = is->audio_clock;
    /* update the audio clock with the pts */
    if (!isnan(af->pts))
        is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;
    else
        is->audio_clock = NAN;
    is->audio_clock_serial = af->serial;
    return resampled_data_size;
}

这里重点讲解一下如何计算wanted_nb_samples这个值,分析一下synchronize_audio函数:

static int synchronize_audio(VideoState *is, int nb_samples)
{
    int wanted_nb_samples = nb_samples;

    /* if not master, then we try to remove or add samples to correct the clock */
    if (get_master_sync_type(is) != AV_SYNC_AUDIO_MASTER) {//不是以音频为基准时
        double diff, avg_diff;
        int min_nb_samples, max_nb_samples;

        diff = get_clock(&is->audclk) - get_master_clock(is);//过去音频和默认时钟的差值

        if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {//如果差值大于AV_NOSYNC_THRESHOLD则正常播放,不做任何处理,一般这种情况就出现错误了
          
            is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum;
            if (is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) {
                /* not enough measures to have a correct estimate */
                is->audio_diff_avg_count++; // 连续20次不同步才进行校正
            } else {
                /* estimate the A-V difference */
                avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
                //                avg_diff = diff;
                if (fabs(avg_diff) >= is->audio_diff_threshold) {
                    wanted_nb_samples = nb_samples + (int)(diff * is->audio_src.freq);
                    min_nb_samples = ((nb_samples * (100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100));
                    max_nb_samples = ((nb_samples * (100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100));
                    // av_clip 用来限制wanted_nb_samples最终落在 min_nb_samples~max_nb_samples
                    // nb_samples *(90%~110%)
                    wanted_nb_samples = av_clip(wanted_nb_samples, min_nb_samples, max_nb_samples);
                }
                av_log(NULL, AV_LOG_INFO, "diff=%f adiff=%f sample_diff=%d apts=%0.3f %f\n",
                       diff, avg_diff, wanted_nb_samples - nb_samples,
                       is->audio_clock, is->audio_diff_threshold);
            }
        } else {
            // > AV_NOSYNC_THRESHOLD 阈值,该干嘛就干嘛
            /* too big difference : may be initial PTS errors, so
               reset A-V filter */
            is->audio_diff_avg_count = 0;
            is->audio_diff_cum       = 0;   // 恢复正常后重置为0
        }
    }

    return wanted_nb_samples;
}

代码中的audio_diff_cum是计算加权总和

我们先看一下audio_diff_avg_coef是什么?

is->audio_diff_avg_coef  = exp(log(0.01) / AUDIO_DIFF_AVG_NB)//exp 自然常数

这个值是固定的,在计算加权总和时也就是一个固定比例

仔细分析一下:s->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum;

这个数最后会循环AUDIO_DIFF_AVG_NB次,得到加权总和,

而avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);

是加权总和/权重和得到的结果avg_diff,后面使用avg_diff作为差值比较

is->audio_diff_threshold = (double)(is->audio_hw_buf_size) / is->audio_tgt.bytes_per_sec;

这个值是计算差值的阀值时间为多少,如果大于这个值就要进行同步~

同步算法很简单

通过diff*采样率得到差值的样本数量

wanted_nb_samples = nb_samples + (int)(diff * is->audio_src.freq);//获取想要的样本数
min_nb_samples = ((nb_samples * (100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100));//样本数减少10%的样本数
max_nb_samples = ((nb_samples * (100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100));//样本数增加10%的样本数
// av_clip 用来限制wanted_nb_samples最终落在 min_nb_samples~max_nb_samples
// nb_samples *(90%~110%)
wanted_nb_samples = av_clip(wanted_nb_samples, min_nb_samples, max_nb_samples);//av_clip就是取中间值,如果大于max就取max,小于min就取min,反正就是将wanted_nb_samples保证在nb_samples *(90%~110%)

然后到达audio_decode_frame会进行重采样,重采样直接会调用swr_set_compensation是样本补偿函数!

4.以外部时钟为基准

外部时钟就是基于前两种时钟的

通过sync_clock_to_slave函数进行设置

static void sync_clock_to_slave(Clock *c, Clock *slave)
{
    double clock = get_clock(c);
    double slave_clock = get_clock(slave);
    if (!isnan(slave_clock) && (isnan(clock) || fabs(clock - slave_clock) > AV_NOSYNC_THRESHOLD))
        set_clock(c, slave_clock, slave->serial);
}

这里的c是外部时钟,slave是其他时钟,可以看出当其他时钟被设置,并且外部时钟没有设置的时候或者是都设置了但是相差大于了AV_NOSYNC_THRESHOLD就要重新设置.

因此可以看出基本上外部时钟只会设置一次,具体设置是看第一帧是音频还是视频,用他们的时钟进行设置即可

总结

具体以什么为基准时就是看你设置的参数,然后通过get_master_clock来获取主时钟,因此3大时钟都会 设置!!!

各个时钟设置位置:

音频时钟是:

 set_clock_at(&is->audclk, is->audio_clock -
                                      (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size)
                                          / is->audio_tgt.bytes_per_sec,
                     is->audio_clock_serial,
                     audio_callback_time / 1000000.0);

视频时钟是:

 if (!isnan(vp->pts))
                update_video_pts(is, vp->pts, vp->pos, vp->serial);

外部时钟是:

音频时钟设置的下面部分:

set_clock_at(&is->audclk, is->audio_clock -
                                      (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size)
                                          / is->audio_tgt.bytes_per_sec,
                     is->audio_clock_serial,
                     audio_callback_time / 1000000.0);
        sync_clock_to_slave(&is->extclk, &is->audclk);

还有一个是视频时钟设置的update_video_pts函数中:

static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) {
    /* update current video pts */
    set_clock(&is->vidclk, pts, serial);
    sync_clock_to_slave(&is->extclk, &is->vidclk);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/786173.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

了解Unity编辑器之组件篇Tilemap(五)

Tilemap&#xff1a;用于创建和编辑2D网格地图的工具。Tilemap的主要作用是简化2D游戏中地图的创建、编辑和渲染过程。以下是一些Tilemap的主要用途&#xff1a; 2D地图绘制&#xff1a;Tilemap提供了一个可视化的编辑器界面&#xff0c;可以快速绘制2D地图&#xff0c;例如迷…

jlink RTT调试 NRF52840

打开 J-Link RTT Viewer 搜索&#xff1a;**J-Link RTT Viewer ** 软件部分 代码部分 #include <stdbool.h> #include <stdint.h> #include "nrf_delay.h" #include "boards.h" //Log需要引用的头文件 #include "nrf_log.h"…

音频转换工具有很多,但是找到好用的还是得看这篇

在日常生活中&#xff0c;我们常常会遇到需要将音频文件转换成不同格式的情况。不过&#xff0c;有些音频转换软件可能需要安装繁琐的插件&#xff0c;这对于一些小伙伴来说可能不太方便。幸运的是&#xff0c;如今有许多免费的音频转换格式软件可供选择&#xff0c;让我们能够…

K3S 安装部署

一、方法1&#xff1a;利用官方源&#xff08;国外源&#xff09;直接一键安装 因 K3s 的核心组件镜像需从 gcr.io 拉取&#xff08;国内网络不通&#xff09;&#xff0c;所以需具备外网访问的环境&#xff0c;适用于服务器均在国外的环境选用&#xff0c;简单粗暴一键安装。…

mysql进阶1——proxysql中间件

文章目录 一、基本了解二、安装部署三、proxysql管理配置3.1 内置库3.1.1 main库表3.1.2 stats库表3.1.3 monitor库 3.2 常用管理变量3.2.1 添加管理用户3.2.2 添加普通用户3.2.3 修改监听套接字 四、多层配置系统4.1 系统结构4.2 修改变量加载配置4.3 启动加载流程 一、基本了…

聊一聊什么是JNDI数据源

大家好&#xff0c;我是G探险者。 我们平时开发项目&#xff0c;连接数据库那块&#xff0c;会采用连接池的方式连进行连接数据库&#xff0c;比如常见的durid,dbcp&#xff0c;c3p0等。那你有没有听过还有一个JNDI数据源呢&#xff0c;反正我以前是很少听说过。可能就是因为自…

梅尔频谱(Mel spectrum)简介及Python实现

梅尔频谱&#xff08;Mel spectrum&#xff09;简介及Python实现 1. 梅尔频谱&#xff08;Mel spectrum&#xff09;简介2. Python可视化测试3.频谱可视化3.1 Mel 频谱可视化3.2 STFT spectrum 参考文献资料 1. 梅尔频谱&#xff08;Mel spectrum&#xff09;简介 在信号处理上…

wordpress框架自定义添加page分页功能

先来看效果图&#xff1a; 一、在主题目录下的functions.php文件里&#xff0c;添加如下分页函数&#xff1a; /** * 数字分页函数 * 说明&#xff1a;因为wordpress默认仅仅提供简单分页&#xff0c;所以要实现数字分页&#xff0c;需要自定义函数 * Param bool $isHome 是…

工业静电监控系统的功能介绍

工业静电监控系统是一种用于监测和控制工业生产过程中静电现象的技术系统。静电是指由于物体间的电荷不平衡而产生的电场现象&#xff0c;它在工业生产中可能导致电击、火花、电磁干扰等质量问题。 工业静电监控系统主要通过使用静电传感器和控制设备来实现对静电场的监测和控…

Java反序列化(0):URLDNS的反序列化调试分析

URLDNS链子是Java反序列化分析的第0课&#xff0c;网上也有很多优质的分析文章。 笔者作为Java安全初学者&#xff0c;也从0到1调试了一遍&#xff0c;现在给出调试笔记。 一. Java反序列化前置知识 Java原生链序列化&#xff1a;利用Java.io.ObjectInputStream对象输出流的w…

中医药行业如何进行数字化转型?看天津同仁堂谈“有道有术有零代码”

张伯礼院士曾指出&#xff0c;中药制造的现代化水平&#xff0c;还停留在10%左右的阶段。中医药行业&#xff0c;老字号企业&#xff0c;该如何通过数字化焕发新活力&#xff1f; 天津同仁堂通过与伙伴云合作&#xff0c;零代码构建数字化系统&#xff0c;让技术与思维共同成长…

【Linux】Tcp协议的通讯流程,浅谈三次握手四次挥手

文章目录 Tcp协议的通讯流程一、协议定制与网络版计算器的实现二、json的使用总结 Tcp协议的通讯流程 上一篇文章我们讲解了如何实现Tcp服务器&#xff0c;Tcp的接口也用了&#xff0c;下面我们就看一下Tcp协议的通讯流程&#xff1a; 在服务端&#xff0c;我们首先要创建一个…

Django on_delete参数在sql级别操作中不生效问题

class AA(models.Model):name models.CharField(max_length128)class Meta:db_table aaclass BB(models.Model):name models.CharField(max_length128)aa models.ForeignKey(AA, nullTrue, on_deletemodels.CASCADE)class Meta:db_table bb 如上当使用ORM删除aa表中的数据…

数字孪生:未来科技的新前沿

数字孪生作为一项新兴的研究方向&#xff0c;正逐渐成为科技界的焦点。它是将现实世界中的实体、系统或过程通过数字化手段进行建模、仿真和分析&#xff0c;形成与实体相对应的数字化副本。数字孪生的发展为我们带来了无限的想象空间&#xff0c;以及解决现实问题的新途径。 在…

opencv-18 什么是色彩空间?

1.什么是色彩空间类型&#xff1f; 色彩空间类型&#xff0c;也称为颜色空间类型或色彩模型&#xff0c;是一种表示图像中颜色的方式。在计算机图形学和数字图像处理中&#xff0c;有许多种色彩空间类型&#xff0c;每种类型有不同的表达方式和特点。 常见的色彩空间类型包括&a…

Vector - CAPL - 诊断模块函数(回调函数信息)

目录 CanTpCopyDataReceived CAPL 调用返回的错误代码及其含义 CanTpGetReceiverAddress CanTpGetRecentAddressExtension CanTpGetSenderAddress 代码示例 CanTpProvideTxData 代码示例 CanTpSetRxBufferSize 代码示例 CanTpCopyDataReceived 功能&#xff1a;通过回…

​《爆肝整理》保姆级系列教程-玩转Charles抓包神器教程(16)-Charles其他骚操作之大结局​

1.简介 今天就说一些Charles的其他操作、以及抓包跨域的问题和常见的问题如何解决。到此Charles这一系列的文章也要和大家说再见了&#xff0c;其他什么小程序、Android7.0等等的问题可以查看宏哥的Fiddler系列文章&#xff0c;只不过是将Fiddler换成Charles而已。 2.模拟403…

vue中使用Base64转码(Tinymce富文本保留HTML标签)

在vue项目中&#xff0c;我们经常使用到富文本编辑器&#xff0c;例如博主的项目&#xff08;见上图&#xff09;&#xff0c;这里需要把富文本内的HTML结构&#xff0c;发放到Android端做混合应用的开发&#xff0c;因此HTML结构必不可少的&#xff01; 但是浏览器在向服务器…

【MySQL基础】

目录 一、概述 1.什么是数据库 2.数据库能干什么 2.1 企业应用 2.2 金融行业 2.3 电子商务 2.4 社交媒体 2.5物联网 3.为什么要用数据库&#xff0c;优势、特性&#xff1f; 3.1 可靠性和稳定性 3.2 数据管理能力 3.3 数据共享和集成 3.4 数据安全性和隐私保护 3…

新零售转型战略:打造数字化时代持续性盈利的商业模式

新零售达成什么样子&#xff0c;能够创造更大得客户价值&#xff0c;从企业得角度来看&#xff0c;他一定要提升企业得获利能力。新零售应该怎么做&#xff1f;以客户需求为本&#xff0c;找出需求&#xff0c;重新创造场景. 从而提升用户体验加强粘性&#xff0c;而蚓链数字化…