From 6fd66db896b30def12633a9fda403c2baefea698 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Tue, 26 Mar 2024 10:41:32 -0700 Subject: [PATCH] amcrest/hikvision: add support for smart detections. publish. --- plugins/amcrest/package-lock.json | 39 ++-- plugins/amcrest/package.json | 8 +- plugins/amcrest/src/amcrest-api.ts | 187 +++++++++++++++--- plugins/amcrest/src/main.ts | 50 ++++- plugins/hikvision/package-lock.json | 139 ++++++------- plugins/hikvision/package.json | 13 +- plugins/hikvision/src/hikvision-camera-api.ts | 5 +- 7 files changed, 303 insertions(+), 138 deletions(-) diff --git a/plugins/amcrest/package-lock.json b/plugins/amcrest/package-lock.json index 2f06882e7a..72e55958dc 100644 --- a/plugins/amcrest/package-lock.json +++ b/plugins/amcrest/package-lock.json @@ -1,19 +1,21 @@ { "name": "@scrypted/amcrest", - "version": "0.0.135", + "version": "0.0.136", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@scrypted/amcrest", - "version": "0.0.135", + "version": "0.0.136", "license": "Apache", "dependencies": { "@scrypted/common": "file:../../common", - "@scrypted/sdk": "file:../../sdk" + "@scrypted/sdk": "file:../../sdk", + "content-type": "^1.0.5" }, "devDependencies": { - "@types/node": "^20.10.8" + "@types/content-type": "^1.1.8", + "@types/node": "^20.11.30" } }, "../../common": { @@ -23,23 +25,22 @@ "dependencies": { "@scrypted/sdk": "file:../sdk", "@scrypted/server": "file:../server", - "http-auth-utils": "^3.0.2", - "node-fetch-commonjs": "^3.1.1", + "http-auth-utils": "^5.0.1", "typescript": "^5.3.3" }, "devDependencies": { - "@types/node": "^20.10.8", + "@types/node": "^20.11.0", "ts-node": "^10.9.2" } }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.3.4", + "version": "0.3.29", "license": "ISC", "dependencies": { "@babel/preset-typescript": "^7.18.6", "adm-zip": "^0.4.13", - "axios": "^0.21.4", + "axios": "^1.6.5", "babel-loader": "^9.1.0", "babel-plugin-const-enum": "^1.1.0", "esbuild": "^0.15.9", @@ -77,15 +78,29 @@ "resolved": "../../sdk", "link": true }, + "node_modules/@types/content-type": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.8.tgz", + "integrity": "sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg==", + "dev": true + }, "node_modules/@types/node": { - "version": "20.10.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz", - "integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==", + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/plugins/amcrest/package.json b/plugins/amcrest/package.json index 0785de1370..8e9491e0e4 100644 --- a/plugins/amcrest/package.json +++ b/plugins/amcrest/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/amcrest", - "version": "0.0.135", + "version": "0.0.136", "description": "Amcrest Plugin for Scrypted", "author": "Scrypted", "license": "Apache", @@ -36,9 +36,11 @@ }, "dependencies": { "@scrypted/common": "file:../../common", - "@scrypted/sdk": "file:../../sdk" + "@scrypted/sdk": "file:../../sdk", + "content-type": "^1.0.5" }, "devDependencies": { - "@types/node": "^20.10.8" + "@types/content-type": "^1.1.8", + "@types/node": "^20.11.30" } } diff --git a/plugins/amcrest/src/amcrest-api.ts b/plugins/amcrest/src/amcrest-api.ts index d7deb075f1..d76b8d8326 100644 --- a/plugins/amcrest/src/amcrest-api.ts +++ b/plugins/amcrest/src/amcrest-api.ts @@ -1,6 +1,74 @@ import { AuthFetchCredentialState, HttpFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch'; -import { Readable } from 'stream'; +import { readLine } from '@scrypted/common/src/read-stream'; +import { parseHeaders, readBody, readMessage } from '@scrypted/common/src/rtsp-server'; +import contentType from 'content-type'; +import { IncomingMessage } from 'http'; +import { EventEmitter, Readable } from 'stream'; +import { Destroyable } from '../../rtsp/src/rtsp'; import { getDeviceInfo } from './probe'; +import { Point } from '@scrypted/sdk'; + +// { +// "Action" : "Cross", +// "Class" : "Normal", +// "CountInGroup" : 1, +// "DetectRegion" : [ +// [ 455, 260 ], +// [ 3586, 260 ], +// [ 3768, 7580 ], +// [ 382, 7451 ] +// ], +// "Direction" : "Enter", +// "EventID" : 10181, +// "GroupID" : 0, +// "Name" : "Rule1", +// "Object" : { +// "Action" : "Appear", +// "BoundingBox" : [ 2856, 1280, 3880, 4880 ], +// "Center" : [ 3368, 3080 ], +// "Confidence" : 0, +// "LowerBodyColor" : [ 0, 0, 0, 0 ], +// "MainColor" : [ 0, 0, 0, 0 ], +// "ObjectID" : 863, +// "ObjectType" : "Human", +// "RelativeID" : 0, +// "Speed" : 0 +// }, +// "PTS" : 43380319830.0, +// "RuleID" : 2, +// "Track" : [], +// "UTC" : 1711446999, +// "UTCMS" : 701 +// } +export interface AmcrestObjectDetails { + Action: string; + BoundingBox: Point; + Center: Point; + Confidence: number; + LowerBodyColor: [number, number, number, number]; + MainColor: [number, number, number, number]; + ObjectID: number; + ObjectType: string; + RelativeID: number; + Speed: number; +} + +export interface AmcrestEventData { + Action: string; + Class: string; + CountInGroup: number; + DetectRegion: Point[]; + Direction: string; + EventID: number; + GroupID: number; + Name: string; + Object: AmcrestObjectDetails; + PTS: number; + RuleID: number; + Track: any[]; + UTC: number; + UTCMS: number; +} export enum AmcrestEvent { MotionStart = "Code=VideoMotion;action=Start", @@ -18,6 +86,10 @@ export enum AmcrestEvent { DahuaTalkHangup = "Code=PassiveHungup;action=Start", DahuaCallDeny = "Code=HungupPhone;action=Pulse", DahuaTalkPulse = "Code=_CallNoAnswer_;action=Pulse", + SmartMotionHuman = "Code=SmartMotionHuman;action=Start", + SmartMotionVehicle = "Code=Vehicle;action=Start", + CrossLineDetection = "Code=CrossLineDetection;action=Start", + CrossRegionDetection = "Code=CrossRegionDetection;action=Start", } export class AmcrestCameraClient { @@ -78,7 +150,8 @@ export class AmcrestCameraClient { return response.body; } - async listenEvents() { + async listenEvents(): Promise { + const events = new EventEmitter(); const url = `http://${this.ip}/cgi-bin/eventManager.cgi?action=attach&codes=[All]`; console.log('preparing event listener', url); @@ -86,32 +159,102 @@ export class AmcrestCameraClient { url, responseType: 'readable', }); - const stream = response.body; + const stream: IncomingMessage = response.body; + (events as any).destroy = () => { + stream.destroy(); + events.removeAllListeners(); + }; + stream.on('close', () => { + events.emit('close'); + }); + stream.on('end', () => { + events.emit('end'); + }); + stream.on('error', e => { + events.emit('error', e); + }); stream.socket.setKeepAlive(true); - stream.on('data', (buffer: Buffer) => { - const data = buffer.toString(); - const parts = data.split(';'); - let index: string; - try { - for (const part of parts) { - if (part.startsWith('index')) { - index = part.split('=')[1]?.trim(); + + const ct = stream.headers['content-type']; + // make content type parsable as content disposition filename + const cd = contentType.parse(ct); + let { boundary } = cd.parameters; + boundary = `--${boundary}`; + const boundaryEnd = `${boundary}--`; + + + (async () => { + while (true) { + let ignore = await readLine(stream); + ignore = ignore.trim(); + if (!ignore) + continue; + if (ignore === boundaryEnd) + continue; + if (ignore !== boundary) { + this.console.error('expected boundary but found', ignore); + throw new Error('expected boundary'); + } + + const message = await readMessage(stream); + events.emit('data', message); + message.unshift(''); + const headers = parseHeaders(message); + const body = await readBody(stream, headers); + + const data = body.toString(); + events.emit('data', data); + + const parts = data.split(';'); + let index: string; + try { + for (const part of parts) { + if (part.startsWith('index')) { + index = part.split('=')[1]?.trim(); + } } } - } - catch (e) { - this.console.error('error parsing index', data); - } - // this.console?.log('event', data); - for (const event of Object.values(AmcrestEvent)) { - if (data.indexOf(event) !== -1) { - stream.emit('event', event, index, data); + catch (e) { + this.console.error('error parsing index', data); } - } - }); + let jsonData: any; + try { + for (const part of parts) { + if (part.startsWith('data')) { + jsonData = JSON.parse(part.split('=')[1]?.trim()); + } + } + } + catch (e) { + this.console.error('error parsing data', data); + } + + for (const event of Object.values(AmcrestEvent)) { + if (data.indexOf(event) !== -1) { + events.emit('event', event, index, data); - return stream; + if (event === AmcrestEvent.SmartMotionHuman) { + events.emit('smart', 'person', jsonData); + } + else if (event === AmcrestEvent.SmartMotionVehicle) { + events.emit('smart', 'vehicle', jsonData); + } + else if (event === AmcrestEvent.CrossLineDetection || event === AmcrestEvent.CrossRegionDetection) { + const eventData: AmcrestEventData = jsonData; + if (eventData?.Object?.ObjectType === 'Human') { + events.emit('smart', 'person', eventData); + } + else if (eventData?.Object?.ObjectType === 'Vehicle') { + events.emit('smart', 'car', eventData); + } + } + } + } + } + })() + .catch(() => stream.destroy()); + return events as any as Destroyable; } async enableContinousRecording(channel: number) { diff --git a/plugins/amcrest/src/main.ts b/plugins/amcrest/src/main.ts index 0a59a43898..d873459a55 100644 --- a/plugins/amcrest/src/main.ts +++ b/plugins/amcrest/src/main.ts @@ -1,11 +1,11 @@ import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers'; import { readLength } from "@scrypted/common/src/read-stream"; -import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, Lock, MediaObject, MediaStreamOptions, Reboot, RequestPictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk"; +import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, Lock, MediaObject, MediaStreamOptions, ObjectsDetected, Reboot, RequestPictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk"; import child_process, { ChildProcess } from 'child_process'; import { PassThrough, Readable, Stream } from "stream"; import { OnvifIntercom } from "../../onvif/src/onvif-intercom"; import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp"; -import { AmcrestCameraClient, AmcrestEvent } from "./amcrest-api"; +import { AmcrestCameraClient, AmcrestEvent, AmcrestEventData } from "./amcrest-api"; const { mediaManager } = sdk; @@ -28,6 +28,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, client: AmcrestCameraClient; videoStreamOptions: Promise; onvifIntercom = new OnvifIntercom(this); + hasSmartDetection: boolean; constructor(nativeId: string, provider: RtspProvider) { super(nativeId, provider); @@ -36,6 +37,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, this.storage.removeItem('amcrestDoorbell'); } + this.hasSmartDetection = this.storage.getItem('hasSmartDetection') === 'true'; this.updateDevice(); this.updateDeviceInfo(); } @@ -184,7 +186,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, if (idx.toString() !== channelNumber) return; } - if (event === AmcrestEvent.MotionStart) { + if (event === AmcrestEvent.MotionStart + || event === AmcrestEvent.SmartMotionHuman + || event === AmcrestEvent.SmartMotionVehicle + || event === AmcrestEvent.CrossLineDetection + || event === AmcrestEvent.CrossRegionDetection) { this.motionDetected = true; resetMotionTimeout(); } @@ -231,6 +237,26 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, } }); + events.on('smart', (className: string, data: AmcrestEventData) => { + if (!this.hasSmartDetection) { + this.hasSmartDetection = true; + this.storage.setItem('hasSmartDetection', 'true'); + this.updateDevice(); + } + + const detected: ObjectsDetected = { + timestamp: Date.now(), + detections: [ + { + score: 1, + className, + } + ], + }; + + this.onDeviceEvent(ScryptedInterface.ObjectDetector, detected); + }); + return events; } @@ -472,13 +498,19 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, if (isDoorbell || twoWayAudio) { interfaces.push(ScryptedInterface.Intercom); } + const enableDahuaLock = this.storage.getItem('enableDahuaLock') === 'true'; if (isDoorbell && doorbellType === DAHUA_DOORBELL_TYPE && enableDahuaLock) { interfaces.push(ScryptedInterface.Lock); } + const continuousRecording = this.storage.getItem('continuousRecording') === 'true'; if (continuousRecording) interfaces.push(ScryptedInterface.VideoRecorder); + + if (this.hasSmartDetection) + interfaces.push(ScryptedInterface.ObjectDetector); + this.provider.updateDevice(this.nativeId, this.name, interfaces, type); } @@ -521,7 +553,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, } const doorbellType = this.storage.getItem('doorbellType'); - + // not sure if this all works, since i don't actually have a doorbell. // good luck! const channel = this.getRtspChannel() || '1'; @@ -548,11 +580,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, } else { args.push( - "-vn", - '-acodec', 'aac', - '-f', 'adts', - 'pipe:3', - ); + "-vn", + '-acodec', 'aac', + '-f', 'adts', + 'pipe:3', + ); contentType = 'Audio/AAC'; } diff --git a/plugins/hikvision/package-lock.json b/plugins/hikvision/package-lock.json index b670cd3a27..ad01ee303e 100644 --- a/plugins/hikvision/package-lock.json +++ b/plugins/hikvision/package-lock.json @@ -1,24 +1,23 @@ { "name": "@scrypted/hikvision", - "version": "0.0.139", + "version": "0.0.140", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/hikvision", - "version": "0.0.139", + "version": "0.0.140", "license": "Apache", "dependencies": { "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", - "@types/xml2js": "^0.4.11", - "content-disposition": "^0.5.4", - "lodash": "^4.17.21", - "xml2js": "^0.6.0" + "@types/xml2js": "^0.4.14", + "content-type": "^1.0.5", + "xml2js": "^0.6.2" }, "devDependencies": { - "@types/content-disposition": "^0.5.8", - "@types/node": "^18.15.11" + "@types/content-type": "^1.1.8", + "@types/node": "^20.11.30" } }, "../../common": { @@ -84,69 +83,50 @@ "resolved": "../../sdk", "link": true }, - "node_modules/@types/content-disposition": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.8.tgz", - "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==", + "node_modules/@types/content-type": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.8.tgz", + "integrity": "sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg==", "dev": true }, "node_modules/@types/node": { - "version": "18.15.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/xml2js": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.11.tgz", - "integrity": "sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA==", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", "dependencies": { "@types/node": "*" } }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "engines": { "node": ">= 0.6" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/xml2js": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", - "integrity": "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" @@ -200,52 +180,47 @@ "webpack-bundle-analyzer": "^4.5.0" } }, - "@types/content-disposition": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.8.tgz", - "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==", + "@types/content-type": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.8.tgz", + "integrity": "sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg==", "dev": true }, "@types/node": { - "version": "18.15.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" - }, - "@types/xml2js": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.11.tgz", - "integrity": "sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA==", + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", "requires": { - "@types/node": "*" + "undici-types": "~5.26.4" } }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", "requires": { - "safe-buffer": "5.2.1" + "@types/node": "*" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "xml2js": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", - "integrity": "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "requires": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" diff --git a/plugins/hikvision/package.json b/plugins/hikvision/package.json index 07999a1e4b..4f4d285899 100644 --- a/plugins/hikvision/package.json +++ b/plugins/hikvision/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/hikvision", - "version": "0.0.139", + "version": "0.0.140", "description": "Hikvision Plugin for Scrypted", "author": "Scrypted", "license": "Apache", @@ -37,13 +37,12 @@ "dependencies": { "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", - "@types/xml2js": "^0.4.11", - "content-disposition": "^0.5.4", - "lodash": "^4.17.21", - "xml2js": "^0.6.0" + "@types/xml2js": "^0.4.14", + "content-type": "^1.0.5", + "xml2js": "^0.6.2" }, "devDependencies": { - "@types/content-disposition": "^0.5.8", - "@types/node": "^18.15.11" + "@types/content-type": "^1.1.8", + "@types/node": "^20.11.30" } } diff --git a/plugins/hikvision/src/hikvision-camera-api.ts b/plugins/hikvision/src/hikvision-camera-api.ts index 1a556632b2..5caf84e307 100644 --- a/plugins/hikvision/src/hikvision-camera-api.ts +++ b/plugins/hikvision/src/hikvision-camera-api.ts @@ -1,7 +1,7 @@ import { AuthFetchCredentialState, HttpFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch'; import { readLine } from '@scrypted/common/src/read-stream'; import { parseHeaders, readBody, readMessage } from '@scrypted/common/src/rtsp-server'; -import contentDisposition from 'content-disposition'; +import contentType from 'content-type'; import { IncomingMessage } from 'http'; import { EventEmitter, Readable } from 'stream'; import { Destroyable } from '../../rtsp/src/rtsp'; @@ -173,7 +173,7 @@ export class HikvisionCameraAPI { const ct = stream.headers['content-type']; // make content type parsable as content disposition filename - const cd = contentDisposition.parse(ct.replace('/', '')); + const cd = contentType.parse(ct); let { boundary } = cd.parameters; boundary = `--${boundary}`; const boundaryEnd = `${boundary}--`; @@ -200,7 +200,6 @@ export class HikvisionCameraAPI { try { if (!headers['content-type'].includes('application/xml') && lastSmartDetection) { - const cd = contentDisposition.parse(headers['content-disposition'] || 'empty'); if (!headers['content-type']?.startsWith('image/jpeg')) { continue; }