cameras: wip codec configuration

This commit is contained in:
Koushik Dutta
2024-08-08 23:06:50 -07:00
parent c15fe4281e
commit 1cd5c194cc
5 changed files with 203 additions and 123 deletions

View File

@@ -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 = '<?xml version="1.0" encoding="UTF-8"?>\r\n' +
'<VCAResource version="2.0" xmlns="http://www.hikvision.com/ver20/XMLSchema">\r\n' +
`<type>${resource}</type>\r\n` +
'</VCAResource>\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<Destroyable> {
const events = new EventEmitter();
(events as any).destroy = () => { };

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "Node16",
"target": "ES2021",
"resolveJsonModule": true,
"moduleResolution": "Node16",

View File

@@ -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<void> {
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<UrlMediaStreamOptions[]> {
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;
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "Node16",
"target": "ES2021",
"resolveJsonModule": true,
"moduleResolution": "Node16",