amcrest: wip auto configure

This commit is contained in:
Koushik Dutta
2024-08-10 10:36:26 -07:00
parent 6379aa89ef
commit 5c69c70013
8 changed files with 425 additions and 272 deletions

View File

@@ -0,0 +1,82 @@
{
"CfgRuleId": 1,
"Class": "FaceDetection",
"CountInGroup": 2,
"DetectRegion": null,
"EventID": 10360,
"EventSeq": 6,
"Faces": [
{
"BoundingBox": [
1504,
2336,
1728,
2704
],
"Center": [
1616,
2520
],
"ObjectID": 94,
"ObjectType": "HumanFace",
"RelativeID": 0
}
],
"FrameSequence": 8251212,
"GroupID": 6,
"Mark": 0,
"Name": "FaceDetection",
"Object": {
"Action": "Appear",
"BoundingBox": [
1504,
2336,
1728,
2704
],
"Center": [
1616,
2520
],
"Confidence": 19,
"FrameSequence": 8251212,
"ObjectID": 94,
"ObjectType": "HumanFace",
"RelativeID": 0,
"SerialUUID": "",
"Source": 0.0,
"Speed": 0,
"SpeedTypeInternal": 0
},
"Objects": [
{
"Action": "Appear",
"BoundingBox": [
1504,
2336,
1728,
2704
],
"Center": [
1616,
2520
],
"Confidence": 19,
"FrameSequence": 8251212,
"ObjectID": 94,
"ObjectType": "HumanFace",
"RelativeID": 0,
"SerialUUID": "",
"Source": 0.0,
"Speed": 0,
"SpeedTypeInternal": 0
}
],
"PTS": 43774941350.0,
"Priority": 0,
"RuleID": 1,
"RuleId": 1,
"Source": -1280470024.0,
"UTC": 947510337,
"UTCMS": 0
}

View File

@@ -0,0 +1,62 @@
{
"Action": "Cross",
"Class": "Normal",
"CountInGroup": 1,
"DetectRegion": [
[
455,
260
],
[
3586,
260
],
[
3768,
7580
],
[
382,
7451
]
],
"Direction": "Enter",
"EventID": 10181,
"GroupID": 0,
"Name": "Rule1",
"Object": {
"Action": "Appear",
"BoundingBox": [
2856,
1280,
3880,
4880
],
"Center": [
3368,
3080
],
"Confidence": 0,
"LowerBodyColor": [
0,
0,
0,
0
],
"MainColor": [
0,
0,
0,
0
],
"ObjectID": 863,
"ObjectType": "Human",
"RelativeID": 0,
"Speed": 0
},
"PTS": 43380319830.0,
"RuleID": 2,
"Track": [],
"UTC": 1711446999,
"UTCMS": 701
}

View File

@@ -4,103 +4,10 @@ import { parseHeaders, readBody } from '@scrypted/common/src/rtsp-server';
import contentType from 'content-type';
import { IncomingMessage } from 'http';
import { EventEmitter, Readable } from 'stream';
import { Destroyable } from '../../rtsp/src/rtsp';
import { createRtspMediaStreamOptions, Destroyable, UrlMediaStreamOptions } from '../../rtsp/src/rtsp';
import { getDeviceInfo } from './probe';
import { Point } from '@scrypted/sdk';
import { MediaStreamConfiguration, MediaStreamOptions, Point } from '@scrypted/sdk';
// Human
// {
// "Action" : "Cross",
// "Class" : "Normal",
// "CountInGroup" : 1,
// "DetectRegion" : [
// [ 455, 260 ],
// [ 3586, 260 ],
// [ 3768, 7580 ],
// [ 382, 7451 ]
// ],
// "Direction" : "Enter",
// "EventID" : 10181,
// "GroupID" : 0,
// "Name" : "Rule1",
// "Object" : {
// "Action" : "Appear",
// "BoundingBox" : [ 2856, 1280, 3880, 4880 ],
// "Center" : [ 3368, 3080 ],
// "Confidence" : 0,
// "LowerBodyColor" : [ 0, 0, 0, 0 ],
// "MainColor" : [ 0, 0, 0, 0 ],
// "ObjectID" : 863,
// "ObjectType" : "Human",
// "RelativeID" : 0,
// "Speed" : 0
// },
// "PTS" : 43380319830.0,
// "RuleID" : 2,
// "Track" : [],
// "UTC" : 1711446999,
// "UTCMS" : 701
// }
// Face
// {
// "CfgRuleId" : 1,
// "Class" : "FaceDetection",
// "CountInGroup" : 2,
// "DetectRegion" : null,
// "EventID" : 10360,
// "EventSeq" : 6,
// "Faces" : [
// {
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
// "Center" : [ 1616, 2520 ],
// "ObjectID" : 94,
// "ObjectType" : "HumanFace",
// "RelativeID" : 0
// }
// ],
// "FrameSequence" : 8251212,
// "GroupID" : 6,
// "Mark" : 0,
// "Name" : "FaceDetection",
// "Object" : {
// "Action" : "Appear",
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
// "Center" : [ 1616, 2520 ],
// "Confidence" : 19,
// "FrameSequence" : 8251212,
// "ObjectID" : 94,
// "ObjectType" : "HumanFace",
// "RelativeID" : 0,
// "SerialUUID" : "",
// "Source" : 0.0,
// "Speed" : 0,
// "SpeedTypeInternal" : 0
// },
// "Objects" : [
// {
// "Action" : "Appear",
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
// "Center" : [ 1616, 2520 ],
// "Confidence" : 19,
// "FrameSequence" : 8251212,
// "ObjectID" : 94,
// "ObjectType" : "HumanFace",
// "RelativeID" : 0,
// "SerialUUID" : "",
// "Source" : 0.0,
// "Speed" : 0,
// "SpeedTypeInternal" : 0
// }
// ],
// "PTS" : 43774941350.0,
// "Priority" : 0,
// "RuleID" : 1,
// "RuleId" : 1,
// "Source" : -1280470024.0,
// "UTC" : 947510337,
// "UTCMS" : 0
// }
export interface AmcrestObjectDetails {
Action: string;
BoundingBox: Point;
@@ -174,6 +81,59 @@ async function readAmcrestMessage(client: Readable): Promise<string[]> {
}
}
function findValue(blob: string, prefix: string, key: string) {
const lines = blob.split('\n');
const value = lines.find(line => line.startsWith(`${prefix}.${key}`));
if (!value)
return;
const parts = value.split('=');
return parts[1];
}
function fromAmcrestAudioCodec(audioCodec: string) {
audioCodec = audioCodec
?.replace('.', '')?.toLowerCase()?.trim();
if (audioCodec?.includes('aac'))
audioCodec = 'aac';
else if (audioCodec?.includes('g711a'))
audioCodec = 'pcm_alaw';
else if (audioCodec?.includes('g711u'))
audioCodec = 'pcm_mulaw';
else if (audioCodec?.includes('g711'))
audioCodec = 'pcm';
return audioCodec;
}
function fromAmcrestVideoCodec(videoCodec: string) {
videoCodec = videoCodec
?.replace('.', '')?.toLowerCase()?.trim();
if (videoCodec?.includes('h264'))
videoCodec = 'h264';
else if (videoCodec?.includes('h265'))
videoCodec = 'h265';
return videoCodec;
}
const amcrestResolutions = {
"D1": [704, 480],
"HD1": [352, 480],
"BCIF": [704, 240],
"2CIF": [704, 240],
"CIF": [352, 240],
"QCIF": [176, 120],
"NHD": [640, 360],
"VGA": [640, 480],
"QVGA": [320, 240]
};
function fromAmcrestResolution(resolution: string) {
const named = amcrestResolutions[resolution];
if (named)
return named;
const parts = resolution.split('x');
return [parseInt(parts[0]), parseInt(parts[1])];
}
export class AmcrestCameraClient {
credential: AuthFetchCredentialState;
@@ -371,6 +331,7 @@ export class AmcrestCameraClient {
async unlock(): Promise<boolean> {
const response = await this.request({
// channel 1? this may fail through nvr.
url: `http://${this.ip}/cgi-bin/accessControl.cgi?action=openDoor&channel=1&UserID=101&Type=Remote`,
responseType: 'text',
});
@@ -379,9 +340,138 @@ export class AmcrestCameraClient {
async lock(): Promise<boolean> {
const response = await this.request({
// channel 1? this may fail through nvr.
url: `http://${this.ip}/cgi-bin/accessControl.cgi?action=closeDoor&channel=1&UserID=101&Type=Remote`,
responseType: 'text',
});
return response.body.includes('OK');
}
async configureCodecs(cameraNumber: number, options: MediaStreamConfiguration) {
if (!options.id?.startsWith('channel'))
throw new Error('invalid id');
const capsResponse = await this.request({
url: `http://${this.ip}/cgi-bin/encode.cgi?action=getConfigCaps&channel=${cameraNumber}`,
responseType: 'text',
});
this.console.log(capsResponse.body);
const formatNumber = Math.max(0, parseInt(options.id?.substring('channel'.length)) - 1);
const format = options.id === 'channel0' ? 'MainFormat' : 'ExtraFormat';
const encode = `Encode[${cameraNumber - 1}].${format}[${formatNumber}]`;
const params = new URLSearchParams();
if (options.video?.bitrate) {
let bitrate = options?.video?.bitrate;
bitrate = Math.round(bitrate / 1000);
params.set(`${encode}.Video.BitRate`, bitrate.toString());
}
if (options.video?.codec === 'h264') {
params.set(`${encode}.Video.Compression`, 'H.264');
params.set(`${encode}.VideoEnable`, 'true');
}
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');
}
if (options.video?.width && options.video?.height) {
params.set(`${encode}.Video.resolution`, `${options.video.width}x${options.video.height}`);
}
if (options.video?.fps) {
params.set(`${encode}.Video.FPS`, options.video.fps.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 === 'constant' ? 'CBR' : 'VBR');
}
if ([...params.keys()].length) {
const response = await this.request({
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
responseType: 'text',
});
this.console.log('reconfigure result', response.body);
}
const vsos = await this.getCodecs(cameraNumber);
const index = vsos.findIndex(vso => vso.id === options.id);
const vso: MediaStreamConfiguration = vsos[index];
const caps = `caps[${cameraNumber - 1}].${format}[${formatNumber}]`;
const resolutions = findValue(capsResponse.body, caps, 'Video.ResolutionTypes').split(',').map(fromAmcrestResolution);
const bitrates = findValue(capsResponse.body, caps, 'Video.BitRateOptions').split(',').map(s => parseInt(s) * 1000);
vso.video.resolutions = resolutions;
vso.video.bitrateRange = [bitrates[0], bitrates[bitrates.length - 1]];
return vso;
}
async getCodecs(cameraNumber: number): Promise<UrlMediaStreamOptions[]> {
const masResponse = await this.request({
url: `http://${this.ip}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
responseType: 'text',
})
const mas = masResponse.body.split('=')[1].trim();
// amcrest reports more streams than are acually available in its responses,
// so checking the max extra streams prevents usage of invalid streams.
const maxExtraStreams = parseInt(mas) || 1;
const vsos = [...Array(maxExtraStreams + 1).keys()].map(subtype => createRtspMediaStreamOptions(undefined, subtype));
const encodeResponse = await this.request({
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`,
responseType: 'text',
});
this.console.log(encodeResponse.body);
for (let i = 0; i < vsos.length; i++) {
const vso = vsos[i];
let encName: string;
if (i === 0) {
encName = `table.Encode[${cameraNumber - 1}].MainFormat[0]`;
}
else {
encName = `table.Encode[${cameraNumber - 1}].ExtraFormat[${i - 1}]`;
}
const videoCodec = fromAmcrestVideoCodec(findValue(encodeResponse.body, encName, 'Video.Compression'));
const audioCodec = fromAmcrestAudioCodec(findValue(encodeResponse.body, encName, 'Audio.Compression'));
if (vso.audio)
vso.audio.codec = audioCodec;
vso.video.codec = videoCodec;
const width = findValue(encodeResponse.body, encName, 'Video.Width');
const height = findValue(encodeResponse.body, encName, 'Video.Height');
if (width && height) {
vso.video.width = parseInt(width);
vso.video.height = parseInt(height);
}
const videoEnable = findValue(encodeResponse.body, encName, 'VideoEnable');
if (videoEnable?.trim() === 'false') {
this.console.warn('Video stream is disabled and should likely be enabled:', encName);
continue;
}
const encodeOptions = findValue(encodeResponse.body, encName, 'Video.BitRate');
if (!encodeOptions)
continue;
vso.video.bitrate = parseInt(encodeOptions) * 1000;
}
return vsos;
}
}

View File

@@ -0,0 +1,9 @@
import { autoconfigureCodecs as ac } from '../../../common/src/autoconfigure-codecs';
import { AmcrestCameraClient } from "./amcrest-api";
export function autoconfigureSettings(client: AmcrestCameraClient, cameraNumber: number) {
return ac(
() => client.getCodecs(cameraNumber),
options => client.configureCodecs(cameraNumber, options),
);
}

View File

@@ -1,26 +1,25 @@
import { automaticallyConfigureSettings } from "@scrypted/common/src/autoconfigure-codecs";
import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers';
import { readLength } from "@scrypted/common/src/read-stream";
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, Lock, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, Reboot, RequestPictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk";
import child_process, { ChildProcess } from 'child_process';
import { PassThrough, Readable, Stream } from "stream";
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { createRtspMediaStreamOptions, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { AmcrestCameraClient, AmcrestEvent, AmcrestEventData } from "./amcrest-api";
import { autoconfigureSettings } from "./amcrest-configure";
const { mediaManager } = sdk;
const AMCREST_DOORBELL_TYPE = 'Amcrest Doorbell';
const DAHUA_DOORBELL_TYPE = 'Dahua Doorbell';
function findValue(blob: string, prefix: string, key: string) {
const lines = blob.split('\n');
const value = lines.find(line => line.startsWith(`${prefix}.${key}`));
if (!value)
return;
const parts = value.split('=');
return parts[1];
}
const rtspChannelSetting: Setting = {
key: 'rtspChannel',
title: 'Channel Number Override',
description: "The channel number to use for snapshots and video. E.g., 1, 2, etc.",
placeholder: '1',
};
class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, Camera, Intercom, Lock, VideoRecorder, Reboot, ObjectDetector {
eventStream: Stream;
@@ -111,57 +110,9 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
async setVideoStreamOptions(options: MediaStreamOptions) {
if (!options.id?.startsWith('channel'))
throw new Error('invalid id');
const channel = parseInt(this.getRtspChannel()) || 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();
if (options.video?.bitrate) {
let bitrate = options?.video?.bitrate;
if (!bitrate)
return;
bitrate = Math.round(bitrate / 1000);
params.set(`${encode}.Video.BitRate`, bitrate.toString());
}
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');
}
if (options.video?.width && options.video?.height) {
params.set(`${encode}.Video.resolution`, `${options.video.width}x${options.video.height}`);
}
if (options.video?.fps) {
params.set(`${encode}.Video.FPS`, options.video.fps.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 === 'constant' ? 'CBR' : 'VBR');
}
if (![...params.keys()].length)
return;
const response = await this.getClient().request({
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
responseType: 'text',
});
this.console.log('reconfigure result', response.body);
return undefined;
const client = this.getClient();
return client.configureCodecs(channel, options);
}
getClient() {
@@ -382,8 +333,13 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
// },
);
const ac = {
...automaticallyConfigureSettings,
};
ac.type = 'button';
ret.push(ac);
return ret;
}
async takeSmartCameraPicture(options?: RequestPictureOptions): Promise<MediaObject> {
@@ -391,15 +347,13 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
async getUrlSettings() {
const rtspChannel = {
...rtspChannelSetting,
subgroup: 'Advanced',
value: this.storage.getItem('rtspChannel'),
};
return [
{
key: 'rtspChannel',
title: 'Channel Number Override',
subgroup: 'Advanced',
description: "The channel number to use for snapshots and video. E.g., 1, 2, etc.",
placeholder: '1',
value: this.storage.getItem('rtspChannel'),
},
rtspChannel,
...await super.getUrlSettings(),
]
}
@@ -409,7 +363,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
createRtspMediaStreamOptions(url: string, index: number) {
const ret = super.createRtspMediaStreamOptions(url, index);
const ret = createRtspMediaStreamOptions(url, index);
ret.tool = 'scrypted';
return ret;
}
@@ -419,93 +373,27 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
if (!this.videoStreamOptions) {
this.videoStreamOptions = (async () => {
let mas: string;
let vsos: UrlMediaStreamOptions[];
const cameraNumber = parseInt(this.getRtspChannel()) || 1;
try {
const response = await client.request({
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
responseType: 'text',
})
mas = response.body.split('=')[1].trim();
this.storage.setItem('maxExtraStreams', mas.toString());
}
catch (e) {
this.console.error('error retrieving max extra streams', e);
mas = this.storage.getItem('maxExtraStreams');
}
const maxExtraStreams = parseInt(mas) || 1;
const channel = parseInt(this.getRtspChannel()) || 1;
const vsos = [...Array(maxExtraStreams + 1).keys()].map(subtype => this.createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${channel}&subtype=${subtype}`, subtype));
try {
const capResponse = await client.request({
url: `http://${this.getHttpAddress()}/cgi-bin/encode.cgi?action=getConfigCaps&channel=0`,
responseType: 'text',
});
this.console.log(capResponse.body);
const encodeResponse = await client.request({
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`,
responseType: 'text',
});
this.console.log(encodeResponse.body);
for (let i = 0; i < vsos.length; i++) {
const vso = vsos[i];
let capName: string;
let encName: string;
if (i === 0) {
capName = `caps[${channel - 1}].MainFormat[0]`;
encName = `table.Encode[${channel - 1}].MainFormat[0]`;
}
else {
capName = `caps[${channel - 1}].ExtraFormat[${i - 1}]`;
encName = `table.Encode[${channel - 1}].ExtraFormat[${i - 1}]`;
}
const videoCodec = findValue(encodeResponse.body, encName, 'Video.Compression')
?.replace('.', '')?.toLowerCase()?.trim();
let audioCodec = findValue(encodeResponse.body, encName, 'Audio.Compression')
?.replace('.', '')?.toLowerCase()?.trim();
if (audioCodec?.includes('aac'))
audioCodec = 'aac';
else if (audioCodec?.includes('g711a'))
audioCodec = 'pcm_alaw';
else if (audioCodec?.includes('g711u'))
audioCodec = 'pcm_mulaw';
else if (audioCodec?.includes('g711'))
audioCodec = 'pcm';
if (vso.audio)
vso.audio.codec = audioCodec;
vso.video.codec = videoCodec;
const width = findValue(encodeResponse.body, encName, 'Video.Width');
const height = findValue(encodeResponse.body, encName, 'Video.Height');
if (width && height) {
vso.video.width = parseInt(width);
vso.video.height = parseInt(height);
}
const bitrateOptions = findValue(capResponse.body, capName, 'Video.BitRateOptions');
if (!bitrateOptions)
continue;
const encodeOptions = findValue(encodeResponse.body, encName, 'Video.BitRate');
if (!encodeOptions)
continue;
const [min, max] = bitrateOptions.split(',');
if (!min || !max)
continue;
vso.video.bitrate = parseInt(encodeOptions) * 1000;
vso.video.maxBitrate = parseInt(max) * 1000;
vso.video.minBitrate = parseInt(min) * 1000;
try {
vsos = await client.getCodecs(cameraNumber);
this.storage.setItem('vsosJSON', JSON.stringify(vsos));
}
catch (e) {
this.console.error('error retrieving stream configurations', e);
vsos = JSON.parse(this.storage.getItem('vsosJSON')) as UrlMediaStreamOptions[];
}
for (const [index, vso] of vsos.entries()) {
vso.url = `rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${cameraNumber}&subtype=${index}`;
}
return vsos;
}
catch (e) {
this.console.error('error retrieving stream configurations', e);
}
vsos = [...Array(2).keys()].map(subtype => this.createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${cameraNumber}&subtype=${subtype}`, subtype));
return vsos;
})();
}
@@ -547,6 +435,19 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
async putSetting(key: string, value: string) {
if (key === automaticallyConfigureSettings.key) {
const client = this.getClient();
autoconfigureSettings(client, parseInt(this.getRtspChannel()) || 1)
.then(() => {
this.log.a('Successfully configured settings.');
})
.catch(e => {
this.log.a('There was an error automatically configuring settings. More information can be viewed in the console.');
this.console.error('error autoconfiguring', e);
});
return;
}
if (key === 'continuousRecording') {
if (value === 'true') {
try {
@@ -588,7 +489,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
// not sure if this all works, since i don't actually have a doorbell.
// good luck!
const channel = this.getRtspChannel() || '1';
const channel = parseInt(this.getRtspChannel()) || 1;
const buffer = await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput);
const ffmpegInput = JSON.parse(buffer.toString()) as FFmpegInput;
@@ -728,8 +629,14 @@ class AmcrestProvider extends RtspProvider {
const password = settings.password?.toString();
const skipValidate = settings.skipValidate?.toString() === 'true';
let twoWayAudio: string;
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
if (settings.autoconfigure) {
const cameraNumber = parseInt(settings.rtspChannel as string) || 1;
await autoconfigureSettings(api, cameraNumber);
}
if (!skipValidate) {
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
try {
const deviceInfo = await api.getDeviceInfo();
@@ -760,8 +667,10 @@ class AmcrestProvider extends RtspProvider {
device.info = info;
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());
device.setIPAddress(settings.ip?.toString());
if (twoWayAudio)
device.putSetting('twoWayAudio', twoWayAudio);
device.updateDeviceInfo();
@@ -784,12 +693,14 @@ class AmcrestProvider extends RtspProvider {
title: 'IP Address',
placeholder: '192.168.2.222',
},
rtspChannelSetting,
{
key: 'httpPort',
title: 'HTTP Port',
description: 'Optional: Override the HTTP Port from the default value of 80',
placeholder: '80',
},
automaticallyConfigureSettings,
{
key: 'skipValidate',
title: 'Skip Validation',
@@ -802,6 +713,7 @@ class AmcrestProvider extends RtspProvider {
createCamera(nativeId: string) {
return new AmcrestCamera(nativeId, this);
}
}
export default AmcrestProvider;

View File

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

View File

@@ -1,16 +1,15 @@
import { automaticallyConfigureSettings } from "@scrypted/common/src/autoconfigure-codecs";
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, Reboot, RequestPictureOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration } from "@scrypted/sdk";
import crypto from 'crypto';
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 { OnvifIntercom } from "../../onvif/src/onvif-intercom";
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { createRtspMediaStreamOptions, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
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";
import { detectionMap, HikvisionCameraAPI, HikvisionCameraEvent } from "./hikvision-camera-api";
const rtspChannelSetting: Setting = {
key: 'rtspChannel',
@@ -364,7 +363,7 @@ export class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom
for (const [id, channel] of detectedChannels.entries()) {
if (cameraNumber && channelToCameraNumber(id) !== cameraNumber)
continue;
const mso = this.createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/ISAPI/Streaming/channels/${id}/${params}`, index++);
const mso = createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/ISAPI/Streaming/channels/${id}/${params}`, index++);
Object.assign(mso.video, channel?.video);
mso.tool = 'scrypted';
ret.push(mso);
@@ -621,6 +620,7 @@ class HikvisionProvider extends RtspProvider {
getAdditionalInterfaces() {
return [
ScryptedInterface.VideoCameraConfiguration,
ScryptedInterface.Reboot,
ScryptedInterface.Camera,
ScryptedInterface.MotionSensor,
@@ -693,10 +693,10 @@ class HikvisionProvider extends RtspProvider {
device.info = info;
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());
device.setIPAddress(settings.ip?.toString());
if (twoWayAudio)
device.putSetting('twoWayAudio', twoWayAudio);
device.updateDeviceInfo();
@@ -714,12 +714,12 @@ class HikvisionProvider extends RtspProvider {
title: 'Password',
type: 'password',
},
rtspChannelSetting,
{
key: 'ip',
title: 'IP Address',
placeholder: '192.168.2.222',
},
rtspChannelSetting,
{
key: 'httpPort',
title: 'HTTP Port',

View File

@@ -7,25 +7,24 @@ export { UrlMediaStreamOptions } from "../../ffmpeg-camera/src/common";
const { mediaManager } = sdk;
export function createRtspMediaStreamOptions(url: string, index: number): UrlMediaStreamOptions {
return {
id: `channel${index}`,
name: `Stream ${index + 1}`,
url,
container: 'rtsp',
video: {
},
audio: {
},
};
}
export class RtspCamera extends CameraBase<UrlMediaStreamOptions> {
takePicture(option?: PictureOptions): Promise<MediaObject> {
throw new Error("The RTSP Camera does not provide snapshots. Install the Snapshot Plugin if snapshots are available via an URL.");
}
createRtspMediaStreamOptions(url: string, index: number): UrlMediaStreamOptions {
return {
id: `channel${index}`,
name: `Stream ${index + 1}`,
url,
container: 'rtsp',
video: {
},
audio: {
},
};
}
getRawVideoStreamOptions(): UrlMediaStreamOptions[] {
let urls: string[] = [];
try {
@@ -41,7 +40,7 @@ export class RtspCamera extends CameraBase<UrlMediaStreamOptions> {
}
// filter out empty strings.
const ret = urls.filter(url => !!url).map((url, index) => this.createRtspMediaStreamOptions(url, index));
const ret = urls.filter(url => !!url).map((url, index) => createRtspMediaStreamOptions(url, index));
if (!ret.length)
return;