RTMP拉流的基本框架
- PullWork类: 总控制
- Init:
初始化音频包队列(audioq),采样帧队列(videoq)。
初始化视频包队列(sampq),图像帧队列(pictq)。
初始化同步模块(AVSync),采用audio master。
初始化音频输出模块(AudioOutSDL),并添加回调函数。
初始化音频解码模块(AudioDecodeLoop),并启动线程。
初始化视频输出模块(VideoOutputLoop),并添加回调函数。
初始化视频解码模块(VideoDecodeLoop),并启动线程。
初始化rtmp播放模块(RTMPPlayer),启动播放线程。 - 添加回调函数,用于RTMPPlayer向音频包队列(audioPacketCallback),或者视频包队列添加packet(videoPacketCallback);视频显示回调函数等(outVideoPicture)。
- PacketQueue类:借鉴ffplay实现
-
设计原则:
1)有边界:可以设置最大缓存数量
2)时长计算:可以结算队列里的数据维持播放的时间长度
3)可阻塞:push和pop的时候可以使用阻塞的方式
4)可非阻塞: push和pop的时候可以使用非阻塞的方式
5)可清空队列
6)可以唤醒队列进行退出 -
PacketQueue由链表方式实现:
typedef struct MyAVPacketList { AVPacket pkt; // 解封装后的包 struct MyAVPacketList *next; // 指向下一个元素 int serial; // 播放序列 } MyAVPacketList;
-
PacketQueue类操作提供以下⽅法:
- packet_queue_init:初始化各个字段的值,并创建mutex和cond。
- packet_queue_destroy:清空节点,清理mutex和cond。
- packet_queue_start:启动,插⼊flush_pkt。
- packet_queue_abort:中止,abort_request_ = 1
- packet_queue_get:获取⼀个包,更新队列属性。
- packet_queue_put:存⼊⼀个包,调用packet_queue_put_private,计算serial,包入队列,队列属性更新(包数量,字节数,时长)。
- packet_queue_put_nullpacket:放⼊“空包”(nullpacket)。放⼊空包意味着流的结束,为了冲刷解码器,将编码器中所有frame都读取出来。
- packet_queue_flush:遍历释放AVPacket(AVpacket对应的数据也被释放掉)。最后将PacketQueue的属性恢复为空队列状态。
-
PacketQueue解释:
- flush_pkt定义为:static AVPacket flush_pkt,是⼀个特殊的packet,可以作为两段不同播放序列的标记。插⼊ flush_pkt 触发PacketQueue其对应的serial进行加1操作,并清空自身缓存avcodec_flush_buffers(),以备新序列的数据进⾏新解码。
- MyAVPacketList的内存是完全由PacketQueue维护的,在put的时候malloc,在get的时候free。AVPacket分两块:一部分是AVPacket结构体的内存,这部分从MyAVPacketList的定义可以看出是和MyAVPacketList共存亡的。另一部分是AVPacket字段指向的内存,这部分一般通过av_packet_unref 函数释放。
- FrameQueue类:参考ffplay,FrameQueue是⼀个环形缓冲区(ring buffer),是用数组实现的⼀个FIFO。适合于明确了缓冲区的最大容量的情形。每一个frame_queue有⼀个写端⼀个读端,写端位于解码线程,读端位于播放线程。
- Frame封装:
// 用于缓存解码后的数据
typedef struct Frame {
AVFrame *frame; // 指向数据帧
AVSubtitle sub; // 用于字幕
Int serial; // 帧序列,在seek的操作时serial会变化
double pts; // 时间戳,单位为秒
double duration; // 该帧持续时间,单位为秒
int int_duration; // 单位为ms,方便统计避免浮点数的问题
int64_t pos; // 该帧在输入文件中的字节位置
int width; // 图像宽度
int height; // 图像高读
int format; //图像(enum AVPixelFormat),声音(enum AVSampleFormat)
AVRational sar; // 图像的宽高比(16:9,4:3),如果未知或未指定则为0/1
int uploaded; // 用来记录该帧是否已经显示过?
int flip_v; // =1则旋转180, = 0则正常播放
} Frame;
- FrameQueue的设计⽐PacketQueue复杂,引⼊了读取节点但节点不出队列的操作、读取下⼀节点也不出队列等等的操作,FrameQueue操作提供以下⽅法:
- frame_queue_unref_item:释放Frame⾥⾯的AVFrame和 AVSubtitle。
- frame_queue_init:初始化队列,确定队列大小,分配AVFrame内存等。
- frame_queue_destory:销毁队列,释放队列中对AVBuffer的引用和AVFrame对象。
- frame_queue_signal:发送唤醒信号
- frame_queue_peek:获取当前Frame对象,获取队列当前Frame, 在调用该函数前先调用frame_queue_nb_remaining确保有frame可读。
- frame_queue_peek_next:获取当前Frame的下⼀Frame,调用之前先调用frame_queue_nb_remaining确保至少有2 Frame在队列。
- frame_queue_peek_last:获取上⼀Frame。
- frame_queue_peek_writable:获取⼀个可写Frame,有最大容量限制,可以以阻塞或⾮阻塞方式进⾏。
- frame_queue_peek_readable:获取⼀个可读Frame,防止队列为空,可以以阻塞或⾮阻塞⽅式进⾏。
- frame_queue_push:更新写索引,此时Frame才真正⼊队列,更新队列属性
- frame_queue_next:更新读索引,此时Frame才真正出队列,更新队列属性。
- frame_queue_unref_item:释放对应的AVFrame和AVSubtitle
- frame_queue_nb_remaining:获取队列Frame节点个数
- frame_queue_last_pos:获取最近播放Frame对应数据在媒体⽂件的位置,主要在seek时使用。
- FrameQueue的解释
- rindex_shown取0/1,表示该帧是否显示过,适用于用于保留上一帧的情况(keep_last为True)。
- 写队列:
frame_queue_peek_writable(&is->pictq) 向队列尾部申请⼀个可写的帧空间,若队列已满,则等待(由SDL_cond * cond控制,由frame_queue_next或frame_queue_signal触发唤醒)。
av_frame_move_ref(vp->frame, src_frame) 将src_frame中所有数据拷贝到vp->frame并复位src_frame,vp->frame中AVBuffer使用引用计数机制,不会执⾏AVBuffer的拷贝动作,仅是修改指针指向值。
frame_queue_push(&is->pictq) 此步仅将frame_queue中的写索引加1,实际的数据写⼊在此步之前已经完成。 - 读队列:读队列和写队列步骤是类似的,基本步骤如下:
从frame_queue_peek_readable获取可读Frame;如果需要更新读索引(出队列该节点)则调用frame_queue_peek_next;并释放帧的内存。