需求

项目使用到海康威视的摄像头设备, 该摄像头会生成 rtsp 地址, 只要使用一些前端第三方库对地址的视频流进行解码编码就能在浏览器播放, 但是因为前端解码编码的性能有限, 会导致播放有极大延迟, 对监控场景来说, 实时性十分重要

WebRTC

目前是主流浏览器原生支持的技术, 主要作用是实现实时通信, 其原理是通过点对点实现低延迟通信, 所谓点对点就是两个节点直接相互通信, 没有中间服务器参与

SRS

是一个开源的实时视频服务器, 支持多种流媒体协议, 其中就支持将 rtsp 协议转为 webrtc 协议

启动

docker 启动 srs

docker run --name=srs --restart=always -d -p 1935:1935 -p 1985:1985 -p 8080:8080 registry.cn-hangzhou.aliyuncs.com/ossrs/srs:5
  • --name=srs 容器名为 srs
  • --restart=always docker 开启后容器会自动重启
  • -d 后台运行容器
  • -p 映射容器端口到本机端口
  • registry.cn-hangzhou.aliyuncs.com/ossrs/srs:5 镜像

启动容器成功后, 打开 http://localhost:8080/players/rtc_player.html 就能看到播放视频的页面, 输入 webrtc 地址就能播放视频, 但此时我们还没有 webrtc 的视频流地址, 下面我们使用 OBS Studio 生成

推流

打开 OBS studio 后, 在底部添加采集源, 点击 + 号后选择屏幕采集, 进入设置 - 直播, 服务选择自定义, 服务器rtmp:// 加上 srs 容器运行所在服务器的域名, 比如 rtmp://localhost, 推流码任意填, 比如 test, 然后确定, 最后点开始直播, 如果没有报错说明推流成功了, 将当前电脑屏幕的画面作为视频流推送到 srs 服务上

最终会生成 webrtc://localhost/test 的视频流地址, 进入 http://localhost:8080/players/rtc_player.html 输入该地址就能看到画面, 这个步骤也叫拉流

拉流

export class SrsRtcPlayerAsync {
  private pc: RTCPeerConnection;
  public stream: MediaStream;
 
  constructor() {
    this.pc = new RTCPeerConnection();
    this.stream = new MediaStream();
    this.pc.ontrack = (event) => {
      this.stream.addTrack(event.track);
    };
  }
 
  async play(url: string) {
    if (url.includes("localhost") || url.includes("127.0.0.1")) {
      throw "向 127.0.0.1 或 localhost 请求视频流无效";
    }
    this.pc.addTransceiver("audio", { direction: "recvonly" });
    this.pc.addTransceiver("video", { direction: "recvonly" });
 
    const offer = await this.pc.createOffer();
    await this.pc.setLocalDescription(offer);
 
    let params = "";
    if (url.includes("?")) {
      const index = url.indexOf("?");
      params = url.slice(index);
    }
    const api = `/rtc/v1/play/${params}`;
 
    try {
      const session = await getSession(api, url, offer);
      await this.pc.setRemoteDescription(
        new RTCSessionDescription({ type: "answer", sdp: session.sdp })
      );
      return true;
    } catch {
      throw "拉流失败";
    }
  }
  close() {
    this.pc && this.pc.close();
  }
}
 
async function getSession(
  api: string,
  streamurl: string,
  offer: RTCSessionDescriptionInit
) {
  const body = {
    streamurl,
    sdp: offer.sdp,
  };
 
  const res = await fetch(api, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  const data = await res.json();
 
  return data;
}

组件中实例化

import { SrsRtcPlayerAsync } from 'rtc-streamer'
import { onMounted, ref } from 'vue'
 
const videoRef = ref<HTMLVideoElement | null>(null)
 
onMounted(() => {
  const srs = new SrsRtcPlayerAsync()
  if (videoRef.value) {
    videoRef.value.srcObject = srs.stream
  }
  srs.play(`webrtc://localhost/test`).catch((error) => {
    console.log(error)
  })
})

请求时要代理到 1985 端口

开发环境下, vite 项目设置代理

...
server: {
    proxy: {
      '/rtc': {
        target: 'http://localhost:1985',
        changeOrigin: true
      }
    }
}
...

生产环境下, nginx 设置代理

location /rtc/ {
   proxy_pass http://localhost:1985/rtc/;
}

ffmpeg 推流

本地安装 ffmpeg 后

ffmpeg -rtsp_transport tcp -i <rtsp地> -c copy -preset ultrafast -f flv rtmp://host.docker.internal/live/rtsp
  • -rtsp-transport tcp 对 rtsp 拉流要使用 tcp 传输协议
  • -i 后接要推送的视频流
  • -c copy 对视频流直接复制,不需要重新编码
  • -preset ultrafast 指定输出的视频格式
  • rtmp://host.docker.internal/live/rtsp 使用 rtmp 协议推送到的 srs 服务上,注意这里使用 host.docker.internal 不是 localhost

通过 docker 容器运行 ffmpeg

docker run --name=ffmpeg --restart=always -d registry.cn-hangzhou.aliyuncs.com/ossrs/srs:encoder ffmpeg -rtsp_transport tcp -i <rtsp地> -c copy -preset ultrafast -f flv rtmp://host.docker.internal/live/rtsp
  • --name=ffmpeg 容器名为 ffmpeg
  • --restart=always docker 开启后容器会自动重启
  • -d 后台运行容器
  • registry.cn-hangzhou.aliyuncs.com/ossrs/srs:encoder 镜像