尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

WebRTC服务端录像全攻略:从转封装到踩坑实战

WebRTC服务端录像全攻略:从转封装到踩坑实战 我们团队做WebRTC远程协作产品的时候被“服务端录像”这个需求卡了很久。听起来很简单把通话过程记录下来存成文件回放时能看。但真上手才发现一个“录”字背后是转封装、时间戳、关键帧、并发写盘一整串问题文档里那些“recorder.start()”的例子压根不够用。这篇文章我就结合这些年做流媒体服务的经验把WebRTC服务端录像从方案选型到落地踩坑完整梳理一遍。内容会覆盖到底在服务端哪个环节去录、转封装和转码怎么选、SRS/Janus/自己写的方案各自怎么操作以及录出来的文件时长不对、黑屏开头、音画不同步这些高频问题的根因和解决办法。适合正在做WebRTC回放、音视频存档、在线巡检录像以及想给自研SFU加录制能力的同学参考。我先声明不推荐上来就选重型方案也不建议迷信“客户端MediaRecorder录一下就完事”。后面我会一一解释为什么。1. 先搞明白服务端录像录的是“流”不是“屏幕”1.1 服务端录像和浏览器录制不是一回事很多产品最初方案是让浏览器端用MediaRecorder录WebM然后上传。这种做法的确简单但它有三个硬伤第一录制依赖网页端一直开着用户一关Tab录像就断第二无人值守场景下做录制的那个人不在浏览器旁边根本没有“网页”可以录第三多路画面需要统一合成时浏览器端各自录各自的后期对齐时间线极其痛苦。服务端录像的本质是在媒体链路的某一环上把RTP/SRTP流“接住”再封装成文件。它不依赖任何终端只要服务没挂推流方在不在线都与你无关。这也意味着写录像模块的人必须理解RTP包结构、编码格式、封装格式和RTCP反馈而不是调一个recorder接口那么简单。1.2 媒体链路中哪个环节最适合落盘先看你采用什么媒体架构。如果是WebRTC P2P直连数据不经过你的服务器想服务端录像就必须先让双方重新协商到SFU或媒体网关把媒体流引到服务端再落盘。如果是SFU架构服务端本身就在转发RTP包录像模块挂在SFU的“下行方向”是最自然的选择——既不影响实时通话又能拿全音视频轨道。还有一点容易忽略WebRTC里媒体包是加密的SRTP服务端需要先解密拿到明文RTP才能做解封装、转码和写入文件。所以在方案选型阶段先确认你的SFU是否支持输出明文RTP包。使用Janus、SRS这类开源服务器时这一步已经帮你做好了如果是自研SFU则要考虑添加一个“录制出口”把RTP包复制一份送出去。为什么要强调“流”而不是“屏幕”因为“录屏幕”是指录浏览器的可视区域本质是采集端行为而服务端拿到的始终是媒体流只能录编码后的音视频不能录某个用户屏幕上叠加的UI元素。如果业务上一定要带操作界面的回放那只能依赖前端同时推一路“桌面共享”流服务端再一并录制——这在设计架构时要提前想清楚。这里要说明一下不是客户端录制就没有价值而是客户端录制和服务端录制往往并存客户端录本地特写服务端录客观事实。两者用途不同不要互相替代。2. 动手前先选型转封装、转码和现成引擎各有各的坑2.1 转封装最省CPU但容器和编码要匹配录制最省钱的做法是把RTP流里的VP8/VP9/H264包原封不动地封进一个容器比如WebM或MP4这个过程叫转封装remux。一句话说就是“不改编码只换盒子”。好处很直接不占CPU、延迟低、能做到非常高的并发。但转封装有前提编码和容器必须兼容。VP8/VP9天然和WebMMKV是一家人直接封装没问题H264则既能进MP4也能进FLV还能进TS。如果你收的是VP8却希望输出通用性最好的MP4那就没法直接封装因为MP4并不广泛支持VP8技术上能塞但兼容性很差。所以选转封装前一定要先确认WebRTC协商出来的编码是什么再决定最终文件格式。一个常见的组合是WebRTC推H264 → 服务端转封装成FLV → 再按需转MP4。这也是SRS录制WebRTC流的默认路径后面实操部分会展开。2.2 转码适合“统一成MP4/H264”的刚性需求转封装解决不了编码不一致的问题。比如浏览器推上来的是VP8/VP9但业务方要求录像文件统一存成H264MP4便于监控平台、老播放器、视频分析系统使用。这时候只能先解码再编码成H264这一步叫转码transcode。转码的代价是CPU/GPU。一路1080P视频软转性能强的服务器也就能跑几路所以专业方案一般会用硬编码NVIDIA NVENC、Intel QSV、VAAPI或者直接选带转码能力的LiveKit Egress这类服务。自己拿FFmpeg命令行做批处理转码也是一种思路但不推荐把线上实时录制直接压给FFmpeg进程——稳定性、断点续录、文件分片都得自己反复造轮子一两个会话看不出来并发一上来全是坑。2.3 四类主流方案的横向对比我个人把服务端录像的实现路线分成四类列个表方便大家选型。方案产物格式是否需要转码并发能力推荐场景Janus录像插件mjr需转成webm/mp4否重封装中已有Janus网关需要录像后处理SRS DVR模块flv可再转mp4否重封装高想用成熟服务器快速上录像能力LiveKit Egresswebm/mp4可关可开高房间型产品想省自研工作量自研aiortc/werift PyAV/FFmpeg任意按需取决于实现需要深度定制已有自研SFU选型的核心逻辑是有多少真实并发录像后续是否需要抽帧、剪辑、AI分析如果只是保存证据、回放查看SRS和Janus足够如果要做内容生产级的统一转码和在线剪辑再考虑LiveKit Egress或自研方案。并发量这个数字不要拍脑袋一定要先压测再定方案之后尽量不换。3. 走一遍真实链路从信令握手到文件落盘3.1 最省事的路线SRS录制我拿SRS 5/6举例它把WebRTC推流、RTMP转封装、DVR录制整合在同一个进程里配置路径很短。关键配置如下listen 1935; max_connections 1000; srs_log_tank console; vhost __defaultVhost__ { http_remux { enabled on; mount [vhost]/[app]/[stream].flv; } dvr { enabled on; dvr_path ./objs/nginx/html/[app]/[stream].[timestamp].flv; dvr_plan session; } }解释一下WebRTC推流端通过WHIP协议把流推到SRS后SRS会先完成SRTP解密、RTP重组再将H264/Opus等内容转封装成FLV。DVR模块从FLV这条链路上按session计划录像一个推流session生成一个FLV文件。timestamp字段会自动带上切片时间方便后续排序。这里有个经验SRS的DVR默认按session切文件即从推流开始到结束一个完整文件。如果会话特别长建议把dvr_plan改成按segment切分并按需要设置dvr_duration比如每60秒一个文件避免出现几十GB的巨无霸文件后续转MP4、上传对象存储都更灵活。切分策略的利弊我放到第4章讲。3.2 用Janus录像插件但要处理mjr文件如果你已经部署了Janus做SFU想快速加录像功能可以用自带的recordplay插件或videoroom插件里的录制开关。Janus录出来的文件不是通用视频文件而是.mjr格式全称是“Janus Recording”里面是带时间戳的原始RTP包序列。mjr文件必须做后处理官方提供了janus-pp-rec工具janus-pp-rec video.mjr video.webm # 如果源流是H264可以转成mp4 janus-pp-rec video.mjr video.mp4mjr的好处是保留最原始的媒体数据转换时可以更灵活地选目标容器和编码坏处是多一道后处理意味着录像不是“落盘就能播”。生产上建议这样配合Janus边录边落mjr后处理器扫描到新文件后立刻转成webm/mp4再推送到对象存储。这里要注意mjr文件是时间敏感的不能延迟太久才处理否则回放的实时性会受影响。3.3 自己写服务端接收流aiortc PyAV落盘示例最后给一条自研路线适合已经维护自研SFU的人。核心思路是用aiortc在服务端扮演一个WebRTC peer与推流端完成信令协商后把收到的音视频轨道分别送进PyAV的编码器写入MP4。下面是一个极简版示例只保留核心逻辑import av from aiortc import RTCPeerConnection, RTCSessionDescription class Recorder: def __init__(self, output_path: str): self.output_path output_path self.container None self.video_stream None self.audio_stream None def add_track(self, track): if self.container is None: self.container av.open(self.output_path, modew) if track.kind video: # VP8进WebM/MKV是稳妥组合H264则可以考虑MP4 self.video_stream self.container.add_stream(vp8, rate90000) elif track.kind audio: self.audio_stream self.container.add_stream(opus, rate48000) self.track track async def run(self): while True: frame await self.track.recv() if frame is None: break if isinstance(frame, av.VideoFrame): for packet in self.video_stream.encode(frame): self.container.mux(packet) elif isinstance(frame, av.AudioFrame): for packet in self.audio_stream.encode(frame): self.container.mux(packet) self.container.close()这段代码能帮你理解底层原理但别直接上生产。实际要补的东西很多同时处理音视频双轨、发送RTCP关键帧请求、处理丢包导致的解码错误、按时间分片、写失败重试等。后面几章会重点讲这些坑。4. 实战踩坑为什么你录出来的文件时长是0、开头是黑的4.1 音频轨缺失无声WebM的时长魔咒WebRTC会话里音频轨缺失的情况比想象中多推流端只采集了屏幕没开麦克风、设备权限没给、音频设备被系统静音等。如果你用转封装方式录WebM而文件里只有视频轨没有音频轨很多播放器会无法正确估算时长经常出现“时长0”“进度条不走”的怪问题。这不是编码的锅是WebM/MKV这类容器依赖音视频轨交错来提供时间基准。解决思路有二一是录之前先判断有没有音频轨没有就在文件命名和索引里标记no-audio回放端按照视频轨时间线播放二是用ffprobe或mediainfo对录制文件做后校验发现缺音轨就自动补一条静音Opus轨这步很直接但会引入转码成本。我实际项目里是两套同时做录制时打标记校验时自动补轨。4.2 关键帧等待为什么开头一片黑服务端从会话中间接入录制时收到的第一帧很可能不是关键帧而是一串参考帧。没有关键帧解码器无法重建完整画面录出来的文件就会以黑屏、花屏开头直到下一个关键帧到来。处理办法是在录制开始时立刻向发送端发RTCP PLIPicture Loss Indication关键帧请求并等待关键帧到达再写入文件。判断关键帧在RTP层面有技巧H264看NAL类型是否为5IDRVP8看payload描述符的S位和payload前三个字节是否为0x9D 0x01 0x2A不要只看RTP Marker位Marker只代表一帧的结束无法区分关键帧与普通帧。如果用了GStreamer可以用decodebin3内部自动等待但直接封装不走解码链路时必须自己处理。4.3 时间戳换算RTP时间戳不是播放时间这是新手最容易懵的地方。RTP包里的时间戳基于采样时钟视频通常90000Hz音频48000Hz它是采样时刻的相对计数值不是Unix时间戳也不是播放时间。录制时如果把RTP时间戳直接当PTS写进MP4音画不同步几乎是必然的。相对可靠的做法是用RTCP Sender ReportSR包建立“RTP时间戳 ↔ NTP时间”的映射关系再换算成容器要求的PTS基准。SR包在推流过程中会周期性发送里面同时带NTP绝对时间和当前的RTP时间戳。如果实现时不想解析SR退而求其次的简化方案是以第一帧到达服务器墙上时间为基准之后每收到一帧按RTP时间戳差值折算相对时间对于大多数录像场景够用但不能做到跨流严格对齐。要同时录多机位的会议场景最好把SR解析做完整。4.4 并发写盘多路会话会把磁盘写崩单路录像可能感觉不到写盘压力一旦并发十几路、几十路1080P流若每路都同步append大文件磁盘IO会成为瓶颈甚至拖垮整个SFU进程。我见过有人把录像和转发放在同一线程里同步写结果磁盘一慢通话全部卡顿。落地经验是录像模块做三层隔离——每路录制协程独立写缓存写盘用异步IO或专门的写入线程录像文件落到独立磁盘或NAS/对象存储不要和SFU的日志、状态库抢IO。同时按固定时长分片比如1到5分钟一个文件既方便上传也能把IO中小文件导致的随机写压力控制在合理范围。线上监控要单独盯iowait和录像目录的写入延迟这两项通常比进程CPU更早暴露问题。5. 进阶设计录像文件的组织、检索与安防设备接入5.1 文件命名与索引让回放按“秒”找到录像文件多了之后能不能在一堆文件里快速定位到某一路流、某一段时间直接决定了功能可用性。给文件命名时就应把检索维度编码进去我的惯例是{streamId}_{yyyyMMddHHmmss}_{yyyyMMddHHmmss}_{hasAudio}.mp4索引表至少要有这些字段streamId、startTime、endTime、filePath、fileSize、hasAudio、encoding、duration。这样回放接口只需要两个参数streamId和timeRange查数据库就能定位到对应文件切片再按需拼接播放清单。如果录像量特别大可以把索引放在ES里按时间范围做聚合查询但普通项目MySQL/PostgreSQL完全够用。5.2 海康/宇视等安防生态如何融入WebRTC录像链路很多监控项目的录像源头不是浏览器而是海康、宇视这类摄像机或平台。它们原生支持ONVIF、RTSP、GB28181但并没有原生的WebRTC能力。要让浏览器低延迟观看通常做法是设备/NVR把RTSP或GB28181流推到流媒体网关网关把流转换成WebRTC再分发给网页端。这里就有一个关键选择录像到底录哪个环节的流。我的建议是历史归档录“原始流”网页回放直接复用归档文件。因为直接从设备/NVR按ONVIF或GB28181拉原始码流时间戳信息齐全、编码统一、能精确到秒做录像检索还不占WebRTC转发带宽。如果你只录WebRTC侧一旦网页端没人观看网关可能就不转发了录像也就断了更麻烦的是很多网关会把音频去掉结果录出来的都是无声录像。这一点做安防接入时特别常见。如果要统一管理可以设计为默认由网关/流媒体服务向NVR定时拉取原始码流并归档浏览器打开历史回放时直接从归档索引里找文件通过HLS或MP4渐进式播放输出。这样WebRTC只负责实时预览不背录像的锅链路清晰得多。5.3 一套能落地的整体架构文字版我来说一套最终沉淀下来的架构不画图用文字描述推流端浏览器/App通过信令服务交换SDP媒体流进入SFU/媒体网关网关在分发实时流的同时把一份RTP流复制给“录制服务”录制服务解码SRTP、等待关键帧、将音视频封装成分片文件分片颗粒度1到5分钟每生成一个分片就向索引服务写入一条记录索引库可用MySQL/PostgreSQL或ES回放服务根据streamId加时间范围查索引返回分片播放清单浏览器用原生video标签/播放器直接播放如果涉及安防设备设备/NVR的RTSP或GB28181流先进入“设备接入网关”网关负责原始流归档必要时再转成WebRTC给实时预览。这套结构的好处是录制与实时转发解耦、存储与回放解耦后续加抽帧分析、水印、剪辑都只需要在归档侧扩展不影响正在进行的通话。路由服务、信令服务、SFU、录制服务、索引服务各自独立部署任何一个模块挂了都不至于让整个系统瘫痪。6. 给录像模块的验收清单断流、自检和可播放性如果只让我留一条建议先明确录像文件的最终用途再定技术选型。只是留证据、回放优先选SRS/Janus这种成熟服务器别一上来就造录制轮子如果业务里已经有自研SFU也不要在SFU主路径里同步写盘单独开录制模块。另一个经验是任何录制方案都要在验收清单里加上“断流续录”和“异常文件自检”两个用例。推流方中途掉线、服务器重启、磁盘写满都要有明确的文件切片和重试机制。上线前我会用一个模拟推流脚本不停断开重连把生成的录像全部用ffprobe检查一遍发现损坏就追查到底是丢包、时间戳还是切片时机的问题。等到真发生事故时会发现录像服务最需要的不是“录得漂亮”而是“不丢、可查、能播”。这些坑我几乎都踩过一遍写出来希望你能少走点弯路。
返回列表