From b902873d449cfbee497c1ef12910e8deb498be0d Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Fri, 7 Feb 2025 20:14:14 -0800 Subject: [PATCH] hikvision: overlay support --- plugins/hikvision/.vscode/settings.json | 2 +- plugins/hikvision/package-lock.json | 4 +- plugins/hikvision/package.json | 2 +- .../hikvision/src/hikvision-api-channels.ts | 11 +++ plugins/hikvision/src/hikvision-camera-api.ts | 44 ++++++++++ plugins/hikvision/src/hikvision-overlay.ts | 88 +++++++++++++++++++ plugins/hikvision/src/main.ts | 32 ++++++- 7 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 plugins/hikvision/src/hikvision-overlay.ts diff --git a/plugins/hikvision/.vscode/settings.json b/plugins/hikvision/.vscode/settings.json index 77ccdbd6d..a620593fa 100644 --- a/plugins/hikvision/.vscode/settings.json +++ b/plugins/hikvision/.vscode/settings.json @@ -1,4 +1,4 @@ { - "scrypted.debugHost": "127.0.0.1", + "scrypted.debugHost": "scrypted-nvr", } \ No newline at end of file diff --git a/plugins/hikvision/package-lock.json b/plugins/hikvision/package-lock.json index a82431219..138ac3455 100644 --- a/plugins/hikvision/package-lock.json +++ b/plugins/hikvision/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/hikvision", - "version": "0.0.162", + "version": "0.0.163", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/hikvision", - "version": "0.0.162", + "version": "0.0.163", "license": "Apache", "dependencies": { "@scrypted/common": "file:../../common", diff --git a/plugins/hikvision/package.json b/plugins/hikvision/package.json index 5e9ee968b..ce63cbf75 100644 --- a/plugins/hikvision/package.json +++ b/plugins/hikvision/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/hikvision", - "version": "0.0.162", + "version": "0.0.163", "description": "Hikvision Plugin for Scrypted", "author": "Scrypted", "license": "Apache", diff --git a/plugins/hikvision/src/hikvision-api-channels.ts b/plugins/hikvision/src/hikvision-api-channels.ts index 1371d1d64..2a066a78f 100644 --- a/plugins/hikvision/src/hikvision-api-channels.ts +++ b/plugins/hikvision/src/hikvision-api-channels.ts @@ -2,6 +2,7 @@ import { HttpFetchOptions } from '@scrypted/common/src/http-auth-fetch'; import { MediaStreamConfiguration, MediaStreamOptions } from '@scrypted/sdk'; import { Readable } from 'stream'; import { Destroyable } from '../../rtsp/src/rtsp'; +import { TextOverlayRoot, VideoOverlayRoot } from './hikvision-overlay'; export interface HikvisionCameraStreamSetup { videoCodecType: string; @@ -22,4 +23,14 @@ export interface HikvisionAPI { putVcaResource(channel: string, resource: 'smart' | 'facesnap' | 'close'): Promise; getCodecs(camNumber: string): Promise; configureCodecs(camNumber: string, channelNumber: string, options: MediaStreamOptions): Promise; + + getOverlay(): Promise<{ + json: VideoOverlayRoot; + xml: any; + }>; + getOverlayText(overlayId: string): Promise<{ + json: TextOverlayRoot; + xml: any; + }>; + updateOverlayText(overlayId: string, entry: TextOverlayRoot): Promise; } diff --git a/plugins/hikvision/src/hikvision-camera-api.ts b/plugins/hikvision/src/hikvision-camera-api.ts index 5ed1c9324..2e4afaa5f 100644 --- a/plugins/hikvision/src/hikvision-camera-api.ts +++ b/plugins/hikvision/src/hikvision-camera-api.ts @@ -11,6 +11,7 @@ import { CapabiltiesResponse } from './hikvision-api-capabilities'; import { HikvisionAPI, HikvisionCameraStreamSetup } from "./hikvision-api-channels"; import { ChannelResponse, ChannelsResponse } from './hikvision-xml-types'; import { getDeviceInfo } from './probe'; +import { TextOverlayRoot, VideoOverlayRoot } from './hikvision-overlay'; export const detectionMap = { human: 'person', @@ -482,4 +483,47 @@ export class HikvisionCameraAPI implements HikvisionAPI { return [...defaultMap.values()]; } } + + async getOverlay() { + const response = await this.request({ + method: 'GET', + url: `http://${this.ip}/ISAPI/System/Video/inputs/channels/1/overlays`, + responseType: 'text', + headers: { + 'Content-Type': 'application/xml', + }, + }); + const json = await xml2js.parseStringPromise(response.body) as VideoOverlayRoot; + + return { json, xml: response.body }; + } + + async getOverlayText(overlayId: string) { + const response = await this.request({ + method: 'GET', + url: `http://${this.ip}//ISAPI/System/Video/inputs/channels/1/overlays/text/${overlayId}`, + responseType: 'text', + headers: { + 'Content-Type': 'application/xml', + }, + }); + const json = await xml2js.parseStringPromise(response.body) as TextOverlayRoot; + + return { json, xml: response.body }; + } + + async updateOverlayText(overlayId: string, entry: TextOverlayRoot) { + const builder = new xml2js.Builder(); + const xml = builder.buildObject(entry); + + await this.request({ + method: 'PUT', + url: `http://${this.ip}//ISAPI/System/Video/inputs/channels/1/overlays/text/${overlayId}`, + responseType: 'text', + headers: { + 'Content-Type': 'application/xml', + }, + body: xml + }); + } } diff --git a/plugins/hikvision/src/hikvision-overlay.ts b/plugins/hikvision/src/hikvision-overlay.ts new file mode 100644 index 000000000..fb0037c94 --- /dev/null +++ b/plugins/hikvision/src/hikvision-overlay.ts @@ -0,0 +1,88 @@ +export interface VideoOverlayRoot { + VideoOverlay: VideoOverlay; +} + +export interface VideoOverlay { + $: VideoOverlayClass; + normalizedScreenSize: NormalizedScreenSize[]; + attribute: Attribute[]; + fontSize: string[]; + TextOverlayList: TextOverlayListElement[]; + DateTimeOverlay: DateTimeOverlay[]; + channelNameOverlay: ChannelNameOverlay[]; + frontColorMode: string[]; + frontColor: string[]; + alignment: string[]; + boundary: string[]; + upDownboundary: string[]; + leftRightboundary: string[]; +} + +export interface VideoOverlayClass { + version: string; + xmlns: string; +} + +export interface DateTimeOverlay { + enabled: string[]; + positionX: string[]; + positionY: string[]; + dateStyle: string[]; + timeStyle: string[]; + displayWeek: string[]; +} + +export interface TextOverlayListElement { + $: TextOverlayList; + TextOverlay: TextOverlay[]; +} + +export interface TextOverlayList { + size: string; +} + +export interface TextOverlay { + id: string[]; + enabled: string[]; + positionX: string[]; + positionY: string[]; + displayText: string[]; + isPersistentText: string[]; +} + +export interface Attribute { + transparent: string[]; + flashing: string[]; +} + +export interface ChannelNameOverlay { + $: VideoOverlayClass; + enabled: string[]; + positionX: string[]; + positionY: string[]; +} + +export interface NormalizedScreenSize { + normalizedScreenWidth: string[]; + normalizedScreenHeight: string[]; +} + + +export interface TextOverlayRoot { + TextOverlay: TextOverlay; +} + +export interface TextOverlay { + $: Empty; + id: string[]; + enabled: string[]; + positionX: string[]; + positionY: string[]; + displayText: string[]; + directAngle: string[]; +} + +export interface Empty { + version: string; + xmlns: string; +} diff --git a/plugins/hikvision/src/main.ts b/plugins/hikvision/src/main.ts index 5573f4ea9..7a7f5840d 100644 --- a/plugins/hikvision/src/main.ts +++ b/plugins/hikvision/src/main.ts @@ -1,5 +1,5 @@ import { automaticallyConfigureSettings, checkPluginNeedsAutoConfigure } from "@scrypted/common/src/autoconfigure-codecs"; -import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, Reboot, RequestPictureOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, VideoCameraConfiguration } from "@scrypted/sdk"; +import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, Reboot, RequestPictureOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, VideoCameraConfiguration, VideoTextOverlay, VideoTextOverlays } from "@scrypted/sdk"; import crypto from 'crypto'; import { PassThrough } from "stream"; import xml2js from 'xml2js'; @@ -27,7 +27,7 @@ function channelToCameraNumber(channel: string) { return channel.substring(0, channel.length - 2); } -export class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom, Reboot, ObjectDetector, VideoCameraConfiguration { +export class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom, Reboot, ObjectDetector, VideoCameraConfiguration, VideoTextOverlays { detectedChannels: Promise>; onvifIntercom = new OnvifIntercom(this); activeIntercom: Awaited>; @@ -43,6 +43,33 @@ export class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom this.updateDeviceInfo(); } + async getVideoTextOverlays(): Promise> { + const client = this.getClient(); + const overlays = await client.getOverlay(); + const ret: Record = {}; + + for (const overlay of overlays.json.VideoOverlay.TextOverlayList) { + const to = overlay.TextOverlay[0]; + ret[to.id[0]] = { + text: to.displayText[0], + } + } + return ret; + } + + async setVideoTextOverlay(id: string, value: VideoTextOverlay): Promise { + const client = this.getClient(); + const overlays = await client.getOverlay(); + // find the overlay by id + const overlay = overlays.json.VideoOverlay.TextOverlayList.find(o => o.TextOverlay[0].id[0] === id); + overlay.TextOverlay[0].enabled[0] = value.text ? 'true' : 'false'; + if (typeof value.text === 'string') + overlay.TextOverlay[0].displayText = [value.text]; + client.updateOverlayText(id, { + TextOverlay: overlay.TextOverlay[0], + }); + } + async reboot() { const client = this.getClient(); await client.reboot(); @@ -626,6 +653,7 @@ class HikvisionProvider extends RtspProvider { ScryptedInterface.Reboot, ScryptedInterface.Camera, ScryptedInterface.MotionSensor, + ScryptedInterface.VideoTextOverlays, ]; }