From dbaec7fc815c1935b582b81589f8e910da2c6a7a Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 6 Dec 2021 22:11:33 -0800 Subject: [PATCH] onvif: smart detections --- plugins/onvif/package-lock.json | 4 +- plugins/onvif/package.json | 2 +- plugins/onvif/src/main.ts | 81 ++++++++++++++++++++++++++------- plugins/onvif/src/onvif-api.ts | 42 ++++++++++++++++- 4 files changed, 108 insertions(+), 21 deletions(-) diff --git a/plugins/onvif/package-lock.json b/plugins/onvif/package-lock.json index f2a2e2df5..dab601f03 100644 --- a/plugins/onvif/package-lock.json +++ b/plugins/onvif/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/onvif", - "version": "0.0.60", + "version": "0.0.61", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/onvif", - "version": "0.0.60", + "version": "0.0.61", "license": "Apache", "dependencies": { "@koush/axios-digest-auth": "^0.8.5", diff --git a/plugins/onvif/package.json b/plugins/onvif/package.json index b08f2b260..ef446aa9d 100644 --- a/plugins/onvif/package.json +++ b/plugins/onvif/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/onvif", - "version": "0.0.60", + "version": "0.0.61", "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 d120f1f4f..73ffa4ff1 100644 --- a/plugins/onvif/src/main.ts +++ b/plugins/onvif/src/main.ts @@ -1,6 +1,6 @@ -import sdk, { MediaObject, ScryptedInterface, Setting, ScryptedDeviceType, PictureOptions, VideoCamera, DeviceDiscovery } from "@scrypted/sdk"; +import sdk, { MediaObject, ScryptedInterface, Setting, ScryptedDeviceType, PictureOptions, VideoCamera, DeviceDiscovery, ObjectDetection, ObjectDetector, ObjectDetectionTypes, ObjectsDetected } from "@scrypted/sdk"; import { EventEmitter, Stream } from "stream"; -import { RtspSmartCamera, RtspProvider, Destroyable, RtspMediaStreamOptions } from "../../rtsp/src/rtsp"; +import { RtspSmartCamera, RtspProvider, Destroyable, UrlMediaStreamOptions } from "../../rtsp/src/rtsp"; import { connectCameraAPI, OnvifCameraAPI, OnvifEvent } from "./onvif-api"; import xml2js from 'xml2js'; import onvif from 'onvif'; @@ -25,10 +25,24 @@ function convertAudioCodec(codec: string) { return codec?.toLowerCase(); } -class OnvifCamera extends RtspSmartCamera { +class OnvifCamera extends RtspSmartCamera implements ObjectDetector { eventStream: Stream; client: OnvifCameraAPI; - rtspMediaStreamOptions: Promise; + rtspMediaStreamOptions: Promise; + + getDetectionInput(detectionId: any, eventId?: any): Promise { + throw new Error("Method not implemented."); + } + + async getObjectTypes(): Promise { + const client = await this.getClient(); + const classes = await client.getEventTypes(); + const faces = classes.includes('face'); + return { + classes, + faces, + } + } async getPictureOptions(): Promise { try { @@ -76,13 +90,13 @@ class OnvifCamera extends RtspSmartCamera { return mediaManager.createMediaObject(snapshot, 'image/jpeg'); } - async getConstructedVideoStreamOptions(): Promise { + async getConstructedVideoStreamOptions(): Promise { if (!this.rtspMediaStreamOptions) { this.rtspMediaStreamOptions = new Promise(async (resolve) => { try { const client = await this.getClient(); const profiles: any[] = await client.getProfiles(); - const ret: RtspMediaStreamOptions[] = []; + const ret: UrlMediaStreamOptions[] = []; for (const { $, name, videoEncoderConfiguration, audioEncoderConfiguration } of profiles) { try { ret.push({ @@ -144,9 +158,21 @@ class OnvifCamera extends RtspSmartCamera { ret.emit('error', e); return; } + + try { + const eventTypes = await client.getEventTypes(); + if (!eventTypes) + return; + if (this.storage.getItem('onvifDetector') !== 'true') { + this.storage.setItem('onvifDetector', 'true'); + this.updateDevice(); + } + } + catch (e) { + } this.console.log('listening events'); const events = client.listenEvents(); - events.on('event', event => { + events.on('event', (event, className) => { if (event === OnvifEvent.MotionBuggy) { this.motionDetected = true; clearTimeout(motionTimeout); @@ -166,6 +192,19 @@ class OnvifCamera extends RtspSmartCamera { this.binaryState = true; else if (event === OnvifEvent.BinaryStop) this.binaryState = false; + else if (event === OnvifEvent.Detection) { + const d: ObjectsDetected = { + timestamp: Date.now(), + faces: className === 'face' ? [] : undefined, + detections: [ + { + score: undefined, + className, + } + ] + } + this.onDeviceEvent(ScryptedInterface.ObjectDetector, d); + } }) })(); ret.destroy = () => { @@ -219,6 +258,17 @@ class OnvifCamera extends RtspSmartCamera { ] } + updateDevice() { + const interfaces: string[] = [...this.provider.getInterfaces()]; + if (this.storage.getItem('onvifDetector') === 'true') + interfaces.push(ScryptedInterface.ObjectDetector); + const doorbell = this.storage.getItem('onvifDoorbell'); + if (doorbell === 'true') + this.provider.updateDevice(this.nativeId, this.name, [...interfaces, ScryptedInterface.BinarySensor], ScryptedDeviceType.Doorbell) + else + this.provider.updateDevice(this.nativeId, this.name, interfaces); + } + async putSetting(key: string, value: string) { this.client = undefined; this.rtspMediaStreamOptions = undefined; @@ -227,10 +277,7 @@ class OnvifCamera extends RtspSmartCamera { return super.putSetting(key, value); this.storage.setItem(key, value); - if (value === 'true') - this.provider.updateDevice(this.nativeId, this.name, [...this.provider.getInterfaces(), ScryptedInterface.BinarySensor], ScryptedDeviceType.Doorbell) - else - this.provider.updateDevice(this.nativeId, this.name, this.provider.getInterfaces()) + this.updateDevice(); } } @@ -242,11 +289,11 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery { onvif.Discovery.on('device', (cam: any, rinfo: any, xml: any) => { // Function will be called as soon as the NVT responses - + // Parsing of Discovery responses taken from my ONVIF-Audit project, part of the 2018 ONVIF Open Source Challenge // Filter out xml name spaces xml = xml.replace(/xmlns([^=]*?)=(".*?")/g, ''); - + let parser = new xml2js.Parser({ attrkey: 'attr', charkey: 'payload', // this ensures the payload is called .payload regardless of whether the XML Tags have Attributes or not @@ -263,12 +310,12 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery { let xaddrs = result['Envelope']['Body'][0]['ProbeMatches'][0]['ProbeMatch'][0]['XAddrs'][0].payload; let scopes = result['Envelope']['Body'][0]['ProbeMatches'][0]['ProbeMatch'][0]['Scopes'][0].payload; scopes = scopes.split(" "); - + let hardware = ""; let name = ""; for (let i = 0; i < scopes.length; i++) { - if (scopes[i].includes('onvif://www.onvif.org/name')) {name = decodeURI(scopes[i].substring(27));} - if (scopes[i].includes('onvif://www.onvif.org/hardware')) {hardware = decodeURI(scopes[i].substring(31));} + if (scopes[i].includes('onvif://www.onvif.org/name')) { name = decodeURI(scopes[i].substring(27)); } + if (scopes[i].includes('onvif://www.onvif.org/hardware')) { hardware = decodeURI(scopes[i].substring(31)); } } let msg = 'Discovery Reply from ' + rinfo.address + ' (' + name + ') (' + hardware + ') (' + xaddrs + ') (' + urn + ')'; this.console.log(msg); @@ -318,7 +365,7 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery { this.storage.setItem('autodiscovery', 'true'); } - else if(ad === 'false') { + else if (ad === 'false') { // auto discovery is disabled, but maybe we can reenable it. if (!cameraCount) { this.console.log('autodiscovery reenabled, no cameras found'); diff --git a/plugins/onvif/src/onvif-api.ts b/plugins/onvif/src/onvif-api.ts index 50cb4440f..38aa47e53 100644 --- a/plugins/onvif/src/onvif-api.ts +++ b/plugins/onvif/src/onvif-api.ts @@ -20,6 +20,7 @@ export enum OnvifEvent { BinaryStart, BinaryStop, CellMotion, + Detection, } function stripNamespaces(topic: string) { @@ -55,6 +56,7 @@ export class OnvifCameraAPI { profiles: Promise; binaryStateEvent: string; digestAuth: AxiosDigestAuth; + detections = new Map(); constructor(public cam: any, username: string, password: string, public console: Console, binaryStateEvent: string, public debug?: boolean) { this.binaryStateEvent = binaryStateEvent @@ -76,7 +78,7 @@ export class OnvifCameraAPI { } if (event.message.message.data && event.message.message.data.simpleItem) { - const dataValue = event.message.message.data.simpleItem.$.Value + const dataValue = event.message.message.data.simpleItem.$.Value; if (eventTopic.includes('MotionAlarm')) { // ret.emit('event', OnvifEvent.MotionBuggy); if (dataValue) @@ -101,6 +103,18 @@ export class OnvifCameraAPI { ret.emit('event', OnvifEvent.MotionBuggy); } } + else if (eventTopic.includes('RuleEngine/ObjectDetector')) { + if (dataValue) { + try { + const eventName = event.message.message.data.simpleItem.$.Name; + const className = this.detections.get(eventName); + ret.emit('event', OnvifEvent.Detection, className); + } + catch (e) { + this.console.warn('error parsing detection', e); + } + } + } } }); return ret; @@ -151,6 +165,32 @@ export class OnvifCameraAPI { }) } + async getEventTypes(): Promise { + return new Promise((resolve, reject) => { + this.cam.getEventProperties((err, data, xml) => { + if (err) { + this.console.log('getEventTypes error', err); + return reject(err); + } + + this.console.log(xml); + for (const [className, entry] of Object.entries(data.topicSet.ruleEngine.objectDetector) as any) { + try { + const eventName = entry.messageDescription.data.simpleItemDescription.$.Name; + this.detections.set(eventName, className); + } + catch (e) { + } + } + + if (this.detections.size === 0) + this.detections = undefined; + + resolve([...this.detections.values()]); + }); + }) + } + async getStreamUrl(profileToken?: string): Promise { if (!profileToken) profileToken = await this.getMainProfileToken();