深入探索:electron应用中利用node.js打造播放rtsp流直播功能

不久前的一个☀️晴朗的早晨,我坐在办公室里一边🔥疯狂敲打键盘,看似努力工作实则是和我的小伙伴们💬聊得不亦乐乎,一边享受着第一口热豆浆☕。突然,身后传来了一声咳嗽声,🙀紧接着一个声音传过来了:"Achuy,跟我来会议室一趟".

😰😰我心头一跳,没想到老大会亲自来我工位找我,还看到我摸鱼了😬。怀着忐忑不安的心情,跟随老大一路走到会议室。

"坐",老大一脸严肃的坐在另一边的位置上与我对望😳。是要警告我不要在上班一直聊天吧?还是说看到我最近的周报,对我近期的工作进度不满意?我头脑瞬间涌现出纷繁的思绪.

"Achuy呀,在整个开发组里面,云课堂客户端一直是你负责,最近产品提出一些新的想法,老板听后觉得这个想法满足用户的需求,并提供良好的用户体验。还能够跟上市场趋势📈,并能以合理的价格和高质量的性能吸引消费者的关注💰,所以我想跟你谈谈一些新增需求"。

听到这里我点了点头,我的内心渐渐平静,犹如大海的波涛渐渐平息🌊🌊,恢复了宁静与平和😌。此刻,所有的焦虑与不安都得到了释放。"嗯还好,需求而已,不是讲摸鱼的事情一切好说",我内心想到😁。

接着老大继续说到:"目前的云课堂客户端主要是用到janus和srs进行直播和互动,现在我们需要做到的就是直接获取硬件组那边录播摄像头的rtsp流,能够实时录制、直播和互动📹🎥".

听到这个需求,我内心拒绝🙅‍♂️🚫,但嘴上却说:"相当于就是云课堂客户端对接硬件录播那边的流,这样能够直接获取录播的rtsp流,这意味着用户可以随时随地进行实时录制,直播和互动。无论是家庭聚会、企业会议还是教学活动,都能轻松记录下来,分享给更多的人,这样后面也能软件+硬件捆绑在一起直接俄一套卖了,挺好的呀👌".

"嗯,是的,目前需要你先试验一下,在electron如何播放rtsp流,大概需要多久时间?",老大的嘴角浮现出一丝丝的微笑😏。

我心头一紧😬,想到我最近时间的摸鱼,还被老大发现了,暗自下定决心,要快速做出一番成绩👊💪。硬着头皮说:"2天..."🗓️🕑。

回到工位上,开始到各大搜索网站上搜各种能在electron上播放rtsp流的方法🕵️‍♂️👨‍💻。

electron应用本质上就是网页套壳的播放应用,所以这个问题本质上就是如何在网页上播放rtsp流。

网页播放rtsp流常见操作

  1. 安装vlc插件
  2. rtsp转rtmp然后使用videojs通过flash播放rtmp
  3. 后端通过ffmpeg实时转码将rtsp流转成video标签能播放的形式

第一套方案已经不太好实现了,现今的浏览器对于vlc插件几乎都不再支持了。

第二套方案flash在2020年也将被chrome停止支持

第三套方案是需求是直接获取录播的rtsp流,所以也不能使用了。

怎么办,第一天就这么过去了...

第二天到了,今天的天气仿佛被灰蒙蒙的云层所笼罩,阳光被遮蔽在厚重的阴霾中。内心之中,我感到一丝焦虑的种子悄然滋生,如同那被层层阴云遮掩的太阳,让我倍感失落和彷徨。与天气的阴沉相伴,我的内心也被犹如漩涡般的焦虑所缠绕,深深地嵌入其中,难以自拔。我顶着个黑眼圈来到我的工位,开起电脑眼前发黑,挨在椅子上。

这时,旁边的李子拍了拍我说到:"怎么回事,没点精神"。我回到:"新需求,网上没啥有用的资料"。李子说:"你之前不是自己搭建了一个Chatgpt应用吗?有问过Chatgpt吗?"。突然间我仿佛看到一道光,犹如夜空中的一颗明星,突然破晓,划破黑暗;或像雷鸣般突如其来,震撼人心,令人无法忽视。它不需要多言,却能一瞬间点燃思绪,引起人们对于世界的深思,使人醒悟于存在的意义。

噢,是的,我完成了ChatGPT的搭建,应该是用得上了呢,迫不及待地来试试看吧。

运用我的CV大法,抄到我的electron应用中,然后运行

嗯,问题不大,应该是这个库版本问题,去github上找找这个node-rtsp-stream库,看看例子demo

跟着demo走,接着一番操作后试错后,成功了,但结果不太妙

js 复制代码
const rtspStream = new Stream({
  name: 'name',
  streamUrl: 'rtsp://192.168.2.162:554/1',
  wsPort: 9925,
  ffmpegOptions: { // options ffmpeg flags
    '-stats': '', // an option with no neccessary value uses a blank string
    '-r': 30 // options with required values specify the value after the key
  }
})
setTimeout(()=>{
  var player = new window.jsmpeg(new WebSocket('ws://localhost:9925/'), {
    canvas: document.querySelector('canvas') // Canvas should be a canvas DOM element
  })	
},2000)

这里注意的是npm下载后引入的jsmpeg有问题,所以从node_modules中拿到,通过require引入,通过查看代码发现

网上一堆直接就将url地址是ws://xxxx的放入到jsmpeg中结果发现 net::ERR_DISALLOWED_URL_SCHEME,从源码中看到如果直接以字符串的形式放入,是通过ajax获取数据的,但是node-rtsp-stream实际上是通过socket传输数据的。下图就是结果,一直花屏。

天色渐暗,傍晚将至,我定下的期限越来越近,我仿佛看到老大询问我试验的情况,我心中烦忧。我重新回顾整个寻找方法的过程,目光落在了播放rtsp流常用操作的第三条后端通过ffmpeg实时转码将rtsp流转成video标签能播放的形式,我猛地想到,node-rtsp-stream实际上就是本地开启服务,然后进行数据传输而已,那么也就是说我也可以本地开启服务,直接用ffmpeg来转,接着数据传输不就完了吗?

接着开始往这个方向去找各种资料,终于集众家所长,硬是在最后时刻试验成功

核心思路

  1. electron通过线程开启本地ffmpeg服务将rtsp视频流转换为flv
  2. electron开启本地websocket服务,通过websocket传输flv视频流
  3. electron视图连接本地websocket,在获取到视频流后,使用flvjs对视频流再一次处理并进行播放

最终,赶在临下班前的半小时,找到老大说,这需求没问题,能做👊👊👊。

完整代码

js 复制代码
const http = require("http");
const { WebSocketServer } = require("ws");
const { spawn } = require("child_process");
const webSocketStream = require("websocket-stream/stream");
class LocalFFmpegServer {
  constructor() {
    this.app = null;
    this.port = 30000;
  }
  parseURL(url) {
    var list = url.split("?");
    console.log(list);
    var _url = list.shift();
    var params = list.join("?");
    if (params === "") {
      return {
        url: _url,
        params: {}
      };
    }
    var result = {};
    var paramsList = params.split("&");
    paramsList.forEach((item) => {
      var l = item.split("=");
      var key = l.shift();
      result[key] = l.join("=");
    });
    console.log(_url);
    return {
      url: _url,
      params: result
    };
  }
  init() {
    var _uid = 0;
    return new Promise((resolve, reject) => {
      const server = http.createServer((req, res) => {
        res.writeHead(200, { "Content-Type": "text/plain" });
        res.end("Not Find Server");
      });
      const wss = new WebSocketServer({ server });
      wss.on("connection", (ws, req) => {
        _uid += 1;
        const id = _uid;
        var { url, params } = this.parseURL(req.url);
        console.log(`【${id} process open】\nurl  =>${url}\nparams =>${params}`);
        if (params['url'] === void 0) {
          reject();
        }
        const stream = webSocketStream(
          ws,
          {
            binary: true,
            browserBufferTimeout: 1000000
          },
          {
            browserBufferTimeout: 1000000
          }
        );
        var ffmpegProcess = null;
        try {
          // 这里的ffmpeg需要将打包的地址进行特换
          // 如xxx/xxx/ffmpeg.exe
          ffmpegProcess = spawn("ffmpeg", [
            "-i",
            params['url'],
            "-c:v",
            "copy",
            "-c:a",
            "copy",
            "-f",
            "flv",
            "pipe:1"
          ]);
          // 处理错误信息
          ffmpegProcess.stderr.on("data", (data) => {
            console.error(`【${id}】FFmpeg Error: ${data}`);
          });
          // 监听FFmpeg进程的退出事件
          ffmpegProcess.on("close", (code) => {
            if (code !== 0) {
              console.error(`【${id}】FFmpeg process exited with code ${code}`);
            }
          });
          ffmpegProcess.stdout.pipe(stream);
        } catch (e) {
          if (ffmpegProcess !== null) {
            ffmpegProcess.kill();
          }
          console.log(e);
        }
      });
      this.app = server;
      // 避免端口被电脑其他服务占用,检测到无法使用后,端口自动加一,直至能使用为止
      var openServer = () => {
        console.log("try open Server");
        if (this.app === null) {
          reject();
          return;
        }
        server
          .listen(this.port, () => {
            resolve();
            console.log(`Server listening on port ${this.port}`);
          })
          .on("error", () => {
            this.port += 1;
            setTimeout(openServer, 1000);
          });
        server.on("close", function() {
          console.log("LocalFFmpegServer close");
        });
      };
      openServer();
    });
  }

  destory() {
    if (this.app !== null) {
      this.app.close();
      this.app = null;
    }
  }
}

使用方式

js 复制代码
var ffmpegServer = new LocalFFmpegServer();
// 不使用时记得调用destory释放服务,释放资源避免引发奇奇怪怪的问题
ffmpegServer.init().then(() => {
  if (flvjs.isSupported()) {
    let video = this.$refs.player;

    this.rtsp = "rtsp://stream.strba.sk:1935/strba/VYHLAD_JAZERO.stream";
    // this.rtsp = "rtsp://192.168.2.162:554/1"
    if (video) {
      this.player = flvjs.createPlayer({
        type: "flv",
        isLive: true,
        url: `ws://localhost:${ffmpegServer.port}/rtsp/101/?url=${this.rtsp}`
      });
      this.player.attachMediaElement(video);
      try {
        this.player.load();
        this.player.play();
      } catch (error) {
        console.log(error);
      }
    }
  }
});

rtsp测试地址

分享rtsp测试地址

来源 地址 延迟
Nordland rtsp://77.110.228.219/axis-media/media.amp 200ms
Norwich rtsp://37.157.51.30/axis-media/media.amp 250ms
Orlando rtsp://97.68.104.34/axis-media/media.am 350ms
PriceCenterPlaza rtsp://132.239.12.145:554/axis-media/media.amp 280ms
Vaison-La-Romaine rtsp://176.139.87.16/axis-media/media.amp
VyhladJazero rtsp://stream.strba.sk:1935/strba/VYHLAD_JAZERO.stream 160ms
Western Cape rtsp://196.21.92.82/axis-media/media.amp 450ms
Zeeland rtsp://213.34.225.97/axis-media/media.amp 270ms
Allendale rtsp://71.83.5.156/axis-media/media.amp 270ms
Bedford Hills rtsp://73.114.177.111/axis-media/media.amp 340ms
相关推荐
今天也想MK代码2 天前
在Swift开发中简化应用程序发布与权限管理的解决方案——SparkleEasy
前端·javascript·chrome·macos·electron·swiftui
yqcoder3 天前
electron 中 ipcRenderer 的常用方法有哪些?
前端·javascript·electron
yqcoder3 天前
electron 中 ipcRenderer 作用
前端·javascript·electron
伍嘉源4 天前
关于electron进程管理的一些认识
前端·javascript·electron
yqcoder4 天前
electron 设置最小窗口缩放
前端·javascript·electron
yqcoder5 天前
区分 electron 全屏和最大化
前端·javascript·electron
li.siyuan5 天前
electron + vue 打包完成后,运行提示 electrion-updater 不存在
前端·vue.js·electron
努力挣钱的小鑫6 天前
【客户端开发】electron 中无法使用 js-cookie 的问题
前端·javascript·electron
蓝胖子不是胖子6 天前
尝鲜electron --将已有vue/react项目转换为桌面应用
vue.js·react.js·electron
非晓为骁6 天前
windows 下 electron-builder ERR_ELECTRON_BUILDER_CANNOT_EXECUTE 报错处理
javascript·windows·electron