Vue+webrtc-streamer实现低延迟RTSP视频直播预览
简介:基于Vue的webrtc-streamer前端Demo代码,面向需要将RTSP视频流接入Web浏览器的前端开发者。该Demo演示了在Node 16环境下,配置RTSP地址并启动webrtc-streamer.exe服务后,将视频流转码为WebRTC流,并在页面上的四个视频窗口中实时播放,可参考应用于低延迟视频监控、在线课堂、远程看板等场景。压缩包体积25.86MB,共收录2015个文件,其中Markdown文档1134个、JavaScript脚本704个,另有JSON配置、TXT说明及少量XML/HTML;大量JS文件承载前端逻辑与依赖库,MD文档则可用于阅读项目说明、配置说明与排错思路,整体结构方便后续二次修改与功能延伸。 目前已有548人学习,适合作为Vue集成WebRTC低延迟视频播放的入门参考。项目虽为Demo,却完整包含了从服务端推流到前端拉流的关键链路,开发者可在此基础上扩展用户鉴权、画质切换、多路流管理等实际功能。
1. 先从需求说起:为什么用 webrtc-streamer 做视频直播预览
如果你的工作里接触过安防摄像头、RTSP 流、低延迟直播这类需求,大概率听过 webrtc-streamer 这个名字。它是一个开源项目,核心能力是让浏览器直接通过 WebRTC 协议拉取 RTSP、RTMP 等格式的流媒体,从而在网页端实现毫秒级延迟的实时画面。而“webrtc-streamer 的 demo 前端 vue 代码”这个标题,说白了就是: 用 Vue 写一个能播放 webrtc-streamer 推送的实时视频流的示例项目 。
我在实际项目里接过类似的需求:摄像头画面要嵌入到 Web 管理后台,页面要同时预览多路视频,还不能让用户装插件。当时对比了 HLS、HTTP-FLV、WebSocket 转发等方案,最后还是选了 webrtc-streamer,原因下面细说。如果你也在做视频监控、大屏展示、设备巡检这类前端,这个 demo 思路完全可以拿来当起点。
先明确一下这套 demo 能做什么、解决什么问题:
- 在 Vue 页面里播放 RTSP 视频流,不需要浏览器装插件,也不用单独开发客户端。
- 做到 500ms 以内的延迟,满足“实时预览”而不是“看录像”级别的需求。
- 通过简单的 API 封装,把设备列表、视频连接、窗口销毁等逻辑收敛成可复用的组件。
适合谁来参考:前端负责人、负责 Web 视频接入的开发者、做 IoT 平台或者安防平台的前端同学。下面是我整个落地过程中整理的方案选型、代码结构、踩坑记录,希望能帮你少走弯路。
2. 方案选型解析:不选 HLS 和 FLV 的理由
2.1 WebRTC、HLS、HTTP-FLV 的核心差异
在决定使用 webrtc-streamer 之前,我先把主流的网页播放方案拉出来对比了一遍。如果你也在做技术选型,这张表可以直接参考:
| 方案 | 延迟水平 | 浏览器兼容性 | 服务端改造量 | 典型适用场景 |
|---|---|---|---|---|
| HLS | 5~30 秒(切片延迟) | 极好,原生支持 | 需要转封装切片 | 点播、直播回放、短视频 |
| HTTP-FLV | 1~3 秒 | 需依赖 flv.js,iOS 兼容差 | 需要流媒体服务支持 FLV | PC 端直播、低延迟场景 |
| WebRTC (webrtc-streamer) | 0.1~0.5 秒 | Chrome/Edge/Firefox 良好 | 需要部署 webrtc-streamer 中转 | 实时监控、视频对讲、大屏联动 |
HLS 最省事,后端把 RTSP 流转成 m3u8 切片后,前端扔一个 <video> 标签就能播。但 HLS 的切片机制决定了延迟下限,少则三五秒,多则十几秒,安防预览场景根本没法用——那边一扇门开了,画面三秒后才出来,就等于没看。
HTTP-FLV 好在可以不依赖原生 WebRTC,但 flv.js 在 iOS 上的兼容性比较差,而且如果你的摄像头数量多,前端对每一路流做 FLV 解封装,CPU 占用会肉眼可见地升高。更麻烦的是 FLV 方案一般需要 nginx-rtmp 或者 SRS 这类服务端做协议转换,反向代理和端口配置又是一堆事。
2.2 webrtc-streamer 的最大优势
webrtc-streamer 的思路不太一样:它自己就是一个独立的流媒体网关,部署好之后对外暴露一组 HTTP API,前端只需要跟这个服务通信,由它去拉取 RTSP 流,然后通过 WebRTC 标准协议把音视频数据传给浏览器。
换句话说,前端不用关心 “视频源到底走的是 RTSP 还是 ONVIF”,只要拿到一长串 URL,丢给 webrtc-streamer 的接口,剩下的连接协商由它完成。这个“解耦”对于做平台型产品非常关键:你要对接海康、大华、宇视,还是普通的 RTSP 摄像头,前端的接入代码是不变的。
而且 WebRTC 底层走的是 RTP/RTCP over UDP/TCP,配合浏览器硬件解码,直播延迟能做到 0.1 到 0.5 秒级别,这是 HLS 远远达不到的。另一个优势是 it 支持多路并发,每路视频独立建立 PeerConnection,互相不阻塞,所以多画面拼接时体验也还算顺滑。
注意:webrtc-streamer 不是万能的。它的本质是“网关 + 协议转换”,所以部署时需要保证 Web 前端、webrtc-streamer 服务、摄像头三者之间网络互通,如果是内外网隔离的环境,还要通过代理或者端口映射来解决。
3. 环境准备与核心概念:先把服务跑起来
3.1 webrtc-streamer 服务部署
我这里是基于 Ubuntu 服务器做的部署,比较简单的方式是直接拉官方 release 编译好的二进制。
# 下载对应的 Linux 版本,这里以最新 release 为例
wget https://github.com/mpromonet/webrtc-streamer/releases/download/v0.8.4/webrtc-streamer-v0.8.4-linux-x64.tar.gz
tar zxvf webrtc-streamer-v0.8.4-linux-x64.tar.gz
cd webrtc-streamer-v0.8.4-linux-x64
# 启动服务,监听 8000 端口
./webrtc-streamer -H 0.0.0.0 -p 8000
启动之后,可以通过浏览器访问 http://服务器IP:8000 查看自带的调试页面,里面会有一个设备列表,默认展示了几个官方示例的 RTSP 地址。如果能看到列表和可以点击播放的画面,说明服务基本没问题。
启动时需要关注的几个参数:
-
-H:绑定监听地址,建议绑0.0.0.0,否则跨机器访问不到。 -
-p:HTTP 端口,默认 8000,如果被占用换一个,前端要对应改。 -
-S:有些版本支持开启 HTTPS,如果你要部署到线上且没有单独的 HTTPS 处理层,建议配上证书。 -
-Q:多人观看同一路流时是否开启转发复用,如果并发量较大建议开启。
注意:webrtc-streamer 默认只是暴露 HTTP 服务,正式环境一定要放到 Nginx 或者 Caddy 后面做反向代理,不建议直接暴露 8000 端口到公网,会有安全风险。
3.2 前端依赖与核心 API
webrtc-streamer 的前端接入有两种方式:一是直接使用官方 JS 包里的 webrtc-streamer.js ,二是通过 npm 包引入。我推荐直接把官方 JS 拷贝到自己项目中,因为 npm 包版本不一定和你的服务端版本匹配,尤其是 API 有变化时,源码级别的引用反而更好排查问题。
核心 API 我整理一下,后面代码都会用到:
| 方法 | 作用 | 关键参数 |
|---|---|---|
getVideoList() | 获取服务端能拉取的视频源列表 | 无 |
connect(video, url, options) | 将指定 URL 的视频流绑定到一个 <video> 标签播放 | video :HTMLVideoElement; url :视频流地址 |
disconnect(video) | 断开浏览器与该视频源的连接,释放资源 | video :HTMLVideoElement |
onicecandidate 等回调 | 处理 WebRTC 连接过程中的状态变化 | 一般只在调试时用 |
更准确地说,早期版本直接生成一个 WebRtcStreamer 对象,调用方式如下:
const webRtcServer = new WebRtcStreamer('http://localhost:8000');
webRtcServer.connect(videoElement, 'rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101');
这套 API 设计得很精简,反而容易让人忽略它背后做的事情。 connect 并不是简单地把流地址塞给视频标签,而是先向 webrtc-streamer 服务发起信令请求,通过 SDP 交换完成 PeerConnection 的建立,之后浏览器才收到音视频数据包。这也是为什么你直接用 <video src="rtsp://..."> 播不了,而走 webrtc-streamer 能播的原因。
4. Vue Demo 实操:从建项目到多画面预览
4.1 搭建 Vue 项目和基础结构
我用的是 Vite + Vue 3 的组合,组件用组合式 API 写,状态管理直接用一个模块级变量,没必要上 Pinia。项目结构大致如下:
src/
├── api/
│ └── video.js # 封装 webrtc-streamer 相关调用
├── components/
│ ├── VideoPlayer.vue # 单路视频播放组件
│ └── VideoGrid.vue # 多路拼接面板
├── pages/
│ └── Monitor.vue # 监控预览页
└── utils/
└── streamer.js # 初始化 WebRtcStreamer 单例
为什么要单独把 streamer.js 抽出来?因为 webrtc-streamer 的前端对象本质上是一个连接管理器,多个视频组件要共用同一个服务连接,如果每个组件都 new WebRtcStreamer() ,会导致连接资源浪费,也容易触发浏览器的连接数限制。单例模式最合适。
4.2 关键代码:封装 webrtc-streamer 调用
先看 utils/streamer.js :
let streamer = null;
export function getStreamer() {
if (!streamer) {
const WebRtcStreamer = window.WebRtcStreamer;
if (!WebRtcStreamer) {
console.error('webrtc-streamer.js 未加载');
return null;
}
streamer = new WebRtcStreamer('http://localhost:8000');
}
return streamer;
}
这里要把官方脚本在 index.html 里用 <script> 标签引入,或者通过 Vite 的 public 目录直接挂载。为什么不用 npm 包?很多时候 npm 包版本滞后,而且它依赖的全局对象名也不稳定,我实际遇到过一次 npm 包导出的对象是 RTCClient ,和官方文档完全对不上,排查了半天。直接引入官方仓库里的 dist/webrtc-streamer.js 是最稳妥的,哪个版本服务端配哪个版本前端脚本,我很确定不会跑偏。
再看 api/video.js ,把连接操作封装成 Promise,方便组件里统一处理错误:
import { getStreamer } from '../utils/streamer';
let currentVideoElement = null;
let currentUrl = '';
export function playVideo(videoElement, url) {
return new Promise((resolve, reject) => {
const streamer = getStreamer();
if (!streamer) {
reject(new Error('webrtc-streamer 初始化失败'));
return;
}
// 如果当前有正在播放的流,先断开
if (currentVideoElement && currentVideoElement !== videoElement) {
streamer.disconnect(currentVideoElement);
}
try {
streamer.connect(videoElement, url);
currentVideoElement = videoElement;
currentUrl = url;
resolve();
} catch (e) {
reject(e);
}
});
}
export function stopVideo() {
if (currentVideoElement) {
getStreamer()?.disconnect(currentVideoElement);
currentVideoElement = null;
}
}
这里有几个关键细节:
- 每次播放新视频前要先断开上一路连接,尤其在同一页面不同 Tab 切换时,否则会看到画面卡死不动。
-
connect的第二个参数不仅仅是 RTSP 地址,webrtc-streamer 还支持传入带参数的 URL,比如分辨率、码率等控制参数,后面优化章节再展开。 - 连接是异步的,但
connect方法本身没有回调,所以我的做法是直接返回成功,然后在视频元素的onplaying事件里做真正的成功判断。
提示:不要把
webrtc-streamer.js和webrtc-streamer.min.js混用。我曾经因为构建工具压缩问题把两个文件同时引入,结果全局对象被覆盖,浏览器报WebRtcStreamer is not a constructor,排查了很久。
4.3 单路播放组件:VideoPlayer.vue
写播放组件时,我踩过一个常见的坑:Vue 的响应式数据和 <video> 标签的 src 属性没有直接关系,WebRTC 播放是通过 attachMediaStream 或者 webrtc-streamer 内部处理的,所以组件里不能依赖 src 绑定。正确做法是给 <video> 加一个 ref ,在生命周期里调用播放逻辑。
<template>
<div class="video-player">
<video
ref="videoRef"
autoplay
muted
playsinline
controls
@click="togglePlay"
></video>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import { playVideo, stopVideo } from '../api/video';
const props = defineProps({
streamUrl: {
type: String,
required: true,
},
});
const videoRef = ref(null);
const isPlaying = ref(false);
async function initPlay() {
if (!props.streamUrl || !videoRef.value) return;
try {
await playVideo(videoRef.value, props.streamUrl);
isPlaying.value = true;
} catch (e) {
console.error('播放失败:', e);
isPlaying.value = false;
}
}
function togglePlay() {
if (isPlaying.value) {
stopVideo();
isPlaying.value = false;
} else {
initPlay();
}
}
onMounted(initPlay);
onBeforeUnmount(() => {
if (videoRef.value) {
getStreamer()?.disconnect(videoRef.value);
}
});
</script>
muted 属性必须带上,这是浏览器自动播放策略的硬性要求。如果不加,Chrome 会直接阻止音视频播放,画面一直停在黑屏状态。如果你确实需要声音,就做成用户点击后手动开启,不要指望页面加载后自动出声。
playsinline 是为了兼容移动端 Safari,避免视频播放时自动全屏。如果你的场景是在手机上预览监控画面,这个属性不加会很崩溃。
autoplay 加上 muted 的组合几乎适配所有主流浏览器。我在 Edge、Chrome 上都测过,没有出现不能自动播放的情况。
4.4 多路视频拼接面板
监控页面通常不止看一路,我这里用一个 Grid 组件来管理多个 VideoPlayer。注意一个关键点: 不要在 v-for 里直接创建多个 WebRtcStreamer 对象 ,因为我们已经用了单例,所以这里只是把不同 URL 传给不同的子组件,连接排队和断开由上面的封装统一处理。
<template>
<div class="video-grid">
<VideoPlayer
v-for="item in streamList"
:key="item.id"
:stream-url="item.url"
class="grid-item"
/>
</div>
</template>
<script setup>
import { ref, onBeforeUnmount } from 'vue';
import VideoPlayer from './VideoPlayer.vue';
const streamList = ref([
{ id: 1, url: 'rtsp://admin:admin@192.168.1.20:554/Streaming/Channels/101' },
{ id: 2, url: 'rtsp://admin:admin@192.168.1.21:554/Streaming/Channels/101' },
]);
onBeforeUnmount(() => {
const streamer = getStreamer();
// 这里需要逐个断开所有已连接的 video 元素,简单做法是遍历标识
document.querySelectorAll('video').forEach((video) => streamer?.disconnect(video));
});
</script>
这个组件只是一个雏形,实际项目里你可能还需要根据设备在线状态动态增删视频源、在子画面里叠加设备名称和时间水印。这些都可以在 VideoPlayer 内部继续扩展,不影响整体结构。
5. 参数调优与常见问题排查实录
5.1 视频参数与播放体验优化
webrtc-streamer 的 connect 方法里其实潜藏着一个性能关键: options 参数。官方文档对这个东西写得很隐晦,但实际调试时它能解决很多问题。
比如分辨率控制:
const options = {
'rtsp_transport': 'tcp',
'stimeout': '5000000',
'max_delay': '500000',
};
streamer.connect(video, url, options);
-
rtsp_transport:RTSP 底层的传输协议,建议在网络环境差的时候固定为tcp,避免 UDP 丢包导致花屏。 -
stimeout:RTSP 会话超时时间,单位微秒,默认可能比较短,长时间没有数据流时会断线重连,导致画面卡住。调大之后稳定性提升明显。 -
max_delay:最大缓冲延迟,值越小延迟越低,但网络波动时容易出现卡顿。一般 500ms 是个比较合适的平衡点。
我调试的时候遇到过一个很典型的问题:画面每隔十几秒就卡住,然后 30 秒左右自动恢复。查了一圈发现是 stimeout 设置太短,服务端认为 RTSP 会话超时,主动断开了流。把 stimeout 调到 5000000 微秒(也就是 500 秒)之后,再也没有出现过这个问题。如果你也遇到“视频看一会儿就死掉”的现象,优先检查这个参数。
另外,webrtc-streamer 还支持在 URL 后面拼接 ?video=... 参数来直接指定编码格式,比如:
rtsp://admin:admin@192.168.1.20:554/Streaming/Channels/101?video=H264
这样做的目的是锁定编码,避免摄像头协商到 H265 而浏览器不支持硬解时,拖慢整个页面的渲染效率。目前主流的浏览器对 H264 的支持是最好的,H265 在 Chrome 里需要额外的硬件支持,兼容性不如 H264。
提示:WebRTC 播放对浏览器的要求不低,如果每路视频都占用大量 CPU,可以考虑开启摄像头子码流(通常只有主码流四分之一的分辨率),或者在服务端做转码。多路预览场景下,用子码流 + H264 + 低分辨率,效果会好很多。
5.2 常见报错对照表
这里把我在开发中遇到的问题整理成了一张表,配合排查思路,省得你一个个踩:
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 页面黑屏,控制台无任何报错 | autoplay 策略不允许无声音播放 | 确认 <video> 标签加了 muted 和 playsinline |
| 显示 “Failed to fetch” | webrtc-streamer 服务没启动或者端口不对 | 检查服务进程,浏览器直接访问 http://IP:8000 测试 |
| 视频一直卡在“connecting” | RTSP 地址不可达或者账号密码错误 | 先用 VLC 验证 RTSP 地址能否播放 |
| 画面出现马赛克、花屏 | UDP 丢包 | 在 options 里设置 rtsp_transport: 'tcp' |
| 视频列表接口返回空数组 | webrtc-streamer 配置里没有添加视频源 | 检查服务启动时的 -s 参数或者依赖的配置文件 |
| 切换页面后视频不释放 | 组件卸载时没有调用 disconnect | 在 onBeforeUnmount 里遍历 video 元素并断开 |
5.3 性能优化与并发限制
并发预览多路视频时,WebRTC 连接的开销不能忽视。每路视频都要建立一条 PeerConnection,对 CPU、内存和网络带宽都有消耗。我实测下来,四路 720P 在普通办公电脑上还能流畅跑,但到了八路就开始出现画面卡顿,CPU 占用飙到 80% 以上。
一些可行的优化手段:
- 限制同时播放的流数 :监控墙默认只播放前几路,其他画面等用户点击后再拉流。
- 使用子码流 :预览用子码流,点击放大时才切换到主码流。
- 统一 H264 :避免浏览器对 H265 硬解支持不足而走软解,软解 4 路就足够压垮低配机器。
- 断开不可见窗口 :当组件切换后台或不可见时,使用
IntersectionObserver自动断开视频流,回到视野再重新连接。
顺便提一个比较隐蔽的问题:如果页面用到了 keep-alive 缓存路由组件,组件不会立刻销毁, onBeforeUnmount 不会触发,视频流可能一直挂着。我的处理方式是在路由切换时,通过 onDeactivated 钩子里主动断开所有视频。这也是 keep-alive 模式下的常见坑,建议大家提前考虑。
6. 一些关于项目结构的建议
很多人第一次接触 webrtc-streamer,会直接把所有逻辑写在单文件组件里,前期能跑,后面一旦要加功能就会很痛苦。我以一个做过维护型项目的经验说:项目结构一开始就要分层。
第一层是 utils/streamer.js ,负责 WebRtcStreamer 单例的初始化和全局状态维护,比如当前播放的 video 元素、当前流地址、连接状态等。
第二层是 api/video.js ,只暴露业务接口,比如 playVideo 、 stopVideo 、 getVideoList 。这一层可以后续换成别的流媒体服务,比如如果你又接入了一个新的视频平台,只需要改这层,组件层完全不用动。
第三层才是业务组件,比如 VideoPlayer、VideoGrid。这一层只关心“我要播放哪个地址”“用户点击了暂停”这样的界面逻辑,不应该出现 WebRTC 相关 API。
这样做还有一个好处:便于单元测试。WebRTC 相关的接口在实际环境里很难模拟,如果全部封装在底层,测试业务组件时只需要 mock 掉 api/video.js 就可以了。
7. 按我的经验,给一个小结
做这个 demo 时,我最大的感受是:webrtc-streamer 的门槛不在于 API 复杂,而在于它把“服务端转流、WebRTC 信令协商、前端播放状态管理”这三件事揉在了一起。网上很多人遇到的“画面出来了一秒立刻断开”“花了半天没出来画面”,大多数不是代码问题,而是某个环境因素没配对。
我个人实际操作中的体会是,调试时要一层一层来:先用 VLC 验证 RTSP 源本身没问题;再用 webrtc-streamer 官方页面验证浏览器和服务端能正常通信;最后才在 Vue 的组件里接代码。遵循这个顺序,你能少掉百分之七十的坑。
最后再分享一个小技巧:如果你只是想在本地快速看效果,不需要一上来就启动 Vue 项目。直接用官方页面上把视频流打通,再通过 http-server 之类轻量工具起一个静态页,把 webrtc-streamer.js 引进去,几十行代码就能验证整个链路,然后再把代码迁移到 Vue 里。这个渐进式方法让我在项目初期的调试效率提高了不少。
更多推荐

所有评论(0)