ffplay解码线程分析

转自: https://zhuanlan.zhihu.com/p/43948483

ffplay的解码线程独立于读线程,并且每种类型的流(AVStream)都有其各自的解码线程,如video_thread用于解码video stream,audio_thread用于解码audio stream,subtitle_thread用于解码subtitle stream。

为方便阅读,先列一张表格,梳理各个变量、函数名称。

其中PacketQueue用于存放从read_thread取到的各自播放时间内的AVPacket。FrameQueue用于存放各自解码后的AVFrame。Clock用于同步音视频。解码线程负责将PacketQueue数据解码为AVFrame,并存入FrameQueue。

对于不同流,其解码过程大同小异,我们先看下video_thead。

对于滤镜部分,先不做分析 ,简化后的代码如下(为方便阅读,以下代码还省略了部分局部变量的声明):

static int video_thread(void *arg)
{
    AVRational tb = is->video_st->time_base;
    AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);

    for (;;) {
        ret = get_video_frame(is, frame); //解码获取一帧视频画面
        if (ret < 0)//解码结束
            goto the_end;
        if (!ret)//没有解码得到画面
            continue;

        duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);//用帧率估计帧时长
        pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);//将pts转化为秒为单位
        ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);//将解码后的帧存入FrameQueue
        av_frame_unref(frame);

        if (ret < 0)
            goto the_end;
    }
 the_end:

    av_frame_free(&frame);
    return 0;
}

线程的总体流程很清晰:

  1. 调用get_video_frame解码一帧图像
  2. “计算”时长和pts
  3. 调用queue_picture放入FrameQueue

get_video_frame简化如下:

static int get_video_frame(VideoState *is, AVFrame *frame)
{
    int got_picture;

    if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0)
        return -1;

    if (got_picture) {
        frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);
        //……
    }

    return got_picture;
}

主要是调用decoder_decode_frame解码。

被简化的部分主要是针对丢帧的一个处理:

//if (got_picture) {
        double dpts = NAN;
        if (frame->pts != AV_NOPTS_VALUE)
            dpts = av_q2d(is->video_st->time_base) * frame->pts;
        if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
            if (frame->pts != AV_NOPTS_VALUE) {
                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) {
                        av_log(NULL, AV_LOG_ERROR, "drop early: %d\n", is->videoq.nb_packets);
                    is->frame_drops_early++;
                    av_frame_unref(frame);
                    got_picture = 0;
                }
            }
        }
//}

丢帧的主要条件是diff - is->frame_last_filter_delay < 0frame_last_filter_delay与滤镜有关,可以先忽略,也就是diff < 0的时候丢帧——pts < get_master_clock(is)的时候丢帧。

控制是否丢帧的开关变量是framedrop,为1,则始终判断是否丢帧;为0,则始终不丢帧;为-1(默认值),则在主时钟不是video的时候,判断是否丢帧。

接下来看下真正解码的过程——decoder_decode_frame,这个函数也包含了对audio和subtitle的解码,同样,先看简化后的主干代码:

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    for (;;) {
        //1. 流连续的情况下,不断调用avcodec_receive_frame获取解码后的frame
        if (d->queue->serial == d->pkt_serial) {
            do {
                ret = avcodec_receive_frame(d->avctx, frame);
                if (ret == AVERROR_EOF) {
                    return 0;
                }
                if (ret >= 0)
                    return 1;
            } while (ret != AVERROR(EAGAIN));
        }

        //2. 取一个packet,顺带过滤“过时”的packet
        do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
                return -1;
        } while (d->queue->serial != d->pkt_serial);

        //3. 将packet送入解码器
        avcodec_send_packet(d->avctx, &pkt);
    }
}

decoder_decode_frame的主干代码是一个循环,要拿到一帧解码数据,或解码出错、文件结束,才会返回。

循环内可以分解为3个步骤:

  1. 流连续的情况下,不断调用avcodec_receive_frame获取解码后的framed->queue就是video PacketQueue(videoq),d->pkt_serial是最近一次取的Packet的序列号。在判断完d->queue->serial == d->pkt_serial确保流连续后,循环调用avcodec_receive_frame,有取到帧,就返回。(即使还没送入新的Packet,这是为了兼容一个Packet可以解出多个Frame的情况)
  2. 取一个packet,顺带过滤“过时”的packet。主要阻塞调用packet_queue_get(参考PacketQueue的分析:https://zhuanlan.zhihu.com/p/43295650)。另外,会在PacketQueue为空时,发送empty_queue_cond条件信号,通知读线程继续读数据。(empty_queue_cond就是continue_read_thread,可以参考read线程的分析,查看读线程何时会等待该条件量:https://zhuanlan.zhihu.com/p/43672062
  3. 将packet送入解码器
avcodec_decode_video2已经被标记为废弃。
@deprecated Use avcodec_send_packet() and avcodec_receive_frame().
建议使用这两个函数进行解码。

在省略代码中有一个packet_pending的概念,用于在send失败时重新发送:

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

如果avcodec_send_packet返回EAGAIN,则把当前pkt存入d->pkt,然后置标志位packet_pending为1。

packet_pending的读取在取Packet时:

do {
    if (d->queue->nb_packets == 0)
        SDL_CondSignal(d->empty_queue_cond);
    //如果有待重发的pkt,则先取待重发的pkt,否则从队列中取一个pkt
    if (d->packet_pending) {
        av_packet_move_ref(&pkt, &d->pkt);
        d->packet_pending = 0;
    } else {
        if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
            return -1;
    }
} while (d->queue->serial != d->pkt_serial);

省略代码中还有针对flush_pkt的处理:

if (pkt.data == flush_pkt.data) {
    avcodec_flush_buffers(d->avctx);
    d->finished = 0;
    d->next_pts = d->start_pts;
    d->next_pts_tb = d->start_pts_tb;
}

了解过PacketQueue的代码,我们知道在往PacketQueue送入一个flush_pkt后,PacketQueue的serial值会加1,而送入的flush_pkt和PacketQueue的serial值保持一致。所以如果有“过时”Packet,过滤后,取到的第一个pkt将是flush_pkt。

根据api要求,此时需要调用avcodec_flush_buffers

上面,我们就分析完了video_thread中关键的get_video_frame函数,根据所分析的代码,已经可以取到正确解码后的一帧数据了。接下来就要把这一帧放入FrameQueue:

duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
av_frame_unref(frame);

主要调用queue_picture

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;

    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);
    frame_queue_push(&is->pictq);
    return 0;
}

queue_picture的代码很直白,先frame_queue_peek_writable取FrameQueue的当前写节点,然后把该拷贝的拷贝给节点(struct Frame)保存,然后frame_queue_push,“push”节点到队列中。唯一需要关注的是,AVFrame的拷贝是通过av_frame_move_ref实现的,所以拷贝后src_frame就是无效的了。

关于FrameQueue的操作,和分析,见: https://zhuanlan.zhihu.com/p/43564980

至此,video的解码过程,除fitler部分,基本都分析完了。

audio的解码过程,不考虑filter部分,与video的解码几乎一样。就不重复分析了。

subtitle的解码过程略有不同,关注下不同的地方。

static int subtitle_thread(void *arg)
{
    VideoState *is = arg;
    Frame *sp;
    int got_subtitle;
    double pts;

    for (;;) {
        //注意这里是先frame_queue_peek_writable再decoder_decode_frame
        if (!(sp = frame_queue_peek_writable(&is->subpq)))
            return 0;

        if ((got_subtitle = decoder_decode_frame(&is->subdec, NULL, &sp->sub)) < 0)
            break;

        pts = 0;

        if (got_subtitle && sp->sub.format == 0) {
            if (sp->sub.pts != AV_NOPTS_VALUE)
                pts = sp->sub.pts / (double)AV_TIME_BASE;
            sp->pts = pts;
            sp->serial = is->subdec.pkt_serial;
            sp->width = is->subdec.avctx->width;
            sp->height = is->subdec.avctx->height;
            sp->uploaded = 0;

            /* now we can update the picture count */
            frame_queue_push(&is->subpq);
        } else if (got_subtitle) {
            avsubtitle_free(&sp->sub);
        }
    }
    return 0;
}

主要流程是:

  1. frame_queue_peek_writable先取一个可写的帧节点
  2. decoder_decode_frame解码一帧字幕

我们会发现subtitle_thread中的主流程顺序和video_thread是相反的,video_thread是先解码后取可写的帧节点,然后写入。

这里,我的猜测是:由于字幕的FrameQueue消耗比较慢(比如常见的一句话对应一帧),这就导致FrameQueue经常是处于堆满的状态。如果先解码,再调用frame_queue_peek_writable,大概率会被阻塞,期间如果发生seek,则解码的这一帧就浪费了。与其先解码再等锁,不如先等锁再解码。当然,这只是一个猜测,具体原因,彻底搞明白了再更新。

解码字幕的时候还走avcodec_decode_subtitle2,对应的decoder_decode_frame简化为:

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    int ret = AVERROR(EAGAIN);
    for (;;) {
        //1. 判断返回值
        if (d->queue->serial == d->pkt_serial) {
            do {
                if (ret == AVERROR_EOF) {
                    return 0;
                }
                if (ret >= 0)
                    return 1;
            } while (ret != AVERROR(EAGAIN));
        }

        //2. 取pkt
        packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial);

        //3.解码
        ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &pkt);
        if (ret < 0) {
            ret = AVERROR(EAGAIN);
        } else {
            if (got_frame && !pkt.data) {//如果是null_packet,且置为pending,下次继续
               d->packet_pending = 1;
               av_packet_move_ref(&d->pkt, &pkt);
            }
            ret = got_frame ? 0 : (pkt.data ? AVERROR(EAGAIN) : AVERROR_EOF);
        }
    }
}

因为和audio/video共用一个函数,而又用的avcodec_decode_subtitle2(相比send/receive只一个函数完成解码)解字幕,所以代码顺序看起来会有些奇怪:

  1. 判断返回值
  2. 取pkt
  3. 解码

步骤1,在步骤3后更合适,但因为是循环体内,所以,也是可以正确运行的。

对于avcodec_decode_subtitle2这里有一个null_packet的处理,null_packet一般在文件读取结束后发出。

static int packet_queue_put_nullpacket(PacketQueue *q, int stream_index)
{
AVPacket pkt1, *pkt = &pkt1; av_init_packet(pkt); pkt->data = NULL; pkt->size = 0; pkt->stream_index = stream_index; return packet_queue_put(q, pkt);
}

在解码字幕时,null_packet用于最后将解码器内剩余的解码数据取出。如果还能取到数据(got_frame == 1),则将null_packet暂存d->pkt,置packet_pending,下次继续取。直到avcodec_decode_subtitle2返回got_frame == 0.

关于这部分的逻辑,在avcodec_decode_subtitle2的注释中有说明:

/* Some decoders (those marked with AV_CODEC_CAP_DELAY) have a delay between input
 * and output. This means that for some packets they will not immediately
 * produce decoded output and need to be flushed at the end of decoding to get
 * all the decoded data. Flushing is done by calling this function with packets
 * with avpkt->data set to NULL and avpkt->size set to 0 until it stops
 * returning subtitles. It is safe to flush even those decoders that are not
 * marked with AV_CODEC_CAP_DELAY, then no subtitles will be returned.
 */

至此,关于ffplay的解码部分就分析完了。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值