mirror of
https://github.com/koush/scrypted.git
synced 2026-02-05 23:22:13 +00:00
Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eab99866f | ||
|
|
e10a4f3c58 | ||
|
|
2585b1832e | ||
|
|
5e8e0d7773 | ||
|
|
7c17b478d7 | ||
|
|
9f5dd55c73 | ||
|
|
b6f400382d | ||
|
|
024b2166b8 | ||
|
|
b49771840e | ||
|
|
4001fc996f | ||
|
|
0d97010ca8 | ||
|
|
e243d99d12 | ||
|
|
86a91dfbe4 | ||
|
|
c86ae752e8 | ||
|
|
b7ca477b98 | ||
|
|
c37f8926b8 | ||
|
|
4b181a8ac9 | ||
|
|
b8439aaec3 | ||
|
|
77d0c33657 | ||
|
|
0b6d61a801 | ||
|
|
71a2d27cbd | ||
|
|
f8f79f5cc2 | ||
|
|
988f297e32 | ||
|
|
6e109d89e0 | ||
|
|
6ada4854bc | ||
|
|
bc5e89668f | ||
|
|
4c11def52b | ||
|
|
8890d307f4 | ||
|
|
9f8f562dcc | ||
|
|
2ce798c8c2 | ||
|
|
4271ef321f | ||
|
|
f976903a29 | ||
|
|
4ca63aadd5 | ||
|
|
6c932aec89 | ||
|
|
d7030c3dcf | ||
|
|
172ebf06de | ||
|
|
5f28c5a291 | ||
|
|
4c9ba5073e | ||
|
|
11d67f36be | ||
|
|
d38357ded9 | ||
|
|
f22e2ccfe7 | ||
|
|
e2b2f68477 | ||
|
|
57e87fbe8d | ||
|
|
31b05162fc | ||
|
|
c63efa0fca | ||
|
|
ce5255aa45 | ||
|
|
4692be1586 | ||
|
|
632d971dd5 | ||
|
|
2f17c85e99 | ||
|
|
9c6cdc9ac3 | ||
|
|
7007456bdd | ||
|
|
73fc738c0b | ||
|
|
abd1227fab | ||
|
|
7d2226df75 | ||
|
|
8f50415920 | ||
|
|
20ed523b30 | ||
|
|
effadb1437 | ||
|
|
07c7c91c63 | ||
|
|
878ddbdf1c | ||
|
|
d95e9c78ea | ||
|
|
49dc1d8f36 | ||
|
|
425e17a88b | ||
|
|
9bca6b0a94 | ||
|
|
3a62d9cd31 | ||
|
|
8f6bedd9d8 | ||
|
|
1c2a9d767f | ||
|
|
7ecee4298c | ||
|
|
4f1aad895f | ||
|
|
94667d2136 | ||
|
|
7d13055eae | ||
|
|
f90140dbd7 | ||
|
|
8b3a66b6ba | ||
|
|
8c03852cfb | ||
|
|
d795cd527d | ||
|
|
a24d986717 | ||
|
|
60ec304e68 | ||
|
|
6a9d498ff8 | ||
|
|
c60821043b | ||
|
|
e5a63dd992 | ||
|
|
f77ea922f2 | ||
|
|
1e8deeb638 | ||
|
|
a28ecb71e1 | ||
|
|
4067455396 | ||
|
|
9b828a6045 | ||
|
|
efce576c68 | ||
|
|
66b314f2aa | ||
|
|
d6ebc1fa85 | ||
|
|
8d756a26bd | ||
|
|
81c28b86d3 | ||
|
|
73f5e03774 | ||
|
|
cd078afcf9 | ||
|
|
6e393514cf | ||
|
|
4b62bceede | ||
|
|
fbbbdd8ab5 | ||
|
|
a0e28c0a28 | ||
|
|
ff28238422 | ||
|
|
4e9744360a | ||
|
|
7336fac8c4 | ||
|
|
6771d17829 | ||
|
|
62f1ca66f6 | ||
|
|
13cc562e68 | ||
|
|
aff1e86d6f | ||
|
|
c1f1e96109 | ||
|
|
a36b3066fe | ||
|
|
cadf10b505 | ||
|
|
ed541629b2 | ||
|
|
7d022548b9 | ||
|
|
9aa9bae3a3 | ||
|
|
7f29b05980 | ||
|
|
b89573e910 | ||
|
|
18426bcdc1 | ||
|
|
f562dd5362 | ||
|
|
1f1218a594 | ||
|
|
1aca97c2ae | ||
|
|
bd41410367 | ||
|
|
291d734a05 | ||
|
|
feec534b86 | ||
|
|
9ae7e6c0b5 | ||
|
|
a6f11d6d0c | ||
|
|
a15af8005b | ||
|
|
c13a3f252a | ||
|
|
0eaf9ef2d9 | ||
|
|
b9fc69347a | ||
|
|
f6e8a363ab | ||
|
|
a6d163ec5a | ||
|
|
2d62944ac1 | ||
|
|
b564553998 | ||
|
|
6e4fdb6e99 | ||
|
|
ca00983ecd | ||
|
|
36b8b9eeed | ||
|
|
fbd6937627 | ||
|
|
7c66826657 | ||
|
|
62c4a8b240 | ||
|
|
af860d840a | ||
|
|
42eb4fc80b | ||
|
|
5c965936e9 | ||
|
|
fe5cc59872 | ||
|
|
5d965ebfa7 | ||
|
|
b462249d93 | ||
|
|
29d8abed45 | ||
|
|
65cb13b0d1 | ||
|
|
522f8e9cba | ||
|
|
16199463ec | ||
|
|
220c010232 | ||
|
|
02238f99b2 | ||
|
|
1e53234cd6 | ||
|
|
824b7327a1 | ||
|
|
81d4a3f249 | ||
|
|
db1bd07b71 | ||
|
|
35026f6b5b | ||
|
|
9160efc2f7 | ||
|
|
6bc1e6a742 | ||
|
|
475e4a60d7 | ||
|
|
1f2edf1a12 | ||
|
|
b3db0aa78f | ||
|
|
0766d67a75 | ||
|
|
d2ac428916 | ||
|
|
945fb16bd6 |
41
.github/workflows/docker-common.yml
vendored
41
.github/workflows/docker-common.yml
vendored
@@ -6,11 +6,11 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
# runs-on: self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: self-hosted
|
||||
# runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
NODE_VERSION: ["18"]
|
||||
NODE_VERSION: ["18", "20"]
|
||||
BASE: ["jammy"]
|
||||
FLAVOR: ["full", "lite", "thin"]
|
||||
steps:
|
||||
@@ -20,28 +20,27 @@ jobs:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
# - name: Set up SSH
|
||||
# uses: MrSquaare/ssh-setup-action@v2
|
||||
# with:
|
||||
# host: 192.168.2.124
|
||||
# private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
# - name: Set up SSH
|
||||
# uses: MrSquaare/ssh-setup-action@v2
|
||||
# with:
|
||||
# host: 192.168.2.119
|
||||
# private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM7 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# with:
|
||||
# platforms: linux/arm64,linux/armhf
|
||||
# append: |
|
||||
# - endpoint: ssh://koush@192.168.2.124
|
||||
# # platforms: linux/arm64
|
||||
# platforms: linux/arm64
|
||||
# # - endpoint: ssh://koush@192.168.2.119
|
||||
# # platforms: linux/armhf
|
||||
with:
|
||||
platforms: linux/arm64,linux/armhf
|
||||
append: |
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
platforms: linux/arm64
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM7 }}
|
||||
platforms: linux/armhf
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
|
||||
43
.github/workflows/docker.yml
vendored
43
.github/workflows/docker.yml
vendored
@@ -15,11 +15,11 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
# runs-on: self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: self-hosted
|
||||
# runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: ["18-jammy-full", "18-jammy-lite", "18-jammy-thin"]
|
||||
BASE: ["18-jammy-full", "18-jammy-lite", "18-jammy-thin", "20-jammy-full", "20-jammy-lite", "20-jammy-thin"]
|
||||
SUPERVISOR: ["", ".s6"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
@@ -39,29 +39,28 @@ jobs:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
# - name: Set up SSH
|
||||
# uses: MrSquaare/ssh-setup-action@v2
|
||||
# with:
|
||||
# host: 192.168.2.124
|
||||
# private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
# - name: Set up SSH
|
||||
# uses: MrSquaare/ssh-setup-action@v2
|
||||
# with:
|
||||
# host: 192.168.2.119
|
||||
# private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM7 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# with:
|
||||
# platforms: linux/arm64,linux/armhf
|
||||
# append: |
|
||||
# - endpoint: ssh://koush@192.168.2.124
|
||||
# # platforms: linux/arm64
|
||||
# platforms: linux/arm64
|
||||
# # - endpoint: ssh://koush@192.168.2.119
|
||||
# # platforms: linux/armhf
|
||||
|
||||
with:
|
||||
platforms: linux/arm64,linux/armhf
|
||||
append: |
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
platforms: linux/arm64
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM7 }}
|
||||
platforms: linux/armhf
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -33,5 +33,5 @@
|
||||
path = plugins/sample-cameraprovider
|
||||
url = ../../koush/scrypted-sample-cameraprovider
|
||||
[submodule "plugins/cloud/node-nat-upnp"]
|
||||
path = plugins/cloud/node-nat-upnp
|
||||
path = plugins/cloud/external/node-nat-upnp
|
||||
url = ../../koush/node-nat-upnp.git
|
||||
|
||||
4
common/package-lock.json
generated
4
common/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/common",
|
||||
"private": true,
|
||||
"version": "1.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
|
||||
171
common/src/async-queue.ts
Normal file
171
common/src/async-queue.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Deferred } from "./deferred";
|
||||
|
||||
class EndError extends Error {
|
||||
}
|
||||
|
||||
export function createAsyncQueue<T>() {
|
||||
let ended: Error | undefined;
|
||||
const waiting: Deferred<T>[] = [];
|
||||
const queued: { item: T, dequeued?: Deferred<void> }[] = [];
|
||||
|
||||
const dequeue = async () => {
|
||||
if (queued.length) {
|
||||
const { item, dequeued: enqueue } = queued.shift()!;
|
||||
enqueue?.resolve();
|
||||
return item;
|
||||
}
|
||||
|
||||
if (ended)
|
||||
throw ended;
|
||||
|
||||
const deferred = new Deferred<T>();
|
||||
waiting.push(deferred);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
const submit = (item: T, dequeued?: Deferred<void>, signal?: AbortSignal) => {
|
||||
if (ended)
|
||||
return false;
|
||||
|
||||
if (waiting.length) {
|
||||
const deferred = waiting.shift();
|
||||
dequeued?.resolve();
|
||||
deferred.resolve(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
const qi = {
|
||||
item,
|
||||
dequeued,
|
||||
};
|
||||
queued!.push(qi);
|
||||
|
||||
signal?.addEventListener('abort', () => {
|
||||
const index = queued.indexOf(qi);
|
||||
if (index === -1)
|
||||
return;
|
||||
queued.splice(index, 1);
|
||||
dequeued?.reject(new Error('abort'));
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function queue() {
|
||||
return (async function* () {
|
||||
while (true) {
|
||||
try {
|
||||
const item = await dequeue();
|
||||
yield item;
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof EndError)
|
||||
return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
function clear(error?: Error) {
|
||||
const ret: T[] = [];
|
||||
const items = queued.splice(0, queued.length);
|
||||
for (const item of items) {
|
||||
if (error)
|
||||
item.dequeued?.reject(error)
|
||||
else
|
||||
item.dequeued?.resolve(undefined);
|
||||
ret.push(item.item);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
return {
|
||||
clear() {
|
||||
return clear();
|
||||
},
|
||||
queued,
|
||||
async pipe(callback: (i: T) => void) {
|
||||
for await (const i of queue()) {
|
||||
callback(i as any);
|
||||
}
|
||||
},
|
||||
submit(item: T, signal?: AbortSignal) {
|
||||
return submit(item, undefined, signal);
|
||||
},
|
||||
end(e?: Error) {
|
||||
if (ended)
|
||||
return false;
|
||||
// catch to prevent unhandled rejection.
|
||||
ended = e || new EndError()
|
||||
clear(e);
|
||||
return true;
|
||||
},
|
||||
async enqueue(item: T, signal?: AbortSignal) {
|
||||
const dequeued = new Deferred<void>();
|
||||
if (!submit(item, dequeued, signal))
|
||||
return false;
|
||||
await dequeued.promise;
|
||||
return true;
|
||||
},
|
||||
dequeue,
|
||||
get queue() {
|
||||
return queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// async function testSlowEnqueue() {
|
||||
// const asyncQueue = createAsyncQueue<number>();
|
||||
|
||||
// asyncQueue.submit(-1);
|
||||
// asyncQueue.submit(-1);
|
||||
// asyncQueue.submit(-1);
|
||||
// asyncQueue.submit(-1);
|
||||
|
||||
// (async () => {
|
||||
// console.log('go');
|
||||
// for (let i = 0; i < 10; i++) {
|
||||
// asyncQueue.submit(i);
|
||||
// await sleep(100);
|
||||
// }
|
||||
// asyncQueue.end(new Error('fail'));
|
||||
// })();
|
||||
|
||||
|
||||
// const runQueue = async (str?: string) => {
|
||||
// for await (const n of asyncQueue.queue) {
|
||||
// console.log(str, n);
|
||||
// }
|
||||
// }
|
||||
|
||||
// runQueue('start');
|
||||
|
||||
// setTimeout(runQueue, 400);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// async function testSlowDequeue() {
|
||||
// const asyncQueue = createAsyncQueue<number>();
|
||||
|
||||
// const runQueue = async (str?: string) => {
|
||||
// for await (const n of asyncQueue.queue) {
|
||||
// await sleep(100);
|
||||
// }
|
||||
// }
|
||||
|
||||
// runQueue()
|
||||
// .catch(e => console.error('queue threw', e));
|
||||
|
||||
// console.log('go');
|
||||
// for (let i = 0; i < 10; i++) {
|
||||
// console.log(await asyncQueue.enqueue(i));
|
||||
// console.log(i);
|
||||
// }
|
||||
// asyncQueue.end(new Error('fail'));
|
||||
// console.log(await asyncQueue.enqueue(555));
|
||||
// }
|
||||
|
||||
// testSlowDequeue();
|
||||
@@ -3,14 +3,13 @@ import sdk from "@scrypted/sdk";
|
||||
|
||||
const { systemManager } = sdk;
|
||||
|
||||
const autoIncludeToken = 'v4';
|
||||
|
||||
export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
|
||||
hasEnabledMixin: { [id: string]: string } = {};
|
||||
pluginsComponent: Promise<any>;
|
||||
unshiftMixin = false;
|
||||
|
||||
constructor(nativeId?: string) {
|
||||
constructor(nativeId?: string, public autoIncludeToken = 'v4') {
|
||||
super(nativeId);
|
||||
|
||||
try {
|
||||
@@ -30,10 +29,12 @@ export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
|
||||
this.maybeEnableMixin(eventSource);
|
||||
});
|
||||
|
||||
for (const id of Object.keys(systemManager.getSystemState())) {
|
||||
const device = systemManager.getDeviceById(id);
|
||||
this.maybeEnableMixin(device);
|
||||
}
|
||||
process.nextTick(() => {
|
||||
for (const id of Object.keys(systemManager.getSystemState())) {
|
||||
const device = systemManager.getDeviceById(id);
|
||||
this.maybeEnableMixin(device);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async shouldEnableMixin(device: ScryptedDevice) {
|
||||
@@ -44,7 +45,7 @@ export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
|
||||
if (!device || device.mixins?.includes(this.id))
|
||||
return;
|
||||
|
||||
if (this.hasEnabledMixin[device.id] === autoIncludeToken)
|
||||
if (this.hasEnabledMixin[device.id] === this.autoIncludeToken)
|
||||
return;
|
||||
|
||||
const match = await this.canMixin(device.type, device.interfaces);
|
||||
@@ -66,9 +67,9 @@ export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
|
||||
}
|
||||
|
||||
setHasEnabledMixin(id: string) {
|
||||
if (this.hasEnabledMixin[id] === autoIncludeToken)
|
||||
if (this.hasEnabledMixin[id] === this.autoIncludeToken)
|
||||
return;
|
||||
this.hasEnabledMixin[id] = autoIncludeToken;
|
||||
this.hasEnabledMixin[id] = this.autoIncludeToken;
|
||||
this.storage.setItem('hasEnabledMixin', JSON.stringify(this.hasEnabledMixin));
|
||||
}
|
||||
|
||||
|
||||
@@ -51,14 +51,8 @@ function silence() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
export class BrowserSignalingSession implements RTCSignalingSession {
|
||||
private pc: RTCPeerConnection;
|
||||
pcDeferred = new Deferred<RTCPeerConnection>();
|
||||
dcDeferred = new Deferred<RTCDataChannel>();
|
||||
microphone: RTCRtpSender;
|
||||
micEnabled = false;
|
||||
onPeerConnection: (pc: RTCPeerConnection) => Promise<void>;
|
||||
options: RTCSignalingOptions = {
|
||||
function createOptions() {
|
||||
const options: RTCSignalingOptions = {
|
||||
userAgent: getUserAgent(),
|
||||
capabilities: {
|
||||
audio: RTCRtpReceiver.getCapabilities?.('audio') || {
|
||||
@@ -76,6 +70,18 @@ export class BrowserSignalingSession implements RTCSignalingSession {
|
||||
height: screen.height,
|
||||
},
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
export class BrowserSignalingSession implements RTCSignalingSession {
|
||||
private pc: RTCPeerConnection;
|
||||
pcDeferred = new Deferred<RTCPeerConnection>();
|
||||
dcDeferred = new Deferred<RTCDataChannel>();
|
||||
microphone: RTCRtpSender;
|
||||
micEnabled = false;
|
||||
onPeerConnection: (pc: RTCPeerConnection) => Promise<void>;
|
||||
__proxy_props = { options: createOptions() };
|
||||
options = createOptions();
|
||||
|
||||
constructor() {
|
||||
}
|
||||
@@ -284,6 +290,10 @@ function createCandidateQueue(console: Console, type: string, session: RTCSignal
|
||||
}
|
||||
}
|
||||
|
||||
export async function legacyGetSignalingSessionOptions(session: RTCSignalingSession) {
|
||||
return typeof session.options === 'object' ? session.options : await session.getOptions();
|
||||
}
|
||||
|
||||
export async function connectRTCSignalingClients(
|
||||
console: Console,
|
||||
offerClient: RTCSignalingSession,
|
||||
@@ -291,8 +301,8 @@ export async function connectRTCSignalingClients(
|
||||
answerClient: RTCSignalingSession,
|
||||
answerSetup: Partial<RTCAVSignalingSetup>
|
||||
) {
|
||||
const offerOptions = await offerClient.getOptions();
|
||||
const answerOptions = await answerClient.getOptions();
|
||||
const offerOptions = await legacyGetSignalingSessionOptions(offerClient);
|
||||
const answerOptions = await legacyGetSignalingSessionOptions(answerClient);
|
||||
const disableTrickle = offerOptions?.disableTrickle || answerOptions?.disableTrickle;
|
||||
|
||||
if (offerOptions?.offer && answerOptions?.offer)
|
||||
|
||||
2
external/werift
vendored
2
external/werift
vendored
Submodule external/werift updated: 9815d03344...b63f339b55
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "18-jammy-full.s6-v0.39.4"
|
||||
version: "18-jammy-full.s6-v0.50.0"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
@@ -35,6 +35,7 @@ backup_exclude:
|
||||
map:
|
||||
- config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
devices:
|
||||
- /dev/mem
|
||||
- /dev/dri/renderD128
|
||||
|
||||
@@ -36,12 +36,6 @@ RUN apt-get -y install \
|
||||
python3-setuptools \
|
||||
python3-wheel
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update && apt-get -y install libedgetpu1-std
|
||||
|
||||
# these are necessary for pillow-simd, additional on disk size is small
|
||||
# but could consider removing this.
|
||||
RUN apt-get -y install \
|
||||
@@ -74,13 +68,12 @@ RUN apt-get -y install \
|
||||
python3-pil \
|
||||
python3-skimage
|
||||
|
||||
# python pip
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
# 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 rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
@@ -113,10 +106,19 @@ RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
python3.9-dev \
|
||||
python3.9-distutils
|
||||
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update && apt-get -y install libedgetpu1-std
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
@@ -127,7 +129,7 @@ ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20230608"
|
||||
ENV SCRYPTED_BASE_VERSION="20230727"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
################################################################
|
||||
|
||||
@@ -43,5 +43,5 @@ ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20230608"
|
||||
ENV SCRYPTED_BASE_VERSION="20230727"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="lite"
|
||||
|
||||
@@ -21,5 +21,5 @@ ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20230608"
|
||||
ENV SCRYPTED_BASE_VERSION="20230727"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="thin"
|
||||
|
||||
@@ -50,7 +50,7 @@ services:
|
||||
# Modify to add the additional volume for Scrypted NVR.
|
||||
# The following example would mount the /mnt/sda/video path on the host
|
||||
# to the /nvr path inside the docker container.
|
||||
# - /mnt/sda/video:/nvr
|
||||
# - /mnt/media/video:/nvr
|
||||
|
||||
# Or use a network mount from one of the CIFS/NFS examples at the top of this file.
|
||||
# - type: volume
|
||||
@@ -67,17 +67,25 @@ services:
|
||||
|
||||
# Default volume for the Scrypted database. Typically should not be changed.
|
||||
- ~/.scrypted/volume:/server/volume
|
||||
devices:
|
||||
devices: [
|
||||
# uncomment the common systems devices to pass
|
||||
# them through to docker.
|
||||
|
||||
# all usb devices, such as coral tpu
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
# "/dev/bus/usb:/dev/bus/usb",
|
||||
|
||||
# hardware accelerated video decoding, opencl, etc.
|
||||
# - /dev/dri:/dev/dri
|
||||
# "/dev/dri:/dev/dri",
|
||||
|
||||
# uncomment below as necessary.
|
||||
# zwave usb serial device
|
||||
# - /dev/ttyACM0:/dev/ttyACM0
|
||||
|
||||
# "/dev/ttyACM0:/dev/ttyACM0",
|
||||
|
||||
# coral PCI devices
|
||||
# - /dev/apex_0:/dev/apex_0
|
||||
# - /dev/apex_1:/dev/apex_1
|
||||
# "/dev/apex_0:/dev/apex_0",
|
||||
# "/dev/apex_1:/dev/apex_1",
|
||||
]
|
||||
|
||||
container_name: scrypted
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -23,10 +23,19 @@ RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
python3.9-dev \
|
||||
python3.9-distutils
|
||||
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update && apt-get -y install libedgetpu1-std
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
@@ -37,7 +46,7 @@ ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20230608"
|
||||
ENV SCRYPTED_BASE_VERSION="20230727"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
################################################################
|
||||
|
||||
@@ -33,12 +33,6 @@ RUN apt-get -y install \
|
||||
python3-setuptools \
|
||||
python3-wheel
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update && apt-get -y install libedgetpu1-std
|
||||
|
||||
# these are necessary for pillow-simd, additional on disk size is small
|
||||
# but could consider removing this.
|
||||
RUN apt-get -y install \
|
||||
@@ -71,13 +65,12 @@ RUN apt-get -y install \
|
||||
python3-pil \
|
||||
python3-skimage
|
||||
|
||||
# python pip
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
# 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 rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
@@ -58,6 +58,9 @@ brew unpin gst-python
|
||||
|
||||
### END HACK WORKAROUND
|
||||
|
||||
# seems to be necessary for python-codecs' pycairo dependency or something?
|
||||
RUN_IGNORE gobject-introspection libffi pkg-config
|
||||
|
||||
# gstreamer plugins
|
||||
RUN_IGNORE brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-libav
|
||||
# gst python bindings
|
||||
|
||||
8
packages/cli/.vscode/launch.json
vendored
8
packages/cli/.vscode/launch.json
vendored
@@ -19,11 +19,11 @@
|
||||
"-r",
|
||||
"ts-node/register"
|
||||
],
|
||||
"preLaunchTask": "npm: build",
|
||||
"args": [
|
||||
"ffplay",
|
||||
"Kitchen",
|
||||
"getRecordingStream",
|
||||
"{\"startTime\":1677699495709}"
|
||||
"Baby Camera@192.168.2.109",
|
||||
"getVideoStream",
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
@@ -35,4 +35,4 @@
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
4
packages/cli/package-lock.json
generated
4
packages/cli/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.0.67",
|
||||
"version": "1.0.69",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "scrypted",
|
||||
"version": "1.0.67",
|
||||
"version": "1.0.69",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/client": "^1.1.43",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.0.67",
|
||||
"version": "1.0.69",
|
||||
"description": "",
|
||||
"main": "./dist/main.js",
|
||||
"bin": {
|
||||
|
||||
@@ -172,8 +172,11 @@ async function main() {
|
||||
ffmpegInput.inputArguments = ffmpegInput.inputArguments.map(i => i === ffmpegInput.url ? ffmpegInput.urls?.[0] : i);
|
||||
}
|
||||
}
|
||||
console.log('ffplay', ...ffmpegInput.inputArguments);
|
||||
child_process.spawn('ffplay', ffmpegInput.inputArguments, {
|
||||
const args = [...ffmpegInput.inputArguments];
|
||||
if (ffmpegInput.h264FilterArguments)
|
||||
args.push(...ffmpegInput.h264FilterArguments);
|
||||
console.log('ffplay', ...args);
|
||||
child_process.spawn('ffplay', args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
sdk.disconnect();
|
||||
|
||||
4
packages/client/package-lock.json
generated
4
packages/client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.54",
|
||||
"version": "1.1.55",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.54",
|
||||
"version": "1.1.55",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.94",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.54",
|
||||
"version": "1.1.55",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MediaObjectOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import axios, { AxiosRequestConfig, AxiosRequestHeaders } from 'axios';
|
||||
import * as eio from 'engine.io-client';
|
||||
import { SocketOptions } from 'engine.io-client';
|
||||
import { Deferred } from "../../../common/src/deferred";
|
||||
@@ -8,7 +8,6 @@ import { BrowserSignalingSession, waitPeerConnectionIceConnected, waitPeerIceCon
|
||||
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';
|
||||
import { createRpcDuplexSerializer, createRpcSerializer } from '../../../server/src/rpc-serializer';
|
||||
@@ -48,9 +47,8 @@ export interface ScryptedClientStatic extends ScryptedStatic {
|
||||
browserSignalingSession?: BrowserSignalingSession;
|
||||
address?: string;
|
||||
connectionType: ScryptedClientConnectionType;
|
||||
authorization?: string;
|
||||
queryToken?: { [parameter: string]: string };
|
||||
rpcPeer: RpcPeer,
|
||||
rpcPeer: RpcPeer;
|
||||
loginResult: ScryptedClientLoginResult;
|
||||
}
|
||||
|
||||
export interface ScryptedConnectionOptions {
|
||||
@@ -59,6 +57,7 @@ export interface ScryptedConnectionOptions {
|
||||
webrtc?: boolean;
|
||||
baseUrl?: string;
|
||||
axiosConfig?: AxiosRequestConfig;
|
||||
previousLoginResult?: ScryptedClientLoginResult;
|
||||
}
|
||||
|
||||
export interface ScryptedLoginOptions extends ScryptedConnectionOptions {
|
||||
@@ -138,6 +137,7 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
|
||||
// should maybe move this into the cloud server itself.
|
||||
const scryptedCloud = response.headers['x-scrypted-cloud'] === 'true';
|
||||
const directAddress = response.headers['x-scrypted-direct-address'];
|
||||
const cloudAddress = response.headers['x-scrypted-cloud-address'];
|
||||
|
||||
return {
|
||||
error: response.data.error as string,
|
||||
@@ -147,20 +147,27 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
|
||||
addresses,
|
||||
scryptedCloud,
|
||||
directAddress,
|
||||
cloudAddress,
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkScryptedClientLogin(options?: ScryptedConnectionOptions) {
|
||||
let { baseUrl } = options || {};
|
||||
const url = combineBaseUrl(baseUrl, 'login');
|
||||
const headers: AxiosRequestHeaders = {};
|
||||
if (options?.previousLoginResult?.authorization)
|
||||
headers.Authorization = options?.previousLoginResult?.authorization;
|
||||
const response = await axios.get(url, {
|
||||
withCredentials: true,
|
||||
headers,
|
||||
...options?.axiosConfig,
|
||||
});
|
||||
const scryptedCloud = response.headers['x-scrypted-cloud'] === 'true';
|
||||
const directAddress = response.headers['x-scrypted-direct-address'];
|
||||
const cloudAddress = response.headers['x-scrypted-cloud-address'];
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
hostname: response.data.hostname as string,
|
||||
redirect: response.data.redirect as string,
|
||||
username: response.data.username as string,
|
||||
@@ -173,9 +180,19 @@ export async function checkScryptedClientLogin(options?: ScryptedConnectionOptio
|
||||
addresses: response.data.addresses as string[],
|
||||
scryptedCloud,
|
||||
directAddress,
|
||||
cloudAddress,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ScryptedClientLoginResult {
|
||||
authorization: string;
|
||||
queryToken: { [parameter: string]: string };
|
||||
localAddresses: string[];
|
||||
scryptedCloud: boolean;
|
||||
directAddress: string;
|
||||
cloudAddress: string;
|
||||
}
|
||||
|
||||
export class ScryptedClientLoginError extends Error {
|
||||
constructor(public result: Awaited<ReturnType<typeof checkScryptedClientLogin>>) {
|
||||
super(result.error);
|
||||
@@ -211,35 +228,78 @@ export async function redirectScryptedLogout(baseUrl?: string) {
|
||||
export async function connectScryptedClient(options: ScryptedClientOptions): Promise<ScryptedClientStatic> {
|
||||
const start = Date.now();
|
||||
let { baseUrl, pluginId, clientName, username, password } = options;
|
||||
|
||||
let authorization: string;
|
||||
let queryToken: any;
|
||||
|
||||
const extraHeaders: { [header: string]: string } = {};
|
||||
let localAddresses: string[];
|
||||
let scryptedCloud: boolean;
|
||||
let directAddress: string;
|
||||
let cloudAddress: string;
|
||||
|
||||
console.log('@scrypted/client', packageJson.version);
|
||||
|
||||
const extraHeaders: { [header: string]: string } = {};
|
||||
|
||||
if (username && password) {
|
||||
const loginResult = await loginScryptedClient(options as ScryptedLoginOptions);
|
||||
if (loginResult.authorization)
|
||||
extraHeaders['Authorization'] = loginResult.authorization;
|
||||
localAddresses = loginResult.addresses;
|
||||
scryptedCloud = loginResult.scryptedCloud;
|
||||
directAddress = loginResult.directAddress;
|
||||
cloudAddress = loginResult.cloudAddress;
|
||||
authorization = loginResult.authorization;
|
||||
queryToken = loginResult.queryToken;
|
||||
console.log('login result', Date.now() - start, loginResult);
|
||||
}
|
||||
else {
|
||||
const loginCheck = await checkScryptedClientLogin({
|
||||
const urlsToCheck = new Set<string>();
|
||||
for (const u of [
|
||||
...options?.previousLoginResult?.localAddresses || [],
|
||||
options?.previousLoginResult?.directAddress,
|
||||
options?.previousLoginResult?.cloudAddress,
|
||||
]) {
|
||||
if (u)
|
||||
urlsToCheck.add(u);
|
||||
}
|
||||
|
||||
// the alternate urls must have a valid response.
|
||||
const loginCheckPromises = [...urlsToCheck].map(async baseUrl => {
|
||||
const loginCheck = await checkScryptedClientLogin({
|
||||
baseUrl,
|
||||
previousLoginResult: options?.previousLoginResult,
|
||||
});
|
||||
|
||||
if (loginCheck.error || loginCheck.redirect)
|
||||
throw new Error('login error');
|
||||
|
||||
if (!loginCheck.authorization || !loginCheck.username || !loginCheck.queryToken) {
|
||||
console.error(loginCheck);
|
||||
throw new Error('malformed login result');
|
||||
}
|
||||
|
||||
return loginCheck;
|
||||
});
|
||||
|
||||
const baseUrlCheck = checkScryptedClientLogin({
|
||||
baseUrl,
|
||||
});
|
||||
loginCheckPromises.push(baseUrlCheck);
|
||||
|
||||
let loginCheck: Awaited<ReturnType<typeof checkScryptedClientLogin>>;
|
||||
try {
|
||||
loginCheck = await Promise.any(loginCheckPromises);
|
||||
}
|
||||
catch (e) {
|
||||
loginCheck = await baseUrlCheck;
|
||||
}
|
||||
|
||||
if (loginCheck.error || loginCheck.redirect)
|
||||
throw new ScryptedClientLoginError(loginCheck);
|
||||
localAddresses = loginCheck.addresses;
|
||||
scryptedCloud = loginCheck.scryptedCloud;
|
||||
directAddress = loginCheck.directAddress;
|
||||
cloudAddress = loginCheck.cloudAddress;
|
||||
username = loginCheck.username;
|
||||
authorization = loginCheck.authorization;
|
||||
queryToken = loginCheck.queryToken;
|
||||
@@ -280,8 +340,27 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
addresses.push(directAddress);
|
||||
}
|
||||
|
||||
if (((scryptedCloud && options.direct === undefined) || options.direct) && cloudAddress) {
|
||||
addresses.push(cloudAddress);
|
||||
}
|
||||
|
||||
const tryAddresses = !!addresses.length;
|
||||
const tryWebrtc = !!globalThis.RTCPeerConnection && (scryptedCloud && options.webrtc === undefined) || options.webrtc;
|
||||
const webrtcLastFailedKey = 'webrtcLastFailed';
|
||||
const canUseWebrtc = !!globalThis.RTCPeerConnection;
|
||||
let tryWebrtc = canUseWebrtc && options.webrtc;
|
||||
// try webrtc by default on scrypted cloud.
|
||||
// but webrtc takes a while to fail, so backoff if it fails to prevent continual slow starts.
|
||||
if (scryptedCloud && canUseWebrtc && globalThis.localStorage && options.webrtc === undefined) {
|
||||
tryWebrtc = true;
|
||||
const webrtcLastFailed = parseFloat(localStorage.getItem(webrtcLastFailedKey));
|
||||
// if webrtc has failed in the past day, dont attempt to use it.
|
||||
const now = Date.now();
|
||||
if (webrtcLastFailed < now && webrtcLastFailed > now - 1 * 24 * 60 * 60 * 1000) {
|
||||
tryWebrtc = false;
|
||||
console.warn('WebRTC API connection recently failed. Skipping.')
|
||||
}
|
||||
}
|
||||
|
||||
console.log({
|
||||
tryLocalAddressess: tryAddresses,
|
||||
tryWebrtc,
|
||||
@@ -458,7 +537,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
const p2pPromises = [...promises];
|
||||
|
||||
promises.push((async () => {
|
||||
const waitDuration = tryWebrtc ? 3000 : (tryAddresses ? 1000 : 0);
|
||||
const waitDuration = tryWebrtc ? 10000 : (tryAddresses ? 1000 : 0);
|
||||
console.log('waiting', waitDuration);
|
||||
if (waitDuration) {
|
||||
// give the peer to peers a second, but then try connecting directly.
|
||||
@@ -485,6 +564,9 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
const any = Promise.any(promises);
|
||||
let { ready, connectionType, address, rpcPeer } = await any;
|
||||
|
||||
if (tryWebrtc && connectionType !== 'webrtc')
|
||||
localStorage.setItem(webrtcLastFailedKey, Date.now().toString());
|
||||
|
||||
console.log('connected', connectionType, address)
|
||||
|
||||
socket = ready;
|
||||
@@ -602,9 +684,15 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
pluginHostAPI: undefined,
|
||||
rtcConnectionManagement,
|
||||
browserSignalingSession,
|
||||
authorization,
|
||||
queryToken,
|
||||
rpcPeer,
|
||||
loginResult: {
|
||||
directAddress,
|
||||
localAddresses,
|
||||
scryptedCloud,
|
||||
queryToken,
|
||||
authorization,
|
||||
cloudAddress,
|
||||
}
|
||||
}
|
||||
|
||||
socket.on('close', () => {
|
||||
|
||||
4
packages/deferred/package-lock.json
generated
4
packages/deferred/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/deferred",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
1
packages/deferred/src/async-queue.ts
Symbolic link
1
packages/deferred/src/async-queue.ts
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../common/src/async-queue.ts
|
||||
1
packages/deferred/src/deferred.ts
Symbolic link
1
packages/deferred/src/deferred.ts
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../common/src/deferred.ts
|
||||
@@ -1 +0,0 @@
|
||||
../../../common/src/deferred.ts
|
||||
2
packages/deferred/src/index.ts
Normal file
2
packages/deferred/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './deferred';
|
||||
export * from './async-queue';
|
||||
23
packages/h264-repacketizer/.vscode/launch.json
vendored
Normal file
23
packages/h264-repacketizer/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ts-node",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"${workspaceFolder}/test/test.ts"
|
||||
],
|
||||
"runtimeArgs": [
|
||||
"-r",
|
||||
"ts-node/register"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
}
|
||||
]
|
||||
}
|
||||
296
packages/h264-repacketizer/package-lock.json
generated
296
packages/h264-repacketizer/package-lock.json
generated
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "@scrypted/h264-packetizer",
|
||||
"name": "@scrypted/h264-repacketizer",
|
||||
"version": "0.0.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/h264-packetizer",
|
||||
"name": "@scrypted/h264-repacketizer",
|
||||
"version": "0.0.7",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"rimraf": "^4.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
},
|
||||
@@ -43,12 +44,121 @@
|
||||
"../sdk/types": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
||||
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz",
|
||||
@@ -64,6 +174,49 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
@@ -76,26 +229,165 @@
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
}
|
||||
},
|
||||
"@jridgewell/resolve-uri": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
|
||||
"dev": true
|
||||
},
|
||||
"@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true
|
||||
},
|
||||
"@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true
|
||||
},
|
||||
"@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
||||
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn-walk": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
|
||||
"dev": true
|
||||
},
|
||||
"arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
},
|
||||
"create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true
|
||||
},
|
||||
"make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz",
|
||||
"integrity": "sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==",
|
||||
"dev": true
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "10.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"rimraf": "^4.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
93
packages/h264-repacketizer/test/test.ts
Normal file
93
packages/h264-repacketizer/test/test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { H264Repacketizer, depacketizeStapA } from '../src/index';
|
||||
import { H264_NAL_TYPE_IDR, H264_NAL_TYPE_PPS, H264_NAL_TYPE_SEI, H264_NAL_TYPE_SPS, H264_NAL_TYPE_STAP_A, RtspServer, getNaluTypesInNalu } from '../../../common/src/rtsp-server';
|
||||
import fs from 'fs';
|
||||
|
||||
import { getNvrSessionStream } from '../../../../nvr/nvr-plugin/src/session-stream';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import { RtpPacket } from '../../../external/werift/packages/rtp/src/rtp/rtp';
|
||||
|
||||
function parse(parameters: string) {
|
||||
const spspps = parameters.split(',');
|
||||
// empty sprop-parameter-sets is apparently a thing:
|
||||
// a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=
|
||||
if (spspps?.length !== 2) {
|
||||
return {
|
||||
sps: undefined,
|
||||
pps: undefined,
|
||||
};
|
||||
}
|
||||
const [sps, pps] = spspps;
|
||||
|
||||
return {
|
||||
sps: Buffer.from(sps, 'base64'),
|
||||
pps: Buffer.from(pps, 'base64'),
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const spspps = parse('Z2QAM6wVFKAoALWQ,aO48sA==');
|
||||
// Z2QAM6wVFKAoALWQ
|
||||
// Z00AMpY1QEABg03BQEFQAAADABAAAAMDKEA=
|
||||
|
||||
|
||||
const repacketizer = new H264Repacketizer(console, 1300, undefined);
|
||||
|
||||
const stream = fs.createReadStream('/Users/koush/Downloads/rtsp/1692537093973.rtsp', {
|
||||
start: 0,
|
||||
highWaterMark: 800000,
|
||||
});
|
||||
|
||||
let rtspParser = new RtspServer(stream as any, '');
|
||||
rtspParser.setupTracks = {
|
||||
'0': {
|
||||
codec: '0',
|
||||
protocol: 'tcp',
|
||||
control: '',
|
||||
destination: 0,
|
||||
},
|
||||
'2': {
|
||||
codec: '2',
|
||||
protocol: 'tcp',
|
||||
control: '',
|
||||
destination: 2,
|
||||
},
|
||||
}
|
||||
for await (const rtspSample of rtspParser.handleRecord()) {
|
||||
if (rtspSample.type !== '0')
|
||||
continue;
|
||||
const rtp = RtpPacket.deSerialize(rtspSample.packet);
|
||||
const nalus = getNaluTypesInNalu(rtp.payload);
|
||||
if (nalus.has(H264_NAL_TYPE_SEI)) {
|
||||
console.warn('SEI', rtp.payload)
|
||||
}
|
||||
if (nalus.has(H264_NAL_TYPE_SPS)) {
|
||||
console.warn('SPS', rtp.payload, spspps.sps)
|
||||
}
|
||||
if (nalus.has(H264_NAL_TYPE_PPS)) {
|
||||
console.warn('PPS', rtp.payload, spspps.sps)
|
||||
}
|
||||
if (nalus.has(H264_NAL_TYPE_STAP_A)) {
|
||||
const parts = depacketizeStapA(rtp.payload);
|
||||
console.log('stapa', parts);
|
||||
for (const part of parts) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (nalus.has(H264_NAL_TYPE_IDR)) {
|
||||
const h264Packetizer = new H264Repacketizer(console, 65535, spspps as any);
|
||||
// offset the stapa packet by -1 so the sequence numbers can be reused.
|
||||
h264Packetizer.extraPackets = -1;
|
||||
const stapas: RtpPacket[] = [];
|
||||
const idr = RtpPacket.deSerialize(rtspSample.packet);
|
||||
h264Packetizer.maybeSendStapACodecInfo(idr, stapas);
|
||||
if (stapas.length === 1) {
|
||||
const stapa = stapas[0].serialize();
|
||||
// console.log(stapa);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
main();
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2019",
|
||||
"target": "ES2020",
|
||||
"noImplicitAny": true,
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
@@ -10,6 +10,7 @@
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true,
|
||||
},
|
||||
"exclude": ["**/node_modules"],
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
|
||||
6
plugins/alexa/package-lock.json
generated
6
plugins/alexa/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.101",
|
||||
"version": "0.2.104",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.8",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -392,14 +392,21 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
})
|
||||
}
|
||||
|
||||
private setReauthenticateAlert() {
|
||||
const msg: string = "Please reauthenticate by following the directions below.";
|
||||
this.log.a(msg);
|
||||
}
|
||||
|
||||
getAccessToken(): Promise<string> {
|
||||
if (this.accessToken)
|
||||
return this.accessToken;
|
||||
|
||||
this.log.clearAlerts();
|
||||
|
||||
const { tokenInfo } = this.storageSettings.values;
|
||||
|
||||
if (tokenInfo === undefined) {
|
||||
this.log.e("Please reauthenticate by following the directions below.");
|
||||
this.setReauthenticateAlert();
|
||||
throw new Error("'tokenInfo' is undefined");
|
||||
}
|
||||
|
||||
@@ -432,19 +439,19 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
case 'invalid_grant':
|
||||
case 'unauthorized_client':
|
||||
self.console.error(error?.response?.data);
|
||||
self.log.e(error?.response?.data?.error_description);
|
||||
self.log.a(error?.response?.data?.error_description);
|
||||
self.storageSettings.values.tokenInfo = undefined;
|
||||
self.accessToken = undefined;
|
||||
break;
|
||||
|
||||
case 'authorization_pending':
|
||||
self.console.warn(error?.response?.data);
|
||||
self.log.w(error?.response?.data?.error_description);
|
||||
self.log.a(error?.response?.data?.error_description);
|
||||
break;
|
||||
|
||||
case 'expired_token':
|
||||
self.console.warn(error?.response?.data);
|
||||
self.log.w(error?.response?.data?.error_description);
|
||||
self.log.a(error?.response?.data?.error_description);
|
||||
self.accessToken = undefined;
|
||||
break;
|
||||
|
||||
@@ -488,6 +495,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
});
|
||||
|
||||
if (accessToken !== undefined) {
|
||||
this.log.clearAlerts();
|
||||
|
||||
try {
|
||||
response.send({
|
||||
"event": {
|
||||
|
||||
@@ -120,7 +120,7 @@ export async function getCameraCapabilities(device: ScryptedDevice): Promise<Dis
|
||||
"interface": "Alexa.RTCSessionController",
|
||||
"version": "3",
|
||||
"configuration": {
|
||||
isFullDuplexAudioSupported: true,
|
||||
"isFullDuplexAudioSupported": true,
|
||||
}
|
||||
} as DiscoveryCapability
|
||||
];
|
||||
|
||||
@@ -7,10 +7,19 @@ import { Response, WebRTCAnswerGeneratedForSessionEvent, WebRTCSessionConnectedE
|
||||
|
||||
export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
constructor(public response: AlexaHttpResponse, public directive: any) {
|
||||
this.options = this.createOptions();
|
||||
this.__proxy_props = { options: this.createOptions() };
|
||||
}
|
||||
|
||||
__proxy_props: { options: RTCSignalingOptions; };
|
||||
options: RTCSignalingOptions;
|
||||
|
||||
async getOptions(): Promise<RTCSignalingOptions> {
|
||||
return {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
private createOptions() {
|
||||
const options: RTCSignalingOptions = {
|
||||
proxy: true,
|
||||
offer: {
|
||||
type: 'offer',
|
||||
@@ -24,7 +33,9 @@ export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
width: 1280,
|
||||
height: 720
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { supportedTypes } from ".";
|
||||
supportedTypes.set(ScryptedDeviceType.Doorbell, {
|
||||
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
|
||||
let capabilities: any[] = [];
|
||||
const displayCategories: DisplayCategory[] = ['DOORBELL'];
|
||||
const displayCategories: DisplayCategory[] = [];
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.RTCSignalingChannel)) {
|
||||
capabilities = await getCameraCapabilities(device);
|
||||
@@ -24,6 +24,9 @@ supportedTypes.set(ScryptedDeviceType.Doorbell, {
|
||||
);
|
||||
}
|
||||
|
||||
// Important: If your device is a video doorbell, make sure that you list CAMERA before DOORBELL in the displayCategories list.
|
||||
displayCategories.push('DOORBELL');
|
||||
|
||||
return {
|
||||
displayCategories,
|
||||
capabilities
|
||||
@@ -38,6 +41,9 @@ supportedTypes.set(ScryptedDeviceType.Doorbell, {
|
||||
if (response)
|
||||
return response;
|
||||
|
||||
if (eventDetails.eventInterface === ScryptedInterface.BinarySensor && eventData === false)
|
||||
return {};
|
||||
|
||||
if (eventDetails.eventInterface === ScryptedInterface.BinarySensor && eventData === true)
|
||||
return {
|
||||
event: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
4
plugins/amcrest/package-lock.json
generated
4
plugins/amcrest/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.123",
|
||||
"version": "0.0.127",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.123",
|
||||
"version": "0.0.127",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.123",
|
||||
"version": "0.0.127",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -95,6 +95,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
for (const element of deviceParameters) {
|
||||
try {
|
||||
const response = await this.getClient().digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=${element.action}`
|
||||
});
|
||||
|
||||
@@ -147,6 +148,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
return;
|
||||
|
||||
const response = await this.getClient().digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`
|
||||
});
|
||||
this.console.log('reconfigure result', response.data);
|
||||
@@ -190,14 +192,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|| event === AmcrestEvent.PhoneCallDetectStart
|
||||
|| event === AmcrestEvent.AlarmIPCStart
|
||||
|| event === AmcrestEvent.DahuaTalkInvite) {
|
||||
if (event === AmcrestEvent.DahuaTalkInvite && payload && multipleCallIds)
|
||||
{
|
||||
if (payload.includes(callerId))
|
||||
{
|
||||
if (event === AmcrestEvent.DahuaTalkInvite && payload && multipleCallIds) {
|
||||
if (payload.includes(callerId)) {
|
||||
this.binaryState = true;
|
||||
}
|
||||
} else
|
||||
{
|
||||
} else {
|
||||
this.binaryState = true;
|
||||
}
|
||||
}
|
||||
@@ -259,25 +258,23 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|
||||
if (!twoWayAudio)
|
||||
twoWayAudio = isDoorbell ? 'Amcrest' : 'None';
|
||||
|
||||
|
||||
if (doorbellType == DAHUA_DOORBELL_TYPE)
|
||||
{
|
||||
|
||||
|
||||
if (doorbellType == DAHUA_DOORBELL_TYPE) {
|
||||
ret.push(
|
||||
{
|
||||
title: 'Multiple Call Buttons',
|
||||
key: 'multipleCallIds',
|
||||
description: 'Some Dahua Doorbells integrate multiple Call Buttons for apartment buildings.',
|
||||
type: 'boolean',
|
||||
value: (this.storage.getItem('multipleCallIds') === 'true').toString(),
|
||||
}
|
||||
{
|
||||
title: 'Multiple Call Buttons',
|
||||
key: 'multipleCallIds',
|
||||
description: 'Some Dahua Doorbells integrate multiple Call Buttons for apartment buildings.',
|
||||
type: 'boolean',
|
||||
value: (this.storage.getItem('multipleCallIds') === 'true').toString(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const multipleCallIds = this.storage.getItem('multipleCallIds');
|
||||
|
||||
if (multipleCallIds)
|
||||
{
|
||||
if (multipleCallIds) {
|
||||
ret.push(
|
||||
{
|
||||
title: 'Caller ID',
|
||||
@@ -288,7 +285,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
ret.push(
|
||||
{
|
||||
@@ -309,11 +306,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
);
|
||||
|
||||
return ret;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async takeSmartCameraPicture(option?: PictureOptions): Promise<MediaObject> {
|
||||
return this.createMediaObject(await this.getClient().jpegSnapshot(), 'image/jpeg');
|
||||
@@ -401,11 +398,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
?.replace('.', '')?.toLowerCase()?.trim();
|
||||
if (audioCodec?.includes('aac'))
|
||||
audioCodec = 'aac';
|
||||
else if (audioCodec.includes('g711a'))
|
||||
else if (audioCodec?.includes('g711a'))
|
||||
audioCodec = 'pcm_alaw';
|
||||
else if (audioCodec.includes('g711u'))
|
||||
else if (audioCodec?.includes('g711u'))
|
||||
audioCodec = 'pcm_ulaw';
|
||||
else if (audioCodec.includes('g711'))
|
||||
else if (audioCodec?.includes('g711'))
|
||||
audioCodec = 'pcm';
|
||||
|
||||
if (vso.audio)
|
||||
@@ -490,7 +487,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
this.videoStreamOptions = undefined;
|
||||
|
||||
super.putSetting(key, value);
|
||||
|
||||
|
||||
this.updateDevice();
|
||||
this.updateDeviceInfo();
|
||||
}
|
||||
@@ -639,6 +636,7 @@ class AmcrestProvider extends RtspProvider {
|
||||
device.setHttpPortOverride(settings.httpPort?.toString());
|
||||
if (twoWayAudio)
|
||||
device.putSetting('twoWayAudio', twoWayAudio);
|
||||
device.updateDeviceInfo();
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ If you experience any trouble logging in, clear the username and password boxes,
|
||||
|
||||
If you are unable to see shared cameras in your separate Arlo account, ensure that both your primary and secondary accounts are upgraded according to this [forum post](https://web.archive.org/web/20230710141914/https://community.arlo.com/t5/Arlo-Secure/Invited-friend-cannot-see-devices-on-their-dashboard-Arlo-Pro-2/m-p/1889396#M1813). Verify the sharing worked by logging in via the Arlo web dashboard.
|
||||
|
||||
**If you add or remove cameras from your main Arlo account, or share/un-share/re-share cameras with the Arlo account used with this plugin, ensure that you reload this plugin to get the updated camera state from Arlo Cloud.**
|
||||
|
||||
## General Setup Notes
|
||||
|
||||
* Ensure that your Arlo account's default 2FA option is set to either SMS or email.
|
||||
|
||||
6
plugins/arlo/package-lock.json
generated
6
plugins/arlo/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.8.11",
|
||||
"version": "0.8.26",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.8.11",
|
||||
"version": "0.8.26",
|
||||
"license": "Apache",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.103",
|
||||
"version": "0.2.104",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.8.11",
|
||||
"version": "0.8.26",
|
||||
"description": "Arlo Plugin for Scrypted",
|
||||
"license": "Apache",
|
||||
"keywords": [
|
||||
|
||||
@@ -92,7 +92,8 @@ MEDIA_USER_AGENTS = {
|
||||
class Arlo(object):
|
||||
BASE_URL = 'my.arlo.com'
|
||||
AUTH_URL = 'ocapi-app.arlo.com'
|
||||
BACKUP_AUTH_HOSTS = ['NTIuMjEwLjMuMTIx', 'MzQuMjU1LjkyLjIxMg==', 'MzQuMjUxLjE3Ny45MA==', 'NTQuMjQ2LjE3MS4x']
|
||||
BACKUP_AUTH_HOSTS = ["NTIuMzEuMTU3LjE4MQ==","MzQuMjQ4LjE1My42OQ==","My4yNDguMTI4Ljc3","MzQuMjQ2LjE0LjI5"]
|
||||
#BACKUP_AUTH_HOSTS = BACKUP_AUTH_HOSTS[2:3]
|
||||
TRANSID_PREFIX = 'web'
|
||||
|
||||
random.shuffle(BACKUP_AUTH_HOSTS)
|
||||
@@ -102,6 +103,7 @@ class Arlo(object):
|
||||
self.password = password
|
||||
self.event_stream = None
|
||||
self.request = None
|
||||
self.logged_in = False
|
||||
|
||||
def to_timestamp(self, dt):
|
||||
if sys.version[0] == '2':
|
||||
@@ -153,6 +155,7 @@ class Arlo(object):
|
||||
self.request = Request(mode="cloudscraper")
|
||||
self.request.session.headers.update(headers)
|
||||
self.BASE_URL = 'myapi.arlo.com'
|
||||
self.logged_in = True
|
||||
|
||||
def LoginMFA(self):
|
||||
device_id = str(uuid.uuid4())
|
||||
@@ -173,6 +176,7 @@ class Arlo(object):
|
||||
|
||||
self.request = Request()
|
||||
try:
|
||||
#raise Exception("testing backup hosts")
|
||||
auth_host = self.AUTH_URL
|
||||
self.request.options(f'https://{auth_host}/api/auth', headers=headers)
|
||||
logger.info("Using primary authentication host")
|
||||
@@ -258,6 +262,7 @@ class Arlo(object):
|
||||
}
|
||||
self.request.session.headers.update(headers)
|
||||
self.BASE_URL = 'myapi.arlo.com'
|
||||
self.logged_in = True
|
||||
|
||||
return complete_auth
|
||||
|
||||
@@ -761,7 +766,7 @@ class Arlo(object):
|
||||
raw=True
|
||||
)
|
||||
|
||||
async def StartStream(self, basestation, camera, mode="rtsp"):
|
||||
async def StartStream(self, basestation, camera, mode="rtsp", eager=True):
|
||||
"""
|
||||
This function returns the url of the rtsp video stream.
|
||||
This stream needs to be called within 30 seconds or else it becomes invalid.
|
||||
@@ -770,6 +775,9 @@ class Arlo(object):
|
||||
|
||||
If mode is set to "dash", returns the url to the mpd file for DASH streaming. Note that DASH
|
||||
has very specific header requirements - see GetMPDHeaders()
|
||||
|
||||
If 'eager' is True, will return the stream url without waiting for Arlo to report that
|
||||
the stream has started.
|
||||
"""
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
@@ -799,6 +807,14 @@ class Arlo(object):
|
||||
},
|
||||
headers={"xcloudId":camera.get('xCloudId'), 'User-Agent': ua}
|
||||
)
|
||||
if mode == "rtsp":
|
||||
nl.stream_url_dict['url'] = nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
|
||||
else:
|
||||
nl.stream_url_dict['url'] = nl.stream_url_dict['url'].replace(":80", "")
|
||||
|
||||
if eager:
|
||||
trigger(self)
|
||||
return nl.stream_url_dict['url']
|
||||
|
||||
def callback(self, event):
|
||||
#return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
|
||||
@@ -806,10 +822,7 @@ class Arlo(object):
|
||||
return None
|
||||
properties = event.get("properties", {})
|
||||
if properties.get("activityState") == "userStreamActive":
|
||||
if mode == "rtsp":
|
||||
return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
|
||||
else:
|
||||
return nl.stream_url_dict['url'].replace(":80", "")
|
||||
return nl.stream_url_dict['url']
|
||||
return None
|
||||
|
||||
return await self.TriggerAndHandleEvent(
|
||||
|
||||
@@ -19,17 +19,13 @@ import requests
|
||||
from requests.exceptions import HTTPError
|
||||
from requests_toolbelt.adapters import host_header_ssl
|
||||
import cloudscraper
|
||||
from curl_cffi import requests as curl_cffi_requests
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from .logging import logger
|
||||
|
||||
|
||||
try:
|
||||
from curl_cffi import requests as curl_cffi_requests
|
||||
HAS_CURL_CFFI = True
|
||||
except:
|
||||
HAS_CURL_CFFI = False
|
||||
|
||||
#from requests_toolbelt.utils import dump
|
||||
#def print_raw_http(response):
|
||||
@@ -39,7 +35,7 @@ except:
|
||||
class Request(object):
|
||||
"""HTTP helper class"""
|
||||
|
||||
def __init__(self, timeout=5, mode="curl" if HAS_CURL_CFFI else "cloudscraper"):
|
||||
def __init__(self, timeout=5, mode="curl"):
|
||||
if mode == "curl":
|
||||
logger.debug("HTTP helper using curl_cffi")
|
||||
self.session = curl_cffi_requests.Session(impersonate="chrome110")
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import asyncio
|
||||
import json
|
||||
import sseclient
|
||||
import threading
|
||||
|
||||
import scrypted_arlo_go
|
||||
|
||||
from .stream_async import Stream
|
||||
from .logging import logger
|
||||
|
||||
@@ -18,35 +19,45 @@ class EventStream(Stream):
|
||||
|
||||
def thread_main(self):
|
||||
event_stream = self.event_stream
|
||||
for event in event_stream:
|
||||
logger.debug(f"Received event: {event}")
|
||||
if event is None:
|
||||
logger.info(f"SSE {id(event_stream)} appears to be broken")
|
||||
while True:
|
||||
try:
|
||||
event = event_stream.Next()
|
||||
except:
|
||||
logger.info(f"SSE {event_stream.UUID} exited")
|
||||
if self.shutting_down_stream is event_stream:
|
||||
self.shutting_down_stream = None
|
||||
return None
|
||||
|
||||
if event.data.strip() == "":
|
||||
logger.debug(f"Received event: {event}")
|
||||
|
||||
if event.strip() == "":
|
||||
continue
|
||||
|
||||
try:
|
||||
response = json.loads(event.data.strip())
|
||||
response = json.loads(event.strip())
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
if response.get('action') == 'logout':
|
||||
if self.event_stream_stop_event.is_set() or \
|
||||
self.shutting_down_stream is event_stream:
|
||||
logger.info(f"SSE {id(event_stream)} disconnected")
|
||||
logger.info(f"SSE {event_stream.UUID} disconnected")
|
||||
self.shutting_down_stream = None
|
||||
event_stream.Close()
|
||||
return None
|
||||
elif response.get('status') == 'connected':
|
||||
if not self.connected:
|
||||
logger.info(f"SSE {id(event_stream)} connected")
|
||||
logger.info(f"SSE {event_stream.UUID} connected")
|
||||
self.initializing = False
|
||||
self.connected = True
|
||||
else:
|
||||
self.event_loop.call_soon_threadsafe(self._queue_response, response)
|
||||
|
||||
self.event_stream = sseclient.SSEClient('https://myapi.arlo.com/hmsweb/client/subscribe?token='+self.arlo.request.session.headers.get('Authorization'), session=self.arlo.request.session)
|
||||
self.event_stream = scrypted_arlo_go.NewSSEClient(
|
||||
'https://myapi.arlo.com/hmsweb/client/subscribe?token='+self.arlo.request.session.headers.get('Authorization'),
|
||||
scrypted_arlo_go.HeadersMap(self.arlo.request.session.headers)
|
||||
)
|
||||
self.event_stream.Start()
|
||||
self.event_stream_thread = threading.Thread(name="EventStream", target=thread_main, args=(self, ))
|
||||
self.event_stream_thread.setDaemon(True)
|
||||
self.event_stream_thread.start()
|
||||
@@ -58,6 +69,7 @@ class EventStream(Stream):
|
||||
self.reconnecting = True
|
||||
self.connected = False
|
||||
self.shutting_down_stream = self.event_stream
|
||||
self.shutting_down_stream.Close()
|
||||
self.event_stream = None
|
||||
await self.start()
|
||||
while self.shutting_down_stream is not None:
|
||||
|
||||
@@ -57,6 +57,9 @@ class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTa
|
||||
if self.arlo_device.get("parentId") and self.arlo_device["parentId"] != self.arlo_device["deviceId"]:
|
||||
parent = self.arlo_device["parentId"]
|
||||
|
||||
if parent in self.provider.hidden_device_ids:
|
||||
parent = None
|
||||
|
||||
return {
|
||||
"info": {
|
||||
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from aioice import Candidate
|
||||
from aiortc import RTCSessionDescription, RTCIceGatherer, RTCIceServer
|
||||
from aiortc.rtcicetransport import candidate_to_aioice, candidate_from_aioice
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from async_timeout import timeout as async_timeout
|
||||
@@ -15,12 +18,14 @@ import scrypted_arlo_go
|
||||
import scrypted_sdk
|
||||
from scrypted_sdk.types import Setting, Settings, SettingValue, Device, Camera, VideoCamera, RequestMediaStreamOptions, VideoClips, VideoClip, VideoClipOptions, MotionSensor, AudioSensor, Battery, Charger, ChargeState, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType
|
||||
|
||||
from .experimental import EXPERIMENTAL
|
||||
from .arlo.arlo_async import USER_AGENTS
|
||||
from .base import ArloDeviceBase
|
||||
from .spotlight import ArloSpotlight, ArloFloodlight, ArloNightlight
|
||||
from .vss import ArloSirenVirtualSecuritySystem
|
||||
from .child_process import HeartbeatChildProcess
|
||||
from .util import BackgroundTaskMixin, async_print_exception_guard
|
||||
from .rtcpeerconnection import BackgroundRTCPeerConnection
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
|
||||
@@ -101,6 +106,11 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
"vmc3040s",
|
||||
]
|
||||
|
||||
PTT_IMPL_CHOICES = [
|
||||
"scrypted-arlo-go",
|
||||
"aiortc",
|
||||
]
|
||||
|
||||
timeout: int = 30
|
||||
intercom_session: ArloCameraIntercomSession = None
|
||||
light: ArloSpotlight = None
|
||||
@@ -312,6 +322,21 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def disable_eager_streams(self) -> bool:
|
||||
if self.storage:
|
||||
return True if self.storage.getItem("disable_eager_streams") else False
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def ptt_impl(self) -> str:
|
||||
impl = self.storage.getItem("ptt_impl")
|
||||
if impl is None:
|
||||
impl = ArloCamera.PTT_IMPL_CHOICES[0]
|
||||
#self.storage.setItem("ptt_impl", impl)
|
||||
return impl
|
||||
|
||||
@property
|
||||
def snapshot_throttle_interval(self) -> int:
|
||||
interval = self.storage.getItem("snapshot_throttle_interval")
|
||||
@@ -371,7 +396,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
"type": "boolean",
|
||||
},
|
||||
)
|
||||
result.append(
|
||||
result.extend([
|
||||
{
|
||||
"group": "General",
|
||||
"key": "eco_mode",
|
||||
@@ -380,8 +405,26 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
"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",
|
||||
}
|
||||
)
|
||||
},
|
||||
{
|
||||
"group": "General",
|
||||
"key": "disable_eager_streams",
|
||||
"title": "Disable Eager Streams",
|
||||
"value": self.disable_eager_streams,
|
||||
"description": "If eager streams are disabled, Scrypted will wait for Arlo Cloud to report that " + \
|
||||
"the camera stream has started before passing the stream URL to downstream consumers.",
|
||||
"type": "boolean",
|
||||
},
|
||||
])
|
||||
if self.has_push_to_talk and EXPERIMENTAL:
|
||||
result.append({
|
||||
"group": "General",
|
||||
"key": "ptt_impl",
|
||||
"title": "Two Way Audio Implementation",
|
||||
"value": self.ptt_impl,
|
||||
"description": "Implementation used to perform two-way audio negotiation.",
|
||||
"choices": ArloCamera.PTT_IMPL_CHOICES,
|
||||
})
|
||||
if self.eco_mode:
|
||||
result.append(
|
||||
{
|
||||
@@ -416,7 +459,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
if key in ["wired_to_power"]:
|
||||
self.storage.setItem(key, value == "true" or value == True)
|
||||
await self.provider.discover_devices()
|
||||
elif key in ["eco_mode"]:
|
||||
elif key in ["eco_mode", "disable_eager_streams"]:
|
||||
self.storage.setItem(key, value == "true" or value == True)
|
||||
elif key == "print_debug":
|
||||
self.logger.info(f"Device Capabilities: {self.arlo_capabilities}")
|
||||
@@ -457,7 +500,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
return await scrypted_sdk.mediaManager.createMediaObject(self.last_picture, "image/jpeg")
|
||||
|
||||
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}")
|
||||
self.logger.debug(f"Got snapshot URL at {pic_url}")
|
||||
|
||||
if pic_url is None:
|
||||
raise Exception("Error taking snapshot: no url returned")
|
||||
@@ -511,15 +554,15 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
|
||||
async def _getVideoStreamURL(self, container: str) -> str:
|
||||
self.logger.info(f"Requesting {container} stream")
|
||||
url = await asyncio.wait_for(self.provider.arlo.StartStream(self.arlo_basestation, self.arlo_device, mode=container), timeout=self.timeout)
|
||||
url = await asyncio.wait_for(self.provider.arlo.StartStream(self.arlo_basestation, self.arlo_device, mode=container, eager=not self.disable_eager_streams), timeout=self.timeout)
|
||||
self.logger.debug(f"Got {container} stream URL at {url}")
|
||||
return url
|
||||
|
||||
@async_print_exception_guard
|
||||
async def getVideoStream(self, options: RequestMediaStreamOptions = None) -> MediaObject:
|
||||
async def getVideoStream(self, options: RequestMediaStreamOptions = {}) -> MediaObject:
|
||||
self.logger.debug("Entered getVideoStream")
|
||||
|
||||
mso = await self.getVideoStreamOptions(id=options["id"])
|
||||
mso = await self.getVideoStreamOptions(id=options.get("id", "default"))
|
||||
mso['refreshAt'] = round(time.time() * 1000) + 30 * 60 * 1000
|
||||
container = mso["container"]
|
||||
|
||||
@@ -555,7 +598,10 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
self.intercom_session = ArloCameraSIPIntercomSession(self)
|
||||
else:
|
||||
# we need to do signaling through arlo cloud apis
|
||||
self.intercom_session = ArloCameraWebRTCIntercomSession(self)
|
||||
if self.ptt_impl == "scrypted-arlo-go":
|
||||
self.intercom_session = ArloCameraWebRTCIntercomSession(self)
|
||||
else:
|
||||
self.intercom_session = ArloCameraPyAVIntercomSession(self)
|
||||
await self.intercom_session.initialize_push_to_talk(media)
|
||||
|
||||
self.logger.info("Intercom initialized")
|
||||
@@ -901,4 +947,103 @@ class ArloCameraSIPIntercomSession(ArloCameraIntercomSession):
|
||||
self.intercom_ffmpeg_subprocess = None
|
||||
if self.arlo_sip is not None:
|
||||
self.arlo_sip.Close()
|
||||
self.arlo_sip = None
|
||||
self.arlo_sip = None
|
||||
|
||||
class ArloCameraPyAVIntercomSession(ArloCameraWebRTCIntercomSession):
|
||||
def start_sdp_answer_subscription(self) -> None:
|
||||
def callback(sdp):
|
||||
if self.arlo_pc and not self.arlo_sdp_answered:
|
||||
if "a=mid:" not in sdp:
|
||||
# arlo appears to not return a mux id in the response, which
|
||||
# doesn't play nicely with our webrtc peers. let's add it
|
||||
sdp += "a=mid:0\r\n"
|
||||
self.logger.info(f"Arlo response sdp:\n{sdp}")
|
||||
|
||||
sdp = RTCSessionDescription(sdp=sdp, type="answer")
|
||||
self.create_task(self.arlo_pc.setRemoteDescription(sdp))
|
||||
self.arlo_sdp_answered = True
|
||||
return self.stop_subscriptions
|
||||
|
||||
self.register_task(
|
||||
self.provider.arlo.SubscribeToSDPAnswers(self.arlo_basestation, self.arlo_device, callback)
|
||||
)
|
||||
|
||||
def start_candidate_answer_subscription(self) -> None:
|
||||
def callback(candidate):
|
||||
if self.arlo_pc:
|
||||
prefix = "a=candidate:"
|
||||
if candidate.startswith(prefix):
|
||||
candidate = candidate[len(prefix):]
|
||||
candidate = candidate.strip()
|
||||
self.logger.info(f"Arlo response candidate: {candidate}")
|
||||
|
||||
candidate = candidate_from_aioice(Candidate.from_sdp(candidate))
|
||||
if candidate.sdpMid is None:
|
||||
# arlo appears to not return a mux id in the response, which
|
||||
# doesn't play nicely with aiortc. let's add it
|
||||
candidate.sdpMid = 0
|
||||
self.create_task(self.arlo_pc.addIceCandidate(candidate))
|
||||
return self.stop_subscriptions
|
||||
|
||||
self.register_task(
|
||||
self.provider.arlo.SubscribeToCandidateAnswers(self.arlo_basestation, self.arlo_device, callback)
|
||||
)
|
||||
|
||||
@async_print_exception_guard
|
||||
async def initialize_push_to_talk(self, media: MediaObject) -> None:
|
||||
self.logger.info("Initializing push to talk")
|
||||
|
||||
ffmpeg_params = json.loads(await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput.value))
|
||||
self.logger.debug(f"Received ffmpeg params: {ffmpeg_params}")
|
||||
|
||||
session_id, ice_servers = self.provider.arlo.StartPushToTalk(self.arlo_basestation, self.arlo_device)
|
||||
self.logger.debug(f"Received ice servers: {[ice['url'] for ice in ice_servers]}")
|
||||
|
||||
ice_servers = [
|
||||
RTCIceServer(urls=ice["url"], credential=ice.get("credential"), username=ice.get("username"))
|
||||
for ice in ice_servers
|
||||
]
|
||||
ice_gatherer = RTCIceGatherer(ice_servers)
|
||||
await ice_gatherer.gather()
|
||||
|
||||
local_candidates = [
|
||||
f"candidate:{Candidate.to_sdp(candidate_to_aioice(candidate))}"
|
||||
for candidate in ice_gatherer.getLocalCandidates()
|
||||
]
|
||||
|
||||
log_candidates = '\n'.join(local_candidates)
|
||||
self.logger.info(f"Local candidates:\n{log_candidates}")
|
||||
|
||||
# MediaPlayer/PyAV will block until the intercom stream starts, and it seems that scrypted waits
|
||||
# for startIntercom to exit before sending data. So, let's do the remaining setup in a coroutine
|
||||
# so this function can return early.
|
||||
# This is required even if we use BackgroundRTCPeerConnection, since setting up MediaPlayer may
|
||||
# block the background thread's event loop and prevent other async functions from running.
|
||||
async def async_setup():
|
||||
pc = self.arlo_pc = BackgroundRTCPeerConnection(self.logger)
|
||||
self.sdp_answered = False
|
||||
|
||||
pc.add_rtsp_audio(ffmpeg_params["url"])
|
||||
|
||||
offer = await pc.createOffer()
|
||||
self.logger.info(f"Arlo offer sdp:\n{offer.sdp}")
|
||||
|
||||
await pc.setLocalDescription(offer)
|
||||
|
||||
self.provider.arlo.NotifyPushToTalkSDP(
|
||||
self.arlo_basestation, self.arlo_device,
|
||||
session_id, offer.sdp
|
||||
)
|
||||
for candidate in local_candidates:
|
||||
self.provider.arlo.NotifyPushToTalkCandidate(
|
||||
self.arlo_basestation, self.arlo_device,
|
||||
session_id, candidate
|
||||
)
|
||||
|
||||
self.create_task(async_setup())
|
||||
|
||||
@async_print_exception_guard
|
||||
async def shutdown(self) -> None:
|
||||
if self.arlo_pc is not None:
|
||||
await self.arlo_pc.close()
|
||||
self.arlo_pc = None
|
||||
|
||||
@@ -27,9 +27,10 @@ from .base import ArloDeviceBase
|
||||
class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
|
||||
arlo_cameras = None
|
||||
arlo_basestations = None
|
||||
all_device_ids: set = set()
|
||||
_arlo_mfa_code = None
|
||||
scrypted_devices = None
|
||||
_arlo = None
|
||||
_arlo: Arlo = None
|
||||
_arlo_mfa_complete_auth = None
|
||||
device_discovery_lock: asyncio.Lock = None
|
||||
|
||||
@@ -157,6 +158,23 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
self.storage.setItem("imap_mfa_interval", interval)
|
||||
return int(interval)
|
||||
|
||||
@property
|
||||
def hidden_devices(self) -> List[str]:
|
||||
hidden = self.storage.getItem("hidden_devices")
|
||||
if hidden is None:
|
||||
hidden = []
|
||||
self.storage.setItem("hidden_devices", hidden)
|
||||
return hidden
|
||||
|
||||
@property
|
||||
def hidden_device_ids(self) -> List[str]:
|
||||
ids = []
|
||||
for id in self.hidden_devices:
|
||||
m = re.match(r".*\((.*)\)$", id)
|
||||
if m is not None:
|
||||
ids.append(m.group(1))
|
||||
return ids
|
||||
|
||||
@property
|
||||
def arlo(self) -> Arlo:
|
||||
if self._arlo is not None:
|
||||
@@ -530,6 +548,16 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
"value": self.plugin_verbosity == "Verbose",
|
||||
"type": "boolean",
|
||||
},
|
||||
{
|
||||
"group": "General",
|
||||
"key": "hidden_devices",
|
||||
"title": "Hidden Devices",
|
||||
"description": "Select the Arlo devices to hide in this plugin. Hidden devices will be removed from Scrypted and will "
|
||||
"not be re-added when the plugin reloads.",
|
||||
"value": self.hidden_devices,
|
||||
"multiple": True,
|
||||
"choices": [id for id in self.all_device_ids],
|
||||
},
|
||||
])
|
||||
|
||||
return results
|
||||
@@ -573,6 +601,11 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
elif key.startswith("imap_mfa"):
|
||||
self.initialize_imap()
|
||||
skip_arlo_client = True
|
||||
elif key == "hidden_devices":
|
||||
if self._arlo is not None and self._arlo.logged_in:
|
||||
self._arlo.Unsubscribe()
|
||||
await self.do_arlo_setup()
|
||||
skip_arlo_client = True
|
||||
else:
|
||||
# force arlo client to be invalidated and reloaded
|
||||
self.invalidate_arlo_client()
|
||||
@@ -618,12 +651,13 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
return await self.discover_devices_impl()
|
||||
|
||||
async def discover_devices_impl(self) -> None:
|
||||
if not self.arlo:
|
||||
if not self._arlo or not self._arlo.logged_in:
|
||||
raise Exception("Arlo client not connected, cannot discover devices")
|
||||
|
||||
self.logger.info("Discovering devices...")
|
||||
self.arlo_cameras = {}
|
||||
self.arlo_basestations = {}
|
||||
self.all_device_ids = set()
|
||||
self.scrypted_devices = {}
|
||||
|
||||
camera_devices = []
|
||||
@@ -632,13 +666,20 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
basestations = self.arlo.GetDevices(['basestation', 'siren'])
|
||||
for basestation in basestations:
|
||||
nativeId = basestation["deviceId"]
|
||||
self.all_device_ids.add(f"{basestation['deviceName']} ({nativeId})")
|
||||
|
||||
self.logger.debug(f"Adding {nativeId}")
|
||||
|
||||
if nativeId in self.arlo_basestations:
|
||||
self.logger.info(f"Skipping basestation {nativeId} ({basestation['modelId']}) as it has already been added")
|
||||
continue
|
||||
|
||||
self.arlo_basestations[nativeId] = basestation
|
||||
|
||||
if nativeId in self.hidden_device_ids:
|
||||
self.logger.info(f"Skipping manifest for basestation {nativeId} ({basestation['modelId']}) as it is hidden")
|
||||
continue
|
||||
|
||||
device = await self.getDevice_impl(nativeId)
|
||||
scrypted_interfaces = device.get_applicable_interfaces()
|
||||
manifest = device.get_device_manifest()
|
||||
@@ -657,11 +698,13 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(child_manifest)
|
||||
provider_to_device_map.setdefault(child_manifest["providerNativeId"], []).append(child_manifest)
|
||||
|
||||
self.logger.info(f"Discovered {len(basestations)} basestations")
|
||||
self.logger.info(f"Discovered {len(self.arlo_basestations)} basestations")
|
||||
|
||||
cameras = self.arlo.GetDevices(['camera', "arloq", "arloqs", "doorbell"])
|
||||
for camera in cameras:
|
||||
nativeId = camera["deviceId"]
|
||||
self.all_device_ids.add(f"{camera['deviceName']} ({nativeId})")
|
||||
|
||||
self.logger.debug(f"Adding {nativeId}")
|
||||
|
||||
if camera["deviceId"] != camera["parentId"] and camera["parentId"] not in self.arlo_basestations:
|
||||
@@ -671,6 +714,11 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
if nativeId in self.arlo_cameras:
|
||||
self.logger.info(f"Skipping camera {nativeId} ({camera['modelId']}) as it has already been added")
|
||||
continue
|
||||
|
||||
if nativeId in self.hidden_device_ids:
|
||||
self.logger.info(f"Skipping camera {camera['deviceId']} ({camera['modelId']}) because it is hidden")
|
||||
continue
|
||||
|
||||
self.arlo_cameras[nativeId] = camera
|
||||
|
||||
if camera["deviceId"] == camera["parentId"]:
|
||||
@@ -683,7 +731,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
manifest = device.get_device_manifest()
|
||||
self.logger.debug(f"Interfaces for {nativeId} ({camera['modelId']} parent {camera['parentId']}): {scrypted_interfaces}")
|
||||
|
||||
if camera["deviceId"] == camera["parentId"]:
|
||||
if camera["deviceId"] == camera["parentId"] or camera["parentId"] in self.hidden_device_ids:
|
||||
provider_to_device_map.setdefault(None, []).append(manifest)
|
||||
else:
|
||||
provider_to_device_map.setdefault(camera["parentId"], []).append(manifest)
|
||||
@@ -701,7 +749,9 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
|
||||
if len(cameras) != len(camera_devices):
|
||||
self.logger.info(f"Discovered {len(cameras)} cameras, but only {len(camera_devices)} are usable")
|
||||
self.logger.info(f"Are all cameras shared with admin permissions?")
|
||||
self.logger.info("This could be because some cameras are hidden.")
|
||||
self.logger.info("If a camera is not hidden but is still missing, ensure all cameras shared with "
|
||||
"admin permissions in the Arlo app.")
|
||||
else:
|
||||
self.logger.info(f"Discovered {len(cameras)} cameras")
|
||||
|
||||
@@ -726,7 +776,11 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
})
|
||||
self.logger.debug("Done discovering devices")
|
||||
|
||||
# force a settings refresh so the hidden devices list can be updated
|
||||
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
|
||||
|
||||
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
|
||||
self.logger.debug(f"Scrypted requested to load device {nativeId}")
|
||||
async with self.device_discovery_lock:
|
||||
return await self.getDevice_impl(nativeId)
|
||||
|
||||
|
||||
107
plugins/arlo/src/arlo_plugin/rtcpeerconnection.py
Normal file
107
plugins/arlo/src/arlo_plugin/rtcpeerconnection.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from aiortc import RTCPeerConnection
|
||||
from aiortc.contrib.media import MediaPlayer
|
||||
import asyncio
|
||||
import threading
|
||||
import queue
|
||||
|
||||
|
||||
class BackgroundRTCPeerConnection:
|
||||
"""Proxy class to use RTCPeerConnection in a background thread.
|
||||
|
||||
The purpose of this proxy is to ensure that RTCPeerConnection operations
|
||||
do not block the main asyncio thread. From testing, it seems that the
|
||||
close() function blocks until the source RTSP server exits, which we
|
||||
have no control over. Additionally, since asyncio coroutines are tied
|
||||
to the event loop they were constructed from, it is not possible to only
|
||||
run close() in a separate thread. Therefore, each instance of RTCPeerConnection
|
||||
is launched within its own ephemeral thread, which cleans itself up once
|
||||
close() completes.
|
||||
"""
|
||||
|
||||
def __init__(self, logger):
|
||||
self.main_loop = asyncio.get_event_loop()
|
||||
self.background_loop = asyncio.new_event_loop()
|
||||
self.logger = logger
|
||||
|
||||
self.thread_started = queue.Queue(1)
|
||||
self.thread = threading.Thread(target=self.__background_main)
|
||||
self.thread.start()
|
||||
self.thread_started.get()
|
||||
|
||||
def __background_main(self):
|
||||
self.logger.info(f"Background RTC loop {self.thread.name} starting")
|
||||
self.pc = RTCPeerConnection()
|
||||
|
||||
asyncio.set_event_loop(self.background_loop)
|
||||
self.thread_started.put(True)
|
||||
self.background_loop.run_forever()
|
||||
|
||||
self.logger.info(f"Background RTC loop {self.thread.name} exiting")
|
||||
|
||||
async def __run_background(self, coroutine, await_result=True, stop_loop=False):
|
||||
fut = self.main_loop.create_future()
|
||||
|
||||
def background_callback():
|
||||
# callback to run on main_loop.
|
||||
def to_main(result, is_error):
|
||||
if is_error:
|
||||
fut.set_exception(result)
|
||||
else:
|
||||
fut.set_result(result)
|
||||
|
||||
# callback to run on background_loop., after the coroutine completes
|
||||
def callback(task):
|
||||
is_error = False
|
||||
if task.exception():
|
||||
result = task.exception()
|
||||
is_error = True
|
||||
else:
|
||||
result = task.result()
|
||||
|
||||
# send results to the main loop
|
||||
self.main_loop.call_soon_threadsafe(to_main, result, is_error)
|
||||
|
||||
# stopping the loop here ensures that the coroutine completed
|
||||
# and doesn't raise any "task not awaited" exceptions
|
||||
if stop_loop:
|
||||
self.background_loop.stop()
|
||||
|
||||
task = self.background_loop.create_task(coroutine)
|
||||
task.add_done_callback(callback)
|
||||
|
||||
# start the callback in the background loop
|
||||
self.background_loop.call_soon_threadsafe(background_callback)
|
||||
|
||||
if not await_result:
|
||||
return None
|
||||
return await fut
|
||||
|
||||
async def createOffer(self):
|
||||
return await self.__run_background(self.pc.createOffer())
|
||||
|
||||
async def setLocalDescription(self, sdp):
|
||||
return await self.__run_background(self.pc.setLocalDescription(sdp))
|
||||
|
||||
async def setRemoteDescription(self, sdp):
|
||||
return await self.__run_background(self.pc.setRemoteDescription(sdp))
|
||||
|
||||
async def addIceCandidate(self, candidate):
|
||||
return await self.__run_background(self.pc.addIceCandidate(candidate))
|
||||
|
||||
async def close(self):
|
||||
await self.__run_background(self.pc.close(), await_result=False, stop_loop=True)
|
||||
|
||||
def add_rtsp_audio(self, rtsp_url):
|
||||
"""Adds an audio track to the RTCPeerConnection given a source RTSP url.
|
||||
|
||||
This constructs a MediaPlayer in the background thread's asyncio loop,
|
||||
since MediaPlayer also utilizes coroutines and asyncio.
|
||||
|
||||
Note that this may block the background thread's event loop if the RTSP
|
||||
server is not yet ready.
|
||||
"""
|
||||
def add_rtsp_audio_background():
|
||||
media_player = MediaPlayer(rtsp_url, format="rtsp")
|
||||
self.pc.addTrack(media_player.audio)
|
||||
|
||||
self.background_loop.call_soon_threadsafe(add_rtsp_audio_background)
|
||||
@@ -1,13 +1,14 @@
|
||||
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.4.0
|
||||
scrypted-arlo-go==0.5.2
|
||||
cloudscraper==1.2.71
|
||||
curl-cffi==0.5.7; platform_machine != 'armv7l'
|
||||
curl-cffi==0.5.7
|
||||
async-timeout==4.0.2
|
||||
beautifulsoup4==4.12.2
|
||||
--extra-index-url=https://www.piwheels.org/simple/
|
||||
aiortc==1.5.0
|
||||
av==9.2.0
|
||||
--extra-index-url=https://bjia56.github.io/armv7l-wheels/
|
||||
--extra-index-url=https://bjia56.github.io/scrypted-arlo-go/
|
||||
--prefer-binary
|
||||
18
plugins/bticino/package-lock.json
generated
18
plugins/bticino/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/bticino",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.11",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/bticino",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.11",
|
||||
"dependencies": {
|
||||
"@slyoldfox/sip": "^0.0.6-1",
|
||||
"sdp": "^3.0.3",
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.85",
|
||||
"version": "0.2.103",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -905,9 +905,9 @@
|
||||
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
@@ -1832,9 +1832,9 @@
|
||||
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/bticino",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.11",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -28,7 +28,6 @@
|
||||
],
|
||||
"pluginDependencies": [
|
||||
"@scrypted/prebuffer-mixin",
|
||||
"@scrypted/pam-diff",
|
||||
"@scrypted/snapshot"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ const { mediaManager } = sdk;
|
||||
export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvider, Intercom, Camera, VideoCamera, Settings, BinarySensor, HttpRequestHandler, VideoClips, Reboot {
|
||||
|
||||
private session: SipCallSession
|
||||
private remoteRtpDescription: RtpDescription
|
||||
private remoteRtpDescription: Promise<RtpDescription>
|
||||
private audioOutForwarder: dgram.Socket
|
||||
private audioOutProcess: ChildProcess
|
||||
private currentMedia: FFmpegInput | MediaStreamUrl
|
||||
@@ -158,9 +158,10 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
|
||||
const audioOutForwarder = await createBindZero()
|
||||
this.audioOutForwarder = audioOutForwarder.server
|
||||
let address = (await this.remoteRtpDescription).address
|
||||
audioOutForwarder.server.on('message', message => {
|
||||
if( this.session )
|
||||
this.session.audioSplitter.send(message, 40004, this.remoteRtpDescription.address)
|
||||
this.session.audioSplitter.send(message, 40004, address)
|
||||
return null
|
||||
});
|
||||
|
||||
@@ -244,7 +245,12 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
client.setKeepAlive(true, 10000)
|
||||
let sip: SipCallSession
|
||||
try {
|
||||
await this.controllerApi.updateStreamEndpoint()
|
||||
if( !this.incomingCallRequest ) {
|
||||
// If this is a "view" call, update the stream endpoint to send it only to "us"
|
||||
// In case of an incoming doorbell event, the C300X is already streaming video to all registered endpoints
|
||||
await this.controllerApi.updateStreamEndpoint()
|
||||
}
|
||||
|
||||
let rtsp: RtspServer;
|
||||
const cleanup = () => {
|
||||
client.destroy();
|
||||
@@ -271,7 +277,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
sip.onCallEnded.subscribe(cleanup)
|
||||
|
||||
// Call the C300X
|
||||
this.remoteRtpDescription = await sip.callOrAcceptInvite(
|
||||
this.remoteRtpDescription = sip.callOrAcceptInvite(
|
||||
( audio ) => {
|
||||
return [
|
||||
//TODO: Payload types are hardcoded
|
||||
|
||||
41
plugins/chromecast/package-lock.json
generated
41
plugins/chromecast/package-lock.json
generated
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/chromecast",
|
||||
"version": "0.1.55",
|
||||
"version": "0.1.56",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/chromecast",
|
||||
"version": "0.1.55",
|
||||
"hasInstallScript": true,
|
||||
"version": "0.1.56",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
@@ -40,38 +39,39 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.199",
|
||||
"version": "0.2.103",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.13.8",
|
||||
"esbuild": "^0.15.9",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"webpack": "^5.59.0"
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
"scrypted-debug": "bin/scrypted-debug.js",
|
||||
"scrypted-deploy": "bin/scrypted-deploy.js",
|
||||
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
|
||||
"scrypted-package-json": "bin/scrypted-package-json.js",
|
||||
"scrypted-readme": "bin/scrypted-readme.js",
|
||||
"scrypted-setup-project": "bin/scrypted-setup-project.js",
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.1",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.22.8",
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
@@ -386,23 +386,24 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@types/node": "^16.11.1",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.13.8",
|
||||
"esbuild": "^0.15.9",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.22.8",
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"webpack": "^5.59.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/chromecast",
|
||||
"version": "0.1.55",
|
||||
"version": "0.1.56",
|
||||
"description": "Send video, audio, and text to speech notifications to Chromecast and Google Home devices",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
1
plugins/cloud/.gitignore
vendored
1
plugins/cloud/.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
out/
|
||||
node_modules/
|
||||
dist/
|
||||
external
|
||||
|
||||
@@ -6,4 +6,4 @@ fs
|
||||
src
|
||||
.vscode
|
||||
dist/*.js
|
||||
node-nat-upnp
|
||||
external
|
||||
|
||||
@@ -3,8 +3,35 @@
|
||||
1. Log into Scrypted Cloud using the login button.
|
||||
2. This Scrypted server is now available at https://home.scrypted.app.
|
||||
|
||||
See below for additional recommendations.
|
||||
|
||||
## Optional but Recommended
|
||||
## Port Forwarding
|
||||
|
||||
1. Set up Port Forwarding with UPNP or Router Forwarding.
|
||||
2. Use the Advanced Tab to verify Port Forwarding is correctly configured.
|
||||
1. Open the Firewall and Port Forwarding Settings on the network's router.
|
||||
2. Use the ports shown in Settings to configure a Port Forwarding rule on the router.
|
||||
|
||||
Use the `Test Port Forward` buttin in `Advanced` Settings tab to verify the configuration is correct.
|
||||
|
||||
## Custom Domains
|
||||
|
||||
Custom Domains can be used with the Cloud Plugin.
|
||||
|
||||
Set up a reverse proxy to the https Forward Port shown in settings.
|
||||
|
||||
|
||||
## Cloudflare Tunnels
|
||||
|
||||
Scrypted Cloud automatically creates a login free tunnel for remote access.
|
||||
|
||||
The following steps are only necessary if you want to associate the tunnel with your existing Cloudflare account to manage it remotely.
|
||||
|
||||
1. Create the Tunnel in the [Cloudflare Zero Trust Dashboard](https://one.dash.cloudflare.com).
|
||||
2. Copy the token shown for the tunnel shown in the `install [token]` command. E.g. `cloudflared service install eyJhI344aA...`.
|
||||
3. Paste the token into the Cloud Plugin Advanced Settings.
|
||||
4. Add a `Public Hostname` to the tunnel.
|
||||
* Choose a (sub)domain.
|
||||
* Service `Type` is `HTTPS` and `URL` is `localhost:port`. Replace the port with `Forward Port` from Cloud Plugin Settings.
|
||||
* Expand `Additional Application Settings` -> `TLS` menus and enable `No TLS Verify`.
|
||||
|
||||
5. Reload Cloud Plugin.
|
||||
6. Verify Cloudflare successfully connected by observing the `Console` Logs.
|
||||
7896
plugins/cloud/package-lock.json
generated
7896
plugins/cloud/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@
|
||||
"scrypted": {
|
||||
"name": "Scrypted Cloud",
|
||||
"type": "API",
|
||||
"realfs": true,
|
||||
"interfaces": [
|
||||
"SystemSettings",
|
||||
"BufferConverter",
|
||||
@@ -40,20 +41,18 @@
|
||||
"@eneris/push-receiver": "^3.1.4",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"axios": "^0.25.0",
|
||||
"bpmux": "^8.1.3",
|
||||
"debug": "^4.3.1",
|
||||
"axios": "^1.4.0",
|
||||
"bpmux": "^8.2.1",
|
||||
"cloudflared": "^0.4.0",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"http-proxy": "^1.18.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nat-upnp": "file:./node-nat-upnp",
|
||||
"query-string": "^6.14.1"
|
||||
"nat-upnp": "file:./external/node-nat-upnp"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/http-proxy": "^1.17.5",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/http-proxy": "^1.17.11",
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/nat-upnp": "^1.1.2",
|
||||
"@types/node": "^18.11.18"
|
||||
"@types/node": "^20.4.5"
|
||||
},
|
||||
"version": "0.1.14"
|
||||
"version": "0.1.41"
|
||||
}
|
||||
|
||||
58
plugins/cloud/src/greenlock.ts
Normal file
58
plugins/cloud/src/greenlock.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import path from 'path';
|
||||
|
||||
// "optionalDependencies": {
|
||||
// "@greenlock/manager": "^3.1.0",
|
||||
// "@koush/greenlock": "^4.0.9",
|
||||
// "acme-dns-01-duckdns": "^3.0.1",
|
||||
// "greenlock-store-fs": "^3.2.2"
|
||||
// },
|
||||
|
||||
export async function registerDuckDns(duckDnsHostname: string, duckDnsToken: string): Promise<{
|
||||
cert: string;
|
||||
chain: string;
|
||||
privkey: string;
|
||||
}> {
|
||||
const pluginVolume = process.env.SCRYPTED_PLUGIN_VOLUME;
|
||||
const greenlockD = path.join(pluginVolume, 'greenlock.d');
|
||||
|
||||
const Greenlock = require('@koush/greenlock');
|
||||
const greenlock = Greenlock.create({
|
||||
packageRoot: process.env.NODE_PATH,
|
||||
configDir: greenlockD,
|
||||
packageAgent: 'Scrypted/1.0',
|
||||
maintainerEmail: 'koushd@gmail.com',
|
||||
notify: function (event, details) {
|
||||
if ('error' === event) {
|
||||
// `details` is an error object in this case
|
||||
console.error(details);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await greenlock.manager
|
||||
.defaults({
|
||||
challenges: {
|
||||
'dns-01': {
|
||||
module: 'acme-dns-01-duckdns',
|
||||
token: duckDnsToken,
|
||||
},
|
||||
},
|
||||
agreeToTerms: true,
|
||||
subscriberEmail: 'koushd@gmail.com',
|
||||
});
|
||||
|
||||
const altnames = [duckDnsHostname];
|
||||
|
||||
const r = await greenlock
|
||||
.add({
|
||||
subject: altnames[0],
|
||||
altnames: altnames
|
||||
});
|
||||
|
||||
const result = await greenlock
|
||||
.get({ servername: duckDnsHostname });
|
||||
|
||||
|
||||
const { pems } = result;
|
||||
return pems;
|
||||
}
|
||||
@@ -11,12 +11,19 @@ import upnp from 'nat-upnp';
|
||||
import net from 'net';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import qs from 'query-string';
|
||||
import { Duplex } from 'stream';
|
||||
import { Duplex, Readable } from 'stream';
|
||||
import tls from 'tls';
|
||||
import Url from 'url';
|
||||
import { createSelfSignedCertificate } from '../../../server/src/cert';
|
||||
import { PushManager } from './push';
|
||||
import { readLine } from '../../../common/src/read-stream';
|
||||
import { qsparse, qsstringify } from "./qs";
|
||||
import * as cloudflared from 'cloudflared';
|
||||
import fs, { mkdirSync, renameSync, rmSync } from 'fs';
|
||||
import { backOff } from "exponential-backoff";
|
||||
import ip from 'ip';
|
||||
import { Deferred } from "@scrypted/common/src/deferred";
|
||||
|
||||
// import { registerDuckDns } from "./greenlock";
|
||||
|
||||
const { deviceManager, endpointManager, systemManager } = sdk;
|
||||
|
||||
@@ -34,9 +41,9 @@ class ScryptedPush extends ScryptedDeviceBase implements BufferConverter {
|
||||
}
|
||||
|
||||
async convert(data: Buffer | string, fromMimeType: string): Promise<Buffer> {
|
||||
if (this.cloud.storageSettings.values.forwardingMode === 'Custom Domain' && this.cloud.storageSettings.values.hostname) {
|
||||
return Buffer.from(`https://${this.cloud.getHostname()}${await this.cloud.getCloudMessagePath()}/${data}`);
|
||||
}
|
||||
const validDomain = this.cloud.getSSLHostname();
|
||||
if (validDomain)
|
||||
return Buffer.from(`https://${validDomain}${await this.cloud.getCloudMessagePath()}/${data}`);
|
||||
|
||||
const url = `http://127.0.0.1/push/${data}`;
|
||||
return this.cloud.whitelist(url, 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.cloud.getHostname()}${SCRYPTED_CLOUD_MESSAGE_PATH}`);
|
||||
@@ -44,6 +51,8 @@ class ScryptedPush extends ScryptedDeviceBase implements BufferConverter {
|
||||
}
|
||||
|
||||
class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings, BufferConverter, DeviceProvider, HttpRequestHandler {
|
||||
cloudflareTunnel: string;
|
||||
cloudflared: Awaited<ReturnType<typeof cloudflared.tunnel>>;
|
||||
manager = new PushManager(DEFAULT_SENDER_ID);
|
||||
server: http.Server;
|
||||
secureServer: https.Server;
|
||||
@@ -83,23 +92,46 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
placeholder: 'my-server.dyndns.com',
|
||||
onPut: () => this.scheduleRefreshPortForward(),
|
||||
},
|
||||
securePort: {
|
||||
title: 'Local HTTPS Port',
|
||||
description: 'The Scrypted Cloud plugin listens on this port for for cloud connections. The router must use UPNP, port forwarding, or a reverse proxy to send requests to this port.',
|
||||
type: 'number',
|
||||
onPut: (ov, nv) => {
|
||||
if (ov && ov !== nv)
|
||||
this.log.a('Reload the Scrypted Cloud Plugin to apply the port change.');
|
||||
duckDnsToken: {
|
||||
hide: true,
|
||||
title: 'Duck DNS Token',
|
||||
placeholder: 'xxxxx123456',
|
||||
onPut: () => {
|
||||
this.storageSettings.values.duckDnsCertValid = false;
|
||||
this.log.a('Reload the Scrypted Cloud Plugin to apply the Duck DNS change.');
|
||||
}
|
||||
},
|
||||
duckDnsHostname: {
|
||||
hide: true,
|
||||
title: 'Duck DNS Hostname',
|
||||
placeholder: 'my-scrypted.duckdns.org',
|
||||
onPut: () => {
|
||||
this.storageSettings.values.duckDnsCertValid = false;
|
||||
this.log.a('Reload the Scrypted Cloud Plugin to apply the Duck DNS change.');
|
||||
}
|
||||
},
|
||||
duckDnsCertValid: {
|
||||
type: 'boolean',
|
||||
hide: true,
|
||||
},
|
||||
upnpPort: {
|
||||
title: 'External HTTPS Port',
|
||||
title: 'From Port',
|
||||
description: "The external network port on router used by port forwarding.",
|
||||
type: 'number',
|
||||
onPut: (ov, nv) => {
|
||||
if (ov !== nv)
|
||||
this.scheduleRefreshPortForward();
|
||||
},
|
||||
},
|
||||
securePort: {
|
||||
title: 'Forward Port',
|
||||
description: 'The internal https port used by the Scrypted Cloud plugin. Connections must be forwarded to this port on this server\'s internal IP address.',
|
||||
type: 'number',
|
||||
onPut: (ov, nv) => {
|
||||
if (ov && ov !== nv)
|
||||
this.log.a('Reload the Scrypted Cloud Plugin to apply the port change.');
|
||||
}
|
||||
},
|
||||
upnpStatus: {
|
||||
title: 'UPNP Status',
|
||||
description: 'The status of the UPNP NAT reservation.',
|
||||
@@ -119,11 +151,28 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
hide: true,
|
||||
json: true,
|
||||
},
|
||||
cloudflaredTunnelToken: {
|
||||
group: 'Advanced',
|
||||
title: 'Cloudflare Tunnel Token',
|
||||
description: 'Optional: Enter the Cloudflare token from the Cloudflare Dashbaord to track and manage the tunnel remotely.',
|
||||
onPut: () => {
|
||||
this.cloudflared?.child.kill();
|
||||
},
|
||||
},
|
||||
cloudflaredTunnelUrl: {
|
||||
group: 'Advanced',
|
||||
title: 'Cloudflare Tunnel URL',
|
||||
description: 'Cloudflare Tunnel URL is a randomized cloud connection, unless a Cloudflare Tunnel Token is provided.',
|
||||
readonly: true,
|
||||
mapGet: () => this.cloudflareTunnel || 'Unavailable',
|
||||
},
|
||||
register: {
|
||||
group: 'Advanced',
|
||||
title: 'Register',
|
||||
type: 'button',
|
||||
onPut: () => this.manager.registrationId.then(r => this.sendRegistrationId(r)),
|
||||
onPut: () => {
|
||||
this.manager.registrationId.then(r => this.sendRegistrationId(r))
|
||||
},
|
||||
description: 'Register server with Scrypted Cloud.',
|
||||
},
|
||||
testPortForward: {
|
||||
@@ -133,12 +182,21 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
onPut: () => this.testPortForward(),
|
||||
description: 'Test the port forward connection from Scrypted Cloud.',
|
||||
},
|
||||
additionalCorsOrigins: {
|
||||
title: "Additional CORS Origins",
|
||||
description: "Debugging purposes only. DO NOT EDIT.",
|
||||
group: 'CORS',
|
||||
multiple: true,
|
||||
combobox: true,
|
||||
defaultValue: [],
|
||||
}
|
||||
});
|
||||
upnpInterval: NodeJS.Timeout;
|
||||
upnpClient = upnp.createClient();
|
||||
upnpStatus = 'Starting';
|
||||
securePort: number;
|
||||
randomBytes = crypto.randomBytes(16).toString('base64');
|
||||
reverseConnections = new Set<Duplex>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -171,7 +229,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
|
||||
this.storageSettings.settings.securePort.onGet = async () => {
|
||||
return {
|
||||
hide: this.storageSettings.values.forwardingMode === 'Disabled',
|
||||
group: this.storageSettings.values.forwardingMode === 'Disabled' ? 'Advanced' : undefined,
|
||||
title: this.storageSettings.values.forwardingMode === 'Disabled' ? 'Cloudflare Port' : 'Forward Port',
|
||||
}
|
||||
};
|
||||
|
||||
@@ -181,6 +240,20 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
};
|
||||
|
||||
// this.storageSettings.settings.duckDnsToken.onGet = async () => {
|
||||
// return {
|
||||
// hide: this.storageSettings.values.forwardingMode === 'Custom Domain'
|
||||
// || this.storageSettings.values.forwardingMode === 'Disabled',
|
||||
// }
|
||||
// };
|
||||
|
||||
// this.storageSettings.settings.duckDnsHostname.onGet = async () => {
|
||||
// return {
|
||||
// hide: this.storageSettings.values.forwardingMode === 'Custom Domain'
|
||||
// || this.storageSettings.values.forwardingMode === 'Disabled',
|
||||
// }
|
||||
// };
|
||||
|
||||
this.log.clearAlerts();
|
||||
|
||||
this.storageSettings.settings.securePort.onPut = (ov, nv) => {
|
||||
@@ -229,17 +302,52 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.storageSettings.values.upnpPort = upnpPort;
|
||||
|
||||
// scrypted cloud will replace localhost with requesting ip.
|
||||
const ip = this.storageSettings.values.forwardingMode === 'Custom Domain'
|
||||
? this.storageSettings.values.hostname?.toString()
|
||||
: (await axios(`https://${SCRYPTED_SERVER}/_punch/ip`)).data.ip;
|
||||
let ip: string;
|
||||
if (this.storageSettings.values.forwardingMode === 'Custom Domain') {
|
||||
ip = this.storageSettings.values.hostname?.toString();
|
||||
if (!ip)
|
||||
throw new Error('Hostname is required for port Custom Domain setup.');
|
||||
}
|
||||
else if (this.storageSettings.values.duckDnsHostname && this.storageSettings.values.duckDnsToken) {
|
||||
try {
|
||||
const url = new URL('https://www.duckdns.org/update');
|
||||
url.searchParams.set('domains', this.storageSettings.values.duckDnsHostname);
|
||||
url.searchParams.set('token', this.storageSettings.values.duckDnsToken);
|
||||
await axios(url.toString());
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('Duck DNS Erorr', e);
|
||||
throw new Error('Duck DNS Error. See Console Logs.');
|
||||
}
|
||||
|
||||
if (!ip)
|
||||
throw new Error('Hostname is required for port Custom Domain setup.');
|
||||
try {
|
||||
throw new Error('not implemented');
|
||||
// const pems = await registerDuckDns(this.storageSettings.values.duckDnsHostname, this.storageSettings.values.duckDnsToken);
|
||||
// this.storageSettings.values.duckDnsCertValid = true;
|
||||
// const certificate = this.storageSettings.values.certificate;
|
||||
// const chain = pems.cert.trim() + '\n' + pems.chain.trim();
|
||||
// if (certificate.certificate !== chain || certificate.serviceKey !== pems.privkey) {
|
||||
// certificate.certificate = chain;
|
||||
// certificate.serviceKey = pems.privkey;
|
||||
// this.storageSettings.values.certificate = certificate;
|
||||
// deviceManager.requestRestart();
|
||||
// }
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error("Let's Encrypt Error", e);
|
||||
throw new Error("Let's Encrypt Error. See Console Logs.");
|
||||
}
|
||||
|
||||
ip = this.storageSettings.values.duckDnsHostname;
|
||||
}
|
||||
else {
|
||||
ip = (await axios(`https://${SCRYPTED_SERVER}/_punch/ip`)).data.ip;
|
||||
}
|
||||
|
||||
if (this.storageSettings.values.forwardingMode === 'Custom Domain')
|
||||
upnpPort = 443;
|
||||
|
||||
this.console.log(`Mapped port https://127.0.0.1:${this.securePort} to https://${ip}:${upnpPort}`);
|
||||
this.console.log(`Scrypted Cloud mapped https://${ip}:${upnpPort} to https://127.0.0.1:${this.securePort}`);
|
||||
|
||||
// the ip is not sent, but should be checked to see if it changed.
|
||||
if (this.storageSettings.values.lastPersistedUpnpPort !== upnpPort || ip !== this.storageSettings.values.lastPersistedIp) {
|
||||
@@ -341,21 +449,21 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
|
||||
async whitelist(localUrl: string, ttl: number, baseUrl: string): Promise<Buffer> {
|
||||
const local = Url.parse(localUrl);
|
||||
const local = new URL(localUrl);
|
||||
|
||||
if (this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname) {
|
||||
return Buffer.from(`${baseUrl}${local.path}`);
|
||||
if (this.getSSLHostname()) {
|
||||
return Buffer.from(`${baseUrl}${local.pathname}`);
|
||||
}
|
||||
|
||||
if (this.whitelisted.has(local.path)) {
|
||||
return Buffer.from(this.whitelisted.get(local.path));
|
||||
if (this.whitelisted.has(local.pathname)) {
|
||||
return Buffer.from(this.whitelisted.get(local.pathname));
|
||||
}
|
||||
|
||||
const { token_info } = this.storageSettings.values;
|
||||
if (!token_info)
|
||||
throw new Error('@scrypted/cloud is not logged in.');
|
||||
const q = qs.stringify({
|
||||
scope: local.path,
|
||||
const q = qsstringify({
|
||||
scope: local.pathname,
|
||||
ttl,
|
||||
})
|
||||
const scope = await axios(`https://${this.getHostname()}/_punch/scope?${q}`, {
|
||||
@@ -365,13 +473,13 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
})
|
||||
|
||||
const { userToken, userTokenSignature } = scope.data;
|
||||
const tokens = qs.stringify({
|
||||
const tokens = qsstringify({
|
||||
user_token: userToken,
|
||||
user_token_signature: userTokenSignature
|
||||
})
|
||||
|
||||
const url = `${baseUrl}${local.path}?${tokens}`;
|
||||
this.whitelisted.set(local.path, url);
|
||||
const url = `${baseUrl}${local.pathname}?${tokens}`;
|
||||
this.whitelisted.set(local.pathname, url);
|
||||
return Buffer.from(url);
|
||||
}
|
||||
|
||||
@@ -383,6 +491,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
`https://${SCRYPTED_SERVER}`,
|
||||
// chromecast receiver. move this into google home and chromecast plugins?
|
||||
'https://koush.github.io',
|
||||
...this.storageSettings.values.additionalCorsOrigins,
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -393,7 +502,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
|
||||
getAuthority() {
|
||||
const upnp_port = this.storageSettings.values.forwardingMode === 'Custom Domain' ? 443 : this.storageSettings.values.upnpPort;
|
||||
const hostname = this.storageSettings.values.forwardingMode === 'Custom Domain' ? this.storageSettings.values.hostname : undefined;
|
||||
const hostname = this.storageSettings.values.forwardingMode === 'Custom Domain'
|
||||
? this.storageSettings.values.hostname
|
||||
: this.storageSettings.values.duckDnsToken && this.storageSettings.values.duckDnsHostname;
|
||||
|
||||
if (upnp_port === 443 && !hostname) {
|
||||
const error = this.storageSettings.values.forwardingMode === 'Custom Domain'
|
||||
@@ -413,7 +524,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
const { upnp_port, hostname } = this.getAuthority();
|
||||
const registration_secret = this.storageSettings.values.registrationSecret || crypto.randomBytes(8).toString('base64');
|
||||
|
||||
const q = qs.stringify({
|
||||
const q = qsstringify({
|
||||
upnp_port,
|
||||
registration_id,
|
||||
sender_id: DEFAULT_SENDER_ID,
|
||||
@@ -479,10 +590,14 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
}
|
||||
|
||||
getSSLHostname() {
|
||||
const validDomain = (this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname)
|
||||
|| (this.storageSettings.values.duckDnsCertValid && this.storageSettings.values.duckDnsHostname && this.storageSettings.values.upnpPort && `${this.storageSettings.values.duckDnsHostname}:${this.storageSettings.values.upnpPort}`);
|
||||
return validDomain;
|
||||
}
|
||||
|
||||
getHostname() {
|
||||
if (this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname)
|
||||
return this.storageSettings.values.hostname;
|
||||
return SCRYPTED_SERVER;
|
||||
return this.getSSLHostname() || SCRYPTED_SERVER;
|
||||
}
|
||||
|
||||
async convert(data: Buffer, fromMimeType: string): Promise<Buffer> {
|
||||
@@ -518,7 +633,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
|
||||
async getOauthUrl(): Promise<string> {
|
||||
const args = qs.stringify({
|
||||
const args = qsstringify({
|
||||
hostname: os.hostname(),
|
||||
registration_id: await this.manager.registrationId,
|
||||
sender_id: DEFAULT_SENDER_ID,
|
||||
@@ -553,9 +668,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
const handler = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
this.console.log(req.socket?.remoteAddress, req.url);
|
||||
|
||||
const url = Url.parse(req.url);
|
||||
if (url.path.startsWith('/web/oauth/callback') && url.query) {
|
||||
const query = qs.parse(url.query);
|
||||
const url = new URL(req.url, 'https://localhost');
|
||||
if (url.pathname.startsWith('/web/oauth/callback') && url.search) {
|
||||
const query = qsparse(url.searchParams);
|
||||
if (!query.callback_url && query.token_info && query.user_info) {
|
||||
this.storageSettings.values.token_info = query.token_info;
|
||||
this.storageSettings.values.lastPersistedRegistrationId = await this.manager.registrationId;
|
||||
@@ -569,16 +684,19 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (url.path === '/web/') {
|
||||
if (this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname)
|
||||
res.setHeader('Location', `https://${this.storageSettings.values.hostname}/endpoint/@scrypted/core/public/`);
|
||||
else
|
||||
else if (url.pathname === '/web/') {
|
||||
const validDomain = this.getSSLHostname();
|
||||
if (validDomain) {
|
||||
res.setHeader('Location', `https://${validDomain}/endpoint/@scrypted/core/public/`);
|
||||
}
|
||||
else {
|
||||
res.setHeader('Location', '/endpoint/@scrypted/core/public/');
|
||||
}
|
||||
res.writeHead(302);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
else if (url.path === '/web/component/home/endpoint') {
|
||||
else if (url.pathname === '/web/component/home/endpoint') {
|
||||
this.proxy.web(req, res, {
|
||||
target: googleHomeTarget.toString(),
|
||||
ignorePath: true,
|
||||
@@ -586,7 +704,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
});
|
||||
return;
|
||||
}
|
||||
else if (url.path === '/web/component/alexa/endpoint') {
|
||||
else if (url.pathname === '/web/component/alexa/endpoint') {
|
||||
this.proxy.web(req, res, {
|
||||
target: alexaTarget.toString(),
|
||||
ignorePath: true,
|
||||
@@ -598,9 +716,13 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.proxy.web(req, res, { headers }, (err) => console.error(err));
|
||||
}
|
||||
|
||||
const wsHandler = (req: http.IncomingMessage, socket: Duplex, head: Buffer) => this.proxy.ws(req, socket, head, { target: wsTarget.toString(), ws: true, secure: false, headers }, (err) => console.error(err));
|
||||
const wsHandler = (req: http.IncomingMessage, socket: Duplex, head: Buffer) => {
|
||||
this.console.log(req.socket?.remoteAddress, req.url);
|
||||
this.proxy.ws(req, socket, head, { target: wsTarget.toString(), ws: true, secure: false, headers }, (err) => console.error(err))
|
||||
};
|
||||
|
||||
this.server = http.createServer(handler);
|
||||
this.server.keepAliveTimeout = 0;
|
||||
this.server.on('upgrade', wsHandler);
|
||||
// this can be localhost because this is a server initiated loopback proxy through bpmux
|
||||
this.server.listen(0, '127.0.0.1');
|
||||
@@ -620,7 +742,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.upnpInterval = setInterval(() => this.refreshPortForward(), 30 * 60 * 1000);
|
||||
this.refreshPortForward();
|
||||
|
||||
const agent = new http.Agent({ maxSockets: Number.MAX_VALUE, keepAlive: true });
|
||||
this.proxy = HttpProxy.createProxy({
|
||||
agent,
|
||||
target: httpTarget,
|
||||
secure: false,
|
||||
});
|
||||
@@ -628,7 +752,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.proxy.on('proxyRes', (res, req) => {
|
||||
res.headers['X-Scrypted-Cloud'] = req.headers['x-scrypted-cloud'];
|
||||
res.headers['X-Scrypted-Direct-Address'] = req.headers['x-scrypted-direct-address'];
|
||||
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address';
|
||||
res.headers['X-Scrypted-Cloud-Address'] = this.cloudflareTunnel;
|
||||
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address, X-Scrypted-Cloud-Address';
|
||||
});
|
||||
|
||||
let backoff = 0;
|
||||
@@ -653,14 +778,21 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
backoff = Date.now();
|
||||
const random = Math.random().toString(36).substring(2);
|
||||
this.console.log('scrypted server requested a connection:', random);
|
||||
|
||||
const registrationId = await this.manager.registrationId;
|
||||
this.ensureReverseConnections(registrationId);
|
||||
|
||||
const client = tls.connect(4001, SCRYPTED_SERVER, {
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
client.on('close', () => this.console.log('scrypted server connection ended:', random));
|
||||
const registrationId = await this.manager.registrationId;
|
||||
client.write(registrationId + '\n');
|
||||
const mux: any = new bpmux.BPMux(client as any);
|
||||
mux.on('handshake', async (socket: Duplex) => {
|
||||
this.ensureReverseConnections(registrationId);
|
||||
|
||||
this.console.warn('mux connection required');
|
||||
|
||||
let local: any;
|
||||
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
@@ -675,9 +807,166 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.startCloudflared();
|
||||
}
|
||||
|
||||
async startCloudflared() {
|
||||
while (true) {
|
||||
try {
|
||||
this.console.log('starting cloudflared');
|
||||
this.cloudflared = await backOff(async () => {
|
||||
const pluginVolume = process.env.SCRYPTED_PLUGIN_VOLUME;
|
||||
const version = 2;
|
||||
const cloudflareD = path.join(pluginVolume, 'cloudflare.d', `v${version}`, `${process.platform}-${process.arch}`);
|
||||
const bin = path.join(cloudflareD, cloudflared.bin);
|
||||
|
||||
if (!fs.existsSync(bin)) {
|
||||
for (let i = 0; i <= version; i++) {
|
||||
const cloudflareD = path.join(pluginVolume, 'cloudflare.d', `v${version}`);
|
||||
rmSync(cloudflareD, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
if (process.platform === 'darwin' && process.arch === 'arm64') {
|
||||
const bin = path.join(cloudflareD, cloudflared.bin);
|
||||
mkdirSync(path.dirname(bin), {
|
||||
recursive: true,
|
||||
});
|
||||
const tmp = `${bin}.tmp`;
|
||||
|
||||
const stream = await axios('https://github.com/scryptedapp/cloudflared/releases/download/2023.8.2/cloudflared-darwin-arm64', {
|
||||
responseType: 'stream',
|
||||
});
|
||||
const write = stream.data.pipe(fs.createWriteStream(tmp));
|
||||
await once(write, 'close');
|
||||
renameSync(tmp, bin);
|
||||
fs.chmodSync(bin, 0o0755)
|
||||
}
|
||||
else {
|
||||
await cloudflared.install(bin);
|
||||
}
|
||||
}
|
||||
process.chdir(cloudflareD);
|
||||
|
||||
const secureUrl = `https://127.0.0.1:${this.securePort}`;
|
||||
const args: any = {};
|
||||
if (this.storageSettings.values.cloudflaredTunnelToken) {
|
||||
args['run'] = null;
|
||||
args['--token'] = this.storageSettings.values.cloudflaredTunnelToken;
|
||||
}
|
||||
else {
|
||||
args['--no-tls-verify'] = null;
|
||||
args['--url'] = secureUrl;
|
||||
}
|
||||
|
||||
const deferred = new Deferred<string>();
|
||||
const cloudflareTunnel = cloudflared.tunnel(args);
|
||||
cloudflareTunnel.child.stdout.on('data', data => this.console.log(data.toString()));
|
||||
cloudflareTunnel.child.stderr.on('data', data => {
|
||||
const string: string = data.toString();
|
||||
this.console.error(string);
|
||||
|
||||
const lines = string.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.includes('hostname'))
|
||||
this.console.log(line);
|
||||
const match = /config=(".*?}")/gm.exec(line)
|
||||
if (match) {
|
||||
const json = match[1];
|
||||
this.console.log(json);
|
||||
try {
|
||||
// the config is already json stringified and needs to be double parsed.
|
||||
// '2023-09-02T21:18:10Z INF Updated to new configuration config="{\"ingress\":[{\"hostname\":\"tunneltest.example.com\", \"originRequest\":{\"noTLSVerify\":true}, \"service\":\"https://localhost:52960\"}, {\"service\":\"http_status:404\"}], \"warp-routing\":{\"enabled\":false}}" version=6'
|
||||
const parsed = JSON.parse(JSON.parse(json));
|
||||
const hostname = parsed.ingress?.[0]?.hostname;
|
||||
if (!hostname)
|
||||
deferred.resolve(undefined)
|
||||
else
|
||||
deferred.resolve(`https://${hostname}`)
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error("Error parsing config", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
cloudflareTunnel.child.on('exit', () => deferred.resolve(undefined));
|
||||
try {
|
||||
this.cloudflareTunnel = await Promise.any([deferred.promise, cloudflareTunnel.url]);
|
||||
if (!this.cloudflareTunnel)
|
||||
throw new Error('cloudflared exited, the provided cloudflare tunnel token may be invalid.')
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('cloudflared error', e);
|
||||
throw e;
|
||||
}
|
||||
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${secureUrl}`);
|
||||
return cloudflareTunnel;
|
||||
}, {
|
||||
startingDelay: 60000,
|
||||
timeMultiple: 1.2,
|
||||
numOfAttempts: 1000,
|
||||
maxDelay: 300000,
|
||||
});
|
||||
|
||||
await once(this.cloudflared.child, 'exit');
|
||||
throw new Error('cloudflared exited.');
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('cloudflared error', e);
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
this.cloudflared = undefined;
|
||||
this.cloudflareTunnel = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensureReverseConnections(registrationId: string) {
|
||||
while (this.reverseConnections.size < 10) {
|
||||
this.createReverseConnection(registrationId);
|
||||
}
|
||||
}
|
||||
|
||||
async createReverseConnection(registrationId: string) {
|
||||
const client = tls.connect(4001, SCRYPTED_SERVER, {
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
this.reverseConnections.add(client);
|
||||
const random = Math.random().toString(36).substring(2);
|
||||
let claimed = false;
|
||||
client.on('close', () => {
|
||||
this.console.log('scrypted server reverse connection ended:', random);
|
||||
this.reverseConnections.delete(client);
|
||||
|
||||
if (claimed)
|
||||
this.ensureReverseConnections(registrationId);
|
||||
});
|
||||
client.write(`reverse:${registrationId}\n`);
|
||||
|
||||
try {
|
||||
const read = await readLine(client);
|
||||
}
|
||||
catch (e) {
|
||||
return;
|
||||
}
|
||||
claimed = true;
|
||||
let local: any;
|
||||
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
const port = (this.server.address() as any).port;
|
||||
|
||||
local = net.connect({
|
||||
port,
|
||||
host: '127.0.0.1',
|
||||
});
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
||||
client.pipe(local).pipe(client);
|
||||
}
|
||||
|
||||
async oauthCallback(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
const reqUrl = new URL(req.url, 'https://localhost');
|
||||
|
||||
16
plugins/cloud/src/qs.ts
Normal file
16
plugins/cloud/src/qs.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export function qsstringify(dict: any) {
|
||||
const params = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(dict)) {
|
||||
params.set(k, v?.toString());
|
||||
}
|
||||
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
export function qsparse(search: URLSearchParams) {
|
||||
const ret: any = {};
|
||||
for (const [k, v] of search.entries()) {
|
||||
ret[k] = v;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
30
plugins/core/package-lock.json
generated
30
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.130",
|
||||
"version": "0.1.142",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.130",
|
||||
"version": "0.1.142",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
@@ -87,34 +87,35 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.21",
|
||||
"version": "0.2.103",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"typescript": "^4.9.3",
|
||||
"webpack": "^5.74.0",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
"scrypted-debug": "bin/scrypted-debug.js",
|
||||
"scrypted-deploy": "bin/scrypted-deploy.js",
|
||||
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
|
||||
"scrypted-package-json": "bin/scrypted-package-json.js",
|
||||
"scrypted-readme": "bin/scrypted-readme.js",
|
||||
"scrypted-setup-project": "bin/scrypted-setup-project.js",
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
@@ -249,12 +250,12 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@types/node": "^18.11.9",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"ncp": "^2.0.0",
|
||||
@@ -262,10 +263,11 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.3",
|
||||
"webpack": "^5.74.0",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.130",
|
||||
"version": "0.1.142",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
@@ -24,7 +24,7 @@
|
||||
],
|
||||
"scrypted": {
|
||||
"name": "Scrypted Core",
|
||||
"type": "DeviceProvider",
|
||||
"type": "Builtin",
|
||||
"interfaces": [
|
||||
"@scrypted/launcher-ignore",
|
||||
"HttpRequestHandler",
|
||||
@@ -34,6 +34,7 @@
|
||||
"Settings"
|
||||
],
|
||||
"pluginDependencies": [
|
||||
"@scrypted/snapshot",
|
||||
"@scrypted/webrtc"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -80,7 +80,8 @@ function createVideoCamera(devices: VideoCamera[], console: Console): VideoCamer
|
||||
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
ffmpegInput.inputArguments.push(...inputs[i].inputArguments);
|
||||
filter.push(`[${i}:v] scale=-1:${h},pad=${w}:ih:(ow-iw)/2 [pos${i}];`)
|
||||
// https://superuser.com/a/891478
|
||||
filter.push(`[${i}:v] scale=(iw*sar)*min(${w}/(iw*sar)\\,${h}/ih):ih*min(${w}/(iw*sar)\\,${h}/ih),pad=${w}:${h}:(${w}-iw*min(${w}/iw\\,${h}/ih))/2:(${h}-ih*min(${w}/iw\\,${h}/ih))/2 [pos${i}];`)
|
||||
}
|
||||
for (let i = inputs.length; i < dim * dim; i++) {
|
||||
ffmpegInput.inputArguments.push(
|
||||
|
||||
24959
plugins/core/ui/package-lock.json
generated
24959
plugins/core/ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,9 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "concurrently --names 'server,client' --prefix-colors 'gray,white.bold' --prefix '{time} ({name})\t' --timestamp-format 'HH:mm:ss.SSS' --kill-others npm:serve-server npm:serve",
|
||||
"serve": "vue-cli-service serve --open",
|
||||
"serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve --open",
|
||||
"serve-server": "cd ../../../server && npm run serve",
|
||||
"build": "vue-cli-service build --dest ../fs/dist",
|
||||
"build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build --dest ../fs/dist",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -26,6 +26,7 @@
|
||||
"draggabilly": "^2.3.0",
|
||||
"engine.io-client": "^5.2.0",
|
||||
"feather-icons": "^4.28.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash": "^4.17.21",
|
||||
"md5": "^2.3.0",
|
||||
"monaco-editor": "^0.27.0",
|
||||
@@ -49,7 +50,7 @@
|
||||
"vue-script2": "^2.1.0",
|
||||
"vue-slider-component": "^3.2.11",
|
||||
"vue-swatches": "^1.0.4",
|
||||
"vue2-google-maps": "^0.10.7",
|
||||
"vue2-leaflet": "^2.7.1",
|
||||
"vuetify": "^2.6.13",
|
||||
"vuex": "^3.6.2",
|
||||
"webpack-dev-server": "^4.9.2",
|
||||
@@ -58,6 +59,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
|
||||
"@babel/plugin-proposal-optional-catch-binding": "^7.18.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.13.8",
|
||||
"@babel/plugin-transform-typescript": "^7.13.0",
|
||||
|
||||
@@ -10,10 +10,18 @@
|
||||
<v-card width="300px" class="elevation-24">
|
||||
<v-card-title style="justify-content: center;" class="headline text-uppercase">Scrypted
|
||||
</v-card-title>
|
||||
<v-card-subtitle style="text-align: center;">{{ $store.state.version }}</v-card-subtitle>
|
||||
<v-card-subtitle v-if="$store.state.loginHostname"
|
||||
style="text-align: center; font-weight: 300; font-size: .75rem !important; font-family: Quicksand, sans-serif!important;"
|
||||
class="text-subtitle-2 text-uppercase">
|
||||
{{ $store.state.version }}
|
||||
<br />
|
||||
Logged into: {{ $store.state.loginHostname
|
||||
}}
|
||||
</v-card-subtitle>
|
||||
<v-card-subtitle v-else style="text-align: center;">{{ $store.state.version }}</v-card-subtitle>
|
||||
<v-list class="transparent">
|
||||
<v-list-item v-for="application in applications" :key="application.name"
|
||||
:to="application.to" :href="application.href">
|
||||
<v-list-item v-for="application in applications" :key="application.name" :to="application.to"
|
||||
:href="application.href">
|
||||
<v-icon small>{{ application.icon }}</v-icon>
|
||||
<v-list-item-title style="text-align: center;">{{ application.name }}
|
||||
</v-list-item-title>
|
||||
@@ -27,11 +35,11 @@
|
||||
<v-card-actions>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn v-on="on" icon href="https://twitter.com/scryptedapp/">
|
||||
<v-icon small>fab fa-twitter</v-icon>
|
||||
<v-btn v-on="on" icon href="https://discord.gg/DcFzmBHYGq">
|
||||
<v-icon small>fab fa-discord</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Twitter</span>
|
||||
<span>Discord</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
@@ -43,19 +51,11 @@
|
||||
</v-tooltip>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn v-on="on" icon href="https://github.com/koush/scrypted">
|
||||
<v-icon small>fab fa-github</v-icon>
|
||||
<v-btn v-on="on" icon href="https://docs.scrypted.app">
|
||||
<v-icon small>fa fa-file-text</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Github</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn v-on="on" icon href="https://discord.gg/DcFzmBHYGq">
|
||||
<v-icon small>fab fa-discord</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Discord</span>
|
||||
<span>Documentation</span>
|
||||
</v-tooltip>
|
||||
<v-spacer></v-spacer>
|
||||
<v-tooltip bottom>
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
</v-card-title>
|
||||
<v-card-subtitle v-if="$store.state.hasLogin === false" style="display: flex; justify-content: center;" class="text-uppercase">Create Account
|
||||
</v-card-subtitle>
|
||||
<v-card-subtitle v-if="$store.state.loginHostname"
|
||||
style="text-align: center; font-weight: 300; font-size: .75rem !important; font-family: Quicksand, sans-serif!important;"
|
||||
class="text-subtitle-2 text-uppercase">Log into: {{ $store.state.loginHostname }}</v-card-subtitle>
|
||||
<v-container grid-list-md>
|
||||
<v-layout wrap>
|
||||
<v-flex xs12>
|
||||
|
||||
@@ -6,14 +6,6 @@
|
||||
</v-card-title>
|
||||
<v-card-text>Connection interrupted.</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn v-on="on" icon href="https://github.com/koush/scrypted">
|
||||
<v-icon small>fab fa-github</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Github</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn v-on="on" icon href="https://discord.gg/DcFzmBHYGq">
|
||||
@@ -22,6 +14,22 @@
|
||||
</template>
|
||||
<span>Discord</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn v-on="on" icon href="https://www.reddit.com/r/Scrypted/">
|
||||
<v-icon small>fab fa-reddit</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Reddit</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn v-on="on" icon href="https://docs.scrypted.app">
|
||||
<v-icon small>fa fa-file-text</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Documentation</span>
|
||||
</v-tooltip>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="reconnect">Reconnect</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -37,6 +37,7 @@ Vue.use(Vue => {
|
||||
baseUrl: getCurrentBaseUrl(),
|
||||
});
|
||||
|
||||
store.commit("setLoginHostname", undefined);
|
||||
store.commit("setHasLogin", undefined);
|
||||
store.commit("setIsLoggedIn", undefined);
|
||||
store.commit("setUsername", undefined);
|
||||
@@ -53,6 +54,7 @@ Vue.use(Vue => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
store.commit("setLoginHostname", response.hostname);
|
||||
if (!response.expiration) {
|
||||
store.commit("setHasLogin", response.hasLogin);
|
||||
throw new Error("Login failed.");
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<div dense nav v-for="category in categories" :key="category">
|
||||
<v-divider></v-divider>
|
||||
<template v-for="category in categories" >
|
||||
<v-subheader>{{ category }}</v-subheader>
|
||||
|
||||
<v-list-item v-for="item in filterComponents(category)" :key="item.id" link :to="getComponentViewPath(item.id)"
|
||||
@@ -39,7 +40,7 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
</div>
|
||||
</template>
|
||||
<v-subheader>Social</v-subheader>
|
||||
<v-list-item link href="https://discord.gg/DcFzmBHYGq" active-class="purple white--text tile">
|
||||
<v-list-item-icon>
|
||||
@@ -72,6 +73,16 @@
|
||||
</v-list-item>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-subheader>Other</v-subheader>
|
||||
<v-list-item link href="https://docs.scrypted.app" active-class="purple white--text tile">
|
||||
<v-list-item-icon>
|
||||
<v-icon small>fa fa-file-text</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item active-class="deep-purple accent-4 white--text">
|
||||
<v-list-item-icon>
|
||||
<v-icon small>fa-code-branch</v-icon>
|
||||
@@ -87,7 +98,7 @@
|
||||
|
||||
<script>
|
||||
import { getComponentViewPath } from "./helpers";
|
||||
import { checkUpdate } from "./plugin/plugin";
|
||||
import { checkServerUpdate } from "./plugin/plugin";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -160,11 +171,9 @@ export default {
|
||||
// in which case fall back and determine what the install type is.
|
||||
const info = await this.$scrypted.systemManager.getComponent("info");
|
||||
const version = await info.getVersion();
|
||||
const scryptedEnv = await info.getScryptedEnv();
|
||||
this.currentVersion = version;
|
||||
const { updateAvailable } = await checkUpdate(
|
||||
"@scrypted/server",
|
||||
version
|
||||
);
|
||||
const { updateAvailable } = await checkServerUpdate(version, scryptedEnv.SCRYPTED_INSTALL_ENVIRONMENT);
|
||||
this.updateAvailable = updateAvailable;
|
||||
}
|
||||
|
||||
@@ -206,6 +215,4 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
@@ -101,7 +101,7 @@
|
||||
</v-layout>
|
||||
</template>
|
||||
<script>
|
||||
import { checkUpdate } from "../plugin/plugin";
|
||||
import { checkServerUpdate } from "../plugin/plugin";
|
||||
import Settings from "../../interfaces/Settings.vue"
|
||||
import {createSystemSettingsDevice} from './system-settings';
|
||||
|
||||
@@ -140,10 +140,8 @@ export default {
|
||||
catch (e) {
|
||||
// old scrypted servers dont support this call, or it may be unimplemented
|
||||
// in which case fall back and determine what the install type is.
|
||||
const { updateAvailable } = await checkUpdate(
|
||||
"@scrypted/server",
|
||||
version
|
||||
);
|
||||
const scryptedEnv = await info.getScryptedEnv();
|
||||
const { updateAvailable } = await checkServerUpdate(version, scryptedEnv.SCRYPTED_INSTALL_ENVIRONMENT);
|
||||
this.updateAvailable = updateAvailable;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ const pluginSnapshot = require("!!raw-loader!./plugin-snapshot.ts").default.spli
|
||||
|
||||
export interface PluginUpdateCheck {
|
||||
updateAvailable?: string;
|
||||
updatePublished?: Date;
|
||||
versions: any;
|
||||
}
|
||||
|
||||
@@ -29,11 +30,17 @@ export async function checkUpdate(npmPackage: string, npmPackageVersion: string)
|
||||
const { data } = response;
|
||||
const versions = Object.values(data.versions).sort((a: any, b: any) => semver.compare(a.version, b.version)).reverse();
|
||||
let updateAvailable: any;
|
||||
let updatePublished: any;
|
||||
let latest: any;
|
||||
if (data["dist-tags"]) {
|
||||
latest = data["dist-tags"].latest;
|
||||
if (npmPackageVersion && semver.gt(latest, npmPackageVersion)) {
|
||||
updateAvailable = latest;
|
||||
try {
|
||||
updatePublished = new Date(data["time"][latest]);
|
||||
} catch {
|
||||
updatePublished = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [k, v] of Object.entries(data['dist-tags'])) {
|
||||
@@ -54,10 +61,36 @@ export async function checkUpdate(npmPackage: string, npmPackageVersion: string)
|
||||
}
|
||||
return {
|
||||
updateAvailable,
|
||||
updatePublished,
|
||||
versions,
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkServerUpdate(version: string, installEnvironment: string): Promise<PluginUpdateCheck> {
|
||||
const { updateAvailable, updatePublished, versions } = await checkUpdate(
|
||||
"@scrypted/server",
|
||||
version
|
||||
);
|
||||
|
||||
if (installEnvironment == "docker" && updatePublished) {
|
||||
console.log(`New scrypted server version published ${updatePublished}`);
|
||||
|
||||
// check if there is a new docker image available, using 'latest' tag
|
||||
// this is done so newer server versions in npm are not immediately
|
||||
// displayed until a docker image has been published
|
||||
let response: AxiosResponse<any> = await axios.get("https://corsproxy.io?https://hub.docker.com/v2/namespaces/koush/repositories/scrypted/tags/latest");
|
||||
const { data } = response;
|
||||
const imagePublished = new Date(data.last_updated);
|
||||
console.log(`Latest docker image published ${imagePublished}`);
|
||||
|
||||
if (imagePublished < updatePublished) {
|
||||
// docker image is not yet published
|
||||
return { updateAvailable: null, updatePublished: null, versions: null }
|
||||
}
|
||||
}
|
||||
return { updateAvailable, updatePublished, versions };
|
||||
}
|
||||
|
||||
export async function installNpm(systemManager: SystemManager, npmPackage: string, version?: string): Promise<string> {
|
||||
const plugins = await systemManager.getComponent('plugins');
|
||||
await plugins.installNpm(npmPackage, version);
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
-webkit-transform-style: preserve-3d;
|
||||
" playsinline autoplay></video>
|
||||
|
||||
<svg :viewBox="`0 0 ${svgWidth} ${svgHeight}`" ref="svg" style="
|
||||
<svg width="100%" height="100%" preserveAspectRatio="none" ref="svg" style="
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
@@ -141,15 +141,23 @@ export default {
|
||||
|
||||
let contents = "";
|
||||
|
||||
const toPercent= (v, d) => {
|
||||
return `${v / d * 100}%`;
|
||||
}
|
||||
|
||||
for (const detection of this.lastDetection.detections || []) {
|
||||
if (!detection.boundingBox) continue;
|
||||
const svgScale = this.svgWidth / 1080;
|
||||
const sw = 2 * svgScale;
|
||||
const s = "red";
|
||||
const x = detection.boundingBox[0];
|
||||
const y = detection.boundingBox[1];
|
||||
const w = detection.boundingBox[2];
|
||||
const h = detection.boundingBox[3];
|
||||
let x = detection.boundingBox[0];
|
||||
let y = detection.boundingBox[1];
|
||||
let w = detection.boundingBox[2];
|
||||
let h = detection.boundingBox[3];
|
||||
|
||||
x = toPercent(x, this.lastDetection?.inputDimensions?.[0] || 1920);
|
||||
y = toPercent(y, this.lastDetection?.inputDimensions?.[1] || 1080);
|
||||
w = toPercent(w, this.lastDetection?.inputDimensions?.[0] || 1920);
|
||||
h = toPercent(h, this.lastDetection?.inputDimensions?.[1] || 1080);
|
||||
|
||||
let t = ``;
|
||||
let toffset = 0;
|
||||
if (detection.score && detection.className !== 'motion') {
|
||||
@@ -159,11 +167,11 @@ export default {
|
||||
const tname = detection.className + (detection.id ? `: ${detection.id}` : '')
|
||||
t += `<tspan x='${x}' dy='${toffset}em'>${tname}</tspan>`
|
||||
|
||||
const fs = 20 * svgScale;
|
||||
const fs = 20;
|
||||
|
||||
const box = `<rect x="${x}" y="${y}" width="${w}" height="${h}" stroke="${s}" stroke-width="${sw}" fill="none" />
|
||||
<text x="${x}" y="${y - 5}" font-size="${fs}" dx="0.05em" dy="0.05em" fill="black">${t}</text>
|
||||
<text x="${x}" y="${y - 5}" font-size="${fs}" fill="white">${t}</text>
|
||||
const box = `<rect x="${x}" y="${y}" width="${w}" height="${h}" stroke="${s}" stroke-width="2" fill="none" />
|
||||
<text x="${x}" y="${y}" font-size="${fs}" dx="0.05em" dy="0.05em" fill="black">${t}</text>
|
||||
<text x="${x}" y="${y}" font-size="${fs}" fill="white">${t}</text>
|
||||
`;
|
||||
contents += box;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,37 @@
|
||||
<template>
|
||||
<v-btn text color="primary" @click="onClick">Login</v-btn>
|
||||
<div>
|
||||
<v-dialog v-model="loginDialog" max-width="300px">
|
||||
<v-card>
|
||||
<v-card-title>Login Required</v-card-title>
|
||||
<v-card-text>Scrypted Management Console is currently inside a browser iframe. For web security, a new tab will be
|
||||
opened, and the
|
||||
browser may prompt to log into this server again.
|
||||
<br />
|
||||
<br />
|
||||
<b>Home Assistant Addon installations must create a new Administrator user</b> within the Scrypted Users sidebar menu to log in from outside of Home Assistant.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer>
|
||||
</v-spacer>
|
||||
<v-btn icon @click="loginDialog = false">Cancel</v-btn>
|
||||
<v-btn icon @click="onClickContinue">OK</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-btn text color="primary" @click="onClick">Login</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import qs from 'query-string';
|
||||
import RPCInterface from "./RPCInterface.vue";
|
||||
import { getCurrentBaseUrl } from '../../../../../packages/client/src';
|
||||
|
||||
export default {
|
||||
mixins: [RPCInterface],
|
||||
data() {
|
||||
return {
|
||||
loginDialog: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onChange() { },
|
||||
isIFrame() {
|
||||
@@ -17,15 +41,18 @@ export default {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
onClickContinue: async function () {
|
||||
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');
|
||||
},
|
||||
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');
|
||||
this.loginDialog = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,55 @@
|
||||
<template>
|
||||
<GmapMap
|
||||
<l-map
|
||||
:center="center"
|
||||
:zoom="zoom"
|
||||
ref="mapRef"
|
||||
style="height: 400px"
|
||||
style="height: 400px;"
|
||||
:options="{
|
||||
mapTypeControl: false,
|
||||
fullscreenControl: false,
|
||||
}"
|
||||
zoomControl: false,
|
||||
attributionControl: false,
|
||||
dragging: false,
|
||||
doubleClickZoom: false,
|
||||
boxZoom: false,
|
||||
scrollWheelZoom: false,
|
||||
touchZoom: false,
|
||||
}"
|
||||
>
|
||||
<GmapMarker v-if="position" :position="position" :label="lazyValue.name" />
|
||||
</GmapMap>
|
||||
<l-tile-layer :url="url" :attribution="attribution"></l-tile-layer>
|
||||
<l-marker :lat-lng="position"></l-marker>
|
||||
<l-control-attribution position="bottomright" :prefix="prefix"></l-control-attribution>
|
||||
</l-map>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { latLng, Icon } from "leaflet";
|
||||
import { LMap, LTileLayer, LMarker, LControlAttribution } from "vue2-leaflet";
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import RPCInterface from "../RPCInterface.vue";
|
||||
|
||||
// https://vue2-leaflet.netlify.app/quickstart/#marker-icons-are-missing
|
||||
delete Icon.Default.prototype._getIconUrl;
|
||||
Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
|
||||
iconUrl: require('leaflet/dist/images/marker-icon.png'),
|
||||
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
|
||||
});
|
||||
|
||||
export default {
|
||||
mixins: [RPCInterface],
|
||||
components: {
|
||||
LMap,
|
||||
LTileLayer,
|
||||
LMarker,
|
||||
LControlAttribution,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
prefix: '<a target="blank" href="https://leafletjs.com/">Leaflet</a>',
|
||||
attribution: '© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
center() {
|
||||
if (!this.position) {
|
||||
return {
|
||||
lat: 0,
|
||||
lng: 0
|
||||
};
|
||||
}
|
||||
return this.position;
|
||||
},
|
||||
zoom() {
|
||||
@@ -33,12 +57,9 @@ export default {
|
||||
},
|
||||
position() {
|
||||
if (!this.lazyValue.position) {
|
||||
return;
|
||||
return latLng(0, 0);
|
||||
}
|
||||
return {
|
||||
lat: this.lazyValue.position.latitude,
|
||||
lng: this.lazyValue.position.longitude
|
||||
};
|
||||
return latLng(this.lazyValue.position.latitude, this.lazyValue.position.longitude);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import './plugins/icons';
|
||||
import vuetify from './plugins/vuetify';
|
||||
import './plugins/script2';
|
||||
import './plugins/clipboard';
|
||||
import './plugins/maps';
|
||||
import './plugins/async-computed';
|
||||
import './plugins/apexcharts';
|
||||
import './plugins/is-mobile';
|
||||
|
||||
@@ -87,6 +87,7 @@ import {
|
||||
faTimeline,
|
||||
faMobile,
|
||||
faBoltLightning,
|
||||
faFileText,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import {
|
||||
@@ -180,6 +181,7 @@ const icons: IconDefinition[] =[
|
||||
faTimeline,
|
||||
faMobile,
|
||||
faBoltLightning,
|
||||
faFileText,
|
||||
];
|
||||
|
||||
for (var icon in icons) {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
import Vue from 'vue';
|
||||
import * as VueGoogleMaps from 'vue2-google-maps';
|
||||
|
||||
Vue.use(VueGoogleMaps, {
|
||||
load: {
|
||||
key: 'AIzaSyCBbKhH_IM1oIZMOO65xOnzgDDrmC2lAoc',
|
||||
libraries: 'places', // This is required if you use the Autocomplete plugin
|
||||
// OR: libraries: 'places,drawing'
|
||||
// OR: libraries: 'places,drawing,visualization'
|
||||
// (as you require)
|
||||
|
||||
//// If you want to set the version, you can do so:
|
||||
// v: '3.26',
|
||||
},
|
||||
});
|
||||
@@ -16,7 +16,8 @@ const store = new Vuex.Store({
|
||||
isLoggedIn: undefined,
|
||||
isLoggedIntoCloud: undefined,
|
||||
isConnected: undefined,
|
||||
hasLogin: undefined
|
||||
hasLogin: undefined,
|
||||
loginHostname: undefined,
|
||||
},
|
||||
mutations: {
|
||||
setSystemState: function (store, systemState) {
|
||||
@@ -65,6 +66,9 @@ const store = new Vuex.Store({
|
||||
setHasLogin(store, hasLogin) {
|
||||
store.hasLogin = hasLogin;
|
||||
},
|
||||
setLoginHostname(store, hostname) {
|
||||
store.loginHostname = hostname;
|
||||
},
|
||||
setVersion(store, version) {
|
||||
store.version = version;
|
||||
},
|
||||
|
||||
4
plugins/coreml/package-lock.json
generated
4
plugins/coreml/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.21",
|
||||
"version": "0.1.28",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.21",
|
||||
"version": "0.1.28",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -34,11 +34,12 @@
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"Settings",
|
||||
"ObjectDetection"
|
||||
"ObjectDetection",
|
||||
"ObjectDetectionPreview"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.21"
|
||||
"version": "0.1.28"
|
||||
}
|
||||
|
||||
@@ -3,12 +3,10 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
from typing import Any, Tuple
|
||||
|
||||
import coremltools as ct
|
||||
import numpy as np
|
||||
import scrypted_sdk
|
||||
from PIL import Image
|
||||
from scrypted_sdk import Setting, SettingValue
|
||||
@@ -37,11 +35,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
if model == "Default":
|
||||
# model = "ssdlite_mobilenet_v2"
|
||||
if "arm" in platform.processor():
|
||||
model = "yolov8n"
|
||||
else:
|
||||
model = "ssdlite_mobilenet_v2"
|
||||
model = "yolov8n_320"
|
||||
self.yolo = "yolo" in model
|
||||
self.yolov8 = "yolov8" in model
|
||||
model_version = "v2"
|
||||
@@ -111,6 +105,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
"ssdlite_mobilenet_v2",
|
||||
"yolov4-tiny",
|
||||
"yolov8n",
|
||||
"yolov8n_320",
|
||||
],
|
||||
"value": model,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
coremltools
|
||||
coremltools==7.0b2
|
||||
|
||||
# pillow for anything not intel linux, pillow-simd is available on x64 linux
|
||||
Pillow>=5.4.1; sys_platform != 'linux' or platform_machine != 'x86_64'
|
||||
|
||||
7
plugins/doorbird/package-lock.json
generated
7
plugins/doorbird/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"doorbird": "^2.1.2"
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.97",
|
||||
"version": "0.2.103",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -45,7 +45,6 @@
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.15.9",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -29,7 +29,6 @@
|
||||
],
|
||||
"pluginDependencies": [
|
||||
"@scrypted/prebuffer-mixin",
|
||||
"@scrypted/pam-diff",
|
||||
"@scrypted/snapshot"
|
||||
]
|
||||
},
|
||||
|
||||
6
plugins/ffmpeg-camera/package-lock.json
generated
6
plugins/ffmpeg-camera/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/ffmpeg-camera",
|
||||
"version": "0.0.21",
|
||||
"version": "0.0.22",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/ffmpeg-camera",
|
||||
"version": "0.0.21",
|
||||
"version": "0.0.22",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.86",
|
||||
"version": "0.2.103",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/ffmpeg-camera",
|
||||
"version": "0.0.21",
|
||||
"version": "0.0.22",
|
||||
"description": "FFmpeg Camera Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -32,7 +32,6 @@
|
||||
],
|
||||
"pluginDependencies": [
|
||||
"@scrypted/prebuffer-mixin",
|
||||
"@scrypted/pam-diff",
|
||||
"@scrypted/snapshot"
|
||||
]
|
||||
},
|
||||
|
||||
4
plugins/hikvision/package-lock.json
generated
4
plugins/hikvision/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.128",
|
||||
"version": "0.0.129",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.128",
|
||||
"version": "0.0.129",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.128",
|
||||
"version": "0.0.129",
|
||||
"description": "Hikvision Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -570,6 +570,7 @@ class HikvisionProvider extends RtspProvider {
|
||||
device.setHttpPortOverride(settings.httpPort?.toString());
|
||||
if (twoWayAudio)
|
||||
device.putSetting('twoWayAudio', twoWayAudio);
|
||||
device.updateDeviceInfo();
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user