mirror of
https://github.com/koush/scrypted.git
synced 2026-03-20 16:40:24 +00:00
hikvision: wip autoconfigure
This commit is contained in:
163
common/src/autoconfigure-codecs.ts
Normal file
163
common/src/autoconfigure-codecs.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { MediaStreamConfiguration, MediaStreamDestination, MediaStreamOptions, Setting } from "@scrypted/sdk";
|
||||
|
||||
export const automaticallyConfigureSettings: Setting = {
|
||||
key: 'autoconfigure',
|
||||
title: 'Automatically Configure Settings',
|
||||
description: 'Automatically configure and valdiate the camera codecs and other settings for optimal Scrypted performance. Some settings will require manual configuration via the camera web admin.',
|
||||
type: 'boolean',
|
||||
value: true,
|
||||
};
|
||||
|
||||
const MEGABIT = 1024 * 1000;
|
||||
|
||||
function getBitrateForResolution(resolution: number) {
|
||||
if (resolution >= 3840 * 2160)
|
||||
return 8 * MEGABIT;
|
||||
if (resolution >= 2688 * 1520)
|
||||
return 3 * MEGABIT;
|
||||
if (resolution >= 1920 * 1080)
|
||||
return 2 * MEGABIT;
|
||||
if (resolution >= 1280 * 720)
|
||||
return MEGABIT;
|
||||
if (resolution >= 640 * 480)
|
||||
return MEGABIT / 2;
|
||||
return MEGABIT / 4;
|
||||
}
|
||||
|
||||
export async function autoconfigureCodecs(getCodecs: () => Promise<MediaStreamOptions[]>, configureCodecs: (options: MediaStreamOptions) => Promise<MediaStreamConfiguration>) {
|
||||
const codecs = await getCodecs();
|
||||
const configurable: MediaStreamConfiguration[] = [];
|
||||
for (const codec of codecs) {
|
||||
const config = await configureCodecs( {
|
||||
id: codec.id,
|
||||
});
|
||||
configurable.push(config);
|
||||
}
|
||||
|
||||
const used: MediaStreamConfiguration[] = [];
|
||||
|
||||
for (const _ of ['local', 'remote', 'low-resolution'] as MediaStreamDestination[]) {
|
||||
// find stream with the highest configurable resolution.
|
||||
let highest: [MediaStreamConfiguration, number] = [undefined, 0];
|
||||
for (const codec of configurable) {
|
||||
if (used.includes(codec))
|
||||
continue;
|
||||
for (const resolution of codec.video.resolutions) {
|
||||
if (resolution[0] * resolution[1] > highest[1]) {
|
||||
highest = [codec, resolution[0] * resolution[1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = highest[0];
|
||||
if (!config)
|
||||
break;
|
||||
|
||||
used.push(config);
|
||||
}
|
||||
|
||||
const findResolutionTarget = (config: MediaStreamConfiguration, width: number, height: number) => {
|
||||
let diff = 999999999;
|
||||
let ret: [number, number];
|
||||
|
||||
for (const res of config.video.resolutions) {
|
||||
const d = Math.abs(res[0] - width) + Math.abs(res[1] - height);
|
||||
if (d < diff) {
|
||||
diff = d;
|
||||
ret = res;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// find the highest resolution
|
||||
const l = used[0];
|
||||
const resolution = findResolutionTarget(l, 8192, 8192);
|
||||
|
||||
// get the fps of 20 or highest available
|
||||
let fps = Math.min(20, Math.max(...l.video.fpsRange));
|
||||
|
||||
await configureCodecs({
|
||||
id: l.id,
|
||||
video: {
|
||||
width: resolution[0],
|
||||
height: resolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: getBitrateForResolution(resolution[0] * resolution[1]),
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
});
|
||||
|
||||
if (used.length === 3) {
|
||||
// find remote and low
|
||||
const r = used[1];
|
||||
const l = used[2];
|
||||
|
||||
const rResolution = findResolutionTarget(r, 1280, 720);
|
||||
const lResolution = findResolutionTarget(l, 640, 360);
|
||||
|
||||
fps = Math.min(20, Math.max(...r.video.fpsRange));
|
||||
await configureCodecs({
|
||||
id: r.id,
|
||||
video: {
|
||||
width: rResolution[0],
|
||||
height: rResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: 1 * MEGABIT,
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
});
|
||||
|
||||
fps = Math.min(20, Math.max(...l.video.fpsRange));
|
||||
await configureCodecs( {
|
||||
id: l.id,
|
||||
video: {
|
||||
width: lResolution[0],
|
||||
height: lResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: MEGABIT / 2,
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
});
|
||||
}
|
||||
else if (used.length == 2) {
|
||||
let target: [number, number];
|
||||
if (resolution[0] * resolution[1] > 1920 * 1080)
|
||||
target = [1280, 720];
|
||||
else
|
||||
target = [640, 360];
|
||||
|
||||
const rResolution = findResolutionTarget(used[1], target[0], target[1]);
|
||||
const fps = Math.min(20, Math.max(...used[1].video.fpsRange));
|
||||
await configureCodecs({
|
||||
id: used[1].id,
|
||||
video: {
|
||||
width: rResolution[0],
|
||||
height: rResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: getBitrateForResolution(rResolution[0] * rResolution[1]),
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
});
|
||||
}
|
||||
else if (used.length === 1) {
|
||||
// no nop
|
||||
}
|
||||
}
|
||||
@@ -110,11 +110,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
this.info = deviceInfo;
|
||||
}
|
||||
|
||||
async setVideoStreamOptions(options: MediaStreamOptions): Promise<void> {
|
||||
async setVideoStreamOptions(options: MediaStreamOptions) {
|
||||
if (!options.id?.startsWith('channel'))
|
||||
throw new Error('invalid id');
|
||||
const channel = parseInt(this.getRtspChannel()) || 1;
|
||||
const formatNumber = parseInt(options.id?.substring('channel'.length)) - 1;
|
||||
const formatNumber = Math.max(0, parseInt(options.id?.substring('channel'.length)) - 1);
|
||||
const format = options.id === 'channel0' ? 'MainFormat' : 'ExtraFormat';
|
||||
const encode = `Encode[${channel - 1}].${format}[${formatNumber}]`;
|
||||
const params = new URLSearchParams();
|
||||
@@ -128,6 +128,15 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
if (options.video?.codec === 'h264') {
|
||||
params.set(`${encode}.Video.Compression`, 'H.264');
|
||||
}
|
||||
if (options.video?.profile) {
|
||||
let profile = 'Main';
|
||||
if (options.video.profile === 'high')
|
||||
profile = 'High';
|
||||
else if (options.video.profile === 'baseline')
|
||||
profile = 'Baseline';
|
||||
params.set(`${encode}.Video.Profile`, profile);
|
||||
|
||||
}
|
||||
if (options.video?.codec === 'h265') {
|
||||
params.set(`${encode}.Video.Compression`, 'H.265');
|
||||
}
|
||||
@@ -136,12 +145,12 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
if (options.video?.fps) {
|
||||
params.set(`${encode}.Video.FPS`, options.video.fps.toString());
|
||||
if (options.video?.idrIntervalMillis) {
|
||||
params.set(`${encode}.Video.GOP`, (options.video.fps * options.video?.idrIntervalMillis / 1000).toString());
|
||||
}
|
||||
}
|
||||
if (options.video?.keyframeInterval) {
|
||||
params.set(`${encode}.Video.GOP`, options.video?.keyframeInterval.toString());
|
||||
}
|
||||
if (options.video?.bitrateControl) {
|
||||
params.set(`${encode}.Video.BitRateControl`, options.video.bitrateControl === 'variable' ? 'VBR' : 'CBR');
|
||||
params.set(`${encode}.Video.BitRateControl`, options.video.bitrateControl === 'constant' ? 'CBR' : 'VBR');
|
||||
}
|
||||
|
||||
if (![...params.keys()].length)
|
||||
@@ -152,6 +161,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log('reconfigure result', response.body);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getClient() {
|
||||
@@ -284,6 +294,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
const ret = await super.getOtherSettings();
|
||||
ret.push(
|
||||
{
|
||||
subgroup: 'Advanced',
|
||||
title: 'Doorbell Type',
|
||||
choices: [
|
||||
'Not a Doorbell',
|
||||
@@ -354,6 +365,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|
||||
ret.push(
|
||||
{
|
||||
subgroup: 'Advanced',
|
||||
title: 'Two Way Audio',
|
||||
value: twoWayAudio,
|
||||
key: 'twoWayAudio',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
@@ -149,6 +149,7 @@ export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> e
|
||||
|
||||
getInterfaces() {
|
||||
return [
|
||||
ScryptedInterface.VideoCameraConfiguration,
|
||||
ScryptedInterface.VideoCamera,
|
||||
ScryptedInterface.Settings,
|
||||
...this.getAdditionalInterfaces()
|
||||
|
||||
@@ -124,6 +124,21 @@ class FFmpegCamera extends CameraBase<UrlMediaStreamOptions> {
|
||||
return mediaManager.createFFmpegMediaObject(ret);
|
||||
}
|
||||
|
||||
isAudioDisabled() {
|
||||
return this.storage.getItem('noAudio') === 'true';
|
||||
}
|
||||
|
||||
async getOtherSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
key: 'noAudio',
|
||||
title: 'No Audio',
|
||||
description: 'Enable this setting if the camera does not have audio or to mute audio.',
|
||||
type: 'boolean',
|
||||
value: (this.isAudioDisabled()).toString(),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class FFmpegProvider extends CameraProviderBase<UrlMediaStreamOptions> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
401
plugins/hikvision/src/hikvision-api-capabilities.ts
Normal file
401
plugins/hikvision/src/hikvision-api-capabilities.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
export interface CapabiltiesResponse {
|
||||
StreamingChannel: StreamingChannel
|
||||
}
|
||||
|
||||
export interface StreamingChannel {
|
||||
$: GeneratedType
|
||||
id: Id[]
|
||||
channelName: ChannelName[]
|
||||
enabled: Enabled[]
|
||||
Transport: Transport[]
|
||||
Video: Video[]
|
||||
Audio: Audio[]
|
||||
isSpportDynamicCapWithCondition: string[]
|
||||
}
|
||||
|
||||
export interface GeneratedType {
|
||||
version: string
|
||||
xmlns: string
|
||||
}
|
||||
|
||||
export interface Id {
|
||||
_: string
|
||||
$: GeneratedType2
|
||||
}
|
||||
|
||||
export interface GeneratedType2 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface ChannelName {
|
||||
_: string
|
||||
$: GeneratedType3
|
||||
}
|
||||
|
||||
export interface GeneratedType3 {
|
||||
min: string
|
||||
max: string
|
||||
}
|
||||
|
||||
export interface Enabled {
|
||||
_: string
|
||||
$: GeneratedType4
|
||||
}
|
||||
|
||||
export interface GeneratedType4 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface Transport {
|
||||
maxPacketSize: MaxPacketSize[]
|
||||
ControlProtocolList: ControlProtocolList[]
|
||||
Multicast: Multicast[]
|
||||
Unicast: Unicast[]
|
||||
Security: Security[]
|
||||
}
|
||||
|
||||
export interface MaxPacketSize {
|
||||
_: string
|
||||
$: GeneratedType5
|
||||
}
|
||||
|
||||
export interface GeneratedType5 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface ControlProtocolList {
|
||||
ControlProtocol: ControlProtocol[]
|
||||
}
|
||||
|
||||
export interface ControlProtocol {
|
||||
streamingTransport: StreamingTransport[]
|
||||
}
|
||||
|
||||
export interface StreamingTransport {
|
||||
_: string
|
||||
$: GeneratedType6
|
||||
}
|
||||
|
||||
export interface GeneratedType6 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface Multicast {
|
||||
enabled: Enabled2[]
|
||||
videoDestPortNo: VideoDestPortNo[]
|
||||
audioDestPortNo: AudioDestPortNo[]
|
||||
}
|
||||
|
||||
export interface Enabled2 {
|
||||
$: GeneratedType7
|
||||
}
|
||||
|
||||
export interface GeneratedType7 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface VideoDestPortNo {
|
||||
$: GeneratedType8
|
||||
}
|
||||
|
||||
export interface GeneratedType8 {
|
||||
min: string
|
||||
max: string
|
||||
default: string
|
||||
}
|
||||
|
||||
export interface AudioDestPortNo {
|
||||
$: GeneratedType9
|
||||
}
|
||||
|
||||
export interface GeneratedType9 {
|
||||
min: string
|
||||
max: string
|
||||
default: string
|
||||
}
|
||||
|
||||
export interface Unicast {
|
||||
enabled: Enabled3[]
|
||||
rtpTransportType: RtpTransportType[]
|
||||
}
|
||||
|
||||
export interface Enabled3 {
|
||||
_: string
|
||||
$: GeneratedType10
|
||||
}
|
||||
|
||||
export interface GeneratedType10 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface RtpTransportType {
|
||||
_: string
|
||||
$: GeneratedType11
|
||||
}
|
||||
|
||||
export interface GeneratedType11 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface Security {
|
||||
enabled: Enabled4[]
|
||||
certificateType: CertificateType[]
|
||||
SecurityAlgorithm: SecurityAlgorithm[]
|
||||
}
|
||||
|
||||
export interface Enabled4 {
|
||||
_: string
|
||||
$: GeneratedType12
|
||||
}
|
||||
|
||||
export interface GeneratedType12 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface CertificateType {
|
||||
_: string
|
||||
$: GeneratedType13
|
||||
}
|
||||
|
||||
export interface GeneratedType13 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface SecurityAlgorithm {
|
||||
algorithmType: AlgorithmType[]
|
||||
}
|
||||
|
||||
export interface AlgorithmType {
|
||||
$: GeneratedType14
|
||||
}
|
||||
|
||||
export interface GeneratedType14 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
enabled: Enabled5[]
|
||||
videoInputChannelID: VideoInputChannelId[]
|
||||
videoCodecType: VideoCodecType[]
|
||||
videoScanType: VideoScanType[]
|
||||
videoResolutionWidth: VideoResolutionWidth[]
|
||||
videoResolutionHeight: VideoResolutionHeight[]
|
||||
videoQualityControlType: VideoQualityControlType[]
|
||||
constantBitRate: ConstantBitRate[]
|
||||
fixedQuality: FixedQuality[]
|
||||
vbrUpperCap: VbrUpperCap[]
|
||||
vbrLowerCap: string[]
|
||||
maxFrameRate: MaxFrameRate[]
|
||||
keyFrameInterval: KeyFrameInterval[]
|
||||
snapShotImageType: SnapShotImageType[]
|
||||
H264Profile: H264Profile[]
|
||||
GovLength: GovLength[]
|
||||
SVC: Svc[]
|
||||
smoothing: Smoothing[]
|
||||
H265Profile: H265Profile[]
|
||||
}
|
||||
|
||||
export interface Enabled5 {
|
||||
_: string
|
||||
$: GeneratedType15
|
||||
}
|
||||
|
||||
export interface GeneratedType15 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface VideoInputChannelId {
|
||||
_: string
|
||||
$: GeneratedType16
|
||||
}
|
||||
|
||||
export interface GeneratedType16 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface VideoCodecType {
|
||||
_: string
|
||||
$: GeneratedType17
|
||||
}
|
||||
|
||||
export interface GeneratedType17 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface VideoScanType {
|
||||
_: string
|
||||
$: GeneratedType18
|
||||
}
|
||||
|
||||
export interface GeneratedType18 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface VideoResolutionWidth {
|
||||
_: string
|
||||
$: GeneratedType19
|
||||
}
|
||||
|
||||
export interface GeneratedType19 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface VideoResolutionHeight {
|
||||
_: string
|
||||
$: GeneratedType20
|
||||
}
|
||||
|
||||
export interface GeneratedType20 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface VideoQualityControlType {
|
||||
_: string
|
||||
$: GeneratedType21
|
||||
}
|
||||
|
||||
export interface GeneratedType21 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface ConstantBitRate {
|
||||
_: string
|
||||
$: GeneratedType22
|
||||
}
|
||||
|
||||
export interface GeneratedType22 {
|
||||
min: string
|
||||
max: string
|
||||
}
|
||||
|
||||
export interface FixedQuality {
|
||||
_: string
|
||||
$: GeneratedType23
|
||||
}
|
||||
|
||||
export interface GeneratedType23 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface VbrUpperCap {
|
||||
_: string
|
||||
$: GeneratedType24
|
||||
}
|
||||
|
||||
export interface GeneratedType24 {
|
||||
min: string
|
||||
max: string
|
||||
}
|
||||
|
||||
export interface MaxFrameRate {
|
||||
_: string
|
||||
$: GeneratedType25
|
||||
}
|
||||
|
||||
export interface GeneratedType25 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface KeyFrameInterval {
|
||||
_: string
|
||||
$: GeneratedType26
|
||||
}
|
||||
|
||||
export interface GeneratedType26 {
|
||||
min: string
|
||||
max: string
|
||||
}
|
||||
|
||||
export interface SnapShotImageType {
|
||||
_: string
|
||||
$: GeneratedType27
|
||||
}
|
||||
|
||||
export interface GeneratedType27 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface H264Profile {
|
||||
_: string
|
||||
$: GeneratedType28
|
||||
}
|
||||
|
||||
export interface GeneratedType28 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface GovLength {
|
||||
_: string
|
||||
$: GeneratedType29
|
||||
}
|
||||
|
||||
export interface GeneratedType29 {
|
||||
min: string
|
||||
max: string
|
||||
}
|
||||
|
||||
export interface Svc {
|
||||
enabled: Enabled6[]
|
||||
SVCMode: Svcmode[]
|
||||
}
|
||||
|
||||
export interface Enabled6 {
|
||||
_: string
|
||||
$: GeneratedType30
|
||||
}
|
||||
|
||||
export interface GeneratedType30 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface Svcmode {
|
||||
_: string
|
||||
$: GeneratedType31
|
||||
}
|
||||
|
||||
export interface GeneratedType31 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface Smoothing {
|
||||
_: string
|
||||
$: GeneratedType32
|
||||
}
|
||||
|
||||
export interface GeneratedType32 {
|
||||
min: string
|
||||
max: string
|
||||
}
|
||||
|
||||
export interface H265Profile {
|
||||
_: string
|
||||
$: GeneratedType33
|
||||
}
|
||||
|
||||
export interface GeneratedType33 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface Audio {
|
||||
enabled: Enabled7[]
|
||||
audioInputChannelID: string[]
|
||||
audioCompressionType: AudioCompressionType[]
|
||||
}
|
||||
|
||||
export interface Enabled7 {
|
||||
_: string
|
||||
$: GeneratedType34
|
||||
}
|
||||
|
||||
export interface GeneratedType34 {
|
||||
opt: string
|
||||
}
|
||||
|
||||
export interface AudioCompressionType {
|
||||
_: string
|
||||
$: GeneratedType35
|
||||
}
|
||||
|
||||
export interface GeneratedType35 {
|
||||
opt: string
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { HttpFetchOptions } from '@scrypted/common/src/http-auth-fetch';
|
||||
import { MediaStreamConfiguration, MediaStreamOptions } from '@scrypted/sdk';
|
||||
import { Readable } from 'stream';
|
||||
import { Destroyable } from '../../rtsp/src/rtsp';
|
||||
|
||||
@@ -18,4 +19,6 @@ export interface HikvisionAPI {
|
||||
checkStreamSetup(channel: string, isOld: boolean): Promise<HikvisionCameraStreamSetup>;
|
||||
jpegSnapshot(channel: string, timeout: number): Promise<Buffer>;
|
||||
listenEvents(): Promise<Destroyable>;
|
||||
getCodecs(camNumber: string): Promise<MediaStreamOptions[]>;
|
||||
configureCodecs(camNumber: string, channelNumber: string, options: MediaStreamOptions): Promise<MediaStreamConfiguration>;
|
||||
}
|
||||
12
plugins/hikvision/src/hikvision-autoconfigure.ts
Normal file
12
plugins/hikvision/src/hikvision-autoconfigure.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { autoconfigureCodecs as ac } from '../../../common/src/autoconfigure-codecs';
|
||||
import { HikvisionAPI } from './hikvision-api-channels';
|
||||
|
||||
export async function autoconfigureSettings(client: HikvisionAPI, camNumber: string) {
|
||||
return ac(
|
||||
() => client.getCodecs(camNumber),
|
||||
(options) => {
|
||||
const channelNumber = options.id.substring(1);
|
||||
return client.configureCodecs(camNumber, channelNumber, options)
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import { HikvisionCameraStreamSetup, HikvisionAPI } from "./hikvision-api-interfaces"
|
||||
import { AuthFetchCredentialState, HttpFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
|
||||
import { readLine } from '@scrypted/common/src/read-stream';
|
||||
import { parseHeaders, readBody, readMessage } from '@scrypted/common/src/rtsp-server';
|
||||
import { MediaStreamConfiguration, MediaStreamOptions } from "@scrypted/sdk";
|
||||
import contentType from 'content-type';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { EventEmitter, Readable } from 'stream';
|
||||
import xml2js from 'xml2js';
|
||||
import { Destroyable } from '../../rtsp/src/rtsp';
|
||||
import { CapabiltiesResponse } from './hikvision-api-capabilities';
|
||||
import { HikvisionAPI, HikvisionCameraStreamSetup } from "./hikvision-api-channels";
|
||||
import { ChannelResponse, ChannelsResponse } from './hikvision-xml-types';
|
||||
import { getDeviceInfo } from './probe';
|
||||
import { sleep } from "@scrypted/common/src/sleep";
|
||||
|
||||
export const detectionMap = {
|
||||
human: 'person',
|
||||
@@ -156,15 +159,15 @@ export class HikvisionCameraAPI implements HikvisionAPI {
|
||||
`<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',
|
||||
},
|
||||
});
|
||||
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();
|
||||
@@ -278,4 +281,170 @@ export class HikvisionCameraAPI implements HikvisionAPI {
|
||||
|
||||
return this.listenerPromise;
|
||||
}
|
||||
|
||||
async configureCodecs(camNumber: string, channelNumber: string, options: MediaStreamOptions): Promise<MediaStreamConfiguration> {
|
||||
const cameraChannel = `${camNumber}${channelNumber}`;
|
||||
let vsos = await this.getCodecs(camNumber);
|
||||
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/ISAPI/Streaming/channels/${cameraChannel}`,
|
||||
responseType: 'text',
|
||||
});
|
||||
const channel: ChannelResponse = await xml2js.parseStringPromise(response.body);
|
||||
const vc = channel.StreamingChannel.Video[0];
|
||||
|
||||
const { video: videoOptions, audio: audioOptions } = options;
|
||||
|
||||
if (videoOptions?.codec) {
|
||||
let videoCodecType: string;
|
||||
switch (videoOptions.codec) {
|
||||
case 'h264':
|
||||
videoCodecType = 'H.264';
|
||||
break;
|
||||
case 'h265':
|
||||
videoCodecType = 'H.265';
|
||||
break;
|
||||
}
|
||||
if (videoCodecType) {
|
||||
vc.videoCodecType = [videoCodecType];
|
||||
vc.SmartCodec = [{
|
||||
enabled: ['false'],
|
||||
}];
|
||||
vc.SVC = [{
|
||||
enabled: ['false'],
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
if (videoOptions?.keyframeInterval)
|
||||
vc.GovLength = [videoOptions.keyframeInterval.toString()];
|
||||
|
||||
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) {
|
||||
vc.H264Profile = [profile];
|
||||
vc.H265Profile = [profile];
|
||||
}
|
||||
}
|
||||
|
||||
if (videoOptions?.width && videoOptions?.height) {
|
||||
vc.videoResolutionWidth = [videoOptions?.width.toString()];
|
||||
vc.videoResolutionHeight = [videoOptions?.height.toString()];
|
||||
}
|
||||
|
||||
|
||||
// can't be set by hikvision. But see if it is settable and doesn't match to direct user.
|
||||
if (videoOptions?.bitrateControl && vc.videoQualityControlType?.[0]) {
|
||||
const constant = videoOptions?.bitrateControl === 'constant';
|
||||
if ((vc.videoQualityControlType[0] === 'CBR' && !constant) || (vc.videoQualityControlType[0] === 'VBR' && constant))
|
||||
throw new Error(options.id + ': The camera video Bitrate Type must be manually set to ' + videoOptions?.bitrateControl + ' in the camera web admin.');
|
||||
}
|
||||
|
||||
if (videoOptions?.bitrateControl) {
|
||||
if (videoOptions?.bitrateControl === 'constant')
|
||||
vc.videoQualityControlType = ['CBR'];
|
||||
else if (videoOptions?.bitrateControl === 'variable')
|
||||
vc.videoQualityControlType = ['VBR'];
|
||||
}
|
||||
|
||||
if (videoOptions?.bitrate) {
|
||||
const br = Math.round(videoOptions?.bitrate / 1000);
|
||||
vc.vbrUpperCap = [br.toString()];
|
||||
vc.constantBitRate = [br.toString()];
|
||||
}
|
||||
|
||||
if (videoOptions?.fps) {
|
||||
// weird calculation here per docs
|
||||
const fps = videoOptions.fps * 100;
|
||||
vc.maxFrameRate = [fps.toString()];
|
||||
// not sure if this is necessary.
|
||||
const gov = parseInt(vc.GovLength[0]);
|
||||
vc.keyFrameInterval = [(gov / videoOptions.fps * 100).toString()];
|
||||
}
|
||||
|
||||
const builder = new xml2js.Builder();
|
||||
const put = builder.buildObject(vc);
|
||||
|
||||
await this.request({
|
||||
method: 'PUT',
|
||||
url: `http://${this.ip}/ISAPI/Streaming/channels/${cameraChannel}`,
|
||||
responseType: 'text',
|
||||
body: put,
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
}
|
||||
});
|
||||
|
||||
const response2 = await this.request({
|
||||
url: `http://${this.ip}/ISAPI/Streaming/channels/${cameraChannel}/capabilities`,
|
||||
responseType: 'text',
|
||||
});
|
||||
|
||||
vsos = await this.getCodecs(camNumber);
|
||||
const vso: MediaStreamConfiguration = vsos.find(vso => vso.id === cameraChannel);
|
||||
|
||||
const capabilities: CapabiltiesResponse = await xml2js.parseStringPromise(response2.body);
|
||||
const v = capabilities.StreamingChannel.Video[0];
|
||||
vso.video.bitrateRange = [parseInt(v.vbrUpperCap[0].$.min) * 1000, parseInt(v.vbrUpperCap[0].$.max) * 1000];
|
||||
const fpsRange = v.maxFrameRate[0].$.opt.split(',').map(fps => parseInt(fps) / 1000);
|
||||
vso.video.fpsRange = [Math.min(...fpsRange), Math.max(...fpsRange)];
|
||||
|
||||
vso.video.bitrateControls = ['constant', 'variable'];
|
||||
vso.video.keyframeIntervalRange = [parseInt(v.GovLength[0].$.min), parseInt(v.GovLength[0].$.max)];
|
||||
const videoResolutionWidths = v.videoResolutionWidth[0].$.opt.split(',').map(w => parseInt(w));
|
||||
const videoResolutionHeights = v.videoResolutionHeight[0].$.opt.split(',').map(h => parseInt(h));
|
||||
vso.video.resolutions = videoResolutionWidths.map((w, i) => ([w, videoResolutionHeights[i]]));
|
||||
|
||||
return vso;
|
||||
}
|
||||
|
||||
async getCodecs(camNumber: string) {
|
||||
const defaultMap = new Map<string, MediaStreamOptions>();
|
||||
defaultMap.set(camNumber + '01', undefined);
|
||||
defaultMap.set(camNumber + '02', undefined);
|
||||
|
||||
try {
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/ISAPI/Streaming/channels`,
|
||||
responseType: 'text',
|
||||
});
|
||||
const xml = response.body;
|
||||
const parsedXml: ChannelsResponse = await xml2js.parseStringPromise(xml);
|
||||
|
||||
const vsos: MediaStreamOptions[] = [];
|
||||
for (const streamingChannel of parsedXml.StreamingChannelList.StreamingChannel) {
|
||||
const [id] = streamingChannel.id;
|
||||
const width = parseInt(streamingChannel?.Video?.[0]?.videoResolutionWidth?.[0]) || undefined;
|
||||
const height = parseInt(streamingChannel?.Video?.[0]?.videoResolutionHeight?.[0]) || undefined;
|
||||
let codec = streamingChannel?.Video?.[0]?.videoCodecType?.[0] as string;
|
||||
codec = codec?.toLowerCase()?.replaceAll('.', '');
|
||||
const vso: MediaStreamOptions = {
|
||||
id,
|
||||
video: {
|
||||
width,
|
||||
height,
|
||||
codec,
|
||||
}
|
||||
}
|
||||
vsos.push(vso);
|
||||
}
|
||||
|
||||
return vsos;
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('error retrieving channel ids', e);
|
||||
return [...defaultMap.values()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
175
plugins/hikvision/src/hikvision-xml-types.ts
Normal file
175
plugins/hikvision/src/hikvision-xml-types.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
export interface ChannelsResponse {
|
||||
StreamingChannelList: StreamingChannelList;
|
||||
}
|
||||
|
||||
export interface StreamingChannelList {
|
||||
$: Empty;
|
||||
StreamingChannel: StreamingChannel[];
|
||||
}
|
||||
|
||||
export interface Empty {
|
||||
version: string;
|
||||
xmlns: string;
|
||||
}
|
||||
|
||||
export interface StreamingChannel {
|
||||
$: Empty;
|
||||
id: string[];
|
||||
channelName: string[];
|
||||
enabled: string[];
|
||||
Transport: Transport[];
|
||||
Video: Video[];
|
||||
Audio: Audio[];
|
||||
}
|
||||
|
||||
export interface Audio {
|
||||
enabled: string[];
|
||||
audioInputChannelID: string[];
|
||||
audioCompressionType: string[];
|
||||
}
|
||||
|
||||
export interface Transport {
|
||||
maxPacketSize: string[];
|
||||
ControlProtocolList: ControlProtocolList[];
|
||||
Unicast: Unicast[];
|
||||
Multicast: Multicast[];
|
||||
Security: Security[];
|
||||
}
|
||||
|
||||
export interface ControlProtocolList {
|
||||
ControlProtocol: ControlProtocol[];
|
||||
}
|
||||
|
||||
export interface ControlProtocol {
|
||||
streamingTransport: string[];
|
||||
}
|
||||
|
||||
export interface Multicast {
|
||||
enabled: string[];
|
||||
destIPAddress: string[];
|
||||
videoDestPortNo: string[];
|
||||
audioDestPortNo: string[];
|
||||
}
|
||||
|
||||
export interface Security {
|
||||
enabled: string[];
|
||||
certificateType: string[];
|
||||
SecurityAlgorithm: SecurityAlgorithm[];
|
||||
}
|
||||
|
||||
export interface SecurityAlgorithm {
|
||||
algorithmType: string[];
|
||||
}
|
||||
|
||||
export interface Unicast {
|
||||
enabled: string[];
|
||||
rtpTransportType: string[];
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
enabled: string[];
|
||||
videoInputChannelID: string[];
|
||||
videoCodecType: string[];
|
||||
videoScanType: string[];
|
||||
videoResolutionWidth: string[];
|
||||
videoResolutionHeight: string[];
|
||||
videoQualityControlType: string[];
|
||||
constantBitRate: string[];
|
||||
fixedQuality: string[];
|
||||
vbrUpperCap: string[];
|
||||
vbrLowerCap: string[];
|
||||
maxFrameRate: string[];
|
||||
keyFrameInterval: string[];
|
||||
snapShotImageType: string[];
|
||||
H264Profile: string[];
|
||||
GovLength: string[];
|
||||
SVC: SVC[];
|
||||
PacketType: string[];
|
||||
smoothing: string[];
|
||||
H265Profile: string[];
|
||||
SmartCodec?: SVC[];
|
||||
}
|
||||
|
||||
export interface SVC {
|
||||
enabled: string[];
|
||||
}
|
||||
|
||||
export interface ChannelResponse {
|
||||
StreamingChannel: StreamingChannel;
|
||||
}
|
||||
|
||||
|
||||
// {
|
||||
// enabled: [
|
||||
// "true",
|
||||
// ],
|
||||
// videoInputChannelID: [
|
||||
// "1",
|
||||
// ],
|
||||
// videoCodecType: [
|
||||
// "H.264",
|
||||
// ],
|
||||
// videoScanType: [
|
||||
// "progressive",
|
||||
// ],
|
||||
// videoResolutionWidth: [
|
||||
// "3840",
|
||||
// ],
|
||||
// videoResolutionHeight: [
|
||||
// "2160",
|
||||
// ],
|
||||
// videoQualityControlType: [
|
||||
// "VBR",
|
||||
// ],
|
||||
// constantBitRate: [
|
||||
// "8192",
|
||||
// ],
|
||||
// fixedQuality: [
|
||||
// "100",
|
||||
// ],
|
||||
// vbrUpperCap: [
|
||||
// "8192",
|
||||
// ],
|
||||
// vbrLowerCap: [
|
||||
// "32",
|
||||
// ],
|
||||
// maxFrameRate: [
|
||||
// "2000",
|
||||
// ],
|
||||
// keyFrameInterval: [
|
||||
// "4000",
|
||||
// ],
|
||||
// snapShotImageType: [
|
||||
// "JPEG",
|
||||
// ],
|
||||
// H264Profile: [
|
||||
// "Main",
|
||||
// ],
|
||||
// GovLength: [
|
||||
// "80",
|
||||
// ],
|
||||
// SVC: [
|
||||
// {
|
||||
// enabled: [
|
||||
// "false",
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// PacketType: [
|
||||
// "PS",
|
||||
// "RTP",
|
||||
// ],
|
||||
// smoothing: [
|
||||
// "50",
|
||||
// ],
|
||||
// H265Profile: [
|
||||
// "Main",
|
||||
// ],
|
||||
// SmartCodec: [
|
||||
// {
|
||||
// enabled: [
|
||||
// "false",
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
@@ -4,12 +4,20 @@ import { PassThrough } from "stream";
|
||||
import xml2js from 'xml2js';
|
||||
import { RtpPacket } from '../../../external/werift/packages/rtp/src/rtp/rtp';
|
||||
import { connectCameraAPI } from '../../onvif/src/onvif-api';
|
||||
import { autoconfigureCodecs, automaticallyConfigureSettings, configureCodecs } from '../../onvif/src/onvif-configure';
|
||||
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
|
||||
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
|
||||
import { HikvisionAPI } from "./hikvision-api-interfaces";
|
||||
import { HikvisionAPI } from "./hikvision-api-channels";
|
||||
import { HikvisionCameraAPI, HikvisionCameraEvent, detectionMap } from "./hikvision-camera-api";
|
||||
import { automaticallyConfigureSettings } from "@scrypted/common/src/autoconfigure-codecs";
|
||||
import { autoconfigureSettings } from "./hikvision-autoconfigure";
|
||||
|
||||
const rtspChannelSetting: Setting = {
|
||||
key: 'rtspChannel',
|
||||
title: 'Channel Number',
|
||||
description: "Optional: The channel number to use for snapshots. E.g., 101, 201, etc. The camera portion, e.g., 1, 2, etc, will be used to construct the RTSP stream.",
|
||||
placeholder: '101',
|
||||
};
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
|
||||
@@ -41,10 +49,10 @@ export class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom
|
||||
}
|
||||
|
||||
async setVideoStreamOptions(options: MediaStreamOptions) {
|
||||
this.detectedChannels = undefined;
|
||||
const client = await connectCameraAPI(this.getHttpAddress(), this.getUsername(), this.getPassword(), this.console, undefined);
|
||||
const ret = await configureCodecs(this.console, client, options);
|
||||
return ret;
|
||||
let vsos = await this.getVideoStreamOptions();
|
||||
const index = vsos.findIndex(vso => vso.id === options.id);
|
||||
const client = this.getClient();
|
||||
return client.configureCodecs(this.getCameraNumber() || '1', (index + 1).toString().padStart(2, '0'), options)
|
||||
}
|
||||
|
||||
async updateDeviceInfo() {
|
||||
@@ -264,15 +272,14 @@ export class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom
|
||||
}
|
||||
|
||||
async getUrlSettings(): Promise<Setting[]> {
|
||||
const rtspSetting = {
|
||||
...rtspChannelSetting,
|
||||
subgroup: 'Advanced',
|
||||
value: this.storage.getItem('rtspChannel'),
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
subgroup: 'Advanced',
|
||||
key: 'rtspChannel',
|
||||
title: 'Channel Number',
|
||||
description: "Optional: The channel number to use for snapshots. E.g., 101, 201, etc. The camera portion, e.g., 1, 2, etc, will be used to construct the RTSP stream.",
|
||||
placeholder: '101',
|
||||
value: this.storage.getItem('rtspChannel'),
|
||||
},
|
||||
rtspSetting,
|
||||
...await super.getUrlSettings(),
|
||||
]
|
||||
}
|
||||
@@ -316,49 +323,32 @@ export class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom
|
||||
if (isOld) {
|
||||
this.console.error('Old NVR. Defaulting to two camera configuration');
|
||||
return defaultMap;
|
||||
} else {
|
||||
}
|
||||
|
||||
try {
|
||||
let channels: MediaStreamOptions[];
|
||||
try {
|
||||
let xml: string;
|
||||
try {
|
||||
const response = await client.request({
|
||||
url: `http://${this.getHttpAddress()}/ISAPI/Streaming/channels`,
|
||||
responseType: 'text',
|
||||
});
|
||||
xml = response.body;
|
||||
this.storage.setItem('channels', xml);
|
||||
}
|
||||
catch (e) {
|
||||
xml = this.storage.getItem('channels');
|
||||
if (!xml)
|
||||
throw e;
|
||||
}
|
||||
const parsedXml = await xml2js.parseStringPromise(xml);
|
||||
|
||||
const ret = new Map<string, MediaStreamOptions>();
|
||||
for (const streamingChannel of parsedXml.StreamingChannelList.StreamingChannel) {
|
||||
const [id] = streamingChannel.id;
|
||||
const width = parseInt(streamingChannel?.Video?.[0]?.videoResolutionWidth?.[0]) || undefined;
|
||||
const height = parseInt(streamingChannel?.Video?.[0]?.videoResolutionHeight?.[0]) || undefined;
|
||||
let codec = streamingChannel?.Video?.[0]?.videoCodecType?.[0] as string;
|
||||
codec = codec?.toLowerCase()?.replaceAll('.', '');
|
||||
const vso: MediaStreamOptions = {
|
||||
id,
|
||||
video: {
|
||||
width,
|
||||
height,
|
||||
codec,
|
||||
}
|
||||
}
|
||||
ret.set(id, vso);
|
||||
}
|
||||
|
||||
return ret;
|
||||
channels = await client.getCodecs(camNumber);
|
||||
this.storage.setItem('channelsJSON', JSON.stringify(channels));
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('error retrieving channel ids', e);
|
||||
this.detectedChannels = undefined;
|
||||
return defaultMap;
|
||||
const raw = this.storage.getItem('channelsJSON');
|
||||
if (!raw)
|
||||
throw e;
|
||||
channels = JSON.parse(raw);
|
||||
}
|
||||
const ret = new Map<string, MediaStreamOptions>();
|
||||
for (const streamingChannel of channels) {
|
||||
const channel = streamingChannel.id;
|
||||
ret.set(channel, streamingChannel);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('error retrieving channel ids', e);
|
||||
this.detectedChannels = undefined;
|
||||
return defaultMap;
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -413,7 +403,8 @@ export class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom
|
||||
|
||||
async putSetting(key: string, value: string) {
|
||||
if (key === automaticallyConfigureSettings.key) {
|
||||
autoconfigureCodecs(this.console, await connectCameraAPI(this.getHttpAddress(), this.getUsername(), this.getPassword(), this.console, undefined))
|
||||
const client = this.getClient();
|
||||
autoconfigureSettings(client, this.getCameraNumber() || '1')
|
||||
.then(() => {
|
||||
this.log.a('Successfully configured settings.');
|
||||
})
|
||||
@@ -660,15 +651,16 @@ class HikvisionProvider extends RtspProvider {
|
||||
const username = settings.username?.toString();
|
||||
const password = settings.password?.toString();
|
||||
|
||||
const api = new HikvisionCameraAPI(httpAddress, username, password, this.console);
|
||||
|
||||
if (settings.autoconfigure) {
|
||||
const client = await connectCameraAPI(httpAddress, username, password, this.console, undefined);
|
||||
await autoconfigureCodecs(this.console, client);
|
||||
const cameraNumber = (settings.rtspChannel as string)?.substring(0, 1) || '1';
|
||||
await autoconfigureSettings(api, cameraNumber);
|
||||
}
|
||||
|
||||
const skipValidate = settings.skipValidate?.toString() === 'true';
|
||||
let twoWayAudio: string;
|
||||
if (!skipValidate) {
|
||||
const api = new HikvisionCameraAPI(httpAddress, username, password, this.console);
|
||||
try {
|
||||
const deviceInfo = await api.getDeviceInfo();
|
||||
|
||||
@@ -702,6 +694,8 @@ class HikvisionProvider extends RtspProvider {
|
||||
device.putSetting('username', username);
|
||||
device.putSetting('password', password);
|
||||
device.setIPAddress(settings.ip?.toString());
|
||||
if (settings.rtspChannel)
|
||||
device.putSetting('rtspChannel', settings.rtspChannel as string);
|
||||
device.setHttpPortOverride(settings.httpPort?.toString());
|
||||
if (twoWayAudio)
|
||||
device.putSetting('twoWayAudio', twoWayAudio);
|
||||
@@ -720,6 +714,7 @@ class HikvisionProvider extends RtspProvider {
|
||||
title: 'Password',
|
||||
type: 'password',
|
||||
},
|
||||
rtspChannelSetting,
|
||||
{
|
||||
key: 'ip',
|
||||
title: 'IP Address',
|
||||
|
||||
@@ -5,10 +5,11 @@ import { Stream } from "stream";
|
||||
import xml2js from 'xml2js';
|
||||
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
import { connectCameraAPI, OnvifCameraAPI } from "./onvif-api";
|
||||
import { autoconfigureCodecs, automaticallyConfigureSettings, configureCodecs, getCodecs } from "./onvif-configure";
|
||||
import { autoconfigureSettings, configureCodecs, getCodecs } from "./onvif-configure";
|
||||
import { listenEvents } from "./onvif-events";
|
||||
import { OnvifIntercom } from "./onvif-intercom";
|
||||
import { OnvifPTZMixinProvider } from "./onvif-ptz";
|
||||
import { automaticallyConfigureSettings } from "@scrypted/common/src/autoconfigure-codecs";
|
||||
|
||||
const { mediaManager, systemManager, deviceManager } = sdk;
|
||||
|
||||
@@ -254,7 +255,7 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
|
||||
|
||||
async putSetting(key: string, value: any) {
|
||||
if (key === automaticallyConfigureSettings.key) {
|
||||
autoconfigureCodecs(this.console, await this.getClient())
|
||||
autoconfigureSettings(this.console, await this.getClient())
|
||||
.then(() => {
|
||||
this.log.a('Successfully configured settings.');
|
||||
})
|
||||
@@ -426,7 +427,7 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
|
||||
|
||||
if (settings.autoconfigure) {
|
||||
const client = await connectCameraAPI(httpAddress, username, password, this.console, undefined);
|
||||
await autoconfigureCodecs(this.console, client);
|
||||
await autoconfigureSettings(this.console, client);
|
||||
}
|
||||
|
||||
const skipValidate = settings.skipValidate?.toString() === 'true';
|
||||
@@ -559,7 +560,7 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
|
||||
adopt.settings.httpPort = entry.port;
|
||||
if (adopt.settings.autoconfigure) {
|
||||
const client = await connectCameraAPI(`${entry.host}:${entry.port || 80}`, adopt.settings.username as string, adopt.settings.password as string, this.console, undefined);
|
||||
await autoconfigureCodecs(this.console, client);
|
||||
await autoconfigureSettings(this.console, client);
|
||||
}
|
||||
await this.createDevice(adopt.settings, adopt.nativeId);
|
||||
this.discoveredDevices.delete(adopt.nativeId);
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { MediaStreamConfiguration, MediaStreamDestination, MediaStreamOptions, Setting, VideoStreamConfiguration } from "@scrypted/sdk";
|
||||
import { OnvifCameraAPI } from "./onvif-api";
|
||||
import { MediaStreamConfiguration, MediaStreamOptions, Setting } from "@scrypted/sdk";
|
||||
import { autoconfigureCodecs as ac } from '../../../common/src/autoconfigure-codecs';
|
||||
import { UrlMediaStreamOptions } from "../../ffmpeg-camera/src/common";
|
||||
|
||||
export const automaticallyConfigureSettings: Setting = {
|
||||
key: 'autoconfigure',
|
||||
title: 'Automatically Configure Settings',
|
||||
description: 'Automatically configure and valdiate the camera codecs and other settings for optimal Scrypted performance. Some settings will require manual configuration via the camera web admin.',
|
||||
type: 'boolean',
|
||||
value: true,
|
||||
};
|
||||
import { OnvifCameraAPI } from "./onvif-api";
|
||||
|
||||
export function computeInterval(fps: number, govLength: number) {
|
||||
if (!fps || !govLength)
|
||||
@@ -16,22 +9,6 @@ export function computeInterval(fps: number, govLength: number) {
|
||||
return govLength / fps * 1000;
|
||||
}
|
||||
|
||||
const MEGABIT = 1024 * 1000;
|
||||
|
||||
function getBitrateForResolution(resolution: number) {
|
||||
if (resolution >= 3840 * 2160)
|
||||
return 8 * MEGABIT;
|
||||
if (resolution >= 2688 * 1520)
|
||||
return 3 * MEGABIT;
|
||||
if (resolution >= 1920 * 1080)
|
||||
return 2 * MEGABIT;
|
||||
if (resolution >= 1280 * 720)
|
||||
return MEGABIT;
|
||||
if (resolution >= 640 * 480)
|
||||
return MEGABIT / 2;
|
||||
return MEGABIT / 4;
|
||||
}
|
||||
|
||||
const onvifToFfmpegVideoCodecMap = {
|
||||
'h264': 'h264',
|
||||
'h265': 'h265',
|
||||
@@ -77,142 +54,11 @@ export function computeBitrate(bitrate: number) {
|
||||
return bitrate * 1000;
|
||||
}
|
||||
|
||||
export async function autoconfigureCodecs(console: Console, client: OnvifCameraAPI) {
|
||||
const codecs = await getCodecs(console, client);
|
||||
const configurable: MediaStreamConfiguration[] = [];
|
||||
for (const codec of codecs) {
|
||||
const config = await configureCodecs(console, client, {
|
||||
id: codec.id,
|
||||
});
|
||||
configurable.push(config);
|
||||
}
|
||||
|
||||
const used: MediaStreamConfiguration[] = [];
|
||||
|
||||
for (const destination of ['local', 'remote', 'low-resolution'] as MediaStreamDestination[]) {
|
||||
// find stream with the highest configurable resolution.
|
||||
let highest: [MediaStreamConfiguration, number] = [undefined, 0];
|
||||
for (const codec of configurable) {
|
||||
if (used.includes(codec))
|
||||
continue;
|
||||
for (const resolution of codec.video.resolutions) {
|
||||
if (resolution[0] * resolution[1] > highest[1]) {
|
||||
highest = [codec, resolution[0] * resolution[1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = highest[0];
|
||||
if (!config)
|
||||
break;
|
||||
|
||||
used.push(config);
|
||||
}
|
||||
|
||||
const findResolutionTarget = (config: MediaStreamConfiguration, width: number, height: number) => {
|
||||
let diff = 999999999;
|
||||
let ret: [number, number];
|
||||
|
||||
for (const res of config.video.resolutions) {
|
||||
const d = Math.abs(res[0] - width) + Math.abs(res[1] - height);
|
||||
if (d < diff) {
|
||||
diff = d;
|
||||
ret = res;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// find the highest resolution
|
||||
const l = used[0];
|
||||
const resolution = findResolutionTarget(l, 8192, 8192);
|
||||
|
||||
// get the fps of 20 or highest available
|
||||
let fps = Math.min(20, Math.max(...l.video.fpsRange));
|
||||
|
||||
await configureCodecs(console, client, {
|
||||
id: l.id,
|
||||
video: {
|
||||
width: resolution[0],
|
||||
height: resolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: getBitrateForResolution(resolution[0] * resolution[1]),
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
});
|
||||
|
||||
if (used.length === 3) {
|
||||
// find remote and low
|
||||
const r = used[1];
|
||||
const l = used[2];
|
||||
|
||||
const rResolution = findResolutionTarget(r, 1280, 720);
|
||||
const lResolution = findResolutionTarget(l, 640, 360);
|
||||
|
||||
fps = Math.min(20, Math.max(...r.video.fpsRange));
|
||||
await configureCodecs(console, client, {
|
||||
id: r.id,
|
||||
video: {
|
||||
width: rResolution[0],
|
||||
height: rResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: 1 * MEGABIT,
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
});
|
||||
|
||||
fps = Math.min(20, Math.max(...l.video.fpsRange));
|
||||
await configureCodecs(console, client, {
|
||||
id: l.id,
|
||||
video: {
|
||||
width: lResolution[0],
|
||||
height: lResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: MEGABIT / 2,
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
});
|
||||
}
|
||||
else if (used.length == 2) {
|
||||
let target: [number, number];
|
||||
if (resolution[0] * resolution[1] > 1920 * 1080)
|
||||
target = [1280, 720];
|
||||
else
|
||||
target = [640, 360];
|
||||
|
||||
const rResolution = findResolutionTarget(used[1], target[0], target[1]);
|
||||
const fps = Math.min(20, Math.max(...used[1].video.fpsRange));
|
||||
await configureCodecs(console, client, {
|
||||
id: used[1].id,
|
||||
video: {
|
||||
width: rResolution[0],
|
||||
height: rResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: getBitrateForResolution(rResolution[0] * rResolution[1]),
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
});
|
||||
}
|
||||
else if (used.length === 1) {
|
||||
// no nop
|
||||
}
|
||||
export async function autoconfigureSettings(console: Console, client: OnvifCameraAPI) {
|
||||
return ac(
|
||||
() => getCodecs(console, client),
|
||||
(options) => configureCodecs(console, client, options)
|
||||
);
|
||||
}
|
||||
|
||||
export async function configureCodecs(console: Console, client: OnvifCameraAPI, options: MediaStreamOptions): Promise<MediaStreamConfiguration> {
|
||||
@@ -277,7 +123,7 @@ export async function configureCodecs(console: Console, client: OnvifCameraAPI,
|
||||
if (videoOptions?.bitrateControl && vc.rateControl?.$?.ConstantBitRate !== undefined) {
|
||||
const constant = videoOptions?.bitrateControl === 'constant';
|
||||
if (vc.rateControl.$.ConstantBitRate !== constant)
|
||||
throw new Error(options.id + ': The camera video Bitrate Type must be set to ' + videoOptions?.bitrateControl + ' in the camera web admin.');
|
||||
throw new Error(options.id + ': camera video Bitrate Type must be manually set to ' + videoOptions?.bitrateControl + ' in the camera web admin.');
|
||||
}
|
||||
|
||||
if (videoOptions?.fps) {
|
||||
|
||||
367
plugins/onvif/xml-dumps/profiles.xml
Normal file
367
plugins/onvif/xml-dumps/profiles.xml
Normal file
@@ -0,0 +1,367 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<env:Envelope>
|
||||
<env:Body>
|
||||
<tr2:GetProfilesResponse>
|
||||
<tr2:Profiles token="Profile_1" fixed="true">
|
||||
<tr2:Name>mainStream</tr2:Name>
|
||||
<tr2:Configurations>
|
||||
<tr2:VideoSource token="VideoSourceToken">
|
||||
<tt:Name>VideoSourceConfig</tt:Name>
|
||||
<tt:UseCount>3</tt:UseCount>
|
||||
<tt:SourceToken>VideoSource_1</tt:SourceToken>
|
||||
<tt:Bounds x="0" y="0" width="3840" height="2160"></tt:Bounds>
|
||||
</tr2:VideoSource>
|
||||
<tr2:AudioSource token="AudioSourceConfigToken">
|
||||
<tt:Name>AudioSourceConfig</tt:Name>
|
||||
<tt:UseCount>3</tt:UseCount>
|
||||
<tt:SourceToken>AudioSourceChannel</tt:SourceToken>
|
||||
</tr2:AudioSource>
|
||||
<tr2:VideoEncoder token="VideoEncoderToken_1" GovLength="60" Profile="Main">
|
||||
<tt:Name>VideoEncoder_1</tt:Name>
|
||||
<tt:UseCount>1</tt:UseCount>
|
||||
<tt:Encoding>H264</tt:Encoding>
|
||||
<tt:Resolution>
|
||||
<tt:Width>3840</tt:Width>
|
||||
<tt:Height>2160</tt:Height>
|
||||
</tt:Resolution>
|
||||
<tt:RateControl ConstantBitRate="false">
|
||||
<tt:FrameRateLimit>20.000000</tt:FrameRateLimit>
|
||||
<tt:BitrateLimit>2048</tt:BitrateLimit>
|
||||
</tt:RateControl>
|
||||
<tt:Multicast>
|
||||
<tt:Address>
|
||||
<tt:Type>IPv4</tt:Type>
|
||||
<tt:IPv4Address>0.0.0.0</tt:IPv4Address>
|
||||
</tt:Address>
|
||||
<tt:Port>8600</tt:Port>
|
||||
<tt:TTL>128</tt:TTL>
|
||||
<tt:AutoStart>false</tt:AutoStart>
|
||||
</tt:Multicast>
|
||||
<tt:Quality>5.000000</tt:Quality>
|
||||
</tr2:VideoEncoder>
|
||||
<tr2:AudioEncoder token="MainAudioEncoderToken">
|
||||
<tt:Name>AudioEncoderConfig</tt:Name>
|
||||
<tt:UseCount>3</tt:UseCount>
|
||||
<tt:Encoding>PCMU</tt:Encoding>
|
||||
<tt:Multicast>
|
||||
<tt:Address>
|
||||
<tt:Type>IPv4</tt:Type>
|
||||
<tt:IPv4Address>0.0.0.0</tt:IPv4Address>
|
||||
</tt:Address>
|
||||
<tt:Port>8602</tt:Port>
|
||||
<tt:TTL>128</tt:TTL>
|
||||
<tt:AutoStart>false</tt:AutoStart>
|
||||
</tt:Multicast>
|
||||
<tt:Bitrate>64</tt:Bitrate>
|
||||
<tt:SampleRate>8</tt:SampleRate>
|
||||
</tr2:AudioEncoder>
|
||||
<tr2:Analytics token="VideoAnalyticsToken">
|
||||
<tt:Name>VideoAnalyticsName</tt:Name>
|
||||
<tt:UseCount>3</tt:UseCount>
|
||||
<tt:AnalyticsEngineConfiguration>
|
||||
<tt:AnalyticsModule Name="MyCellMotionModule" Type="tt:CellMotionEngine">
|
||||
<tt:Parameters>
|
||||
<tt:SimpleItem Name="Sensitivity" Value="60" />
|
||||
<tt:ElementItem Name="Layout">
|
||||
<tt:CellLayout Columns="22" Rows="15">
|
||||
<tt:Transformation>
|
||||
<tt:Translate x="-1.000000" y="-1.000000" />
|
||||
<tt:Scale x="0.090909" y="0.133333" />
|
||||
</tt:Transformation>
|
||||
</tt:CellLayout>
|
||||
</tt:ElementItem>
|
||||
</tt:Parameters>
|
||||
</tt:AnalyticsModule>
|
||||
<tt:AnalyticsModule Name="MyTamperDetecModule"
|
||||
Type="extxsd:TamperEngine">
|
||||
<tt:Parameters>
|
||||
<tt:SimpleItem Name="Sensitivity" Value="0" />
|
||||
<tt:ElementItem Name="Transformation">
|
||||
<tt:Transformation>
|
||||
<tt:Translate x="-1.000000" y="-1.000000" />
|
||||
<tt:Scale x="0.002841" y="0.004167" />
|
||||
</tt:Transformation>
|
||||
</tt:ElementItem>
|
||||
<tt:ElementItem Name="Field">
|
||||
<tt:PolygonConfiguration>
|
||||
<tt:Polygon>
|
||||
<tt:Point x="0" y="0" />
|
||||
<tt:Point x="0" y="480" />
|
||||
<tt:Point x="704" y="480" />
|
||||
<tt:Point x="704" y="0" />
|
||||
</tt:Polygon>
|
||||
</tt:PolygonConfiguration>
|
||||
</tt:ElementItem>
|
||||
</tt:Parameters>
|
||||
</tt:AnalyticsModule>
|
||||
</tt:AnalyticsEngineConfiguration>
|
||||
<tt:RuleEngineConfiguration>
|
||||
<tt:Rule Name="MyMotionDetectorRule" Type="tt:CellMotionDetector">
|
||||
<tt:Parameters>
|
||||
<tt:SimpleItem Name="MinCount" Value="5" />
|
||||
<tt:SimpleItem Name="AlarmOnDelay" Value="1000" />
|
||||
<tt:SimpleItem Name="AlarmOffDelay" Value="1000" />
|
||||
<tt:SimpleItem Name="ActiveCells" Value="2P8AAA==" />
|
||||
</tt:Parameters>
|
||||
</tt:Rule>
|
||||
<tt:Rule Name="MyTamperDetectorRule" Type="extxsd:TamperDetector">
|
||||
<tt:Parameters>
|
||||
<tt:ElementItem Name="Field">
|
||||
<tt:PolygonConfiguration>
|
||||
<tt:Polygon>
|
||||
<tt:Point x="0" y="0" />
|
||||
<tt:Point x="0" y="0" />
|
||||
<tt:Point x="0" y="0" />
|
||||
<tt:Point x="0" y="0" />
|
||||
</tt:Polygon>
|
||||
</tt:PolygonConfiguration>
|
||||
</tt:ElementItem>
|
||||
</tt:Parameters>
|
||||
</tt:Rule>
|
||||
</tt:RuleEngineConfiguration>
|
||||
</tr2:Analytics>
|
||||
</tr2:Configurations>
|
||||
</tr2:Profiles>
|
||||
<tr2:Profiles token="Profile_2" fixed="true">
|
||||
<tr2:Name>subStream</tr2:Name>
|
||||
<tr2:Configurations>
|
||||
<tr2:VideoSource token="VideoSourceToken">
|
||||
<tt:Name>VideoSourceConfig</tt:Name>
|
||||
<tt:UseCount>3</tt:UseCount>
|
||||
<tt:SourceToken>VideoSource_1</tt:SourceToken>
|
||||
<tt:Bounds x="0" y="0" width="3840" height="2160"></tt:Bounds>
|
||||
</tr2:VideoSource>
|
||||
<tr2:AudioSource token="AudioSourceConfigToken">
|
||||
<tt:Name>AudioSourceConfig</tt:Name>
|
||||
<tt:UseCount>3</tt:UseCount>
|
||||
<tt:SourceToken>AudioSourceChannel</tt:SourceToken>
|
||||
</tr2:AudioSource>
|
||||
<tr2:VideoEncoder token="VideoEncoderToken_2" GovLength="60" Profile="Main">
|
||||
<tt:Name>VideoEncoder_2</tt:Name>
|
||||
<tt:UseCount>1</tt:UseCount>
|
||||
<tt:Encoding>H264</tt:Encoding>
|
||||
<tt:Resolution>
|
||||
<tt:Width>640</tt:Width>
|
||||
<tt:Height>360</tt:Height>
|
||||
</tt:Resolution>
|
||||
<tt:RateControl ConstantBitRate="false">
|
||||
<tt:FrameRateLimit>30.000000</tt:FrameRateLimit>
|
||||
<tt:BitrateLimit>256</tt:BitrateLimit>
|
||||
</tt:RateControl>
|
||||
<tt:Multicast>
|
||||
<tt:Address>
|
||||
<tt:Type>IPv4</tt:Type>
|
||||
<tt:IPv4Address>0.0.0.0</tt:IPv4Address>
|
||||
</tt:Address>
|
||||
<tt:Port>8606</tt:Port>
|
||||
<tt:TTL>128</tt:TTL>
|
||||
<tt:AutoStart>false</tt:AutoStart>
|
||||
</tt:Multicast>
|
||||
<tt:Quality>3.000000</tt:Quality>
|
||||
</tr2:VideoEncoder>
|
||||
<tr2:AudioEncoder token="MainAudioEncoderToken">
|
||||
<tt:Name>AudioEncoderConfig</tt:Name>
|
||||
<tt:UseCount>3</tt:UseCount>
|
||||
<tt:Encoding>PCMU</tt:Encoding>
|
||||
<tt:Multicast>
|
||||
<tt:Address>
|
||||
<tt:Type>IPv4</tt:Type>
|
||||
<tt:IPv4Address>0.0.0.0</tt:IPv4Address>
|
||||
</tt:Address>
|
||||
<tt:Port>8602</tt:Port>
|
||||
<tt:TTL>128</tt:TTL>
|
||||
<tt:AutoStart>false</tt:AutoStart>
|
||||
</tt:Multicast>
|
||||
<tt:Bitrate>64</tt:Bitrate>
|
||||
<tt:SampleRate>8</tt:SampleRate>
|
||||
</tr2:AudioEncoder>
|
||||
<tr2:Analytics token="VideoAnalyticsToken">
|
||||
<tt:Name>VideoAnalyticsName</tt:Name>
|
||||
<tt:UseCount>3</tt:UseCount>
|
||||
<tt:AnalyticsEngineConfiguration>
|
||||
<tt:AnalyticsModule Name="MyCellMotionModule" Type="tt:CellMotionEngine">
|
||||
<tt:Parameters>
|
||||
<tt:SimpleItem Name="Sensitivity" Value="60" />
|
||||
<tt:ElementItem Name="Layout">
|
||||
<tt:CellLayout Columns="22" Rows="15">
|
||||
<tt:Transformation>
|
||||
<tt:Translate x="-1.000000" y="-1.000000" />
|
||||
<tt:Scale x="0.090909" y="0.133333" />
|
||||
</tt:Transformation>
|
||||
</tt:CellLayout>
|
||||
</tt:ElementItem>
|
||||
</tt:Parameters>
|
||||
</tt:AnalyticsModule>
|
||||
<tt:AnalyticsModule Name="MyTamperDetecModule"
|
||||
Type="extxsd:TamperEngine">
|
||||
<tt:Parameters>
|
||||
<tt:SimpleItem Name="Sensitivity" Value="0" />
|
||||
<tt:ElementItem Name="Transformation">
|
||||
<tt:Transformation>
|
||||
<tt:Translate x="-1.000000" y="-1.000000" />
|
||||
<tt:Scale x="0.002841" y="0.004167" />
|
||||
</tt:Transformation>
|
||||
</tt:ElementItem>
|
||||
<tt:ElementItem Name="Field">
|
||||
<tt:PolygonConfiguration>
|
||||
<tt:Polygon>
|
||||
<tt:Point x="0" y="0" />
|
||||
<tt:Point x="0" y="480" />
|
||||
<tt:Point x="704" y="480" />
|
||||
<tt:Point x="704" y="0" />
|
||||
</tt:Polygon>
|
||||
</tt:PolygonConfiguration>
|
||||
</tt:ElementItem>
|
||||
</tt:Parameters>
|
||||
</tt:AnalyticsModule>
|
||||
</tt:AnalyticsEngineConfiguration>
|
||||
<tt:RuleEngineConfiguration>
|
||||
<tt:Rule Name="MyMotionDetectorRule" Type="tt:CellMotionDetector">
|
||||
<tt:Parameters>
|
||||
<tt:SimpleItem Name="MinCount" Value="5" />
|
||||
<tt:SimpleItem Name="AlarmOnDelay" Value="1000" />
|
||||
<tt:SimpleItem Name="AlarmOffDelay" Value="1000" />
|
||||
<tt:SimpleItem Name="ActiveCells" Value="2P8AAA==" />
|
||||
</tt:Parameters>
|
||||
</tt:Rule>
|
||||
<tt:Rule Name="MyTamperDetectorRule" Type="extxsd:TamperDetector">
|
||||
<tt:Parameters>
|
||||
<tt:ElementItem Name="Field">
|
||||
<tt:PolygonConfiguration>
|
||||
<tt:Polygon>
|
||||
<tt:Point x="0" y="0" />
|
||||
<tt:Point x="0" y="0" />
|
||||
<tt:Point x="0" y="0" />
|
||||
<tt:Point x="0" y="0" />
|
||||
</tt:Polygon>
|
||||
</tt:PolygonConfiguration>
|
||||
</tt:ElementItem>
|
||||
</tt:Parameters>
|
||||
</tt:Rule>
|
||||
</tt:RuleEngineConfiguration>
|
||||
</tr2:Analytics>
|
||||
</tr2:Configurations>
|
||||
</tr2:Profiles>
|
||||
<tr2:Profiles token="Profile_3" fixed="true">
|
||||
<tr2:Name>thirdStream</tr2:Name>
|
||||
<tr2:Configurations>
|
||||
<tr2:VideoSource token="VideoSourceToken">
|
||||
<tt:Name>VideoSourceConfig</tt:Name>
|
||||
<tt:UseCount>3</tt:UseCount>
|
||||
<tt:SourceToken>VideoSource_1</tt:SourceToken>
|
||||
<tt:Bounds x="0" y="0" width="3840" height="2160"></tt:Bounds>
|
||||
</tr2:VideoSource>
|
||||
<tr2:AudioSource token="AudioSourceConfigToken">
|
||||
<tt:Name>AudioSourceConfig</tt:Name>
|
||||
<tt:UseCount>3</tt:UseCount>
|
||||
<tt:SourceToken>AudioSourceChannel</tt:SourceToken>
|
||||
</tr2:AudioSource>
|
||||
<tr2:VideoEncoder token="VideoEncoderToken_3" GovLength="60" Profile="Main">
|
||||
<tt:Name>VideoEncoder_3</tt:Name>
|
||||
<tt:UseCount>1</tt:UseCount>
|
||||
<tt:Encoding>H264</tt:Encoding>
|
||||
<tt:Resolution>
|
||||
<tt:Width>1280</tt:Width>
|
||||
<tt:Height>720</tt:Height>
|
||||
</tt:Resolution>
|
||||
<tt:RateControl ConstantBitRate="false">
|
||||
<tt:FrameRateLimit>30.000000</tt:FrameRateLimit>
|
||||
<tt:BitrateLimit>512</tt:BitrateLimit>
|
||||
</tt:RateControl>
|
||||
<tt:Multicast>
|
||||
<tt:Address>
|
||||
<tt:Type>IPv4</tt:Type>
|
||||
<tt:IPv4Address>0.0.0.0</tt:IPv4Address>
|
||||
</tt:Address>
|
||||
<tt:Port>8612</tt:Port>
|
||||
<tt:TTL>128</tt:TTL>
|
||||
<tt:AutoStart>false</tt:AutoStart>
|
||||
</tt:Multicast>
|
||||
<tt:Quality>3.000000</tt:Quality>
|
||||
</tr2:VideoEncoder>
|
||||
<tr2:AudioEncoder token="MainAudioEncoderToken">
|
||||
<tt:Name>AudioEncoderConfig</tt:Name>
|
||||
<tt:UseCount>3</tt:UseCount>
|
||||
<tt:Encoding>PCMU</tt:Encoding>
|
||||
<tt:Multicast>
|
||||
<tt:Address>
|
||||
<tt:Type>IPv4</tt:Type>
|
||||
<tt:IPv4Address>0.0.0.0</tt:IPv4Address>
|
||||
</tt:Address>
|
||||
<tt:Port>8602</tt:Port>
|
||||
<tt:TTL>128</tt:TTL>
|
||||
<tt:AutoStart>false</tt:AutoStart>
|
||||
</tt:Multicast>
|
||||
<tt:Bitrate>64</tt:Bitrate>
|
||||
<tt:SampleRate>8</tt:SampleRate>
|
||||
</tr2:AudioEncoder>
|
||||
<tr2:Analytics token="VideoAnalyticsToken">
|
||||
<tt:Name>VideoAnalyticsName</tt:Name>
|
||||
<tt:UseCount>3</tt:UseCount>
|
||||
<tt:AnalyticsEngineConfiguration>
|
||||
<tt:AnalyticsModule Name="MyCellMotionModule" Type="tt:CellMotionEngine">
|
||||
<tt:Parameters>
|
||||
<tt:SimpleItem Name="Sensitivity" Value="60" />
|
||||
<tt:ElementItem Name="Layout">
|
||||
<tt:CellLayout Columns="22" Rows="15">
|
||||
<tt:Transformation>
|
||||
<tt:Translate x="-1.000000" y="-1.000000" />
|
||||
<tt:Scale x="0.090909" y="0.133333" />
|
||||
</tt:Transformation>
|
||||
</tt:CellLayout>
|
||||
</tt:ElementItem>
|
||||
</tt:Parameters>
|
||||
</tt:AnalyticsModule>
|
||||
<tt:AnalyticsModule Name="MyTamperDetecModule"
|
||||
Type="extxsd:TamperEngine">
|
||||
<tt:Parameters>
|
||||
<tt:SimpleItem Name="Sensitivity" Value="0" />
|
||||
<tt:ElementItem Name="Transformation">
|
||||
<tt:Transformation>
|
||||
<tt:Translate x="-1.000000" y="-1.000000" />
|
||||
<tt:Scale x="0.002841" y="0.004167" />
|
||||
</tt:Transformation>
|
||||
</tt:ElementItem>
|
||||
<tt:ElementItem Name="Field">
|
||||
<tt:PolygonConfiguration>
|
||||
<tt:Polygon>
|
||||
<tt:Point x="0" y="0" />
|
||||
<tt:Point x="0" y="480" />
|
||||
<tt:Point x="704" y="480" />
|
||||
<tt:Point x="704" y="0" />
|
||||
</tt:Polygon>
|
||||
</tt:PolygonConfiguration>
|
||||
</tt:ElementItem>
|
||||
</tt:Parameters>
|
||||
</tt:AnalyticsModule>
|
||||
</tt:AnalyticsEngineConfiguration>
|
||||
<tt:RuleEngineConfiguration>
|
||||
<tt:Rule Name="MyMotionDetectorRule" Type="tt:CellMotionDetector">
|
||||
<tt:Parameters>
|
||||
<tt:SimpleItem Name="MinCount" Value="5" />
|
||||
<tt:SimpleItem Name="AlarmOnDelay" Value="1000" />
|
||||
<tt:SimpleItem Name="AlarmOffDelay" Value="1000" />
|
||||
<tt:SimpleItem Name="ActiveCells" Value="2P8AAA==" />
|
||||
</tt:Parameters>
|
||||
</tt:Rule>
|
||||
<tt:Rule Name="MyTamperDetectorRule" Type="extxsd:TamperDetector">
|
||||
<tt:Parameters>
|
||||
<tt:ElementItem Name="Field">
|
||||
<tt:PolygonConfiguration>
|
||||
<tt:Polygon>
|
||||
<tt:Point x="0" y="0" />
|
||||
<tt:Point x="0" y="0" />
|
||||
<tt:Point x="0" y="0" />
|
||||
<tt:Point x="0" y="0" />
|
||||
</tt:Polygon>
|
||||
</tt:PolygonConfiguration>
|
||||
</tt:ElementItem>
|
||||
</tt:Parameters>
|
||||
</tt:Rule>
|
||||
</tt:RuleEngineConfiguration>
|
||||
</tr2:Analytics>
|
||||
</tr2:Configurations>
|
||||
</tr2:Profiles>
|
||||
</tr2:GetProfilesResponse>
|
||||
</env:Body>
|
||||
</env:Envelope>
|
||||
@@ -26,10 +26,6 @@ export class RtspCamera extends CameraBase<UrlMediaStreamOptions> {
|
||||
};
|
||||
}
|
||||
|
||||
getChannelFromMediaStreamOptionsId(id: string) {
|
||||
return id.substring('channel'.length);
|
||||
}
|
||||
|
||||
getRawVideoStreamOptions(): UrlMediaStreamOptions[] {
|
||||
let urls: string[] = [];
|
||||
try {
|
||||
@@ -234,7 +230,7 @@ export abstract class RtspSmartCamera extends RtspCamera {
|
||||
|
||||
async putSetting(key: string, value: SettingValue) {
|
||||
this.putSettingBase(key, value);
|
||||
this.listener.then(l => l.emit('error', new Error("new settings")));
|
||||
this.listener?.then(l => l.emit('error', new Error("new settings")));
|
||||
}
|
||||
|
||||
async takePicture(options?: RequestPictureOptions) {
|
||||
|
||||
Reference in New Issue
Block a user