1. 为什么会有 OneLive:线上活动延迟翻车后的产物
OneLive 这个名字,最初是为了救急才起的。去年团队接到一场线上新品发布会的直播需求,原本用的是某家商业直播服务,结果正式活动当天播放延迟直接飙到 30 多秒,主持人念完口令,弹幕里观众还在聊上一个环节,评论区直接炸锅。活动结束复盘时,我干脆决定自己搭一套从推流、播放到互动的轻量级直播系统,也就是后来团队内部代号叫 OneLive 的项目。
名字有两层意思:一层是“一场直播”,把事情收敛到最简单;另一层是“one live,一整套直播闭环”,从采集推流到观众播放,再到弹幕互动、自动录制回放,都放到同一个方案里,不再依赖黑盒的商业直播服务。如果你跟当时的我一样,既不想把直播交给第三方,又不想一上来就搞运维复杂的媒体服务集群,那 OneLive 这套思路可以直接参考。下面这篇文章我不会讲太虚的概念,全部是选型、配置、参数和踩坑经验,适合一个人或小团队搭一套能真实承载线上活动的直播系统时照着做。
1.1 一次让我失眠的线上发布会
那次活动的技术问题其实很典型。我们在测试阶段只开了几个测试机位,当时看画面和延迟都还行,结果正式开始时观众量一上来,播放器就开始不断缓冲,延迟一路从几秒涨到半分钟。最尴尬的瞬间是主持人说“现在开始抽奖”,观众端还停在 3 分钟前的画面,满屏弹幕都在问“什么奖”。这类问题的根源,是播放器为了平滑播放默认给了很大的缓冲,服务端又考虑了低带宽用户做了多层转码,再加上 HLS 切片时间偏长,三层因素叠在一起,延迟翻上天也就不奇怪了。
那场活动在线人数其实不算高,但口碑上的损失非常直接。也就是从那天起,我给自己定了一个看起来朴素的目标:任何一场由 OneLive 承载的直播,普通观众必须在 5 秒内看到主播当前的动作,强互动环节要压到 1 秒左右,否则就重新设计链路。
1.2 OneLive 给自己定的三个目标
后来这些想法被整理成了三个明确目标,所有技术选型都要对着这三个目标检查,达不到就直接砍掉:
- 第一,延迟可控。普通观众端到端延迟控制在 5 秒内,连麦、抽奖这类强互动场景控制在 1 秒左右。
- 第二,链路闭环。推流、播放、弹幕、录制、回放都要能跑通,不能只做半成品。半成品在正式活动里大概率会变成事故。
- 第三,成本透明。活动制直播不该按年费买商业方案,该有按量付费、用完即关、账单可查的能力。
这三个目标直接决定了后面所有协议选型和服务选型。比如最开始有人提议直接上多云多活的大集群,但 OneLive 的目标只是单场活动 5 秒延迟,那用一台有固定带宽的服务器配合 CDN 转发就够用了,完全没必要为用不上的复杂度买单。搞清自己到底要解决什么问题,比急着选工具重要得多。
2. OneLive 的技术选型:直播协议与服务器组合的取舍
选协议是搭 OneLive 的第一步,因为它直接决定观众看到多快的画面,也决定服务端要维护多少套能力。我先把市面上常见的直播协议按延迟和适用场景拉了一张表,再对照三个目标做取舍。
2.1 三种主流协议的延迟特性
| 协议 | 典型延迟 | 核心特点 | OneLive 里的用途 |
|---|---|---|---|
| RTMP | 1-3秒 | 推流最成熟,浏览器不原生支持 | 作为推流入口和录制源 |
| HTTP-FLV | 1-3秒 | 基于 HTTP 传输 FLV,PC 浏览器可用 flv.js 播放 | 桌面端低延迟播放 |
| HLS/LL-HLS | 常规6-30秒,LL-HLS约3-5秒 | 兼容性最强,iOS/Android 都可用 | 移动端播放兜底 |
| WebRTC | 0.5-1秒 | 实时性最强,但信令和网关复杂度高 | 强互动房间专用 |
| SRT | 推流端增加0.5-1秒 | 可靠传输,抗丢包,适合弱网 | 断流时的备用推流通道 |
这张表不要把它看死,直接说结论:播放协议没有一个是全能的。RTMP 做推流很稳,但浏览器不原生支持;HTTP-FLV 延迟不高,但 iOS Safari 因为不支持 MSE 用不了;HLS 哪儿都能播,但标准切片的延迟对互动环节是灾难;WebRTC 又快又准,但投入成本明显高。OneLive 的做法不是只选一个协议,而是让不同端走不同协议,再由服务端统一分发,各取所长。
2.2 服务端:为什么围绕 SRS 建核心
服务端选型也踩过几轮。当时考虑过 Nginx-RTMP、SRS、Janus、MediaMTX。Nginx-RTMP 简单,但只做 RTMP 和基础 HLS,HTTP-FLV、WebRTC 这些要另外拼模块;Janus 做 WebRTC 很强,但拿它来做普通直播的推流、录制和转封装,有点小题大做,维护成本也不低。最后我选了 SRS,主要是看中它一个进程同时支持 RTMP 推流、HLS/LL-HLS、HTTP-FLV、SRT 和 WebRTC 分发,还自带 HTTP 回调接口,可以拿到推流、断流、播放这些事件做业务逻辑。
SRS 的配置风格长得像 Nginx,团队上手快,需要微调的也就一段配置。OneLive 核心只开了四块能力,大致是这样:
# srs.conf 片段,按实际版本微调 listen 1935; http_api { enabled on; listen 1985; } rtc_server { enabled on; listen 8000; } vhost __defaultVhost__ { http_remux { enabled on; mount [vhost]/[app]/[stream].flv; } hls { enabled on; hls_fragment 2; hls_window 6; hls_part_duration 0.5; } rtc { enabled on; } }这段配置的意思很直白:1935 端口收 RTMP 推流,1985 端口提供 HTTP API,8000 端口跑 WebRTC,默认 vhost 下同时输出 HTTP-FLV、低延迟 HLS 和 WebRTC。真正用的时候还要按 SRS 版本调参数,但整体路子就是这样。
2.3 端到端链路长什么样
不画复杂的架构图,我用一段文字把 OneLive 的完整链路说清楚。
推流端有三种情况:固定机位用 OBS,脚本化视频源用 FFmpeg,弱网现场用 SRT。这些流先进 SRS,SRS 在内存和磁盘里保留最新流状态,然后同时输出多路下游——PC 播放器拉 HTTP-FLV,iOS 播放器拉 LL-HLS,强互动房间的观众走 WebRTC。与此同时,录制模块从 SRS 拉一路 RTMP 写进 FLV 文件,弹幕服务通过 WebSocket 独立接收和广播消息,Redis 只负责跨实例转发。
这条链路看起来环节不少,但每个环节都是必须存在的。只要把协议和端的关系理清,实际部署就是一台媒体服务器加一个弹幕服务,再加上云上的转码和录制任务,比想象中清爽很多。也正是因为链路足够简单,后续排查故障时才能快速定位问题出在推流端、服务端还是播放器端。
3. 推流端参数调优:不只看码率,关键帧间隔才是延迟暴君
很多人搭直播上来就问“码率调多少”,这其实是最后一个问题。先搞清楚编码参数对延迟的影响,码率才有意义。OneLive 推流端最有价值的经验都不是所谓“高码率拉画质”,而是一组看起来不起眼的编码参数。
3.1 推流命令与核心参数
OBS 里我推荐这么设:输出模式选“高级”,视频编码器用 x264,1080p 场景码率给 4.5Mbps,720p 给 2.5Mbps,音频用 AAC、48kHz、192kbps,关键帧间隔设为 2 秒。关键帧间隔(也就是 GOP)才是和延迟强相关的参数,因为播放器必须等到一个关键帧才能开始解码,关键帧间隔越长,观众从开始播放到看到画面的时间越不稳定。
如果不用 OBS,而是用 FFmpeg 推流,我实际用的命令大概长这样:
ffmpeg -re -i source.mp4 \ -c:v libx264 -preset veryfast -tune zerolatency \ -g 60 -keyint_min 60 -sc_threshold 0 -bf 0 \ -b:v 4500k -maxrate 4500k -bufsize 9000k \ -c:a aac -b:a 192k -ar 48000 \ -f flv rtmp://one-live.example.com/live/meeting几个参数得解释一下:-re是让 FFmpeg 按原始时间戳匀速读取,避免推流速度失控;-g 60表示每 60 帧出一个关键帧,如果视频是 30fps,正好 2 秒一个关键帧;-bf 0是关闭 B 帧;-bufsize一般是码率的两倍,给编码器一点缓冲但不要太多。这里的域名和流名直接换成你自己的 SRS 地址。
3.2 编码参数里的“反直觉”细节
编码参数里有些细节是反直觉的,最容易踩坑。
第一,B 帧要关。B 帧能让压缩率更好,但代价是流程重排和额外延迟。直播场景下为了省那一点码率牺牲延迟,非常不划算。第二,GOP 不能一刀切。2 秒是通用选择,但如果观众端切换清晰度很频繁,可以适当把关键帧间距缩到 1 秒,这样切档后更快出画面,代价是码率会高一些。第三,-tune zerolatency这个参数不是万能药。它减少编码器缓冲,适合直播;但如果你同时要做精品录制,就别用它,因为画面细节会有不少损失。我的做法是直播和录制定义为两条独立任务,直播用低延迟参数,录制从另一路流做转码。
音频也有讲究。采样率最好统一到 48kHz,推流端、服务端、播放端一旦采样率不一致,轻微的音画不同步非常难排查。你可以用 ffprobe 检查推流实际输出的音频参数,如果发现混入了 44.1kHz 的源,就提前做 resample,别把这个隐患留到正式活动里。
3.3 弱网环境下的断流自救
推流端还有一个绕不开的问题是弱网。曾经有次活动,主播在酒店用 Wi-Fi 推流,隔几分钟就断一次,OBS 重连倒是积极,但每次重连后观众端都要重新缓冲,体验很差。后来我给 OneLive 加了两道保险。
第一道是 FFmpeg 推流的自动重连脚本。FFmpeg 本身断线不会自动重推,用 shell 包一层循环就行:
while true; do ffmpeg -re -i "$INPUT" \ -c:v libx264 -preset veryfast -tune zerolatency \ -g 60 -bf 0 -b:v 4500k -maxrate 4500k -bufsize 9000k \ -c:a aac -b:a 192k -ar 48000 \ -f flv "rtmp://one-live.example.com/live/meeting" echo "推流断开,3 秒后重试" sleep 3 done第二道是给重要直播准备 SRT 备用通道。SRT 自带丢包重传和拥塞控制,在 10% 左右的丢包环境下比 RTMP 稳很多,代价是推流端多 0.5-1 秒延迟。对于发布会、公开课这类不是极限低延迟的场景,用 SRT 推流收益很明显。OneLive 最终把重要场次的默认推流从 RTMP 换成了 SRT,断流率肉眼可见地降了下来。
4. 播放端低延迟改造:从 HLS 切片到 WebRTC 兜底
服务端和推流端都准备好后,最影响观众体验的是播放端。OneLive 播放端经历了三版迭代,第一版全是 HLS,延迟惨不忍睹;第二版 PC 端改成 HTTP-FLV,移动端保持 HLS;第三版才把 iOS 切到 LL-HLS,并给互动房间留了 WebRTC 兜底。
4.1 为什么默认方案是 HTTP-FLV
桌面端是 OneLive 的大头,而桌面端低延迟最省事的方案就是 HTTP-FLV。SRS 开了http_remux之后,观众直接用 flv.js 拉一个.flv地址就能播放。这里有几个 flv.js 的配置很关键:
if (flvjs.isSupported()) { const flvPlayer = flvjs.createPlayer({ type: 'flv', url: 'https://live.example.com/live/meeting.flv' }, { enableStashBuffer: false, isLive: true, autoCleanupSourceBuffer: true }) flvPlayer.attachMediaElement(video) flvPlayer.load() flvPlayer.play() }enableStashBuffer: false一定要关掉。flv.js 默认会把数据攒进内部缓冲区,追求平滑播放,但直播场景下这等于人为加了 2 到 3 秒延迟。关掉之后画面可能偶尔多一点缓冲,但延迟能明显压下去。autoCleanupSourceBuffer则是防止长时间直播时内存不断上涨,跑两小时以上就会出现明显卡顿,这个参数能缓解。
4.2 iOS 端与 LL-HLS 的取舍
iOS Safari 不认 flv.js 那套 MSE,所以移动端只能走 HLS。第一版我用的是普通 HLS,切片 6 秒,播放器再叠加缓冲,实测延迟经常 15 秒以上,互动环节基本没法用。后来 SRS 支持 LL-HLS 之后,我把切片改小,同时开启了分片内部分段(part)机制,Safari 在发生成完整个切片前就能拉到前几个 part,延迟被压到 3-5 秒。
LL-HLS 的配置我放在 HLS 块里,核心是hls_fragment控制在 2 秒,hls_part_duration控制在 0.5 秒,hls_window设成 6 秒左右,避免播放列表无限膨胀。实际效果是 iOS 上的延迟从“聊天跟不上画面”变成了“基本能同步”,对发布会来说完全够用。
4.3 多端播放器回退策略
播放器策略不是简单写死一种格式就完事。OneLive 的播放页做法是先做环境检测,再决定加载哪种播放器:
- 支持 MSE 且是 PC 浏览器:优先 HTTP-FLV,配置低缓冲模式。
- iOS Safari 或不支持 MSE:原生播放器拉 LL-HLS。
- 强互动房间(抽奖、连麦、投票):单独接 WebRTC 播放,保证延迟在 1 秒左右。
还有一个兜底逻辑:HTTP-FLV 播放过程中如果连续报错超过 3 次,就自动切换到 HLS 链路。这个回退不追求低延迟,但至少保证观众能看到画面。做过线上活动的人都知道,90% 的投诉都来自“黑屏”,而不是“慢了 3 秒”,所以回退机制必须存在。平时测试时,我也会强制把 PC 浏览器切到移动预览模式,验证 HLS 链路是通的。
5. 弹幕互动和消息架构:一个不抖动的聊天室怎么搭
OneLive 不只是放视频,弹幕互动才是活动氛围的放大器。弹幕模块虽然独立于视频流,但它承担的压力一点不小,搞不好会拖垮整个活动页面。
5.1 WebSocket 网关与 Redis Pub/Sub
弹幕消息的路径是这样的:用户点击发送,客户端把消息发给 WebSocket 接入层;接入层做鉴权和限流,再推进消息总线;订阅了同一个房间的所有客户端从总线拿到消息并展示。OneLive 的消息总线用的是 Redis Pub/Sub,原因很简单:单实例时进程内广播够用,但后面如果加了第二台接入层,跨实例广播就必须走独立组件,Redis 是现成的,运维成本低。
服务端做了两个核心动作。一是连接鉴权,进直播间前由业务服务签发一个带过期时间的 token,WebSocket 连上后先发鉴权消息,网关校验通过才允许订阅房间。二是限流,每个用户每分钟最多发 10 条,抽奖时刻临时放开到 30 条,防刷屏也防消息雪崩。如果不限流,用户拿脚本就能把聊天室刷成一片空白,活动效果直接归零。
5.2 客户端消息队列与批量渲染
弹幕消息的流量峰值往往出现在抽奖时刻。比如抽奖时一秒可能进来几百条消息,如果让每条消息都立刻操作一次 DOM,页面会卡到完全没法看。OneLive 的前端解决办法是消息队列加批量渲染:收到消息先推入队列,用requestAnimationFrame每帧取出一批消息,最多 20 条,统一创建和销毁弹幕元素。
const queue = [] function enqueue(msg) { queue.push(msg) } function flush() { const items = queue.splice(0, 20) items.forEach(item => renderDanmaku(item)) requestAnimationFrame(flush) } requestAnimationFrame(flush)这个策略看起来简单,但效果非常明显。高峰期的渲染帧率从 10 多帧恢复到 60 帧,页面其他交互也不卡了。至于极端超量场景,比如单秒上千条,我会丢弃一部分普通消息,只保留系统公告和抽奖口令,宁可不刷屏,也不让活动交互崩溃。
5.3 心跳、断线重连与消息补偿
WebSocket 还有一个天然问题:断线。移动网络切换、后台切回来都会导致连接断开。OneLive 做了三件事。
心跳是每 30 秒发一次 ping,服务端 60 秒内收不到就断开连接并清理房间成员。断线重连使用指数退避加随机抖动,第一次重试 1 秒后,第二次 2 秒,第三次 4 秒,最多不超过 15 秒,再加 0-3 秒的随机延迟,避免服务端恢复时所有客户端同时重连造成重连风暴。最重要的是消息补偿:重连成功后,客户端通过 HTTP 接口拉取最近 60 秒的公告和抽奖消息,把断线期间漏掉的系统消息补回来。用户消息可以不补,但官方公告一条都不能少。
6. 录制、转码与成本控制:别让一场直播烧掉一个月预算
直播活动结束后往往还要有回放,所以录制和转码不能省,但也不能不管成本。OneLive 在这方面吃过亏,最后总结出来的原则是:录制要稳、转码要少、资源要能关机。
6.1 录制链路与回放自动生成
录制模块从 SRS 拉一路 RTMP,实时写入 FLV 文件。我不用 MP4 直接录,是因为 FLV 对中途断流更宽容,即使进程被杀,前面已经写入的内容也大概率能拿回来回放,直接录 MP4 遇到中途崩溃,文件经常直接报废。命令也很简单:
ffmpeg -i rtmp://one-live.example.com/live/meeting \ -c copy -f flv /data/records/meeting_20250101_200000.flv活动结束后再把 FLV 转封装成 MP4,用-c copy不重新编码,速度快也不损画质:
ffmpeg -i meeting_20250101_200000.flv -c copy meeting_20250101_200000.mp4文件名一定要带时间戳,否则主播断线后重新推流,覆盖同名文件就会把前面的内容丢了。OneLive 是按 10 分钟切一个分片文件,回放前再合并,这样任何一个分片出问题都不会影响整场回放。
6.2 转码档位怎么定
转码是直播里最容易预算失控的环节。OneLive 第一版贪心,直接出了原画、1080p、720p、480p、360p 五档,服务器日常 CPU 70% 以上,活动还没开始成本就上来了。后来砍成两档:原画加 720p,大部分观众 720p 足够,原画留给画质要求高的回放。转码用 NVENC 之类硬件编码,参数也要注意低延迟:
ffmpeg -i rtmp://one-live.example.com/live/meeting \ -c:v h264_nvenc -b:v 2500k -g 60 -bf 0 \ -c:a aac -b:a 96k -ar 48000 \ -f flv rtmp://one-live.example.com/live/meeting_720p转码进程会额外增加延迟,所以直播链路里我只转一档,而且不让转码进程和主播放链路抢 CPU。如果想要更高画质,回放阶段重新转一版 1080p 就行,观众看回放不在乎多等两分钟。
6.3 活动制直播的成本止血经验
成本控制本质上是要能“用完就关”。OneLive 的活动流程是:开场前两小时启动一台按量计费实例,开转码服务、弹幕服务、录制任务;活动结束后一小时自动扫描所有进程,超过 30 分钟没有推流就强制停止转码和录制,只保留媒体服务器实例待机。这个自动清理流程救过我一次——有场活动凌晨结束,转码进程忘了关,CPU 满负荷跑到第二天中午,要不是账单提醒可能还要继续烧。
带宽方面,我建议按流量计费而不是买固定带宽包,固定带宽包如果当天没用到,钱就白花了;按流量计费更贴合活动制直播的波浪式用量。CDN 流量也只在播放人数上来时才有意义,观众少的内部测试直接走服务器出口,能省出不少钱。
7. 真实踩坑记录:时间戳不同步、CDN 缓存与断流恢复
OneLive 现在跑得比较顺,但这一路踩了不少坑。这几个坑非常典型,每一个都能让线上活动翻车,值得单独记录一下排查思路。
7.1 音画不同步:凶手是一串编码参数
第一次内部测试时,出现了画面比声音慢 2 秒的问题。我按这个顺序排查:先看主播端 OBS 预览,声音和画面是同步的,排除采集源问题;再看服务端日志,没有丢包和告警;最后录了一段流,用 ffprobe 检查音频和视频流的起始时间,发现视频流的 start_time 比音频流大了不少,问题锁定在编码端。
原因就是编码参数里同时开了 B 帧、使用了默认的 lookahead 缓冲,导致视频编码器输出顺序被重排,播放器为了等视频帧不断往后积压。解法是关掉 B 帧、加-tune zerolatency、必要时降低rc-lookahead。改完之后重新录制,两个流的起始时间对齐,画面和声音就回到了同一节奏。直播推流的编码器参数不是越小越好,也不是越快越好,关键是要减少播放端的等待。
7.2 直播变成“回放”:CDN 缓存惹的祸
还有一次观众反馈“直播画面可以拖进度条”,一查吓一跳。第一次排查先看 CDN 回源有没有问题,回源正常;再看播放器,不是播放器的问题;最后翻响应头才发现,CDN 默认把.flv当普通文件缓存了,后面的观众访问到的是 CDN 边缘节点里的旧文件,等于在看回放。
解决办法是在 CDN 配置里针对直播域名下的/live/路径关闭缓存,转发模式直接回源;同时在服务端响应头里加上Cache-Control: no-cache,HLS 的 m3u8 文件也要禁止缓存。流媒体文件是时间相关的,缓存一个秒级别的旧文件就是事故。这类问题在本地环境测不出来,因为本地没有 CDN 边缘缓存,只有上了正式域名,从不同地区访问时才暴露。
7.3 断流恢复:自动重连脚本与 SRT 备用通道
最后一个坑是断流恢复。主播端网络抖动,OBS 自动重连后,服务端有了新推流,但部分观众还是黑屏或者一直停在旧画面上。后来发现是播放器没有立刻感知新的推流 session,继续等旧的缓冲,最后因为超时才重新拉流。解决方法是播放器加一个状态监听:页面回到可见状态时主动检查是否能拉到最新时间戳,连续 5 秒无新数据就强制刷新播放器;推流端重连成功后也等待 2 秒再推,给服务端清理旧 session 留时间。
另一个更稳的做法是启用 SRT 备用推流通道。SRS 同时监听 RTMP 和 SRT,重要活动的主播端网络不好时,直接切到 SRT 地址继续推。SRT 的丢包重传把很多弱网抖动消化在传输层,播放端基本无感。OneLive 后期把重要场次的推流默认改成 SRT,断流恢复时间从平均十几秒降到几秒内,这个改进是最值得的。
做完这些,OneLive 才算是真正从“能播”变成了“能稳定地播”。如果让我重新搭一遍,我大概率还是会从这三个切口入手:先解决延迟,再补稳定性,最后才上高级互动。最后分享一个我自己很受用的习惯——每次活动前都做一次全链路演练,主播端故意断网、播放端强制切后台、弹幕脚本刷量,把能想到的故障全部模拟一遍。这套演练虽然麻烦,但比活动当天手忙脚乱强得多。