Support for old Hikvision NVR by suppressing unsupported requests and limiting number of alert streams

This commit is contained in:
Craig Federighi
2022-01-01 19:38:16 -08:00
parent 654aff710e
commit 1cb9dc34e1
2 changed files with 86 additions and 29 deletions

View File

@@ -1,5 +1,6 @@
import { Readable } from 'stream';
import AxiosDigestAuth from '@koush/axios-digest-auth';
import { EventEmitter } from "stream";
export enum HikVisionCameraEvent {
MotionDetected = "<eventType>VMD</eventType>",
@@ -24,6 +25,8 @@ export interface HikVisionCameraStreamSetup {
export class HikVisionCameraAPI {
digestAuth: AxiosDigestAuth;
deviceModel : Promise<string>;
listenerPromise : Promise<EventEmitter>;
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<string> {
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<boolean> {
// 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<HikVisionCameraStreamSetup> {
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(/<channelID>(.*?)</)?.[1] || data.match(/<dynChannelID>(.*?)</)?.[1];
if (this.channel
&& data.indexOf(`<channelID>${this.channel.substr(0, 1)}</channelID>`) === -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(/<channelID>(.*?)</)?.[1] || data.match(/<dynChannelID>(.*?)</)?.[1];
if (this.channel
&& data.indexOf(`<channelID>${this.channel.substr(0, 1)}</channelID>`) === -1) {
continue;
}
stream.emit('event', event, cameraNumber);
}
}
stream.emit('event', event, cameraNumber);
}
}
});
});
resolve(stream);
});
}
return stream;
const eventSource = await this.listenerPromise;
return eventSource;
}
}

View File

@@ -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',