diff --git a/plugins/hikvision/src/hikvision-camera-api.ts b/plugins/hikvision/src/hikvision-camera-api.ts index c3a220fd6..b548ef6c5 100644 --- a/plugins/hikvision/src/hikvision-camera-api.ts +++ b/plugins/hikvision/src/hikvision-camera-api.ts @@ -1,5 +1,6 @@ import { Readable } from 'stream'; import AxiosDigestAuth from '@koush/axios-digest-auth'; +import { EventEmitter } from "stream"; export enum HikVisionCameraEvent { MotionDetected = "VMD", @@ -24,6 +25,8 @@ export interface HikVisionCameraStreamSetup { export class HikVisionCameraAPI { digestAuth: AxiosDigestAuth; + deviceModel : Promise; + listenerPromise : Promise; constructor(public ip: string, username: string, password: string, public channel: string, public console: Console) { this.digestAuth = new AxiosDigestAuth({ @@ -32,7 +35,42 @@ export class HikVisionCameraAPI { }); } + async checkDeviceModel() : Promise { + if (!this.deviceModel) { + this.deviceModel = new Promise(async (resolve, reject) => { + try { + const response = await this.digestAuth.request({ + method: "GET", + responseType: 'text', + url: `http://${this.ip}/ISAPI/System/deviceInfo`, + }); + const deviceModel = response.data.match(/>(.*?)<\/model>/)?.[1]; + resolve(deviceModel); + } catch (e) { + this.console.error('error checking NVR model', e); + resolve("unknown"); + } + }); + } + return await this.deviceModel; + } + + async checkIsOldModel() : Promise { + // The old Hikvision DS-7608NI-E2 doesn't support channel capability checks, and the requests cause errors + const model = await this.checkDeviceModel(); + return model.match(/DS-7608NI-E2/) != undefined; + } + async checkStreamSetup(): Promise { + const isOld = await this.checkIsOldModel(); + if (isOld) { + this.console.error('NVR is old version. Defaulting camera capabilities to H.264/AAC'); + return { + videoCodecType: "H.264", + audioCodecType: "AAC", + } + } + const response = await this.digestAuth.request({ method: "GET", responseType: 'text', @@ -67,30 +105,38 @@ export class HikVisionCameraAPI { } async listenEvents() { - const url = `http://${this.ip}/ISAPI/Event/notification/alertStream`; - this.console.log('listener url', url); - const response = await this.digestAuth.request({ - method: "GET", - url, - responseType: 'stream', - }); - const stream = response.data as Readable; + // support multiple cameras listening to a single single stream + if (!this.listenerPromise) { + this.listenerPromise = new Promise(async (resolve, reject) => { + const url = `http://${this.ip}/ISAPI/Event/notification/alertStream`; + this.console.log('listener url', url); - stream.on('data', (buffer: Buffer) => { - const data = buffer.toString(); - // this.console.log(data); - for (const event of Object.values(HikVisionCameraEvent)) { - if (data.indexOf(event) !== -1) { - const cameraNumber = data.match(/(.*?)(.*?)${this.channel.substr(0, 1)}`) === -1) { - continue; + const response = await this.digestAuth.request({ + method: "GET", + url, + responseType: 'stream', + }); + const stream = response.data as Readable; + + stream.on('data', (buffer: Buffer) => { + const data = buffer.toString(); + // this.console.log(data); + for (const event of Object.values(HikVisionCameraEvent)) { + if (data.indexOf(event) !== -1) { + const cameraNumber = data.match(/(.*?)(.*?)${this.channel.substr(0, 1)}`) === -1) { + continue; + } + stream.emit('event', event, cameraNumber); + } } - stream.emit('event', event, cameraNumber); - } - } - }); + }); + resolve(stream); + }); + } - return stream; + const eventSource = await this.listenerPromise; + return eventSource; } } diff --git a/plugins/hikvision/src/main.ts b/plugins/hikvision/src/main.ts index f867200f0..a6a7ec5fb 100644 --- a/plugins/hikvision/src/main.ts +++ b/plugins/hikvision/src/main.ts @@ -26,13 +26,20 @@ class HikVisionCamera extends RtspSmartCamera implements Camera { events.on('close', () => ret.emit('error', new Error('close'))); events.on('error', e => ret.emit('error', e)); events.on('event', (event: HikVisionCameraEvent, cameraNumber: string) => { - // if (this.getRtspChannel() && cameraNumber !== this.getCameraNumber()) { - // return; - // } if (event === HikVisionCameraEvent.MotionDetected || event === HikVisionCameraEvent.LineDetection || event === HikVisionCameraEvent.FieldDetection) { - this.motionDetected = true; + if (this.getRtspChannel() && cameraNumber !== this.getCameraNumber()) { + // this.console.error(`### Skipping motion event ${cameraNumber} != ${this.getCameraNumber()}`); + return; + } + + // this.console.error('### Detected motion, camera: ', cameraNumber); + const prevMotion = this.motionDetected; + if (!prevMotion) { + this.motionDetected = true; + this.console.log(`motionDetected, camera ${this.getCameraNumber()}`); + } clearTimeout(motionTimeout); motionTimeout = setTimeout(() => this.motionDetected = false, 30000); } @@ -57,7 +64,7 @@ class HikVisionCamera extends RtspSmartCamera implements Camera { this.log.a(`This camera is configured for ${streamSetup.videoCodecType} on the main channel. Configuring it it for H.264 is recommended for optimal performance.`); } if (!this.isAudioDisabled() && streamSetup.audioCodecType && streamSetup.audioCodecType !== 'AAC') { - this.log.a(`This camera is configured for ${streamSetup.audioCodecType} on the main channel. Configuring it it for AAC is recommended for optimal performance.`); + this.log.a(`This camera is configured for ${streamSetup.audioCodecType} on the main channel. Configuring it for AAC is recommended for optimal performance.`); } })(); @@ -116,8 +123,12 @@ class HikVisionCamera extends RtspSmartCamera implements Camera { if (!this.channelIds) { const client = this.createClient(); this.channelIds = new Promise(async (resolve, reject) => { - try { - + const isOld = await client.checkIsOldModel(); + if (isOld) { + this.console.error('Old NVR. Defaulting to two camera configuration'); + const camNumber = this.getCameraNumber() || '1'; + resolve([camNumber + '01', camNumber + '02']); + } else try { const response = await client.digestAuth.request({ url: `http://${this.getHttpAddress()}/ISAPI/Streaming/channels`, responseType: 'text',