From 4d7a3c2dfc9fb7e0e8f48e9890aca5c1909b22f0 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sat, 26 Mar 2022 20:57:20 -0700 Subject: [PATCH] unifi-protect: use pipes, safe kill ffmpeg interocm --- plugins/unifi-protect/package-lock.json | 45 +++++++++++++-- plugins/unifi-protect/package.json | 3 +- plugins/unifi-protect/src/camera.ts | 76 ++++++++++++++----------- plugins/unifi-protect/src/main.ts | 2 +- 4 files changed, 84 insertions(+), 42 deletions(-) diff --git a/plugins/unifi-protect/package-lock.json b/plugins/unifi-protect/package-lock.json index 5206b2251..fd5521c77 100644 --- a/plugins/unifi-protect/package-lock.json +++ b/plugins/unifi-protect/package-lock.json @@ -1,15 +1,16 @@ { "name": "@scrypted/unifi-protect", - "version": "0.0.106", + "version": "0.0.107", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/unifi-protect", - "version": "0.0.106", + "version": "0.0.107", "license": "Apache", "dependencies": { "@koush/unifi-protect": "file:../../external/unifi-protect", + "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", "node-fetch": "^3.2.1", "ws": "^8.5.0" @@ -20,13 +21,29 @@ "@types/ws": "^7.4.7" } }, + "../../common": { + "version": "1.0.1", + "license": "ISC", + "dependencies": { + "@koush/werift": "file:../external/werift/packages/webrtc", + "@koush/wrtc": "^0.5.3", + "@scrypted/sdk": "file:../sdk", + "http-auth-utils": "^3.0.2", + "node-fetch-commonjs": "^3.1.1", + "typescript": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^16.9.0" + } + }, "../../external/unifi-protect": { - "version": "3.0.2", + "name": "@koush/unifi-protect", + "version": "3.0.5", "license": "ISC", "dependencies": { "abort-controller": "^3.0.0", "domexception": "^4.0.0", - "node-fetch-cjs": "=3.2.0", + "node-fetch": "^3.2.1", "util": "^0.12.4", "ws": "^8.5.0" }, @@ -45,7 +62,7 @@ }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.0.174", + "version": "0.0.177", "license": "ISC", "dependencies": { "@babel/preset-typescript": "^7.16.7", @@ -85,6 +102,10 @@ "resolved": "../../external/unifi-protect", "link": true }, + "node_modules/@scrypted/common": { + "resolved": "../../common", + "link": true + }, "node_modules/@scrypted/sdk": { "resolved": "../../sdk", "link": true @@ -226,13 +247,25 @@ "abort-controller": "^3.0.0", "domexception": "^4.0.0", "eslint": "^8.9.0", - "node-fetch-cjs": "=3.2.0", + "node-fetch": "^3.2.1", "rimraf": "^3.0.2", "typescript": "^4.5.5", "util": "^0.12.4", "ws": "^8.5.0" } }, + "@scrypted/common": { + "version": "file:../../common", + "requires": { + "@koush/werift": "file:../external/werift/packages/webrtc", + "@koush/wrtc": "^0.5.3", + "@scrypted/sdk": "file:../sdk", + "@types/node": "^16.9.0", + "http-auth-utils": "^3.0.2", + "node-fetch-commonjs": "^3.1.1", + "typescript": "^4.4.3" + } + }, "@scrypted/sdk": { "version": "file:../../sdk", "requires": { diff --git a/plugins/unifi-protect/package.json b/plugins/unifi-protect/package.json index 9adfdfcf8..dc3d2b01b 100644 --- a/plugins/unifi-protect/package.json +++ b/plugins/unifi-protect/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/unifi-protect", - "version": "0.0.106", + "version": "0.0.107", "description": "Unifi Protect Plugin for Scrypted", "author": "Scrypted", "license": "Apache", @@ -37,6 +37,7 @@ }, "dependencies": { "@koush/unifi-protect": "file:../../external/unifi-protect", + "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", "node-fetch": "^3.2.1", "ws": "^8.5.0" diff --git a/plugins/unifi-protect/src/camera.ts b/plugins/unifi-protect/src/camera.ts index ea6246979..9b24a327b 100644 --- a/plugins/unifi-protect/src/camera.ts +++ b/plugins/unifi-protect/src/camera.ts @@ -1,14 +1,15 @@ import sdk, { ScryptedDeviceBase, DeviceProvider, Settings, Setting, VideoCamera, MediaObject, MotionSensor, ScryptedInterface, Camera, MediaStreamOptions, Intercom, ScryptedMimeTypes, FFMpegInput, ObjectDetector, PictureOptions, ObjectDetectionTypes, ObjectsDetected, Notifier, VideoCameraConfiguration, OnOff, MediaStreamUrl } from "@scrypted/sdk"; import { ProtectCameraChannelConfig, ProtectCameraConfigInterface, ProtectCameraLcdMessagePayload } from "./unifi-protect"; import child_process, { ChildProcess } from 'child_process'; -import { ffmpegLogInitialOutput } from '../../../common/src/media-helpers'; -import { fitHeightToWidth } from "../../../common/src/resolution-utils"; -import { listenZero } from "../../../common/src/listen-cluster"; +import { ffmpegLogInitialOutput, safeKillFFmpeg } from '@scrypted/common/src/media-helpers'; +import { fitHeightToWidth } from "@scrypted/common/src/resolution-utils"; +import { listenZero } from "@scrypted/common/src/listen-cluster"; import net from 'net'; import WS from 'ws'; import { once } from "events"; import { FeatureFlagsShim } from "./shim"; import { UnifiProtect } from "./main"; +import { Readable } from "stream"; const { log, deviceManager, mediaManager } = sdk; @@ -90,6 +91,8 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco } async startIntercom(media: MediaObject) { + this.stopIntercom(); + const buffer = await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput); const ffmpegInput = JSON.parse(buffer.toString()) as FFMpegInput; @@ -106,32 +109,6 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco const websocket = new WS(talkbackUrl, { rejectUnauthorized: false }); await once(websocket, 'open'); - const server = new net.Server(async (socket) => { - server.close(); - - this.console.log('sending audio data to', talkbackUrl); - - try { - while (websocket.readyState === WS.OPEN) { - await once(socket, 'readable'); - while (true) { - const data = socket.read(); - if (!data) - break; - websocket.send(data, e => { - if (e) - socket.destroy(); - }); - } - } - } - finally { - this.console.log('talkback ended') - this.intercomProcess.kill(); - } - }); - const port = await listenZero(server) - const args = [ '-hide_banner', @@ -152,17 +129,48 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco "-ac", camera.talkbackSettings.channels.toString(), "-b:a", "16k", "-f", "adts", - `tcp://127.0.0.1:${port}`, + `pipe:3`, ); this.console.log('starting 2 way audio', args); const ffmpeg = await mediaManager.getFFmpegPath(); - this.intercomProcess = child_process.spawn(ffmpeg, args); - this.intercomProcess.on('exit', () => { - websocket.close(); - this.intercomProcess = undefined; + const cp = this.intercomProcess = child_process.spawn(ffmpeg, args, { + stdio: ['pipe', 'pipe', 'pipe', 'pipe'], }); + this.intercomProcess.on('exit', () => { + this.intercomProcess = undefined; + websocket.close(); + }); + + websocket.on('close', () => safeKillFFmpeg(cp)); + + (async () => { + const socket = cp.stdio[3] as Readable; + this.console.log('sending audio data to', talkbackUrl); + + try { + while (websocket.readyState === WS.OPEN) { + await once(socket, 'readable'); + while (true) { + const data = socket.read(); + if (!data) + break; + websocket.send(data, e => { + if (e) { + safeKillFFmpeg(cp); + } + }); + } + } + } + finally { + this.console.log('talkback ended') + safeKillFFmpeg(cp); + } + })(); + + ffmpegLogInitialOutput(this.console, this.intercomProcess); } diff --git a/plugins/unifi-protect/src/main.ts b/plugins/unifi-protect/src/main.ts index 45a3700e7..912987708 100644 --- a/plugins/unifi-protect/src/main.ts +++ b/plugins/unifi-protect/src/main.ts @@ -1,6 +1,6 @@ import sdk, { ScryptedDeviceBase, DeviceProvider, Settings, Setting, ScryptedDeviceType, Device, ScryptedInterface, ObjectsDetected, ObjectDetectionResult } from "@scrypted/sdk"; import { ProtectApi, ProtectApiUpdates, ProtectNvrUpdatePayloadCameraUpdate, ProtectNvrUpdatePayloadEventAdd } from "./unifi-protect"; -import { createInstanceableProviderPlugin, enableInstanceableProviderMode, isInstanceableProviderModeEnabled } from '../../../common/src/provider-plugin'; +import { createInstanceableProviderPlugin, enableInstanceableProviderMode, isInstanceableProviderModeEnabled } from '@scrypted/common/src/provider-plugin'; import fetch, { RequestInfo, RequestInit, Response } from "node-fetch"; import { defaultSensorTimeout, UnifiCamera } from "./camera"; import { FeatureFlagsShim, LastSeenShim } from "./shim";