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