Análisis de ffplay player (3) ---- Análisis del hilo de decodificación

1. Marco de subprocesos de decodificación

El hilo de decodificación de ffplay es independiente del hilo de lectura de datos y a cada flujo se le asigna su propio hilo de decodificación.

  • video_thread se utiliza para decodificar video_stream
  • audio_thread se utiliza para decodificar audio_stream
  • subtitle_thread se usa para decodificar subtitle_stream

Estructura de embalaje del decodificador

typedef struct Decoder {
    
    
    AVPacket pkt;
    PacketQueue	*queue;         // 数据包队列
    AVCodecContext	*avctx;     // 解码器上下文
    int		pkt_serial;         // 包序列
    int		finished;           // =0,解码器处于工作状态;=非0,解码器处于空闲状态
    int		packet_pending;     // =0,解码器处于异常状态,需要考虑重置解码器;=1,解码器处于正常状态
    SDL_cond	*empty_queue_cond;  // 检查到packet队列空时发送 signal缓存read_thread读取数据
    int64_t		start_pts;          // 初始化时是stream的start time
    AVRational	start_pts_tb;       // 初始化时是stream的time_base
    int64_t		next_pts;           // 记录最近一次解码后的frame的pts,当解出来的部分帧没有有效的pts时则使用next_pts进行推算
    AVRational	next_pts_tb;        // next_pts的单位
    SDL_Thread	*decoder_tid;       // 线程句柄
} Decoder;

Funciones de operación relacionadas con el decodificador

- 初始化解码器
static void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond) 
- 启用解码器线程
static int decoder_start(Decoder *d, int (*fn)(void *), const char *thread_name, void* arg)
- 解帧
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) 
- 终止解码器
static void decoder_abort(Decoder *d, FrameQueue *fq)
- 销毁解码器
static void decoder_destroy(Decoder *d)

Inicie el hilo del decodificador:

decoder_init
decoder_start

El proceso específico de decodificación del hilo:

decoder_decode_frame

Salga del hilo de decodificación:

decoder_abort
decoder_destroy

2. Función de hilo de decodificación de video

Fuente de datos: el avpacket leído por read_thread se coloca en la cola de video de paquetes correspondiente.

Procesamiento de datos: video_thread lee el AVPacket en la cola de video del paquete para decodificarlo, obtiene el AVFrame y lo coloca en la cola de video del cuadro correspondiente.

Exportación de datos: lea el fotograma en la cola de vídeo de fotogramas en video_refresh para visualizarlo.

// 视频解码线程
static int video_thread(void *arg)
{
    
    
    VideoState *is = arg;
    AVFrame *frame = av_frame_alloc();  // 分配解码帧
    double pts;                 // pts
    double duration;            // 帧持续时间
    int ret;
    //1 获取stream timebase
    AVRational tb = is->video_st->time_base; // 获取stream timebase
    //2 获取帧率,以便计算每帧picture的duration
    AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);


    if (!frame)
        return AVERROR(ENOMEM);

    for (;;) {
    
      // 循环取出视频解码的帧数据
        // 3 获取解码后的视频帧
        ret = get_video_frame(is, frame);
        if (ret < 0)
            goto the_end;   //解码结束, 什么时候会结束
        if (!ret)           //没有解码得到画面, 什么情况下会得不到解后的帧
            continue;
            // 4 计算帧持续时间和换算pts值为秒
            // 1/帧率 = duration 单位秒, 没有帧率时则设置为0, 有帧率帧计算出帧间隔
            duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){
    
    frame_rate.den, frame_rate.num}) : 0);
            // 根据AVStream timebase计算出pts值, 单位为秒
            pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            // 5 将解码后的视频帧插入队列
            ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
            // 6 释放frame对应的数据
            av_frame_unref(frame);

        if (ret < 0) // 返回值小于0则退出线程
            goto the_end;
    }
the_end:
    av_frame_free(&frame);
    return 0;
}

El código anterior es el código fuente en ffplay, pero eliminé la parte del filtro del código para una mejor experiencia de lectura.

Puedes ver en el código que el proceso es muy simple.

Obtiene principalmente el cuadro a través de get_video_frame, calcula los puntos reales y la duración y luego lo inserta en la cola de video del cuadro a través de queue_picture.

¿Por qué todavía necesitas usar queue_picture en lugar de insertarlo directamente?

Como queremos que el código sea altamente utilizable, encapsulamos AVFrame en la estructura Frame a través de la función queue_picture y colocamos la estructura Frame en la cola, de modo que los cuadros de video, audio y subtítulos se puedan almacenar usando una interfaz de cola.

Primero echemos un vistazo a la función get_video_frame:

static int get_video_frame(VideoState *is, AVFrame *frame)
{
    
    
    int got_picture;
    // 1. 获取解码后的视频帧
    if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0) {
    
    
        return -1; // 返回-1意味着要退出解码线程, 所以要分析decoder_decode_frame什么情况下返回-1
    }

    if (got_picture) {
    
    
        // 2. 分析获取到的该帧是否要drop掉, 该机制的目的是在放入帧队列前先drop掉过时的视频帧
        double dpts = NAN;

        if (frame->pts != AV_NOPTS_VALUE)
            dpts = av_q2d(is->video_st->time_base) * frame->pts;    //计算出秒为单位的pts

        frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);

        if (framedrop>0 || // 允许drop帧
            (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER))//非视频同步模式
        {
    
    
            if (frame->pts != AV_NOPTS_VALUE) {
    
     // pts值有效
                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) {
    
     // packet队列至少有1帧数据
                    is->frame_drops_early++;
                    printf("%s(%d) diff:%lfs, drop frame, drops:%d\n",
                           __FUNCTION__, __LINE__, diff, is->frame_drops_early);
                    av_frame_unref(frame);
                    got_picture = 0;
                }
            }
        }
    }

    return got_picture;
}

Después de leer el cuadro a través de decoder_decode_frame, se juzga si se debe descartar el cuadro. El principio es observar la diferencia de sincronización entre el cuadro de video y otros cuadros. get_master_clock significa obtener los pts de otros cuadros y luego compararlos con los pts. del cuadro de video actual. Se dice que el valor es diff y luego se usa la condición de juicio para determinar si se descarta. Esto se discutirá en detalle al sincronizar audio y video.

Al mirar el código, descubrí que se volvió a llamar a decoder_decode_frame para leer el marco. ¿Por qué?

Debido a que la función que realmente lee el marco es decoder_decode_frame, esta función controla el tipo de marco.

Echa un vistazo al código:

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    
    
    int ret = AVERROR(EAGAIN);

    for (;;) {
    
    
        AVPacket pkt;
        // 1. 流连续情况下获取解码后的帧
        if (d->queue->serial == d->pkt_serial) {
    
     // 1.1 先判断是否是同一播放序列的数据
            do {
    
    
                if (d->queue->abort_request)
                    return -1;  // 是否请求退出
                // 1.2. 获取解码帧
                switch (d->avctx->codec_type) {
    
    
                case AVMEDIA_TYPE_VIDEO:
                    ret = avcodec_receive_frame(d->avctx, frame);
                    //printf("frame pts:%ld, dts:%ld\n", frame->pts, frame->pkt_dts);
                    if (ret >= 0) {
    
    
                        if (decoder_reorder_pts == -1) {
    
    
                            frame->pts = frame->best_effort_timestamp;
                        } else if (!decoder_reorder_pts) {
    
    
                            frame->pts = frame->pkt_dts;
                        }
                    }
                    break;
                case AVMEDIA_TYPE_AUDIO:
                    ret = avcodec_receive_frame(d->avctx, frame);
                    if (ret >= 0) {
    
    
                        AVRational tb = (AVRational){
    
    1, frame->sample_rate};    //
                        if (frame->pts != AV_NOPTS_VALUE) {
    
    
                            // 如果frame->pts正常则先将其从pkt_timebase转成{1, frame->sample_rate}
                            // pkt_timebase实质就是stream->time_base
                            frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
                        }
                        else if (d->next_pts != AV_NOPTS_VALUE) {
    
    
                            // 如果frame->pts不正常则使用上一帧更新的next_pts和next_pts_tb
                            // 转成{1, frame->sample_rate}
                            frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);
                        }
                        if (frame->pts != AV_NOPTS_VALUE) {
    
    
                            // 根据当前帧的pts和nb_samples预估下一帧的pts
                            d->next_pts = frame->pts + frame->nb_samples;
                            d->next_pts_tb = tb; // 设置timebase
                        }
                    }
                    break;
                }

                // 1.3. 检查解码是否已经结束,解码结束返回0
                if (ret == AVERROR_EOF) {
    
    
                    d->finished = d->pkt_serial;
                    printf("avcodec_flush_buffers %s(%d)\n", __FUNCTION__, __LINE__);
                    avcodec_flush_buffers(d->avctx);
                    return 0;
                }
                // 1.4. 正常解码返回1
                if (ret >= 0)
                    return 1;
            } while (ret != AVERROR(EAGAIN));   // 1.5 没帧可读时ret返回EAGIN,需要继续送packet
        }

        // 2 获取一个packet,如果播放序列不一致(数据不连续)则过滤掉“过时”的packet
        do {
    
    
            // 2.1 如果没有数据可读则唤醒read_thread, 实际是continue_read_thread SDL_cond
            if (d->queue->nb_packets == 0)  // 没有数据可读
                SDL_CondSignal(d->empty_queue_cond);// 通知read_thread放入packet
            // 2.2 如果还有pending的packet则使用它
            if (d->packet_pending) {
    
    
                av_packet_move_ref(&pkt, &d->pkt);
                d->packet_pending = 0;
            } else {
    
    
                // 2.3 阻塞式读取packet
                if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
                    return -1;
            }
        } while (d->queue->serial != d->pkt_serial);// 如果不是同一播放序列(流不连续)则继续读取

        // 3 将packet送入解码器
        if (pkt.data == flush_pkt.data) {
    
    //
            // when seeking or when switching to a different stream
            avcodec_flush_buffers(d->avctx); //清空里面的缓存帧
            d->finished = 0;        // 重置为0
            d->next_pts = d->start_pts;     // 主要用在了audio
            d->next_pts_tb = d->start_pts_tb;// 主要用在了audio
        } else {
    
    
            if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
    
    
                int got_frame = 0;
                ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &pkt);
                if (ret < 0) {
    
    
                    ret = AVERROR(EAGAIN);
                } else {
    
    
                    if (got_frame && !pkt.data) {
    
    
                        d->packet_pending = 1;
                        av_packet_move_ref(&d->pkt, &pkt);
                    }
                    ret = got_frame ? 0 : (pkt.data ? AVERROR(EAGAIN) : AVERROR_EOF);
                }
            } else {
    
    
                if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {
    
    
                    av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
                    d->packet_pending = 1;
                    av_packet_move_ref(&d->pkt, &pkt);
                }
            }
            av_packet_unref(&pkt);	// 一定要自己去释放音视频数据
        }
    }
}

Esta función primero verifica si el número de serie de la cola de paquetes y el número de serie de decodificación son consistentes. Si siempre se distingue por el tipo de decodificador, obtenga la trama a través de avcodec_receive_frame. Si se obtiene la trama, regresará. Si no se obtiene la trama y no hay EOF, entonces envíe un paquete. Sin embargo, antes de eso, verificará si el número de serie de la cola de paquetes y el número de serie de decodificación son consistentes. Si son inconsistentes, el paquete se descartará hasta que sean consistentes, y luego el paquete se enviará. Si el envío es flush_pkt, se llamará avcodec_flush_buffers para borrar el caché. Si es un paquete de subtítulos, se llamará avcodec_decode_subtitle2. De lo contrario, se llamará avcodec_decode_subtitle2. Simplemente llame a avcodec_send_packet directamente.

static int queue_picture(VideoState *is, AVFrame *src_frame, double pts,
                         double duration, int64_t pos, int serial)
{
    
    
    Frame *vp;
    if (!(vp = frame_queue_peek_writable(&is->pictq))) // 检测队列是否有可写空间
        return -1;      // 请求退出则返回-1
    // 执行到这步说已经获取到了可写入的Frame
    vp->sar = src_frame->sample_aspect_ratio;
    vp->uploaded = 0;

    vp->width = src_frame->width;
    vp->height = src_frame->height;
    vp->format = src_frame->format;

    vp->pts = pts;
    vp->duration = duration;
    vp->pos = pos;
    vp->serial = serial;

    set_default_window_size(vp->width, vp->height, vp->sar);

    av_frame_move_ref(vp->frame, src_frame); // 将src中所有数据转移到dst中,并复位src。
    frame_queue_push(&is->pictq);   // 更新写索引位置
    return 0;
}

A partir de esta parte del código, podemos saber que el AVFrame leído se encapsula en un Marco, y luego los puntos y la duración se configuran en segundos, y luego se configuran algunos tamaños de ventana predeterminados y se colocan en la cola.

Esto finaliza el hilo de decodificación de vídeo~~~

3. Función de hilo de decodificación de audio

// 音频解码线程
static int audio_thread(void *arg)
{
    
    
    VideoState *is = arg;
    AVFrame *frame = av_frame_alloc();  // 分配解码帧
    Frame *af;
#if CONFIG_AVFILTER
    int last_serial = -1;
    int64_t dec_channel_layout;
    int reconfigure;
#endif
    int got_frame = 0;  // 是否读取到帧
    AVRational tb;      // timebase
    int ret = 0;

    if (!frame)
        return AVERROR(ENOMEM);

    do {
    
    
        // 1. 读取解码帧
        if ((got_frame = decoder_decode_frame(&is->auddec, frame, NULL)) < 0)
            goto the_end;

        if (got_frame) {
    
    
            tb = (AVRational){
    
    1, frame->sample_rate};   // 设置为sample_rate为timebase
                // 2. 获取可写Frame
                if (!(af = frame_queue_peek_writable(&is->sampq)))  // 获取可写帧
                    goto the_end;
                // 3. 设置Frame并放入FrameQueue
                af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
                af->pos = frame->pkt_pos;
                af->serial = is->auddec.pkt_serial;
                af->duration = av_q2d((AVRational){
    
    frame->nb_samples, frame->sample_rate});

                av_frame_move_ref(af->frame, frame);
                frame_queue_push(&is->sampq);

        }
    } while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);
the_end:
    av_frame_free(&frame);
    return ret;
}

El proceso de audio y el proceso de video son básicamente los mismos, pero un punto es ¿por qué aquí se usa tb = (AVRational){1, frame->sample_rate} al convertir pts y duración en segundos en lugar de usar stream->time_base?

La razón principal es que al procesar audio en decoder_decode_frame, time_base se convierte en (AVRational){1, frame->sample_rate}

                case AVMEDIA_TYPE_AUDIO:
                    ret = avcodec_receive_frame(d->avctx, frame);
                    if (ret >= 0) {
    
    
                        AVRational tb = (AVRational){
    
    1, frame->sample_rate};    //
                        if (frame->pts != AV_NOPTS_VALUE) {
    
    
                            // 如果frame->pts正常则先将其从pkt_timebase转成{1, frame->sample_rate}
                            // pkt_timebase实质就是stream->time_base
                            frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
                        }
                        else if (d->next_pts != AV_NOPTS_VALUE) {
    
    
                            // 如果frame->pts不正常则使用上一帧更新的next_pts和next_pts_tb
                            // 转成{1, frame->sample_rate}
                            frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);
                        }
                        if (frame->pts != AV_NOPTS_VALUE) {
    
    
                            // 根据当前帧的pts和nb_samples预估下一帧的pts
                            d->next_pts = frame->pts + frame->nb_samples;
                            d->next_pts_tb = tb; // 设置timebase
                        }
                    }
                    break;

Supongo que te gusta

Origin blog.csdn.net/m0_60565784/article/details/131790543
Recomendado
Clasificación