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 { 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 = () => { };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"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 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;
|
||||
}
|
||||
|
||||
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": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
Reference in New Issue
Block a user