mirror of
https://github.com/koush/scrypted.git
synced 2026-02-09 08:42:19 +00:00
homekit/amcrest: dynamic streaming
This commit is contained in:
@@ -18,7 +18,6 @@ The optimal/reliable codec settings can be found in the documentation for the [H
|
||||
* Specify `Type` is `Doorbell` (at top under device Name)
|
||||
* `Username` admin
|
||||
* `Password` (see below)
|
||||
* `Default Stream` set to properly configured video codec stream (Main Stream = `Stream 1`; Sub Stream 1 = `Stream 2`; Sub Stream 2 = `Stream 3`; and so on)
|
||||
* `Doorbell Type` is `Amcrest Doorbell`
|
||||
|
||||
The `admin` user account credentials is required to (1) add doorbell to Scrypted or (2) change codec settings with `IP Config Software` or `Amcrest Surveillance Pro` applications.
|
||||
@@ -36,8 +35,6 @@ Each 'Channel' or (camera) Device attached to the NVR must be configured as sepa
|
||||
* `IP Address` NVR's IP Address
|
||||
* `Snapshot URL Override` camera's IP address (preferred) or specific port number of NVR for that camera (may work). That is: `http://<camera IP address>/cgi-bin/snapshot.cgi` or `http://<NVR IP address>:<NVR port # for camera>/cgi-bin/snapshot.cgi`
|
||||
* `Channel Number Override` camera's channel number as known to DVR
|
||||
* `Default Stream` Properly configured video codec stream (Main Stream = `Stream 1`; Sub Stream 1 = `Stream 2`; Sub Stream 2 = `Stream 3`; and so on)
|
||||
|
||||
|
||||
|
||||
# Troubleshooting
|
||||
@@ -45,7 +42,6 @@ Each 'Channel' or (camera) Device attached to the NVR must be configured as sepa
|
||||
* Is the URL attempting to use HTTPS? Try disabling HTTPS on the device to see if that resolves issue (do not use self-signed certs).
|
||||
* Does your account (`Username`) have proper permissions ("Authority" in Amcrest speak)? Try granting all Authority for testing. See below `User Account Authority (Camera or NVR)`.
|
||||
* Amcrest Doorbell: `Username` is **admin** and `Password` is the device/camera password -- not Amcrest Smart Home (Cloud) account password.
|
||||
* Check that you have specified the correct `Default Stream` number in device (in Scrypted).
|
||||
* Check that you have configured the correct Stream number's codec settings (in Amcrest admin page (Main Stream or Sub Stream(s)).
|
||||
|
||||
## User Account Authority (Camera or NVR)
|
||||
|
||||
77
plugins/amcrest/package-lock.json
generated
77
plugins/amcrest/package-lock.json
generated
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.86",
|
||||
"version": "0.0.89",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.86",
|
||||
"version": "0.0.89",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/multiparty": "^0.0.33",
|
||||
"multiparty": "^4.2.2"
|
||||
@@ -18,18 +19,27 @@
|
||||
"@types/node": "^16.11.0"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.134",
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.14.5",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.14.5",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.15.4",
|
||||
"@babel/plugin-transform-typescript": "^7.15.8",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@koush/werift": "file:../external/werift/packages/webrtc",
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.174",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
@@ -39,7 +49,6 @@
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"webpack": "^5.59.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -56,7 +65,8 @@
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.22.8",
|
||||
"typescript-json-schema": "^0.50.1"
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
@@ -71,6 +81,10 @@
|
||||
"axios": "^0.21.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
"resolved": "../../common",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
@@ -110,9 +124,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -231,16 +245,21 @@
|
||||
"axios": "^0.21.4"
|
||||
}
|
||||
},
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@koush/werift": "file:../external/werift/packages/webrtc",
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@types/node": "^16.9.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.14.5",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.14.5",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.15.4",
|
||||
"@babel/plugin-transform-typescript": "^7.15.8",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@types/node": "^16.11.1",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
@@ -253,11 +272,11 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.22.8",
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"webpack": "^5.59.0"
|
||||
"webpack": "^5.59.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"@types/multiparty": {
|
||||
@@ -292,9 +311,9 @@
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.8.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.86",
|
||||
"version": "0.0.89",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -35,6 +35,7 @@
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@types/multiparty": "^0.0.33",
|
||||
"multiparty": "^4.2.2"
|
||||
},
|
||||
|
||||
@@ -4,22 +4,34 @@ import { AmcrestCameraClient, AmcrestEvent, amcrestHttpsAgent } from "./amcrest-
|
||||
import { RtspSmartCamera, RtspProvider, Destroyable, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
import { EventEmitter } from "stream";
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { ffmpegLogInitialOutput } from '../../../common/src/media-helpers';
|
||||
import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers';
|
||||
import net from 'net';
|
||||
import { listenZero } from "../../../common/src/listen-cluster";
|
||||
import { readLength } from "../../../common/src/read-stream";
|
||||
import { listenZero } from "@scrypted/common/src/listen-cluster";
|
||||
import { readLength } from "@scrypted/common/src/read-stream";
|
||||
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
|
||||
import { parse } from "path";
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, Camera, Intercom {
|
||||
eventStream: Stream;
|
||||
cp: ChildProcess;
|
||||
client: AmcrestCameraClient;
|
||||
maxExtraStreams: number;
|
||||
maxExtraStreams: Promise<number>;
|
||||
videoStreamOptions: Promise<UrlMediaStreamOptions[]>;
|
||||
onvifIntercom = new OnvifIntercom(this);
|
||||
|
||||
constructor(nativeId: string, provider: RtspProvider) {
|
||||
@@ -189,7 +201,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
description: 'Amcrest cameras may support both Amcrest and ONVIF two way audio protocols. ONVIF generally performs better when supported.',
|
||||
choices,
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -223,27 +235,88 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
async getConstructedVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
|
||||
let mas = this.maxExtraStreams;
|
||||
const client = this.getClient();
|
||||
|
||||
if (!this.maxExtraStreams) {
|
||||
const client = this.getClient();
|
||||
try {
|
||||
const response = await client.digestAuth.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
|
||||
responseType: 'text',
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
})
|
||||
this.maxExtraStreams = parseInt(response.data.split('=')[1].trim());
|
||||
mas = this.maxExtraStreams;
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('error retrieving max extra streams', e);
|
||||
}
|
||||
this.maxExtraStreams = (async () => {
|
||||
let mas: string;
|
||||
try {
|
||||
const response = await client.digestAuth.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
|
||||
responseType: 'text',
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
})
|
||||
mas = response.data.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');
|
||||
this.maxExtraStreams = undefined;
|
||||
}
|
||||
return parseInt(mas) || 1;
|
||||
})();
|
||||
}
|
||||
mas = mas || 1;
|
||||
const channel = this.getRtspChannel() || '1';
|
||||
return [...Array(mas + 1).keys()].map(subtype => this.createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${channel}&subtype=${subtype}`, subtype));
|
||||
|
||||
if (!this.videoStreamOptions) {
|
||||
this.videoStreamOptions = (async () => {
|
||||
const mas = await this.maxExtraStreams;
|
||||
const channel = parseInt(this.getRtspChannel()) || 1;
|
||||
const vsos = [...Array(mas + 1).keys()].map(subtype => this.createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${channel}&subtype=${subtype}`, subtype));
|
||||
|
||||
try {
|
||||
const capResponse = await client.digestAuth.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/encode.cgi?action=getConfigCaps&channel=0`,
|
||||
responseType: 'text',
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
});
|
||||
this.console.log(capResponse.data);
|
||||
const encodeResponse = await client.digestAuth.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`,
|
||||
responseType: 'text',
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
});
|
||||
this.console.log(encodeResponse.data);
|
||||
|
||||
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 bitrateOptions = findValue(capResponse.data, capName, 'Video.BitRateOptions');
|
||||
if (!bitrateOptions)
|
||||
continue;
|
||||
|
||||
const encodeOptions = findValue(encodeResponse.data, 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;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('error retrieving stream configurations', e);
|
||||
}
|
||||
|
||||
return vsos;
|
||||
})();
|
||||
}
|
||||
|
||||
return this.videoStreamOptions;
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string) {
|
||||
|
||||
4
plugins/homekit/package-lock.json
generated
4
plugins/homekit/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "0.0.224",
|
||||
"version": "0.0.226",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "0.0.224",
|
||||
"version": "0.0.226",
|
||||
"dependencies": {
|
||||
"@koush/qrcode-terminal": "^0.12.0",
|
||||
"hap-nodejs": "file:../../external/HAP-NodeJS",
|
||||
|
||||
@@ -39,5 +39,5 @@
|
||||
"@types/node": "^14.17.9",
|
||||
"@types/url-parse": "^1.4.3"
|
||||
},
|
||||
"version": "0.0.224"
|
||||
"version": "0.0.226"
|
||||
}
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
import sdk, { Camera, Intercom, MediaStreamOptions, ScryptedDevice, ScryptedInterface, VideoCamera, VideoCameraConfiguration } from '@scrypted/sdk';
|
||||
import dgram, { SocketType } from 'dgram';
|
||||
import { once } from 'events';
|
||||
import os from 'os';
|
||||
import { RtcpRrPacket } from '../../../../../external/werift/packages/rtp/src/rtcp/rr';
|
||||
import { RtcpPacketConverter } from '../../../../../external/werift/packages/rtp/src/rtcp/rtcp';
|
||||
import { RtpPacket } from '../../../../../external/werift/packages/rtp/src/rtp/rtp';
|
||||
import { ProtectionProfileAes128CmHmacSha1_80 } from '../../../../../external/werift/packages/rtp/src/srtp/const';
|
||||
import { SrtcpSession } from '../../../../../external/werift/packages/rtp/src/srtp/srtcp';
|
||||
import { HomeKitSession } from '../../common';
|
||||
import { CameraController, CameraStreamingDelegate, PrepareStreamCallback, PrepareStreamRequest, PrepareStreamResponse, StartStreamRequest, StreamingRequest, StreamRequestCallback, StreamRequestTypes } from '../../hap';
|
||||
import { startRtpSink } from '../../rtp/rtp-ffmpeg-input';
|
||||
import { createSnapshotHandler } from '../camera/camera-snapshot';
|
||||
import { startCameraStreamFfmpeg } from './camera-streaming-ffmpeg';
|
||||
import { CameraStreamingSession } from './camera-streaming-session';
|
||||
import { startCameraStreamSrtp } from './camera-streaming-srtp';
|
||||
|
||||
export class DynamicBitrateSession {
|
||||
currentBitrate: number;
|
||||
@@ -21,7 +6,7 @@ export class DynamicBitrateSession {
|
||||
lastPerfectBitrate: number;
|
||||
lastTotalPacketsLost = 0;
|
||||
|
||||
constructor(initialBitrate: number, public minBitrate: number, public maxBitrate: number) {
|
||||
constructor(initialBitrate: number, public minBitrate: number, public maxBitrate: number, public console?: Console) {
|
||||
this.currentBitrate = initialBitrate;
|
||||
this.lastPerfectBitrate = initialBitrate;
|
||||
}
|
||||
@@ -72,7 +57,7 @@ export class DynamicBitrateSession {
|
||||
this.currentBitrate = Math.max(this.minBitrate, this.currentBitrate);
|
||||
this.currentBitrate = Math.min(this.maxBitrate, this.currentBitrate);
|
||||
|
||||
console.log('Packets lost:', packetsLost);
|
||||
this.console?.log('Packets lost:', packetsLost);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -102,6 +102,10 @@ export async function* handleFragmentsRequests(device: ScryptedDevice & VideoCam
|
||||
['-acodec', aacLowEncoder, '-profile:a', 'aac_low'] :
|
||||
['-acodec', 'libfdk_aac', '-profile:a', 'aac_eld']),
|
||||
'-ar', `${AudioRecordingSamplerateValues[configuration.audioCodec.samplerate]}k`,
|
||||
// technically, this should be used for VBR (which this plugin offers).
|
||||
// will see about changing it later.
|
||||
// '-q:a', '3',
|
||||
// this is used for CBR.
|
||||
'-b:a', `${configuration.audioCodec.bitrate}k`,
|
||||
'-ac', `${configuration.audioCodec.audioChannels}`
|
||||
];
|
||||
|
||||
@@ -192,7 +192,7 @@ export function createCameraStreamingDelegate(device: ScryptedDevice & VideoCame
|
||||
|
||||
if (dynamicBitrate) {
|
||||
const initialBitrate = request.video.max_bit_rate * 1000;
|
||||
const dynamicBitrateSession = new DynamicBitrateSession(initialBitrate, minBitrate, maxBitrate);
|
||||
const dynamicBitrateSession = new DynamicBitrateSession(initialBitrate, minBitrate, maxBitrate, console);
|
||||
|
||||
session.tryReconfigureBitrate = (reason: string, bitrate: number) => {
|
||||
dynamicBitrateSession.onBitrateReconfigured(bitrate);
|
||||
|
||||
Reference in New Issue
Block a user