mirror of
https://github.com/koush/scrypted.git
synced 2026-02-07 16:02:13 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7614d12363 | ||
|
|
3189317b2d | ||
|
|
410d11248f | ||
|
|
28e1f5ac8a | ||
|
|
bafe73d296 | ||
|
|
f17ce50f17 | ||
|
|
18f5872be1 | ||
|
|
fdccaaa65e | ||
|
|
6a55172924 | ||
|
|
1e41af77fa | ||
|
|
e169a6e02d | ||
|
|
ef55c834af | ||
|
|
3812ad92ac | ||
|
|
0bdb402e7b | ||
|
|
1588ea250b |
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": {
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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.22",
|
||||
"version": "0.6.24",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
||||
|
||||
@@ -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