mirror of
https://github.com/koush/scrypted.git
synced 2026-09-22 19:50:39 +01:00
cameras: wip codec configuration
This commit is contained in:
@@ -7,6 +7,7 @@ import { IncomingMessage } from 'http';
|
|||||||
import { EventEmitter, Readable } from 'stream';
|
import { EventEmitter, Readable } from 'stream';
|
||||||
import { Destroyable } from '../../rtsp/src/rtsp';
|
import { Destroyable } from '../../rtsp/src/rtsp';
|
||||||
import { getDeviceInfo } from './probe';
|
import { getDeviceInfo } from './probe';
|
||||||
|
import { sleep } from "@scrypted/common/src/sleep";
|
||||||
|
|
||||||
export const detectionMap = {
|
export const detectionMap = {
|
||||||
human: 'person',
|
human: 'person',
|
||||||
@@ -53,7 +54,7 @@ export class HikvisionCameraAPI implements HikvisionAPI {
|
|||||||
},
|
},
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
credential: this.credential,
|
credential: this.credential,
|
||||||
body,
|
body: typeof urlOrOptions !== 'string' && !(urlOrOptions instanceof URL) ? urlOrOptions?.body : body,
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -135,6 +136,42 @@ export class HikvisionCameraAPI implements HikvisionAPI {
|
|||||||
return response.body;
|
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> {
|
async listenEvents(): Promise<Destroyable> {
|
||||||
const events = new EventEmitter();
|
const events = new EventEmitter();
|
||||||
(events as any).destroy = () => { };
|
(events as any).destroy = () => { };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "Node16",
|
||||||
"target": "ES2021",
|
"target": "ES2021",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"moduleResolution": "Node16",
|
"moduleResolution": "Node16",
|
||||||
|
|||||||
@@ -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 { AddressInfo } from "net";
|
||||||
import onvif from 'onvif';
|
import onvif from 'onvif';
|
||||||
import { Stream } from "stream";
|
import { Stream } from "stream";
|
||||||
import xml2js from 'xml2js';
|
import xml2js from 'xml2js';
|
||||||
import { Destroyable, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||||
import { connectCameraAPI, OnvifCameraAPI, OnvifEvent } from "./onvif-api";
|
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 { OnvifIntercom } from "./onvif-intercom";
|
||||||
import { OnvifPTZMixinProvider } from "./onvif-ptz";
|
import { OnvifPTZMixinProvider } from "./onvif-ptz";
|
||||||
import { listenEvents } from "./onvif-events";
|
|
||||||
|
|
||||||
const { mediaManager, systemManager, deviceManager } = sdk;
|
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 {
|
class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, VideoCameraConfiguration, Reboot {
|
||||||
eventStream: Stream;
|
eventStream: Stream;
|
||||||
client: OnvifCameraAPI;
|
client: OnvifCameraAPI;
|
||||||
@@ -51,55 +32,8 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
|
|||||||
|
|
||||||
async setVideoStreamOptions(options: MediaStreamOptions): Promise<void> {
|
async setVideoStreamOptions(options: MediaStreamOptions): Promise<void> {
|
||||||
const client = await this.getClient();
|
const client = await this.getClient();
|
||||||
const profiles: any[] = await client.getProfiles();
|
await configureCodecs(client, options);
|
||||||
const profile = profiles.find(profile => profile.$.token === options.id);
|
this.rtspMediaStreamOptions = undefined;
|
||||||
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();
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeviceInfo() {
|
async updateDeviceInfo() {
|
||||||
@@ -187,55 +121,31 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getConstructedVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
|
async getConstructedVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
|
||||||
if (!this.rtspMediaStreamOptions) {
|
if (this.rtspMediaStreamOptions)
|
||||||
this.rtspMediaStreamOptions = new Promise(async (resolve) => {
|
return this.rtspMediaStreamOptions;
|
||||||
try {
|
|
||||||
const client = await this.getClient();
|
this.rtspMediaStreamOptions = new Promise(async (resolve) => {
|
||||||
const profiles: any[] = await client.getProfiles();
|
try {
|
||||||
const ret: UrlMediaStreamOptions[] = [];
|
const client = await this.getClient();
|
||||||
for (const { $, name, videoEncoderConfiguration, audioEncoderConfiguration } of profiles) {
|
const ret = await getCodecs(this.console, client);
|
||||||
try {
|
|
||||||
ret.push({
|
if (!ret.length)
|
||||||
id: $.token,
|
throw new Error('onvif camera had no profiles.');
|
||||||
metadata: {
|
|
||||||
videoId: videoEncoderConfiguration?.$?.token,
|
if (this.isAudioDisabled()) {
|
||||||
audioId: audioEncoderConfiguration?.$?.token,
|
for (const r of ret) {
|
||||||
},
|
r.audio = null;
|
||||||
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 (!ret.length)
|
|
||||||
throw new Error('onvif camera had no profiles.');
|
|
||||||
|
|
||||||
resolve(ret);
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
|
||||||
this.rtspMediaStreamOptions = undefined;
|
resolve(ret);
|
||||||
this.console.error('error retrieving onvif profiles', e);
|
}
|
||||||
resolve(undefined);
|
catch (e) {
|
||||||
}
|
this.rtspMediaStreamOptions = undefined;
|
||||||
})
|
this.console.error('error retrieving onvif profiles', e);
|
||||||
}
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return this.rtspMediaStreamOptions;
|
return this.rtspMediaStreamOptions;
|
||||||
}
|
}
|
||||||
|
|||||||
133
plugins/onvif/src/onvif-configure.ts
Normal file
133
plugins/onvif/src/onvif-configure.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "Node16",
|
||||||
"target": "ES2021",
|
"target": "ES2021",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"moduleResolution": "Node16",
|
"moduleResolution": "Node16",
|
||||||
|
|||||||
Reference in New Issue
Block a user