资讯动态

基于LiveKit重构WebRTC低延迟流媒体平台:架构设计与实践

发布时间:2026/10/23 7:04:24 来源:尧图企业网站定制
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配上。Lets 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 ?expires1700000000 tokeneyJhbGciOi... definitionhd watermark12345服务端在处理播放请求时会先校验token和过期时间再查一遍视频的发布状态。如果视频正在转码中返回HTTP 403和一个自定义错误码播放器收到后自动轮询转码进度接口转完再加载。一开始我图省事直接返回错误让用户手动刷新后来被运营提了好几次意见才改成自动轮询。加密方面点播文件用了HLS AES-128加密密钥归在密钥管理服务里统一存储。密钥发放和播放鉴权是同一次请求完成的省掉了一次密钥分发延迟。实测对起播时间的影响只有30毫秒左右完全可以接受。5. 语音识别STT与AI能力集成5.1 STT场景定义哪些流需要转写哪些不需要语音识别不是所有流都要跑一遍的纯CPU识别一路实时音频的成本比你想象得高。我一开始贪多求全把所有音轨都送进识别服务结果服务器成本翻倍识别准确率还被无关噪声拖累了。痛定思痛后我给平台加了一个音频智能路由层。它会根据频道类型和轨道元数据做判断会议类频道全部转写监控类频道只在有报警事件触发时转写直播类频道默认不转写用户点击字幕按钮后才动态开启。这个路由规则可以做到毫秒级判断运行时开销几乎可以忽略。路由判断的逻辑很简单直播和会议场景在创建时就已经打了标签报警事件也有独立的触发器所以只需在媒体服务内部维护一张映射表频道ID对应是否需要STTSTT服务实例地址是哪个。这样音频分流和转写任务是解耦的扩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。毕竟架构是长出来的不是设计出来的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑