Desarrollo de reproductores multiplataforma (7) análisis de principios de sincronización de audio y video, desencapsulación, decodificación y sincronización de videos

Introducción

Este artículo presenta principalmente cómo "ffplay" implementa las capacidades de desencapsulación, decodificación y sincronización de audio y video. El próximo artículo separará algunas capacidades básicas basadas en las capacidades ya proporcionadas por "ffplay" para facilitar la expansión posterior.

desencapsulación

La desencapsulación, como su nombre lo indica, es en realidad desensamblar el paquete encapsulado y obtener los datos comprimidos correspondientes al audio y al video. Echemos un vistazo al diagrama de arquitectura principal de decapsulación.

Lo siguiente toma prestado el diagrama de arquitectura ffplay dibujado por **"Thor"** , y extraigo el desencapsulado correspondiente:

「Av_register_all」

Como se muestra en el siguiente código, de hecho, en versiones superiores, no es necesario llamar a av_register_all.

/* register all codecs, demux and protocols */
#if CONFIG_AVDEVICE
    avdevice_register_all();
#endif
复制代码

「avformat_open_input」

int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options);
复制代码

avformat_open_inputEl significado es abrir un flujo de entrada y leer el encabezado de encapsulación. Finalmente, la transmisión debe avformat_close_input()cerrarse .

「avformat_find_stream_info」

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
复制代码

avformat_find_stream_infoLa función es principalmente detectar la información básica de las transmisiones de audio y video, y la decodificación se abrirá internamente para la decodificación y detección.

「Av_read_frame」

int av_read_frame(AVFormatContext *s, AVPacket *pkt);
复制代码

av_read_frameEsta función lee principalmente los datos de audio, video y subtítulos desencapsulados. Hay una cosa a la que prestar atención en el código fuente de ffplay, el código es el siguiente:

De acuerdo con las sugerencias en el código fuente, ffplay establece descartar para prohibir **"leer todos los cuadros"**. Porque cuando desacoplaba el código ffplay, no presté atención a esta función, lo que provocó que no se pudieran leer los datos durante la desencapsulación de prueba.

La av_read_freamfunción siempre read_threadleerá en un bucle infinito en la capa de línea.

descodificación

La decodificación de audio, video y subtítulos en ffplay crea hilos independientes respectivamente.En la stream_component_openfunción , el código principal es el siguiente:

static int stream_component_open(VideoState *is, int stream_index) {
  
   ...
    
    /* 根据codec_id查找解码器 */
    codec = avcodec_find_decoder(avctx->codec_id);    
  
   ...
    //打开解码器
     if ((ret = avcodec_open2(avctx, codec, &opts)) < 0) {
        goto fail;
    }     
   
   //在这里对流信息设置了默认,默认我的理解就是可以读取所有的帧
    ic->streams[stream_index]->discard = AVDISCARD_DEFAULT;
    switch (avctx->codec_type) {
        case AVMEDIA_TYPE_AUDIO:

         ...

            /* prepare audio output 准备音频输出*/
            //调用audio_open打开sdl音频输出,实际打开的设备参数保存在audio_tgt,返回值表示输出设备的缓冲区大小
            if ((ret = audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
                goto fail;
     ...
            
            // 初始化ffplay封装的音频解码器
            decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);
     ...
            // 启动音频解码线程
            if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)
                goto out;
            SDL_PauseAudioDevice(audio_dev, 0);
            break;
        case AVMEDIA_TYPE_VIDEO:
     ...
            // 初始化ffplay封装的视频解码器
            decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
            // 启动视频频解码线程
            if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)
                goto out;
            is->queue_attachments_req = 1; // 使能请求mp3、aac等音频文件的封面
            break;
        case AVMEDIA_TYPE_SUBTITLE: // 视频是类似逻辑处理
      ...
            decoder_init(&is->subdec, avctx, &is->subtitleq, is->continue_read_thread);
            if ((ret = decoder_start(&is->subdec, subtitle_thread, "subtitle_decoder", is)) < 0)
                goto out;
            break;
        default:
            break;
    }
}
复制代码

根据上面简略代码,可以看出,音频、视频、字幕,分别在 audio_threadvideo_threadsubtitle_thread 线程中进行解码。

解码的核心 api 如下:

读取解码后的 AVFrame 原始数据

avcodec_receive_frame(d->avctx, frame);
复制代码

将压缩数据发送到解码缓冲队列中

avcodec_send_packet(d->avctx, &pkt);
复制代码

自此,在配合 packet queue, 只要队列中有数据,那么就会调用解码函数,解码成功就会存入对应的 frame queue 中。

音视频同步

何为音视频同步,其实它的作用就是保证音频播放的时间与视频播放的时间要在一条时间线上。所以这里就产生出了,如何保证在一条时间线上,这个时候就需要一个参考,在 ffplay 中默认使用了视频参考音频的方式,

其一,视频以音频为基准(推荐,用的最多):

  • 视频播放时间以音频播放时间为准,如果视频比音频快,那么将等待其实就是定帧,如果视频比音频慢,就需要进行丢帧处理了。

其二,音频以视频 pts 为基准

其三,以外部时钟为基准

如果视频向音频同步,那么首先是需要音频播放的时候,将音频播放的 pts 给存储起来,在 ffplay 源码中,是这样进行操作的,主要是在 sdl_audio_callback 代码中,我这里为了便于理解,直接上伪代码吧。

「音频播放 pts 设置」

static void sdl_audio_callback() {
    //拿到回调开始时间
    long audio_callback_time = av_gettime_relative(); // while 可能产生延迟,最后时间需要算上中间处理的时间
    //PCM 音频采样 1024  * 2 通道 * 16/2 byte * 缓存 2 帧 = 8192;
    int sdl_audio_buf_size = 8192;

    int audio_pts = 0;
    int ret = sdl_audio_buf_size;

    while (ret > 0)//循环读取,直到读取到足够的数据
    {
        //1、拿到解码的数据
        uint8_t *data_ = NULL;
        //2. 拿到解码后的 PCM 数据
        int size = get_audio_frame(&data_);
        //3、将 pcm 数据,copy 到 sdl 缓冲区中
        copy_to_sdl(data_);
        ret -= size;
        audio_pts+=size/audio_sample_rate;
    }

    //4、设置音频时钟
    //这里的含义就是为了更加准确的计算出当前实际播放了多少音频数据
    set_audio_clock(audio_pts-(实际写入到缓存区的音频数据/(sample_rate*2*2)),audio_callback_time);
}
复制代码

ffplay 主要设置时钟的代码,我给简化成了伪代码,这样看起来或许会更好理解一点。

「视频播放同步设置」

这里我还是以伪代码进行同步

static void video_refresh()
{
    //拿到当前时间
    int time = av_gettime_relative() / 1000000.0;
    //1、计算当前帧显示的持续时长
    int last_duration = 当前视频帧pts - 上一帧pts;
    double delay = last_duration;
    //2、拿到真正需要 delay 时间
    double diff = 当前视频帧pts - 获取音频播放的pts;
   //拿到最大的一个同步范围,ffplay 最小是0.04s ,最大是 0.1s
    double sync_threshold = FFMAX(0.04,
                           FFMIN(0.1, delay));

    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;
    }
    //设置最后一帧显示的时间
    frame_timer  += delay;
    if (frame_timer <= 0)//如果是第一帧,那么直接设置当前系统时间
        frame_timer = time;

    //判断是否丢帧,如果已经大于显示的一帧数据,那么进行丢帧
    double next_duration = get_next_frame().pts - get_last_frame().pts;
    if (time > frame_timer + next_duration){
        drop_frame();
        return;
    }

    //显示视频帧
    display();

    //其实就是显示多久
    sleep(delay);

}
复制代码

上面伪代码的注释都比较清晰,相信大家能看得懂,或者你可以直接看 ffplay 源码部分

SDL 预览

SDL预览部分,我这里就不必在分析了,因为代码都是一样的。

总结

该篇简略的分析了 ffplay 实现原理,个人觉得 ffplay 内部最不容易理解的其实是音视频同步,其它都还是比较容易看的懂。建议还是自己读一下 ffplay 源码,这样才会对 ffplay 的理解更加深入。

参考

Supongo que te gusta

Origin juejin.im/post/7079738863576416263
Recomendado
Clasificación