From e630589489667456ee69fa16f43fa93af499dcd6 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 27 Feb 2023 13:19:32 -0800 Subject: [PATCH] cameras: auto detect two way audio --- plugins/amcrest/package-lock.json | 4 ++-- plugins/amcrest/package.json | 2 +- plugins/amcrest/src/amcrest-api.ts | 10 +++++++++ plugins/amcrest/src/main.ts | 15 ++++++++++++- plugins/ffmpeg-camera/src/common.ts | 1 + plugins/hikvision/package-lock.json | 4 ++-- plugins/hikvision/package.json | 2 +- plugins/hikvision/src/hikvision-camera-api.ts | 11 ++++++++++ plugins/hikvision/src/main.ts | 21 ++++++++++++++----- plugins/onvif/package-lock.json | 4 ++-- plugins/onvif/package.json | 2 +- plugins/onvif/src/main.ts | 15 +++++++++++++ plugins/onvif/src/onvif-intercom.ts | 17 +++++++++++---- 13 files changed, 89 insertions(+), 19 deletions(-) diff --git a/plugins/amcrest/package-lock.json b/plugins/amcrest/package-lock.json index 2fa506598..44271e477 100644 --- a/plugins/amcrest/package-lock.json +++ b/plugins/amcrest/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/amcrest", - "version": "0.0.117", + "version": "0.0.118", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/amcrest", - "version": "0.0.117", + "version": "0.0.118", "license": "Apache", "dependencies": { "@koush/axios-digest-auth": "^0.8.5", diff --git a/plugins/amcrest/package.json b/plugins/amcrest/package.json index 8cee48e0b..bedc452f3 100644 --- a/plugins/amcrest/package.json +++ b/plugins/amcrest/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/amcrest", - "version": "0.0.117", + "version": "0.0.118", "description": "Amcrest Plugin for Scrypted", "author": "Scrypted", "license": "Apache", diff --git a/plugins/amcrest/src/amcrest-api.ts b/plugins/amcrest/src/amcrest-api.ts index 9ee89558e..5bf1318a0 100644 --- a/plugins/amcrest/src/amcrest-api.ts +++ b/plugins/amcrest/src/amcrest-api.ts @@ -32,6 +32,16 @@ export class AmcrestCameraClient { }); } + async checkTwoWayAudio() { + const response = await this.digestAuth.request({ + httpsAgent: amcrestHttpsAgent, + method: "GET", + responseType: 'text', + url: `http://${this.ip}/cgi-bin/devAudioOutput.cgi?action=getCollect`, + }); + return (response.data as string).includes('result=1'); + } + // appAutoStart=true // deviceType=IP4M-1041B // hardwareVersion=1.00 diff --git a/plugins/amcrest/src/main.ts b/plugins/amcrest/src/main.ts index 0847cd58f..732532f2d 100644 --- a/plugins/amcrest/src/main.ts +++ b/plugins/amcrest/src/main.ts @@ -545,9 +545,10 @@ class AmcrestProvider extends RtspProvider { const username = settings.username?.toString(); const password = settings.password?.toString(); const skipValidate = settings.skipValidate === 'true'; + let twoWayAudio: string; if (!skipValidate) { + const api = new AmcrestCameraClient(httpAddress, username, password, this.console); try { - const api = new AmcrestCameraClient(httpAddress, username, password, this.console); const deviceInfo = await api.getDeviceInfo(); settings.newCamera = deviceInfo.deviceType; @@ -558,6 +559,16 @@ class AmcrestProvider extends RtspProvider { this.console.error('Error adding Amcrest camera', e); throw e; } + + try { + if (await api.checkTwoWayAudio()) { + // onvif seems to work better than Amcrest, except for AD110. + twoWayAudio = 'ONVIF'; + } + } + catch (e) { + this.console.warn('Error probing two way audio', e); + } } settings.newCamera ||= 'Hikvision Camera'; @@ -569,6 +580,8 @@ class AmcrestProvider extends RtspProvider { device.putSetting('password', password); device.setIPAddress(settings.ip?.toString()); device.setHttpPortOverride(settings.httpPort?.toString()); + if (twoWayAudio) + device.putSetting('twoWayAudio', twoWayAudio); return nativeId; } diff --git a/plugins/ffmpeg-camera/src/common.ts b/plugins/ffmpeg-camera/src/common.ts index 490724805..9831da37b 100644 --- a/plugins/ffmpeg-camera/src/common.ts +++ b/plugins/ffmpeg-camera/src/common.ts @@ -208,6 +208,7 @@ export abstract class CameraProviderBase e name, interfaces, type: type || ScryptedDeviceType.Camera, + info: deviceManager.getNativeIds().includes(nativeId) ? deviceManager.getDeviceState(nativeId)?.info : undefined, }); } diff --git a/plugins/hikvision/package-lock.json b/plugins/hikvision/package-lock.json index 5f5655647..5d84c8680 100644 --- a/plugins/hikvision/package-lock.json +++ b/plugins/hikvision/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/hikvision", - "version": "0.0.123", + "version": "0.0.124", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/hikvision", - "version": "0.0.123", + "version": "0.0.124", "license": "Apache", "dependencies": { "@koush/axios-digest-auth": "^0.8.5", diff --git a/plugins/hikvision/package.json b/plugins/hikvision/package.json index b7b7a081a..34f2b06f9 100644 --- a/plugins/hikvision/package.json +++ b/plugins/hikvision/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/hikvision", - "version": "0.0.123", + "version": "0.0.124", "description": "Hikvision Plugin for Scrypted", "author": "Scrypted", "license": "Apache", diff --git a/plugins/hikvision/src/hikvision-camera-api.ts b/plugins/hikvision/src/hikvision-camera-api.ts index 9deb9a4d8..71331082f 100644 --- a/plugins/hikvision/src/hikvision-camera-api.ts +++ b/plugins/hikvision/src/hikvision-camera-api.ts @@ -43,6 +43,17 @@ export class HikvisionCameraAPI { return getDeviceInfo(this.digestAuth, this.ip); } + async checkTwoWayAudio() { + const response = await this.digestAuth.request({ + httpsAgent: hikvisionHttpsAgent, + method: "GET", + responseType: 'text', + url: `http://${this.ip}/ISAPI/System/TwoWayAudio/channels`, + }); + + return (response.data as string).includes('Speaker'); + } + async checkDeviceModel(): Promise { if (!this.deviceModel) { this.deviceModel = this.getDeviceInfo().then(d => d.deviceModel).catch(e => { diff --git a/plugins/hikvision/src/main.ts b/plugins/hikvision/src/main.ts index d9156c745..a8dada3b9 100644 --- a/plugins/hikvision/src/main.ts +++ b/plugins/hikvision/src/main.ts @@ -1,13 +1,12 @@ import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers'; import { readLength } from '@scrypted/common/src/read-stream'; -import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoStreamOptions } from "@scrypted/sdk"; +import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting } from "@scrypted/sdk"; import child_process, { ChildProcess } from 'child_process'; import { PassThrough, Readable } from "stream"; -import { sleep } from "../../../common/src/sleep"; +import xml2js from 'xml2js'; import { OnvifIntercom } from "../../onvif/src/onvif-intercom"; import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp"; -import { getChannel, HikvisionCameraAPI, HikvisionCameraEvent } from "./hikvision-camera-api"; -import xml2js from 'xml2js'; +import { HikvisionCameraAPI, HikvisionCameraEvent } from "./hikvision-camera-api"; import { hikvisionHttpsAgent } from './probe'; const { mediaManager } = sdk; @@ -537,9 +536,10 @@ class HikvisionProvider extends RtspProvider { const username = settings.username?.toString(); const password = settings.password?.toString(); const skipValidate = settings.skipValidate === 'true'; + let twoWayAudio: string; if (!skipValidate) { + const api = new HikvisionCameraAPI(httpAddress, username, password, this.console); try { - const api = new HikvisionCameraAPI(httpAddress, username, password, this.console); const deviceInfo = await api.getDeviceInfo(); settings.newCamera = deviceInfo.deviceName; @@ -553,6 +553,15 @@ class HikvisionProvider extends RtspProvider { this.console.error('Error adding Hikvision camera', e); throw e; } + + try { + if (await api.checkTwoWayAudio()) { + twoWayAudio = 'Hikvision'; + } + } + catch (e) { + this.console.warn('Error probing two way audio', e); + } } settings.newCamera ||= 'Hikvision Camera'; @@ -564,6 +573,8 @@ class HikvisionProvider extends RtspProvider { device.putSetting('password', password); device.setIPAddress(settings.ip?.toString()); device.setHttpPortOverride(settings.httpPort?.toString()); + if (twoWayAudio) + device.putSetting('twoWayAudio', twoWayAudio); return nativeId; } diff --git a/plugins/onvif/package-lock.json b/plugins/onvif/package-lock.json index 8313e8db3..91b66c3f3 100644 --- a/plugins/onvif/package-lock.json +++ b/plugins/onvif/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/onvif", - "version": "0.0.113", + "version": "0.0.114", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/onvif", - "version": "0.0.113", + "version": "0.0.114", "license": "Apache", "dependencies": { "@koush/axios-digest-auth": "^0.8.5", diff --git a/plugins/onvif/package.json b/plugins/onvif/package.json index f8307f9e3..281a1a6ec 100644 --- a/plugins/onvif/package.json +++ b/plugins/onvif/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/onvif", - "version": "0.0.113", + "version": "0.0.114", "description": "ONVIF Camera Plugin for Scrypted", "author": "Scrypted", "license": "Apache", diff --git a/plugins/onvif/src/main.ts b/plugins/onvif/src/main.ts index a897651d0..b5daa0b96 100644 --- a/plugins/onvif/src/main.ts +++ b/plugins/onvif/src/main.ts @@ -561,6 +561,21 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery { device.putSetting('password', password); device.setIPAddress(settings.ip?.toString()); device.setHttpPortOverride(settings.httpPort?.toString()); + + const intercom = new OnvifIntercom(device); + try { + intercom.url = (await device.getConstructedVideoStreamOptions())[0].url; + if (await intercom.checkIntercom()) { + device.putSetting('onvifTwoWay', 'true'); + } + } + catch (e) { + this.console.warn("error while probing intercom", e); + } + finally { + intercom.intercomClient?.client.destroy(); + } + return nativeId; } diff --git a/plugins/onvif/src/onvif-intercom.ts b/plugins/onvif/src/onvif-intercom.ts index 63ae9450b..32ec7cc29 100644 --- a/plugins/onvif/src/onvif-intercom.ts +++ b/plugins/onvif/src/onvif-intercom.ts @@ -65,6 +65,8 @@ function* parseCodecs(audioSection: string): Generator { } } +const Require = 'www.onvif.org/ver20/backchannel'; + export class OnvifIntercom implements Intercom { intercomClient: RtspClient; url: string; @@ -72,9 +74,7 @@ export class OnvifIntercom implements Intercom { constructor(public camera: RtspSmartCamera) { } - async startIntercom(media: MediaObject) { - await this.stopIntercom(); - + async checkIntercom() { const username = this.camera.storage.getItem("username"); const password = this.camera.storage.getItem("password"); const url = new URL(this.url); @@ -83,7 +83,6 @@ export class OnvifIntercom implements Intercom { this.intercomClient = new RtspClient(url.toString()); this.intercomClient.console = this.camera.console; await this.intercomClient.options(); - const Require = 'www.onvif.org/ver20/backchannel'; const describe = await this.intercomClient.describe({ Require, @@ -95,6 +94,16 @@ export class OnvifIntercom implements Intercom { if (!audioBackchannel) throw new Error('ONVIF audio backchannel not found'); + return audioBackchannel; + } + + async startIntercom(media: MediaObject) { + await this.stopIntercom(); + + const audioBackchannel = await this.checkIntercom(); + if (!audioBackchannel) + throw new Error('ONVIF audio backchannel not found'); + const rtp = await reserveUdpPort(); const rtcp = rtp + 1;