播放器实战之ffplay数据结构解析

2022-03-21 18:41:27 浏览数 (1)

大家好,我是小涂,昨天晚上给大家进行了一场直播,这次直播内容主要分享了一些自己的学习方法和一些简单的理财分享,中途又再次出现了一次意外,原本是在b站上来进行直播的,后面有回音,影响直播效果,所以就备战到视频号了,后期直播就在视频号进行了,同时如果下次有直播会提前通知大家:

同时昨天还完成了一个小目标,就是看完了第一本保险的书籍,看完之后,真的颠覆了我对保险的认知,这块知识非常实用,关乎到你、父母、以及小孩以后买保险;这本书非常通俗易懂,所以我非常推荐大家有空可以去看看:

还有对理财这方面感兴趣的朋友,可以关注我另外一个有留言功能的号,都是关于指数基金方面的,后期会有自己以前学习如何读财务报表的分享:

好了,今天主要继续给大家分享关于ffplay播放器的内容,废话不多说,咋们正式开始:

一、ffplay数据结构分析:

在分析一些相关数据结构之前呢,你首先可以下载4.2版本的ffmpeg源码便于阅读:

代码语言:javascript复制
https://github.com/FFmpeg/FFmpeg

在Ffplay.c里面有播放器总管结构体,也就是这个结构体里面包含了很多关于播放器的封装:

代码语言:javascript复制
typedef struct VideoState {

/* 省略了*/

}} VideoState;

这个struct VideoState结构体,我会抽取重要的封装来分享:

代码语言:javascript复制
typedef struct VideoState {
     Clock audclk;//音频时钟
     Clock vidclk;//视频时钟
     Clock extclk;//外部时钟

    FrameQueue pictq;//视频帧队列
    FrameQueue subpq;//字幕帧队列
    FrameQueue sampq;//采样帧队列

    Decoder auddec; //音频解码器
    Decoder viddec;//视频解码器
    Decoder subdec;//字幕解码器
    
    PacketQueue audioq;//音频packet队列
    
    struct AudioParams audio_src;//音频frame参数
    
}VideoState;

上面是为了好分析一下目前觉得重要的一些结构体成员,简化来的哈。

那么我们首先来分析一下这个时钟结构体typedef struct Clock:

代码语言:javascript复制
typedef struct Clock {

    double pts;// 时钟基础,当前帧(待播放)显示时间戳,
    播放后,当前帧变成上一帧
 
 
    double pts_drift; //当前pts与当前系统时钟的差值,
 audio,video对于该值是独立的
    
    double last_updated;//当前时钟最后一次更新时间,
    也可以称为当前时钟时间
    double speed;//时钟速度控制,用于控制播放速度
    int serial;//播放序列          
    int paused;// =1 ,则说明是暂停状态
    int *queue_serial; //指向当前queue_serial
} 

注意:上面所说的播放序列式指⼀段连续的播放动作,⼀个seek操作会启动⼀段新的播 放序列。

接下来就是typedef struct FrameQueue:

代码语言:javascript复制
typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE];//帧队列中最大的个数
    int rindex;//读索引,待播放时读取此帧进行播放,播放后此帧变为上一帧
    int windex;//写索引
    int size;//当前总帧数
    int max_size;//最大存储帧数
    int keep_last;//=1,说明要在队列里面保持最后一帧的数据不释放,
    只在销毁队列的时候才将其真正释放
    int rindex_shown;//初始化为0,配合Keep_last=1使用
    SDL_mutex *mutex;//互斥量
    SDL_cond *cond;//条件变量
    PacketQueue *pktq;//数据队列缓冲队列
} FrameQueue;

说明一下,FrameQueue是⼀个环形缓冲区(ring buffer),是⽤数组实现的⼀个FIFO。数组⽅式的环形缓冲区适合于 事先明确了缓冲区的最⼤容量的情形。

ffplay中创建了三个frame_queue:⾳频frame_queue,视频frame_queue,字幕frame_queue。每⼀ 个frame_queue⼀个写端⼀个读端,写端位于解码线程,读端位于播放线程。

关于这个FrameQueue操作接口,我们下篇再进行分析。

对了,结构体struct FrameQueue成员里面的Frame,也分析一下:

代码语言:javascript复制
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; //该帧在输入文件中的字节位置     
    int width;//图像宽度
    int height;//图像高度
    int format;//对应图像或者声音格式
    AVRational sar;//图像的宽高比
    int uploaded;//用来记录该帧是否已经显示过
    int flip_v;=1,则旋转180,;=0,则正常播放
} Frame;

真正存储解码后⾳视频数据的结构体为AVFrame ,存储字幕则使⽤AVSubtitle,该Frame的设计是为了⾳ 频、视频、字幕帧通⽤,所以Frame结构体的设计类似AVFrame,部分成员变量只对不同类型有作⽤,⽐ 如sar只对视频有作⽤。⾥⾯也包含了serial播放序列(每次seek时都切换serial),sar(图像的宽⾼⽐(16:9,4:3...),该值来 ⾃AVFrame结构体的sample_aspect_ratio变量)。

好了,接下来我们继续分析typedef struct Decoder(解码器封装):

代码语言:javascript复制
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;

下面接着是struct PacketQueue:(保存解封装后的数据,即保存AVPacket),不过在分析这个结构体之前,我们先来分析typedef struct MyAVPacketList:

代码语言:javascript复制
typedef struct MyAVPacketList {
    AVPacket *pkt;//解封装后的数据
    int serial; //播放序列
} MyAVPacketList;

serial字段主要⽤于标记当前节点的播放序列号,ffplay中多处⽤到serial的概念,主要⽤来区分是否连续 数据,每做⼀次seek,该serial都会做 1的递增,以区分不同的播放序列。serial字段在我们ffplay的分析 中应⽤⾮常⼴泛,谨记他是⽤来区分数据否连续先.

下面我们继续分析struct PacketQueue:

代码语言:javascript复制
typedef struct PacketQueue {
    AVFifoBuffer *pkt_list;//输入输出缓冲队列
    int nb_packets;//包数量,也就是队列元素数量
    int size;//队列所有元素的数据⼤⼩总和
    int64_t duration;//队列所有元素的数据播放持续时间
    int abort_request;//⽤户退出请求标志
    int serial;//播放序列号,和MyAVPacketList的serial作⽤相同
    SDL_mutex *mutex;// ⽤于维持PacketQueue的多线程安全
    (SDL_mutex 可以按pthread_mutex_t理解)
    SDL_cond *cond;// ⽤于读、写线程相互通知
    (SDL_cond可以按pthrea d_cond_t理解)
} PacketQueue;

该结构体内定义了“队列”⾃身的属性。上⾯的注释对每个字段作了简单的介绍,这⾥也看到了serial字段, MyAVPacketList的serial字段的赋值来⾃PacketQueue的serial,每个PacketQueue的serial是独⽴的。

最后,我们来分析一下struct AudioParams:

代码语言:javascript复制
typedef struct AudioParams {
    int freq;// 采样率
    int channels;// 通道数
    int64_t channel_layout;// 通道布局,⽐如2.1声道,5.1声道等
    enum AVSampleFormat fmt;// ⾳频采样格
    式,⽐如AV_SAMPLE_FM T_S16表示为有符号16
    bit深度,交错排列模式
    int frame_size;// ⼀个采样单元占⽤的字节数(⽐如2通道
28时,则左右通道各采样⼀次合成⼀个采样单元)
    int bytes_per_sec;//// ⼀秒时间的字节数,
    ⽐如采样率48Khz,2 channel,16bit,则⼀秒48000*2*16/8=192000
} AudioParams;

总结:

好了,今天的分享就到这里了,我们下期见!

0 人点赞