☰
基于LiveKit重构WebRTC低延迟流媒体平台:架构设计与实践
2026/10/23 7:04:23 网站建设 项目流程

1. 项目概述

1.1 核心需求解析

用了多年EasyDSS这套流媒体方案,说实话早期版本应付小区监控、小型直播场景还算凑合,一旦接入量上来,转码瓶颈和信令抖动立刻暴露。尤其最近两年WebRTC成了浏览器实时通信的事实标准,低延迟互动直播、在线课堂、远程巡检这类场景,靠传统的RTMP/HTTP-FLV推流已经跟不上节奏。所以我把这套老平台从底层拆开重做了一遍,核心思路就是用一个开源实时引擎替换掉原来笨重的信令网关,再把媒体处理链路全部收编进微服务架构里。

这个项目的本质,是用一套统一架构同时吃下四类需求:一是WebRTC低延迟双向通信,二是视频文件转码与点播回放,三是语音识别转写,四是集群化部署的负载均衡与故障转移。标题里提到的"重构",不是简单换个SDK,而是把信令、媒体、转码、存储、调度这些模块彻底解耦,让每一块都能独立扩展。

如果你手头正在维护一套自研的流媒体系统,或者用开源组件搭过音视频服务,这篇文章应该能帮你省掉几个月的试错时间。我会把架构拆分思路、关键模块替换方案、集群调度策略、踩过的坑,以及从信令到媒体流的全链路调试方法,完整过一遍。

1.2 重构前的老架构痛点

老平台走的是经典三层结构:设备端通过RTMP或GB28181推流到接入层,接入层转封装成FLV交给CDN分发,点播走Nginx静态文件服务,信令则用自研的WebSocket协议硬扛。这套结构在1000路以内还能稳定运行,超过这个量级就出现三个要命的问题。

第一个问题是信令网关的单点瓶颈。所有控制消息,包括推流鉴权、播放请求、设备上下线,全部挤在一台信令服务器上,一旦连接数超过设计阈值,整个系统的建流成功率直线下降。第二个问题是转码服务的资源浪费,老方案里转码和封装是绑在同一个进程里的,一路4K视频转码能占满四个CPU核心,其他任务全部排队。第三个问题是协议扩展性差,新业务要支持WebRTC,老的信令栈根本不认识SDP和ICE协商,硬接的话等于要推倒重写。

这三个痛点在项目启动前做了量化评估:信令服务在800路并发时CPU占用就冲到85%以上,转码任务排队平均延迟超过2秒,协议扩展一个功能普遍需要两周开发周期。结论很简单,必须重构,而且不能再用单体思路去缝补。

2. LiveKit重构方案:为什么选它

2.1 选型对比:LiveKit、Janus、mediasoup怎么选

做WebRTC服务端选型,市面上主流就三套:LiveKit、Janus、mediasoup。我花了三周时间把三套都搭起来做了压测,最后选了LiveKit,理由很实际。

Janus胜在灵活,所有插件都是独立进程,但是部署运维成本高,每个插件都要单独配一套Nginx转发规则,而且它的ICE处理逻辑偏底层,排障需要你非常熟悉RFC标准。mediasoup的API设计是纯Node.js风格,和我们的服务端技术栈比较接近,但它只提供媒体层能力,信令和房间管理要完全自己写,这个开发量比想象中大多了。

LiveKit的优势是"开箱即用":房间管理、信令协商、SFU媒体转发、录制接口、token鉴权全都内置了。它本身就是分布式设计,多节点可以通过Redis做协调,节点自动发现和故障转移都处理好了。压测数据也很能打,单台8核16G的服务器实测跑了2000路音频+500路视频转发,CPU占用稳定在60%左右。更重要的是它的客户端SDK覆盖了Web、iOS、Android、Flutter,大量端侧逻辑不用重复造轮子。

2.2 定位调整:LiveKit不只做通信层,还做控制中枢

很多人把LiveKit当成一个纯WebRTC通信组件用,我在重构里把它提到了更高的位置:作为整个平台的信令和控制中枢。这是因为LiveKit内部的房间状态机设计得非常清楚,参与者加入/离开、轨道发布/取消、数据消息广播,这些语义刚好能映射到流媒体平台里的推流/拉流/设备控制。

具体做法是,设备端的推流动作建模成"参与者加入房间":摄像头或编码器推流前,先向LiveKit请求一个带自定义属性的token,属性里携带设备ID和通道ID;直播观看则建模成"订阅房间内轨道",播放器SDK直接订阅对应视频轨道。这种建模方式让我省掉了一整层自研信令服务,原来需要自己维护的在线状态、心跳超时、重连逻辑全部交给LiveKit处理。

不过注意,LiveKit默认房间是任何人都能主动加入的,生产环境必须开启房间访问控制。我在每个房间创建时绑定了服务端校验的回调接口,只有在业务系统里完成鉴权的设备才能拿到入会token。这样通信层的开放性和业务层的安全性就解耦了。

2.3 服务部署:一套LiveKit集群的落地配置

LiveKit集群部署官方文档讲得比较简略,真正落地的细节得自己磨。最简配置是单节点加Redis,适合验证;生产环境我直接上了三节点集群,部署图示如下:

浏览器客户端/移动端SDK │ ▼ 负载均衡 / Nginx L4转发 │ ┌────┼────┐ ▼ ▼ ▼ 节点A 节点B 节点C │ │ │ └────┼────┘ ▼ Redis协调层(房间状态、节点发现) │ ▼ 媒体传输层(SFU转发/录制/转码)

核心配置参数有几个必须说清楚。第一,rtc.tcp_port范围要按实际防火墙规则放开,UDP端口段设置为20000-30000,这是一般云服务商安全组的默认放行段。第二,redis.address必须配置为所有节点都能访问的内部地址,不建议走公网,省得晚高峰延迟影响ICE候选收集。第三,每个节点实例设置node.ip为本机内网IP,这样节点间流量不走公网绕路。

如果你只有一台服务器,又想先跑通测试,一个节点加Redis完全够,官方镜像里用--dev模式可以省掉TLS证书配置,但浏览器WebRTC在非安全上下文下会拒绝getUserMedia,所以公网测试务必把TLS配上。Let's Encrypt的证书申请脚本加上renew定时任务是最省心的免费方案。

3. 核心能力改造:WebRTC低延迟链路

3.1 浏览器端接入:从HTTP拉流升级为WebRTC订阅

老平台的播放器用的是HTTP-FLV或者HLS,延迟在3到10秒之间,做直播答题、远程手术示教这类场景完全不行。WebRTC的端到端延迟是可以做到1秒以内的,但前提是整个链路不能有转码插队。我在新平台里保留了两条播放通道:默认走HTTP-FLV兼容老设备,互动场景走WebRTC直连。

浏览器端接入LiveKit的代码很简洁。我封装了一个播放器适配层,内部逻辑分三步:首先是请求业务服务器换取token,然后创建Room实例并连接,最后订阅远端视频轨道并挂载到<video>标签。

import { Room, RoomEvent } from 'livekit-client'; async function startWebRTCPlayback(channelId, videoElement) { // 1. 向业务服务端获取带权限的token const resp = await fetch(`/api/livekit/token?channelId=${channelId}`); const { token, serverUrl } = await resp.json(); // 2. 创建房间并连接 const room = new Room({ adaptiveStream: true, dynacast: true, }); await room.connect(serverUrl, token); // 3. 订阅远端视频轨道 room .on(RoomEvent.TrackSubscribed, (track) => { const video = track.attach(videoElement); video.play().catch(() => {}); }) .on(RoomEvent.TrackUnsubscribed, (track) => { track.detach(); }); }

这里有两个参数值得特别注意:adaptiveStream和dynacast都建议打开。前者会让服务端根据网络情况动态降低发送分辨率,后者能让客户端暂停未被观看的视频流,这两项对移动端流量节省效果非常明显。

实测下来,在普通家用宽带上,1080P的WebRTC直播延迟能稳定在400到800毫秒。需要提醒的是,Windows端Chrome浏览器的ICE连接偶发会走TCP回退,延迟会跳到2秒左右。这个问题可以通过在服务端强制UDP候选优先解决。

3.2 服务端接入:GB28181设备如何纳入WebRTC体系

这个项目里最麻烦的不是浏览器端,而是把老旧的监控设备拉进WebRTC体系。很多摄像头只支持GB28181协议,它们不会SDP协商,更不懂ICE,直接把它们当LiveKit客户端是行不通的。

我的处理方案是加了一个"协议网关服务",专门做GB28181到WebRTC的桥接。设备推流到网关时,网关通过LiveKit服务端API创建一个房间,再把设备端的PS流解封装成RTP包,转成WebRTC的音频/视频轨道推给LiveKit。观众端订阅这个房间的轨道时,看到的就是一个标准WebRTC流。

// 协议网关内部核心伪代码 func bridgeGB28181ToLiveKit(streamURL string) { session := StartGB28181Session(streamURL) room := lkClient.CreateRoom("camera-001") publisher := room.ConnectAsPublisher("gb-bridge-01") for { psPacket := session.ReadPSPacket() rtpPacket := DemuxPS(psPacket) // PS解封装为RTP track := ConvertRTPToVideoTrack(rtpPacket) publisher.PublishTrack(track) } }

这个桥接方案有个隐藏的红利:以前GB28181设备最多被几个并发拉流就撑不住了,现在通过LiveKit的SFU中转,一路源流可以分发到几百个观众端,因为观众不再直接和摄像头建流,而是和服务器建流。设备端只维护和服务器的单条连接,压力小了一个量级。

3.3 信令与业务系统对接:鉴权、拉流、踢流怎么做

LiveKit的Token有完整的JWT标准实现,但业务系统对接时要注意一个坑:默认Token的有效期是7天,这对直播场景太长了。我改成业务服务每次请求都生成短期Token,有效期默认2小时,直播结束后服务端主动调用撤销房间接口,确保过期Token无法再次入场。

踢流操作也有讲究。直接调用LiveKit的删除参与者接口只能断开一条连接,如果设备端异常重连,马上会带着新Token回来。更稳妥的做法是在业务层维护一块黑名单,在鉴权回调时直接拒绝,从源头掐断。这样就算用户暴力破解了Token,黑名单校验依然生效。

再看存量用户的平滑迁移。老平台的所有用户信息、设备权限、频道关系都存在业务库里,直播间的概念和LiveKit房间并不完全等价。我加了一个映射表,把业务频道表的主键和LiveKit房间名做了绑定,查询走业务库,媒体流走LiveKit,两边各司其职。

4. 视频转码与点播VOD:从"能看"到"看得流畅"

4.1 转码链路重构:硬解硬编+GPU池化

老平台转码是纯CPU软编,一路1080P转720P要吃掉两个半核心,几十路并发就能打满一台高配服务器。重构后的转码服务跑在GPU池上,用NVIDIA的硬编硬解接口,单张主流显卡实测能同时处理8到12路1080P转码,CPU占用只有原来的十分之一。

我之前在某公司干活时,曾经把CPU转码和GPU转码混跑过,结论是混合策略最优:音频转码走CPU,视频转码走GPU,封装和切片走轻量进程。这样避免GPU上的音频队列排队拖慢视频输出。转码任务的调度用了一个简单但有效的优先级公式:交互式直播转码权重为5,普通点播转码权重为2,离线批量转码权重为1,按权重抢占资源,不能让离线任务饿死实时任务。

还有一个细节很多人忽略:转码输出必须保持GOP结构对齐。点播场景下播放器拖动进度条时需要关键帧才能定位,如果GOP太大,拖动响应会非常迟钝。我把转码输出的关键帧间隔强制设置为2秒,实测拖动延迟压在300毫秒以内,用户体验提升非常明显。

4.2 点播VOD架构:从Nginx静态文件到分布式对象存储

老平台的点播文件直接丢在Nginx静态目录,磁盘用满就得加机器,扩容要手动迁移数据。重构后我把点播文件全部迁移到了S3协议兼容的对象存储,同时保留本地SSD做热缓存。播放请求先查本地缓存,命中直接回,没命中再从对象存储拉流并回填缓存。

点播索引表做了分片设计,按频道ID哈希分到64个分片里,单表数据量控制在500万行以内。每次上传转码结束后,转码服务会回调元数据服务,更新视频时长、分辨率、码率、封面图这些字段。播放器加载详情页时,只查元数据接口,真正的媒体流才走对象存储,这样详情页接口的响应时间基本恒定,不会随着视频量增加而劣化。

关于视频格式,HLS是点播兜底方案,所有端都能播。但为了低延迟场景,我还保留了一份MP4的渐进式下载文件,处理移动端微信内置浏览器时特别好用。切片时长我调整过三轮,最终定为4秒,兼容性最好,起播速度也不慢。

4.3 VOD接口设计:标准协议与自定义参数的平衡

点播接口整体遵循HLS协议标准,但我在播放地址里额外携带了几个自定义参数:加密标识、清晰度标记、播放器水印ID。这样播放器解析地址时,不需要额外发送请求就能完成鉴权和业务渲染。

GET /vod/{channelId}/{videoId}/index.m3u8 ?expires=1700000000 &token=eyJhbGciOi... &definition=hd &watermark=12345

服务端在处理播放请求时会先校验token和过期时间,再查一遍视频的发布状态。如果视频正在转码中,返回HTTP 403和一个自定义错误码,播放器收到后自动轮询转码进度接口,转完再加载。一开始我图省事直接返回错误让用户手动刷新,后来被运营提了好几次意见才改成自动轮询。

加密方面,点播文件用了HLS AES-128加密,密钥归在密钥管理服务里统一存储。密钥发放和播放鉴权是同一次请求完成的,省掉了一次密钥分发延迟。实测对起播时间的影响只有30毫秒左右,完全可以接受。

5. 语音识别STT与AI能力集成

5.1 STT场景定义:哪些流需要转写,哪些不需要

语音识别不是所有流都要跑一遍的,纯CPU识别一路实时音频的成本比你想象得高。我一开始贪多求全,把所有音轨都送进识别服务,结果服务器成本翻倍,识别准确率还被无关噪声拖累了。

痛定思痛后,我给平台加了一个"音频智能路由层"。它会根据频道类型和轨道元数据做判断:会议类频道全部转写;监控类频道只在有报警事件触发时转写;直播类频道默认不转写,用户点击字幕按钮后才动态开启。这个路由规则可以做到毫秒级判断,运行时开销几乎可以忽略。

路由判断的逻辑很简单,直播和会议场景在创建时就已经打了标签,报警事件也有独立的触发器,所以只需在媒体服务内部维护一张映射表:频道ID对应是否需要STT,STT服务实例地址是哪个。这样音频分流和转写任务是解耦的,扩STT服务时不用动媒体主链路。

5.2 实时转写链路:音频轨道如何送进识别引擎

LiveKit发布音频轨道的格式是Opus编码。大多数成熟STT引擎即使能处理Opus,直接喂的效果也不理想。我的做法是接一个音频解码服务,把Opus解码成16kHz单声道的PCM,再通过gRPC流式接口送到识别引擎。

from livekit import rtc import pyaudio async def audio_stream_handler(room): @room.on("track_subscribed") def on_track(track, publication, participant): if track.kind == rtc.TrackKind.KIND_AUDIO: audio_stream = rtc.audio_stream(track) async for frame in audio_stream: pcm = frame.data.tobytes() # Opus已解码为PCM stt_client.send_chunk(pcm)

这里有个经验之谈:实时识别要采用"增量识别+定期强制句尾"策略。纯增量识别会让断句一直悬着,字幕显示永远慢半拍;纯固定间隔切分又容易切断单词。我的做法是250毫秒推送一次识别增量,同时每2秒触发一次强制句尾判断,如果当前音频能量低于阈值就断句输出。这套规则在中文会议场景下实测断句准确率能达到九成以上。

识别结果回传走的是LiveKit的数据消息通道,客户端通过RoomEvent.DataReceived事件接收字幕,整个过程不需要额外建立WebSocket连接,复用现有媒体通道,稳定性和实时性都有保障。

6. 集群语音与EasyDSS架构整合实践

6.1 集群语音通信:多方通话与对讲的信令设计

EasyDSS老版本的对讲功能是点对点直连,两个人通话没问题,多方呼叫就乱成一锅粥。新平台里我把所有对讲逻辑收编到LiveKit的房间模型里:一个对讲组就是一个房间,组内成员加入房间即可通话。

信令设计上,每个对讲组绑定三个自定义事件:邀请入组、强制静音、紧急插话。邀请入组通过LiveKit服务端API调用参与者新增,强制静音就是修改参与者轨道权限,紧急插话则是提升某个参与者的音频发布权限并给其他人发通知。这些事件全部走LiveKit的数据消息通道,保证和媒体流在同一条路径上,不会出现信令到了但音视频没到的割裂问题。

举个例子来说,一个5人参与的对讲调度场景里,调度员用的终端权限级别最高。他按下紧急插话键的瞬间,服务端收到消息,立即把其他四个参与者的音频轨道降权(无法发声音),同时把调度员的音频轨道提权,整个过程大概200毫秒内完成。老平台做类似功能时,仅权限同步就要经过信令服务器转发两次,延迟高了一倍不止。

6.2 集群调度:从单机到三节点起步的扩容策略

平台业务量上来之后,单节点LiveKit必然扛不住。我做的集群策略很直白:横向扩展节点数量,每个节点的角色完全对等,通过Redis协调状态。下图是简化后的生产部署拓扑:

业务API服务 ---> Redis协调层 <--- 节点A LiveKit ↑ 节点B LiveKit │ 节点C LiveKit 媒体工作岗位池(转码/STT/录制)

扩容时只需在负载均衡器里添加新节点IP,新节点启动后自动从Redis拉取全量房间状态,对外提供服务。这种对等节点设计和传统主从模式最大的区别是:任何一台机器宕机,剩余节点都能自动接手其房间的媒体流,因为房间状态是共享的。

我这里还做了一个媒体流主动迁移的优化。当某节点的CPU负载超过70%时,调度器会挑选一部分观看类房间(无发布端的纯订阅房间)迁移到低负载节点。迁移时先在新节点建好同名的空房间,再把旧节点的webhook事件转发过去,观众端SDK会感知到断线并自动重连。实测一次迁移对观众端的影响只有一次ICE重协商,体感几乎无感。

6.3 与EasyDSS老体系的兼容策略:双轨并行平滑过渡

直接推倒重来风险太大,老设备的接入协议和运维习惯不是说改就能改的。我采用了双轨并行的过渡方案:老设备继续走GB28181或RTMP流到协议网关,网关转成LiveKit轨道参与新平台调度;新设备则直接使用LiveKit SDK接入。

这个方案的关键在于统一了媒体的"逻辑标识"。老设备的通道ID和新设备的房间ID在业务库里保持唯一映射,业务层只认逻辑标识,不关心底层是RTMP还是WebRTC。这样上层业务系统完全不用改,接入层可以单独演进。

我用一段配置说明了映射关系,维护起来非常简单:

# 老设备通道号 -> LiveKit房间映射表 channels: - old_id: "cam-001-rtmp" room_name: "camera-001" protocol: "rtmp" - old_id: "cam-002-gb28181" room_name: "camera-002" protocol: "gb28181" - old_id: "intercom-group-01" room_name: "talk-group-01" protocol: "livekit"

双轨并行期间最大的坑是"老设备拉流逻辑没变,但新平台的鉴权变了"。老设备在深夜可能会自动重连,按老逻辑只能拿到老的加密方式,直接就失败了。我专门写了一个兼容鉴权模块,在过渡期内同时接受新旧两套加密方式的Token,稳定运行一个月后,确认老设备列表已经基本退网,才关闭旧鉴权通道。这个过渡期如果压缩到几周,早晚会被老设备的重连投诉砸到。

7. 常见问题与排查技巧实录

7.1 LiveKit连不上:ICE协商失败排查法

遇到LiveKit连不上,核心排查点是ICE候选收集是否完整。客户端日志里如果只有host候选,没有srflx或relay候选,说明NAT穿透配置有问题。我常用的排查流程:

# 1. 确认UDP端口段在防火墙里放行 sudo firewall-cmd --list-ports # 2. 检查LiveKit配置文件中的UDP端口范围 grep -A3 "rtc:" /etc/livekit.yaml # 3. 用WebRTC工具页测本机UDP连通性 # 4. 客户端日志抓取ICE候选列表,确认是否有srflx类型

生产环境我建议强制开启TURN服务,把relay候选作为兜底。因为部分校园网和企业网对UDP封锁严重,没有TURN就只能干瞪眼。TURN服务的用户认证需要和LiveKit Token联动,这块配置官方文档写得不算清楚,我踩了一晚上才搞定,核心就是要让TURN服务的用户名和Token里的turn属性匹配。

7.2 转码后花屏或音画不同步

转码花屏大概率是GPU显存不足或者驱动bug导致的关键帧丢失。排查时先看显卡的利用率,如果显存占用超过90%,立刻降低并发路数。音画不同步多半是音频转码和视频转码走了不同处理流水线,最简单的修复手段是让两条流水线的起始buf都用同一份系统时钟,并确保切分时的时间戳基准一致。

实际操作中,我遇到过GPU转码输出一切正常,但HLS切片出来音画不同步约300毫秒的诡异问题。后来定位到是切片器用了自己的参考时钟,而不是转码输出的PTS。解决方法是强制切片器的起始时间戳锚定到输入流的PTS,不要用系统当前时间,问题立刻消失。

7.3 语音识别经常漏字或断句错乱

漏字最常出现在说话人语速过快且口音较重的情况下。我在音频送入STT之前加了一个VAD语音检测模块,先过滤掉静音片段,再按语音段送入识别。这样引擎不用处理大段的静音,识别准确率能显著提升。

断句错乱则是增量识别策略的参数没调好。我这里有一组经验值:短句场景用350毫秒增量、句尾静音阈值设为-45dB;长句场景用500毫秒增量、句尾静音阈值设为-50dB。如果你要优化的场景是多人会议,建议把增量时长调小到300毫秒,否则说话人切换时的上下文残留会干扰识别。

7.4 集群节点间房间状态不同步

集群节点状态不同步的表现是:用户A在节点1创建了房间,用户B始终在节点2拉不到房间列表。检查Redis的key是否存在是最快的定位方式。

redis-cli -h <redis-host> keys "room:*"

如果key存在但状态过期,大概率是LiveKit的节点心跳配置异常。每个节点默认会向Redis上报心跳,如果节点时间不同步,会导致心跳过期时间错乱。我在部署规范里强制要求所有节点启用NTP时间同步,这个坑后来再没碰到过。

最后说一个我在整个重构过程中最深的体会:流媒体平台重构的关键不是把某个组件替换成更先进的组件,而是把"模块边界"重新划分干净。LiveKit再强,如果老的信令逻辑还藏在业务代码里,改造完也只是换了个壳。我这次把LiveKit定位成纯粹的通信与控制中枢,转码、存储、STT、调度全部独立成服务,每个服务只向外面暴露清晰的接口,后续迭代新功能时心里非常踏实。

如果你也想做类似的改造,我建议不要一上来就追求微服务满天飞,先把LiveKit单节点跑通,把WebRTC的播放链路和转码链路走顺,再逐步加入集群和STT。毕竟架构是长出来的,不是设计出来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询