mirror of
https://github.com/koush/scrypted.git
synced 2026-02-03 06:03:27 +00:00
amcrest: wip auto configure
This commit is contained in:
82
plugins/amcrest/dumps/amcrest-face-detected.json
Normal file
82
plugins/amcrest/dumps/amcrest-face-detected.json
Normal 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
|
||||
}
|
||||
62
plugins/amcrest/dumps/amcrest-human-detected.json
Normal file
62
plugins/amcrest/dumps/amcrest-human-detected.json
Normal 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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
9
plugins/amcrest/src/amcrest-configure.ts
Normal file
9
plugins/amcrest/src/amcrest-configure.ts
Normal 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),
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -149,7 +149,6 @@ export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> e
|
||||
|
||||
getInterfaces() {
|
||||
return [
|
||||
ScryptedInterface.VideoCameraConfiguration,
|
||||
ScryptedInterface.VideoCamera,
|
||||
ScryptedInterface.Settings,
|
||||
...this.getAdditionalInterfaces()
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user