Compare commits

..

19 Commits

Author SHA1 Message Date
Koushik Dutta
28e1f5ac8a prepublish 2023-02-25 09:55:03 -08:00
Koushik Dutta
bafe73d296 server: add support for admin configuration via env 2023-02-25 09:54:54 -08:00
Koushik Dutta
f17ce50f17 webrtc: werift config should also respect turn server preference 2023-02-25 09:33:09 -08:00
Koushik Dutta
18f5872be1 core: fix oauth on rehosted urls 2023-02-24 22:00:48 -08:00
Koushik Dutta
fdccaaa65e client: publish 2023-02-24 19:57:45 -08:00
Koushik Dutta
6a55172924 server/rpc: add support for clobbering non-state properties and methods 2023-02-24 19:41:50 -08:00
Koushik Dutta
1e41af77fa rebroadcast: add external url request support 2023-02-24 18:58:28 -08:00
Koushik Dutta
e169a6e02d rebroadcast: add rtsp path to prevent port guessing access 2023-02-24 18:49:16 -08:00
Koushik Dutta
ef55c834af sdk: getVideoStream routing hints 2023-02-24 18:48:39 -08:00
Koushik Dutta
3812ad92ac core: fix safari weirdness on async methods and window open 2023-02-24 16:26:07 -08:00
Koushik Dutta
0bdb402e7b core: use target _blank when starting oauth from inside iframe 2023-02-24 16:11:38 -08:00
Koushik Dutta
1588ea250b server: publish rpc change 2023-02-24 16:01:52 -08:00
Koushik Dutta
47f603877b prepublish 2023-02-24 16:01:29 -08:00
Koushik Dutta
9adc803206 prepublish 2023-02-24 16:00:47 -08:00
Koushik Dutta
031e290017 google-device-access: implement new api 2023-02-24 11:50:19 -08:00
Koushik Dutta
a9ab2d0110 google-device-access: use new state api 2023-02-24 11:42:52 -08:00
Koushik Dutta
9592b6087b Merge branch 'main' of github.com:koush/scrypted 2023-02-24 11:30:21 -08:00
Koushik Dutta
5c34213b5d google-device-access: fixup project 2023-02-24 11:30:15 -08:00
Nick Berardi
4435fe1e0a alexa: ensure we are talking to the correct API endpoint (#580)
* alexa plugin: ensure we are talking to the right endpoint region for a customer

* added the api endpoint as a visible setting
2023-02-24 09:41:11 -08:00
27 changed files with 240 additions and 144 deletions

View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/rpc",
"version": "0.0.6",
"version": "0.0.7",
"description": "",
"main": "dist/index.js",
"scripts": {

View File

@@ -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": {

View File

@@ -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;
}

View File

@@ -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,
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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();
});
}
}

View File

@@ -1 +0,0 @@
../../../common/fs/unavailable.jpg

View File

@@ -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());

View File

@@ -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",

View File

@@ -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",

View File

@@ -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)

View File

@@ -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();

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,
};
}

View File

@@ -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

View File

@@ -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.

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/server",
"version": "0.6.21",
"version": "0.6.22",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/server",
"version": "0.6.21",
"version": "0.6.22",
"license": "ISC",
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/server",
"version": "0.6.20",
"version": "0.6.23",
"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": "",

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 = process.env.SCRYPTED_ADMIN_TOKEN;
return;
}
const [checkHash, ...tokenParts] = token.split('#');
const tokenPart = tokenParts?.join('#');
if (checkHash && tokenPart) {