mirror of
https://github.com/koush/scrypted.git
synced 2026-02-07 16:02:13 +00:00
Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b75d4cbfd4 | ||
|
|
8c0bb7b205 | ||
|
|
ef64515e56 | ||
|
|
302272e437 | ||
|
|
80e433f6ef | ||
|
|
60786aba2b | ||
|
|
256fde46f6 | ||
|
|
e1a7dd367e | ||
|
|
8612ba3462 | ||
|
|
ab638f26be | ||
|
|
02b881a2d2 | ||
|
|
35475b03e2 | ||
|
|
0b55c777f8 | ||
|
|
68f86d214c | ||
|
|
2abea2d25b | ||
|
|
1c2f17b9f9 | ||
|
|
e3d4800e4f | ||
|
|
d2f175715b | ||
|
|
93c1a699f1 | ||
|
|
41570e9134 | ||
|
|
3ef75854c2 | ||
|
|
c88a638f4e | ||
|
|
793c4da33a | ||
|
|
68f071660e | ||
|
|
8ea5b6aca6 | ||
|
|
2f13c77444 | ||
|
|
981ad183f5 | ||
|
|
8748be82ef | ||
|
|
a347fc2b73 | ||
|
|
002bf3b52c | ||
|
|
72abcd79ec | ||
|
|
86e5b824c7 | ||
|
|
43f6f176f0 | ||
|
|
bc543aa28e | ||
|
|
e90db378e8 | ||
|
|
f2907532aa | ||
|
|
866706505a | ||
|
|
59db3b622c | ||
|
|
7451b9903a | ||
|
|
aded2e43b1 | ||
|
|
031a7527e1 | ||
|
|
2aca568707 | ||
|
|
6b22d34831 | ||
|
|
429d9ec5a6 | ||
|
|
b426668146 | ||
|
|
8bce14f834 | ||
|
|
7511abf768 | ||
|
|
180c12e8cc | ||
|
|
1ed7d03a20 | ||
|
|
9e7b57f154 | ||
|
|
205fdb0222 | ||
|
|
d8f3edee1e | ||
|
|
90c9efc8a6 | ||
|
|
3893ccd776 | ||
|
|
1b154f14bc | ||
|
|
2e3eba4350 | ||
|
|
450f05910a | ||
|
|
22505c9226 | ||
|
|
7120bf86ff | ||
|
|
b49742204f | ||
|
|
6fda76a5e8 | ||
|
|
08bd785d45 | ||
|
|
aa9ddb35aa | ||
|
|
7997c07179 | ||
|
|
a67e24d5dc | ||
|
|
0d4da0dd06 | ||
|
|
993e903f3b | ||
|
|
fbb11a5312 | ||
|
|
ea72d2159b | ||
|
|
1892fdb529 | ||
|
|
1e16793b20 | ||
|
|
2f6c577b47 | ||
|
|
212306449b | ||
|
|
16445bc38e | ||
|
|
b6e9e15d4f | ||
|
|
39abd49ea0 | ||
|
|
05b9b49732 | ||
|
|
1857acac66 | ||
|
|
fedf184847 | ||
|
|
d2afac0dd6 | ||
|
|
6844b55983 | ||
|
|
379dabc182 | ||
|
|
df3c751f2d | ||
|
|
da714d1f94 | ||
|
|
34ee29b7b4 | ||
|
|
4c48f50e01 | ||
|
|
81a5a4349c | ||
|
|
8526c92dcc | ||
|
|
73fefeec26 | ||
|
|
6060b50856 | ||
|
|
d29cd7e421 | ||
|
|
8589283135 | ||
|
|
837dae5f02 | ||
|
|
c26aa2d01e | ||
|
|
c98eca23ab | ||
|
|
eb5d1ac4f6 | ||
|
|
37b0e46dd0 | ||
|
|
042dd84520 | ||
|
|
62d5c145c2 | ||
|
|
1ea3774849 | ||
|
|
9d8345e901 | ||
|
|
9ed850e327 | ||
|
|
957d27b8ef | ||
|
|
b74a957ecb | ||
|
|
debaedfd8c | ||
|
|
0123a97e3c | ||
|
|
a32d47e192 | ||
|
|
90ed8bd3f5 | ||
|
|
c4f4002f55 | ||
|
|
1ea2828e78 | ||
|
|
eb864456df | ||
|
|
51af4f07ff | ||
|
|
f6201acf2a | ||
|
|
96ac479c73 | ||
|
|
0c08875de3 | ||
|
|
bd05fc1b5d | ||
|
|
5a0d325718 | ||
|
|
015794c1d1 | ||
|
|
02d5b429b7 | ||
|
|
e169d154e7 | ||
|
|
01c7b5674a | ||
|
|
a7a1aed0dc | ||
|
|
6bb3f0fd19 | ||
|
|
7828de9d50 | ||
|
|
ea77bb29d0 | ||
|
|
bb184247d0 | ||
|
|
dbc45173ae | ||
|
|
95a23b2882 | ||
|
|
212883e84b | ||
|
|
1200537d62 | ||
|
|
5f6adc9449 | ||
|
|
7d17236ca7 | ||
|
|
028401362a | ||
|
|
69927be4f4 | ||
|
|
ffee1c5cc2 | ||
|
|
ebc3a03e2c | ||
|
|
4246e3c476 | ||
|
|
3fce0838f1 | ||
|
|
2609e301fe | ||
|
|
f4737bf2ac | ||
|
|
fc102aa526 | ||
|
|
9ef33e156f | ||
|
|
881865a0cb | ||
|
|
be5643cc53 | ||
|
|
7e6eba1596 | ||
|
|
27dde776a6 | ||
|
|
b24159a22a | ||
|
|
b6c242b9d5 | ||
|
|
2fbaa12caa | ||
|
|
eb5a497e82 | ||
|
|
66a0ea08ec | ||
|
|
0527baf14a | ||
|
|
c7c5c6eed5 | ||
|
|
143c950c19 | ||
|
|
8d0bb0fa97 | ||
|
|
964274e50c | ||
|
|
e9844528aa | ||
|
|
0609fc8986 | ||
|
|
9331b71433 | ||
|
|
21f8239db7 | ||
|
|
86042ec3fe | ||
|
|
cdb87fb268 | ||
|
|
63dcd35b17 | ||
|
|
951c3b9be6 | ||
|
|
ed642bb3fe | ||
|
|
8093cdd3d9 | ||
|
|
fcbfc3a73f | ||
|
|
94945a48bd | ||
|
|
e360ede5cb | ||
|
|
bc9ec73567 | ||
|
|
cd7e570508 | ||
|
|
1b06c9c11d | ||
|
|
154ab42d15 | ||
|
|
1929f6e8ed | ||
|
|
58bfa17cfe | ||
|
|
38c7006055 | ||
|
|
b5e16b45a9 | ||
|
|
9c13668812 | ||
|
|
a1ca724d6b | ||
|
|
1b032d669c | ||
|
|
c492c15081 | ||
|
|
ee7076384b | ||
|
|
717cac721a | ||
|
|
af41c853bc | ||
|
|
109b716753 | ||
|
|
07930508fe | ||
|
|
a291abe375 | ||
|
|
f4f34b2da8 | ||
|
|
3b4de526ba | ||
|
|
5de67fca86 | ||
|
|
98dc0b1b6d |
2
.github/workflows/docker-HEAD.yml
vendored
2
.github/workflows/docker-HEAD.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
|
||||
14
.github/workflows/docker-common.yml
vendored
14
.github/workflows/docker-common.yml
vendored
@@ -9,13 +9,13 @@ on:
|
||||
- cron: '30 8 2 * *'
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
NODE_VERSION: ["18"]
|
||||
BUILDPACK_DEPS_BASE: ["bullseye"]
|
||||
BASE: ["bullseye", "bookworm"]
|
||||
FLAVOR: ["full", "lite", "thin"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
@@ -43,13 +43,15 @@ jobs:
|
||||
- name: Build and push Docker image (scrypted-common)
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
build-args: NODE_VERSION=${{ matrix.NODE_VERSION }}
|
||||
context: docker/
|
||||
file: docker/Dockerfile.${{ matrix.FLAVOR }}
|
||||
build-args: |
|
||||
NODE_VERSION=${{ matrix.NODE_VERSION }}
|
||||
BASE=${{ matrix.BASE }}
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile.${{ matrix.FLAVOR }}
|
||||
platforms: linux/amd64,linux/arm64,linux/armhf
|
||||
push: true
|
||||
tags: |
|
||||
koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BUILDPACK_DEPS_BASE }}-${{ matrix.FLAVOR }}
|
||||
koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
# ${{ matrix.NODE_VERSION == '16-bullseye' && 'koush/scrypted-common:latest' || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
64
.github/workflows/docker.yml
vendored
64
.github/workflows/docker.yml
vendored
@@ -1,19 +1,19 @@
|
||||
name: Publish Scrypted
|
||||
name: Publish Scrypted Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
docker_tag:
|
||||
description: 'Docker Tag'
|
||||
tag:
|
||||
description: "The npm tag used to build the Docker image. The tag will be resolved as a specific version on npm, and that will be used to version the docker image."
|
||||
required: true
|
||||
package_version:
|
||||
description: 'Package Version'
|
||||
publish_tag:
|
||||
description: "The versioned tag for the published Docker image. NPM will use the minor version, Docker should only specify a patch version."
|
||||
required: false
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -23,26 +23,24 @@ jobs:
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: get-npm-version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@master
|
||||
|
||||
- name: NPM Package Request
|
||||
id: npm-request
|
||||
uses: fjogeleit/http-request-action@v1
|
||||
with:
|
||||
path: server
|
||||
url: 'https://registry.npmjs.org/@scrypted/server'
|
||||
method: 'GET'
|
||||
|
||||
- name: Print Version
|
||||
run: echo "Version ${{ github.event.inputs.package_version || steps.package-version.outputs.current-version }}"
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: Set NPM Version
|
||||
id: package-version
|
||||
run: echo "NPM_VERSION=${{ fromJson(steps.npm-request.outputs.response)['dist-tags'][ github.event.inputs.tag] }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -56,25 +54,31 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image (scrypted)
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
BASE=${{ matrix.BASE }}
|
||||
SCRYPTED_INSTALL_VERSION=${{ github.event.inputs.package_version }}
|
||||
context: docker/
|
||||
file: docker/Dockerfile${{ matrix.SUPERVISOR }}
|
||||
SCRYPTED_INSTALL_VERSION=${{ steps.package-version.outputs.NPM_VERSION }}
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile${{ matrix.SUPERVISOR }}
|
||||
platforms: linux/amd64,linux/arm64,linux/armhf
|
||||
push: true
|
||||
tags: |
|
||||
${{ format('koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.package_version || steps.package-version.outputs.current-version) }}
|
||||
${{ matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '.s6' && format('koush/scrypted:{0}', github.event.inputs.docker_tag) || '' }}
|
||||
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '' && 'koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '' && 'koush/scrypted:thin' || '' }}
|
||||
${{ format('koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '.s6' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '' && 'koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '' && 'koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '' && 'koush/scrypted:thin' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:lite-s6' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:thin-s6' || '' }}
|
||||
|
||||
${{ format('ghcr.io/koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.package_version || steps.package-version.outputs.current-version) }}
|
||||
${{ matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '.s6' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.docker_tag) || '' }}
|
||||
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:thin' || '' }}
|
||||
${{ format('ghcr.io/koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '.s6' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:thin' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:lite-s6' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:thin-s6' || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
- name: Run install script
|
||||
run: |
|
||||
cat ./docker/install-scrypted-dependencies-linux.sh | sudo SERVICE_USER=$USER bash
|
||||
cat ./install/local/install-scrypted-dependencies-linux.sh | sudo SERVICE_USER=$USER bash
|
||||
|
||||
- name: Test server is running
|
||||
run: |
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Run install script
|
||||
run: |
|
||||
mkdir -p ~/.scrypted
|
||||
bash ./docker/install-scrypted-dependencies-mac.sh
|
||||
bash ./install/local/install-scrypted-dependencies-mac.sh
|
||||
|
||||
- name: Test server is running
|
||||
run: |
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
- name: Run install script
|
||||
run: |
|
||||
.\docker\install-scrypted-dependencies-win.ps1
|
||||
.\install\local\install-scrypted-dependencies-win.ps1
|
||||
|
||||
- name: Test server is running
|
||||
run: |
|
||||
|
||||
@@ -23,6 +23,7 @@ Select the appropriate guide. After installation is finished, remember to visit
|
||||
* Windows
|
||||
* [Local Installation](https://github.com/koush/scrypted/wiki/Installation:-Windows)
|
||||
* [WSL2 Installation](https://github.com/koush/scrypted/wiki/Installation:-WSL2-Windows)
|
||||
* [Home Assistant OS](https://github.com/koush/scrypted/wiki/Installation:-Home-Assistant-OS)
|
||||
<!-- * Docker Desktop is [not supported](https://github.com/koush/scrypted/wiki/Installation:-Docker-Desktop). -->
|
||||
* [ReadyNAS: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-ReadyNAS)
|
||||
* [Synology: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-Synology-NAS)
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Duplex } from 'stream';
|
||||
import { cloneDeep } from './clone-deep';
|
||||
import { listenZeroSingleClient } from './listen-cluster';
|
||||
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from './media-helpers';
|
||||
import { createRtspParser } from "./rtsp-server";
|
||||
import { parseSdp } from "./sdp-utils";
|
||||
import { StreamChunk, StreamParser } from './stream-parser';
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
@@ -57,9 +59,13 @@ export async function parseResolution(cp: ChildProcess) {
|
||||
}
|
||||
|
||||
async function parseInputToken(cp: ChildProcess, token: string) {
|
||||
let processed = 0;
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
cp.on('exit', () => reject(new Error('ffmpeg exited while waiting to parse stream information: ' + token)));
|
||||
const parser = (data: Buffer) => {
|
||||
processed += data.length;
|
||||
if (processed > 10000)
|
||||
return resolve(undefined);
|
||||
const stdout: string = data.toString().split('Output ')[0];
|
||||
const idx = stdout.lastIndexOf(`${token}: `);
|
||||
if (idx !== -1) {
|
||||
@@ -77,7 +83,11 @@ async function parseInputToken(cp: ChildProcess, token: string) {
|
||||
};
|
||||
cp.stdout.on('data', parser);
|
||||
cp.stderr.on('data', parser);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
cp.stdout.removeAllListeners('data');
|
||||
cp.stderr.removeAllListeners('data');
|
||||
});
|
||||
}
|
||||
|
||||
export async function parseVideoCodec(cp: ChildProcess) {
|
||||
@@ -158,8 +168,6 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
|
||||
const args = ffmpegInput.inputArguments.slice();
|
||||
|
||||
let needSdp = false;
|
||||
|
||||
const ensureActive = (killed: () => void) => {
|
||||
if (!isActive) {
|
||||
killed();
|
||||
@@ -211,11 +219,6 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
}
|
||||
}
|
||||
|
||||
if (needSdp) {
|
||||
args.push('-sdp_file', `pipe:${pipeCount++}`);
|
||||
stdio.push('pipe');
|
||||
}
|
||||
|
||||
// start ffmpeg process with child process pipes
|
||||
args.unshift('-hide_banner');
|
||||
safePrintFFmpegArguments(console, args);
|
||||
@@ -225,20 +228,6 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
ffmpegLogInitialOutput(console, cp, undefined, options?.storage);
|
||||
cp.on('exit', () => kill(new Error('ffmpeg exited')));
|
||||
|
||||
let sdp: Promise<Buffer[]>;
|
||||
if (needSdp) {
|
||||
sdp = new Promise<Buffer[]>(resolve => {
|
||||
const ret: Buffer[] = [];
|
||||
cp.stdio[pipeCount - 1].on('data', buffer => {
|
||||
ret.push(buffer);
|
||||
resolve(ret);
|
||||
});
|
||||
})
|
||||
}
|
||||
else {
|
||||
sdp = Promise.resolve([]);
|
||||
}
|
||||
|
||||
// now parse the created pipes
|
||||
const start = () => {
|
||||
for (const p of startParsers) {
|
||||
@@ -268,10 +257,17 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
});
|
||||
};
|
||||
|
||||
// tbh parsing stdout is super sketchy way of doing this.
|
||||
parseAudioCodec(cp).then(result => inputAudioCodec = result);
|
||||
parseResolution(cp).then(result => inputVideoResolution = result);
|
||||
await parseVideoCodec(cp).then(result => inputVideoCodec = result);
|
||||
await parseVideoCodec(cp);
|
||||
const rtsp = (options.parsers as any).rtsp as ReturnType<typeof createRtspParser>;
|
||||
rtsp.sdp.then(sdp => {
|
||||
const parsed = parseSdp(sdp);
|
||||
const audio = parsed.msections.find(msection=>msection.type === 'audio');
|
||||
const video = parsed.msections.find(msection=>msection.type === 'video');
|
||||
inputVideoCodec = video?.codec;
|
||||
inputAudioCodec = audio?.codec;
|
||||
});
|
||||
|
||||
const sdp = rtsp.sdp.then(sdpString => [Buffer.from(sdpString)]);
|
||||
|
||||
return {
|
||||
start,
|
||||
|
||||
@@ -217,14 +217,12 @@ const acontrol = 'a=control:';
|
||||
const artpmap = 'a=rtpmap:';
|
||||
export function parseMSection(msection: string[]) {
|
||||
const control = msection.find(line => line.startsWith(acontrol))?.substring(acontrol.length);
|
||||
const rtpmapFirst = msection.find(line => line.startsWith(artpmap));
|
||||
const mline = parseMLine(msection[0]);
|
||||
|
||||
let codec = parseRtpMap(mline.type, rtpmapFirst).codec;
|
||||
|
||||
const rtpmaps = msection.filter(line => line.startsWith(artpmap)).map(line => parseRtpMap(mline.type, line));
|
||||
|
||||
const rawRtpmaps = msection.filter(line => line.startsWith(artpmap));
|
||||
const rtpmaps = rawRtpmaps.map(line => parseRtpMap(mline.type, line));
|
||||
const codec = parseRtpMap(mline.type, rawRtpmaps[0]).codec;
|
||||
let direction: string;
|
||||
|
||||
for (const checkDirection of ['sendonly', 'sendrecv', 'recvonly', 'inactive']) {
|
||||
const found = msection.find(line => line === 'a=' + checkDirection);
|
||||
if (found) {
|
||||
|
||||
2
external/werift
vendored
2
external/werift
vendored
Submodule external/werift updated: 140faa891d...91be7cf469
47
install/config.yaml
Executable file
47
install/config.yaml
Executable file
@@ -0,0 +1,47 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "18-bullseye-full.s6-v0.13.2"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
arch:
|
||||
- amd64
|
||||
- aarch64
|
||||
- armv7
|
||||
init: false
|
||||
ingress: true
|
||||
ingress_port: 11080
|
||||
panel_icon: mdi:memory
|
||||
hassio_api: true
|
||||
homeassistant_api: true
|
||||
ingress_stream: true
|
||||
host_network: true
|
||||
gpio: true
|
||||
usb: true
|
||||
uart: true
|
||||
video: true
|
||||
image: "ghcr.io/koush/scrypted"
|
||||
environment:
|
||||
SCRYPTED_INSTALL_PLUGIN: "@scrypted/homeassistant"
|
||||
SCRYPTED_VOLUME: "/data/scrypted_data"
|
||||
SCRYPTED_NVR_VOLUME: "/data/scrypted_nvr"
|
||||
SCRYPTED_ADMIN_ADDRESS: "172.30.32.2"
|
||||
SCRYPTED_ADMIN_USERNAME: "homeassistant"
|
||||
backup_exclude:
|
||||
- '/server/**'
|
||||
- '/data/scrypted_nvr/**'
|
||||
- '/data/scrypted_data/plugins/**'
|
||||
map:
|
||||
- config:rw
|
||||
- media:rw
|
||||
devices:
|
||||
- /dev/mem
|
||||
- /dev/dri/renderD128
|
||||
- /dev/apex_0
|
||||
- /dev/apex_1
|
||||
- /dev/apex_2
|
||||
- /dev/apex_3
|
||||
- /dev/dri/card0
|
||||
- /dev/vchiq
|
||||
- /dev/video10
|
||||
- /dev/video0
|
||||
@@ -6,8 +6,8 @@
|
||||
# This common file will be used by both Docker and the linux
|
||||
# install script.
|
||||
################################################################
|
||||
ARG BUILDPACK_DEPS_BASE="bullseye"
|
||||
FROM debian:${BUILDPACK_DEPS_BASE} as header
|
||||
ARG BASE="bullseye"
|
||||
FROM debian:${BASE} as header
|
||||
|
||||
RUN apt-get update && apt-get -y install curl wget
|
||||
|
||||
@@ -24,6 +24,13 @@ RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y install libedgetpu1-std
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
RUN if [ "$(uname -m)" = "x86_64" ]; \
|
||||
then \
|
||||
apt-get -y install \
|
||||
intel-opencl-icd; \
|
||||
fi
|
||||
|
||||
RUN apt-get -y install software-properties-common apt-utils
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y upgrade
|
||||
@@ -74,11 +81,12 @@ RUN if [ "$(uname -m)" != "x86_64" ]; \
|
||||
fi
|
||||
|
||||
# python pip
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install aiofiles debugpy typing_extensions psutil
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
@@ -1,5 +1,5 @@
|
||||
ARG BUILDPACK_DEPS_BASE="bullseye"
|
||||
FROM debian:${BUILDPACK_DEPS_BASE} as header
|
||||
ARG BASE="bullseye"
|
||||
FROM debian:${BASE} as header
|
||||
|
||||
RUN apt-get update && apt-get -y install curl wget
|
||||
|
||||
@@ -32,8 +32,9 @@ RUN apt-get -y install \
|
||||
python3-wheel
|
||||
|
||||
# python pip
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install aiofiles debugpy typing_extensions psutil
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
ENV SCRYPTED_DOCKER_SERVE="true"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
@@ -19,4 +19,4 @@ RUN python3 -m pip install --upgrade pip
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install aiofiles debugpy typing_extensions psutil
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
@@ -5,7 +5,8 @@ FROM koush/scrypted-common:${BASE}
|
||||
RUN apt-get -y install \
|
||||
libnss-mdns \
|
||||
avahi-discover \
|
||||
libavahi-compat-libdnssd-dev
|
||||
libavahi-compat-libdnssd-dev \
|
||||
xz-utils
|
||||
|
||||
# copy configurations and scripts
|
||||
COPY fs /
|
||||
@@ -1,5 +1,5 @@
|
||||
ARG BUILDPACK_DEPS_BASE="bullseye"
|
||||
FROM debian:${BUILDPACK_DEPS_BASE} as header
|
||||
ARG BASE="bullseye"
|
||||
FROM debian:${BASE} as header
|
||||
|
||||
RUN apt-get update && apt-get -y install curl wget
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
set -x
|
||||
|
||||
NODE_VERSION=18
|
||||
BUILDPACK_DEPS_BASE=bullseye
|
||||
IMAGE_BASE=bookworm
|
||||
FLAVOR=full
|
||||
BASE=$NODE_VERSION-$BUILDPACK_DEPS_BASE-$FLAVOR
|
||||
BASE=$NODE_VERSION-$IMAGE_BASE-$FLAVOR
|
||||
echo $BASE
|
||||
SUPERVISOR=.s6
|
||||
SUPERVISOR_BASE=$BASE$SUPERVISOR
|
||||
|
||||
docker build -t koush/scrypted-common:$BASE -f Dockerfile.$FLAVOR \
|
||||
--build-arg NODE_VERSION=$NODE_VERSION --build-arg BUILDPACK_DEPS_BASE=$BUILDPACK_DEPS_BASE . && \
|
||||
--build-arg NODE_VERSION=$NODE_VERSION --build-arg BASE=$IMAGE_BASE . && \
|
||||
\
|
||||
docker build -t koush/scrypted:$SUPERVISOR_BASE -f Dockerfile$SUPERVISOR \
|
||||
--build-arg BASE=$BASE .
|
||||
@@ -32,14 +32,17 @@ services:
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
|
||||
# uncomment this and a line below as needed.
|
||||
# devices:
|
||||
# zwave usb serial device
|
||||
# - /dev/ttyACM0:/dev/ttyACM0
|
||||
# all usb devices, such as coral tpu
|
||||
# - /dev/bus/usb:/dev/bus/usb
|
||||
# intel hardware accelerated video decoding
|
||||
# - /dev/dri:/dev/dri
|
||||
devices:
|
||||
# hardware accelerated video decoding, opencl, etc.
|
||||
- /dev/dri:/dev/dri
|
||||
# uncomment below as necessary.
|
||||
# zwave usb serial device
|
||||
# - /dev/ttyACM0:/dev/ttyACM0
|
||||
# all usb devices, such as coral tpu
|
||||
# - /dev/bus/usb:/dev/bus/usb
|
||||
# coral PCI devices
|
||||
# - /dev/apex_0:/dev/apex_0
|
||||
# - /dev/apex_1:/dev/apex_1
|
||||
|
||||
volumes:
|
||||
- ~/.scrypted/volume:/server/volume
|
||||
@@ -42,7 +42,7 @@ fi
|
||||
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum)
|
||||
DOCKER_COMPOSE_YML=$SCRYPTED_HOME/docker-compose.yml
|
||||
echo "Created $DOCKER_COMPOSE_YML"
|
||||
curl -s https://raw.githubusercontent.com/koush/scrypted/main/docker/docker-compose.yml | sed s/SET_THIS_TO_SOME_RANDOM_TEXT/"$(echo $RANDOM | md5sum | head -c 32)"/g > $DOCKER_COMPOSE_YML
|
||||
curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/docker-compose.yml | sed s/SET_THIS_TO_SOME_RANDOM_TEXT/"$(echo $RANDOM | md5sum | head -c 32)"/g > $DOCKER_COMPOSE_YML
|
||||
|
||||
echo "Setting permissions on $SCRYPTED_HOME"
|
||||
chown -R $SERVICE_USER $SCRYPTED_HOME
|
||||
@@ -3,8 +3,8 @@
|
||||
# This common file will be used by both Docker and the linux
|
||||
# install script.
|
||||
################################################################
|
||||
ARG BUILDPACK_DEPS_BASE="bullseye"
|
||||
FROM debian:${BUILDPACK_DEPS_BASE} as header
|
||||
ARG BASE="bullseye"
|
||||
FROM debian:${BASE} as header
|
||||
|
||||
RUN apt-get update && apt-get -y install curl wget
|
||||
|
||||
@@ -21,6 +21,13 @@ RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y install libedgetpu1-std
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
RUN if [ "$(uname -m)" = "x86_64" ]; \
|
||||
then \
|
||||
apt-get -y install \
|
||||
intel-opencl-icd; \
|
||||
fi
|
||||
|
||||
RUN apt-get -y install software-properties-common apt-utils
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y upgrade
|
||||
@@ -71,11 +78,12 @@ RUN if [ "$(uname -m)" != "x86_64" ]; \
|
||||
fi
|
||||
|
||||
# python pip
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install aiofiles debugpy typing_extensions psutil
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
BIN
install/icon.png
Normal file
BIN
install/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -48,7 +48,7 @@ ENV() {
|
||||
echo "ignoring ENV $1"
|
||||
}
|
||||
|
||||
source <(curl -s https://raw.githubusercontent.com/koush/scrypted/main/docker/template/Dockerfile.full.header)
|
||||
source <(curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/template/Dockerfile.full.header)
|
||||
|
||||
if [ -z "$SERVICE_USER" ]
|
||||
then
|
||||
@@ -87,7 +87,7 @@ if [ "$PYTHON_VERSION" != "3.10" ]
|
||||
then
|
||||
RUN python$PYTHON_VERSION -m pip install typing
|
||||
fi
|
||||
RUN python$PYTHON_VERSION -m pip install aiofiles debugpy typing_extensions opencv-python psutil
|
||||
RUN python$PYTHON_VERSION -m pip install debugpy typing_extensions opencv-python psutil
|
||||
|
||||
echo "Installing Scrypted Launch Agent..."
|
||||
|
||||
@@ -20,7 +20,7 @@ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";"
|
||||
|
||||
|
||||
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install --upgrade pip
|
||||
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install aiofiles debugpy typing_extensions typing opencv-python
|
||||
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install debugpy typing_extensions typing opencv-python
|
||||
|
||||
npx -y scrypted@latest install-server
|
||||
|
||||
BIN
install/logo.png
Normal file
BIN
install/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -23,10 +23,19 @@ async function example() {
|
||||
if (!backyard)
|
||||
throw new Error('Device not found');
|
||||
|
||||
backyard.listen(ScryptedInterface.ObjectDetector, (source, details, data) => {
|
||||
backyard.listen(ScryptedInterface.ObjectDetector, async (source, details, data) => {
|
||||
const results = data as ObjectsDetected;
|
||||
console.log(results);
|
||||
})
|
||||
console.log('detection results', results);
|
||||
// detections that are flagged for retention will have a detectionId.
|
||||
// tf etc won't retain automatically, and this requires a wrapping detector like Scrypted NVR Object Detection
|
||||
// to decide which frames to keep. Otherwise saving all images would be extremely poor performance.
|
||||
if (!results.detectionId)
|
||||
return;
|
||||
|
||||
const media = await backyard.getDetectionInput(results.detectionId);
|
||||
const jpeg = await sdk.mediaManager.convertMediaObjectToBuffer(media, 'image/jpeg');
|
||||
// do something with the buffer like save to disk or send to a service.
|
||||
});
|
||||
}
|
||||
|
||||
example();
|
||||
|
||||
12
packages/client/package-lock.json
generated
12
packages/client/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.51",
|
||||
"version": "1.1.54",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.51",
|
||||
"version": "1.1.54",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.80",
|
||||
"@scrypted/types": "^0.2.91",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
@@ -21,9 +21,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.2.80",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.80.tgz",
|
||||
"integrity": "sha512-YVu7jcD5sYgjJLP7kH1K2FJzqrlcjdpDxzZoLXudZCKiujldbmLYcwglSgnN9bRqkKZcGOfru/WssvQj+0JioQ=="
|
||||
"version": "0.2.91",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.91.tgz",
|
||||
"integrity": "sha512-GfWil8cl2QwlTXk506ZXDALQfuv7zN48PtPlpmBMO/IYTQFtb+RB2zr+FwC9gdvRaZgs9NCCS2Fiig1OY7uxdQ=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.51",
|
||||
"version": "1.1.54",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -17,7 +17,7 @@
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.80",
|
||||
"@scrypted/types": "^0.2.91",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
|
||||
@@ -78,25 +78,48 @@ export interface ScryptedClientOptions extends Partial<ScryptedLoginOptions> {
|
||||
transports?: string[];
|
||||
}
|
||||
|
||||
function isInstalledApp() {
|
||||
return globalThis.navigator?.userAgent.includes('InstalledApp');
|
||||
}
|
||||
|
||||
function isRunningStandalone() {
|
||||
return globalThis.matchMedia?.('(display-mode: standalone)').matches || globalThis.navigator?.userAgent.includes('InstalledApp');
|
||||
return globalThis.matchMedia?.('(display-mode: standalone)').matches || isInstalledApp();
|
||||
}
|
||||
|
||||
export async function logoutScryptedClient(baseUrl?: string) {
|
||||
const url = baseUrl ? new URL('/logout', baseUrl).toString() : '/logout';
|
||||
const url = combineBaseUrl(baseUrl, 'logout');
|
||||
const response = await axios(url, {
|
||||
withCredentials: true,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export function getCurrentBaseUrl() {
|
||||
// an endpoint within scrypted will be served at /endpoint/[org/][id]
|
||||
// find the endpoint prefix and anything prior to that will be the server base url.
|
||||
const url = new URL(window.location.href);
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
let endpointPath = window.location.pathname;
|
||||
const parts = endpointPath.split('/');
|
||||
const index = parts.findIndex(p => p === 'endpoint');
|
||||
if (index === -1) {
|
||||
// console.warn('path not recognized, does not contain the segment "endpoint".')
|
||||
return undefined;
|
||||
}
|
||||
const keep = parts.slice(0, index);
|
||||
keep.push('');
|
||||
url.pathname = keep.join('/');
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export async function loginScryptedClient(options: ScryptedLoginOptions) {
|
||||
let { baseUrl, username, password, change_password, maxAge } = options;
|
||||
// pwa should stay logged in for a year.
|
||||
if (!maxAge && isRunningStandalone())
|
||||
maxAge = 365 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const url = `${baseUrl || ''}/login`;
|
||||
const url = combineBaseUrl(baseUrl, 'login');
|
||||
const response = await axios.post(url, {
|
||||
username,
|
||||
password,
|
||||
@@ -129,7 +152,7 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
|
||||
|
||||
export async function checkScryptedClientLogin(options?: ScryptedConnectionOptions) {
|
||||
let { baseUrl } = options || {};
|
||||
const url = `${baseUrl || ''}/login`;
|
||||
const url = combineBaseUrl(baseUrl, 'login');
|
||||
const response = await axios.get(url, {
|
||||
withCredentials: true,
|
||||
...options?.axiosConfig,
|
||||
@@ -145,6 +168,7 @@ export async function checkScryptedClientLogin(options?: ScryptedConnectionOptio
|
||||
error: response.data.error as string,
|
||||
authorization: response.data.authorization as string,
|
||||
queryToken: response.data.queryToken as any,
|
||||
token: response.data.token as string,
|
||||
addresses: response.data.addresses as string[],
|
||||
scryptedCloud,
|
||||
directAddress,
|
||||
@@ -175,9 +199,12 @@ export function redirectScryptedLogin(options?: {
|
||||
globalThis.location.href = redirect_uri;
|
||||
}
|
||||
|
||||
export function combineBaseUrl(baseUrl: string, rootPath: string) {
|
||||
return baseUrl ? new URL(rootPath, baseUrl).toString() : '/' + rootPath;
|
||||
}
|
||||
|
||||
export async function redirectScryptedLogout(baseUrl?: string) {
|
||||
baseUrl = baseUrl || '';
|
||||
globalThis.location.href = `${baseUrl}/logout`;
|
||||
globalThis.location.href = combineBaseUrl(baseUrl, 'logout');
|
||||
}
|
||||
|
||||
export async function connectScryptedClient(options: ScryptedClientOptions): Promise<ScryptedClientStatic> {
|
||||
@@ -219,9 +246,10 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
}
|
||||
|
||||
let socket: IOClientSocket;
|
||||
const endpointPath = `/endpoint/${pluginId}`;
|
||||
const eioPath = `endpoint/${pluginId}/engine.io/api`;
|
||||
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
|
||||
const eioOptions: Partial<SocketOptions> = {
|
||||
path: `${endpointPath}/engine.io/api`,
|
||||
path: eioEndpoint,
|
||||
withCredentials: true,
|
||||
extraHeaders,
|
||||
rejectUnauthorized: false,
|
||||
@@ -238,14 +266,15 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
// if the cert has been accepted. Other browsers seem fine.
|
||||
// So the default is not to connect to IP addresses on Chrome, but do so on other browsers.
|
||||
const isChrome = globalThis.navigator?.userAgent.includes('Chrome');
|
||||
const isNotChromeOrIsInstalledApp = !isChrome || isInstalledApp();
|
||||
|
||||
const addresses: string[] = [];
|
||||
const localAddressDefault = !isChrome;
|
||||
const localAddressDefault = isNotChromeOrIsInstalledApp;
|
||||
if (((scryptedCloud && options.local === undefined && localAddressDefault) || options.local) && localAddresses) {
|
||||
addresses.push(...localAddresses);
|
||||
}
|
||||
|
||||
const directAddressDefault = directAddress && (!isChrome || !isIPAddress(directAddress));
|
||||
const directAddressDefault = directAddress && (isNotChromeOrIsInstalledApp || !isIPAddress(directAddress));
|
||||
if (((scryptedCloud && options.direct === undefined && directAddressDefault) || options.direct) && directAddress) {
|
||||
addresses.push(directAddress);
|
||||
}
|
||||
|
||||
4
packages/h264-repacketizer/package-lock.json
generated
4
packages/h264-repacketizer/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/h264-packetizer",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/h264-packetizer",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/h264-repacketizer",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
2
plugins/alexa/.vscode/settings.json
vendored
2
plugins/alexa/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "10.10.0.50",
|
||||
"scrypted.debugHost": "koushik-ubuntu",
|
||||
}
|
||||
7
plugins/alexa/package-lock.json
generated
7
plugins/alexa/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.5",
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
@@ -17,7 +17,8 @@
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"version": "0.2.85",
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.101",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.5",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
4
plugins/amcrest/package-lock.json
generated
4
plugins/amcrest/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.121",
|
||||
"version": "0.0.122",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.121",
|
||||
"version": "0.0.122",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.121",
|
||||
"version": "0.0.122",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -33,6 +33,16 @@ export class AmcrestCameraClient {
|
||||
});
|
||||
}
|
||||
|
||||
async reboot() {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
url: `http://${this.ip}/cgi-bin/magicBox.cgi?action=reboot`,
|
||||
});
|
||||
return response.data as string;
|
||||
}
|
||||
|
||||
async checkTwoWayAudio() {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers';
|
||||
import { readLength } from "@scrypted/common/src/read-stream";
|
||||
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, PictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk";
|
||||
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, PictureOptions, Reboot, 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";
|
||||
@@ -23,7 +23,7 @@ function findValue(blob: string, prefix: string, key: string) {
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, Camera, Intercom, VideoRecorder {
|
||||
class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, Camera, Intercom, VideoRecorder, Reboot {
|
||||
eventStream: Stream;
|
||||
cp: ChildProcess;
|
||||
client: AmcrestCameraClient;
|
||||
@@ -37,9 +37,15 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
this.storage.removeItem('amcrestDoorbell');
|
||||
}
|
||||
|
||||
this.updateDevice();
|
||||
this.updateDeviceInfo();
|
||||
}
|
||||
|
||||
async reboot() {
|
||||
const client = this.getClient();
|
||||
await client.reboot();
|
||||
}
|
||||
|
||||
getRecordingStreamCurrentTime(recordingStream: MediaObject): Promise<number> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
@@ -440,6 +446,29 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
return this.videoStreamOptions;
|
||||
}
|
||||
|
||||
updateDevice() {
|
||||
const doorbellType = this.storage.getItem('doorbellType');
|
||||
const isDoorbell = doorbellType === AMCREST_DOORBELL_TYPE || doorbellType === DAHUA_DOORBELL_TYPE;
|
||||
// true is the legacy value before onvif was added.
|
||||
const twoWayAudio = this.storage.getItem('twoWayAudio') === 'true'
|
||||
|| this.storage.getItem('twoWayAudio') === 'ONVIF'
|
||||
|| this.storage.getItem('twoWayAudio') === 'Amcrest';
|
||||
|
||||
const interfaces = this.provider.getInterfaces();
|
||||
let type: ScryptedDeviceType = undefined;
|
||||
if (isDoorbell) {
|
||||
type = ScryptedDeviceType.Doorbell;
|
||||
interfaces.push(ScryptedInterface.BinarySensor)
|
||||
}
|
||||
if (isDoorbell || twoWayAudio) {
|
||||
interfaces.push(ScryptedInterface.Intercom);
|
||||
}
|
||||
const continuousRecording = this.storage.getItem('continuousRecording') === 'true';
|
||||
if (continuousRecording)
|
||||
interfaces.push(ScryptedInterface.VideoRecorder);
|
||||
this.provider.updateDevice(this.nativeId, this.name, interfaces, type);
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string) {
|
||||
if (key === 'continuousRecording') {
|
||||
if (value === 'true') {
|
||||
@@ -461,27 +490,8 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
this.videoStreamOptions = undefined;
|
||||
|
||||
super.putSetting(key, value);
|
||||
const doorbellType = this.storage.getItem('doorbellType');
|
||||
const isDoorbell = doorbellType === AMCREST_DOORBELL_TYPE || doorbellType === DAHUA_DOORBELL_TYPE;
|
||||
// true is the legacy value before onvif was added.
|
||||
const twoWayAudio = this.storage.getItem('twoWayAudio') === 'true'
|
||||
|| this.storage.getItem('twoWayAudio') === 'ONVIF'
|
||||
|| this.storage.getItem('twoWayAudio') === 'Amcrest';
|
||||
|
||||
const interfaces = this.provider.getInterfaces();
|
||||
let type: ScryptedDeviceType = undefined;
|
||||
if (isDoorbell) {
|
||||
type = ScryptedDeviceType.Doorbell;
|
||||
interfaces.push(ScryptedInterface.BinarySensor)
|
||||
}
|
||||
if (isDoorbell || twoWayAudio) {
|
||||
interfaces.push(ScryptedInterface.Intercom);
|
||||
}
|
||||
const continuousRecording = this.storage.getItem('continuousRecording') === 'true';
|
||||
if (continuousRecording)
|
||||
interfaces.push(ScryptedInterface.VideoRecorder);
|
||||
this.provider.updateDevice(this.nativeId, this.name, interfaces, type);
|
||||
|
||||
|
||||
this.updateDevice();
|
||||
this.updateDeviceInfo();
|
||||
}
|
||||
|
||||
@@ -576,6 +586,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
class AmcrestProvider extends RtspProvider {
|
||||
getAdditionalInterfaces() {
|
||||
return [
|
||||
ScryptedInterface.Reboot,
|
||||
ScryptedInterface.VideoCameraConfiguration,
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.AudioSensor,
|
||||
|
||||
6
plugins/arlo/package-lock.json
generated
6
plugins/arlo/package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.7.13",
|
||||
"version": "0.7.21",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.7.13",
|
||||
"version": "0.7.21",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.87",
|
||||
"version": "0.2.101",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.7.13",
|
||||
"version": "0.7.21",
|
||||
"description": "Arlo Plugin for Scrypted",
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
|
||||
@@ -24,6 +24,7 @@ limitations under the License.
|
||||
# Import helper classes that are part of this library.
|
||||
|
||||
from .request import Request
|
||||
from .host_picker import pick_host
|
||||
from .mqtt_stream_async import MQTTStream
|
||||
from .sse_stream_async import EventStream
|
||||
from .logging import logger
|
||||
@@ -31,6 +32,7 @@ from .logging import logger
|
||||
# Import all of the other stuff.
|
||||
from datetime import datetime, timedelta
|
||||
from cachetools import cached, TTLCache
|
||||
import scrypted_arlo_go
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
@@ -38,6 +40,7 @@ import base64
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
|
||||
stream_class = MQTTStream
|
||||
|
||||
@@ -137,8 +140,7 @@ class Arlo(object):
|
||||
self.BASE_URL = 'myapi.arlo.com'
|
||||
|
||||
def LoginMFA(self):
|
||||
self.request = Request()
|
||||
|
||||
device_id = str(uuid.uuid4())
|
||||
headers = {
|
||||
'DNT': '1',
|
||||
'schemaVersion': '1',
|
||||
@@ -149,11 +151,35 @@ class Arlo(object):
|
||||
'Referer': f'https://{self.BASE_URL}/',
|
||||
'Source': 'arloCamWeb',
|
||||
'TE': 'Trailers',
|
||||
'x-user-device-id': device_id,
|
||||
'x-user-device-automation-name': 'QlJPV1NFUg==',
|
||||
'x-user-device-type': 'BROWSER',
|
||||
'Host': self.AUTH_URL,
|
||||
}
|
||||
|
||||
self.request = Request()
|
||||
try:
|
||||
auth_host = self.AUTH_URL
|
||||
self.request.options(f'https://{auth_host}/api/auth', headers=headers)
|
||||
logger.info("Using primary authentication host")
|
||||
except Exception as e:
|
||||
# in case cloudflare rejects our auth request...
|
||||
logger.warning(f"Using fallback authentication host due to: {e}")
|
||||
|
||||
backup_hosts = list(scrypted_arlo_go.BACKUP_AUTH_HOSTS())
|
||||
random.shuffle(backup_hosts)
|
||||
|
||||
auth_host = pick_host([
|
||||
base64.b64decode(h.encode("utf-8")).decode("utf-8")
|
||||
for h in backup_hosts
|
||||
], self.AUTH_URL, "/api/auth")
|
||||
|
||||
self.request = Request(mode="ip")
|
||||
|
||||
# Authenticate
|
||||
self.request.options(f'https://{auth_host}/api/auth', headers=headers)
|
||||
auth_body = self.request.post(
|
||||
f'https://{self.AUTH_URL}/api/auth',
|
||||
f'https://{auth_host}/api/auth',
|
||||
params={
|
||||
'email': self.username,
|
||||
'password': str(base64.b64encode(self.password.encode('utf-8')), 'utf-8'),
|
||||
@@ -168,7 +194,7 @@ class Arlo(object):
|
||||
|
||||
# Retrieve MFA factor id
|
||||
factors_body = self.request.get(
|
||||
f'https://{self.AUTH_URL}/api/getFactors',
|
||||
f'https://{auth_host}/api/getFactors',
|
||||
params={'data': auth_body['data']['issued']},
|
||||
headers=headers,
|
||||
raw=True
|
||||
@@ -181,8 +207,8 @@ class Arlo(object):
|
||||
|
||||
# Start factor auth
|
||||
start_auth_body = self.request.post(
|
||||
f'https://{self.AUTH_URL}/api/startAuth',
|
||||
{'factorId': factor_id},
|
||||
f'https://{auth_host}/api/startAuth',
|
||||
params={'factorId': factor_id},
|
||||
headers=headers,
|
||||
raw=True
|
||||
)
|
||||
@@ -192,8 +218,8 @@ class Arlo(object):
|
||||
nonlocal self, factor_auth_code, headers
|
||||
|
||||
finish_auth_body = self.request.post(
|
||||
f'https://{self.AUTH_URL}/api/finishAuth',
|
||||
{
|
||||
f'https://{auth_host}/api/finishAuth',
|
||||
params={
|
||||
'factorAuthCode': factor_auth_code,
|
||||
'otp': code
|
||||
},
|
||||
@@ -201,6 +227,8 @@ class Arlo(object):
|
||||
raw=True
|
||||
)
|
||||
|
||||
self.request = Request()
|
||||
|
||||
# Update Authorization code with new code
|
||||
headers = {
|
||||
'Auth-Version': '2',
|
||||
@@ -349,7 +377,7 @@ class Arlo(object):
|
||||
body['from'] = self.user_id+'_web'
|
||||
body['to'] = basestation_id
|
||||
|
||||
self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/notify/'+body['to'], body, headers={"xcloudId":basestation.get('xCloudId')})
|
||||
self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/notify/'+body['to'], params=body, headers={"xcloudId":basestation.get('xCloudId')})
|
||||
return body.get('transId')
|
||||
|
||||
def Ping(self, basestation):
|
||||
@@ -629,7 +657,7 @@ class Arlo(object):
|
||||
def trigger(self):
|
||||
nl.stream_url_dict = self.request.post(
|
||||
f'https://{self.BASE_URL}/hmsweb/users/devices/startStream',
|
||||
{
|
||||
params={
|
||||
"to": camera.get('parentId'),
|
||||
"from": self.user_id + "_web",
|
||||
"resource": "cameras/" + camera.get('deviceId'),
|
||||
@@ -702,7 +730,7 @@ class Arlo(object):
|
||||
def trigger(self):
|
||||
self.request.post(
|
||||
f"https://{self.BASE_URL}/hmsweb/users/devices/fullFrameSnapshot",
|
||||
{
|
||||
params={
|
||||
"to": camera.get("parentId"),
|
||||
"from": self.user_id + "_web",
|
||||
"resource": "cameras/" + camera.get("deviceId"),
|
||||
@@ -885,7 +913,7 @@ class Arlo(object):
|
||||
logger.debug(f"Library cache miss for {from_date}, {to_date}")
|
||||
return self.request.post(
|
||||
f'https://{self.BASE_URL}/hmsweb/users/library',
|
||||
{
|
||||
params={
|
||||
'dateFrom': from_date,
|
||||
'dateTo': to_date
|
||||
}
|
||||
|
||||
29
plugins/arlo/src/arlo_plugin/arlo/host_picker.py
Normal file
29
plugins/arlo/src/arlo_plugin/arlo/host_picker.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import ssl
|
||||
from socket import setdefaulttimeout
|
||||
import requests
|
||||
from requests_toolbelt.adapters import host_header_ssl
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import ExtensionOID
|
||||
|
||||
from .logging import logger
|
||||
|
||||
|
||||
setdefaulttimeout(5)
|
||||
|
||||
|
||||
def pick_host(hosts, hostname_to_match, endpoint_to_test):
|
||||
session = requests.Session()
|
||||
session.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
|
||||
|
||||
for host in hosts:
|
||||
try:
|
||||
c = ssl.get_server_certificate((host, 443))
|
||||
c = x509.load_pem_x509_certificate(c.encode("utf-8"))
|
||||
if hostname_to_match in c.subject.rfc4514_string() or \
|
||||
hostname_to_match in c.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME).value.get_values_for_type(x509.DNSName):
|
||||
r = session.post(f"https://{host}{endpoint_to_test}", headers={"Host": hostname_to_match})
|
||||
r.raise_for_status()
|
||||
return host
|
||||
except Exception as e:
|
||||
logger.warning(f"{host} is invalid: {e}")
|
||||
raise Exception("no valid hosts found!")
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
import requests
|
||||
from requests.exceptions import HTTPError
|
||||
from requests_toolbelt.adapters import host_header_ssl
|
||||
import cloudscraper
|
||||
import time
|
||||
import uuid
|
||||
|
||||
@@ -27,8 +29,13 @@ import uuid
|
||||
class Request(object):
|
||||
"""HTTP helper class"""
|
||||
|
||||
def __init__(self, timeout=5):
|
||||
self.session = requests.Session()
|
||||
def __init__(self, timeout=5, mode="cloudscraper"):
|
||||
if mode == "cloudscraper":
|
||||
from .arlo_async import USER_AGENTS
|
||||
self.session = cloudscraper.CloudScraper(browser={"custom": USER_AGENTS["arlo"]})
|
||||
elif mode == "ip":
|
||||
self.session = requests.Session()
|
||||
self.session.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
|
||||
self.timeout = timeout
|
||||
|
||||
def gen_event_id(self):
|
||||
@@ -37,7 +44,7 @@ class Request(object):
|
||||
def get_time(self):
|
||||
return int(time.time_ns() / 1_000_000)
|
||||
|
||||
def _request(self, url, method='GET', params={}, headers={}, stream=False, raw=False):
|
||||
def _request(self, url, method='GET', params={}, headers={}, raw=False, skip_event_id=False):
|
||||
|
||||
## uncomment for debug logging
|
||||
"""
|
||||
@@ -51,14 +58,13 @@ class Request(object):
|
||||
req_log.propagate = True
|
||||
#"""
|
||||
|
||||
url = f'{url}?eventId={self.gen_event_id()}&time={self.get_time()}'
|
||||
if not skip_event_id:
|
||||
url = f'{url}?eventId={self.gen_event_id()}&time={self.get_time()}'
|
||||
|
||||
if method == 'GET':
|
||||
#print('COOKIES: ', self.session.cookies.get_dict())
|
||||
r = self.session.get(url, params=params, headers=headers, stream=stream, timeout=self.timeout)
|
||||
r = self.session.get(url, params=params, headers=headers, timeout=self.timeout)
|
||||
r.raise_for_status()
|
||||
if stream is True:
|
||||
return r
|
||||
elif method == 'PUT':
|
||||
r = self.session.put(url, json=params, headers=headers, timeout=self.timeout)
|
||||
r.raise_for_status()
|
||||
@@ -81,14 +87,14 @@ class Request(object):
|
||||
else:
|
||||
raise HTTPError('Request ({0} {1}) failed: {2}'.format(method, url, r.json()), response=r)
|
||||
|
||||
def get(self, url, params={}, headers={}, stream=False, raw=False):
|
||||
return self._request(url, 'GET', params=params, headers=headers, stream=stream, raw=raw)
|
||||
def get(self, url, **kwargs):
|
||||
return self._request(url, 'GET', **kwargs)
|
||||
|
||||
def put(self, url, params={}, headers={}, raw=False):
|
||||
return self._request(url, 'PUT', params=params, headers=headers, raw=raw)
|
||||
def put(self, url, **kwargs):
|
||||
return self._request(url, 'PUT', **kwargs)
|
||||
|
||||
def post(self, url, params={}, headers={}, raw=False):
|
||||
return self._request(url, 'POST', params=params, headers=headers, raw=raw)
|
||||
def post(self, url, **kwargs):
|
||||
return self._request(url, 'POST', **kwargs)
|
||||
|
||||
def options(self, url, headers={}, raw=False):
|
||||
return self._request(url, 'OPTIONS', headers=headers, raw=raw)
|
||||
def options(self, url, **kwargs):
|
||||
return self._request(url, 'OPTIONS', **kwargs)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from async_timeout import timeout as async_timeout
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import threading
|
||||
@@ -10,8 +12,9 @@ from typing import List, TYPE_CHECKING
|
||||
import scrypted_arlo_go
|
||||
|
||||
import scrypted_sdk
|
||||
from scrypted_sdk.types import Setting, Settings, Device, Camera, VideoCamera, VideoClips, VideoClip, VideoClipOptions, MotionSensor, AudioSensor, Battery, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType
|
||||
from scrypted_sdk.types import Setting, Settings, SettingValue, Device, Camera, VideoCamera, VideoClips, VideoClip, VideoClipOptions, MotionSensor, AudioSensor, Battery, Charger, ChargeState, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType
|
||||
|
||||
from .debug import EXPERIMENTAL
|
||||
from .base import ArloDeviceBase
|
||||
from .spotlight import ArloSpotlight, ArloFloodlight
|
||||
from .vss import ArloSirenVirtualSecuritySystem
|
||||
@@ -23,7 +26,7 @@ if TYPE_CHECKING:
|
||||
from .provider import ArloProvider
|
||||
|
||||
|
||||
class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, VideoClips, MotionSensor, AudioSensor, Battery):
|
||||
class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, VideoClips, MotionSensor, AudioSensor, Battery, Charger):
|
||||
MODELS_WITH_SPOTLIGHTS = [
|
||||
"vmc4040p",
|
||||
"vmc2030",
|
||||
@@ -75,12 +78,38 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
intercom_session = None
|
||||
light: ArloSpotlight = None
|
||||
vss: ArloSirenVirtualSecuritySystem = None
|
||||
picture_lock: asyncio.Lock = None
|
||||
|
||||
# eco mode bookkeeping
|
||||
last_picture: bytes = None
|
||||
last_picture_time: datetime = datetime(1970, 1, 1)
|
||||
|
||||
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
|
||||
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
|
||||
self.picture_lock = asyncio.Lock()
|
||||
|
||||
self.start_motion_subscription()
|
||||
self.start_audio_subscription()
|
||||
self.start_battery_subscription()
|
||||
self.create_task(self.delayed_init())
|
||||
|
||||
async def delayed_init(self) -> None:
|
||||
if not self.has_battery:
|
||||
return
|
||||
|
||||
iterations = 1
|
||||
while not self.stop_subscriptions:
|
||||
if iterations > 100:
|
||||
self.logger.error("Delayed init exceeded iteration limit, giving up")
|
||||
return
|
||||
|
||||
try:
|
||||
self.chargeState = ChargeState.Charging.value if self.wired_to_power else ChargeState.NotCharging.value
|
||||
return
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Delayed init failed, will try again: {e}")
|
||||
await asyncio.sleep(0.1)
|
||||
iterations += 1
|
||||
|
||||
def start_motion_subscription(self) -> None:
|
||||
def callback(motionDetected):
|
||||
@@ -104,7 +133,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
)
|
||||
|
||||
def start_battery_subscription(self) -> None:
|
||||
if self.wired_to_power:
|
||||
if not self.has_battery:
|
||||
return
|
||||
|
||||
def callback(batteryLevel):
|
||||
@@ -123,19 +152,18 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
ScryptedInterface.Settings.value,
|
||||
])
|
||||
|
||||
if self.two_way_audio:
|
||||
results.discard(ScryptedInterface.RTCSignalingChannel.value)
|
||||
results.add(ScryptedInterface.Intercom.value)
|
||||
if EXPERIMENTAL:
|
||||
if self.two_way_audio:
|
||||
results.discard(ScryptedInterface.RTCSignalingChannel.value)
|
||||
results.add(ScryptedInterface.Intercom.value)
|
||||
|
||||
if self.webrtc_emulation:
|
||||
results.add(ScryptedInterface.RTCSignalingChannel.value)
|
||||
results.discard(ScryptedInterface.Intercom.value)
|
||||
if self.webrtc_emulation:
|
||||
results.add(ScryptedInterface.RTCSignalingChannel.value)
|
||||
results.discard(ScryptedInterface.Intercom.value)
|
||||
|
||||
if self.has_battery:
|
||||
results.add(ScryptedInterface.Battery.value)
|
||||
|
||||
if self.wired_to_power:
|
||||
results.discard(ScryptedInterface.Battery.value)
|
||||
results.add(ScryptedInterface.Charger.value)
|
||||
|
||||
if self.has_siren or self.has_spotlight or self.has_floodlight:
|
||||
results.add(ScryptedInterface.DeviceProvider.value)
|
||||
@@ -146,9 +174,10 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
if self.has_cloud_recording:
|
||||
results.add(ScryptedInterface.VideoClips.value)
|
||||
|
||||
if not self._can_push_to_talk():
|
||||
results.discard(ScryptedInterface.RTCSignalingChannel.value)
|
||||
results.discard(ScryptedInterface.Intercom.value)
|
||||
if EXPERIMENTAL:
|
||||
if not self._can_push_to_talk():
|
||||
results.discard(ScryptedInterface.RTCSignalingChannel.value)
|
||||
results.discard(ScryptedInterface.Intercom.value)
|
||||
|
||||
return list(results)
|
||||
|
||||
@@ -215,6 +244,21 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def eco_mode(self) -> bool:
|
||||
if self.storage:
|
||||
return True if self.storage.getItem("eco_mode") else False
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def snapshot_throttle_interval(self) -> bool:
|
||||
interval = self.storage.getItem("snapshot_throttle_interval")
|
||||
if interval is None:
|
||||
interval = 60
|
||||
self.storage.setItem("snapshot_throttle_interval", interval)
|
||||
return int(interval)
|
||||
|
||||
@property
|
||||
def has_cloud_recording(self) -> bool:
|
||||
return self.provider.arlo.GetSmartFeatures(self.arlo_device).get("planFeatures", {}).get("eventRecording", False)
|
||||
@@ -244,18 +288,45 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
if self.has_battery:
|
||||
result.append(
|
||||
{
|
||||
"group": "General",
|
||||
"key": "wired_to_power",
|
||||
"title": "Plugged In to External Power",
|
||||
"value": self.wired_to_power,
|
||||
"description": "Informs Scrypted that this device is plugged in to an external power source. " + \
|
||||
"Will allow features like persistent prebuffer to work, however will no longer report this device's battery percentage. " + \
|
||||
"Will allow features like persistent prebuffer to work. " + \
|
||||
"Note that a persistent prebuffer may cause excess battery drain if the external power is not able to charge faster than the battery consumption rate.",
|
||||
"type": "boolean",
|
||||
},
|
||||
)
|
||||
if self._can_push_to_talk():
|
||||
result.append(
|
||||
{
|
||||
"group": "General",
|
||||
"key": "eco_mode",
|
||||
"title": "Eco Mode",
|
||||
"value": self.eco_mode,
|
||||
"description": "Configures Scrypted to limit the number of requests made to this camera. " + \
|
||||
"Additional eco mode settings will appear when this is turned on.",
|
||||
"type": "boolean",
|
||||
}
|
||||
)
|
||||
if self.eco_mode:
|
||||
result.append(
|
||||
{
|
||||
"group": "Eco Mode",
|
||||
"key": "snapshot_throttle_interval",
|
||||
"title": "Snapshot Throttle Interval",
|
||||
"value": self.snapshot_throttle_interval,
|
||||
"description": "Time, in minutes, to throttle snapshot requests. " + \
|
||||
"When eco mode is on, snapshot requests to the camera will be throttled for the given duration. " + \
|
||||
"Cached snapshots may be returned if the time since the last snapshot has not exceeded the interval. " + \
|
||||
"A value of 0 will disable throttling even when eco mode is on.",
|
||||
"type": "number",
|
||||
}
|
||||
)
|
||||
if self._can_push_to_talk() and EXPERIMENTAL:
|
||||
result.extend([
|
||||
{
|
||||
"group": "General",
|
||||
"key": "two_way_audio",
|
||||
"title": "(Experimental) Enable native two-way audio",
|
||||
"value": self.two_way_audio,
|
||||
@@ -263,6 +334,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
"type": "boolean",
|
||||
},
|
||||
{
|
||||
"group": "General",
|
||||
"key": "webrtc_emulation",
|
||||
"title": "(Highly Experimental) Emulate WebRTC Camera",
|
||||
"value": self.webrtc_emulation,
|
||||
@@ -274,10 +346,28 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
return result
|
||||
|
||||
@async_print_exception_guard
|
||||
async def putSetting(self, key, value) -> None:
|
||||
async def putSetting(self, key: str, value: SettingValue) -> None:
|
||||
if not self.validate_setting(key, value):
|
||||
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
|
||||
return
|
||||
|
||||
if key in ["webrtc_emulation", "two_way_audio", "wired_to_power"]:
|
||||
self.storage.setItem(key, value == "true" or value == True)
|
||||
await self.provider.discover_devices()
|
||||
elif key in ["eco_mode"]:
|
||||
self.storage.setItem(key, value == "true" or value == True)
|
||||
else:
|
||||
self.storage.setItem(key, value)
|
||||
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
|
||||
|
||||
def validate_setting(self, key: str, val: SettingValue) -> bool:
|
||||
if key == "snapshot_throttle_interval":
|
||||
try:
|
||||
val = int(val)
|
||||
except ValueError:
|
||||
self.logger.error(f"Invalid snapshot throttle interval '{val}' - must be an integer")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def getPictureOptions(self) -> List[ResponsePictureOptions]:
|
||||
return []
|
||||
@@ -296,13 +386,27 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
self.logger.warning(f"Could not fetch from prebuffer due to: {e}")
|
||||
self.logger.warning("Will try to fetch snapshot from Arlo cloud")
|
||||
|
||||
pic_url = await asyncio.wait_for(self.provider.arlo.TriggerFullFrameSnapshot(self.arlo_basestation, self.arlo_device), timeout=self.timeout)
|
||||
self.logger.debug(f"Got snapshot URL for at {pic_url}")
|
||||
async with self.picture_lock:
|
||||
if self.eco_mode and self.snapshot_throttle_interval > 0:
|
||||
if datetime.now() - self.last_picture_time <= timedelta(minutes=self.snapshot_throttle_interval):
|
||||
self.logger.info("Using cached image")
|
||||
return await scrypted_sdk.mediaManager.createMediaObject(self.last_picture, "image/jpeg")
|
||||
|
||||
if pic_url is None:
|
||||
raise Exception("Error taking snapshot")
|
||||
pic_url = await asyncio.wait_for(self.provider.arlo.TriggerFullFrameSnapshot(self.arlo_basestation, self.arlo_device), timeout=self.timeout)
|
||||
self.logger.debug(f"Got snapshot URL for at {pic_url}")
|
||||
|
||||
return await scrypted_sdk.mediaManager.createMediaObject(str.encode(pic_url), ScryptedMimeTypes.Url.value)
|
||||
if pic_url is None:
|
||||
raise Exception("Error taking snapshot")
|
||||
|
||||
async with async_timeout(self.timeout):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(pic_url) as resp:
|
||||
if resp.status != 200:
|
||||
raise Exception(f"Unexpected status downloading snapshot image: {resp.status}")
|
||||
self.last_picture = await resp.read()
|
||||
self.last_picture_time = datetime.now()
|
||||
|
||||
return await scrypted_sdk.mediaManager.createMediaObject(self.last_picture, "image/jpeg")
|
||||
|
||||
async def getVideoStreamOptions(self) -> List[ResponseMediaStreamOptions]:
|
||||
return [
|
||||
|
||||
1
plugins/arlo/src/arlo_plugin/debug.py
Normal file
1
plugins/arlo/src/arlo_plugin/debug.py
Normal file
@@ -0,0 +1 @@
|
||||
EXPERIMENTAL = False
|
||||
@@ -573,7 +573,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
self.scrypted_devices = {}
|
||||
|
||||
camera_devices = []
|
||||
provider_to_device_map = {}
|
||||
provider_to_device_map = {None: []}
|
||||
|
||||
basestations = self.arlo.GetDevices(['basestation', 'siren'])
|
||||
for basestation in basestations:
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
paho-mqtt==1.6.1
|
||||
sseclient==0.0.22
|
||||
aiohttp==3.8.4
|
||||
requests==2.28.2
|
||||
cachetools==5.3.0
|
||||
scrypted-arlo-go==0.0.1
|
||||
scrypted-arlo-go==0.0.2
|
||||
cloudscraper==1.2.71
|
||||
cryptography==38.0.4
|
||||
async-timeout==4.0.2
|
||||
--extra-index-url=https://www.piwheels.org/simple/
|
||||
--extra-index-url=https://bjia56.github.io/scrypted-arlo-go/
|
||||
--prefer-binary
|
||||
@@ -88,7 +88,10 @@ class CastDevice extends ScryptedDeviceBase implements MediaPlayer, Refresh, Eng
|
||||
}
|
||||
|
||||
client.removeAllListeners();
|
||||
client.close();
|
||||
try {
|
||||
client.close();
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
client.client.on('close', cleanup);
|
||||
client.on('error', err => {
|
||||
@@ -149,6 +152,14 @@ class CastDevice extends ScryptedDeviceBase implements MediaPlayer, Refresh, Eng
|
||||
}
|
||||
|
||||
async load(media: string | MediaObject, options: MediaPlayerOptions) {
|
||||
if (this.mediaPlayerPromise) {
|
||||
try {
|
||||
(await this.mediaPlayerPromise).close();
|
||||
} catch (e) {
|
||||
}
|
||||
this.mediaPlayerPromise = undefined;
|
||||
this.mediaPlayerStatus = undefined;
|
||||
}
|
||||
let url: string;
|
||||
let urlMimeType: string;
|
||||
|
||||
@@ -341,15 +352,7 @@ class CastDevice extends ScryptedDeviceBase implements MediaPlayer, Refresh, Eng
|
||||
});
|
||||
})
|
||||
|
||||
player.getStatus((err, status) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
this.mediaPlayerStatus = status;
|
||||
this.updateState();
|
||||
resolve(player);
|
||||
})
|
||||
resolve(player);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
6
plugins/cloud/package-lock.json
generated
6
plugins/cloud/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/cloud",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/cloud",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": "^3.1.4",
|
||||
"@scrypted/common": "file:../../common",
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.82",
|
||||
"version": "0.2.97",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"@types/nat-upnp": "^1.1.2",
|
||||
"@types/node": "^18.11.18"
|
||||
},
|
||||
"version": "0.1.13"
|
||||
"version": "0.1.14"
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ import { once } from 'events';
|
||||
import http from 'http';
|
||||
import HttpProxy from 'http-proxy';
|
||||
import https from 'https';
|
||||
import throttle from "lodash/throttle";
|
||||
import upnp from 'nat-upnp';
|
||||
import net, { AddressInfo } from 'net';
|
||||
import net from 'net';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import qs from 'query-string';
|
||||
@@ -210,6 +209,11 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
})
|
||||
|
||||
this.updateCors();
|
||||
|
||||
if (!this.storageSettings.values.token_info && process.env.SCRYPTED_CLOUD_TOKEN) {
|
||||
this.storageSettings.values.token_info = process.env.SCRYPTED_CLOUD_TOKEN;
|
||||
this.manager.registrationId.then(r => this.sendRegistrationId(r));
|
||||
}
|
||||
}
|
||||
|
||||
scheduleRefreshPortForward() {
|
||||
|
||||
@@ -29,7 +29,7 @@ class ChromecastViewCameraExample implements StartStop {
|
||||
}
|
||||
async stop() {
|
||||
device.running = false;
|
||||
return chromecast.stop();
|
||||
await chromecast.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.110",
|
||||
"version": "0.1.128",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.110",
|
||||
"version": "0.1.128",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.110",
|
||||
"version": "0.1.128",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -19,7 +19,7 @@ export class LauncherMixin extends ScryptedDeviceBase implements MixinProvider,
|
||||
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: DeviceState): Promise<any> {
|
||||
mixinDeviceState.applicationInfo = {
|
||||
icon: 'fa ' + typeToIcon(mixinDeviceState.type),
|
||||
href: '/endpoint/@scrypted/core/public/#/device/' + mixinDeviceState.id,
|
||||
href: '#/device/' + mixinDeviceState.id,
|
||||
}
|
||||
return mixinDevice;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,13 @@ const { systemManager, deviceManager, endpointManager } = sdk;
|
||||
const indexHtml = fs.readFileSync('dist/index.html').toString();
|
||||
|
||||
export function getAddresses() {
|
||||
const addresses = Object.entries(os.networkInterfaces()).filter(([iface]) => iface.startsWith('en') || iface.startsWith('eth') || iface.startsWith('wlan')).map(([_, addr]) => addr).flat().map(info => info.address).filter(address => address);
|
||||
const addresses: string[] = [];
|
||||
for (const [iface, nif] of Object.entries(os.networkInterfaces())) {
|
||||
if (iface.startsWith('en') || iface.startsWith('eth') || iface.startsWith('wlan')) {
|
||||
addresses.push(iface);
|
||||
addresses.push(...nif.map(addr => addr.address));
|
||||
}
|
||||
}
|
||||
return addresses;
|
||||
}
|
||||
|
||||
@@ -29,7 +35,6 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
router: any = Router();
|
||||
publicRouter: any = Router();
|
||||
mediaCore: MediaCore;
|
||||
launcher: LauncherMixin;
|
||||
scriptCore: ScriptCore;
|
||||
aggregateCore: AggregateCore;
|
||||
automationCore: AutomationCore;
|
||||
@@ -37,17 +42,18 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
localAddresses: string[];
|
||||
storageSettings = new StorageSettings(this, {
|
||||
localAddresses: {
|
||||
title: 'Scrypted Server Address',
|
||||
description: 'The IP address used by the Scrypted server. Set this to the wired IP address to prevent usage of a wireless address.',
|
||||
title: 'Scrypted Server Addresses',
|
||||
description: 'The IP addresses used by the Scrypted server. Set this to the wired IP address to prevent usage of a wireless address.',
|
||||
combobox: true,
|
||||
multiple: true,
|
||||
async onGet() {
|
||||
return {
|
||||
choices: getAddresses(),
|
||||
};
|
||||
},
|
||||
mapGet: () => this.localAddresses?.[0],
|
||||
mapGet: () => this.localAddresses,
|
||||
onPut: async (oldValue, newValue) => {
|
||||
this.localAddresses = newValue ? [newValue] : undefined;
|
||||
this.localAddresses = newValue?.length ? newValue : undefined;
|
||||
const service = await sdk.systemManager.getComponent('addresses');
|
||||
service.setLocalAddresses(this.localAddresses);
|
||||
},
|
||||
@@ -66,7 +72,6 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
},
|
||||
);
|
||||
this.mediaCore = new MediaCore('mediacore');
|
||||
})();
|
||||
(async () => {
|
||||
await deviceManager.onDeviceDiscovered(
|
||||
@@ -77,7 +82,6 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
},
|
||||
);
|
||||
this.scriptCore = new ScriptCore();
|
||||
})();
|
||||
|
||||
(async () => {
|
||||
@@ -89,7 +93,6 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
},
|
||||
);
|
||||
this.automationCore = new AutomationCore();
|
||||
})();
|
||||
|
||||
deviceManager.onDeviceDiscovered({
|
||||
@@ -112,7 +115,6 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
},
|
||||
);
|
||||
this.aggregateCore = new AggregateCore();
|
||||
})();
|
||||
|
||||
|
||||
@@ -125,19 +127,19 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
},
|
||||
);
|
||||
this.users = new UsersCore();
|
||||
})();
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
try {
|
||||
const service = await sdk.systemManager.getComponent('addresses');
|
||||
this.localAddresses = await service.getLocalAddresses();
|
||||
this.localAddresses = await service.getLocalAddresses(true);
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
await this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
@@ -146,15 +148,15 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
if (nativeId === 'launcher')
|
||||
return new LauncherMixin('launcher');
|
||||
if (nativeId === 'mediacore')
|
||||
return this.mediaCore;
|
||||
return this.mediaCore ||= new MediaCore();
|
||||
if (nativeId === ScriptCoreNativeId)
|
||||
return this.scriptCore;
|
||||
return this.scriptCore ||= new ScriptCore();
|
||||
if (nativeId === AutomationCoreNativeId)
|
||||
return this.automationCore;
|
||||
return this.automationCore ||= new AutomationCore()
|
||||
if (nativeId === AggregateCoreNativeId)
|
||||
return this.aggregateCore;
|
||||
return this.aggregateCore ||= new AggregateCore();
|
||||
if (nativeId === UsersNativeId)
|
||||
return this.users;
|
||||
return this.users ||= new UsersCore();
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
@@ -211,9 +213,9 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
const u = new URL(endpoint);
|
||||
|
||||
const rewritten = indexHtml
|
||||
.replace('href="/endpoint/@scrypted/core/public/manifest.json"', `href="/endpoint/@scrypted/core/public/manifest.json${u.search}"`)
|
||||
.replace('href="/endpoint/@scrypted/core/public/img/icons/apple-touch-icon-152x152.png"', `href="/endpoint/@scrypted/core/public/img/icons/apple-touch-icon-152x152.png${u.search}"`)
|
||||
.replace('href="/endpoint/@scrypted/core/public/img/icons/safari-pinned-tab.svg"', `href="/endpoint/@scrypted/core/public/img/icons/safari-pinned-tab.svg${u.search}"`)
|
||||
.replace('href="manifest.json"', `href="manifest.json${u.search}"`)
|
||||
.replace('href="img/icons/apple-touch-icon-152x152.png"', `href="img/icons/apple-touch-icon-152x152.png${u.search}"`)
|
||||
.replace('href="img/icons/safari-pinned-tab.svg"', `href="img/icons/safari-pinned-tab.svg${u.search}"`)
|
||||
;
|
||||
response.send(rewritten, {
|
||||
headers: {
|
||||
|
||||
@@ -5,6 +5,7 @@ const { systemManager, deviceManager, mediaManager, endpointManager } = sdk;
|
||||
import { RequestMediaObjectHost, FileHost, BufferHost } from './converters';
|
||||
import url from 'url';
|
||||
|
||||
export const MediaCoreNativeId = 'mediacore';
|
||||
export class MediaCore extends ScryptedDeviceBase implements DeviceProvider, BufferConverter, HttpRequestHandler {
|
||||
httpHost: BufferHost;
|
||||
httpsHost: BufferHost;
|
||||
@@ -12,8 +13,8 @@ export class MediaCore extends ScryptedDeviceBase implements DeviceProvider, Buf
|
||||
fileHost: FileHost;
|
||||
filesHost: FileHost;
|
||||
|
||||
constructor(nativeId: string) {
|
||||
super(nativeId);
|
||||
constructor() {
|
||||
super(MediaCoreNativeId);
|
||||
|
||||
this.fromMimeType = ScryptedMimeTypes.SchemePrefix + 'scrypted-media';
|
||||
this.toMimeType = ScryptedMimeTypes.MediaObject;
|
||||
|
||||
@@ -148,11 +148,12 @@
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Drawer from "./components/Drawer.vue";
|
||||
import { removeAlert, getAlertIcon } from "./components/helpers";
|
||||
import router from "./router";
|
||||
import { getCurrentBaseUrl, logoutScryptedClient } from '../../../../packages/client/src/index';
|
||||
import Login from "./Login.vue";
|
||||
import Reconnect from "./Reconnect.vue";
|
||||
import Drawer from "./components/Drawer.vue";
|
||||
import { getAlertIcon, removeAlert } from "./components/helpers";
|
||||
import router from "./router";
|
||||
import store from "./store";
|
||||
|
||||
export default {
|
||||
@@ -176,7 +177,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
goHome() {
|
||||
window.location ='/';
|
||||
window.location = getCurrentBaseUrl();
|
||||
},
|
||||
toggleDarkMode() {
|
||||
this.darkMode = !this.darkMode;
|
||||
@@ -186,8 +187,9 @@ export default {
|
||||
reload() {
|
||||
window.location.reload();
|
||||
},
|
||||
logout() {
|
||||
axios.get("/logout").then(() => window.location.reload());
|
||||
async logout() {
|
||||
await logoutScryptedClient(getCurrentBaseUrl());
|
||||
window.location.reload();
|
||||
},
|
||||
async clearAlerts() {
|
||||
const alerts = await this.$scrypted.systemManager.getComponent("alerts");
|
||||
|
||||
@@ -118,14 +118,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Login from "./Login.vue";
|
||||
import App from "./App.vue";
|
||||
import store from "./store";
|
||||
import VueRouter from "vue-router";
|
||||
import Reconnect from "./Reconnect.vue";
|
||||
import { getAllDevices } from "./common/mixin";
|
||||
import { ScryptedInterface } from "@scrypted/types";
|
||||
import axios from 'axios';
|
||||
import VueRouter from "vue-router";
|
||||
import { combineBaseUrl, getCurrentBaseUrl, logoutScryptedClient } from '../../../../packages/client/src/index';
|
||||
import App from "./App.vue";
|
||||
import Login from "./Login.vue";
|
||||
import Reconnect from "./Reconnect.vue";
|
||||
import { getAllDevices } from "./common/mixin";
|
||||
import store from "./store";
|
||||
|
||||
const nvrInstall = '/component/plugin/install/@scrypted/nvr'
|
||||
|
||||
@@ -163,8 +164,9 @@ export default {
|
||||
this.refreshApplications();
|
||||
},
|
||||
methods: {
|
||||
logout() {
|
||||
axios.get("/logout").then(() => window.location.reload());
|
||||
async logout() {
|
||||
await logoutScryptedClient(getCurrentBaseUrl());
|
||||
window.location.reload();
|
||||
},
|
||||
refreshApplications() {
|
||||
if (!this.$store.state.isConnected || !this.$store.state.isLoggedIn || this.$route.name !== 'Launcher')
|
||||
@@ -176,10 +178,13 @@ export default {
|
||||
const applications = getAllDevices(systemManager).filter(device => device.interfaces.includes(ScryptedInterface.LauncherApplication));
|
||||
this.applications = applications.map(app => {
|
||||
const appId = app.interfaces.includes(ScryptedInterface.ScryptedPlugin) ? app.pluginId : app.id;
|
||||
const baseUrl = getCurrentBaseUrl();
|
||||
const defaultUrl = combineBaseUrl(baseUrl, `endpoint/${appId}/public/`);
|
||||
|
||||
const ret = {
|
||||
name: (app.applicationInfo && app.applicationInfo.name) || app.name,
|
||||
icon: app.applicationInfo && app.applicationInfo.icon,
|
||||
href: (app.applicationInfo && app.applicationInfo.href) || `/endpoint/${appId}/public/`,
|
||||
href: (app.applicationInfo && app.applicationInfo.href) || defaultUrl,
|
||||
};
|
||||
return ret;
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Vue from "vue";
|
||||
import { checkScryptedClientLogin, connectScryptedClient, loginScryptedClient, redirectScryptedLogin } from '../../../../packages/client/src/index';
|
||||
import { checkScryptedClientLogin, connectScryptedClient, getCurrentBaseUrl, loginScryptedClient, redirectScryptedLogin } from '../../../../packages/client/src/index';
|
||||
import store from './store';
|
||||
|
||||
function hasValue(state: any, property: string) {
|
||||
@@ -22,7 +22,7 @@ function isValidDevice(id: string) {
|
||||
|
||||
export function loginScrypted(username: string, password: string, change_password: string) {
|
||||
return loginScryptedClient({
|
||||
baseUrl: undefined,
|
||||
baseUrl: getCurrentBaseUrl(),
|
||||
username,
|
||||
password,
|
||||
change_password,
|
||||
@@ -33,6 +33,8 @@ Vue.use(Vue => {
|
||||
Vue.prototype.$connectScrypted = () => {
|
||||
const clientPromise = connectScryptedClient({
|
||||
pluginId: '@scrypted/core',
|
||||
// need this in case the scrypted server is proxied.
|
||||
baseUrl: getCurrentBaseUrl(),
|
||||
});
|
||||
|
||||
store.commit("setHasLogin", undefined);
|
||||
@@ -40,11 +42,14 @@ Vue.use(Vue => {
|
||||
store.commit("setUsername", undefined);
|
||||
store.commit("setIsConnected", undefined);
|
||||
|
||||
return checkScryptedClientLogin()
|
||||
return checkScryptedClientLogin({
|
||||
baseUrl: getCurrentBaseUrl(),
|
||||
})
|
||||
.then(response => {
|
||||
if (response.redirect) {
|
||||
redirectScryptedLogin({
|
||||
redirect: response.redirect,
|
||||
baseUrl: getCurrentBaseUrl(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { Terminal } from "xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import eio from "engine.io-client";
|
||||
import { sleep } from "../common/sleep";
|
||||
import { getCurrentBaseUrl } from "../../../../../packages/client/src";
|
||||
|
||||
export default {
|
||||
props: ["deviceId"],
|
||||
@@ -64,10 +65,12 @@ export default {
|
||||
},
|
||||
reconnect(term) {
|
||||
this.buffer = [];
|
||||
const endpointPath = `/endpoint/@scrypted/core`;
|
||||
const baseUrl = getCurrentBaseUrl();
|
||||
const eioPath = `endpoint/@scrypted/core/engine.io/console/${this.deviceId}`;
|
||||
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
|
||||
|
||||
const options = {
|
||||
path: `${endpointPath}/engine.io/console/${this.deviceId}`,
|
||||
path: eioEndpoint,
|
||||
};
|
||||
const rootLocation = `${window.location.protocol}//${window.location.host}`;
|
||||
this.socket = eio(rootLocation, options);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import eio from "engine.io-client";
|
||||
import { getCurrentBaseUrl } from "../../../../../packages/client/src";
|
||||
|
||||
export default {
|
||||
props: ["deviceId"],
|
||||
@@ -28,10 +29,12 @@ export default {
|
||||
term.open(this.$refs.terminal);
|
||||
fitAddon.fit();
|
||||
|
||||
const endpointPath = `/endpoint/@scrypted/core`;
|
||||
const baseUrl = getCurrentBaseUrl();
|
||||
const eioPath = `endpoint/@scrypted/core/engine.io/repl/${this.deviceId}`;
|
||||
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
|
||||
|
||||
const options = {
|
||||
path: `${endpointPath}/engine.io/repl/${this.deviceId}`,
|
||||
path: eioEndpoint,
|
||||
};
|
||||
const rootLocation = `${window.location.protocol}//${window.location.host}`;
|
||||
this.socket = eio(rootLocation, options);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import eio from "engine.io-client";
|
||||
import { getCurrentBaseUrl } from "../../../../../../packages/client/src";
|
||||
|
||||
export default {
|
||||
socket: null,
|
||||
@@ -27,8 +28,11 @@ export default {
|
||||
term.open(this.$refs.terminal);
|
||||
fitAddon.fit();
|
||||
|
||||
const baseUrl = getCurrentBaseUrl();
|
||||
const eioPath = `engine.io/shell`;
|
||||
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
|
||||
const options = {
|
||||
path: `/engine.io/shell`,
|
||||
path: eioEndpoint,
|
||||
};
|
||||
const rootLocation = `${window.location.protocol}//${window.location.host}`;
|
||||
this.socket = eio(rootLocation, options);
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { ScryptedStatic } from '@scrypted/types';
|
||||
import { DeviceManager } from '@scrypted/types';
|
||||
import { DeviceCreator } from '@scrypted/types';
|
||||
import { ScryptedInterface } from '@scrypted/types';
|
||||
import { Scriptable } from '@scrypted/types';
|
||||
import { SystemManager } from '@scrypted/types';
|
||||
import { DeviceCreator, Scriptable, ScryptedInterface, ScryptedStatic, SystemManager } from '@scrypted/types';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import semver from 'semver';
|
||||
import { getAllDevices } from '../../common/mixin';
|
||||
import { sleep } from '../../common/sleep';
|
||||
import { getComponentWebPath } from "../helpers";
|
||||
const componentPath = getComponentWebPath('script');
|
||||
const pluginSnapshot = require("!!raw-loader!./plugin-snapshot.ts").default.split('\n')
|
||||
.filter(line => !line.includes('SCRYPTED_FILTER_EXAMPLE_LINE'))
|
||||
.join('\n')
|
||||
.trim();
|
||||
|
||||
import { getComponentWebPath } from "../helpers";
|
||||
const componentPath = getComponentWebPath('script');
|
||||
|
||||
export interface PluginUpdateCheck {
|
||||
updateAvailable?: string;
|
||||
versions: any;
|
||||
@@ -64,11 +59,11 @@ export async function checkUpdate(npmPackage: string, npmPackageVersion: string)
|
||||
}
|
||||
|
||||
export async function installNpm(systemManager: SystemManager, npmPackage: string, version?: string): Promise<string> {
|
||||
let suffix = version ? `/${version}` : '';
|
||||
const response = await axios.post(
|
||||
`${componentPath}/install/${npmPackage}${suffix}`
|
||||
);
|
||||
return response.data.id;
|
||||
const plugins = await systemManager.getComponent('plugins');
|
||||
await plugins.installNpm(npmPackage, version);
|
||||
await sleep(0);
|
||||
const plugin = systemManager.getDeviceById(npmPackage)
|
||||
return plugin.id;
|
||||
}
|
||||
|
||||
export function getNpmPath(npmPackage: string) {
|
||||
|
||||
@@ -2,30 +2,28 @@
|
||||
<span>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn small text v-on="on" :href="`/endpoint/${device.id}/public/`">
|
||||
<v-btn small text v-on="on" :href="endpointHref">
|
||||
<font-awesome-icon size="lg" :icon="['fab', 'chrome']" :color="colors.blue.base" />
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>View the public endpoint of this plugin.</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn small text v-on="on" :href="`/endpoint/${device.id}/`">
|
||||
<font-awesome-icon size="lg" icon="user-secret" :color="colors.red.base" />
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>View the private http endpoint of this plugin.</span>
|
||||
</v-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import { getCurrentBaseUrl } from "../../../../../packages/client/src";
|
||||
import RPCInterface from "./RPCInterface.vue";
|
||||
import colors from "vuetify/es5/util/colors";
|
||||
|
||||
export default {
|
||||
mixins: [RPCInterface],
|
||||
data() {
|
||||
const baseUrl = getCurrentBaseUrl();
|
||||
const endpoint = `endpoint/${this.device.id}/public/`;
|
||||
const endpointHref = baseUrl ? new URL(endpoint, baseUrl).pathname : '/' + endpoint;
|
||||
|
||||
return {
|
||||
endpointHref,
|
||||
colors
|
||||
};
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<script>
|
||||
import qs from 'query-string';
|
||||
import RPCInterface from "./RPCInterface.vue";
|
||||
import { getCurrentBaseUrl } from '../../../../../packages/client/src';
|
||||
|
||||
export default {
|
||||
mixins: [RPCInterface],
|
||||
@@ -16,9 +17,18 @@ export default {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
onClick: function () {
|
||||
// https://stackoverflow.com/a/39387533
|
||||
const windowReference = this.isIFrame() ? window.open(undefined, '_blank') : undefined;
|
||||
onClick: async function () {
|
||||
// must escape iframe for login.
|
||||
if (this.isIFrame()) {
|
||||
const endpointManager = this.$scrypted.endpointManager;
|
||||
const ep = await endpointManager.getPublicLocalEndpoint();
|
||||
const u = new URL(ep);
|
||||
u.hash = window.location.hash;
|
||||
u.pathname = '/endpoint/@scrypted/core/public/';
|
||||
window.open(u.toString(), '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
this.rpc()
|
||||
.getOauthUrl()
|
||||
.then(data => {
|
||||
@@ -51,10 +61,7 @@ export default {
|
||||
r: window.location.toString(),
|
||||
});
|
||||
url.search = qs.stringify(querystring);
|
||||
if (windowReference)
|
||||
windowReference.location = url.toString();
|
||||
else
|
||||
window.location = url.toString();
|
||||
window.location = url.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ module.exports = {
|
||||
// clientsClaim: true,
|
||||
}
|
||||
},
|
||||
publicPath: process.env.NODE_ENV === 'production' ? '/endpoint/@scrypted/core/public' : '/',
|
||||
publicPath: process.env.NODE_ENV === 'production' ? '' : '/',
|
||||
|
||||
runtimeCompiler: true,
|
||||
devServer: {
|
||||
|
||||
6
plugins/coreml/package-lock.json
generated
6
plugins/coreml/package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.15",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.15",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.85",
|
||||
"version": "0.2.101",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -34,12 +34,11 @@
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"Settings",
|
||||
"BufferConverter",
|
||||
"ObjectDetection"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.12"
|
||||
"version": "0.1.15"
|
||||
}
|
||||
|
||||
@@ -22,12 +22,9 @@ def parse_label_contents(contents: str):
|
||||
ret[row_number] = content.strip()
|
||||
return ret
|
||||
|
||||
|
||||
MIME_TYPE = 'x-scrypted-coreml/x-raw-image'
|
||||
|
||||
class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Settings):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(MIME_TYPE, nativeId=nativeId)
|
||||
super().__init__(nativeId=nativeId)
|
||||
|
||||
labelsFile = self.downloadFile('https://raw.githubusercontent.com/koush/coreml-survival-guide/master/MobileNetV2%2BSSDLite/coco_labels.txt', 'coco_labels.txt')
|
||||
modelFile = self.downloadFile('https://github.com/koush/coreml-survival-guide/raw/master/MobileNetV2%2BSSDLite/ObjectDetection/ObjectDetection/MobileNetV2_SSDLite.mlmodel', 'MobileNetV2_SSDLite.mlmodel')
|
||||
@@ -58,11 +55,11 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
else:
|
||||
out_dict = self.model.predict({'image': input, 'confidenceThreshold': self.minThreshold })
|
||||
|
||||
coordinatesList = out_dict['coordinates']
|
||||
coordinatesList = out_dict['coordinates'].astype(float)
|
||||
|
||||
objs = []
|
||||
|
||||
for index, confidenceList in enumerate(out_dict['confidence']):
|
||||
for index, confidenceList in enumerate(out_dict['confidence'].astype(float)):
|
||||
values = confidenceList
|
||||
maxConfidenceIndex = max(range(len(values)), key=values.__getitem__)
|
||||
maxConfidence = confidenceList[maxConfidenceIndex]
|
||||
|
||||
4
plugins/doorbird/.gitignore
vendored
Normal file
4
plugins/doorbird/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
dist/
|
||||
11
plugins/doorbird/.npmignore
Normal file
11
plugins/doorbird/.npmignore
Normal file
@@ -0,0 +1,11 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
*.map
|
||||
fs
|
||||
src
|
||||
.vscode
|
||||
dist/*.js
|
||||
dist/*.txt
|
||||
HAP-NodeJS
|
||||
.gitmodules
|
||||
23
plugins/doorbird/.vscode/launch.json
vendored
Normal file
23
plugins/doorbird/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Scrypted Debugger",
|
||||
"address": "${config:scrypted.debugHost}",
|
||||
"port": 10081,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"**/plugin-remote-worker.*",
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"preLaunchTask": "scrypted: deploy+debug",
|
||||
"sourceMaps": true,
|
||||
"localRoot": "${workspaceFolder}/out",
|
||||
"remoteRoot": "/plugin/",
|
||||
"type": "node"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
plugins/doorbird/.vscode/settings.json
vendored
Normal file
4
plugins/doorbird/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
20
plugins/doorbird/.vscode/tasks.json
vendored
Normal file
20
plugins/doorbird/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "scrypted: deploy+debug",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "silent",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
},
|
||||
"command": "npm run scrypted-vscode-launch ${config:scrypted.debugHost}",
|
||||
},
|
||||
]
|
||||
}
|
||||
9
plugins/doorbird/README.md
Normal file
9
plugins/doorbird/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Doorbird Plugin for Scrypted
|
||||
|
||||
The Doorbird Plugin bridges compatible Doorbird video doorbell cameras to Scrypted.
|
||||
|
||||
# Notes
|
||||
* Make sure that the user you want to use for the Doorbird plugin login has the API access rights.
|
||||
* Doorbrid cameras are quite limited in terms of maximum number of concurrent streams. Keep this in mind if you are also using other software with the Doorbird station. You have the possibility to override the internally used RTSP URL and provide another RTSP server which provides the video stream.
|
||||
* The doorbird mobile apps always have precedence over the public LAN API. So when somebody uses the Doorbird app to talk to the Doorbird station, the streams will be interrupted.
|
||||
* The doorbird camera just provides JPEG snapshots with VGA resolution. You can use the scrypted snapshot plugin to get a snapshot from the higher resolution video stream. Just set the option in the snapshot plugin to "enabled".
|
||||
349
plugins/doorbird/package-lock.json
generated
Normal file
349
plugins/doorbird/package-lock.json
generated
Normal file
@@ -0,0 +1,349 @@
|
||||
{
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"doorbird": "^2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^18.15.11",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"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.2.97",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.15.9",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
"scrypted-debug": "bin/scrypted-debug.js",
|
||||
"scrypted-deploy": "bin/scrypted-deploy.js",
|
||||
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
|
||||
"scrypted-package-json": "bin/scrypted-package-json.js",
|
||||
"scrypted-setup-project": "bin/scrypted-setup-project.js",
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@koush/axios-digest-auth": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
|
||||
"integrity": "sha512-EZMM0gMJ3hMUD4EuUqSwP6UGt5Vmw2TZtY7Ypec55AnxkExSXM0ySgPtqkAcnL43g1R27yAg/dQL7dRTLMqO3Q==",
|
||||
"dependencies": {
|
||||
"auth-header": "^1.0.0",
|
||||
"axios": "^0.21.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@koush/axios-digest-auth/node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
"resolved": "../../common",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/auth-header": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz",
|
||||
"integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz",
|
||||
"integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chacha-js": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chacha-js/-/chacha-js-2.1.1.tgz",
|
||||
"integrity": "sha512-0ySdjUv/oUkr2cjCo00CNil8Y9f39nm5/3pCgc6hO3X7LvMLBnmugQ5WZ+3Z2SwP9jX7oMIjU3m6p23thtMnHA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"readable-stream": "^1.0.33"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "src/bin/cross-env.js",
|
||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/doorbird": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/doorbird/-/doorbird-2.1.2.tgz",
|
||||
"integrity": "sha512-ivwwsS/nOslDnuLg3UB60Axo76w5LQuZ67mCPEeWFr5+HbGYRL7PCY3iLjWYaIakh5+IvZyFPHKR4yHAvAc1WQ==",
|
||||
"dependencies": {
|
||||
"axios": "^1.2.1",
|
||||
"chacha-js": "^2.1.1",
|
||||
"libsodium-wrappers-sumo": "^0.7.11"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/libsodium-sumo": {
|
||||
"version": "0.7.11",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.11.tgz",
|
||||
"integrity": "sha512-bY+7ph7xpk51Ez2GbE10lXAQ5sJma6NghcIDaSPbM/G9elfrjLa0COHl/7P6Wb/JizQzl5UQontOOP1z0VwbLA=="
|
||||
},
|
||||
"node_modules/libsodium-wrappers-sumo": {
|
||||
"version": "0.7.11",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.11.tgz",
|
||||
"integrity": "sha512-DGypHOmJbB1nZn89KIfGOAkDgfv5N6SBGC3Qvmy/On0P0WD1JQvNRS/e3UL3aFF+xC0m+MYz5M+MnRnK2HMrKQ==",
|
||||
"dependencies": {
|
||||
"libsodium-sumo": "^0.7.11"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "0.0.1",
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
plugins/doorbird/package.json
Normal file
46
plugins/doorbird/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
"build": "scrypted-webpack",
|
||||
"prepublishOnly": "cross-env NODE_ENV=production scrypted-webpack",
|
||||
"prescrypted-vscode-launch": "scrypted-webpack",
|
||||
"scrypted-vscode-launch": "scrypted-deploy-debug",
|
||||
"scrypted-deploy-debug": "scrypted-deploy-debug",
|
||||
"scrypted-debug": "scrypted-debug",
|
||||
"scrypted-deploy": "scrypted-deploy",
|
||||
"scrypted-readme": "scrypted-readme",
|
||||
"scrypted-package-json": "scrypted-package-json"
|
||||
},
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
"plugin",
|
||||
"doorbird"
|
||||
],
|
||||
"scrypted": {
|
||||
"name": "Doorbird Plugin",
|
||||
"type": "DeviceProvider",
|
||||
"interfaces": [
|
||||
"DeviceProvider",
|
||||
"DeviceCreator",
|
||||
"Settings"
|
||||
],
|
||||
"pluginDependencies": [
|
||||
"@scrypted/prebuffer-mixin",
|
||||
"@scrypted/pam-diff",
|
||||
"@scrypted/snapshot"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"doorbird": "^2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^18.15.11",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
92
plugins/doorbird/src/doorbird-api.ts
Normal file
92
plugins/doorbird/src/doorbird-api.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import Doorbird, { DoorbirdUdpSocket, Scheme, Response, DoorbirdInfoBHA } from 'doorbird';
|
||||
|
||||
export interface ApiRingEvent {
|
||||
event: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
export interface ApiMotionEvent {
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export type ApiRingCallback = (event: ApiRingEvent) => void;
|
||||
export type ApiMotionCallback = (event: ApiMotionEvent) => void;
|
||||
|
||||
export class DoorbirdAPI {
|
||||
|
||||
private console?: Console
|
||||
private doorbird: Doorbird;
|
||||
private doorbirdUdpSocket: DoorbirdUdpSocket;
|
||||
private ringCallback: ApiRingCallback;
|
||||
private motionCallback: ApiMotionCallback;
|
||||
private intercomId: String;
|
||||
|
||||
constructor(host: string, username: string, password: string, console?: Console) {
|
||||
this.console = console;
|
||||
this.doorbird = new Doorbird({
|
||||
scheme: Scheme.http,
|
||||
host: host,
|
||||
username: username,
|
||||
password: password
|
||||
});
|
||||
this.intercomId = username.substring(0, 6);
|
||||
this.console?.log("Doorbird: Our intercomId is: ", this.intercomId);
|
||||
}
|
||||
|
||||
startEventSocket() {
|
||||
this.console?.log("Doorbird: starting event socket listening...");
|
||||
|
||||
// initialize dgram UDP socket where Doorbird stations broadcast their event info
|
||||
this.doorbirdUdpSocket = this.doorbird.startUdpSocket(6524); // 6524 or 35344 - both shall contain the same payload
|
||||
|
||||
// register a listener for ring events
|
||||
this.doorbirdUdpSocket.registerRingListener(ringEvent => {
|
||||
this.console?.log("Doorbird: Event from IntercomId:", ringEvent.intercomId);
|
||||
// Make sure that we only call this if the intercom ID matches our desired one
|
||||
if (ringEvent.intercomId === this.intercomId) {
|
||||
this.ringCallback({
|
||||
event: ringEvent.event,
|
||||
timestamp: ringEvent.timestamp
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// register a listener for motion events
|
||||
this.doorbirdUdpSocket.registerMotionListener(motionEvent => {
|
||||
this.console?.log("Doorbird: Event from IntercomId:", motionEvent.intercomId);
|
||||
// Make sure that we only call this if the intercom ID matches our desired one
|
||||
if (motionEvent.intercomId === this.intercomId) {
|
||||
this.motionCallback({
|
||||
timestamp: motionEvent.timestamp
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stopEventSocket() {
|
||||
this.console?.log("Doorbird: stopping event socket listening...");
|
||||
this.doorbirdUdpSocket.close();
|
||||
}
|
||||
|
||||
registerRingCallback(ringCallback: ApiRingCallback) {
|
||||
this.ringCallback = ringCallback;
|
||||
}
|
||||
|
||||
registerMotionCallback(motionCallback: ApiMotionCallback) {
|
||||
this.motionCallback = motionCallback;
|
||||
}
|
||||
|
||||
async getImage(): Promise<Buffer> {
|
||||
this.console?.log("Doorbird: getting JPEG image...");
|
||||
return this.doorbird.getImage();
|
||||
}
|
||||
|
||||
async getInfo(): Promise<any> {
|
||||
const dbInfo = await this.doorbird.getInfo();
|
||||
return {
|
||||
deviceType: dbInfo.BHA.VERSION[0]['DEVICE-TYPE'],
|
||||
firmwareVersion: dbInfo.BHA.VERSION[0].FIRMWARE,
|
||||
buildNumber: dbInfo.BHA.VERSION[0].BUILD_NUMBER,
|
||||
serialNumber: dbInfo.BHA.VERSION[0].WIFI_MAC_ADDR,
|
||||
}
|
||||
}
|
||||
}
|
||||
565
plugins/doorbird/src/main.ts
Normal file
565
plugins/doorbird/src/main.ts
Normal file
@@ -0,0 +1,565 @@
|
||||
import { listenZero } from '@scrypted/common/src/listen-cluster';
|
||||
import sdk, { BinarySensor, Camera, DeviceProvider, DeviceCreator, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, PictureOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, MotionSensor } from '@scrypted/sdk';
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
|
||||
import net from 'net';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { PassThrough, Readable } from "stream";
|
||||
import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import { readLength } from "@scrypted/common/src/read-stream";
|
||||
import { ApiRingEvent, ApiMotionEvent, DoorbirdAPI } from "./doorbird-api";
|
||||
|
||||
const { deviceManager, mediaManager } = sdk;
|
||||
|
||||
class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, VideoCamera, Settings, BinarySensor, MotionSensor {
|
||||
doorbirdApi: DoorbirdAPI | undefined;
|
||||
binarySensorTimeout: NodeJS.Timeout;
|
||||
motionSensorTimeout: NodeJS.Timeout;
|
||||
doorbellAudioActive: boolean;
|
||||
audioTXProcess: ChildProcess;
|
||||
audioRXProcess: ChildProcess;
|
||||
audioSilenceProcess: ChildProcess;
|
||||
audioRXClientSocket: net.Socket;
|
||||
pendingPicture: Promise<MediaObject>;
|
||||
|
||||
constructor(nativeId: string, public provider: DoorbirdCamProvider) {
|
||||
super(nativeId);
|
||||
this.binaryState = false;
|
||||
this.doorbellAudioActive = false;
|
||||
|
||||
this.updateDeviceInfo();
|
||||
}
|
||||
|
||||
getDoorbirdApi() {
|
||||
const ip = this.storage.getItem('ip');
|
||||
if (!ip)
|
||||
return undefined;
|
||||
|
||||
if (!this.doorbirdApi) {
|
||||
this.doorbirdApi = new DoorbirdAPI(this.getIPAddress(), this.getUsername(), this.getPassword(), this.console);
|
||||
|
||||
this.getDoorbirdApi()?.registerRingCallback((event: ApiRingEvent) => {
|
||||
this.console?.log("Ring event");
|
||||
this.console?.log("Event:", event.event);
|
||||
this.console?.log("Time:", event.timestamp);
|
||||
this.triggerBinarySensor();
|
||||
});
|
||||
this.getDoorbirdApi()?.registerMotionCallback((event: ApiMotionEvent) => {
|
||||
this.console?.log("Motion event");
|
||||
this.console?.log("Time:", event.timestamp);
|
||||
this.triggerMotionSensor();
|
||||
});
|
||||
this.getDoorbirdApi()?.startEventSocket();
|
||||
}
|
||||
return this.doorbirdApi;
|
||||
}
|
||||
|
||||
async updateDeviceInfo(): Promise<void> {
|
||||
const ip = this.storage.getItem('ip');
|
||||
if (!ip)
|
||||
return;
|
||||
|
||||
const deviceInfo: DeviceInformation = {
|
||||
...this.info,
|
||||
ip
|
||||
};
|
||||
|
||||
const response = await this.getDoorbirdApi()?.getInfo();
|
||||
|
||||
deviceInfo.firmware = response.firmwareVersion + '-' + response.buildNumber;
|
||||
|
||||
this.info = deviceInfo;
|
||||
}
|
||||
|
||||
async takePicture(option?: PictureOptions): Promise<MediaObject> {
|
||||
if (!this.pendingPicture) {
|
||||
this.pendingPicture = this.takePictureThrottled(option);
|
||||
this.pendingPicture.finally(() => this.pendingPicture = undefined);
|
||||
}
|
||||
|
||||
return this.pendingPicture;
|
||||
}
|
||||
|
||||
async takePictureThrottled(option?: PictureOptions): Promise<MediaObject> {
|
||||
return this.createMediaObject(await this.getDoorbirdApi().getImage(), 'image/jpeg');
|
||||
}
|
||||
|
||||
// Unfortunately, the Doorbird public API only offers JPEG snapshots with VGA resolution.
|
||||
// Recommendation: use the snapshot plugin to get snapshots with maximum resolution.
|
||||
public async getPictureOptions(): Promise<PictureOptions[]> {
|
||||
return [{
|
||||
id: 'VGA',
|
||||
picture: { width: 640, height: 480 }
|
||||
}];
|
||||
}
|
||||
|
||||
public async putSetting(key: string, value: string | number | boolean) {
|
||||
|
||||
this.doorbirdApi?.stopEventSocket();
|
||||
this.doorbirdApi = undefined;
|
||||
|
||||
this.storage.setItem(key, value.toString());
|
||||
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
|
||||
|
||||
this.provider.updateDevice(this.nativeId, this.name);
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
key: 'username',
|
||||
title: 'Username',
|
||||
value: this.storage.getItem('username'),
|
||||
description: 'Required: Username for Doorbird HTTP API.',
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
title: 'Password',
|
||||
value: this.storage.getItem('password'),
|
||||
type: 'password',
|
||||
description: 'Required: Password for Doorbird HTTP API.',
|
||||
},
|
||||
{
|
||||
key: 'ip',
|
||||
title: 'IP Address',
|
||||
placeholder: '192.168.1.100',
|
||||
value: this.storage.getItem('ip'),
|
||||
description: 'Required: IP address of the Doorbird station.',
|
||||
},
|
||||
{
|
||||
key: 'httpPort',
|
||||
subgroup: 'Advanced',
|
||||
title: 'HTTP Port Override',
|
||||
placeholder: '80',
|
||||
value: this.storage.getItem('httpPort'),
|
||||
description: 'Use this if you have some network firewall rules which change the HTTP port of the camera HTTP port.',
|
||||
},
|
||||
{
|
||||
key: 'rtspUrl',
|
||||
subgroup: 'Advanced',
|
||||
title: 'RTSP URL Override',
|
||||
placeholder: 'rtsp://192.168.2.100/my_doorbird_video_stream',
|
||||
value: this.storage.getItem('rtspUrl'),
|
||||
description: 'Use this in case you are already using another RTSP server/proxy (e.g. mediamtx, go2rtc, etc.) to limit the number of streams from the camera.',
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// When the intercom is started, we also start the audio receiver which receives audio fro the doorbird microphone.
|
||||
// This audio is then fed into ffmpeg instead of the silent audio from the silence generator.
|
||||
// We also start another process(audioTXProcess) which sends audio to the doorbird speaker.
|
||||
async startIntercom(media: MediaObject): Promise<void> {
|
||||
await this.startAudioReceiver();
|
||||
await this.startAudioTransmitter(media);
|
||||
}
|
||||
|
||||
async stopIntercom(): Promise<void> {
|
||||
this.stopAudioTransmitter();
|
||||
this.stopAudioReceiver();
|
||||
}
|
||||
|
||||
async startAudioTransmitter(media: MediaObject): Promise<void> {
|
||||
const ffmpegInput: FFmpegInput = JSON.parse((await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput)).toString());
|
||||
|
||||
const ffmpegArgs = ffmpegInput.inputArguments.slice();
|
||||
ffmpegArgs.push(
|
||||
'-vn', '-dn', '-sn',
|
||||
'-acodec', 'pcm_mulaw',
|
||||
'-flags', '+global_header',
|
||||
'-ac', '1',
|
||||
'-ar', '8k',
|
||||
'-f', 'mulaw',
|
||||
'pipe:3'
|
||||
);
|
||||
|
||||
safePrintFFmpegArguments(console, ffmpegArgs);
|
||||
const cp = child_process.spawn(await mediaManager.getFFmpegPath(), ffmpegArgs, {
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
this.audioTXProcess = cp;
|
||||
ffmpegLogInitialOutput(console, cp);
|
||||
cp.on('exit', () => this.console.log('Doorbird: Audio transmitter ended.'));
|
||||
cp.stdout.on('data', data => this.console.log(data.toString()));
|
||||
cp.stderr.on('data', data => this.console.log(data.toString()));
|
||||
|
||||
const socket = cp.stdio[3] as Readable;
|
||||
|
||||
const username: string = this.getUsername();
|
||||
const password: string = this.getPassword();
|
||||
const audioTxUrl: string = `${this.getHttpBaseAddress()}/bha-api/audio-transmit.cgi`;
|
||||
|
||||
this.console.log('Doorbird: Starting audio transmitter...');
|
||||
|
||||
(async () => {
|
||||
this.console.log('Doorbird: audio transmitter started.');
|
||||
|
||||
const passthrough = new PassThrough();
|
||||
const digestAuth = new AxiosDigestAuth({
|
||||
username,
|
||||
password
|
||||
});
|
||||
digestAuth.request({
|
||||
method: 'POST',
|
||||
url: audioTxUrl,
|
||||
headers: {
|
||||
'Content-Type': 'audio/basic',
|
||||
'Content-Length': '9999999'
|
||||
},
|
||||
data: passthrough,
|
||||
});
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const data = await readLength(socket, 1024);
|
||||
passthrough.push(data);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
finally {
|
||||
this.console.log('Doorbird: audio transmitter finished.');
|
||||
passthrough.end();
|
||||
}
|
||||
|
||||
this.stopAudioTransmitter();
|
||||
})();
|
||||
}
|
||||
|
||||
stopAudioTransmitter() {
|
||||
this.audioTXProcess?.kill('SIGKILL');
|
||||
this.audioTXProcess = undefined;
|
||||
}
|
||||
|
||||
async startAudioReceiver(): Promise<void> {
|
||||
|
||||
const audioRxUrl = `${this.getHttpBaseAddress()}/bha-api/audio-receive.cgi`;
|
||||
|
||||
this.console.log('Doorbird: Starting audio receiver...');
|
||||
|
||||
const ffmpegPath = await mediaManager.getFFmpegPath();
|
||||
|
||||
const ffmpegArgs = [
|
||||
'-hide_banner',
|
||||
'-nostats',
|
||||
'-analyzeduration', '0',
|
||||
'-probesize', '32',
|
||||
'-re',
|
||||
'-ar', '8000',
|
||||
'-ac', '1',
|
||||
'-f', 'mulaw',
|
||||
'-i', `${audioRxUrl}`,
|
||||
'-acodec', 'copy',
|
||||
'-f', 'mulaw',
|
||||
'pipe:3'
|
||||
];
|
||||
|
||||
safePrintFFmpegArguments(console, ffmpegArgs);
|
||||
const cp = child_process.spawn(ffmpegPath, ffmpegArgs, {
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
this.audioRXProcess = cp;
|
||||
ffmpegLogInitialOutput(console, cp);
|
||||
|
||||
cp.on('exit', () => {
|
||||
this.console.log('Doorbird: audio receiver ended.')
|
||||
this.audioRXProcess = undefined;
|
||||
});
|
||||
cp.stdout.on('data', data => this.console.log(data.toString()));
|
||||
cp.stderr.on('data', data => this.console.log(data.toString()));
|
||||
|
||||
this.doorbellAudioActive = true;
|
||||
cp.stdio[3].on('data', data => {
|
||||
if (this.doorbellAudioActive && this.audioRXClientSocket) {
|
||||
this.audioRXClientSocket.write(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stopAudioReceiver() {
|
||||
this.doorbellAudioActive = false;
|
||||
this.audioRXProcess?.kill('SIGKILL');
|
||||
this.audioRXProcess = undefined;
|
||||
}
|
||||
|
||||
async getVideoStreamOptions(): Promise<ResponseMediaStreamOptions[]> {
|
||||
return [{
|
||||
id: 'default',
|
||||
name: 'default',
|
||||
container: '', // must be empty to support prebuffering
|
||||
video: {
|
||||
codec: 'h264'
|
||||
},
|
||||
audio: { /*this.isAudioDisabled() ? null : {}, */
|
||||
// this is a hint to let homekit, et al, know that it's OPUS audio and does not need transcoding.
|
||||
codec: 'pcm_mulaw',
|
||||
}
|
||||
}]; }
|
||||
|
||||
async getVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
|
||||
|
||||
const port = await this.startAudioRXServer();
|
||||
|
||||
const ffmpegInput: FFmpegInput = {
|
||||
url: undefined,
|
||||
inputArguments: [
|
||||
'-analyzeduration', '0',
|
||||
'-probesize', '32',
|
||||
'-fflags', 'nobuffer',
|
||||
'-flags', 'low_delay',
|
||||
'-f', 'rtsp',
|
||||
'-rtsp_transport', 'tcp',
|
||||
'-i', `${this.getRtspAddress()}`,
|
||||
'-f', 'mulaw',
|
||||
'-ac', '1',
|
||||
'-ar', '8000',
|
||||
'-channel_layout', 'mono',
|
||||
'-use_wallclock_as_timestamps', 'true',
|
||||
'-i', `tcp://127.0.0.1:${port}?tcp_nodelay=1`,
|
||||
],
|
||||
mediaStreamOptions: options,
|
||||
};
|
||||
|
||||
return mediaManager.createFFmpegMediaObject(ffmpegInput);
|
||||
}
|
||||
|
||||
async startSilenceGenerator() {
|
||||
|
||||
if (this.audioSilenceProcess)
|
||||
return;
|
||||
|
||||
this.console.log('Doorbird: starting audio silence generator...')
|
||||
|
||||
const ffmpegPath = await mediaManager.getFFmpegPath();
|
||||
const ffmpegArgs = [
|
||||
'-hide_banner',
|
||||
'-nostats',
|
||||
'-re',
|
||||
'-f', 'lavfi',
|
||||
'-i', 'anullsrc=r=8000:cl=mono',
|
||||
'-f', 'mulaw',
|
||||
'pipe:3'
|
||||
];
|
||||
|
||||
safePrintFFmpegArguments(console, ffmpegArgs);
|
||||
const cp = child_process.spawn(ffmpegPath, ffmpegArgs, {
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
this.audioSilenceProcess = cp;
|
||||
ffmpegLogInitialOutput(console, cp);
|
||||
|
||||
cp.on('exit', () => {
|
||||
this.console.log('Doorbird: audio silence generator ended.')
|
||||
this.audioSilenceProcess = undefined;
|
||||
});
|
||||
cp.stdout.on('data', data => this.console.log(data.toString()));
|
||||
cp.stderr.on('data', data => this.console.log(data.toString()));
|
||||
cp.stdio[3].on('data', data => {
|
||||
if (!this.doorbellAudioActive && this.audioRXClientSocket) {
|
||||
this.audioRXClientSocket.write(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stopSilenceGenerator() {
|
||||
this.audioSilenceProcess?.kill();
|
||||
this.audioSilenceProcess = null;
|
||||
}
|
||||
|
||||
async startAudioRXServer(): Promise<number> {
|
||||
|
||||
const server = net.createServer(async (clientSocket) => {
|
||||
clearTimeout(serverTimeout);
|
||||
|
||||
this.audioRXClientSocket = clientSocket;
|
||||
|
||||
this.startSilenceGenerator();
|
||||
|
||||
this.audioRXClientSocket.on('close', () => {
|
||||
this.stopSilenceGenerator();
|
||||
this.audioRXClientSocket = null;
|
||||
});
|
||||
});
|
||||
const serverTimeout = setTimeout(() => {
|
||||
this.console.log('Doorbird: timed out waiting for tcp client from ffmpeg');
|
||||
server.close();
|
||||
}, 30000);
|
||||
const port = await listenZero(server);
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
triggerBinarySensor() {
|
||||
this.binaryState = true;
|
||||
clearTimeout(this.binarySensorTimeout);
|
||||
this.binarySensorTimeout = setTimeout(() => this.binaryState = false, 3000);
|
||||
}
|
||||
|
||||
triggerMotionSensor() {
|
||||
this.motionDetected = true;
|
||||
clearTimeout(this.motionSensorTimeout);
|
||||
this.motionSensorTimeout = setTimeout(() => this.motionDetected = false, 3000);
|
||||
}
|
||||
|
||||
setHttpPortOverride(port: string) {
|
||||
this.storage.setItem('httpPort', port || '');
|
||||
}
|
||||
|
||||
getHttpBaseAddress() {
|
||||
return `http://${this.getUsername()}:${this.getPassword()}@${this.getIPAddress()}:${this.storage.getItem('httpPort') || 80}`;
|
||||
}
|
||||
|
||||
getRtspAddress() {
|
||||
if (this.storage.getItem('rtspUrl') !== undefined) {
|
||||
return this.storage.getItem('rtspUrl');
|
||||
}
|
||||
else {
|
||||
return this.getRtspDefaultAddress();
|
||||
}
|
||||
}
|
||||
|
||||
getRtspDefaultAddress() {
|
||||
return `rtsp://${this.getUsername()}:${this.getPassword()}@${this.getIPAddress()}/mpeg/media.amp`;
|
||||
}
|
||||
|
||||
getIPAddress() {
|
||||
return this.storage.getItem('ip');
|
||||
}
|
||||
|
||||
setIPAddress(ip: string) {
|
||||
return this.storage.setItem('ip', ip);
|
||||
}
|
||||
|
||||
getUsername() {
|
||||
return this.storage.getItem('username');
|
||||
}
|
||||
|
||||
getPassword() {
|
||||
return this.storage.getItem('password');
|
||||
}
|
||||
}
|
||||
|
||||
export class DoorbirdCamProvider extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
||||
|
||||
devices = new Map<string, any>();
|
||||
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
|
||||
for (const camId of deviceManager.getNativeIds()) {
|
||||
if (camId)
|
||||
this.getDevice(camId);
|
||||
}
|
||||
}
|
||||
|
||||
async createDevice(settings: DeviceCreatorSettings, nativeId?: string): Promise<string> {
|
||||
|
||||
let info: DeviceInformation = {};
|
||||
|
||||
const host = settings.ip?.toString();
|
||||
const username = settings.username?.toString();
|
||||
const password = settings.password?.toString();
|
||||
const skipValidate = settings.skipValidate === 'true';
|
||||
|
||||
if (!skipValidate) {
|
||||
const api = new DoorbirdAPI(host, username, password, this.console);
|
||||
try {
|
||||
const deviceInfo = await api.getInfo();
|
||||
|
||||
settings.newCamera = deviceInfo.deviceType;
|
||||
info.model = deviceInfo.deviceType;
|
||||
info.serialNumber = deviceInfo.serialNumber;
|
||||
info.mac = deviceInfo.serialNumber;
|
||||
info.manufacturer = 'Bird Home Automation GmbH';
|
||||
info.managementUrl = 'https://webadmin.doorbird.com';
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('Error adding Doorbird camera', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
settings.newCamera ||= 'Doorbird Camera';
|
||||
|
||||
nativeId ||= randomBytes(4).toString('hex');
|
||||
const name = settings.newCamera?.toString();
|
||||
await this.updateDevice(nativeId, name);
|
||||
|
||||
const device = await this.getDevice(nativeId) as DoorbirdCamera;
|
||||
device.info = info;
|
||||
device.putSetting('username', username);
|
||||
device.putSetting('password', password);
|
||||
device.setIPAddress(settings.ip.toString());
|
||||
device.setHttpPortOverride(settings.httpPort?.toString());
|
||||
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
async getCreateDeviceSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
key: 'username',
|
||||
title: 'Username',
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
title: 'Password',
|
||||
type: 'password',
|
||||
},
|
||||
{
|
||||
key: 'ip',
|
||||
title: 'IP Address',
|
||||
placeholder: '192.168.2.222',
|
||||
},
|
||||
{
|
||||
key: 'httpPort',
|
||||
title: 'HTTP Port',
|
||||
description: 'Optional: Override the HTTP Port from the default value of 80',
|
||||
placeholder: '80',
|
||||
},
|
||||
{
|
||||
key: 'skipValidate',
|
||||
title: 'Skip Validation',
|
||||
description: 'Add the device without verifying the credentials and network settings.',
|
||||
type: 'boolean',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
updateDevice(nativeId: string, name: string) {
|
||||
return deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
interfaces: [
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.VideoCamera,
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Intercom,
|
||||
ScryptedInterface.BinarySensor,
|
||||
ScryptedInterface.MotionSensor
|
||||
],
|
||||
type: ScryptedDeviceType.Doorbell,
|
||||
info: deviceManager.getNativeIds().includes(nativeId) ? deviceManager.getDeviceState(nativeId)?.info : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
getDevice(nativeId: string) {
|
||||
let ret = this.devices.get(nativeId);
|
||||
if (!ret) {
|
||||
ret = this.createCamera(nativeId);
|
||||
if (ret)
|
||||
this.devices.set(nativeId, ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
if( this.devices.delete( nativeId ) ) {
|
||||
this.console.log("Doorbird: Removed device from list: " + id + " / " + nativeId )
|
||||
}
|
||||
}
|
||||
|
||||
createCamera(nativeId: string): DoorbirdCamera {
|
||||
return new DoorbirdCamera(nativeId, this);
|
||||
}
|
||||
}
|
||||
|
||||
export default new DoorbirdCamProvider();
|
||||
11
plugins/doorbird/tsconfig.json
Normal file
11
plugins/doorbird/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"moduleResolution": "Node16",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user