hikvision: wip autoconfigure

This commit is contained in:
Koushik Dutta
2024-08-09 21:39:43 -07:00
parent c2756a3a4a
commit 6379aa89ef
16 changed files with 1404 additions and 248 deletions

View 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
}
}

View File

@@ -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',

View File

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

View File

@@ -149,6 +149,7 @@ export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> e
getInterfaces() {
return [
ScryptedInterface.VideoCameraConfiguration,
ScryptedInterface.VideoCamera,
ScryptedInterface.Settings,
...this.getAdditionalInterfaces()

View File

@@ -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> {

View File

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

View 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
}

View File

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

View 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)
}
);
}

View File

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

View 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",
// ],
// },
// ],
// }

View File

@@ -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',

View File

@@ -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);

View File

@@ -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) {

View 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>

View File

@@ -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) {