From b9ae0e27e6e87b2432dcc9438ae96e9e1d78ebef Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 14 Mar 2022 00:57:43 -0700 Subject: [PATCH] amcrest: publish continuous recording --- common/.gitignore | 1 + common/.vscode/launch.json | 21 +++++++++++++++++++++ common/package.json | 1 + common/src/eval/scrypted-eval.ts | 4 ++-- common/src/ffmpeg-to-wrtc.ts | 29 +++++++++++++++++------------ common/src/rtsp-server.ts | 17 ++++++++++++----- plugins/amcrest/package-lock.json | 4 ++-- plugins/amcrest/package.json | 2 +- plugins/rtsp/src/rtsp.ts | 20 ++++++++++++++------ 9 files changed, 71 insertions(+), 28 deletions(-) create mode 100644 common/.vscode/launch.json diff --git a/common/.gitignore b/common/.gitignore index bea91304d..14a48037b 100644 --- a/common/.gitignore +++ b/common/.gitignore @@ -3,3 +3,4 @@ node_modules .gcloud/ dist/ scrypted.db +src/test.ts diff --git a/common/.vscode/launch.json b/common/.vscode/launch.json new file mode 100644 index 000000000..4bebc08d3 --- /dev/null +++ b/common/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "pwa-node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/dist/common/src/test.js", + "preLaunchTask": "npm: build", + "outFiles": [ + "${workspaceFolder}/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/common/package.json b/common/package.json index ba2666192..a83ba628a 100644 --- a/common/package.json +++ b/common/package.json @@ -4,6 +4,7 @@ "description": "", "main": "index.js", "scripts": { + "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", diff --git a/common/src/eval/scrypted-eval.ts b/common/src/eval/scrypted-eval.ts index dcbc3a4d0..7fbe262e7 100644 --- a/common/src/eval/scrypted-eval.ts +++ b/common/src/eval/scrypted-eval.ts @@ -120,7 +120,7 @@ export function createMonacoEvalDefaults(extraLibs: { [lib: string]: string }) { const libs = Object.assign(getTypeDefs(), extraLibs); - function monacoEvalDefaultsFunction(monaco, safeLibs, libs) { + function monacoEvalDefaultsFunction(monaco: any, safeLibs: any, libs: any) { monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions( Object.assign( {}, @@ -223,7 +223,7 @@ export function createScriptDevice(baseInterfaces: string[]): ScriptDeviceImpl { const iface = methodInterfaces.get(method); if (iface) { allInterfaces.push(iface); - device[method] = handler[method].bind(handler); + (device as any)[method] = handler[method].bind(handler); } } return allInterfaces; diff --git a/common/src/ffmpeg-to-wrtc.ts b/common/src/ffmpeg-to-wrtc.ts index 71b23ef3c..813eb2545 100644 --- a/common/src/ffmpeg-to-wrtc.ts +++ b/common/src/ffmpeg-to-wrtc.ts @@ -17,6 +17,18 @@ const configuration: RTCConfiguration = { ], }; +export function isPeerConnectionAlive(pc :RTCPeerConnection) { + if (pc.iceConnectionState === 'disconnected' + || pc.iceConnectionState === 'failed' + || pc.iceConnectionState === 'closed') + return false; + if (pc.connectionState === 'closed' + || pc.connectionState === 'disconnected' + || pc.connectionState === 'failed') + return false; + return true; +} + let wrtc: any; function initalizeWebRtc() { if (wrtc) @@ -214,20 +226,12 @@ export async function startRTCPeerConnectionFFmpegInput(ffInput: FFMpegInput, op } const checkConn = () => { - if (pc.iceConnectionState === 'disconnected' - || pc.iceConnectionState === 'failed' - || pc.iceConnectionState === 'closed') { + if (!isPeerConnectionAlive(pc)) cleanup(); - } - if (pc.connectionState === 'closed' - || pc.connectionState === 'disconnected' - || pc.connectionState === 'failed') { - cleanup(); - } } - pc.onconnectionstatechange = checkConn; - pc.oniceconnectionstatechange = checkConn; + pc.addEventListener('connectionstatechange', checkConn); + pc.addEventListener('iceconnectionstatechange', checkConn); setTimeout(() => { if (pc.connectionState !== 'connected') { @@ -273,6 +277,7 @@ export async function startRTCPeerConnection(console: Console, mediaObject: Medi }); await pc.setRemoteDescription(answer); + return pc; } catch (e) { pc.close(); @@ -314,7 +319,7 @@ export async function startBrowserRTCSignaling(camera: ScryptedDevice & RTCSigna camera.startRTCSignalingSession(session, options); } else { - startRTCPeerConnectionForBrowser(console, await camera.getVideoStream(), session, options); + return startRTCPeerConnectionForBrowser(console, await camera.getVideoStream(), session, options); } } catch (e) { diff --git a/common/src/rtsp-server.ts b/common/src/rtsp-server.ts index f2ccc4e5c..e5ace0f74 100644 --- a/common/src/rtsp-server.ts +++ b/common/src/rtsp-server.ts @@ -6,7 +6,7 @@ import { findTrack } from './sdp-utils'; import dgram from 'dgram'; import net from 'net'; import tls from 'tls'; -import { DIGEST } from 'http-auth-utils/src/index'; +import { DIGEST } from 'http-auth-utils/dist/index'; import crypto from 'crypto'; export const RTSP_FRAME_MAGIC = 36; @@ -244,9 +244,11 @@ export class RtspClient extends RtspBase { }); } - async setup(channel: number, path?: string) { + async setup(channelOrPort: number, path?: string, udp?: boolean) { + const protocol = udp ? 'UDP' : 'TCP'; + const client = udp ? 'client_port' : 'interleaved'; const headers: any = { - Transport: `RTP/AVP/TCP;unicast;interleaved=${channel}-${channel + 1}`, + Transport: `RTP/AVP/${protocol};unicast;${client}=${channelOrPort}-${channelOrPort + 1}`, }; const response = await this.request('SETUP', headers, path) if (response.headers.session) { @@ -266,14 +268,19 @@ export class RtspClient extends RtspBase { return response; } - async play() { + async play(start: string = '0.000') { const headers: any = { - Range: 'npt=0.000-', + Range: `npt=${start}-`, }; await this.request('PLAY', headers); return this.client; } + async pause() { + await this.request('PAUSE'); + return this.client; + } + async teardown() { try { return await this.request('TEARDOWN'); diff --git a/plugins/amcrest/package-lock.json b/plugins/amcrest/package-lock.json index 191515de3..f1ab0f385 100644 --- a/plugins/amcrest/package-lock.json +++ b/plugins/amcrest/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/amcrest", - "version": "0.0.90", + "version": "0.0.91", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/amcrest", - "version": "0.0.90", + "version": "0.0.91", "license": "Apache", "dependencies": { "@koush/axios-digest-auth": "^0.8.5", diff --git a/plugins/amcrest/package.json b/plugins/amcrest/package.json index a09c7c6ff..abbcb61f6 100644 --- a/plugins/amcrest/package.json +++ b/plugins/amcrest/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/amcrest", - "version": "0.0.90", + "version": "0.0.91", "description": "Amcrest Plugin for Scrypted", "author": "Scrypted", "license": "Apache", diff --git a/plugins/rtsp/src/rtsp.ts b/plugins/rtsp/src/rtsp.ts index 501d7201a..edbe6926a 100644 --- a/plugins/rtsp/src/rtsp.ts +++ b/plugins/rtsp/src/rtsp.ts @@ -50,18 +50,15 @@ export class RtspCamera extends CameraBase { return ret; } - async createVideoStream(vso: UrlMediaStreamOptions): Promise { - if (!vso) - throw new Error('video streams not set up or no longer exists.'); - + addRtspCredentials(rtspUrl: string) { // ignore this deprecation warning. the WHATWG URL class will trim the password // off if it is empty, resulting in urls like rtsp://admin@foo.com/. // this causes ffmpeg to fail on sending a blank password. // we need to send it as follows: rtsp://admin:@foo.com/. // Note the trailing colon. // issue: https://github.com/koush/scrypted/issues/134 - const parsedUrl = url.parse(vso.url); - this.console.log('rtsp stream url', vso.url); + const parsedUrl = url.parse(rtspUrl); + this.console.log('rtsp stream url', rtspUrl); const username = this.storage.getItem("username"); const password = this.storage.getItem("password"); if (username) { @@ -71,7 +68,10 @@ export class RtspCamera extends CameraBase { } const stringUrl = url.format(parsedUrl); + return stringUrl; + } + createFfmpegMediaObject(stringUrl: string, vso: MediaStreamOptions) { const ret: FFMpegInput = { url: stringUrl, inputArguments: [ @@ -84,6 +84,14 @@ export class RtspCamera extends CameraBase { return mediaManager.createFFmpegMediaObject(ret); } + async createVideoStream(vso: UrlMediaStreamOptions): Promise { + if (!vso) + throw new Error('video streams not set up or no longer exists.'); + + const stringUrl = this.addRtspCredentials(vso.url); + return this.createFfmpegMediaObject(stringUrl, vso); + } + // hide the description from CameraBase that indicates it is only used for snapshots getUsernameDescription(): string { return;