mirror of
https://github.com/koush/scrypted.git
synced 2026-02-03 22:23:27 +00:00
Compare commits
258 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 | ||
|
|
a05595ecc7 | ||
|
|
87be4648f1 | ||
|
|
60e51adb41 | ||
|
|
ace7720fe1 | ||
|
|
b9eb74d403 | ||
|
|
fb7353383d | ||
|
|
bee119b486 | ||
|
|
0b6ffc2b87 | ||
|
|
3863527b4d | ||
|
|
51c48f4a1c | ||
|
|
4c138e9b4c | ||
|
|
e762c305a3 | ||
|
|
5bce335288 | ||
|
|
8201e9883a | ||
|
|
74e5884285 | ||
|
|
9cffd9ffbe | ||
|
|
d8b617f2ae | ||
|
|
aeb564aa5d | ||
|
|
45f672883a | ||
|
|
c0ff857a1b | ||
|
|
64f7e31f54 | ||
|
|
6b55f8876e | ||
|
|
718a31f2c5 | ||
|
|
c1e1d50fa5 | ||
|
|
75c4a1939f | ||
|
|
0d703c2aff | ||
|
|
0a6e4fda75 | ||
|
|
4c2de9e443 | ||
|
|
b8a4fedf1a | ||
|
|
79d9f1d4a1 | ||
|
|
983213c578 | ||
|
|
7dd3d71ebd | ||
|
|
493f8deeef | ||
|
|
b29f2d5ee1 | ||
|
|
96bda10123 | ||
|
|
3294700d31 | ||
|
|
0cf77d4c76 | ||
|
|
953841e3a5 | ||
|
|
393c1017df | ||
|
|
f50176d14a | ||
|
|
7f2bf0b542 | ||
|
|
9e3990400c | ||
|
|
95eed80735 | ||
|
|
be43d0c017 | ||
|
|
386ea9a98a | ||
|
|
9b40978f61 | ||
|
|
f0ee435cd0 | ||
|
|
30748784ef | ||
|
|
8310e33719 | ||
|
|
1d18697161 | ||
|
|
d500b3fd6c | ||
|
|
95ae916b6c | ||
|
|
ec3e16f20f | ||
|
|
30d28f543c | ||
|
|
e0cce24999 | ||
|
|
409b25f8b0 | ||
|
|
8f278abec8 | ||
|
|
d6179dab82 | ||
|
|
ed186e2142 | ||
|
|
3c021bb2c8 | ||
|
|
c522edc622 | ||
|
|
022a103bcb | ||
|
|
efd125b6e4 | ||
|
|
19f7688a65 | ||
|
|
7f18e4629c | ||
|
|
dfe2c937a1 | ||
|
|
47d7a23a3d |
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: |
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -32,9 +32,6 @@
|
||||
[submodule "plugins/sample-cameraprovider"]
|
||||
path = plugins/sample-cameraprovider
|
||||
url = ../../koush/scrypted-sample-cameraprovider
|
||||
[submodule "plugins/tensorflow-lite/sort_oh"]
|
||||
path = plugins/sort-tracker/sort_oh
|
||||
url = ../../koush/sort_oh.git
|
||||
[submodule "plugins/cloud/node-nat-upnp"]
|
||||
path = plugins/cloud/node-nat-upnp
|
||||
url = ../../koush/node-nat-upnp.git
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
export class Deferred<T> {
|
||||
finished = false;
|
||||
resolve!: (value: T|PromiseLike<T>) => void;
|
||||
reject!: (error: Error) => void;
|
||||
resolve!: (value: T|PromiseLike<T>) => this;
|
||||
reject!: (error: Error) => this;
|
||||
promise: Promise<T> = new Promise((resolve, reject) => {
|
||||
this.resolve = v => {
|
||||
this.finished = true;
|
||||
resolve(v);
|
||||
return this;
|
||||
};
|
||||
this.reject = e => {
|
||||
this.finished = true;
|
||||
reject(e);
|
||||
return this;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -681,7 +681,7 @@ export class RtspClient extends RtspBase {
|
||||
});
|
||||
}
|
||||
|
||||
async setup(options: RtspClientTcpSetupOptions | RtspClientUdpSetupOptions) {
|
||||
async setup(options: RtspClientTcpSetupOptions | RtspClientUdpSetupOptions, headers?: Headers) {
|
||||
const protocol = options.type === 'udp' ? '' : '/TCP';
|
||||
const client = options.type === 'udp' ? 'client_port' : 'interleaved';
|
||||
let port: number;
|
||||
@@ -697,9 +697,9 @@ export class RtspClient extends RtspBase {
|
||||
port = options.dgram.address().port;
|
||||
options.dgram.on('message', data => options.onRtp(undefined, data));
|
||||
}
|
||||
const headers: any = {
|
||||
headers = Object.assign({
|
||||
Transport: `RTP/AVP${protocol};unicast;${client}=${port}-${port + 1}`,
|
||||
};
|
||||
}, headers);
|
||||
const response = await this.request('SETUP', headers, options.path);
|
||||
let interleaved: {
|
||||
begin: number;
|
||||
|
||||
@@ -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.43",
|
||||
"version": "1.1.54",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.43",
|
||||
"version": "1.1.54",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.78",
|
||||
"@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.78",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.78.tgz",
|
||||
"integrity": "sha512-SiIUh9ph96aZPjt/oO+W/mlJobrP02ADwFDI9jnvw8/UegUti2x/7JE8Pi3kGXOIkN+cX74Qg4xJEMIpdpO1zw=="
|
||||
"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.43",
|
||||
"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.78",
|
||||
"@scrypted/types": "^0.2.91",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { timeoutPromise } from "../../../common/src/promise-utils";
|
||||
import { BrowserSignalingSession, waitPeerConnectionIceConnected, waitPeerIceConnectionClosed } from "../../../common/src/rtc-signaling";
|
||||
import { DataChannelDebouncer } from "../../../plugins/webrtc/src/datachannel-debouncer";
|
||||
import type { IOSocket } from '../../../server/src/io';
|
||||
import { MediaObject } from '../../../server/src/plugin/mediaobject';
|
||||
import type { MediaObjectRemote } from '../../../server/src/plugin/plugin-api';
|
||||
import { attachPluginRemote } from '../../../server/src/plugin/plugin-remote';
|
||||
import { RpcPeer } from '../../../server/src/rpc';
|
||||
@@ -77,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,
|
||||
@@ -128,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,
|
||||
@@ -144,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,
|
||||
@@ -174,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> {
|
||||
@@ -218,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,
|
||||
@@ -237,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);
|
||||
}
|
||||
@@ -505,22 +535,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
console.log('api attached', Date.now() - start);
|
||||
|
||||
mediaManager.createMediaObject = async<T extends MediaObjectOptions>(data: any, mimeType: string, options: T) => {
|
||||
const mo: MediaObjectRemote & {
|
||||
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: any,
|
||||
[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
|
||||
} = {
|
||||
[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
|
||||
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: {
|
||||
mimeType,
|
||||
sourceId: options?.sourceId,
|
||||
},
|
||||
mimeType,
|
||||
sourceId: options?.sourceId,
|
||||
async getData() {
|
||||
return data;
|
||||
},
|
||||
};
|
||||
return mo as any;
|
||||
return new MediaObject(mimeType, data, options) as any;
|
||||
}
|
||||
|
||||
const { browserSignalingSession, connectionManagementId, updateSessionId } = rpcPeer.params;
|
||||
|
||||
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",
|
||||
|
||||
20
plugins/amcrest/package-lock.json
generated
20
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",
|
||||
@@ -16,7 +16,7 @@
|
||||
"multiparty": "^4.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.0"
|
||||
"@types/node": "^18.15.11"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.68",
|
||||
"version": "0.2.87",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -100,9 +100,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.0.tgz",
|
||||
"integrity": "sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg=="
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
||||
},
|
||||
"node_modules/auth-header": {
|
||||
"version": "1.0.0",
|
||||
@@ -291,9 +291,9 @@
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "16.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.0.tgz",
|
||||
"integrity": "sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg=="
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
||||
},
|
||||
"auth-header": {
|
||||
"version": "1.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.121",
|
||||
"version": "0.0.122",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -36,12 +36,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/multiparty": "^0.0.33",
|
||||
"multiparty": "^4.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.0"
|
||||
"@types/node": "^18.15.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.12",
|
||||
"version": "0.7.21",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.7.12",
|
||||
"version": "0.7.21",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.85",
|
||||
"version": "0.2.101",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.7.12",
|
||||
"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,
|
||||
@@ -273,10 +345,29 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
])
|
||||
return result
|
||||
|
||||
async def putSetting(self, key, value) -> None:
|
||||
@async_print_exception_guard
|
||||
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")
|
||||
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 []
|
||||
@@ -295,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
|
||||
@@ -480,6 +480,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
|
||||
return results
|
||||
|
||||
@async_print_exception_guard
|
||||
async def putSetting(self, key: str, value: SettingValue) -> None:
|
||||
if not self.validate_setting(key, value):
|
||||
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
|
||||
@@ -492,7 +493,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
# force arlo client to be invalidated and reloaded
|
||||
self.invalidate_arlo_client()
|
||||
elif key == "plugin_verbosity":
|
||||
self.storage.setItem(key, "Verbose" if value == "true" else "Normal")
|
||||
self.storage.setItem(key, "Verbose" if value == "true" or value == True else "Normal")
|
||||
self.propagate_verbosity()
|
||||
skip_arlo_client = True
|
||||
else:
|
||||
@@ -572,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.108",
|
||||
"version": "0.1.128",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.108",
|
||||
"version": "0.1.128",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.108",
|
||||
"version": "0.1.128",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -27,25 +27,8 @@ export class Scheduler {
|
||||
];
|
||||
|
||||
const date = new Date();
|
||||
if (schedule.clockType === 'AM' || schedule.clockType === 'PM') {
|
||||
let hour = schedule.hour;
|
||||
if (schedule.clockType === 'AM') {
|
||||
if (hour === 12)
|
||||
hour -= 12;
|
||||
}
|
||||
else {
|
||||
if (hour != 12)
|
||||
hour += 12;
|
||||
}
|
||||
date.setHours(hour);
|
||||
date.setMinutes(schedule.minute, 0, 0);
|
||||
}
|
||||
else if (schedule.clockType === '24HourClock') {
|
||||
date.setHours(schedule.hour, schedule.minute, 0, 0);
|
||||
}
|
||||
else {
|
||||
throw new Error('sunrise/sunset clock not supported');
|
||||
}
|
||||
date.setHours(schedule.hour);
|
||||
date.setMinutes(schedule.minute);
|
||||
|
||||
const ret: ScryptedDevice = {
|
||||
async setName() { },
|
||||
@@ -65,7 +48,7 @@ export class Scheduler {
|
||||
if (!days[day])
|
||||
continue;
|
||||
|
||||
source.log.i(`event will fire at ${future}`);
|
||||
source.log.i(`event will fire at ${future.toLocaleString()}`);
|
||||
return future;
|
||||
}
|
||||
source.log.w('event will never fire');
|
||||
@@ -80,6 +63,7 @@ export class Scheduler {
|
||||
}
|
||||
|
||||
const delay = when.getTime() - Date.now();
|
||||
source.log.i(`event will fire in ${Math.round(delay / 60 / 1000)} minutes.`);
|
||||
|
||||
let timeout = setTimeout(() => {
|
||||
reschedule();
|
||||
|
||||
@@ -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;
|
||||
|
||||
118
plugins/core/ui/package-lock.json
generated
118
plugins/core/ui/package-lock.json
generated
@@ -13,7 +13,6 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.3.0",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.8",
|
||||
"@radial-color-picker/vue-color-picker": "^2.3.0",
|
||||
"@scrypted/client": "file:../../../packages/client",
|
||||
"@scrypted/common": "file:../../../common",
|
||||
"@scrypted/sdk": "file:../../../sdk",
|
||||
"@scrypted/types": "file:../../../sdk/types",
|
||||
@@ -32,6 +31,7 @@
|
||||
"register-service-worker": "^1.7.2",
|
||||
"router": "^1.3.6",
|
||||
"semver": "^6.3.0",
|
||||
"v-calendar": "^2.4.1",
|
||||
"vue": "^2.7.14",
|
||||
"vue-apexcharts": "^1.6.2",
|
||||
"vue-async-computed": "^3.9.0",
|
||||
@@ -118,27 +118,24 @@
|
||||
},
|
||||
"../../../packages/client": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.37",
|
||||
"version": "1.1.48",
|
||||
"extraneous": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.64",
|
||||
"adm-zip": "^0.5.9",
|
||||
"@scrypted/types": "^0.2.78",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.2.2",
|
||||
"linkfs": "^2.1.0",
|
||||
"memfs": "^3.4.1",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/node": "^17.0.17",
|
||||
"typescript": "^4.7.4"
|
||||
"@types/node": "^18.14.2",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
},
|
||||
"../../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.68",
|
||||
"version": "0.2.87",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -175,7 +172,7 @@
|
||||
},
|
||||
"../../../sdk/types": {
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.2.63",
|
||||
"version": "0.2.79",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/rimraf": "^3.0.2",
|
||||
@@ -2265,6 +2262,16 @@
|
||||
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
|
||||
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@radial-color-picker/color-wheel": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radial-color-picker/color-wheel/-/color-wheel-2.2.0.tgz",
|
||||
@@ -2287,10 +2294,6 @@
|
||||
"vue": "^2.5.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client": {
|
||||
"resolved": "../../../packages/client",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
"resolved": "../../../common",
|
||||
"link": true
|
||||
@@ -7819,7 +7822,6 @@
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.24.0.tgz",
|
||||
"integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
@@ -7828,6 +7830,14 @@
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns-tz": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz",
|
||||
"integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==",
|
||||
"peerDependencies": {
|
||||
"date-fns": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
@@ -18977,6 +18987,31 @@
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v-calendar": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/v-calendar/-/v-calendar-2.4.1.tgz",
|
||||
"integrity": "sha512-nhzOlHM2cinv+8jIcnAx+nTo63U40szv3Ig41uLMpGK1U5sApgCP6ggigprsnlMOM5VRq1G/1B8rNHkRrLbGjw==",
|
||||
"dependencies": {
|
||||
"core-js": "^3.15.2",
|
||||
"date-fns": "^2.22.1",
|
||||
"date-fns-tz": "^1.1.4",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.4.0",
|
||||
"vue": "^2.5.18"
|
||||
}
|
||||
},
|
||||
"node_modules/v-calendar/node_modules/core-js": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
|
||||
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==",
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
||||
@@ -22773,6 +22808,12 @@
|
||||
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
|
||||
"dev": true
|
||||
},
|
||||
"@popperjs/core": {
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
|
||||
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==",
|
||||
"peer": true
|
||||
},
|
||||
"@radial-color-picker/color-wheel": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radial-color-picker/color-wheel/-/color-wheel-2.2.0.tgz",
|
||||
@@ -22792,22 +22833,6 @@
|
||||
"@radial-color-picker/rotator": "2.1.0"
|
||||
}
|
||||
},
|
||||
"@scrypted/client": {
|
||||
"version": "file:../../../packages/client",
|
||||
"requires": {
|
||||
"@scrypted/types": "^0.2.64",
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/node": "^17.0.17",
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.2.2",
|
||||
"linkfs": "^2.1.0",
|
||||
"memfs": "^3.4.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
},
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../../common",
|
||||
"requires": {
|
||||
@@ -27308,8 +27333,13 @@
|
||||
"date-fns": {
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.24.0.tgz",
|
||||
"integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw=="
|
||||
},
|
||||
"date-fns-tz": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz",
|
||||
"integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"de-indent": {
|
||||
"version": "1.0.2",
|
||||
@@ -36063,6 +36093,24 @@
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"dev": true
|
||||
},
|
||||
"v-calendar": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/v-calendar/-/v-calendar-2.4.1.tgz",
|
||||
"integrity": "sha512-nhzOlHM2cinv+8jIcnAx+nTo63U40szv3Ig41uLMpGK1U5sApgCP6ggigprsnlMOM5VRq1G/1B8rNHkRrLbGjw==",
|
||||
"requires": {
|
||||
"core-js": "^3.15.2",
|
||||
"date-fns": "^2.22.1",
|
||||
"date-fns-tz": "^1.1.4",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
|
||||
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"v8-compile-cache": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"register-service-worker": "^1.7.2",
|
||||
"router": "^1.3.6",
|
||||
"semver": "^6.3.0",
|
||||
"v-calendar": "^2.4.1",
|
||||
"vue": "^2.7.14",
|
||||
"vue-apexcharts": "^1.6.2",
|
||||
"vue-async-computed": "^3.9.0",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<v-btn :dark="!isLive" v-on="on" small :color="isLive ? 'white' : 'blue'" :outlined="isLive">
|
||||
<v-icon small color="white" :outlined="isLive">fa fa-calendar-alt</v-icon> {{ monthDay }}</v-btn>
|
||||
</template>
|
||||
<v-date-picker @input="datePicked"></v-date-picker>
|
||||
<vc-date-picker mode="date" :value="startTime" @input="datePicked"></vc-date-picker>
|
||||
</v-dialog>
|
||||
|
||||
<v-btn v-if="showNvr" :dark="!isLive" small :color="isLive ? 'white' : adjustingTime ? 'green' : 'blue'"
|
||||
@@ -181,8 +181,8 @@ export default {
|
||||
methods: {
|
||||
datePicked(value) {
|
||||
this.dateDialog = false;
|
||||
const dt = datePickerLocalTimeToUTC(value);
|
||||
this.streamRecorder(dt);
|
||||
if (value && value.getTime)
|
||||
this.streamRecorder(value.getTime());
|
||||
},
|
||||
doTimeScroll(e) {
|
||||
if (!this.device.interfaces.includes(ScryptedInterface.VideoRecorder))
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export default {
|
||||
watch: {
|
||||
device() {
|
||||
this.watchDevice();
|
||||
this.refresh();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -17,10 +17,21 @@ export default {
|
||||
VueMarkdown,
|
||||
CardTitle,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
token: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
refresh() {
|
||||
this.token++;
|
||||
}
|
||||
},
|
||||
asyncComputed: {
|
||||
readme: {
|
||||
async get() {
|
||||
return this.device.getReadmeMarkdown();;
|
||||
await this.token;
|
||||
return this.device.getReadmeMarkdown();
|
||||
},
|
||||
default: undefined,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-checkbox v-if="lazyValue.type === 'boolean'" dense :readonly="lazyValue.readonly" v-model="booleanValue"
|
||||
<vc-date-picker v-if="lazyValue.type === 'date'" mode="date" v-model="dateValue" :is-range="lazyValue.combobox"></vc-date-picker>
|
||||
<vc-date-picker v-else-if="lazyValue.type === 'time'" mode="time" v-model="dateValue"
|
||||
class="hide-header" :is-range="lazyValue.combobox"></vc-date-picker>
|
||||
<vc-date-picker v-else-if="lazyValue.type === 'datetime'" mode="datetime" v-model="dateValue" :is-range="lazyValue.combobox"></vc-date-picker>
|
||||
<v-checkbox v-else-if="lazyValue.type === 'boolean'" dense :readonly="lazyValue.readonly" v-model="booleanValue"
|
||||
:label="lazyValue.title" :hint="lazyValue.description" :placeholder="lazyValue.placeholder" persistent-hint
|
||||
@change="save" :class="lazyValue.description ? 'mb-2' : ''"></v-checkbox>
|
||||
<div v-else-if="lazyValue.type === 'qrcode'">
|
||||
@@ -134,6 +138,25 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
dateValue: {
|
||||
get() {
|
||||
if (this.lazyValue.combobox) {
|
||||
return {
|
||||
start: new Date(parseInt(this.lazyValue.value?.[0]) || Date.now()),
|
||||
end: new Date(parseInt(this.lazyValue.value?.[1]) || Date.now()),
|
||||
};
|
||||
}
|
||||
return new Date(parseInt(this.lazyValue.value) || Date.now());
|
||||
},
|
||||
set(val) {
|
||||
if (this.lazyValue.combobox) {
|
||||
this.lazyValue.value = [val.start.getTime(), val.end.getTime()];
|
||||
}
|
||||
else {
|
||||
this.lazyValue.value = val.getTime();
|
||||
}
|
||||
}
|
||||
},
|
||||
booleanValue: {
|
||||
get() {
|
||||
return (
|
||||
@@ -251,6 +274,7 @@ export default {
|
||||
},
|
||||
createLazyValue() {
|
||||
var type = this.value.type || "";
|
||||
|
||||
if (type.indexOf("[]") == -1 && type !== "clippath") {
|
||||
return cloneDeep(this.value);
|
||||
}
|
||||
@@ -265,6 +289,7 @@ export default {
|
||||
},
|
||||
createInputValue() {
|
||||
var type = this.lazyValue.type || "";
|
||||
|
||||
if (type.indexOf("[]") == -1 && type !== "clippath") {
|
||||
return this.lazyValue;
|
||||
}
|
||||
@@ -287,4 +312,8 @@ export default {
|
||||
.shift-up {
|
||||
margin-top: -8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
.hide-header .vc-date {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,11 +40,11 @@
|
||||
<v-btn v-on="on" small>
|
||||
<v-icon x-small>fa fa-calendar-alt</v-icon>
|
||||
|
||||
{{ year }}-{{ month }}-{{ date }}
|
||||
{{ new Date(date).getFullYear() }}-{{ new Date(date).getMonth() }}-{{ new Date(date).getDate() }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-date-picker @input="onDate"> </v-date-picker>
|
||||
<vc-date-picker mode="date" @input="onDate" v-model="date"> </vc-date-picker>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-btn text small disabled v-if="pages">{{ pageRange }}</v-btn>
|
||||
@@ -70,7 +70,6 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { datePickerLocalTimeToUTC } from "../common/date";
|
||||
import { fetchClipThumbnail, fetchClipUrl } from "../common/videoclip";
|
||||
import RPCInterface from "./RPCInterface.vue";
|
||||
import Vue from "vue";
|
||||
@@ -129,14 +128,11 @@ export default {
|
||||
clips: {
|
||||
async get() {
|
||||
await this.refreshNonce;
|
||||
const date = new Date();
|
||||
const date = new Date(this.date);
|
||||
date.setMilliseconds(0);
|
||||
date.setSeconds(0);
|
||||
date.setMinutes(0);
|
||||
date.setHours(0);
|
||||
date.setFullYear(this.year);
|
||||
date.setMonth(this.month - 1);
|
||||
date.setDate(this.date);
|
||||
console.log(date);
|
||||
const dt = date.getTime();
|
||||
const ret = await this.device.getVideoClips({
|
||||
@@ -165,9 +161,7 @@ export default {
|
||||
fetchingImages: [],
|
||||
page: 1,
|
||||
dialog: false,
|
||||
date: new Date().getDate(),
|
||||
month: new Date().getMonth() + 1,
|
||||
year: new Date().getFullYear(),
|
||||
date: Date.now(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -202,11 +196,8 @@ export default {
|
||||
onDate(value) {
|
||||
this.page = 1;
|
||||
this.dialog = false;
|
||||
const dt = datePickerLocalTimeToUTC(value);
|
||||
const d = new Date(dt);
|
||||
this.month = d.getMonth() + 1;
|
||||
this.date = d.getDate();
|
||||
this.year = d.getFullYear();
|
||||
console.log(value);
|
||||
this.date = value;
|
||||
this.refresh();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,33 +1,12 @@
|
||||
<template>
|
||||
<v-layout row wrap justify-center align-center>
|
||||
<v-flex xs3 md2 lg2 xl1 v-for="day of days" :key="day">
|
||||
<v-btn
|
||||
block
|
||||
class="white--text"
|
||||
@click="toggleDay(day)"
|
||||
color="info"
|
||||
small
|
||||
:text="!lazyValue[day]"
|
||||
>{{ day.substring(0, 3) }}</v-btn>
|
||||
<v-btn block class="white--text" @click="toggleDay(day)" color="info" small :text="!lazyValue[day]">{{
|
||||
day.substring(0, 3) }}</v-btn>
|
||||
</v-flex>
|
||||
<v-flex xs12>
|
||||
<v-layout justify-center align-center>
|
||||
<v-time-picker v-model="time" format="24hr" @input="onChange"></v-time-picker>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
<v-flex xs12>
|
||||
<v-layout justify-center align-center>
|
||||
<v-flex xs12 md8 lg6 xl4>
|
||||
<v-select
|
||||
xs3
|
||||
reverse
|
||||
:items="clockTypes"
|
||||
solo
|
||||
item-value="id"
|
||||
v-model="lazyValue.clockType"
|
||||
@input="onChange"
|
||||
></v-select>
|
||||
</v-flex>
|
||||
<vc-date-picker v-model="time" class="hide-header" @input="onChange" mode="time"></vc-date-picker>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
@@ -52,62 +31,37 @@ function zeroPrefix(arr, len) {
|
||||
arr.push(i >= 10 ? i.toString() : "0" + i);
|
||||
}
|
||||
}
|
||||
const clockTypes = [
|
||||
{
|
||||
id: "AM",
|
||||
text: "AM"
|
||||
},
|
||||
{
|
||||
id: "PM",
|
||||
text: "PM"
|
||||
},
|
||||
{
|
||||
text: "24 Hour Clock",
|
||||
id: "TwentyFourHourClock"
|
||||
},
|
||||
{
|
||||
text: "Before Sunrise",
|
||||
id: "BeforeSunrise"
|
||||
},
|
||||
{
|
||||
text: "After Sunrise",
|
||||
id: "AfterSunrise"
|
||||
},
|
||||
{
|
||||
text: "Before Sunset",
|
||||
id: "BeforeSunset"
|
||||
},
|
||||
{
|
||||
text: "After Sunset",
|
||||
id: "AfterSunset"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
zeroPrefix(hours, 24);
|
||||
zeroPrefix(minutes, 59);
|
||||
|
||||
export default {
|
||||
mixins: [RPCInterface],
|
||||
data: function() {
|
||||
data: function () {
|
||||
return {
|
||||
clockTypes,
|
||||
days,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
time: {
|
||||
get() {
|
||||
return `${this.lazyValue.hour}:${this.lazyValue.minute}`;
|
||||
const date = new Date();
|
||||
date.setMilliseconds(0);
|
||||
date.setSeconds(0);
|
||||
date.setMinutes(this.lazyValue.minute);
|
||||
date.setHours(this.lazyValue.hour);
|
||||
return date;
|
||||
},
|
||||
set(value) {
|
||||
this.lazyValue.hour = value.split(":")[0];
|
||||
this.lazyValue.minute = value.split(":")[1];
|
||||
this.lazyValue.hour = value.getHours();
|
||||
this.lazyValue.minute = value.getMinutes();
|
||||
this.onChange();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleDay: function(day) {
|
||||
toggleDay: function (day) {
|
||||
this.lazyValue[day] = !this.lazyValue[day];
|
||||
this.onChange();
|
||||
},
|
||||
@@ -117,11 +71,10 @@ export default {
|
||||
ret.minute = ret.minute || 0;
|
||||
return ret;
|
||||
},
|
||||
onChange: function() {
|
||||
onChange: function () {
|
||||
const schedule = {
|
||||
hour: parseInt(this.lazyValue.hour) || 0,
|
||||
minute: parseInt(this.lazyValue.minute) || 0,
|
||||
clockType: this.lazyValue.clockType || "AM",
|
||||
};
|
||||
days.forEach(day => {
|
||||
schedule[day] = this.lazyValue[day] || false;
|
||||
@@ -139,9 +92,15 @@ export default {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.semicolon-pad {
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
|
||||
.hide-header .vc-date {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,13 @@ import './plugins/is-mobile';
|
||||
import Launcher from './Launcher.vue'
|
||||
import './registerServiceWorker'
|
||||
|
||||
import VCalendar from 'v-calendar';
|
||||
|
||||
// Use v-calendar & v-date-picker components
|
||||
Vue.use(VCalendar, {
|
||||
componentPrefix: 'vc', // Use <vc-calendar /> instead of <v-calendar />
|
||||
});
|
||||
|
||||
// STYLES
|
||||
// Main Theme SCSS
|
||||
// import './assets/scss/theme.scss'
|
||||
|
||||
@@ -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.9",
|
||||
"version": "0.1.15",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.9",
|
||||
"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": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user