简介:基于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 里。这个渐进式方法让我在项目初期的调试效率提高了不少。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

邀请您加入社区

更多推荐