深入探索: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
相关推荐
fellow993 小时前
DSH Desktop:把 DeepSeek Harness 变成真正的桌面应用
electron·agent·ai编程·deepseek
CDwenhuohuo20 小时前
electron pc项目打包成桌面端
前端·javascript·electron
不可能片场2 天前
命令行中文变问号 我用环境变量救了场
前端·electron
不可能片场2 天前
AI视频口型对齐 用音频驱动说话镜头
前端·electron
Zoom3 天前
【开源】7 天,我用 Rust 复刻了 FinalShell!Rhost v1.0.0 发布
rust·electron·shell
爱丶不疚4 天前
Electron: Sentry 都采集了什么?你知道吗
前端·electron
格数致用4 天前
03- Luckysheet与Vue3+Electron集成实战指南
electron·vue3·luckysheet
fellow996 天前
Electron + pnpm 在鸿蒙应用里跑起来
electron·harmonyos·openharmony·deepseek·deepseekharness
记得开心一点嘛6 天前
Trellora:基于 Electron、React 和本地知识库的 AI 知识工作台
人工智能·react.js·electron
500846 天前
React Native for OpenHarmony 实战:三方库 react-native-url-polyfill 的鸿蒙化适配指南
javascript·react native·react.js·性能优化·electron·harmonyos