浏览器播放监控画面

背景

10年前就有在浏览器播放监控画面的需求,当初需求会稍微复杂一点,除了播放实时监控画面之外,可能还需要对摄像机做控制,比如云台旋转、画面截图、录像、调取录像进行回访、快进、倒退等等。目前项目上又碰到了浏览器展示实时监控画面的需求,不过目前来看,需要要简单一些,只要能展示实时监控画面就可以。

10年前没有找到很好的解决方案,可能是需求本身相对复杂一点,而且,当初也许没有现在这样相对成熟的解决方案,毕竟,经过这么多年的发展,摄像机厂商也不像当年那样各自为战,视频编码格式以及控制方式各种不统一,目前,据简单了解,通过ONVIF协议实现控制也相对比较简单,关键是,绝大部分的摄像机厂商都会遵守ONVIF协议。

实现方式

以前是通过厂商SDK编写浏览器插件实现的,现在当然不需要了,只是展示实时监控录像,实现方式会简单许多。

从摄像机获取实时监控视频流的最简单方式,应该是获取他的rtsp流,毕竟,一条rtsp://xxxx命令就可以拉流过来了,只不过由于浏览器不支持RTSP,所以还需要想办法做一些转换。

浏览器播放实时监控视频其实不外乎两个方案:一个还是以前的老方案,通过插件的方式直接播放RTSP,但是,插件几乎要被淘汰了,即使不淘汰,使用起来也非常不方便,所以,这个方案也基本不考虑。另外一个方案就是想办法转换RTSP为浏览器支持的WebRTC,我们就采用这一方案实现。

所以第一步就是:想办法转换RTSP为浏览器支持的WebRTC。

MediaMTX

Media MTX 是一个高性能的开源媒体服务器,用于处理实时音视频流。它支持多种协议,包括 RTSP、RTMP 和 WebRTC,适合用作直播、视频监控(如 IPCAM)、和流媒体中继的解决方案。Media MTX 轻量、灵活,并具有广泛的协议兼容性,非常适合嵌入式设备和云环境。

既然他支持RTSP以及WebRTC,我们就用他来做流媒体中继服务器,目的是让mediaMTX接收到摄像机的RTSP流,然后转换为WebRTC,为前端浏览器访问摄像机实时监控画面提供服务。

享受开源的好处吧,没有开源,我们现在寸步难行。

下载地址:https://github.com/bluenviron/mediamtx/releases/:

找到适合自己的版本,我用的是Ubantu系统,所以找了这个:

复制代码
root@kmkf2:/mediaMTX# mkdir /mediaMTX
root@kmkf2:/mediaMTX# cd /mediaMTX
root@kmkf2:/mediaMTX# wget https://github.com/bluenviron/mediamtx/releases/download/v1.12.3/mediamtx_v1.12.3_linux_amd64.tar.gz

之后,查看一下下载的内容,解包:

复制代码
root@kmkf2:/mediaMTX#  ls -lrt
-rw-r--r-- 1 root root 16784260 May 28 04:43 mediamtx_v1.12.3_linux_amd64.tar.gz
root@kmkf2:/mediaMTX# tar -zxvf  mediamtx_v1.12.3_linux_amd64.tar.gz
root@kmkf2:/mediaMTX# ls -lrt
-rw-r--r-- 1 root root    29435 May 28 04:39 mediamtx.yml.bak
-rw-r--r-- 1 root root     1062 May 28 04:39 LICENSE
-rwxr-xr-x 1 root root 33002954 May 28 04:41 mediamtx
-rw-r--r-- 1 root root 16784260 May 28 04:43 mediamtx_v1.12.3_linux_amd64.tar.gz
-rw-r--r-- 1 root root    29609 Jun 13 11:51 mediamtx.yml

齐活了,可以开干了。

配置mediaMTX

配置文件是mediamtx.yml,配置也非常简单,只有最后面这几行需要配置:

复制代码
# Path settings

# Settings in "paths" are applied to specific paths, and the map key
# is the name of the path.
# Any setting in "pathDefaults" can be overridden here.
# It's possible to use regular expressions by using a tilde as prefix,
# for example "~^(test1|test2)$" will match both "test1" and "test2",
# for example "~^prefix" will match all paths that start with "prefix".
paths:
  # example:
  # my_camera:
  #   source: rtsp://my_camera
  cam1:
    source: rtsp://admin:xxx@youripcip:554/stream1
    #sourceProtocol: rtsp
    sourceOnDemand: yes  # 表示当有人观看时才连接摄像头  

需要注意的是cam1source: rtsp://admin:xxx@youripcip:554/stream1 这两处:

  1. cam1:是你的摄像机名称,浏览器访问的识别参数。
  2. source:配置摄像机,其中rtsp协议后面的admin:xxx是访问用户名和密码,youripcip是摄像机ip地址,这个地址当然要能访问得到才行,554是摄像机rtsp协议的默认端口,后面的stream1是摄像机提供rtsp服务的具体访问路径,这里的每一个配置都和摄像机厂商有关,不同的厂商访问地址是不一样的,需要查看摄像机说明书。

这里配置完成之后就可以启动服务了:

复制代码
root@kmkf2:/mediaMTX# ./mediamtx
2025/06/13 21:49:18 INF MediaMTX v1.12.3
2025/06/13 21:49:18 INF configuration loaded from /mediaMTX/mediamtx.yml
2025/06/13 21:49:18 INF [metrics] listener opened on :9998
2025/06/13 21:49:18 INF [RTSP] listener opened on :8554 (TCP), :8000 (UDP/RTP), :8001 (UDP/RTCP)
2025/06/13 21:49:18 INF [RTMP] listener opened on :1935
2025/06/13 21:49:18 INF [HLS] listener opened on :8888
2025/06/13 21:49:18 INF [WebRTC] listener opened on :8889 (HTTP), :8189 (ICE/UDP)
2025/06/13 21:49:18 INF [SRT] listener opened on :8890 (UDP)
2025/06/13 21:49:18 INF [API] listener opened on :9997

如果能看到以上信息,配置就成功了。但是是否能连接到摄像机,只有访问的时候才能知道。

编写html访问

接下来就可以编写html代码访问实时监控画面了。

复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>WebRTC 监控流</title>
  <style>
    .container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 20px;
      padding: 20px;
    }
    .button {
      padding: 20px 40px;
      font-size: 24px;
      cursor: pointer;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 10px;
      min-width: 300px;
      transition: background-color 0.3s;
      font-weight: bold;
    }
    .button:hover {
      background-color: #45a049;
    }
    video {
      max-width: 100%;
      height: auto;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>摄像头</h1>
    <video id="video" autoplay playsinline controls muted></video>
    <button class="button" onclick="captureImage()">抓图</button>
  </div>

  <script>
    async function startStream(videoId, camPath) {
      try {
        console.log('开始建立WebRTC连接...');
        const pc = new RTCPeerConnection();

        pc.ontrack = function (event) {
          console.log('收到视频流');
          document.getElementById(videoId).srcObject = event.streams[0];
        };

        pc.oniceconnectionstatechange = function() {
          console.log('ICE连接状态:', pc.iceConnectionState);
        };

        pc.onicecandidate = function(event) {
          console.log('ICE候选:', event.candidate);
        };

        const offer = await pc.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true });
        await pc.setLocalDescription(offer);
        console.log('本地描述设置完成');

        console.log('正在连接服务器...');
        const res = await fetch(`http://192.168.xx.xxx:8889/cam1/whep`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/sdp'
          },
          body: offer.sdp,
        });

        if (!res.ok) {
          throw new Error(`HTTP error! status: ${res.status}`);
        }

        const answer = await res.text();
        console.log('收到服务器响应');
        await pc.setRemoteDescription({ type: 'answer', sdp: answer });
        console.log('远程描述设置完成');
      } catch (error) {
        console.error('发生错误:', error);
        alert('连接失败: ' + error.message);
      }
    }

    async function captureImage() {
      try {
        // 使用canvas从视频流中抓取图片
        const video = document.getElementById('video');
        const canvas = document.createElement('canvas');
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
        
        // 将canvas转换为blob
        const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.95));
        const url = URL.createObjectURL(blob);
        
        // 创建下载链接
        const a = document.createElement('a');
        a.href = url;
        a.download = `snapshot_${new Date().toISOString().replace(/[:.]/g, '-')}.jpg`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
      } catch (error) {
        console.error('抓图失败:', error);
        alert('抓图失败: ' + error.message);
      }
    }

    startStream("video", "cam1");
  </script>
</body>
</html>

以上代码,包括调试过程,都交给cursor完成的,cursor确实很强大,个人认为比通义灵码方便且强大,方便只操作方面,生成的代码可以一键生效,强大是他的能力。

然后就能看到实时监控画面了,如果看不到,可以继续问cursor,AI比师傅好的地方就是你可以不停问,他会不厌其烦反复修改,你不用跟他客气,他就是帮你干活的,你尽管用命令的语气、使用合适prompt问他就OK。

以上

相关推荐
ChinaRainbowSea几秒前
补充:问题:CORS ,前后端访问跨域问题
java·spring boot·后端·spring
KiddoStone10 分钟前
多实例schedule job同步数据流的数据一致性设计和实现方案
java
岁忧31 分钟前
(LeetCode 每日一题) 1865. 找出和为指定值的下标对 (哈希表)
java·c++·算法·leetcode·go·散列表
YuTaoShao35 分钟前
【LeetCode 热题 100】240. 搜索二维矩阵 II——排除法
java·算法·leetcode
考虑考虑1 小时前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干2 小时前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying2 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·2 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
Bug退退退1233 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠3 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github