diff --git a/plugins/hikvision/src/hikvision-camera-api.ts b/plugins/hikvision/src/hikvision-camera-api.ts
index 7d6137505..89cb4cdbe 100644
--- a/plugins/hikvision/src/hikvision-camera-api.ts
+++ b/plugins/hikvision/src/hikvision-camera-api.ts
@@ -7,6 +7,7 @@ import { IncomingMessage } from 'http';
import { EventEmitter, Readable } from 'stream';
import { Destroyable } from '../../rtsp/src/rtsp';
import { getDeviceInfo } from './probe';
+import { sleep } from "@scrypted/common/src/sleep";
export const detectionMap = {
human: 'person',
@@ -53,7 +54,7 @@ export class HikvisionCameraAPI implements HikvisionAPI {
},
rejectUnauthorized: false,
credential: this.credential,
- body,
+ body: typeof urlOrOptions !== 'string' && !(urlOrOptions instanceof URL) ? urlOrOptions?.body : body,
});
return response;
}
@@ -135,6 +136,42 @@ export class HikvisionCameraAPI implements HikvisionAPI {
return response.body;
}
+ async getVcaResource(channel: string) {
+ const response = await this.request({
+ url: `http://${this.ip}/ISAPI/System/Video/inputs/channels/${getChannel(channel)}/VCAResource`,
+ responseType: 'text',
+ });
+
+ return response.body as string;
+ }
+
+ async putVcaResource(channel: string, resource: 'smart' | 'facesnap' | 'close') {
+ const current = await this.getVcaResource(channel);
+ // no op
+ if (current.includes(resource))
+ return true;
+
+ const xml = '\r\n' +
+ '\r\n' +
+ `${resource}\r\n` +
+ '\r\n';
+
+ const response = await this.request({
+ body: xml,
+ method: 'PUT',
+ url: `http://${this.ip}/ISAPI/System/Video/inputs/channels/${getChannel(channel)}/VCAResource`,
+ responseType: 'text',
+ headers: {
+ 'Content-Type': 'application/xml',
+ },
+ });
+
+ // need to reboot after this change.
+ await this.reboot();
+ // return false to indicate that the change will take effect after the reboot.
+ return false;
+ }
+
async listenEvents(): Promise {
const events = new EventEmitter();
(events as any).destroy = () => { };
diff --git a/plugins/hikvision/tsconfig.json b/plugins/hikvision/tsconfig.json
index 34a847ad8..ba9b4d395 100644
--- a/plugins/hikvision/tsconfig.json
+++ b/plugins/hikvision/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "module": "commonjs",
+ "module": "Node16",
"target": "ES2021",
"resolveJsonModule": true,
"moduleResolution": "Node16",
diff --git a/plugins/onvif/src/main.ts b/plugins/onvif/src/main.ts
index c44201179..1d19d857f 100644
--- a/plugins/onvif/src/main.ts
+++ b/plugins/onvif/src/main.ts
@@ -1,36 +1,17 @@
-import sdk, { AdoptDevice, Device, DeviceCreatorSettings, DeviceDiscovery, DeviceInformation, DiscoveredDevice, Intercom, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PanTiltZoom, PanTiltZoomCommand, PictureOptions, Reboot, RequestPictureOptions, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, VideoCameraConfiguration } from "@scrypted/sdk";
+import sdk, { AdoptDevice, Device, DeviceCreatorSettings, DeviceDiscovery, DeviceInformation, DiscoveredDevice, Intercom, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, PictureOptions, Reboot, RequestPictureOptions, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, VideoCamera, VideoCameraConfiguration } from "@scrypted/sdk";
import { AddressInfo } from "net";
import onvif from 'onvif';
import { Stream } from "stream";
import xml2js from 'xml2js';
-import { Destroyable, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
-import { connectCameraAPI, OnvifCameraAPI, OnvifEvent } from "./onvif-api";
+import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
+import { connectCameraAPI, OnvifCameraAPI } from "./onvif-api";
+import { computeBitrate, computeInterval, configureCodecs, convertAudioCodec, getCodecs } from "./onvif-configure";
+import { listenEvents } from "./onvif-events";
import { OnvifIntercom } from "./onvif-intercom";
import { OnvifPTZMixinProvider } from "./onvif-ptz";
-import { listenEvents } from "./onvif-events";
const { mediaManager, systemManager, deviceManager } = sdk;
-function computeInterval(fps: number, govLength: number) {
- if (!fps || !govLength)
- return;
- return govLength / fps * 1000;
-}
-
-function computeBitrate(bitrate: number) {
- if (!bitrate)
- return;
- return bitrate * 1000;
-}
-
-function convertAudioCodec(codec: string) {
- if (codec?.toLowerCase()?.includes('mp4a'))
- return 'aac';
- if (codec?.toLowerCase()?.includes('aac'))
- return 'aac';
- return codec?.toLowerCase();
-}
-
class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, VideoCameraConfiguration, Reboot {
eventStream: Stream;
client: OnvifCameraAPI;
@@ -51,55 +32,8 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
async setVideoStreamOptions(options: MediaStreamOptions): Promise {
const client = await this.getClient();
- const profiles: any[] = await client.getProfiles();
- const profile = profiles.find(profile => profile.$.token === options.id);
- const configuration = profile.videoEncoderConfiguration;
-
- const videoOptions = options.video;
-
- switch (videoOptions.codec) {
- case 'h264':
- configuration.encoding = 'H264';
-
- if (videoOptions.idrIntervalMillis && videoOptions.fps) {
- configuration.H264 ||= {};
- configuration.H264.govLength = Math.floor(videoOptions.fps * videoOptions.idrIntervalMillis / 1000);
- }
- if (videoOptions.keyframeInterval) {
- configuration.H264 ||= {};
- configuration.H264.govLength = videoOptions.keyframeInterval;
- }
- if (videoOptions.profile) {
- configuration.H264 ||= {};
- configuration.H264.profile = videoOptions.profile;
- }
- break;
- }
-
- if (videoOptions.width && videoOptions.height) {
- configuration.resolution ||= {};
- configuration.resolution.width = videoOptions.width;
- configuration.resolution.height = videoOptions.height;
- }
-
- if (videoOptions?.bitrate) {
- configuration.rateControl ||= {};
- configuration.rateControl.bitrateLimit = Math.floor(videoOptions.bitrate / 1000);
- }
- if (videoOptions.fps) {
- configuration.rateControl ||= {};
- configuration.rateControl.frameRateLimit = videoOptions.fps;
- configuration.rateControl.encodingInterval = 1;
- }
-
- return new Promise((r, f) => {
- client.cam.setVideoEncoderConfiguration(configuration, (e: Error, result: any) => {
- if (e)
- return f(e);
-
- r();
- })
- });
+ await configureCodecs(client, options);
+ this.rtspMediaStreamOptions = undefined;
}
async updateDeviceInfo() {
@@ -187,55 +121,31 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
}
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: UrlMediaStreamOptions[] = [];
- for (const { $, name, videoEncoderConfiguration, audioEncoderConfiguration } of profiles) {
- try {
- ret.push({
- id: $.token,
- metadata: {
- videoId: videoEncoderConfiguration?.$?.token,
- audioId: audioEncoderConfiguration?.$?.token,
- },
- name: name,
- container: 'rtsp',
- url: await client.getStreamUrl($.token),
- video: {
- fps: videoEncoderConfiguration?.rateControl?.frameRateLimit,
- bitrate: computeBitrate(videoEncoderConfiguration?.rateControl?.bitrateLimit),
- width: videoEncoderConfiguration?.resolution?.width,
- height: videoEncoderConfiguration?.resolution?.height,
- codec: videoEncoderConfiguration?.encoding?.toLowerCase(),
- idrIntervalMillis: computeInterval(videoEncoderConfiguration?.rateControl?.frameRateLimit,
- videoEncoderConfiguration?.$.GovLength),
- },
- audio: this.isAudioDisabled() ? null : {
- bitrate: computeBitrate(audioEncoderConfiguration?.bitrate),
- codec: convertAudioCodec(audioEncoderConfiguration?.encoding),
- }
- })
- }
- catch (e) {
- this.console.error('error retrieving onvif profile', $.token, e);
- }
+ if (this.rtspMediaStreamOptions)
+ return this.rtspMediaStreamOptions;
+
+ this.rtspMediaStreamOptions = new Promise(async (resolve) => {
+ try {
+ const client = await this.getClient();
+ const ret = await getCodecs(this.console, client);
+
+ if (!ret.length)
+ throw new Error('onvif camera had no profiles.');
+
+ if (this.isAudioDisabled()) {
+ for (const r of ret) {
+ r.audio = null;
}
-
- if (!ret.length)
- throw new Error('onvif camera had no profiles.');
-
- resolve(ret);
}
- catch (e) {
- this.rtspMediaStreamOptions = undefined;
- this.console.error('error retrieving onvif profiles', e);
- resolve(undefined);
- }
- })
- }
+
+ resolve(ret);
+ }
+ catch (e) {
+ this.rtspMediaStreamOptions = undefined;
+ this.console.error('error retrieving onvif profiles', e);
+ resolve(undefined);
+ }
+ });
return this.rtspMediaStreamOptions;
}
diff --git a/plugins/onvif/src/onvif-configure.ts b/plugins/onvif/src/onvif-configure.ts
new file mode 100644
index 000000000..81e370a6c
--- /dev/null
+++ b/plugins/onvif/src/onvif-configure.ts
@@ -0,0 +1,133 @@
+import { MediaStreamOptions } from "@scrypted/sdk";
+import { OnvifCameraAPI } from "./onvif-api";
+import { UrlMediaStreamOptions } from "../../ffmpeg-camera/src/common";
+
+export function computeInterval(fps: number, govLength: number) {
+ if (!fps || !govLength)
+ return;
+ return govLength / fps * 1000;
+}
+
+export function convertAudioCodec(codec: string) {
+ if (codec?.toLowerCase()?.includes('mp4a'))
+ return 'aac';
+ if (codec?.toLowerCase()?.includes('aac'))
+ return 'aac';
+ return codec?.toLowerCase();
+}
+
+export function computeBitrate(bitrate: number) {
+ if (!bitrate)
+ return;
+ return bitrate * 1000;
+}
+
+export async function configureCodecs(client: OnvifCameraAPI, options: MediaStreamOptions) {
+ const profiles: any[] = await client.getProfiles();
+ const profile = profiles.find(profile => profile.$.token === options.id);
+ const configuration = profile.videoEncoderConfiguration;
+
+ const videoOptions = options.video;
+
+ switch (videoOptions?.codec) {
+ case 'h264':
+ configuration.encoding = 'H264';
+
+ if (videoOptions?.idrIntervalMillis && videoOptions?.fps) {
+ configuration.H264 ||= {};
+ configuration.H264.govLength = Math.floor(videoOptions?.fps * videoOptions?.idrIntervalMillis / 1000);
+ }
+ if (videoOptions?.keyframeInterval) {
+ configuration.H264 ||= {};
+ configuration.H264.govLength = videoOptions?.keyframeInterval;
+ }
+ if (videoOptions?.profile) {
+ let profile: string;
+ switch (videoOptions.profile) {
+ case 'baseline':
+ profile = 'Baseline';
+ break;
+ case 'main':
+ profile = 'Main';
+ break;
+ case 'high':
+ profile = 'High';
+ break;
+ }
+ if (profile) {
+ configuration.H264 ||= {};
+ configuration.H264.profile = profile;
+ }
+ }
+ break;
+ }
+
+ if (videoOptions?.width && videoOptions?.height) {
+ configuration.resolution ||= {};
+ configuration.resolution.width = videoOptions?.width;
+ configuration.resolution.height = videoOptions?.height;
+ }
+
+ if (videoOptions?.bitrate) {
+ configuration.rateControl ||= {};
+ configuration.rateControl.bitrateLimit = Math.floor(videoOptions?.bitrate / 1000);
+ }
+
+ if (videoOptions?.bitrateControl) {
+ configuration.rateControl ||= {};
+ configuration.rateControl.$ ||= {};
+ configuration.rateControl.$.ConstantBitrate = videoOptions?.bitrateControl === 'constant';
+ }
+
+ if (videoOptions?.fps) {
+ configuration.rateControl ||= {};
+ configuration.rateControl.frameRateLimit = videoOptions?.fps;
+ configuration.rateControl.encodingInterval = 1;
+ }
+
+ return new Promise((r, f) => {
+ client.cam.setVideoEncoderConfiguration(configuration, (e: Error, result: any) => {
+ if (e)
+ return f(e);
+
+ r();
+ })
+ });
+}
+
+export async function getCodecs(console: Console, client: OnvifCameraAPI) {
+ const profiles: any[] = await client.getProfiles();
+ const ret: UrlMediaStreamOptions[] = [];
+ for (const { $, name, videoEncoderConfiguration, audioEncoderConfiguration } of profiles) {
+ try {
+ ret.push({
+ id: $.token,
+ metadata: {
+ videoId: videoEncoderConfiguration?.$?.token,
+ audioId: audioEncoderConfiguration?.$?.token,
+ },
+ name: name,
+ container: 'rtsp',
+ url: await client.getStreamUrl($.token),
+ video: {
+ fps: videoEncoderConfiguration?.rateControl?.frameRateLimit,
+ bitrate: computeBitrate(videoEncoderConfiguration?.rateControl?.bitrateLimit),
+ width: videoEncoderConfiguration?.resolution?.width,
+ height: videoEncoderConfiguration?.resolution?.height,
+ codec: videoEncoderConfiguration?.encoding?.toLowerCase(),
+ idrIntervalMillis: computeInterval(videoEncoderConfiguration?.rateControl?.frameRateLimit,
+ videoEncoderConfiguration?.$.GovLength),
+ },
+ audio: {
+ bitrate: computeBitrate(audioEncoderConfiguration?.bitrate),
+ codec: convertAudioCodec(audioEncoderConfiguration?.encoding),
+ }
+ })
+ }
+ catch (e) {
+ console.error('error retrieving onvif profile', $.token, e);
+ }
+ }
+
+ return ret;
+}
diff --git a/plugins/onvif/tsconfig.json b/plugins/onvif/tsconfig.json
index 34a847ad8..ba9b4d395 100644
--- a/plugins/onvif/tsconfig.json
+++ b/plugins/onvif/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "module": "commonjs",
+ "module": "Node16",
"target": "ES2021",
"resolveJsonModule": true,
"moduleResolution": "Node16",