OneLive 这个项目,是我花了大半个季度时间从零搭出来的一套直播系统。起因很朴素:想做一个自己能完全掌控“推流、转码、分发、播放”全链路的直播间,而不是东拼西凑地组合各种现成服务。开做之前,我原本以为最难的环节是编码和解码,真正做完才意识到,最麻烦的是工程化落地的那些细节。
这篇内容适合正在做直播功能,或者打算自建音视频核心链路的同学参考,尤其是对延迟有要求、又需要稳定应对并发观看场景的团队。我会把 OneLive 的设计思路、技术选型、核心实现、完整搭建过程,以及我压测时踩过的坑全部梳理一遍,全是实操向的内容,不会停留在概念讨论上。
如果你最近也在折腾类似的东西,希望这篇文章能帮你省掉一些弯路。
1. 项目定位与需求收敛
1.1 先搞清做的是哪种直播
直播这个东西,表面看都是“一个主播推流,N 个观众拉流”,但不同场景背后的技术路线差得很远。互动直播要求延迟在几百毫秒以内,主播和观众要连麦、要抢麦、要做音视频房间,这种场景必须用 WebRTC 这类实时传输方案。广播型直播,比如企业年会、技术分享、大型赛事转播,对延迟的容忍度要高很多,2 到 5 秒完全没问题,经典 RTMP 加 HLS 组合就能扛住。
我做 OneLive 之前的第一个决定,就是把它定位成广播型直播平台,而不是互动直播。这个定位直接影响后面所有技术选型。互动直播要解决信令协商、P2P 连接、音视频房间同步这些很重的工程问题,第一版完全吃掉不现实。广播型直播链路相对线性,推流端、转码端、分发端、播放端各司其职,更适合用“先跑通再做深”的节奏推进。
为了验证这个定位,我列了一个需求清单:开播、观看、录制回放、房间管理、聊天弹幕、多码率转码。其中聊天弹幕不是 OneLive 的核心,我准备用内部的消息模块先顶着,真正花精力的是开播稳定性和观看流畅度。后来整个项目推进下来,需求聚焦这个决定帮我省了特别多时间。
1.2 技术目标与功能边界
OneLive 的技术目标,其实就三条。第一,推流端在弱网环境下能保持稳定,不轻易断流。第二,播放端打开直播间要称得上秒开,首屏渲染不能让人一直盯着黑屏。第三,转码和分发链路要能横向扩展,单房间万人在线不至于直接把服务打崩。
围绕这三个目标,功能边界自然而然就清晰了。不做连麦 PK,不做美颜特效,不做复杂 IM 系统。第一版只聚焦广播链路,把每一条链路上的工程做到位。我见过不少项目死于贪多,功能铺得很开,最后没有一个模块能稳定跑在线上。OneLive 宁可功能少一点,也要保证每个功能都是能上生产环境的水平。
可能有人会觉得,直播不搞连麦互动,是不是太单薄了。但换个角度看,如果连最基础的“稳定开播 + 流畅观看”都做不到,连麦只会让系统复杂度爆炸。先把地基压实,再往上面盖楼,这是工程项目最朴素也最有效的逻辑。
1.3 为什么不直接买现成的直播服务
有人会问,直播行业那么成熟,云厂商都提供整套服务,为什么还自己搭?我的回答很直接:第三方直播服务是一个黑盒,你可以在上面快速跑业务,但一旦出现线上问题,你没有办法知道每一层发生了什么,也没法针对自己的业务做定制优化。
OneLive 并不是从零写编码器,也不是重新发明协议栈,而是基于开源媒体组件整合出的系统。自研的部分是业务层和调度层,也就是云厂商通常不开放的那部分逻辑。这样做的好处很明显:协议层面的东西社区已经打磨了十几年,直接用就行;但调度、鉴权、房间状态、资源伸缩这些核心逻辑握在自己手里,出了问题能快速定位,也能按自己的场景调优。
这个选择也有代价:需要自己维护基础设施。但站在长期角度看,技术壁垒留在自己团队手里,比省几个月的开发时间更值。尤其是做音视频这种复杂领域,踩过坑的经验无法从文档里获得。
2. 架构设计与协议选型
2.1 直播链路的五个环节
OneLive 整条链路可以拆成五段:推流端、接入层、处理层、分发层、播放端。
推流端负责把摄像头或者屏幕画面采集、编码,通过 RTMP 协议推送到接入层。接入层做两件事:校验推流端的身份和权限,以及把流原始数据接入到内部消息通道。处理层是最忙的环节,承担转码、录制、水印等任务,把一路高码率原流转换成多路不同清晰度的输出流。分发层负责把流送到各地边缘节点,播放端则从就近的边缘节点拉流。
五层之间,我做了一个重要决定:接入层和处理层不直接点对点通信,而是通过消息队列异步解耦。这样做的原因是避免单点故障扩散。假设某个处理节点突然挂掉,接入层不会跟着崩,流可以暂时堆积在消息通道里,等新的处理节点上线后再继续消费。对于直播场景来说,短暂的延迟增大比直接断流要容易接受得多。
2.2 推流协议:RTMP 是底线,SRT 是储备
推流协议,最常用的是 RTMP。RTMP 基于 TCP,数据传输可靠,兼容性极好,绝大多数推流工具和播放器都支持。但它也有明显弱点:TCP 本身就是面向可靠连接的,在弱网环境下面临丢包和重传,容易出现延迟和卡顿;协议本身不方便加密;时钟基准同步能力也比较弱。
OneLive 第一版选择 RTMP 作为默认推流协议,完全是因为兼容性。我先保证市面上主流的推流工具能直接推流进来。为了弥补弱网表现,我在推流端加了两个补偿:第一,把推流缓冲区调到一个合适的值,让短时间的网络抖动消化在缓冲区里,不直接表现为画面中断;第二,实现了自动重连和签名续推,推流断开后能在几秒内恢复,避免主播端感知明显。
SRT 我也在实际环境里测过,抗丢包能力确实显著优于 RTMP,尤其适合网络状况很差的长距离传输。但它的问题是播放侧支持不成熟,基本都要在服务端转成其他协议才能播放。OneLive 第一版没有引进来,但把 SRT 作为后续升级储备。技术路线要先把生态最成熟的一条路走通,再谈锦上添花。
2.3 播放协议:HLS 保底,FLV 提速
播放端的选择,直接影响用户感知到的延迟和流畅度。最稳妥的方案是 HLS:基于 HTTP,天然适合 CDN 分发,兼容性覆盖所有市面播放器,但延迟受切片时长限制,通常 5 到 10 秒,交互感很差。
OneLive 没有只做一套播放协议,而是同时对外提供 HTTP-FLV 和 HLS 两条通道。HTTP-FLV 的延迟能压到 1 到 3 秒,因为 FLV 不需要切片,流式数据可以直接推给播放器,这个时延对普通直播场景来说已经很舒服。播放器端启动时会根据自身能力探测,优先尝试 FLV 连接,不支持的话再回退到 HLS。
这里有个容易被忽略的点:通道切换不能太激进。播放器探测 FLV 失败时,如果立刻切 HLS,会给用户造成一次短暂中断。我采取的方案是并发探测,播放器同时请求 FLV 和 HLS 的探测数据,谁先拿到完整的关键帧,就用谁开播。实际体验下来,这种情况下的开播成功率和稳定度都更好。
2.4 转码层与分发层怎么分工
转码层做的事情,是输入一路高码率原流,输出多种码率的流。常见输出档位是 1080p、720p、360p 三档,播放端根据带宽自动选择合适档位。这样做的好处很实在:主播推流可能推到 6Mbps,但观众手机处在 4G 网络下,如果不转码,根本拉不动这么高的码率。
转码是 CPU 密集任务,所以我在 OneLive 里把它做成独立 Worker 池。调度器按队列分发转码任务,每台机器的 Worker 数量根据 CPU 核数和内存动态调节,而不是写死一个数字。分发层则负责把转码后的流推到不同地域的边缘节点,播放器请求地址时,调度服务根据 IP 归属返回最近的边缘节点,让观众就近拉流,降低源站压力。
这套分工并不复杂,但边界必须清晰:转码层只关心媒体处理,不管哪个房间有多少观众;分发层只关心边缘节点的状态和覆盖关系,不管转码参数。每次想加功能时,先想清楚这个功能该放在哪一层,避免逻辑纠缠在一起。
3. 核心模块的实操实现
3.1 房间状态与推流鉴权
直播间的核心是房间。OneLive 把每个直播间映射为一个房间 ID,房间状态存放在分布式缓存里,字段包括主播身份、当前流状态、在线观众数、已生成的清晰度列表。创建和销毁走业务服务,接入层不直接操作房间数据,只负责查询和校验。
鉴权采用双重校验。第一步,推流地址里带上一次性 Token,Token 由业务服务生成,带过期时间,好处是防止推流地址被截取后长期有效。第二步,接入层收到推流请求后,向业务服务回调校验 Token 和房间状态,通过之后才允许建立推流连接。这一步能挡住未授权推流,也能防止有人往别人的直播间塞垃圾流。
实际联调时我发现一个隐蔽坑:主播断流后自动重推,如果接入层没有合理的流对象复用逻辑,会生成多个同名流对象,播放端一旦拿到错误的流对象,画面就会串掉。我的解决方案是引入流签名,每个房间同一时间只允许一个活跃推流,新推流直接替换旧推流,而不是同时存在两份。
3.2 转码资源测算与参数配置
转码是资源消耗大户。以 1080p 30 帧的软编转码为例,一路输出就会占用两颗 CPU 核心相当高的比例,如果再同时输出三路清晰度,机器压力立刻上来。第一版 OneLive 的策略是 CPU 软编加并发上限控制,每台 Worker 机器上设置最大转码任务数,超过部分进入排队。
但排队机制本身也有问题:如果一个普通直播间没有多少观众,却在开播瞬间立刻占用三路转码任务,资源白白浪费。我后来改成弹性转码:普通直播间默认只转一路高清流,等在线数超过阈值后,再异步开拓其他清晰度。热门房间和普通房间的资源使用差异就这么被拉平了。
再说具体编码参数。我采用的组合是:视频编码格式 H.264,关键帧间隔 2 秒,码率控制使用动态码率,音频采样率 44100,音频码率 128Kbps。关键帧间隔这个参数,直接影响播放端的起播速度。GOP 越大,播放器要等关键帧的时间越长,首屏就越慢;GOP 太短又会降低压缩效率、增大带宽消耗。2 秒这个值是 OneLive 经过几轮压测之后定下来的平衡点。
3.3 秒开优化:连接、GOP 缓存与播放列表
首帧秒开,是直播客户端体验的分水岭。OneLive 最初 HLS 起播要 2 秒多,优化后压到 300 毫秒左右,主要靠三条路径。
一是 GOP 缓存。播放器发起拉流时,如果服务端从头传,它要等一个 GOP 周期才能等到关键帧,这段时间画面一直是黑的。边缘节点缓存最近一段 GOP 数据,新播放器接入时先发缓存里最近的关键帧,播放器立刻就能开始解码。
二是播放器预连接。在用户真正进入直播间之前,播放器先对播放地址做一次探测请求,提前建立连接和会话,把握手开销提前消耗掉。用户一点进来,直接复用这个热会话,省掉了一整轮网络协商。
三是播放列表裁剪。HLS 播放器启动时要解析 m3u8 列表,列表越长解析越慢。OneLive 把列表裁剪成只包含最近几个切片,并把分片时长控制在 1 到 2 秒,这样拉流和解析都快,首帧自然更快。
3.4 录制、回放与消息模块
录制功能,本质上就是把转码前的原流落到存储上,同时按时间切片。OneLive 在录制时生成独立的 TS 文件,并在直播结束后自动把切片列表拼接成回放文件。回放不使用直播流的动态时间戳,而是生成一套独立的静态时间轴,避免回放时出现进度跳动。
聊天的实现用主题订阅模式,主播和观众通过内部消息通道订阅房间话题。消息模块不直接读取直播间数据库,而是通过事件源更新在线状态、热度数据等展示信息。这个设计还有一个额外的好处:如果消息通道临时抖动,不影响音视频主链路,观众不会因为聊天挂了而看不到画面。
4. 从零搭建 OneLive 的完整过程
4.1 先搭一套可复现的本地联调环境
写业务代码前,我花了一天时间把本地环境彻底搭好。组件全部用容器方式启动,缓存、消息队列、数据库各一个独立容器,媒体处理组件以独立进程的方式跑在宿主机上,方便我随时翻日志。
音视频项目有个必须从一开始就做的功课:链路日志。每一路流从推流接入开始,都要有一个唯一的流 ID,从接入层流到处理层再到分发层,一路带下去。我第一版没做这个,后来碰见问题日志根本串不起来,找原因找到怀疑人生。后来花了半天,把链路 ID 逻辑补上,排查问题效率直接翻倍。这个东西在开发阶段投入很少,后期收益大到不成比例。
4.2 关键参数的计算方法
聊几个我实际测算过的数据。
带宽预算:假设一个热门房间有一万在线观众,人均消耗码率按 1Mbps 算,源站出口带宽至少要一万乘一,也就是 10Gbps。单机永远不可能扛住这个量,所以必须靠边缘节点分担。每个边缘节点要预留多少带宽,我的算法是覆盖人数乘以人均码率,再乘 1.5 冗余系数。冗余系数必须预留,因为直播码率是波动的,峰值时刻远高于平均值。
转码并发:一台 8 核 16G 的机器,实测能跑 3 路 1080p 转码,或者 6 路 720p 转码。如果每场直播都要生成三路清晰度,那一台机器大概只能支撑 3 到 4 个热门直播间同时开播,所以转码队列、扩容、缩容策略要在项目初期就规划好。
存储容量:录制一小时 1080p 直播,大概会产生 4 到 6GB 文件。假设每天有 100 小时录制任务,一天就是 500GB 左右规模,还没算日志和回放。如果磁盘规划没跟上,处理磁盘满的情况会是线上事故级别的麻烦。
4.3 一次标准的推流播放联调流程
把联调流程记录在这里,方便你照着走一遍。
第一步,推流端拼接地址:rtmp://接入层地址/live/房间ID,带上一次性 Token。推流启动后,接入层日志里能看到校验通过和流创建成功的记录,同时消息队列出现 stream_started 事件。
第二步,转码 Worker 通过消费队列拿到任务,在日志里输出识别到的分辨率、码率、帧率。转码完成后,分发层把生成的文件推到边缘存储,同时更新播放列表。
第三步,打开播放页,播放器先请求调度接口获取播放地址,再按协议优先级去探测拉流,控制台里能看到 network 请求从发起到渲染首帧的耗时。
如果联调失败,我的排查顺序永远是推流端日志、接入层日志、播放端网络请求,不要一上来就翻播放器代码。很多人调不出来,就是因为第一步方向错了,问题明明在服务端,却在播放端反复追查。
5. 踩坑实录与问题排查技巧
5.1 音画不同步:一次时间戳的教训
第一次完整联调,画面和声音对不上,口型明显滞后几百毫秒。最开始我怀疑播放器缓存,调了半天没效果。后来抓包分析,才发现是推流端音频和视频的时间戳基准不一致:视频按帧率生成时间戳,音频按采样率生成时间戳,两套基准没有对齐,服务端转封装时只能原样保留这个错位。
解决办法是在接入层做音视频时间戳归一化。以视频时间轴做基准,把音频时间戳映射到同一时间轴。这一步必须在转封装阶段做,拖到分发层就来不及了。另外,长时间直播还得注意采集设备时钟漂移,两三个小时的直播,漂移积累下来也会造成明显的音画差距,有条件的话要做周期性的时钟校正。
5.2 首帧延迟从 2 秒压到 300 毫秒
首帧优化的过程里有个反直觉的发现:最开始把播放切片的时长从 4 秒改成 2 秒,首帧延迟几乎没变化。真正起作用的,是 GOP 缓存和播放器预连接。这说明性能问题要依靠定位工具去找到真正的瓶颈,不能拍脑袋。
GOP 缓存这个手段也不是设置越多越好。缓存窗口太长,新播放器接入时会先收到大量过期数据,既浪费带宽又拖慢首帧,所以我最终把缓存窗口控制在 3 到 4 秒。既保证新播放器能在 1 秒左右起播,又不会形成数据堆积。细节越细,工程出来的效果差异越大。
5.3 高并发下的转码节点雪崩
压测把单房间在线数拉到接近容量上限时,转码节点 CPU 先飙满,新任务全部排队,播放端因为拿不到新切片开始大面积卡顿。这其实不是单一节点的问题,而是资源不足时的级联反应。
我做了三个修复。最小并发保证:每个直播间至少保留一路最低清晰度流,保证画面不断。任务优先级:已有房间的转码任务优先于新房间任务。熔断机制:节点 CPU 超过阈值后拒绝新增任务,调度层自动把任务转移给其他空闲节点。
经过这轮压测,我对线上系统的理解更新了一点:系统的设计基准不是“正常负载”,而是“故障时刻还能不能提供可用服务”。所有调度和资源策略,都应该围绕坏的时候会发生什么来设计。
5.4 问题速查表与排查顺序
实际问题排查时,我建议按依赖方向去定位,先链路入口到出口走一遍,再验证单项。下面这个速查表是 OneLive 实际调试中最常用的几类问题。
| 问题现象 | 可能原因 | 解决建议 |
|---|---|---|
| 推流后播放端黑屏 | 新播放器等不到关键帧 | 打开 GOP 缓存,拉流时先发缓存关键帧 |
| 音画不同步 | 推流端音视频时间戳基准不一致 | 接入层做音视频时间戳归一化 |
| 延迟突然变大 | 网络抖动导致缓冲区堆积 | 调低播放器缓冲区,开启推流端码率自适应 |
| 转码 CPU 突然打满 | 并发转码任务过多 | 做任务队列、优先级和熔断机制 |
| 断流后重推失败 | 推流 Token 过期或流签名冲突 | 重推时刷新 Token,确保流签名幂等 |
| HLS 播放列表解析慢 | 切片数量过多 | 裁剪播放列表,控制切片数量 |
| 直播中途掉线频繁 | 推流端弱网重传策略不当 | 开启断线自动重连并设置合理缓冲 |
OneLive 这个项目做下来,我最大的感受是:直播链路中没有一个环节是可以当成“理所当然”略过的。推流端一句话能带过的采样率,转码端没人注意的关键帧间隔,播放端永远的秒开问题,每一个小参数背后都是一整套取舍。如果你也想做类似的直播系统,我的建议是先把整条链路图画清楚,让每一层只负责一件事,然后每个模块做完立刻测,不要攒到最后一口气验证。
最后再分享一个我实际用下来最有价值的小习惯:开发第一天就把链路追踪埋好。每一路流从推流到播放,带上同一个标识。前期多花一个小时,后面能省下一整天的排查时间。OneLive 目前支撑的场景还是广播型直播多,后续我打算把低延迟的实时通道也接进来,让它可以同时覆盖互动的场景,不过这又是另一个项目的故事了。