diff --git a/package-lock.json b/package-lock.json index e1f905fb..a26ceafa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,10 +18,10 @@ "date-fns": "^2.28.0", "electron-log": "^4.4.6", "execa": "^6.1.0", - "jsmpeg": "^1.0.0", "lodash": "^4.17.21", "nanoid": "^3.3.1", "node-rtsp-stream": "^0.0.9", + "node-rtsp-stream-es6": "^1.0.7", "polished": "^4.1.4", "qr-scanner": "^1.4.1", "react": "^17.0.2", @@ -4616,6 +4616,11 @@ "node": ">=0.12.0" } }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -11680,6 +11685,23 @@ "ws": "^7.0.0" } }, + "node_modules/node-rtsp-stream-es6": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/node-rtsp-stream-es6/-/node-rtsp-stream-es6-1.0.7.tgz", + "integrity": "sha512-UsOt0Ouj6ZY6AG4QMsDBtttUq2+0JfwdptW/bCjOKh3a1FmGmBLLgmMYZIi1V3uu++MBkZT7NWarJ0tKw+YdYw==", + "dependencies": { + "jsmpeg": "^1.0.0", + "ws": "7.1.2" + } + }, + "node_modules/node-rtsp-stream-es6/node_modules/ws": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.2.tgz", + "integrity": "sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==", + "dependencies": { + "async-limiter": "^1.0.0" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -19317,6 +19339,11 @@ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -24695,6 +24722,25 @@ "ws": "^7.0.0" } }, + "node-rtsp-stream-es6": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/node-rtsp-stream-es6/-/node-rtsp-stream-es6-1.0.7.tgz", + "integrity": "sha512-UsOt0Ouj6ZY6AG4QMsDBtttUq2+0JfwdptW/bCjOKh3a1FmGmBLLgmMYZIi1V3uu++MBkZT7NWarJ0tKw+YdYw==", + "requires": { + "jsmpeg": "^1.0.0", + "ws": "7.1.2" + }, + "dependencies": { + "ws": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.2.tgz", + "integrity": "sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==", + "requires": { + "async-limiter": "^1.0.0" + } + } + } + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", diff --git a/package.json b/package.json index fc055448..bade374e 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "date-fns": "^2.28.0", "electron-log": "^4.4.6", "execa": "^6.1.0", - "jsmpeg": "^1.0.0", "lodash": "^4.17.21", "nanoid": "^3.3.1", "node-rtsp-stream": "^0.0.9", diff --git a/src/main/RtspServer/script.ts b/script/rtspServer.js similarity index 53% rename from src/main/RtspServer/script.ts rename to script/rtspServer.js index f6157fb6..8eed879d 100644 --- a/src/main/RtspServer/script.ts +++ b/script/rtspServer.js @@ -1,9 +1,12 @@ -import RTSPServer from 'node-rtsp-stream'; +const Stream = require('node-rtsp-stream'); -new RTSPServer.Stream({ - name: 'name', - streamUrl: 'rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mp4', - wsPort: 9999, +// Get node args +const args = process.argv.slice(2); + +new Stream({ + name: 'RTSP-Stream', + streamUrl: args[0], + wsPort: parseInt(args[1]), ffmpegOptions: { // options ffmpeg flags '-stats': '', // an option with no neccessary value uses a blank string diff --git a/snowpack.config.js b/snowpack.config.js index 726b3dc8..8819a19a 100644 --- a/snowpack.config.js +++ b/snowpack.config.js @@ -2,6 +2,7 @@ module.exports = { extends: 'electron-snowpack/config/snowpack.js', mount: { 'src/shared': '/shared', + script: '/script', }, alias: { '@/': './src/', @@ -12,4 +13,4 @@ module.exports = { packageOptions: { knownEntrypoints: ['date-fns/fp/format', 'react-is'], }, -} +}; diff --git a/src/main/preload/api.ts b/src/main/preload/api.ts index f8237e5b..4283cbec 100644 --- a/src/main/preload/api.ts +++ b/src/main/preload/api.ts @@ -33,6 +33,6 @@ export const preload = { }, rtsp: { start: (url: string) => ipcRenderer.invoke(RTSP_START, url), - stop: () => ipcRenderer.send(RTSP_STOP), + stop: (url: string) => ipcRenderer.send(RTSP_STOP, url), }, }; diff --git a/src/main/rtspServer.ts b/src/main/rtspServer.ts index 1f527ac8..933e2a3b 100644 --- a/src/main/rtspServer.ts +++ b/src/main/rtspServer.ts @@ -1,6 +1,8 @@ import { log } from '@/main/logger'; -import { ipcMain } from 'electron'; -import { execaNode, ExecaChildProcess } from 'execa'; +import { app, ipcMain } from 'electron'; +import { ExecaChildProcess, execa } from 'execa'; +import path from 'path'; +import { isDev } from '@/main/isDev'; interface RtspProcess { process: ExecaChildProcess; @@ -8,18 +10,26 @@ interface RtspProcess { } const rtspServers: Map = new Map(); -let nextPort = 9000; + +// Stack array of ports from (9000 to 9060) to use for rtsp servers +const ports = Array.from({ length: 61 }, (_, i) => i + 9000); ipcMain.handle('rtsp_start', (event, url: string) => { - const process = execaNode('script.ts'); + const nextPort = ports.shift() ?? 9000; + const process = execa('node', [ + isDev + ? './script/rtspServer.js' + : path.join(app.getAppPath(), '../renderer/script/rtspServer.js'), + url, + nextPort.toString(), + ]); - log.info('starting rtsp process'); rtspServers.set(url, { process, wsPort: nextPort, }); - return nextPort++; + return nextPort; }); ipcMain.on('rtsp_stop', (event, url: string) => { @@ -28,7 +38,8 @@ ipcMain.on('rtsp_stop', (event, url: string) => { if (!rtspProcess) { return; } - + log.info('stopping rtsp process'); + ports.push(rtspProcess.wsPort); rtspProcess.process.kill(); rtspServers.delete(url); }); diff --git a/src/renderer/components/Feed/Feeds/CameraFeed.tsx b/src/renderer/components/Feed/Feeds/CameraFeed.tsx index d3b999a8..d52165d8 100644 --- a/src/renderer/components/Feed/Feeds/CameraFeed.tsx +++ b/src/renderer/components/Feed/Feeds/CameraFeed.tsx @@ -9,7 +9,7 @@ import * as React from 'react'; import { FC, useEffect, useRef, useState } from 'react'; import { log } from '@/renderer/logger'; import { QRFeed } from './QRFeed/QRFeed'; -import { RTSPFeed } from './RTSPFeed/RTSPFeed'; +import { RTSPFeed } from './RTSPFeed'; interface Props { feed: ICameraFeed; @@ -147,7 +147,7 @@ const View: FC = ({ feed }) => { ); case CameraType.RTSP: - return ; + return ; default: return ; } diff --git a/src/renderer/components/Feed/Feeds/RTSPFeed.tsx b/src/renderer/components/Feed/Feeds/RTSPFeed.tsx new file mode 100644 index 00000000..eb4d668f --- /dev/null +++ b/src/renderer/components/Feed/Feeds/RTSPFeed.tsx @@ -0,0 +1,51 @@ +import React, { useEffect, useRef } from 'react'; +import JSMpeg from '@cycjimmy/jsmpeg-player'; +import { log } from '@/renderer/logger'; + +interface Props { + url: string; +} + +export const RTSPFeed = ({ url }: Props) => { + const videoRef = useRef(null); + const canvasRef = useRef(null); + + useEffect(() => { + log.info(`Starting RTSP server for ${url}`); + const videoWrapper = videoRef.current; + const canvas = canvasRef.current; + let videoElement: JSMpeg.VideoElement | null = null; + const startServer = async () => { + const port = (await window.preloadApi.rtsp.start(url)) as number; + log.info(`RTSP server started on port ${port}`); + if (videoWrapper && canvas) { + videoElement = new JSMpeg.VideoElement( + videoWrapper, + `ws://localhost:${port}`, + { + canvas, + autoplay: true, + audio: false, + } + ); + } + }; + startServer().catch((e) => log.error(e)); + + return () => { + log.info('Stopping RTSP server'); + window.preloadApi.rtsp.stop(url); + if (videoElement) { + videoElement.destroy(); + } + }; + }, [url]); + + return ( + <> +
+ +
+ + ); +}; diff --git a/src/renderer/components/Feed/Feeds/RTSPFeed/RTSPFeed.tsx b/src/renderer/components/Feed/Feeds/RTSPFeed/RTSPFeed.tsx deleted file mode 100644 index f17608ac..00000000 --- a/src/renderer/components/Feed/Feeds/RTSPFeed/RTSPFeed.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import JSMpeg from '@cycjimmy/jsmpeg-player'; -import { log } from '@/renderer/logger'; - -export const RTSPFeed = () => { - // Execute rtspServer node script with stream url as argument - const videoRef = useRef(null); - useEffect(() => { - const startServer = async () => { - const port = (await window.preloadApi.rtsp.start('rtsp://')) as number; - log.info(`RTSP server started on port ${port}`); - }; - startServer().catch((e) => log.error(e)); - }, []); - - // Create video element - useEffect(() => { - const videoWrapper = videoRef.current; - if (videoWrapper) { - new JSMpeg.VideoElement(videoWrapper, 'ws://localhost:9999', { - autoplay: true, - audio: false, - }); - } - }, [videoRef]); - - return ( - <> -
- - ); -}; diff --git a/src/renderer/components/Feed/Feeds/RTSPFeed/index.tsx b/src/renderer/components/Feed/Feeds/RTSPFeed/index.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/renderer/types/jsmpeg.d.ts b/src/renderer/types/jsmpeg.d.ts index 5dfae4a9..ad338839 100644 --- a/src/renderer/types/jsmpeg.d.ts +++ b/src/renderer/types/jsmpeg.d.ts @@ -9,5 +9,7 @@ declare module '@cycjimmy/jsmpeg-player' { audio?: boolean; } ); + + destroy(): void; } } diff --git a/src/renderer/types/nodeRtspStream.d.ts b/src/renderer/types/nodeRtspStream.d.ts deleted file mode 100644 index b5a4e5e3..00000000 --- a/src/renderer/types/nodeRtspStream.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare module 'node-rtsp-stream' { - class Stream { - constructor(params: { - name: string; - streamUrl: string; - wsPort: number; - ffmpegOptions: { - '-stats': string; - '-r': number; - }; - }); - } -}