From c6d83c66bb52a9b1cc29974e5ff1ac28e51ff144 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Fri, 11 Mar 2022 11:07:13 -0800 Subject: [PATCH] homekit/amcrest: dynamic streaming --- plugins/amcrest/README.md | 4 - plugins/amcrest/package-lock.json | 77 +++++++----- plugins/amcrest/package.json | 3 +- plugins/amcrest/src/main.ts | 119 ++++++++++++++---- plugins/homekit/package-lock.json | 4 +- plugins/homekit/package.json | 2 +- .../types/camera/camera-dynamic-bitrate.ts | 19 +-- .../src/types/camera/camera-recording.ts | 4 + .../src/types/camera/camera-streaming.ts | 2 +- 9 files changed, 156 insertions(+), 78 deletions(-) diff --git a/plugins/amcrest/README.md b/plugins/amcrest/README.md index 21f825893..4a2a9d529 100644 --- a/plugins/amcrest/README.md +++ b/plugins/amcrest/README.md @@ -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:///cgi-bin/snapshot.cgi` or `http://:/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) diff --git a/plugins/amcrest/package-lock.json b/plugins/amcrest/package-lock.json index 0af99cd64..c5881e33d 100644 --- a/plugins/amcrest/package-lock.json +++ b/plugins/amcrest/package-lock.json @@ -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", diff --git a/plugins/amcrest/package.json b/plugins/amcrest/package.json index 6bc296566..dcca9e9f9 100644 --- a/plugins/amcrest/package.json +++ b/plugins/amcrest/package.json @@ -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" }, diff --git a/plugins/amcrest/src/main.ts b/plugins/amcrest/src/main.ts index 64dba1962..b09d3d1d3 100644 --- a/plugins/amcrest/src/main.ts +++ b/plugins/amcrest/src/main.ts @@ -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; + videoStreamOptions: Promise; 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 { - 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) { diff --git a/plugins/homekit/package-lock.json b/plugins/homekit/package-lock.json index d87b4fab1..578d94a93 100644 --- a/plugins/homekit/package-lock.json +++ b/plugins/homekit/package-lock.json @@ -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", diff --git a/plugins/homekit/package.json b/plugins/homekit/package.json index de327cf83..b711e65a0 100644 --- a/plugins/homekit/package.json +++ b/plugins/homekit/package.json @@ -39,5 +39,5 @@ "@types/node": "^14.17.9", "@types/url-parse": "^1.4.3" }, - "version": "0.0.224" + "version": "0.0.226" } diff --git a/plugins/homekit/src/types/camera/camera-dynamic-bitrate.ts b/plugins/homekit/src/types/camera/camera-dynamic-bitrate.ts index 6cad1a520..2d9959e55 100644 --- a/plugins/homekit/src/types/camera/camera-dynamic-bitrate.ts +++ b/plugins/homekit/src/types/camera/camera-dynamic-bitrate.ts @@ -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; }; } diff --git a/plugins/homekit/src/types/camera/camera-recording.ts b/plugins/homekit/src/types/camera/camera-recording.ts index 34f9f847f..9c9141785 100644 --- a/plugins/homekit/src/types/camera/camera-recording.ts +++ b/plugins/homekit/src/types/camera/camera-recording.ts @@ -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}` ]; diff --git a/plugins/homekit/src/types/camera/camera-streaming.ts b/plugins/homekit/src/types/camera/camera-streaming.ts index fced4668d..0365a3f28 100644 --- a/plugins/homekit/src/types/camera/camera-streaming.ts +++ b/plugins/homekit/src/types/camera/camera-streaming.ts @@ -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);