播放器的推流:同一个封装,目标换成一行地址

从零做一个全平台播放器·约 1525 字

录制写的是文件名,推流写的是 rtmp:// 这一行地址。在这条链上它们是同一段代码:同一个录制器、同一个封装实现、同一条写线程,队列、访问单元、时间戳那几步都照旧。这一篇写这一行地址是怎么被解释的——格式从哪来、协议地址要补哪些名字和参数、写失败怎么分档,以及这条链上推流的快慢由谁定。

录制与推流的差别只在输出那一行地址

录制器的接口就两个参数:源地址,输出地址。写文件名是录制,写 rtmp:// 是推流。录制器自己不认协议,它把第二行地址原样交给封装实现,由那一侧决定怎么开。

所以「推流」在这条链上不是换一个类、也不是开一个开关:同一个封装实现,同一个流程——开输出、写头、逐包交错写、写尾。封装那一篇讲的写线程、访问单元合并与时间戳那两步,推流一样走一遍。

换的是格式怎么来、目标怎么打开。

对外只有设置与查询,都在 open 之前

录制器对外的设置都在 open 之前生效:选源怎么取、输出怎么封(这两项不设就默认走 ffmpeg),指定视频与音频的编码,指定两条轨道各自怎么处理(哪一轨重编、哪一轨直接拷)。

封装实现本身也走注册表:除了默认那套基于 ffmpeg 的,另有一条基于 zlmediakit 的实现,走它自己的封装与推流。换的是封装这一段——录制器的入口、队列与编码都不动。

指定编码那一步同时就是取舍那一步:把某一侧的编码设成「没有」,那一轨就被丢掉——只推画面、只推声音就是这么来的。

open 之后这条链自己跑:源读完,或者 IO 出错,它自己收尾关闭,不用上层去关;想主动停,调一次关闭。中间想知道进展就直接问:状态、进度、源的总时长——直播给不出总时长,所以直播也 seek 不了。

挂上来的观察者收五路回调:状态变化、进度、IO 错、编码错、完成。两类错是分开报的:IO 那一路是读写与连接上的事,编码那一路是某条轨编不动了(回调里带上是哪条轨)。进度里那个已处理时长允许取「不以时间计」,给不出时间的源用它表示。

输出格式:先按后缀猜,猜不到按协议映射

一行地址怎么变成封装格式:先按后缀猜,没有后缀时按协议映射到格式名

封装实现手里只有那一行地址,格式从它身上推。先按扩展名猜。

猜出来之后有两处要改。一处是 mp4 装不下非 AAC 的音频,这种情况自动换成 mov——换的是容器,编码与流都不动。另一处是地址压根没有扩展名,协议地址一般都没有;这时把 :// 前面那段当成封装格式名去查。这里有两处名字对不上:rtmp 系列承载的其实是 flv 的数据,库里没有叫 rtmp 的封装格式,得映射成 flv;rtsps 要映射成 rtsp。

rtsp 那一侧还补两句参数:传输强制走 TCP,因为 UDP 跨网段容易丢包,丢到超时就断了;发送带一个低延迟的选项。

开输出那一步不分本地还是网络:同一个打开函数,给文件名它去开文件,给 rtmp:// 它去建连接。只有那些自己管连接的封装格式(标着「无文件」的)才跳过这一步——它们自己拿地址去连。

起流与写出去的失败处置

写失败的两档处置:输出坏了就收摊,单帧的问题只丢这一帧

开输出与写头这两步,失败都当场收摊:把输出上下文放掉、返回失败,后面不再试。写头失败还会把这条链标成初始化失败,写线程看到这个标记直接退出——不会一边拿着没写头的容器一边往里塞包。

写还是封装那一篇那句块交错写,失败分两档。

一档是输出本身坏了:连接断、对端关闭、磁盘满、句柄不对、IO 错。这类报错并收摊,把输出上下文放掉,不再往里写——留着一个坏掉的上下文,收尾写文件尾的时候会跳起来崩。

另一档是这一帧自己的问题,比如解码时间戳不是单调增的,容器不收这一帧。这类只丢这一帧,接着写后面的:第一次打一条日志,之后只累计次数。原因很实际——一次 seek,或者一路抖动的时间戳,可能连着上百个包都不合规矩,每个都打日志就把日志刷废了。

落盘还有一个开关:封装库默认会攒一批字节,等到关闭才落;这条链把它打开,所以边转边拉那种用法能立刻看到产物。断了对端也不自己重来:这条链收尾,要接着推,得重新开一次。

推流的快慢:源与写出去的速度

这条链上没有按墙钟节流的动作:写线程从队列取包,队列满了阻塞入队(这条队列见封装那一篇),压力一路顶回源头。所以节奏由最慢的一头定——源是直播,慢的那头多半是写出去的速度;源是文件,慢的那头是能写多快,这时链不会按内容的原时长慢慢推。

对端写得慢的时候,这层压力会一路传回源那一侧:输出队列顶满,编码停下,解码停下,读也跟着停下——内存不会因为推得慢而堆起来。

同一路内容,源是文件时推出去花的钟表时间比内容本身的时长短,占的带宽不会因为推得快而变。要让推流按内容本身的时长走,得在源头限速——这条链的节拍只由这两头给。

到这里这条链走完了:帧源、帧到包、包到文件、目标换成一行地址(这一篇)。再往下要换的是编码那一侧——各平台拿自己的硬编替掉这里这个软件编码器,封装与输出这一段不动。

引擎 avox 在 GitHub 开源:github.com/xxxzhou/avox,这条录制链、封装这一层与输出目标那一段都在里面。

音视频开发 播放器 录制 推流 RTMP 封装 C++