ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RTSP转WebRTC:协议原理、网关部署与浏览器播放实战

RTSP转WebRTC:协议原理、网关部署与浏览器播放实战

简介:面向Web前端与流媒体开发者的RTSP转WebRTC播放方案资源包,解决网页端无需插件实时播放RTSP监控或直播流的需求。包内含Windows AMD64平台可直接运行的webrtc-streamer服务程序,配套前端播放组件、信令配置和多个HTML/JS示例,覆盖媒体捕获、H.264转VP8/VP9编解码、SDP协商到页面渲染的完整链路;利用该程序可接收RTSP流并输出符合WebRTC规范的封装格式,再由浏览器原生API完成播放。压缩包共94个文件,以js、html、css等前端资源为主,另有wasm和PoseNet等模型文件,整体约8.77MB,目录结构清晰,便于按功能定位和二次开发。已有524人学习下载,适合具备一定WebRTC或流媒体基础、希望快速集成实时视频能力的开发者。通过自带示例与配置说明,可以迅速搭建RTSP到WebRTC的转换服务,理解RTCPeerConnection、RTCSessionDescription等核心API的实际调用方式;同时,资源中提供的多房间相关示例也可用于扩展视频会议或低延迟直播等业务场景,实用价值较高。

1. RTSP转WebRTC:为什么浏览器直接看不了监控流

RTSP流在浏览器里打不开,不是兼容性的锅,是协议层面的死结。摄像头吐出来的RTSP流走的是TCP 554端口,靠客户端主动拉流;WebRTC走的是UDP,靠ICE协商打洞,由浏览器原生支持。两者之间缺一道翻译:把RTSP的SDP翻译成WebRTC的SDP,把RTP转成SRTP。这道翻译链路,就是常说的"RTSP转WebRTC网关"。链路看着短,实际牵扯信令、媒体协商、NAT穿透三个环节,每个环节都有坑。这里把整条链路拆开讲:从选型到部署,从参数设置到前端接入,再附上实际踩过的黑屏、花屏、高延迟排查记录。适合刚接手网页监控项目、想把内网RTSP摄像头推到浏览器上播放的开发者。目标就一个——照着搭,能出画面,延迟落在可接受范围。

2. 技术选型:为什么弃用HLS,网关怎么选

2.1 RTSP与WebRTC的协议差异,到底差在哪

RTSP本身不传媒体,它只是控制协议,真正的音视频数据走在RTP上。RTSP用TCP 554端口做控制通道,媒体数据既可以走UDP,也可以走TCP隧道。而WebRTC不认识RTSP,它只认SDP——一份描述媒体格式、传输地址、编解码参数的文本。网关要做的事,本质上是把RTSP侧的SDP翻成WebRTC侧的SDP,再把RTP包转成SRTP包,同时处理DTLS握手。

这个转换不能简单透传。RTSP的RTP通常不带加密,WebRTC的SRTP强制加密;RTSP的传输地址是摄像头直接暴露出来的IP和端口,WebRTC要经过ICE协商选出路径。还有时间戳:RTSP的RTP时间戳基于采样时钟,WebRTC要求用毫秒时间戳做同步。这些差异决定了网关不能是一个单纯转发器,必须做真正的协议翻译。

2.2 为什么不用HLS,也不用MJPEG

很多人在网页里看RTSP,第一反应是转HLS。HLS确实成熟,但它的切片机制决定了延迟下限:一个切片通常2到6秒,播放器还要攒几个切片才起播,加起来最少6到10秒延迟。监控场景下,这个延迟意味着"听到敲门声,画面还停在门口没人"。

用MJPEG倒是没延迟,但代价是带宽和CPU。一路1080p的MJPEG,比特率能冲到几十Mbps,内网无所谓,一旦走公网就是灾难。WebRTC的延迟在几百毫秒到1秒之间,码率自适应也做得好——带宽够就高清,不够就降帧。这在监控场景里是最合适的平衡点。

2.3 网关选型:开源组件优先,别自研轮子

做RTSP转WebRTC,技术上可以自研,但我不建议。WebRTC最麻烦的不是媒体转发,而是ICE、DTLS、SRTP这一整层安全与穿透协议。自己从零实现ICE状态机,光是"候选地址优先级""连通性检查超时"这些细节,就够排几个星期的坑。成熟的做法是直接用开源的媒体网关,把精力花在部署和调参上。

选型时有三个硬指标。第一,支持RTSP拉流,最好能自动重连,摄像头掉线再上线时不用人工干预。第二,ICE实现完整,至少支持STUN穿透,否则公网访问会卡在NAT后面。第三,支持转码或至少支持转封装——如果摄像头输出H.264,浏览器能解,但遇到H.265的摄像头,很多浏览器根本不支持,网关得能转成H.264。我在项目里用的是一个集成了上述能力的开源网关,部署方式是下载预编译二进制,一个配置文件加一条启动命令就能跑起来。社区在维护,遇到问题有人踩过,至少不慌。

提示:如果摄像头输出H.265,前端多半白屏。要么让网关转码,要么换H.264摄像头,两条路都行,但转码要多花CPU。

2.4 架构理解:拉流、转协议、推流三个模块

网关内部可以拆成三个模块:

模块职责关键技术点
RTSP拉流端主动连接摄像头,建立RTSP会话TCP连接保持、心跳、断线重连
协议转换层RTP转SRTP,SDP重新协商编解码参数映射、时间戳换算
WebRTC推流端与浏览器建立PeerConnectionICE协商、DTLS握手、数据面

这三个模块在网关里通常是一体的,但理解这个拆分对排查问题很有用。黑屏时先确认是哪一段断了:拉流端有没有拿到RTP包?推流端的ICE状态是不是connected?逐段排查,比对着整个链路发呆快得多。网关日志一般会分模块打点,看到"rtsp connected"和"webrtc connected"两条都出现,链路才算通。

3. 搭建网关:拉流、转协议、推流三步走

3.1 准备环境与下载网关

网关部署在内网一台Linux机器上,和摄像头在同一网段。我用的是Debian系统,配置要求不高,双核2GB内存足够跑10路以内。下载预编译二进制后,先建工作目录,再写配置文件。

mkdir -p /opt/media-gateway/config cd /opt/media-gateway wget https://example.com/media-gateway.tar.gz tar -xzf media-gateway.tar.gz

下载地址是举例,实际以项目发布页为准。解压后的目录里要重点关注config目录下的YAML文件,所有拉流和推流参数都在这里。不要把二进制直接扔在root目录下跑,日志和配置会散得到处都是,后面排查找不着东西。

3.2 拉流参数配置

# config/gateway.yaml rtsp: host: 0.0.0.0 port: 554 timeout: 10s reconnect_interval: 5s streams: - id: cam01 url: rtsp://192.168.1.100:554/stream1 transport: tcp audio: false

timeout是拉流响应的超时时间,有的摄像头应答慢,设太短会误判失败。reconnect_interval是断线重连间隔,摄像头重启后5秒内能自动恢复。transport建议用tcp,UDP虽然省带宽,但内网丢包一多就花屏。audio先关掉,把视频跑通再加音频,减少变量。

3.3 WebRTC推流参数配置

webrtc: ice_servers: - urls: stun:stun.example.com:19302 udp_port_range: "50000-50100" dtls_cert_path: /etc/ssl/certs/dtls.pem max_bitrate_kbps: 4096

ice_servers里的STUN是给NAT穿透用的,纯内网部署可以留空,但考虑公网访问就配上。udp_port_range限制SRTP端口范围,防火墙只放行这一段就行。dtls_cert_path如果不配,网关启动时会自动生成自签名证书,前端不用管证书,WebRTC的DTLS握手会处理。max_bitrate_kbps限制上行码率,防止多路并发把上行带宽打满,按实际带宽调,别贪高。

启动命令:

./media-gateway -c config/gateway.yaml

启动后看日志,出现"stream cam01 connected"就说明拉流成功。如果一直报"connection timeout",先用ffprobe在服务器本机确认RTSP地址能拉通,排除地址错误或网络隔离。这一步能省掉后面很多折腾。

3.4 验证链路是否打通

拉流成功后不要急着写前端,先用网关自带的管理接口确认推流端状态。访问管理端口,比如http://服务器IP:8080/stats,能看到当前活跃会话、码率、ICE状态。ICE状态如果是"connected",说明网关侧一切就绪,问题大概率在前端;如果一直"checking",先查UDP端口和防火墙。

提示:防火墙最少放行TCP 554(RTSP)、UDP 50000-50100(SRTP)、TCP 8080(管理接口)。少放一个端口,ICE就会卡在checking状态出不来,花再多时间调前端都没用。

4. 前端接入:信令流程、WebRTC API与播放器代码

4.1 信令流程:SDP交换与ICE候选

网关和浏览器之间需要交换两类信息:SDP(媒体描述)和ICE候选地址。网关通常提供一个HTTP接口,前端向接口发起请求,拿到SDP之后直接在浏览器里新建RTCPeerConnection,把SDP塞进去。这个流程比传统WebRTC信令服务器要轻,因为网关已经是服务端,不需要额外的信令中转设备。

关键在先后顺序:先创建PeerConnection,再setRemoteDescription,最后setLocalDescription并把answer回传。反过来的话,ICE候选可能对不上,前端会一直卡在connecting。

4.2 前端播放代码

async function startPlayback(streamId, videoElement) { const gatewayBase = 'http://192.168.1.10:8080'; const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.example.com:19302' }] }); pc.ontrack = (event) => { videoElement.srcObject = event.streams[0]; }; // 从网关获取远端SDP const resp = await fetch(`${gatewayBase}/webrtc/offer?stream=${streamId}`); const offerSdp = await resp.text(); await pc.setRemoteDescription({ type: 'offer', sdp: offerSdp }); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); // 把answer回传给网关 await fetch(`${gatewayBase}/webrtc/answer`, { method: 'POST', body: JSON.stringify({ streamId, sdp: answer.sdp }) }); }

这段代码做了四件事:创建PeerConnection、向网关请求SDP、设置远程描述并生成本地回答、把回答回传。pc.ontrack里把媒体流挂到video元素的srcObject上,画面就出来了。iceServers不能省,如果网关侧NAT穿透失败,前端至少要能直连局域网IP,留空的话ICE候选收集不完整。

4.3 video元素与播放参数

<video id="monitor" autoplay muted playsinline controls />

autoplay是必须的,WebRTC流不像普通视频,没有用户手势就不会自动播放。muted也必须有,浏览器自动播放策略要求带声音的媒体必须由用户主动触发,先静音让画面跑起来。playsinline是给iOS Safari用的,不加的话在iPhone上会强制全屏播放。controls加上去,方便桌面端测暂停和音量。

4.4 常见报错:NotAllowedError与SDP不匹配

前端最常见的报错是NotAllowedError,原因是video元素没有muted属性,被浏览器拦截了自动播放。其次常见的是"Failed to execute 'setRemoteDescription'"——通常是网关返回的SDP格式不对或streamId不存在。处理方式:先确认streamId在网关配置里存在,再用浏览器console打印收到的SDP文本,手动检查m-line是否包含video。如果SDP里没有video m-line,网关那边拉流就没成功,前端怎么调整都没用。

5. 排坑实战:黑屏、花屏、延迟的排查记录

5.1 黑屏:ICE candidate没配对

现象:前端pc.oniceconnectionstate一直处于checking,video元素黑屏,没有任何报错。

原因:防火墙只放行了TCP 554和HTTP 8080,UDP 50000-50100没放行,SRTP包进不来。ICE的candidate虽然选出来了,但连通性检查收不到回应。

解决:在防火墙上放行UDP端口段,重启网关。iceconnectionstate变成connected的一瞬间,画面就出来了。从那以后我每次排查黑屏,先看ICE状态再查防火墙,不先折腾前端代码。

5.2 花屏:关键帧丢失

现象:画面能出来,但每隔几秒就花屏一次,像打满马赛克,过一两秒又恢复。

原因:TCP通道上的RTP包在拥塞时被网关丢弃,丢的不是普通帧,是I帧。I帧一丢,后续的P帧全解码不出来,只能等下一个I帧到达才能恢复。

解决:把拉流transport从udp改为tcp,同时把网关的丢包重传开关打开。改完之后花屏不再是连续出现,只有摄像头抖动严重时偶尔闪一下。如果是WiFi摄像头,优先检查无线信号强度,RTP丢包在WiFi环境下几乎无法完全避免。

5.3 延迟飘高:jitterBuffer把缓冲区撑大了

现象:延迟从正常500ms突然飘到2秒以上,画面变成慢动作,过一会儿又恢复。

原因:前端播放器的jitterBuffer在码率突增时扩容,缓冲区一深,延迟自然上去。码率降下来之后,缓冲区不会立刻缩小,延迟就维持在高位。

解决:在PeerConnection配置里加playoutDelayHint,限制缓冲深度:

pc.playoutDelayHint = 200; // 单位毫秒

200ms的意思是"我给你200ms的缓冲空间,别再多攒了"。设完之后延迟稳定在400ms左右,画面也不卡。这个参数在不同浏览器上表现有差异,Chrome支持得最好,Safari有的版本会忽略,需要实测。

5.4 音频缺失:编解码器不一致

现象:视频正常,但没有声音,控制台不报任何错误。

原因:摄像头输出的是AAC格式音频,网关默认WebRTC侧的音频是opus,没有做转码,音频轨被丢弃了。

解决:在网关配置里把audio从false改成true,并把音频编码设置为aac到opus转码。前提是网关编译了音频转码模块,否则只能换输出opus的摄像头。调试时先用VLC拉RTSP确认摄像头确实有音频轨,再排查网关侧。

5.5 摄像头掉线后不回画面

现象:摄像头断电重启后,前端一直黑屏,网关日志显示EOF,但新会话起不来。

原因:RTSP拉流断开后,网关没有释放旧会话,RTSP端口被占住,新的连接进不来。

解决:把reconnect_interval缩短到3秒,并在监控脚本里加一个探测逻辑——发现session断开就调用网关管理接口强制重置。不处理的话,每次摄像头重启都得手动重启网关,监控项目里这很致命。

6. 进阶技巧:用Stats API量化延迟,告别"感觉卡了"

6.1 为什么不用ping测延迟

监控流的延迟指从摄像头采集到浏览器渲染的时间差,ping测的是网络双向时延,完全是两回事。要量化这条链路的真实延迟,得用WebRTC自带的Stats API。它直接暴露了ICE选路RTT、解码帧数、丢包率等关键指标,比用秒表估准得多。

6.2 代码实现

async function measureLatency(pc) { const stats = await pc.getStats(null); let currentRoundTrip = 0; let framesDecoded = 0; stats.forEach(report => { if (report.type === 'candidate-pair' && report.state === 'succeeded') { currentRoundTrip = report.currentRoundTripTime * 1000; } if (report.type === 'inbound-rtp' && report.kind === 'video') { framesDecoded = report.framesDecoded; } }); return { currentRoundTrip, framesDecoded }; }

currentRoundTripTime反映ICE连通性检查的RTT,也就是网关和浏览器之间的网络往返时间。真实端到端延迟还要加上网关的转发耗时和浏览器的渲染延迟,但RTT已经是能拿到的最直接数据。framesDecoded用来判断花屏——如果这个值长时间不增长,说明解码线程卡死了,问题在渲染侧而不是网络侧。

6.3 结合时间戳验证

Stats API给的是网络层数据,要验证端到端延迟,我一般在摄像头前放一个秒表,手机拍屏幕,摄像头对着秒表,然后对比浏览器画面里的秒表和真实时间差。这个方法土,但最实在。测出来的值如果超过1秒,优先检查网关上是否有转码在跑——转码耗费的时间远大于网络传输,H.265转H.264一路720p就可能吃掉100ms以上的延迟。

从那以后,我每次搭完一条新的流,都强制走一遍这个流程:先看ICE状态,再量jitterBuffer,最后用秒表校准延迟。不敢说每次都完美,但至少翻车概率低了很多。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进