FFmpeg之ffplay源码各个结构体中的serial字段分析

我的ffmpeg开源项目地址Viktor_ffmpeg
该项目主要以学习ffmpeg为主,代码中将ffplay摘抄出来(主要去除了sdl,使用c++的std),用自己的方式实现(面向对象)。
从该项目中你可以学到
1.如何使用ffmpeg解码视频并播放
2.如何多个视频串行播放
该项目是以剪映的功能为目标

ffplay源码中多处用到serial概念,用于区分是否连续的数据。

serial主要存在于如下几个ffplay的结构体中

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

typedef struct PacketQueue {
    MyAVPacketList *first_pkt, *last_pkt;
    .....
    int serial;
    .....
} PacketQueue;

typedef struct Decoder {
    .....
    int pkt_serial;
    .....
} Decoder;

typedef struct Clock {
    .....
    int serial;           /* clock is based on a packet with this serial */
    .....
    int *queue_serial;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;


typedef struct Frame {
    .....
    int serial;
    double pts;           /* presentation timestamp for the frame 帧的表示时间戳,即相对总时长的偏移位置*/
    double duration;      /* estimated duration of the frame 帧的估计持续时间*/
    int64_t pos;          /* byte position of the frame in the input file 帧的字节位置*/
    .....
} Frame;

typedef struct VideoState {
    .....
    int audio_clock_serial;
    .....
}

首先分析PacketQueue和MyAVPacketList中的serial

所有结构体中的serial的赋值、更改都是由这两个结构体中的serial更改开始或者引起的。

PacketQueue.serial == MyAVPacketList.serial

packet_queue_put_private方法中,保证二者相等,并且二者的值的改变永远在该方法中。

static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
    MyAVPacketList *pkt1;
    .....
    //如果放入的是flush_pkt,需要增加 队列的序列号,以区分不连续的两段数据
    if (pkt == &flush_pkt)
        q->serial++;
    pkt1->serial = q->serial;//用队列的序列号 标记节点
    .....
    return 0;
}
什么情况下PacketQueue.serial 和MyAVPacketList.serial的值会更改??

static void packet_queue_start(PacketQueue *q)
{
     .....
    /**
     这里放入了一个flush_pkt
     fush_pkt定义是static AVPacket flush_pkt;
     是一个特殊的packet,主要用来作为非连续的两端数据的“分界”标记
     */
    packet_queue_put_private(q, &flush_pkt);
    .....
}


static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{
    int ret;
    .....
    ret = packet_queue_put_private(q, pkt);
    .....
    if (pkt != &flush_pkt && ret < 0)
        av_packet_unref(pkt);
    return ret;
}

这2个方法packet_queue_startpacket_queue_put分别在如下情况调用
packet_queue_start:
在stream_component_open中的switch case语句中会依次打开video,auido,subtitle开始解码:decoder_start---->packet_queue_start
即开始解码时,会往各自的PacketQueue中放入一个flush_pkt

packet_queue_put:
packet_queue_put方法被调用的地方很多,主要看read_thread方法中后面的for语句中seek相关操作。在seek成功后,会先调用packet_queue_flush清除PacketQueue的缓存,然后调用packet_queue_put往PacketQueue中放入一个flush_pkt,标记发生一次seek

if (is->seek_req) {
    .....
    ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR,
                "%s: error while seeking\n", is->ic->url);
    } else {
        /*
        1.清除PacketQueue的缓存,并放入一个flush_pkt。放入的flush_pkt可以让PacketQueue的serial增1,以区分seek前后的数据
        2.同步外部时钟
        */
        if (is->audio_stream >= 0) {
            packet_queue_flush(&is->audioq);
            packet_queue_put(&is->audioq, &flush_pkt);
        }
        if (is->subtitle_stream >= 0) {
            packet_queue_flush(&is->subtitleq);
            packet_queue_put(&is->subtitleq, &flush_pkt);
        }
        if (is->video_stream >= 0) {
            packet_queue_flush(&is->videoq);
            packet_queue_put(&is->videoq, &flush_pkt);
        }
        .....
    }
    .....
}

分析到这里可以看出:
serial值的改变都是因为放入一个flush_pkt导致!
放入flush_pkt时机:
1.一个是在开始解码时,会往各自的PacketQueue中放入一个flush_pkt,导致serial初次更改,此时serial=1;
2.其他的改变都是在seek成功后清除了PacketQueue数据后放入一个flush_pkt,并且都是累加,例如运行起来后,seek了一次,这时serial=2

Decoder.pkt_serial

Decoder.pkt_serial的赋值在packet_queue_get方法中
decoder_decode_frame--->do while(1)---->if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial)
在video,audio,subtitle3个解码线程各自解码时,即decoder_decode_frame方法中的do while(1)语句中,从各自的PacketQueue中获取AVPacket时给Decoder.pkt_serial赋值的,

do {     
    if (d->queue->nb_packets == 0)
        .....
    if (d->packet_pending) {
        
    } else {
        if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
            return -1;
    }
    if (d->queue->serial == d->pkt_serial)
        break;
    av_packet_unref(&pkt);
} while (1);
----------------------------
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
{
    MyAVPacketList *pkt1;
    .....
    for (;;) {
            .....
            if (serial)//如果需要输出serial,把serial输出
                *serial = pkt1->serial;
            av_free(pkt1);
            .....
        } else if (!block) {
           .....
        } else {
            .....
        }
    }
    .....
    return ret;
}

那么Decoder的pkt_serial的作用,或者pkt_serial都用在什么地方呢?

1.在decoder_decode_frame方法中比较是否一个连续的流"d->queue->serial == d->pkt_serial"
2.在decoder_decode_frame方法中取一个packet的do while中packet_queue_get方法中赋值,然后比较是否同一个流之后跳出do while
3.在get_video_frame方法中,丢帧处理时做if条件判断使用is->viddec.pkt_serial == is->vidclk.serial
4.在audio_thread中给Frame.serial赋值 af->serial = is->auddec.pkt_serial
5.在vidoe_thread的queue_picture方法中给Frame.serial赋值 vp->serial = serial
6.在subtitle_thread中给Frame.serial赋值 sp->serial = is->subdec.pkt_serial
7.在decoder_decode_frame方法中给Decoder.finished赋值

关于Decoder.finished赋值主要也在decoder_decode_frame方法中,
一次是解码完成时:d->finished = d->pkt_serial
一次是:d->finished = 0(过滤掉flush_pkt这种无效数据)

Decoder.finished主要作用在read_thread方法中判断是否播放完成,决定是要循环播放还是退出,
例如is->auddec.finished == is->audioq.serial
关于Decoder.finished在read_thread方法判断是否播放完成,可以如下追踪finished的来源decoder_decode_frame-->packet_queue_get(PacketQueue.serial-->MyAVPacketList.serial-->Decoder.pkt_serial-->Decoder.finished) -->read_thread-->is->auddec.finished == is->audioq.serial && is->viddec.finished == is->videoq.serial)

Frame.serial

来自于:
1.视频-->video_thread解码---->queue_picture入队----->is->viddec.pkt_serial----->vp->serial = serial
2.音频-->audio_thread解码---->frame入队列---->af->serial = is->auddec.pkt_serial'
3.文字-->subtitle_thread解码---->frame入队列---->sp->serial = is->subdec.pkt_serial
即Frame的serial来自音频,视频,文字解码时赋值,并且都是来自各自的解码Decoder.pkt_serial:
PacketQueue.serial-->MyAVPacketList.serial-->Decoder.pkt_serial-->Frame.serial

作用:
1.frame_queue_last_pos 条件判断是否同流
2.video_refresh 中:
----a.vp->serial != is->videoq.serial条件判断,标记一个节点已经被读过
----b.lastvp->serial != vp->serial 条件判断,更新VideoState.frame_timer
----c.update_video_pts 用于给视频时钟和外部时钟更新serial(见Clock.serial分析)
----d.vp_duration 条件判断是否同流
----e.字幕展示逻辑中,条件判断是否同流
3.sdl_audio_callback-->audio_decode_frame-->do while中做条件判断4.sdl_audio_callback-->audio_decode_frame---->is->audio_clock_serial = af->serial给audio_clock_serial赋值
其实最主要作用就是更新Clock.serial

Clock.serial和Clock.queue_serial

在set_clock_at中赋值:set_clock-->set_clock_at
set_clock方法主要被调用关键点:
1.read_thread中seek成功后给extclk.serial=0(即seek成功后将同步外部时钟extclk.serial清0)
2.set_clock_speed-->set_clock方法改变速度时,给外部时钟extclk.serial赋值,即自己给自己赋值
3.stream_toggle_pause-->set_clock方法暂停播放时,给vidclk,extclk各自的serial赋值,即自己给自己赋值
4.update_video_pts-->set_clock和sync_clock_to_slave(video_refresh视频)
sdl_audio_callback->set_clock和sync_clock_to_slave(sdl_audio_callback音频)sync_clock_to_slave-->set_clock即音频,视频各自的时钟同步到extclk

作用:
1.get_clock中用于判断Clock中的serial是否同一个流(*c->queue_serial != c->serial)
2.在get_video_frame方法中,丢帧处理时做if条件判断使用is->viddec.pkt_serial == is->vidclk.serial

Clock.queue_serial
(在init_clock中赋值,来自于PacketQueue.serial的地址,即指针,所以在packet_queue_put_private中给
PacketQueue.serial赋值,即给Clock.queue_serial赋值
作用:get_clock中用于判断Clock中的serial是否同一个流(*c->queue_serial != c->serial)

VideoState.audio_clock_serial

audio_clock_serial感觉比较鸡肋,这个值就是给音频重采样完成后同步audclk中的serial中使用的(即调用set_clock_at方法)。
sdl_audio_callback-->audio_decode_frame
1.在audio_decode_frame中使用Frame结构体中的serial给audio_clock_serial赋值(is->audio_clock_serial = af->serial);
2.回到sdl_audio_callback后去同步audclk时再用audio_clock_serial给audclk.serial赋值。

视频同步时钟直接调用update_video_pts,使用的serial就是来自Frame.serial

而音频同步却绕了一步,将Frame.serial赋值给audio_clock_serial,再在sdl_audio_callback中使用audio_clock_serial去同步时钟。
这里考虑的原因可能是sdl_audio_callback方法是opensles中调用的,在sdl_audio_callback方法中获取不到Frame,所以采用这种方式。

整理!!

先让我们整理下整个流程,ffplay的main函数是入口函数!
main-->stream_open
stream_open会启动一个read_thread线程,
在read_thread中干了2件大事

1.启动3个流(video,audio,subtitle)stream_component_open。
2.for循环去读取AVPacket,将AVPacket放入3个流各自的PacketQueue中。
接着3个流各自的stream_component_open中又启动了线程:
video_thread,audio_thread,subtitle_thread
到这里为止,共启动了4个线程
1.read_thread(主要读取AVPacket,存入3个流的PacketQueue中)
2.video_thread--->get_video_frame--->decoder_decode_frame
3.audio_thread--->decoder_decode_frame
4.subtitle_thread--->decoder_decode_frame

video_thread,audio_thread,subtitle_thread 3个线程都调用了decoder_decode_frame方法去解码,
就是将read_thread中for循环读取的AVPacket取出去解码,然后将解码后的Frame放入3个流各自的FrameQueue中

关于展示画面和播放声音

main-->event_loop-->refresh_loop_wait_event--->video_refresh
在video_refresh中不停的去视频对应的FrameQueue中获取AVFrame去显示

至于声音播放是在音频的stream_component_open--->audio_open---->打开了opensles去播放音频,opensles会不停的调用sdl_audio_callback
sdl_audio_callback--->audio_decode_frame(重采样)

image.png

整个流程主要是这些,回到主题,分析serial!
在3个流的stream_component_open中,都有

decoder_init--->d->queue = queue和d->pkt_serial = -1
decoder_start--->packet_queue_start:会将一个flush_pkt放入到视频PacketQueue中,且这时PacketQueue.serial == MyAVPacketList.serial == 1

(注意这里我们只分析视频流,另外2个流几乎相同,暂不分析)
此时PacketQueue.serial == MyAVPacketList.serial == 1,Decoder.pkt_serial = -1
当read_thread正常启动,正常读取一个AVPacket,假设为avPacket1存入PacketQueue中(记住此时PacketQueue.first_pkt==flush_pkt,PacketQueue.last_pkt==avPacket1)
目前为止
PacketQueue.serial == MyAVPacketList.serial == 1,Decoder.pkt_serial = -1
PacketQueue中的first_pkt==flush_pkt和last_pkt==avPacket1两个MyAVPacketList的serial都是1

同时video_thread运行
video_thread中的for循环启动--->get_video_frame--->decoder_decode_frame

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    int ret = AVERROR(EAGAIN);
    for (;;) {
        AVPacket pkt;
        .....
        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) {
                            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:
                        ....
                        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));
        }

        do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            if (d->packet_pending) {//如果有待重发的pkt,则先取待重发的pkt
                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;
            }
            if (d->queue->serial == d->pkt_serial)
                break;
            av_packet_unref(&pkt);
        } while (1);

        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;
        } else {
            if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
                .....
            } else {
                if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {
                    d->packet_pending = 1;
                    av_packet_move_ref(&d->pkt, &pkt);
                }
            }
            av_packet_unref(&pkt);
        }
    }
}

decoder_decode_frame方法:
第一次进入该方法,for循环开始,
1.if (d->queue->serial == d->pkt_serial)不满足,因为上面的PacketQueue.serial=1,Decoder.pkt_serial = -1
2.接着走do while(1)--->packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial),取出数据,该数据为flush_pkt,重点!!!这时在packet_queue_get方法中Decoder.pkt_serial已经为1了
3.继续进入下面的if判断,因为是flush_pkt,所以执行avcodec_flush_buffers,d->finished = 0等操作

第2次for循环
1.if (d->queue->serial == d->pkt_serial)满足,因为此时PacketQueue.serial=1,Decoder.pkt_serial = 1
2.进入do while (ret != AVERROR(EAGAIN))---->avcodec_receive_frame,去获取解码后的数据,这时ret==AVERROR(EAGAIN),可以看下avcodec_receive_frame方法返回值说明
,AVERROR(EAGAIN)说明(output is not available in this state - user must try to send new input,大致意思就是这个状态下表明输入端还么有数据,就是avcodec_send_packet还没有输入数据)
3.跳出do while(ret != AVERROR(EAGAIN)),接着执行下面的do while (1)获取一个AVPacket,此时是avPacket1
4.执行下面的if判断,走else,然后将数据通过avcodec_send_packet输入
(这里多说一句,如果avcodec_send_packet返回AVERROR(EAGAIN),说明输出端应该及时调用avcodec_receive_frame读取数据,然后再重新发送一次该AVPacket。
可以看到,d->packet_pending = 1,然后do while(1)逻辑中
if (d->packet_pending)也是将数据再次赋值,而不是从packet_queue_get中获取)

第3次for循环
1.if (d->queue->serial == d->pkt_serial)满足,因为此时PacketQueue.serial=1,Decoder.pkt_serial = 1
2.进入do while (ret != AVERROR(EAGAIN))---->avcodec_receive_frame,去获取解码后的数据,此时正常逻辑ret==0,跳出整个for循环,decoder_decode_frame方法返回

综上decoder_decode_frame---->get_video_frame--->video_thread将解码得到的AVFrame存入FrameQueue---->queue_picture(....., is->viddec.pkt_serial)
---->Frame.serial=viddec.pkt_serial
到这里video_thread中的for循环已经走了一遍,此时总结下

PacketQueue.serial == MyAVPacketList.serial == 1,Decoder.pkt_serial == 1,存入FrameQueue中的Frame.serial=1
然后video_thread中的for循环一直执行,不停从PacketQueue中获取AVPacket去解码然后存入FrameQueue

到目前为止只剩下Clock的serial还未赋值(audclk,vidclk,extclk)
这个肯定是在去展示画面或者播放音频的时候去赋值的

1.在video_refresh--->update_video_pts--->set_clock_at和sync_clock_to_slave,会分别给vidclk.serial和extclk.serial赋值
2.在音频播放过程中sdl_audio_callback--->set_clock_at和sync_clock_to_slave,会分别给audclk.serial和extclk.serial赋值
并且赋值的serial都是来自Frame.serial,目前都是1

这时如果发生seek操作,那么上面说的read_thread线程中for循环读取AVPacket过程中,会检查是否有seek操作,一旦有就会将3个流各自的PacketQueue清空(不会清空PacketQueue.serial),并放入一个flush_pkt,且serial++。

此时PacketQueue.serial == MyAVPacketList.serial == 2,并且for循环继续往下执行读取AVPacket,假设此时读取一个AVPacket为avPacket2存入PacketQueue中
(记住此时PacketQueue.first_pkt==flush_pkt,PacketQueue.last_pkt==avPacket2)

目前因为seek操作之后
PacketQueue.serial == MyAVPacketList.serial == 2,Decoder.pkt_serial = 1
PacketQueue中的first_pkt==flush_pkt和last_pkt==avPacket2两个MyAVPacketList的serial都是2

3个流的decoder_decode_frame方法还在继续执行,关于decoder_decode_frame方法的再次for循环不用在分析了,和上面逻辑是一样的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容