FFmpeg之ffplay源码简要分析

本文简要分析了ffplay的源码,包括基本架构、数据结构如AVPacket队列和AVFrame队列的实现,以及时钟同步和参数。ffplay使用多线程处理音视频解复用和解码,确保数据解码不互相干扰。通过AVPacket队列和AVFrame队列实现数据存储和同步,同时有音频、视频和系统外部时钟用于音画同步。文章还介绍了ffplay的流解码线程和画面刷新机制。
摘要由CSDN通过智能技术生成

1 ffplay 基本架构

1.1 视频解码播放的基本流程

ffmpeg视频解码播放的基本流程如下图所示:

首先对网络媒体数据流进行解封装得到一般的视频封装格式比如MP4等,如果是本地播放的媒体文件就不需要解协议;

然后对视频媒体文件进行解封装,得到未经过解码的视频、音频或者字幕流数据,在ffmpeg中得到的是AVPacket;

然后分别对字幕、音频和视频数据进行解码,分别得到字幕、PCM数据和YUV数据;

由于不同数据体积不同解码速度不同,视频解码相对比较慢,如果解码完就立马播放就会出现音频和视频播放不一致的情况因此需要进行音画同步;

最后将音频视频数据分别输出到对应的设备完成播放。

ffplay是使用SDL(一种跨平台的音视频播放框架)进行具体平台的音视频播放,其使用方式和windows的API很像,比如创建窗口、打开音频输出、处理事件循环等。我们不用太关注于该框架的具体细节,大部分情况下根据函数或者变量名称就能判断涉及SDL的相关内容的含义。

ffplay内部使用多个线程完成音视频的解复用和解码过程,保证不同数据解码直接互不干扰。

1.2 ffplay的基本代码框架

上图是ffplay的基本框架图,ffplay处理音频的基本流程是创建三个线程分别解封装

ffplay中参数传递使用一个VideoState传递,其中包含了各个部分运行时所需要的所有参数,比较庞大;

event_loop主要处理用户输入事件,比如切换显示模式,调整音量大小,控制播放seek等操作;

video_refresh是真正刷新视频画面的函数。

stream_open中初始化需要使用的队列、clock以及相关参数,并创建相关的线程比如音视频解复用线程、音视频解码线程。

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

2 ffplay中使用到的数据结构

2.1 AVPacket队列

//avpackt的队列节点,serial表示当前packet的序号
typedef struct MyAVPacketList {
    AVPacket *pkt;
    int serial;
} MyAVPacketList;

//avpacket的队列的head节点,也就是说PacketQueue是一个带有头结点的队列,该头结点中定义了一些队列相关的metadata以及队列使用到的锁和条件变量(保证队列的线程安全)
typedef struct PacketQueue {
    AVFifoBuffer *pkt_list;         //ffmpeg实现的FIFO缓冲区
    int nb_packets;                 //当前队列中avpcket的数量
    int size;                       //队列中所有数据的总字节数
    int64_t duration;               //队列所有及诶大的时长之和
    int abort_request;              //是否终止对队列的操作,用于安全快速的退出
    int serial;                     //序列号
    SDL_mutex *mutex;               //保证线程安全额锁
    SDL_cond *cond;                 //读写的条件变量
} PacketQueue;

PacketQueue是一个线程安全FIFO队列,数据节点是MyAVPacketList,内部使用AVFifoBuffer实现数据的存取, abort_request控制队列的状态,muxte,cond进行同步和临界区保护。下面是PacketQueue一系列的操作api:

packet_queue_put_private:队列添加packet的具体实现,基本逻辑为检查队列的大小如果不足则扩张,扩张的规则比较简单就是增加一个packet的大小,然后将packet写入到队列中;

packet_queue_put:写入一个packet,会对packet进行复制,实际上写入的是实现是packet_queue_put_private;

packet_queue_put_nullpacket:写入一个空packet,具体是调用packet_queue_put实现,那和普通包有何区别,怀疑是为了保障代码的兼容性,因为旧的api就是构造一个空包写入;

packet_queue_init:初始化队列,设置关于队列的一些状态信息、锁和信号量;

packet_queue_flush:刷新队列,将队列中的数据读取并free,并设置相关的状态到初始状态;

packet_queue_destroy:销毁队列,主要是刷新队列并销毁锁和信号量;

packet_queue_abort:暂停队列,即设置abort_request;

packet_queue_start:开始队列,清除标志位abort_request并自增serial;

packet_queue_get:从队列中取出一个packet。

队列的实现是一个普通的队列,需要注意的细节就是serial的更新时机,在放入同一个队列时队列会将自己的serial号赋值给对应的packet包,并且只有触发packet_queue_start和packet_queue_flush两个事件时才会更新serial。serial能够用来区分不同时刻解封装得到的packet是不是连续的包,如果播放时突然暂停,再resume的话serial更新,上一个packet和下一个packet就不是连续的。

static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt){
    MyAVPacketList pkt1;
    //...
    pkt1.serial = q->serial;
    //...
}

static void packet_queue_start(PacketQueue *q){
    //...
    q->serial++;
    //...
}

static void packet_queue_flush(PacketQueue *q){
    MyAVPacketList pkt1;
    //...
    q->serial++;
    //...
}

2.2 AVFrame队列

//解码出来的avframe数据存储节点
typedef struct Frame {
    AVFrame *frame;         //解码的音频或者视频数据
    AVSubtitle sub;         //解码的字母数据
    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 */
    int width;
    int height;
    int format;
    AVRational sar;         //采样宽高比
    int uploaded;           //当前帧是否已经已经上屏,若果已经上屏则不会再刷新
    int flip_v;             //控制是否垂直翻转
} Frame;

//avframe队列的head节点
typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE];      //固定队列,环形缓冲区
    int rindex;                         //读索引,队头
    int windex;                         //写索引,队尾
    int size;                           //当前队列中节点个数
    int max_size;                       //最大允许存储的节点个数,方便区分是否full
    int keep_last;                      //是否要保留最后一个读节点,1的话最后一个节点就不会被覆盖
    int rindex_shown;                   //当前节点是否已经显示
    SDL_mutex *mutex;                   //锁
    SDL_cond *cond;                     //条件变量
    PacketQueue *pktq;                  //关联的packet队列
} FrameQueue;

Frame是同时能够表示音频、视频、字母的大杂烩,同时包含了一些状态信息来表示当前Frame的具体状态。FrameQueue是使用存储Frame节点的线程安全队列,该线程安全队列并没有类似PacketQueue使用AVFifoBuffer实现,而是用栈上的数组实现线程安全的环形队列。FrameQueue的实现一切未高效率让路,一方面使用静态数组(最大不超过16个节点)不用考虑动态内存的问题,减少比不要的内存操作的小号,另一方面队列的lock力度比较小,针对rindex和wrindx都未进行加锁,因为程序能够保证两个线程独立的更新和读取对应的值。

FrameQueue的一些操作函数:

frame_queue_init:初始化avframe队列,实现很简单主要是初始化队列的内存和metadata以及锁等,另外初始化的时候会使用av_frame_alloc提前分配好对应的frame的内存;

frame_queue_unref_item:销毁Frame节点中的数据;

fra

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值