hikvision: overlay support

This commit is contained in:
Koushik Dutta
2025-02-07 20:14:14 -08:00
parent a38d803b86
commit b902873d44
7 changed files with 177 additions and 6 deletions

View File

@@ -1,4 +1,4 @@
{
"scrypted.debugHost": "127.0.0.1",
"scrypted.debugHost": "scrypted-nvr",
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/hikvision",
"version": "0.0.162",
"version": "0.0.163",
"description": "Hikvision Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -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<boolean>;
getCodecs(camNumber: string): Promise<MediaStreamOptions[]>;
configureCodecs(camNumber: string, channelNumber: string, options: MediaStreamOptions): Promise<MediaStreamConfiguration>;
getOverlay(): Promise<{
json: VideoOverlayRoot;
xml: any;
}>;
getOverlayText(overlayId: string): Promise<{
json: TextOverlayRoot;
xml: any;
}>;
updateOverlayText(overlayId: string, entry: TextOverlayRoot): Promise<void>;
}

View File

@@ -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
});
}
}

View File

@@ -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;
}

View File

@@ -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<Map<string, MediaStreamOptions>>;
onvifIntercom = new OnvifIntercom(this);
activeIntercom: Awaited<ReturnType<typeof startRtpForwarderProcess>>;
@@ -43,6 +43,33 @@ export class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom
this.updateDeviceInfo();
}
async getVideoTextOverlays(): Promise<Record<string, VideoTextOverlay>> {
const client = this.getClient();
const overlays = await client.getOverlay();
const ret: Record<string, VideoTextOverlay> = {};
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<void> {
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,
];
}