ffplay源码分析__解码函数decoder_decode_frame

 前言

decoder_decode_frame函数主要功能是用来解码,在video_thread线程和audio_thread线程中被调用。这个函数有很多判断序列号的地方,序列号主要是跳转时用到的,跳转时PacketQueue的序列号会变,所以先说下序列号。

一、PacketQueue的serial,MyAVPacketList的serial,Decoder的pkt_serial

1、PacketQueue的serial

typedef struct PacketQueue {
    AVFifoBuffer *pkt_list; //ffmpeg实现的先进先出的队列
    int nb_packets; // 队列中AVPacket数量
    int size; //队列占用内存大小
    int64_t duration; // 队列所有数据包的持续时间
    int abort_request; //是否结束
    int serial; //队列序列号
    SDL_mutex *mutex; //队列锁
    SDL_cond *cond;//队列条件变量
} PacketQueue;

PacketQueue是所有序列号的源头,其他序列号是直接或间接被PacketQueue影响的。跳转时,PacketQueue的序列号会增加,代表一个新的流。

在stream_open()中调用packet_queue_init(),PacketQueue序列号初始化为0。

接下来在read_thread线程-->stream_component_open()-->decoder_start()-->packet_queue_start()中,运行q->serial++,此时PacketQueue的serial为1。

2、MyAVPacketList的serial

typedef struct MyAVPacketList {
    AVPacket *pkt;
    int serial;
} MyAVPacketList;

由于AVPacket信息中不带序列号,所以用MyAVPacketList结构体封装了一下序列号信息。

MyAVPacketList序列号第一次初始化是在read_thread线程,从文件读取Avpacket数据向PacketQueue队列插入数据时赋值的。调用顺序是:read_thread线程-->packet_queue_put()-->packet_queue_put_private()。

在函数packet_queue_put_private中,MyAVPacketList的序列号第一次赋值为PacketQueue的序列号。(可以把PacketQueue看成容器,每一个MyAVPacketList都是容器的一个元素,容器的序列号决定了每一个元素的序列号)。

3、Decoder的pkt_serial

Decoder的pkt_serial在调用decoder_init时初始化的,初始为-1。调用顺序:read_thread线程-->stream_component_open()-->decoder_init()。

二、decoder_decode_frame

for (;;) {
        //解码成功return返回1
		if (d->queue->serial == d->pkt_serial) {...}
           
		//从队列中取出Avpacket数据,并判断Avpacket的序列号和Decoder的序列号是否一致,
		//不一致继续取,直到序列号一致为止
		do {...} while (1);
		
		//不断的向解码器输送数据,用于解码
        if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {...} 
        else {
            if (avcodec_send_packet(d->avctx, d->pkt) == AVERROR(EAGAIN)) {
                ...
            } else {
                av_packet_unref(d->pkt);
            }
        }
    }

decoder_decode_frame内部主要是for循环,不断从PacketQueue队列中get数据,用avcodec_send_packet函数向解码器发送数据,并用avcodec_receive_frame获取解码后的数据,解码成功直接return返回。可以分为三部分。

1、从解码器获取数据

if (d->queue->serial == d->pkt_serial) {
    do {
        if (d->queue->abort_request)
            return -1;

        switch (d->avctx->codec_type) {
            case AVMEDIA_TYPE_VIDEO:
                ret = avcodec_receive_frame(d->avctx, frame);
                if (ret >= 0) {
                    //static int decoder_reorder_pts = -1;
                    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 = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
                    else if (d->next_pts != AV_NOPTS_VALUE)
                        frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);
                    if (frame->pts != AV_NOPTS_VALUE) {
                        d->next_pts = frame->pts + frame->nb_samples;
                        d->next_pts_tb = tb;
                    }
                }
                break;
        }
        if (ret == AVERROR_EOF) {
            d->finished = d->pkt_serial;
            avcodec_flush_buffers(d->avctx);
            return 0;
        }
        if (ret >= 0)
            return 1;
    } while (ret != AVERROR(EAGAIN));
}

由于PacketQueue的serial的初始值(为1)和Decoder的pkt_serial初始值(-1)不一致,第一次运行时,if语句内部的代码没有运行。

if语句内的代码主要功能是:不论是视频还是音频,都用avcodec_receive_frame函数获取解码后的Avframe数据,如果获取成功直接跳出for循环并返回1。

获取到解码数据后,对于视频做了pts矫正。frame->pts=frame->best_effort_timestamp;

//static int decoder_reorder_pts = -1;
if (decoder_reorder_pts == -1) { //初始值为-1
    frame->pts = frame->best_effort_timestamp;
} 
/**
*使用各种启发式估计的帧时间戳,以流时基为单位
*-编码:未使用
*-解码:由libavcodec设置,由用户读取。
*/
int64_t best_effort_timestamp;

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

2、从PacketQueue队列中get数据

第二部分代码如下:

do {
    if (d->queue->nb_packets == 0)
        SDL_CondSignal(d->empty_queue_cond);
    if (d->packet_pending) { //初始化为0
        d->packet_pending = 0;
    } else {
        int old_serial = d->pkt_serial;
        if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
            return -1;
        if (old_serial != d->pkt_serial) {
            avcodec_flush_buffers(d->avctx);
            d->finished = 0;
            d->next_pts = d->start_pts;
            d->next_pts_tb = d->start_pts_tb;
        }
    }
    if (d->queue->serial == d->pkt_serial)
        break;
    av_packet_unref(d->pkt);
} while (1);

第二部分是在一个do{}while(1)循环中。d->empty_queue_cond是在decoder_init函数中赋值的,d->empty_queue_cond是is->continue_read_thread,

if (d->queue->nb_packets == 0)
        SDL_CondSignal(d->empty_queue_cond);

先判断队列中是否有数据,如果没有用SDL_CondSignal函数向read_thread线程发信号,防止read_thread线程在SDL_CondWaitTimeout处等待,就是唤醒read_thread线程,立即读取数据。

接下来用packet_queue_get函数从队列取数据。

/* return < 0 if aborted, 0 if no packet and > 0 if packet.  */
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
{
    MyAVPacketList pkt1;
    int ret;

    SDL_LockMutex(q->mutex);

    for (;;) {
        if (q->abort_request) {
            ret = -1;
            break;
        }

        if (av_fifo_size(q->pkt_list) >= sizeof(pkt1)) {
            av_fifo_generic_read(q->pkt_list, &pkt1, sizeof(pkt1), NULL);
            q->nb_packets--;
            q->size -= pkt1.pkt->size + sizeof(pkt1);
            q->duration -= pkt1.pkt->duration;
            av_packet_move_ref(pkt, pkt1.pkt);
            if (serial)
                *serial = pkt1.serial;
            av_packet_free(&pkt1.pkt);
            ret = 1;
            break;
        } else if (!block) {
            ret = 0;
            break;
        } else {
            SDL_CondWait(q->cond, q->mutex);
        }
    }
    SDL_UnlockMutex(q->mutex);
    return ret;
}

在packet_queue_get函数内部,首先判断队列中是否有数据,有则用av_fifo_generic_read函数把读取的数据保存到pkt1中,再用av_packet_move_ref()把pkt1的数据拷贝到pkt中,并用            av_packet_free函数释放ptk1的内存。

如果队列中没有数据则一直在SDL_CondWait(q->cond, q->mutex)处等待,直到read_thread线程中,packet_queue_put_private函数调用SDL_CondSignal()发送信号为止。即read_thread线程读取到数据后,唤醒解码线程立即解码。packet_queue_put_private()调用SDL_CondSignal()的代码如下:

 Decoder的pkt_serial通过指针传参,赋值为pkt1的序列号,ptk1的序列号等于PacketQueue的序列号,所以此时Decoder的pkt_serial也等于PacketQueue的序列号。

if (serial)
    *serial = pkt1.serial;

接下来有句代码如下:

if (old_serial != d->pkt_serial) {
    avcodec_flush_buffers(d->avctx);
    d->finished = 0;
    d->next_pts = d->start_pts;
    d->next_pts_tb = d->start_pts_tb;
}

old_serial是上一次取的MyAVPacketList的序列号,对于第一次运行来说,old_serial是d->pkt_serial的初始值-1,所以这里会有一次刷新解码器缓存的操作,当读取到第一个MyAVPacketList后,Decoder的pkt_serial等于PacketQueue的序列号,即d->queue->serial 和 d->pkt_serial相等,所以下面的代码会跳出do{}while(1)循环。

if (d->queue->serial == d->pkt_serial)
    break;

 接下来运行第三部分。

3、向解码器输送数据

if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
    省略...
} else {
    if (avcodec_send_packet(d->avctx, d->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;
    } else {
        av_packet_unref(d->pkt);
    }
}

略去字幕部分,代码功能主要是用avcodec_send_packet函数向解码器发送数据。由于在for循环中,此时PacketQueue的serial(为1)和Decoder的serial(为1)已经一致了,所以第一部分的功能开始运行,用avcodec_receive_frame函数获取解码后的数据。

如果获取到解码数据,跳出for循环直接返回。如果没有获取到解码数据,就一直在for循环中,不断从队列中get数据向解码器发送,直到获取到解码数据为止。

有可能向解码器输送多个Avpacket后,才会获取到解码数据。

4、函数返回值

当函数avcodec_receive_frame获取到解码数据后,程序跳出for循环并返回1

当解码器没有数据可读时返回0

出现解码错误时返回负数

三、总结:

decoder_decode_frame函数的主要功能是:判断从PacketQueue取出的MyAVpacketList序列号是否和Decoder的序列号一致,如果两者不一致,就一直从队列中取,直到取到的序列号一致为止。如果两者序列号一致,就用avcodec_send_packet函数向解码器输送数据,然后通过avcodec_receive_frame从解码器获取解码后的数据返回。

正确取到解码数据后返回1,读取到解码器末尾时返回0,其余返回负数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值