mirror of
https://github.com/koush/scrypted.git
synced 2026-02-07 16:02:13 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7614d12363 | ||
|
|
3189317b2d | ||
|
|
410d11248f | ||
|
|
28e1f5ac8a | ||
|
|
bafe73d296 | ||
|
|
f17ce50f17 | ||
|
|
18f5872be1 | ||
|
|
fdccaaa65e | ||
|
|
6a55172924 | ||
|
|
1e41af77fa | ||
|
|
e169a6e02d | ||
|
|
ef55c834af | ||
|
|
3812ad92ac | ||
|
|
0bdb402e7b | ||
|
|
1588ea250b | ||
|
|
47f603877b | ||
|
|
9adc803206 | ||
|
|
031e290017 | ||
|
|
a9ab2d0110 | ||
|
|
9592b6087b | ||
|
|
5c34213b5d | ||
|
|
4435fe1e0a |
78
packages/client/package-lock.json
generated
78
packages/client/package-lock.json
generated
@@ -1,24 +1,20 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.37",
|
||||
"version": "1.1.38",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.37",
|
||||
"version": "1.1.38",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.64",
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.2.2",
|
||||
"linkfs": "^2.1.0",
|
||||
"memfs": "^3.4.1",
|
||||
"rimraf": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/node": "^17.0.17",
|
||||
"typescript": "^4.7.4"
|
||||
@@ -63,15 +59,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"node_modules/@types/adm-zip": {
|
||||
"version": "0.4.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz",
|
||||
"integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ip": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
|
||||
@@ -87,14 +74,6 @@
|
||||
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
|
||||
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
|
||||
@@ -177,11 +156,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fs-monkey": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
|
||||
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -220,22 +194,6 @@
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/linkfs": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
|
||||
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
|
||||
},
|
||||
"node_modules/memfs": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
|
||||
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
|
||||
"dependencies": {
|
||||
"fs-monkey": "1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -340,15 +298,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"@types/adm-zip": {
|
||||
"version": "0.4.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz",
|
||||
"integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/ip": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
|
||||
@@ -364,11 +313,6 @@
|
||||
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
|
||||
"dev": true
|
||||
},
|
||||
"adm-zip": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
|
||||
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
|
||||
@@ -426,11 +370,6 @@
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
|
||||
},
|
||||
"fs-monkey": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
|
||||
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -463,19 +402,6 @@
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"linkfs": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
|
||||
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
|
||||
},
|
||||
"memfs": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
|
||||
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
|
||||
"requires": {
|
||||
"fs-monkey": "1.0.3"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.37",
|
||||
"version": "1.1.38",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -12,18 +12,14 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/node": "^17.0.17",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.64",
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.2.2",
|
||||
"linkfs": "^2.1.0",
|
||||
"memfs": "^3.4.1",
|
||||
"rimraf": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
4
packages/rpc/package-lock.json
generated
4
packages/rpc/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.0.20",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -35,7 +35,7 @@
|
||||
"dependencies": {
|
||||
"@types/node": "^16.6.1",
|
||||
"alexa-smarthome-ts": "^0.0.1",
|
||||
"axios": "^0.24.0",
|
||||
"axios": "^1.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface } from '@scrypted/sdk';
|
||||
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue, Settings } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
|
||||
import { isSupported } from './types';
|
||||
@@ -12,16 +12,22 @@ const { systemManager, deviceManager } = sdk;
|
||||
const client_id = "amzn1.application-oa2-client.3283807e04d8408eb44a698c10f9dd13";
|
||||
const client_secret = "bed445e2b26730acd818b90e175b275f6b67b18ff8645e571c5b3e311fa75ee9";
|
||||
|
||||
class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler, MixinProvider {
|
||||
class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler, MixinProvider, Settings {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
tokenInfo: {
|
||||
hide: true,
|
||||
json: true,
|
||||
json: true
|
||||
},
|
||||
syncedDevices: {
|
||||
multiple: true,
|
||||
hide: true,
|
||||
hide: true
|
||||
},
|
||||
apiEndpoint: {
|
||||
title: 'Alexa Endpoint',
|
||||
description: 'This is the endpoint Alexa will use to send events to. This is set after you login.',
|
||||
type: 'string',
|
||||
readonly: true
|
||||
}
|
||||
});
|
||||
|
||||
handlers = new Map<string, AlexaHandler>();
|
||||
@@ -85,6 +91,13 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
});
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
|
||||
return mixinDevice;
|
||||
}
|
||||
@@ -99,11 +112,41 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
deviceManager.requestRestart();
|
||||
}
|
||||
|
||||
readonly endpoints: string[] = [
|
||||
'api.amazonalexa.com',
|
||||
'api.eu.amazonalexa.com',
|
||||
'api.fe.amazonalexa.com'
|
||||
];
|
||||
|
||||
async getAlexaEndpoint() : Promise<string> {
|
||||
if (this.storageSettings.values.apiEndpoint)
|
||||
return this.storageSettings.values.apiEndpoint;
|
||||
|
||||
try {
|
||||
const accessToken = await this.getAccessToken();
|
||||
const response = await axios.get(`https://${this.endpoints[0]}/v1/alexaApiEndpoint`, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken,
|
||||
}
|
||||
});
|
||||
|
||||
const endpoint: string = response.data.endpoints[0];
|
||||
this.storageSettings.values.apiEndpoint = endpoint;
|
||||
return endpoint;
|
||||
} catch (err) {
|
||||
this.console.error(err);
|
||||
|
||||
// default to NA/RoW endpoint if we can't get the endpoint.
|
||||
return this.endpoints[0];
|
||||
}
|
||||
}
|
||||
|
||||
async postEvent(data: any) {
|
||||
const accessToken = await this.getAccessToken();
|
||||
const endpoint = await this.getAlexaEndpoint();
|
||||
const self = this;
|
||||
|
||||
return axios.post('https://api.amazonalexa.com/v3/events', data, {
|
||||
return axios.post(`https://${endpoint}/v3/events`, data, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken,
|
||||
}
|
||||
@@ -267,6 +310,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
const json = JSON.parse(request.body);
|
||||
const { grant } = json.directive.payload;
|
||||
this.storageSettings.values.tokenInfo = grant;
|
||||
this.storageSettings.values.apiEndpoint = undefined;
|
||||
this.accessToken = undefined;
|
||||
|
||||
const self = this;
|
||||
@@ -274,6 +318,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
self.console.error(`Failed to handle the AcceptGrant directive because ${reason}`);
|
||||
|
||||
this.storageSettings.values.tokenInfo = undefined;
|
||||
this.storageSettings.values.apiEndpoint = undefined;
|
||||
this.accessToken = undefined;
|
||||
|
||||
response.send(JSON.stringify({
|
||||
@@ -311,6 +356,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
this.console.error(`AcceptGrant.Response failed because ${error}`);
|
||||
|
||||
this.storageSettings.values.tokenInfo = undefined;
|
||||
this.storageSettings.values.apiEndpoint = undefined;
|
||||
this.accessToken = undefined;
|
||||
throw error;
|
||||
}
|
||||
@@ -455,7 +501,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
catch (e) {
|
||||
this.console.error(`request failed due to invalid authorization`, e);
|
||||
response.send(e.message, {
|
||||
code: 500,
|
||||
code: 500
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
// this could be a low resolution screen, no way of knowing, so never send a
|
||||
// 1080p+ stream.
|
||||
screen: {
|
||||
devicePixelRatio: 1, // TODO: get this from the device
|
||||
width: 1280,
|
||||
height: 720,
|
||||
}
|
||||
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.91",
|
||||
"version": "0.1.94",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.91",
|
||||
"version": "0.1.94",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.91",
|
||||
"version": "0.1.94",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -2,14 +2,23 @@
|
||||
<v-btn text color="primary" @click="onClick">Login</v-btn>
|
||||
</template>
|
||||
<script>
|
||||
import RPCInterface from "./RPCInterface.vue";
|
||||
import qs from 'query-string';
|
||||
import RPCInterface from "./RPCInterface.vue";
|
||||
|
||||
export default {
|
||||
mixins: [RPCInterface],
|
||||
methods: {
|
||||
onChange() { },
|
||||
isIFrame() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
onClick: function () {
|
||||
// https://stackoverflow.com/a/39387533
|
||||
const windowReference = this.isIFrame() ? window.open(undefined, '_blank') : undefined;
|
||||
this.rpc()
|
||||
.getOauthUrl()
|
||||
.then(data => {
|
||||
@@ -22,7 +31,9 @@ export default {
|
||||
u = new URL(redirect_uri);
|
||||
}
|
||||
catch (e) {
|
||||
u = new URL(redirect_uri, window.location.href);
|
||||
const baseURI = new URL(document.baseURI);
|
||||
const scryptedRootURI = new URL('../../../../', baseURI);
|
||||
u = new URL('.' + redirect_uri, scryptedRootURI);
|
||||
u.hostname = 'localhost';
|
||||
}
|
||||
if (u.hostname === 'localhost') {
|
||||
@@ -40,7 +51,10 @@ export default {
|
||||
r: window.location.toString(),
|
||||
});
|
||||
url.search = qs.stringify(querystring);
|
||||
window.location = url.toString();
|
||||
if (windowReference)
|
||||
windowReference.location = url.toString();
|
||||
else
|
||||
window.location = url.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../common/fs/unavailable.jpg
|
||||
@@ -1,14 +1,13 @@
|
||||
import sdk, { DeviceManifest, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, HumiditySensor, MediaObject, MotionSensor, OauthClient, Refresh, ScryptedDeviceType, ScryptedInterface, Setting, Settings, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, VideoCamera, MediaStreamOptions, BinarySensor, DeviceInformation, RTCAVSignalingSetup, Camera, PictureOptions, ObjectsDetected, ObjectDetector, ObjectDetectionTypes, FFmpegInput, RequestMediaStreamOptions, Readme, RTCSignalingChannel, RTCSessionControl, RTCSignalingSession, ResponseMediaStreamOptions, RTCSignalingOptions, RTCSignalingSendIceCandidate, ScryptedMimeTypes, MediaStreamUrl } from '@scrypted/sdk';
|
||||
import { ScryptedDeviceBase } from '@scrypted/sdk';
|
||||
import qs from 'query-string';
|
||||
import ClientOAuth2 from 'client-oauth2';
|
||||
import { URL } from 'url';
|
||||
import axios from 'axios';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { connectRTCSignalingClients } from '@scrypted/common/src/rtc-signaling';
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import fs from 'fs';
|
||||
import sdk, { BinarySensor, Camera, DeviceInformation, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, HumiditySensor, MediaObject, MediaStreamUrl, MotionSensor, OauthClient, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PictureOptions, Readme, Refresh, RequestMediaStreamOptions, ResponseMediaStreamOptions, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, TemperatureCommand, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, VideoCamera } from '@scrypted/sdk';
|
||||
import axios from 'axios';
|
||||
import ClientOAuth2 from 'client-oauth2';
|
||||
import { randomBytes } from 'crypto';
|
||||
import fs from 'fs';
|
||||
import throttle from 'lodash/throttle';
|
||||
import qs from 'query-string';
|
||||
import { URL } from 'url';
|
||||
|
||||
const { deviceManager, mediaManager, endpointManager, systemManager } = sdk;
|
||||
|
||||
@@ -101,6 +100,10 @@ class NestRTCSessionControl implements RTCSessionControl {
|
||||
constructor(public camera: NestCamera, public options: { streamExtensionToken: string, mediaSessionId: string }) {
|
||||
}
|
||||
|
||||
async setPlayback(options: { audio: boolean; video: boolean; }): Promise<void> {
|
||||
|
||||
}
|
||||
|
||||
async getRefreshAt(): Promise<number> {
|
||||
return this.refreshAt;
|
||||
}
|
||||
@@ -372,6 +375,52 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
|
||||
this.reload();
|
||||
}
|
||||
|
||||
async setTemperature(command: TemperatureCommand): Promise<void> {
|
||||
// set this in case round trip is slow.
|
||||
let { mode, setpoint } = command;
|
||||
if (mode) {
|
||||
const nestMode = toNestMode(mode);
|
||||
this.device.traits['sdm.devices.traits.ThermostatMode'].mode = nestMode;
|
||||
|
||||
this.executeCommandSetMode = {
|
||||
command: 'sdm.devices.commands.ThermostatMode.SetMode',
|
||||
params: {
|
||||
mode: nestMode,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (command.setpoint) {
|
||||
mode ||= fromNestMode(this.device.traits['sdm.devices.traits.ThermostatMode'].mode);
|
||||
|
||||
this.executeCommandSetCelsius = {
|
||||
command: setpointReverseMap.get(mode),
|
||||
params: {
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof command.setpoint === 'number') {
|
||||
if (mode === ThermostatMode.Heat) {
|
||||
this.executeCommandSetCelsius.params.heatCelsius = command.setpoint;
|
||||
|
||||
}
|
||||
else if (mode === ThermostatMode.Cool) {
|
||||
this.executeCommandSetCelsius.params.coolCelsius = command.setpoint;
|
||||
}
|
||||
else {
|
||||
this.executeCommandSetCelsius.params.coolCelsius = command.setpoint;
|
||||
this.executeCommandSetCelsius.params.heatCelsius = command.setpoint;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.executeCommandSetCelsius.params.heatCelsius = command[0];
|
||||
this.executeCommandSetCelsius.params.coolCelsius = command[1];
|
||||
}
|
||||
}
|
||||
await this.executeThrottle();
|
||||
await this.refresh(null, true);
|
||||
}
|
||||
|
||||
async setTemperatureUnit(temperatureUnit: TemperatureUnit): Promise<void> {
|
||||
// not supported by API. throw?
|
||||
}
|
||||
@@ -398,26 +447,37 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
|
||||
const heat = device.traits?.['sdm.devices.traits.ThermostatTemperatureSetpoint']?.heatCelsius;
|
||||
const cool = device.traits?.['sdm.devices.traits.ThermostatTemperatureSetpoint']?.coolCelsius;
|
||||
|
||||
let setpoint: number | [number, number];
|
||||
if (this.thermostatMode === ThermostatMode.Heat) {
|
||||
this.thermostatSetpoint = heat;
|
||||
this.thermostatSetpointHigh = undefined;
|
||||
this.thermostatSetpointLow = undefined;
|
||||
setpoint = heat;
|
||||
}
|
||||
else if (this.thermostatMode === ThermostatMode.Cool) {
|
||||
this.thermostatSetpoint = cool;
|
||||
this.thermostatSetpointHigh = undefined;
|
||||
this.thermostatSetpointLow = undefined;
|
||||
setpoint = cool;
|
||||
}
|
||||
else if (this.thermostatMode === ThermostatMode.HeatCool) {
|
||||
this.thermostatSetpoint = undefined;
|
||||
this.thermostatSetpointHigh = heat;
|
||||
this.thermostatSetpointLow = cool;
|
||||
setpoint = [heat, cool];
|
||||
}
|
||||
else {
|
||||
this.thermostatSetpoint = undefined;
|
||||
this.thermostatSetpointHigh = undefined;
|
||||
this.thermostatSetpointLow = undefined;
|
||||
}
|
||||
|
||||
this.temperatureSetting = {
|
||||
activeMode: this.thermostatActiveMode,
|
||||
mode: this.thermostatMode,
|
||||
setpoint,
|
||||
availableModes: modes,
|
||||
}
|
||||
}
|
||||
|
||||
async refresh(refreshInterface: string, userInitiated: boolean): Promise<void> {
|
||||
@@ -560,6 +620,8 @@ export class GoogleSmartDeviceAccess extends ScryptedDeviceBase implements Oauth
|
||||
}
|
||||
})();
|
||||
}
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
}
|
||||
|
||||
async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
|
||||
const payload = JSON.parse(Buffer.from(JSON.parse(request.body).message.data, 'base64').toString());
|
||||
|
||||
4
plugins/prebuffer-mixin/package-lock.json
generated
4
plugins/prebuffer-mixin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.9.72",
|
||||
"version": "0.9.73",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.9.72",
|
||||
"version": "0.9.73",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.9.72",
|
||||
"version": "0.9.73",
|
||||
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RtspServer, Headers } from "@scrypted/common/src/rtsp-server";
|
||||
import net from 'net';
|
||||
import { Headers, RtspServer } from "@scrypted/common/src/rtsp-server";
|
||||
import fs from 'fs';
|
||||
import net from 'net';
|
||||
|
||||
// non standard extension that dumps the rtp payload to a file.
|
||||
export class FileRtspServer extends RtspServer {
|
||||
@@ -8,8 +8,8 @@ export class FileRtspServer extends RtspServer {
|
||||
segmentBytesWritten = 0;
|
||||
writeConsole: Console;
|
||||
|
||||
constructor(client: net.Socket, sdp?: string) {
|
||||
super(client, sdp);
|
||||
constructor(client: net.Socket, sdp?: string, checkRequest?: (method: string, url: string, headers: Headers, rawMessage: string[]) => Promise<boolean>) {
|
||||
super(client, sdp, undefined, checkRequest);
|
||||
|
||||
this.client.on('close', () => {
|
||||
if (this.writeStream)
|
||||
|
||||
@@ -1114,10 +1114,16 @@ class PrebufferSession {
|
||||
return chunk;
|
||||
}
|
||||
|
||||
const client = await listenZeroSingleClient();
|
||||
const hostname = options?.route === 'external' ? '0.0.0.0' : undefined;
|
||||
const client = await listenZeroSingleClient(hostname);
|
||||
const rtspServerPath = '/' + crypto.randomBytes(8).toString('hex');
|
||||
socketPromise = client.clientPromise.then(async (socket) => {
|
||||
sdp = addTrackControls(sdp);
|
||||
server = new FileRtspServer(socket, sdp);
|
||||
server = new FileRtspServer(socket, sdp, async (method, url, headers, rawMessage) => {
|
||||
server.checkRequest = undefined;
|
||||
const u = new URL(url);
|
||||
return u.pathname === rtspServerPath;
|
||||
});
|
||||
server.writeConsole = this.console;
|
||||
if (session.parserSpecific) {
|
||||
const parserSpecific = session.parserSpecific as RtspSessionParserSpecific;
|
||||
@@ -1142,7 +1148,20 @@ class PrebufferSession {
|
||||
interleavePassthrough = session.parserSpecific && serverPortMap.size === 0;
|
||||
return socket;
|
||||
})
|
||||
url = client.url.replace('tcp://', 'rtsp://');
|
||||
url = client.url.replace('tcp://', 'rtsp://') + rtspServerPath;
|
||||
if (hostname) {
|
||||
try {
|
||||
const addresses = await sdk.endpointManager.getLocalAddresses();
|
||||
const [address] = addresses;
|
||||
if (address) {
|
||||
const u = new URL(url);
|
||||
u.hostname = address;
|
||||
url = u.toString();
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const client = await listenZeroSingleClient();
|
||||
@@ -1252,7 +1271,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
const u = new URL(url);
|
||||
|
||||
for (const session of this.sessions.values()) {
|
||||
if (u.pathname.endsWith(session.rtspServerPath)) {
|
||||
if (u.pathname === '/' + session.rtspServerPath) {
|
||||
server.console = session.console;
|
||||
prebufferSession = session;
|
||||
prebufferSession.ensurePrebufferSession();
|
||||
@@ -1260,7 +1279,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
server.sdp = await prebufferSession.sdp;
|
||||
return true;
|
||||
}
|
||||
if (u.pathname.endsWith(session.rtspServerMutedPath)) {
|
||||
if (u.pathname === '/' + session.rtspServerMutedPath) {
|
||||
server.console = session.console;
|
||||
prebufferSession = session;
|
||||
prebufferSession.ensurePrebufferSession();
|
||||
@@ -1326,7 +1345,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
}
|
||||
|
||||
async getVideoStream(options?: RequestMediaStreamOptions): Promise<MediaObject> {
|
||||
if (options?.directMediaStream)
|
||||
if (options?.route === 'direct')
|
||||
return this.mixinDevice.getVideoStream(options);
|
||||
|
||||
await this.ensurePrebufferSessions();
|
||||
|
||||
4
plugins/snapshot/package-lock.json
generated
4
plugins/snapshot/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/snapshot",
|
||||
"version": "0.0.43",
|
||||
"version": "0.0.45",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/snapshot",
|
||||
"version": "0.0.43",
|
||||
"version": "0.0.45",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@types/node": "^16.6.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/snapshot",
|
||||
"version": "0.0.43",
|
||||
"version": "0.0.45",
|
||||
"description": "Snapshot Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
@@ -22,6 +22,7 @@
|
||||
"camera"
|
||||
],
|
||||
"scrypted": {
|
||||
"realfs": true,
|
||||
"name": "Snapshot Plugin",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from 'fs';
|
||||
import { addVideoFilterArguments } from '@scrypted/common/src/ffmpeg-helpers';
|
||||
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from '@scrypted/common/src/media-helpers';
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
@@ -115,7 +116,11 @@ export async function ffmpegFilterImageBuffer(inputJpeg: Buffer, options: FFmpeg
|
||||
input.write(inputJpeg);
|
||||
input.end();
|
||||
|
||||
return ffmpegFilterImageInternal(cp, options);
|
||||
return ffmpegFilterImageInternal(cp, options)
|
||||
.catch(e => {
|
||||
fs.writeFileSync("/tmp/test.jpg", inputJpeg);
|
||||
throw e;
|
||||
})
|
||||
}
|
||||
|
||||
export async function ffmpegFilterImage(inputArguments: string[], options: FFmpegImageFilterOptions) {
|
||||
@@ -168,14 +173,16 @@ export async function ffmpegFilterImageInternal(cp: ChildProcess, options: FFmpe
|
||||
cp.stdio[3].on('data', data => buffers.push(data));
|
||||
|
||||
const to = options.timeout ? setTimeout(() => {
|
||||
console.log('ffmpeg stream to image conversion timed out.');
|
||||
console.log('ffmpeg input to image conversion timed out.');
|
||||
safeKillFFmpeg(cp);
|
||||
}, 10000) : undefined;
|
||||
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
const exit = once(cp, 'exit');
|
||||
await once(cp.stdio[3], 'end').catch(() => {});
|
||||
const [exitCode] = await exit;
|
||||
clearTimeout(to);
|
||||
if (exitCode && !buffers.length)
|
||||
throw new Error(`ffmpeg stream to image conversion failed with exit code: ${exitCode}`);
|
||||
throw new Error(`ffmpeg input to image conversion failed with exit code: ${exitCode}, ${cp.spawnargs.join(' ')}`);
|
||||
|
||||
return Buffer.concat(buffers);
|
||||
}
|
||||
@@ -207,7 +214,7 @@ export async function ffmpegFilterImageStream(cp: ChildProcess, options: FFmpegI
|
||||
if (last)
|
||||
resolve(last);
|
||||
else
|
||||
reject(new Error(`ffmpeg stream to image conversion failed with exit code: ${exitCode}`));
|
||||
reject(new Error(`ffmpeg stream to image conversion failed with exit code: ${exitCode}, ${cp.spawnargs.join(' ')}`));
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import { AutoenableMixinProvider } from "@scrypted/common/src/autoenable-mixin-provider";
|
||||
import { createMapPromiseDebouncer, RefreshPromise, singletonPromise, TimeoutError, timeoutPromise } from "@scrypted/common/src/promise-utils";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { createMapPromiseDebouncer, RefreshPromise, singletonPromise, TimeoutError } from "@scrypted/common/src/promise-utils";
|
||||
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
|
||||
import sdk, { BufferConverter, BufferConvertorOptions, Camera, FFmpegInput, MediaObject, MixinProvider, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from "@scrypted/sdk";
|
||||
import axios, { Axios } from "axios";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import https from 'https';
|
||||
import path from 'path';
|
||||
import MimeType from 'whatwg-mimetype';
|
||||
@@ -72,22 +72,12 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
},
|
||||
snapshotsFromPrebuffer: {
|
||||
title: 'Snapshots from Prebuffer',
|
||||
description: 'Prefer snapshots from the Rebroadcast Plugin prebuffer when available. This setting uses considerable CPU to convert a video stream into a snapshot.',
|
||||
type: 'boolean',
|
||||
defaultValue: !this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera),
|
||||
},
|
||||
snapshotMode: {
|
||||
title: 'Snapshot Mode',
|
||||
description: 'Set the snapshot mode to accomodate cameras with slow snapshots that may hang HomeKit.\nSetting the mode to "Never Wait" will only use recently available snapshots.\nSetting the mode to "Timeout" will cancel slow snapshots.',
|
||||
description: 'Prefer snapshots from the Rebroadcast Plugin prebuffer when available. This setting uses considerable CPU to convert a video stream into a snapshot. The Default setting will use the camera snapshot and fall back to prebuffer on failure.',
|
||||
choices: [
|
||||
'Default',
|
||||
'Never Wait',
|
||||
'Timeout',
|
||||
'Enabled',
|
||||
'Disabled',
|
||||
],
|
||||
mapGet(value) {
|
||||
// renamed the setting value.
|
||||
return value === 'Normal' ? 'Default' : value;
|
||||
},
|
||||
defaultValue: 'Default',
|
||||
},
|
||||
snapshotResolution: {
|
||||
@@ -107,7 +97,6 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
type: 'clippath',
|
||||
},
|
||||
});
|
||||
axiosClient: Axios | AxiosDigestAuth;
|
||||
snapshotDebouncer = createMapPromiseDebouncer<Buffer>();
|
||||
errorPicture: RefreshPromise<Buffer>;
|
||||
timeoutPicture: RefreshPromise<Buffer>;
|
||||
@@ -117,6 +106,7 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
lastErrorImagesClear = 0;
|
||||
static lastGeneratedErrorImageTime = 0;
|
||||
lastAvailablePicture: Buffer;
|
||||
psos: ResponsePictureOptions[];
|
||||
|
||||
constructor(public plugin: SnapshotPlugin, options: SettingsMixinDeviceOptions<Camera>) {
|
||||
super(options);
|
||||
@@ -132,7 +122,40 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
let needSoftwareResize = !!(options?.picture?.width || options?.picture?.height);
|
||||
|
||||
let takePicture: (options?: RequestPictureOptions) => Promise<Buffer>;
|
||||
if (this.storageSettings.values.snapshotsFromPrebuffer) {
|
||||
const { snapshotsFromPrebuffer } = this.storageSettings.values;
|
||||
let usePrebufferSnapshots: boolean;
|
||||
switch (snapshotsFromPrebuffer) {
|
||||
case 'true':
|
||||
case 'Enabled':
|
||||
usePrebufferSnapshots = true;
|
||||
break;
|
||||
case 'Disabled':
|
||||
usePrebufferSnapshots = false;
|
||||
break;
|
||||
default:
|
||||
if (!this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera))
|
||||
usePrebufferSnapshots = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// unifi cameras send stale snapshots which are unusable for events,
|
||||
// so force a prebuffer snapshot in this instance.
|
||||
// if prebuffer is not available, it will fall back.
|
||||
if (eventSnapshot && usePrebufferSnapshots !== false) {
|
||||
try {
|
||||
const psos = await this.getPictureOptions();
|
||||
if (psos?.[0]?.staleDuration) {
|
||||
usePrebufferSnapshots = true;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
let takePrebufferPicture: () => Promise<Buffer>;
|
||||
const preparePrebufferSnapshot = async () => {
|
||||
if (takePrebufferPicture)
|
||||
return takePrebufferPicture;
|
||||
try {
|
||||
const realDevice = systemManager.getDeviceById<VideoCamera>(this.id);
|
||||
const msos = await realDevice.getVideoStreamOptions();
|
||||
@@ -148,114 +171,124 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
request.prebuffer = eventSnapshot ? 1000 : 6000;
|
||||
if (this.lastAvailablePicture)
|
||||
request.refresh = false;
|
||||
takePicture = async () => mediaManager.convertMediaObjectToBuffer(await realDevice.getVideoStream(request), 'image/jpeg');
|
||||
this.console.log('snapshotting active prebuffer');
|
||||
takePrebufferPicture = async () => {
|
||||
// this.console.log('snapshotting active prebuffer');
|
||||
return mediaManager.convertMediaObjectToBuffer(await realDevice.getVideoStream(request), 'image/jpeg');
|
||||
};
|
||||
return takePrebufferPicture;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (usePrebufferSnapshots) {
|
||||
takePicture = await preparePrebufferSnapshot();
|
||||
}
|
||||
|
||||
if (!takePicture) {
|
||||
if (!this.storageSettings.values.snapshotUrl) {
|
||||
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera)) {
|
||||
takePicture = async (options?: RequestPictureOptions) => {
|
||||
const internalTakePicture = async () => {
|
||||
if (!options?.id && this.storageSettings.values.defaultSnapshotChannel !== 'Camera Default') {
|
||||
try {
|
||||
if (!psos)
|
||||
psos = await this.mixinDevice.getPictureOptions();
|
||||
const pso = psos.find(pso => pso.name === this.storageSettings.values.defaultSnapshotChannel);
|
||||
if (!options)
|
||||
options = {};
|
||||
options.id = pso.id;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
return this.mixinDevice.takePicture(options).then(mo => mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg'))
|
||||
}
|
||||
if (this.storageSettings.values.snapshotUrl) {
|
||||
let username: string;
|
||||
let password: string;
|
||||
|
||||
// full resolution setging ignores resize.
|
||||
if (this.storageSettings.values.snapshotResolution === 'Full Resolution') {
|
||||
if (options)
|
||||
options.picture = undefined;
|
||||
return internalTakePicture();
|
||||
}
|
||||
|
||||
// if resize wasn't requested, continue as normal.
|
||||
const resizeRequested = !!options?.picture;
|
||||
if (!resizeRequested)
|
||||
return internalTakePicture();
|
||||
|
||||
// resize was requested
|
||||
|
||||
// crop and scale needs to operate on the full resolution image.
|
||||
if (this.storageSettings.values.snapshotCropScale?.length) {
|
||||
options.picture = undefined;
|
||||
// resize after the cop and scale.
|
||||
needSoftwareResize = resizeRequested;
|
||||
return internalTakePicture();
|
||||
}
|
||||
|
||||
// determine see if that can be handled by camera hardware
|
||||
let psos: ResponsePictureOptions[];
|
||||
try {
|
||||
if (!psos)
|
||||
psos = await this.mixinDevice.getPictureOptions();
|
||||
if (!psos?.[0]?.canResize) {
|
||||
needSoftwareResize = true;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
if (needSoftwareResize)
|
||||
options.picture = undefined;
|
||||
|
||||
return internalTakePicture();
|
||||
};
|
||||
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Settings)) {
|
||||
const settings = await this.mixinDevice.getSettings();
|
||||
username = settings?.find(setting => setting.key === 'username')?.value?.toString();
|
||||
password = settings?.find(setting => setting.key === 'password')?.value?.toString();
|
||||
}
|
||||
else if (this.storageSettings.values.snapshotsFromPrebuffer) {
|
||||
takePicture = async () => {
|
||||
throw new PrebufferUnavailableError();
|
||||
}
|
||||
|
||||
let axiosClient: AxiosDigestAuth | AxiosInstance;
|
||||
if (username && password) {
|
||||
axiosClient = new AxiosDigestAuth({
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
else {
|
||||
takePicture = () => {
|
||||
throw new Error('Snapshot Unavailable (snapshotUrl empty)');
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!this.axiosClient) {
|
||||
let username: string;
|
||||
let password: string;
|
||||
|
||||
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Settings)) {
|
||||
const settings = await this.mixinDevice.getSettings();
|
||||
username = settings?.find(setting => setting.key === 'username')?.value?.toString();
|
||||
password = settings?.find(setting => setting.key === 'password')?.value?.toString();
|
||||
}
|
||||
|
||||
if (username && password) {
|
||||
this.axiosClient = new AxiosDigestAuth({
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.axiosClient = axios;
|
||||
}
|
||||
axiosClient = axios;
|
||||
}
|
||||
|
||||
takePicture = () => this.axiosClient.request({
|
||||
takePicture = () => axiosClient.request({
|
||||
httpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'arraybuffer',
|
||||
url: this.storageSettings.values.snapshotUrl,
|
||||
}).then(async (response: { data: any; }) => response.data);
|
||||
}
|
||||
else if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera)) {
|
||||
takePicture = async (options?: RequestPictureOptions) => {
|
||||
const internalTakePicture = async () => {
|
||||
if (!options?.id && this.storageSettings.values.defaultSnapshotChannel !== 'Camera Default') {
|
||||
try {
|
||||
const psos = await this.getPictureOptions();
|
||||
const pso = psos.find(pso => pso.name === this.storageSettings.values.defaultSnapshotChannel);
|
||||
if (!options)
|
||||
options = {};
|
||||
options.id = pso.id;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
return this.mixinDevice.takePicture(options).then(mo => mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg'))
|
||||
}
|
||||
|
||||
// full resolution setging ignores resize.
|
||||
if (this.storageSettings.values.snapshotResolution === 'Full Resolution') {
|
||||
if (options)
|
||||
options.picture = undefined;
|
||||
return internalTakePicture();
|
||||
}
|
||||
|
||||
// if resize wasn't requested, continue as normal.
|
||||
const resizeRequested = !!options?.picture;
|
||||
if (!resizeRequested)
|
||||
return internalTakePicture();
|
||||
|
||||
// resize was requested
|
||||
|
||||
// crop and scale needs to operate on the full resolution image.
|
||||
if (this.storageSettings.values.snapshotCropScale?.length) {
|
||||
options.picture = undefined;
|
||||
// resize after the cop and scale.
|
||||
needSoftwareResize = resizeRequested;
|
||||
return internalTakePicture();
|
||||
}
|
||||
|
||||
// determine see if that can be handled by camera hardware
|
||||
try {
|
||||
const psos = await this.getPictureOptions();
|
||||
if (!psos?.[0]?.canResize) {
|
||||
needSoftwareResize = true;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
if (needSoftwareResize)
|
||||
options.picture = undefined;
|
||||
|
||||
return internalTakePicture()
|
||||
.catch(async e => {
|
||||
// the camera snapshot failed, try to fallback to prebuffer snapshot.
|
||||
if (usePrebufferSnapshots === false)
|
||||
throw e;
|
||||
const fallback = await preparePrebufferSnapshot();
|
||||
if (!fallback)
|
||||
throw e;
|
||||
return fallback();
|
||||
})
|
||||
};
|
||||
}
|
||||
else if (usePrebufferSnapshots) {
|
||||
takePicture = async () => {
|
||||
throw new PrebufferUnavailableError();
|
||||
}
|
||||
}
|
||||
else {
|
||||
takePicture = () => {
|
||||
throw new Error('Snapshot Unavailable (snapshotUrl empty)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pendingPicture = this.snapshotDebouncer(options, async () => {
|
||||
@@ -286,34 +319,20 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
}, 60000);
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('Snapshot failed', e);
|
||||
// do not mask event snapshots, as they're used for detections and not
|
||||
// user facing display.
|
||||
if (eventSnapshot)
|
||||
throw e;
|
||||
// allow reusing the current picture to mask errors
|
||||
picture = await this.createErrorImage(e);
|
||||
}
|
||||
return picture;
|
||||
});
|
||||
|
||||
let { snapshotMode } = this.storageSettings.values;
|
||||
if (eventSnapshot) {
|
||||
// event snapshots must be fulfilled
|
||||
snapshotMode = 'Default';
|
||||
}
|
||||
else if (snapshotMode === 'Never Wait' && !options?.periodicRequest) {
|
||||
// non periodic snapshots should use a short timeout.
|
||||
snapshotMode = 'Timeout';
|
||||
}
|
||||
|
||||
let data: Buffer;
|
||||
try {
|
||||
switch (snapshotMode) {
|
||||
case 'Never Wait':
|
||||
throw new NeverWaitError();
|
||||
case 'Timeout':
|
||||
data = await timeoutPromise(1000, pendingPicture);
|
||||
break;
|
||||
default:
|
||||
data = await pendingPicture;
|
||||
break;
|
||||
}
|
||||
data = await pendingPicture;
|
||||
}
|
||||
catch (e) {
|
||||
// allow reusing the current picture to mask errors
|
||||
@@ -443,7 +462,9 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
}
|
||||
|
||||
async getPictureOptions() {
|
||||
return this.mixinDevice.getPictureOptions();
|
||||
if (!this.psos)
|
||||
this.psos = await this.mixinDevice.getPictureOptions();
|
||||
return this.psos;
|
||||
}
|
||||
|
||||
getMixinSettings(): Promise<Setting[]> {
|
||||
|
||||
4
plugins/webrtc/package-lock.json
generated
4
plugins/webrtc/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.1.33",
|
||||
"version": "0.1.34",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.1.33",
|
||||
"version": "0.1.34",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.1.33",
|
||||
"version": "0.1.34",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -419,12 +419,13 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
|
||||
}
|
||||
}
|
||||
|
||||
const iceServers = this.storageSettings.values.useTurnServer
|
||||
? [weriftStunServer, weriftTurnServer]
|
||||
: [weriftStunServer];
|
||||
|
||||
return {
|
||||
iceUseIpv6: false,
|
||||
iceServers: [
|
||||
weriftStunServer,
|
||||
weriftTurnServer,
|
||||
],
|
||||
iceServers,
|
||||
...ret,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -530,13 +530,13 @@ class RequestMediaStreamOptions(TypedDict):
|
||||
container: str
|
||||
destination: MediaStreamDestination
|
||||
destinationId: str
|
||||
directMediaStream: bool
|
||||
id: str
|
||||
metadata: Any
|
||||
name: str
|
||||
prebuffer: float
|
||||
prebufferBytes: float
|
||||
refresh: bool
|
||||
route: Any | Any
|
||||
tool: MediaStreamTool
|
||||
video: VideoStreamOptions
|
||||
pass
|
||||
@@ -555,7 +555,6 @@ class RequestRecordingStreamOptions(TypedDict):
|
||||
container: str
|
||||
destination: MediaStreamDestination
|
||||
destinationId: str
|
||||
directMediaStream: bool
|
||||
duration: float
|
||||
id: str
|
||||
loop: bool
|
||||
@@ -565,6 +564,7 @@ class RequestRecordingStreamOptions(TypedDict):
|
||||
prebuffer: float
|
||||
prebufferBytes: float
|
||||
refresh: bool
|
||||
route: Any | Any
|
||||
startTime: float
|
||||
tool: MediaStreamTool
|
||||
video: VideoStreamOptions
|
||||
|
||||
@@ -568,12 +568,13 @@ export type MediaStreamDestination = "local" | "remote" | "medium-resolution" |
|
||||
|
||||
export interface RequestMediaStreamOptions extends MediaStreamOptions {
|
||||
/**
|
||||
* When retrieving media, setting disableMediaProxies=true
|
||||
* will bypass any intermediaries (NVR, rebroadcast) and retrieve
|
||||
* it directly from the source. This is useful in cases when
|
||||
* peer to peer connections are possible and preferred, such as WebRTC.
|
||||
* When retrieving media, setting route directs how the media should be
|
||||
* retrieved and exposed. A direct route will get the stream
|
||||
* as is from the source. This will bypass any intermediaries if possible,
|
||||
* such as an NVR or restreamers.
|
||||
* An external route will request that that provided route is exposed to the local network.
|
||||
*/
|
||||
directMediaStream?: boolean;
|
||||
route?: 'external' | 'direct';
|
||||
|
||||
/**
|
||||
* Specify the stream refresh behavior when this stream is requested.
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.6.21",
|
||||
"version": "0.6.23",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.6.21",
|
||||
"version": "0.6.23",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.6.20",
|
||||
"version": "0.6.24",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
||||
@@ -69,7 +69,7 @@
|
||||
"build": "tsc --outDir dist",
|
||||
"postbuild": "node test/check-build-output.js",
|
||||
"prepublishOnly": "npm version patch && git add package.json && npm run build && git commit -m prepublish",
|
||||
"postpublish": "git push origin v$npm_package_version",
|
||||
"postpublish": "git tag v$npm_package_version && git push origin v$npm_package_version",
|
||||
"docker": "scripts/github-workflow-publish-docker.sh"
|
||||
},
|
||||
"author": "",
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { once } from 'events';
|
||||
import net from 'net';
|
||||
|
||||
export async function listenZero(server: net.Server) {
|
||||
server.listen(0);
|
||||
export async function listenZero(server: net.Server, hostname?: string) {
|
||||
server.listen(0, hostname);
|
||||
await once(server, 'listening');
|
||||
return (server.address() as net.AddressInfo).port;
|
||||
}
|
||||
|
||||
export async function listenZeroSingleClient() {
|
||||
export async function listenZeroSingleClient(hostname?: string) {
|
||||
const server = new net.Server();
|
||||
const port = await listenZero(server);
|
||||
const port = await listenZero(server, hostname);
|
||||
|
||||
const clientPromise = new Promise<net.Socket>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
@@ -24,7 +24,7 @@ export async function listenZeroSingleClient() {
|
||||
});
|
||||
});
|
||||
|
||||
clientPromise.catch(() => {});
|
||||
clientPromise.catch(() => { });
|
||||
|
||||
return {
|
||||
server,
|
||||
|
||||
@@ -10,6 +10,7 @@ function newDeviceProxy(id: string, systemManager: SystemManagerImpl) {
|
||||
}
|
||||
|
||||
class DeviceProxyHandler implements PrimitiveProxyHandler<any>, ScryptedDevice {
|
||||
customProperties: Map<string | number | symbol, any>;
|
||||
device: Promise<ScryptedDevice>;
|
||||
constructor(public id: string, public systemManager: SystemManagerImpl) {
|
||||
}
|
||||
@@ -43,10 +44,34 @@ class DeviceProxyHandler implements PrimitiveProxyHandler<any>, ScryptedDevice {
|
||||
}
|
||||
}
|
||||
|
||||
deleteProperty(target: any, p: string | symbol): boolean {
|
||||
const prop = p.toString();
|
||||
if (Object.keys(ScryptedInterfaceProperty).includes(prop))
|
||||
return false;
|
||||
|
||||
this.customProperties ||= new Map();
|
||||
this.customProperties.set(p, undefined);
|
||||
return true;
|
||||
}
|
||||
|
||||
set(target: any, p: string | symbol, newValue: any, receiver: any): boolean {
|
||||
const prop = p.toString();
|
||||
if (Object.keys(ScryptedInterfaceProperty).includes(prop))
|
||||
return false;
|
||||
|
||||
this.customProperties ||= new Map();
|
||||
this.customProperties.set(p, newValue);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get(target: any, p: PropertyKey, receiver: any): any {
|
||||
if (p === 'id')
|
||||
return this.id;
|
||||
|
||||
if (this.customProperties?.has(p))
|
||||
return this.customProperties.get(p);
|
||||
|
||||
const handled = RpcPeer.handleFunctionInvocations(this, target, p, receiver);
|
||||
if (handled)
|
||||
return handled;
|
||||
|
||||
@@ -171,7 +171,7 @@ class RpcProxy implements PrimitiveProxyHandler<any> {
|
||||
// todo: error constructor adds a "cause" variable in Chrome 93, Node v??
|
||||
export class RPCResultError extends Error {
|
||||
constructor(peer: RpcPeer, message: string, public cause?: Error, options?: { name: string, stack: string | undefined }) {
|
||||
super(`${peer.selfName}:${peer.peerName}: ${message}`);
|
||||
super(`${message}\n${peer.selfName}:${peer.peerName}`);
|
||||
|
||||
if (options?.name) {
|
||||
this.name = options?.name;
|
||||
|
||||
@@ -194,6 +194,12 @@ async function start() {
|
||||
// lack of login from cookie auth.
|
||||
|
||||
const checkToken = (token: string) => {
|
||||
if (process.env.SCRYPTED_ADMIN_USERNAME && process.env.SCRYPTED_ADMIN_TOKEN === token) {
|
||||
res.locals.username = process.env.SCRYPTED_ADMIN_USERNAME;
|
||||
res.locals.aclId = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const [checkHash, ...tokenParts] = token.split('#');
|
||||
const tokenPart = tokenParts?.join('#');
|
||||
if (checkHash && tokenPart) {
|
||||
@@ -544,8 +550,31 @@ async function start() {
|
||||
await checkResetLogin();
|
||||
|
||||
const hostname = os.hostname()?.split('.')?.[0];
|
||||
|
||||
const addresses = ((await scrypted.addressSettings.getLocalAddresses()) || getHostAddresses(true, true)).map(address => `https://${address}:${SCRYPTED_SECURE_PORT}`);
|
||||
|
||||
// env/header based admin login
|
||||
if (res.locals.username && res.locals.username === process.env.SCRYPTED_ADMIN_USERNAME) {
|
||||
res.send({
|
||||
username: res.locals.username,
|
||||
token: process.env.SCRYPTED_ADMIN_TOKEN,
|
||||
addresses,
|
||||
hostname,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// env based anon admin login
|
||||
if (process.env.SCRYPTED_DISABLE_AUTHENTICATION === 'true') {
|
||||
res.send({
|
||||
expiration: ONE_DAY_MILLISECONDS,
|
||||
username: 'anonymous',
|
||||
addresses,
|
||||
hostname,
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
// basic auth
|
||||
if (req.protocol === 'https' && req.headers.authorization) {
|
||||
const username = await new Promise(resolve => {
|
||||
const basicChecker = basicAuth.check((req) => {
|
||||
@@ -570,16 +599,7 @@ async function start() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.SCRYPTED_DISABLE_AUTHENTICATION === 'true') {
|
||||
res.send({
|
||||
expiration: ONE_DAY_MILLISECONDS,
|
||||
username: 'anonymous',
|
||||
addresses,
|
||||
hostname,
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
// cookie auth
|
||||
try {
|
||||
const login_user_token = getSignedLoginUserTokenRawValue(req);
|
||||
if (!login_user_token)
|
||||
|
||||
Reference in New Issue
Block a user