mirror of
https://github.com/koush/scrypted.git
synced 2026-02-07 16:02:13 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1301247ea3 | ||
|
|
2798fe4d3d | ||
|
|
55a76a86dc | ||
|
|
cebd49fadb | ||
|
|
90adb11f27 | ||
|
|
cea5c95c82 | ||
|
|
0405e13181 | ||
|
|
5659499c16 | ||
|
|
d272a4b86f | ||
|
|
f8a8ed4241 | ||
|
|
892b978065 | ||
|
|
c81c55c12e | ||
|
|
bb9d98921b | ||
|
|
4c66efc4af | ||
|
|
0547ed9a32 | ||
|
|
b046822282 | ||
|
|
b033d24451 | ||
|
|
15464229ad | ||
|
|
93ad50db73 | ||
|
|
427139e8df | ||
|
|
b1100398ec | ||
|
|
b40a2eaf6e | ||
|
|
17c9440fd9 | ||
|
|
ea63a96444 | ||
|
|
0f02f96b89 | ||
|
|
6ce538bb23 | ||
|
|
29ab0e79de | ||
|
|
e07cd13ef3 | ||
|
|
0cbb26051c | ||
|
|
fcb8d938ee | ||
|
|
98fe1d412a | ||
|
|
c19ec63f98 | ||
|
|
a41e915f69 | ||
|
|
f0db59f6d2 | ||
|
|
8e691ff2ee | ||
|
|
42e0810bc0 | ||
|
|
68e91ad996 | ||
|
|
e163aa8153 | ||
|
|
268225647e | ||
|
|
93f94b0b0a | ||
|
|
db73baf4c1 | ||
|
|
404cf47d2e | ||
|
|
b751f77b0b | ||
|
|
884ce3e175 | ||
|
|
0cb0071874 |
@@ -70,11 +70,7 @@ export function getH264DecoderArgs(): CodecArgs {
|
||||
],
|
||||
};
|
||||
|
||||
if (isRaspberryPi()) {
|
||||
ret['Raspberry Pi'] = ['-c:v', 'h264_mmal'];
|
||||
ret[V4L2] = ['-c:v', 'h264_v4l2m2m'];
|
||||
}
|
||||
else if (os.platform() === 'linux') {
|
||||
if (os.platform() === 'linux') {
|
||||
ret[V4L2] = ['-c:v', 'h264_v4l2m2m'];
|
||||
}
|
||||
else if (os.platform() === 'win32') {
|
||||
|
||||
@@ -247,6 +247,7 @@ export function createRtspParser(options?: StreamParserOptions): RtspStreamParse
|
||||
'tcp',
|
||||
...(options?.vcodec || []),
|
||||
...(options?.acodec || []),
|
||||
'-pkt_size', '64000',
|
||||
'-f', 'rtsp',
|
||||
],
|
||||
findSyncFrame(streamChunks: StreamChunk[]) {
|
||||
@@ -394,7 +395,7 @@ export class RtspClient extends RtspBase {
|
||||
hasGetParameter = true;
|
||||
contentBase: string;
|
||||
|
||||
constructor(public url: string) {
|
||||
constructor(public readonly url: string) {
|
||||
super();
|
||||
const u = new URL(url);
|
||||
const port = parseInt(u.port) || 554;
|
||||
@@ -511,6 +512,42 @@ export class RtspClient extends RtspBase {
|
||||
}
|
||||
}
|
||||
|
||||
async *handleStream(): AsyncGenerator<{
|
||||
rtcp: boolean,
|
||||
header: Buffer,
|
||||
packet: Buffer,
|
||||
channel: number,
|
||||
}> {
|
||||
while (true) {
|
||||
const header = await readLength(this.client, 4);
|
||||
// can this even happen? since the RTSP request method isn't a fixed
|
||||
// value like the "RTSP" in the RTSP response, I don't think so?
|
||||
if (header[0] !== RTSP_FRAME_MAGIC) {
|
||||
if (header.toString() !== 'RTSP')
|
||||
throw this.createBadHeader(header);
|
||||
|
||||
this.client.unshift(header);
|
||||
|
||||
// do what with this?
|
||||
const message = await super.readMessage();
|
||||
const body = await this.readBody(parseHeaders(message));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const length = header.readUInt16BE(2);
|
||||
const packet = await readLength(this.client, length);
|
||||
const id = header.readUInt8(1);
|
||||
|
||||
yield {
|
||||
channel: id,
|
||||
rtcp: id % 2 === 1,
|
||||
header,
|
||||
packet,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async readLoop() {
|
||||
const deferred = new Deferred<void>();
|
||||
|
||||
@@ -613,7 +650,8 @@ export class RtspClient extends RtspBase {
|
||||
const { parseHTTPHeadersQuotedKeyValueSet } = await import('http-auth-utils/dist/utils');
|
||||
|
||||
if (this.wwwAuthenticate.includes('Basic')) {
|
||||
const hash = BASIC.computeHash(url);
|
||||
const parsedUrl = new URL(this.url);
|
||||
const hash = BASIC.computeHash({ username: parsedUrl.username, password: parsedUrl.password });
|
||||
return `Basic ${hash}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,6 @@ ENV NODE_OPTIONS="--dns-result-order=ipv4first"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20240321"
|
||||
ENV SCRYPTED_BASE_VERSION="20250101"
|
||||
|
||||
CMD ["/bin/sh", "-c", "ulimit -c 0; exec npm --prefix /server exec scrypted-serve"]
|
||||
|
||||
@@ -46,6 +46,6 @@ ENV NODE_OPTIONS="--dns-result-order=ipv4first"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20240321"
|
||||
ENV SCRYPTED_BASE_VERSION="20250101"
|
||||
|
||||
CMD ["/bin/sh", "-c", "ulimit -c 0; exec npm --prefix /server exec scrypted-serve"]
|
||||
|
||||
4
plugins/cloud/package-lock.json
generated
4
plugins/cloud/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/cloud",
|
||||
"version": "0.2.48",
|
||||
"version": "0.2.49",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/cloud",
|
||||
"version": "0.2.48",
|
||||
"version": "0.2.49",
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": "^4.3.0",
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -52,5 +52,5 @@
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"version": "0.2.48"
|
||||
"version": "0.2.49"
|
||||
}
|
||||
|
||||
@@ -1046,12 +1046,16 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
args['--url'] = tunnelUrl;
|
||||
}
|
||||
|
||||
// if error messages are detected after 10 minutes from tunnel attempt start,
|
||||
// kill the tunnel.
|
||||
const tenMinutesMs = 10 * 60 * 1000;
|
||||
const tunnelStart = Date.now();
|
||||
const deferred = new Deferred<string>();
|
||||
|
||||
const cloudflareTunnel = cloudflared.tunnel(args);
|
||||
deferred.resolvePromise(cloudflareTunnel.url);
|
||||
|
||||
const processData = (string: string) => {
|
||||
this.console.error(string);
|
||||
|
||||
const lines = string.split('\n');
|
||||
for (const line of lines) {
|
||||
if ((line.includes('Unregistered tunnel connection')
|
||||
@@ -1059,12 +1063,10 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
|| line.includes('Register tunnel error')
|
||||
|| line.includes('Failed to serve tunnel')
|
||||
|| line.includes('Failed to get tunnel'))
|
||||
&& deferred.finished) {
|
||||
this.console.warn('Cloudflare registration failed after tunnel started. The old tunnel may be invalid. Terminating.');
|
||||
&& (deferred.finished || Date.now() - tunnelStart > tenMinutesMs)) {
|
||||
this.console.warn('Cloudflare registration failure detected. Terminating.');
|
||||
cloudflareTunnel.child.kill();
|
||||
}
|
||||
if (line.includes('hostname'))
|
||||
this.console.log(line);
|
||||
const match = /config=(".*?}")/gm.exec(line)
|
||||
if (match) {
|
||||
const json = match[1];
|
||||
@@ -1109,7 +1111,10 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
throw e;
|
||||
}
|
||||
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${tunnelUrl}`);
|
||||
return cloudflareTunnel;
|
||||
return {
|
||||
url: deferred.promise,
|
||||
child: cloudflareTunnel.child,
|
||||
};
|
||||
}, {
|
||||
startingDelay: 60000,
|
||||
timeMultiple: 1.2,
|
||||
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.102",
|
||||
"version": "0.3.103",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.102",
|
||||
"version": "0.3.103",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.102",
|
||||
"version": "0.3.103",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function checkLegacyLxc() {
|
||||
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT !== SCRYPTED_INSTALL_ENVIRONMENT_LXC)
|
||||
return;
|
||||
|
||||
sdk.log.a('This system is currently running the legacy LXC installation method and must be migrated to the new LXC manually: https://docs.scrypted.app/installation.html#proxmox-ve-container-reset');
|
||||
sdk.log.a('This system is currently running the legacy LXC installation method and must be migrated to the new LXC manually: https://docs.scrypted.app/install/proxmox-ve.html#proxmox-ve-container-reset');
|
||||
}
|
||||
|
||||
const DOCKER_COMPOSE_SH_PATH = '/root/.scrypted/docker-compose.sh';
|
||||
|
||||
2
plugins/dummy-switch/.vscode/settings.json
vendored
2
plugins/dummy-switch/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
}
|
||||
130
plugins/dummy-switch/package-lock.json
generated
130
plugins/dummy-switch/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/dummy-switch",
|
||||
"version": "0.0.24",
|
||||
"version": "0.0.25",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/dummy-switch",
|
||||
"version": "0.0.24",
|
||||
"version": "0.0.25",
|
||||
"dependencies": {
|
||||
"@types/node": "^16.6.1",
|
||||
"axios": "^1.3.6"
|
||||
@@ -23,35 +23,41 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.97",
|
||||
"version": "0.3.106",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -63,11 +69,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
@@ -92,11 +96,11 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz",
|
||||
"integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==",
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
@@ -121,9 +125,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -182,35 +186,39 @@
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"@types/node": "^16.9.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
"@types/node": "^20.11.0",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^22.10.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.26.11",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
@@ -224,11 +232,11 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz",
|
||||
"integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==",
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
@@ -247,9 +255,9 @@
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.24"
|
||||
"version": "0.0.25"
|
||||
}
|
||||
|
||||
@@ -2,11 +2,60 @@ import { BinarySensor, DeviceCreator, DeviceCreatorSettings, DeviceProvider, Loc
|
||||
import sdk from '@scrypted/sdk';
|
||||
import { ReplaceMotionSensor, ReplaceMotionSensorNativeId } from './replace-motion-sensor';
|
||||
import { ReplaceBinarySensor, ReplaceBinarySensorNativeId } from './replace-binary-sensor';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
|
||||
const { log, deviceManager } = sdk;
|
||||
|
||||
class DummyDevice extends ScryptedDeviceBase implements OnOff, Lock, StartStop, OccupancySensor, MotionSensor, BinarySensor, Settings {
|
||||
timeout: NodeJS.Timeout;
|
||||
storageSettings = new StorageSettings(this, {
|
||||
reset: {
|
||||
title: 'Reset Sensor',
|
||||
description: 'Reset the motion sensor and binary sensor after the given seconds. Enter 0 to never reset.',
|
||||
defaultValue: 10,
|
||||
type: 'number',
|
||||
placeholder: '10',
|
||||
onPut: () => {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
},
|
||||
actionTypes: {
|
||||
title: 'Action Types',
|
||||
description: 'Select the action types to expose.',
|
||||
defaultValue: [
|
||||
ScryptedInterface.OnOff,
|
||||
ScryptedInterface.StartStop,
|
||||
ScryptedInterface.Lock,
|
||||
],
|
||||
multiple: true,
|
||||
choices: [
|
||||
ScryptedInterface.OnOff,
|
||||
ScryptedInterface.StartStop,
|
||||
ScryptedInterface.Lock,
|
||||
],
|
||||
onPut: () => {
|
||||
this.reportInterfaces();
|
||||
},
|
||||
},
|
||||
sensorTypes: {
|
||||
title: 'Sensor Types',
|
||||
description: 'Select the sensor types to expose.',
|
||||
defaultValue: [
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.BinarySensor,
|
||||
ScryptedInterface.OccupancySensor,
|
||||
],
|
||||
multiple: true,
|
||||
choices: [
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.BinarySensor,
|
||||
ScryptedInterface.OccupancySensor,
|
||||
],
|
||||
onPut: () => {
|
||||
this.reportInterfaces();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
constructor(nativeId: string) {
|
||||
super(nativeId);
|
||||
@@ -19,6 +68,22 @@ class DummyDevice extends ScryptedDeviceBase implements OnOff, Lock, StartStop,
|
||||
this.occupied = false;
|
||||
}
|
||||
|
||||
async reportInterfaces() {
|
||||
const interfaces: ScryptedInterface[] = this.storageSettings.values.sensorTypes || [];
|
||||
if (!interfaces.length)
|
||||
interfaces.push(ScryptedInterface.MotionSensor, ScryptedInterface.BinarySensor, ScryptedInterface.OccupancySensor);
|
||||
const actionTyoes = this.storageSettings.values.actionTypes || [];
|
||||
if (!actionTyoes.length)
|
||||
actionTyoes.push(ScryptedInterface.OnOff, ScryptedInterface.StartStop, ScryptedInterface.Lock);
|
||||
|
||||
await sdk.deviceManager.onDeviceDiscovered({
|
||||
nativeId: this.nativeId,
|
||||
interfaces: [...interfaces, ...actionTyoes, ScryptedInterface.Settings],
|
||||
type: ScryptedDeviceType.Switch,
|
||||
name: this.providedName,
|
||||
});
|
||||
}
|
||||
|
||||
lock(): Promise<void> {
|
||||
return this.turnOff();
|
||||
}
|
||||
@@ -31,20 +96,12 @@ class DummyDevice extends ScryptedDeviceBase implements OnOff, Lock, StartStop,
|
||||
stop(): Promise<void> {
|
||||
return this.turnOff();
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
key: 'reset',
|
||||
title: 'Reset Sensor',
|
||||
description: 'Reset the motion sensor and binary sensor after the given seconds. Enter 0 to never reset.',
|
||||
value: this.storage.getItem('reset') || '10',
|
||||
placeholder: '10',
|
||||
}
|
||||
]
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
async putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
this.storage.setItem(key, value.toString());
|
||||
clearTimeout(this.timeout);
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
// note that turnOff locks the lock
|
||||
@@ -131,12 +188,6 @@ class DummyDeviceProvider extends ScryptedDeviceBase implements DeviceProvider,
|
||||
const nativeId = 'shell:' + Math.random().toString();
|
||||
const name = settings.name?.toString();
|
||||
|
||||
await this.onDiscovered(nativeId, name);
|
||||
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
async onDiscovered(nativeId: string, name: string) {
|
||||
await deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
@@ -151,6 +202,8 @@ class DummyDeviceProvider extends ScryptedDeviceBase implements DeviceProvider,
|
||||
],
|
||||
type: ScryptedDeviceType.Switch,
|
||||
});
|
||||
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) {
|
||||
@@ -163,11 +216,6 @@ class DummyDeviceProvider extends ScryptedDeviceBase implements DeviceProvider,
|
||||
if (!ret) {
|
||||
ret = new DummyDevice(nativeId);
|
||||
|
||||
// remove legacy scriptable interface
|
||||
if (ret.interfaces.includes(ScryptedInterface.Scriptable)) {
|
||||
setTimeout(() => this.onDiscovered(ret.nativeId, ret.providedName), 2000);
|
||||
}
|
||||
|
||||
if (ret)
|
||||
this.devices.set(nativeId, ret);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
4
plugins/homekit/package-lock.json
generated
4
plugins/homekit/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.61",
|
||||
"version": "1.2.62",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.61",
|
||||
"version": "1.2.62",
|
||||
"dependencies": {
|
||||
"@koush/werift-src": "file:../../external/werift",
|
||||
"check-disk-space": "^3.4.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.61",
|
||||
"version": "1.2.62",
|
||||
"description": "HomeKit Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
|
||||
@@ -519,11 +519,15 @@ export class H264Repacketizer {
|
||||
// after the codec information. so codec information can be changed between
|
||||
// idr and non-idr? maybe it is not applied until next idr?
|
||||
}
|
||||
else if (nalType === NAL_TYPE_IDR) {
|
||||
// this is uncommon but has been seen on tapo.
|
||||
// i have no clue how they can fit an idr frame into a single packet stapa.
|
||||
}
|
||||
else if (nalType === 0) {
|
||||
// nal delimiter or something. usually empty.
|
||||
}
|
||||
else {
|
||||
this.console.warn('Skipped a stapa type. Please report this to @koush on Discord.', nalType)
|
||||
this.console.warn('Skipped a stapa type.', nalType)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
198
plugins/objectdetector/package-lock.json
generated
198
plugins/objectdetector/package-lock.json
generated
@@ -1,22 +1,19 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.60",
|
||||
"version": "0.1.65",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.60",
|
||||
"version": "0.1.65",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"polygon-clipping": "^0.15.7",
|
||||
"semver": "^7.5.4"
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/semver": "^7.5.6"
|
||||
"@types/node": "^20.11.0"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
@@ -25,34 +22,40 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.12",
|
||||
"version": "0.3.106",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -64,11 +67,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
@@ -88,67 +89,12 @@
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/polygon-clipping": {
|
||||
"version": "0.15.7",
|
||||
"resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.7.tgz",
|
||||
"integrity": "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA==",
|
||||
"dependencies": {
|
||||
"robust-predicates": "^3.0.2",
|
||||
"splaytree": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/robust-predicates": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
||||
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/splaytree": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/splaytree/-/splaytree-3.1.2.tgz",
|
||||
"integrity": "sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A=="
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node-moving-things-tracker": {
|
||||
"version": "0.9.1",
|
||||
"extraneous": true,
|
||||
@@ -172,35 +118,39 @@
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"@types/node": "^20.11.0",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^22.10.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.26.11",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
@@ -212,57 +162,11 @@
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"@types/semver": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
|
||||
"dev": true
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"polygon-clipping": {
|
||||
"version": "0.15.7",
|
||||
"resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.7.tgz",
|
||||
"integrity": "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA==",
|
||||
"requires": {
|
||||
"robust-predicates": "^3.0.2",
|
||||
"splaytree": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"robust-predicates": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
||||
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"splaytree": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/splaytree/-/splaytree-3.1.2.tgz",
|
||||
"integrity": "sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.60",
|
||||
"version": "0.1.65",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
@@ -46,12 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"polygon-clipping": "^0.15.7",
|
||||
"semver": "^7.5.4"
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/semver": "^7.5.6"
|
||||
"@types/node": "^20.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import crypto from 'crypto';
|
||||
import { AutoenableMixinProvider } from "../../../common/src/autoenable-mixin-provider";
|
||||
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
|
||||
import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes';
|
||||
import { fixLegacyClipPath, insidePolygon, normalizeBoxToClipPath, polygonOverlap } from './polygon';
|
||||
import { fixLegacyClipPath, normalizeBox, polygonContainsBoundingBox, polygonIntersectsBoundingBox } from './polygon';
|
||||
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor } from './smart-motionsensor';
|
||||
import { SMART_OCCUPANCYSENSOR_PREFIX, SmartOccupancySensor } from './smart-occupancy-sensor';
|
||||
import { getAllDevices, safeParseJson } from './util';
|
||||
@@ -545,7 +545,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (!o.boundingBox)
|
||||
continue;
|
||||
|
||||
const box = normalizeBoxToClipPath(o.boundingBox, detection.inputDimensions);
|
||||
const box = normalizeBox(o.boundingBox, detection.inputDimensions);
|
||||
|
||||
let included: boolean;
|
||||
// need a way to explicitly include package zone.
|
||||
@@ -572,13 +572,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
|
||||
let match = false;
|
||||
if (zoneInfo?.type === 'Contain') {
|
||||
match = insidePolygon(box[0] as Point, zoneValue) &&
|
||||
insidePolygon(box[1], zoneValue) &&
|
||||
insidePolygon(box[2], zoneValue) &&
|
||||
insidePolygon(box[3], zoneValue);
|
||||
match = polygonContainsBoundingBox(zoneValue, box);
|
||||
}
|
||||
else {
|
||||
match = polygonOverlap(box, zoneValue);
|
||||
match = polygonIntersectsBoundingBox(zoneValue, box);
|
||||
}
|
||||
|
||||
const classes = zoneInfo?.classes?.length ? zoneInfo?.classes : this.model?.classes || [];
|
||||
@@ -604,7 +601,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
// prevents errant motion from the on screen time changing every second.
|
||||
if (this.hasMotionType && included === undefined) {
|
||||
const defaultInclusionZone: ClipPath = [[0, .1], [1, .1], [1, .9], [0, .9]];
|
||||
included = polygonOverlap(box, defaultInclusionZone);
|
||||
included = polygonIntersectsBoundingBox(defaultInclusionZone, box);
|
||||
}
|
||||
|
||||
// if there are inclusion zones and this object
|
||||
@@ -868,8 +865,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
if (value && this.model.settings?.find(s => s.key === key)?.multiple) {
|
||||
vs = JSON.stringify(value);
|
||||
if (value) {
|
||||
const found = this.model.settings?.find(s => s.key === key);
|
||||
if (found?.multiple || found?.type === 'clippath')
|
||||
vs = JSON.stringify(value);
|
||||
}
|
||||
|
||||
if (key === 'analyzeButton') {
|
||||
|
||||
@@ -1,17 +1,99 @@
|
||||
import type { ClipPath, Point } from '@scrypted/sdk';
|
||||
import polygonClipping from 'polygon-clipping';
|
||||
|
||||
// const polygonOverlap = require('polygon-overlap');
|
||||
// const insidePolygon = require('point-inside-polygon');
|
||||
// x y w h
|
||||
export type BoundingBox = [number, number, number, number];
|
||||
/**
|
||||
* Checks if a line segment intersects with another line segment
|
||||
*/
|
||||
function lineIntersects(
|
||||
[x1, y1]: Point,
|
||||
[x2, y2]: Point,
|
||||
[x3, y3]: Point,
|
||||
[x4, y4]: Point
|
||||
): boolean {
|
||||
// Calculate the denominators for intersection check
|
||||
const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
|
||||
if (denom === 0) return false; // Lines are parallel
|
||||
|
||||
export function polygonOverlap(p1: Point[], p2: Point[]) {
|
||||
const intersect = polygonClipping.intersection([p1], [p2]);
|
||||
return !!intersect.length;
|
||||
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom;
|
||||
const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom;
|
||||
|
||||
// Check if intersection point lies within both line segments
|
||||
return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1;
|
||||
}
|
||||
|
||||
export function insidePolygon(point: Point, polygon: Point[]) {
|
||||
const intersect = polygonClipping.intersection([polygon], [[point, [point[0] + 1, point[1]], [point[0] + 1, point[1] + 1]]]);
|
||||
return !!intersect.length;
|
||||
/**
|
||||
* Checks if a point is inside a polygon using ray casting algorithm
|
||||
*/
|
||||
function pointInPolygon([x, y]: Point, polygon: ClipPath): boolean {
|
||||
let inside = false;
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
const [xi, yi] = polygon[i];
|
||||
const [xj, yj] = polygon[j];
|
||||
|
||||
const intersect = ((yi > y) !== (yj > y)) &&
|
||||
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
|
||||
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a bounding box to an array of its corner points
|
||||
*/
|
||||
function boundingBoxToPoints([x, y, w, h]: BoundingBox): Point[] {
|
||||
return [
|
||||
[x, y], // top-left
|
||||
[x + w, y], // top-right
|
||||
[x + w, y + h], // bottom-right
|
||||
[x, y + h] // bottom-left
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a polygon intersects with a bounding box
|
||||
*/
|
||||
export function polygonIntersectsBoundingBox(polygon: ClipPath, boundingBox: BoundingBox): boolean {
|
||||
// Get bounding box corners
|
||||
const boxPoints = boundingBoxToPoints(boundingBox);
|
||||
|
||||
// Check if any polygon edge intersects with any bounding box edge
|
||||
for (let i = 0; i < polygon.length; i++) {
|
||||
const nextI = (i + 1) % polygon.length;
|
||||
const polygonPoint1 = polygon[i];
|
||||
const polygonPoint2 = polygon[nextI];
|
||||
|
||||
// Check against all bounding box edges
|
||||
for (let j = 0; j < boxPoints.length; j++) {
|
||||
const nextJ = (j + 1) % boxPoints.length;
|
||||
const boxPoint1 = boxPoints[j];
|
||||
const boxPoint2 = boxPoints[nextJ];
|
||||
|
||||
if (lineIntersects(polygonPoint1, polygonPoint2, boxPoint1, boxPoint2)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no edges intersect, check if either shape contains a point from the other
|
||||
if (pointInPolygon(polygon[0], boxPoints) || pointInPolygon(boxPoints[0], polygon))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a polygon completely contains a bounding box
|
||||
*/
|
||||
export function polygonContainsBoundingBox(polygon: ClipPath, boundingBox: BoundingBox): boolean {
|
||||
// Check if all corners of the bounding box are inside the polygon
|
||||
const boxPoints = boundingBoxToPoints(boundingBox);
|
||||
return boxPoints.every(point => pointInPolygon(point, polygon));
|
||||
}
|
||||
|
||||
|
||||
export function normalizeBox(box: BoundingBox, dims: Point): BoundingBox {
|
||||
return [box[0] / dims[0], box[1] / dims[1], box[2] / dims[0], box[3] / dims[1]];
|
||||
}
|
||||
|
||||
export function fixLegacyClipPath(clipPath: ClipPath): ClipPath {
|
||||
@@ -34,26 +116,3 @@ export function fixLegacyClipPath(clipPath: ClipPath): ClipPath {
|
||||
|
||||
return clipPath.map(p => p.map(c => c / 100)) as ClipPath;
|
||||
}
|
||||
|
||||
export function normalizeBoxToClipPath(boundingBox: [number, number, number, number], inputDimensions: [number, number]): [Point, Point, Point, Point] {
|
||||
let [x, y, width, height] = boundingBox;
|
||||
let x2 = x + width;
|
||||
let y2 = y + height;
|
||||
// the zones are point paths in percentage format
|
||||
x = x / inputDimensions[0];
|
||||
y = y / inputDimensions[1];
|
||||
x2 = x2 / inputDimensions[0];
|
||||
y2 = y2 / inputDimensions[1];
|
||||
return [[x, y], [x2, y], [x2, y2], [x, y2]];
|
||||
}
|
||||
|
||||
export function polygonArea(p: Point[]): number {
|
||||
let area = 0;
|
||||
const n = p.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const j = (i + 1) % n;
|
||||
area += p[i][0] * p[j][1];
|
||||
area -= p[j][0] * p[i][1];
|
||||
}
|
||||
return Math.abs(area / 2);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
|
||||
this.storageSettings.settings.detections.onGet = async () => {
|
||||
const objectDetector: ObjectDetector = this.storageSettings.values.objectDetector;
|
||||
const choices = (await objectDetector?.getObjectTypes())?.classes || [];
|
||||
const choices = (await objectDetector?.getObjectTypes?.())?.classes || [];
|
||||
return {
|
||||
hide: !objectDetector,
|
||||
choices,
|
||||
|
||||
@@ -2,7 +2,7 @@ import sdk, { Camera, ClipPath, EventListenerRegister, Image, ObjectDetection, O
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { levenshteinDistance } from "./edit-distance";
|
||||
import type { ObjectDetectionPlugin } from "./main";
|
||||
import { normalizeBoxToClipPath, polygonOverlap } from "./polygon";
|
||||
import { normalizeBox, polygonIntersectsBoundingBox } from "./polygon";
|
||||
|
||||
export const SMART_OCCUPANCYSENSOR_PREFIX = 'smart-occupancysensor-';
|
||||
|
||||
@@ -150,8 +150,8 @@ export class SmartOccupancySensor extends ScryptedDeviceBase implements Settings
|
||||
if (zone?.length >= 3) {
|
||||
if (!d.boundingBox)
|
||||
return false;
|
||||
const detectionBoxPath = normalizeBoxToClipPath(d.boundingBox, detected.inputDimensions);
|
||||
if (!polygonOverlap(detectionBoxPath, zone))
|
||||
const detectionBox = normalizeBox(d.boundingBox, detected.inputDimensions);
|
||||
if (!polygonIntersectsBoundingBox(zone, detectionBox))
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
4
plugins/openvino/package-lock.json
generated
4
plugins/openvino/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.148",
|
||||
"version": "0.1.153",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.148",
|
||||
"version": "0.1.153",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -48,5 +48,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.148"
|
||||
"version": "0.1.153"
|
||||
}
|
||||
|
||||
@@ -25,8 +25,12 @@ try:
|
||||
except:
|
||||
OpenVINOTextRecognition = None
|
||||
|
||||
predictExecutor = concurrent.futures.ThreadPoolExecutor(thread_name_prefix="OpenVINO-Predict")
|
||||
prepareExecutor = concurrent.futures.ThreadPoolExecutor(thread_name_prefix="OpenVINO-Prepare")
|
||||
predictExecutor = concurrent.futures.ThreadPoolExecutor(
|
||||
thread_name_prefix="OpenVINO-Predict"
|
||||
)
|
||||
prepareExecutor = concurrent.futures.ThreadPoolExecutor(
|
||||
thread_name_prefix="OpenVINO-Prepare"
|
||||
)
|
||||
|
||||
availableModels = [
|
||||
"Default",
|
||||
@@ -132,7 +136,7 @@ class OpenVINOPlugin(
|
||||
gpu = True
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# AUTO mode can cause conflicts or hide errors with NPU and GPU
|
||||
# so try to be explicit and fall back accordingly.
|
||||
mode = self.storage.getItem("mode") or "Default"
|
||||
@@ -140,7 +144,7 @@ class OpenVINOPlugin(
|
||||
mode = "AUTO"
|
||||
|
||||
if npu:
|
||||
mode = 'NPU'
|
||||
mode = "NPU"
|
||||
elif len(dgpus):
|
||||
mode = f"AUTO:{','.join(dgpus)},CPU"
|
||||
# forcing GPU can cause crashes on older GPU.
|
||||
@@ -242,12 +246,42 @@ class OpenVINOPlugin(
|
||||
self.requestRestart()
|
||||
|
||||
self.infer_queue = ov.AsyncInferQueue(self.compiled_model)
|
||||
|
||||
def predict(output):
|
||||
if not self.yolo:
|
||||
objs = []
|
||||
for values in output[0][0]:
|
||||
valid, index, confidence, l, t, r, b = values
|
||||
if valid == -1:
|
||||
break
|
||||
|
||||
def torelative(value: float):
|
||||
return value * self.model_dim
|
||||
|
||||
l = torelative(l)
|
||||
t = torelative(t)
|
||||
r = torelative(r)
|
||||
b = torelative(b)
|
||||
|
||||
obj = Prediction(index - 1, confidence, Rectangle(l, t, r, b))
|
||||
objs.append(obj)
|
||||
|
||||
return objs
|
||||
|
||||
if self.scrypted_yolov10:
|
||||
return yolo.parse_yolov10(output[0])
|
||||
if self.scrypted_yolo_nas:
|
||||
return yolo.parse_yolo_nas([output[1], output[0]])
|
||||
return yolo.parse_yolov9(output[0])
|
||||
|
||||
def callback(infer_request, future: asyncio.Future):
|
||||
try:
|
||||
output = infer_request.get_output_tensor(0)
|
||||
self.loop.call_soon_threadsafe(future.set_result, output)
|
||||
output = infer_request.get_output_tensor(0).data
|
||||
objs = predict(output)
|
||||
self.loop.call_soon_threadsafe(future.set_result, objs)
|
||||
except Exception as e:
|
||||
self.loop.call_soon_threadsafe(future.set_exception, e)
|
||||
|
||||
self.infer_queue.set_callback(callback)
|
||||
|
||||
print(
|
||||
@@ -323,39 +357,6 @@ class OpenVINOPlugin(
|
||||
return super().get_input_format()
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
async def predict(input_tensor):
|
||||
f = asyncio.Future(loop = self.loop)
|
||||
self.infer_queue.start_async(input_tensor, f)
|
||||
|
||||
output_tensors = await f
|
||||
|
||||
if not self.yolo:
|
||||
output = output_tensors
|
||||
for values in output.data[0][0]:
|
||||
valid, index, confidence, l, t, r, b = values
|
||||
if valid == -1:
|
||||
break
|
||||
|
||||
def torelative(value: float):
|
||||
return value * self.model_dim
|
||||
|
||||
l = torelative(l)
|
||||
t = torelative(t)
|
||||
r = torelative(r)
|
||||
b = torelative(b)
|
||||
|
||||
obj = Prediction(index - 1, confidence, Rectangle(l, t, r, b))
|
||||
objs.append(obj)
|
||||
|
||||
return objs
|
||||
|
||||
output = output_tensors.data
|
||||
if self.scrypted_yolov10:
|
||||
return yolo.parse_yolov10(output[0])
|
||||
if self.scrypted_yolo_nas:
|
||||
return yolo.parse_yolo_nas([output[1], output[0]])
|
||||
return yolo.parse_yolov9(output[0])
|
||||
|
||||
def prepare():
|
||||
# the input_tensor can be created with the shared_memory=True parameter,
|
||||
# but that seems to cause issues on some platforms.
|
||||
@@ -376,21 +377,19 @@ class OpenVINOPlugin(
|
||||
im = im.reshape((1, 3, self.model_dim, self.model_dim))
|
||||
im = im.astype(np.float32) / 255.0
|
||||
im = np.ascontiguousarray(im) # contiguous
|
||||
input_tensor = ov.Tensor(array=im)
|
||||
elif self.yolo:
|
||||
input_tensor = ov.Tensor(
|
||||
array=np.expand_dims(np.array(input), axis=0).astype(np.float32)
|
||||
)
|
||||
im = np.expand_dims(np.array(input), axis=0).astype(np.float32)
|
||||
else:
|
||||
input_tensor = ov.Tensor(array=np.expand_dims(np.array(input), axis=0))
|
||||
return input_tensor
|
||||
im = np.expand_dims(np.array(input), axis=0)
|
||||
return im
|
||||
|
||||
try:
|
||||
input_tensor = await asyncio.get_event_loop().run_in_executor(
|
||||
prepareExecutor, lambda: prepare()
|
||||
)
|
||||
objs = await predict(input_tensor)
|
||||
|
||||
f = asyncio.Future(loop=self.loop)
|
||||
self.infer_queue.start_async(input_tensor, f)
|
||||
objs = await f
|
||||
except:
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
@@ -17,7 +17,3 @@ Medium: 720p (500 Kbps)
|
||||
Low (if available): 320p (100 Kbps)
|
||||
|
||||
The `Key Frame (IDR) Interval` should be set to `4` seconds. This setting is usually configured in frames. So if the camera frame rate is `30`, the interval would be `120`. If the camera frame rate is `15` the interval would be `60`. The value can be calculated as `IDR Interval = FPS * 4`.
|
||||
|
||||
## Transcoding
|
||||
|
||||
Some cameras may not allow configuration of the video codec (h264) or IDR Interval. The camera may also only have a single high bitrate stream which will fail to stream when viewing on low bandwidth remote connections. In this case, Transcoding should be enabled for `Remote Stream` and `Remote Recording Stream` to ensure there isn't a bandwidth issue.
|
||||
|
||||
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.10.39",
|
||||
"version": "0.10.43",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.39",
|
||||
"version": "0.10.43",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.39",
|
||||
"version": "0.10.43",
|
||||
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
@@ -26,7 +26,7 @@
|
||||
"name": "Rebroadcast Plugin",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"DeviceProvider",
|
||||
"Settings",
|
||||
"MixinProvider",
|
||||
"BufferConverter"
|
||||
],
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
|
||||
import { getDebugModeH264EncoderArgs, getH264EncoderArgs } from '@scrypted/common/src/ffmpeg-hardware-acceleration';
|
||||
import { addVideoFilterArguments } from '@scrypted/common/src/ffmpeg-helpers';
|
||||
import { ListenZeroSingleClientTimeoutError, closeQuiet, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
|
||||
import { readLength } from '@scrypted/common/src/read-stream';
|
||||
@@ -8,7 +7,7 @@ import { addTrackControls, getSpsPps, parseSdp } from '@scrypted/common/src/sdp-
|
||||
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import { StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser';
|
||||
import sdk, { BufferConverter, ChargeState, DeviceProvider, EventListenerRegister, FFmpegInput, ForkWorker, H264Info, MediaObject, MediaStreamDestination, MediaStreamOptions, MixinProvider, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, VideoCamera, VideoCameraConfiguration, WritableDeviceState } from '@scrypted/sdk';
|
||||
import sdk, { BufferConverter, ChargeState, EventListenerRegister, FFmpegInput, ForkWorker, H264Info, MediaObject, MediaStreamDestination, MediaStreamOptions, MixinProvider, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, VideoCamera, VideoCameraConfiguration, WritableDeviceState } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import crypto from 'crypto';
|
||||
import { once } from 'events';
|
||||
@@ -24,7 +23,6 @@ import { connectRFC4571Parser, startRFC4571Parser } from './rfc4571';
|
||||
import { RtspSessionParserSpecific, startRtspSession } from './rtsp-session';
|
||||
import { getSpsResolution } from './sps-resolution';
|
||||
import { createStreamSettings } from './stream-settings';
|
||||
import { TRANSCODE_MIXIN_PROVIDER_NATIVE_ID, TranscodeMixinProvider, getTranscodeMixinProviderId } from './transcode-settings';
|
||||
|
||||
const { mediaManager, log, systemManager, deviceManager } = sdk;
|
||||
|
||||
@@ -73,7 +71,7 @@ class PrebufferSession {
|
||||
|
||||
activeClients = 0;
|
||||
inactivityTimeout: NodeJS.Timeout;
|
||||
audioConfigurationKey: string;
|
||||
syntheticInputIdKey: string;
|
||||
ffmpegInputArgumentsKey: string;
|
||||
ffmpegOutputArgumentsKey: string;
|
||||
lastDetectedAudioCodecKey: string;
|
||||
@@ -89,7 +87,7 @@ class PrebufferSession {
|
||||
this.storage = mixin.storage;
|
||||
this.console = mixin.console;
|
||||
this.mixinDevice = mixin.mixinDevice;
|
||||
this.audioConfigurationKey = 'audioConfiguration-' + this.streamId;
|
||||
this.syntheticInputIdKey = 'syntheticInputIdKey-' + this.streamId;
|
||||
this.ffmpegInputArgumentsKey = 'ffmpegInputArguments-' + this.streamId;
|
||||
this.ffmpegOutputArgumentsKey = 'ffmpegOutputArguments-' + this.streamId;
|
||||
this.lastDetectedAudioCodecKey = 'lastDetectedAudioCodec-' + this.streamId;
|
||||
@@ -117,10 +115,6 @@ class PrebufferSession {
|
||||
return !this.enabled || this.shouldDisableBatteryPrebuffer();
|
||||
}
|
||||
|
||||
get canPrebuffer() {
|
||||
return (this.advertisedMediaStreamOptions.container !== 'rawvideo' && this.advertisedMediaStreamOptions.container !== 'ffmpeg') || this.storage.getItem(this.ffmpegOutputArgumentsKey);
|
||||
}
|
||||
|
||||
getLastH264Probe(): H264Info {
|
||||
const str = this.storage.getItem(this.lastH264ProbeKey);
|
||||
if (!str) {
|
||||
@@ -230,12 +224,20 @@ class PrebufferSession {
|
||||
|
||||
getParser(mediaStreamOptions: MediaStreamOptions) {
|
||||
let parser: string;
|
||||
const rtspParser = this.storage.getItem(this.rtspParserKey);
|
||||
let rtspParser = this.storage.getItem(this.rtspParserKey);
|
||||
|
||||
let isDefault = !rtspParser || rtspParser === 'Default';
|
||||
|
||||
if (!this.canUseRtspParser(mediaStreamOptions)) {
|
||||
parser = STRING_DEFAULT;
|
||||
isDefault = true;
|
||||
rtspParser = undefined;
|
||||
}
|
||||
else {
|
||||
if (isDefault) {
|
||||
// use the plugin default
|
||||
rtspParser = localStorage.getItem('defaultRtspParser');
|
||||
}
|
||||
switch (rtspParser) {
|
||||
case FFMPEG_PARSER_TCP:
|
||||
case FFMPEG_PARSER_UDP:
|
||||
@@ -251,7 +253,7 @@ class PrebufferSession {
|
||||
|
||||
return {
|
||||
parser,
|
||||
isDefault: !rtspParser || rtspParser === 'Default',
|
||||
isDefault,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,6 +328,19 @@ class PrebufferSession {
|
||||
const group = "Streams";
|
||||
const subgroup = `Stream: ${this.streamName}`;
|
||||
|
||||
if (this.mixin.streamSettings.storageSettings.values.synthenticStreams.includes(this.streamId)) {
|
||||
const nonSynthetic = [...this.mixin.sessions.keys()].filter(s => s && !s.startsWith('synthetic:'));
|
||||
settings.push({
|
||||
group,
|
||||
subgroup,
|
||||
key: this.syntheticInputIdKey,
|
||||
title: 'Synthetic Stream Source',
|
||||
description: 'The source stream to transcode.',
|
||||
choices: nonSynthetic,
|
||||
value: this.storage.getItem(this.syntheticInputIdKey),
|
||||
});
|
||||
}
|
||||
|
||||
const addFFmpegInputSettings = () => {
|
||||
settings.push(
|
||||
{
|
||||
@@ -351,7 +366,7 @@ class PrebufferSession {
|
||||
key: this.ffmpegOutputArgumentsKey,
|
||||
value: this.storage.getItem(this.ffmpegOutputArgumentsKey),
|
||||
choices: [
|
||||
'-c:v libx264 -pix_fmt yuvj420p -preset ultrafast -bf 0'
|
||||
'-c:v libx264 -pix_fmt yuvj420p -preset ultrafast -bf 0 -g 60 -r 15 -b:v 500000 -bufsize 1000000 -maxrate 500000'
|
||||
],
|
||||
combobox: true,
|
||||
},
|
||||
@@ -364,11 +379,6 @@ class PrebufferSession {
|
||||
const parser = this.getParser(this.advertisedMediaStreamOptions);
|
||||
const defaultValue = parser.parser;
|
||||
|
||||
const scryptedOptions = [
|
||||
SCRYPTED_PARSER_TCP,
|
||||
SCRYPTED_PARSER_UDP,
|
||||
];
|
||||
|
||||
const currentParser = parser.isDefault ? STRING_DEFAULT : parser.parser;
|
||||
|
||||
settings.push(
|
||||
@@ -381,7 +391,8 @@ class PrebufferSession {
|
||||
value: currentParser,
|
||||
choices: [
|
||||
STRING_DEFAULT,
|
||||
...scryptedOptions,
|
||||
SCRYPTED_PARSER_TCP,
|
||||
SCRYPTED_PARSER_UDP,
|
||||
FFMPEG_PARSER_TCP,
|
||||
FFMPEG_PARSER_UDP,
|
||||
],
|
||||
@@ -518,7 +529,19 @@ class PrebufferSession {
|
||||
};
|
||||
this.parsers = rbo.parsers;
|
||||
|
||||
const mo = await this.mixinDevice.getVideoStream(mso);
|
||||
let mo: MediaObject;
|
||||
if (this.mixin.streamSettings.storageSettings.values.synthenticStreams.includes(this.streamId)) {
|
||||
const syntheticInputId = this.storage.getItem(this.syntheticInputIdKey);
|
||||
if (!syntheticInputId)
|
||||
throw new Error('synthetic stream has not been configured with an input');
|
||||
const realDevice = systemManager.getDeviceById<VideoCamera>(this.mixin.id);
|
||||
mo = await realDevice.getVideoStream({
|
||||
id: syntheticInputId,
|
||||
});
|
||||
}
|
||||
else {
|
||||
mo = await this.mixinDevice.getVideoStream(mso);
|
||||
}
|
||||
const isRfc4571 = mo.mimeType === 'x-scrypted/x-rfc4571';
|
||||
|
||||
let session: ParserSession<PrebufferParsers>;
|
||||
@@ -1282,9 +1305,6 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
}
|
||||
|
||||
async getVideoStream(options?: RequestMediaStreamOptions): Promise<MediaObject> {
|
||||
if (options?.route === 'direct')
|
||||
return this.mixinDevice.getVideoStream(options);
|
||||
|
||||
await this.ensurePrebufferSessions();
|
||||
|
||||
let id = options?.id;
|
||||
@@ -1294,8 +1314,6 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
let videoFilterArguments: string;
|
||||
let destinationVideoBitrate: number;
|
||||
|
||||
const transcodingEnabled = this.mixins?.includes(getTranscodeMixinProviderId());
|
||||
|
||||
const msos = await this.mixinDevice.getVideoStreamOptions();
|
||||
let result: {
|
||||
stream: ResponseMediaStreamOptions,
|
||||
@@ -1355,58 +1373,23 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
}
|
||||
|
||||
id = result.stream.id;
|
||||
// this.console.log('Selected stream', result.stream.name);
|
||||
// transcoding video should never happen transparently since it is CPU intensive.
|
||||
// encourage users at every step to configure proper codecs.
|
||||
// for this reason, do not automatically supply h264 encoder arguments
|
||||
// even if h264 is requested, to force a visible failure.
|
||||
if (transcodingEnabled && this.streamSettings.storageSettings.values.transcodeStreams?.includes(result.title)) {
|
||||
h264EncoderArguments = transcodeStorageSettings.h264EncoderArguments?.split(' ');
|
||||
if (this.streamSettings.storageSettings.values.videoFilterArguments)
|
||||
videoFilterArguments = this.streamSettings.storageSettings.values.videoFilterArguments;
|
||||
}
|
||||
}
|
||||
|
||||
let session = this.sessions.get(id);
|
||||
let ffmpegInput: FFmpegInput;
|
||||
if (!session.canPrebuffer) {
|
||||
this.console.log('Source container can not be prebuffered. Using a direct media stream.');
|
||||
session = undefined;
|
||||
}
|
||||
if (!session) {
|
||||
const mo = await this.mixinDevice.getVideoStream(options);
|
||||
if (!transcodingEnabled)
|
||||
return mo;
|
||||
ffmpegInput = await mediaManager.convertMediaObjectToJSON(mo, ScryptedMimeTypes.FFmpegInput);
|
||||
}
|
||||
else {
|
||||
// ffmpeg probing works better if the stream does NOT start on a sync frame. the pre-sps/pps data is used
|
||||
// as part of the stream analysis, and sync frame is immediately used. otherwise the sync frame is
|
||||
// read and tossed during rtsp analysis.
|
||||
// if ffmpeg is not in used (ie, not transcoding or implicitly rtsp),
|
||||
// trust that downstream is not using ffmpeg and start with a sync frame.
|
||||
const findSyncFrame = !transcodingEnabled
|
||||
&& (!options?.container || options?.container === 'rtsp')
|
||||
&& options?.tool !== 'ffmpeg';
|
||||
ffmpegInput = await session.getVideoStream(findSyncFrame, options);
|
||||
}
|
||||
if (!session)
|
||||
throw new Error('stream not found');
|
||||
|
||||
ffmpegInput = await session.getVideoStream(true, options);
|
||||
|
||||
ffmpegInput.h264EncoderArguments = h264EncoderArguments;
|
||||
ffmpegInput.destinationVideoBitrate = destinationVideoBitrate;
|
||||
|
||||
if (transcodingEnabled && this.streamSettings.storageSettings.values.missingCodecParameters) {
|
||||
if (!ffmpegInput.mediaStreamOptions)
|
||||
ffmpegInput.mediaStreamOptions = { id };
|
||||
ffmpegInput.mediaStreamOptions.oobCodecParameters = true;
|
||||
}
|
||||
|
||||
if (ffmpegInput.h264FilterArguments && videoFilterArguments)
|
||||
addVideoFilterArguments(ffmpegInput.h264FilterArguments, videoFilterArguments)
|
||||
else if (videoFilterArguments)
|
||||
ffmpegInput.h264FilterArguments = ['-filter_complex', videoFilterArguments];
|
||||
|
||||
if (transcodingEnabled)
|
||||
ffmpegInput.videoDecoderArguments = this.streamSettings.storageSettings.values.videoDecoderArguments?.split(' ');
|
||||
return mediaManager.createFFmpegMediaObject(ffmpegInput, {
|
||||
sourceId: this.id,
|
||||
});
|
||||
@@ -1489,6 +1472,22 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
})();
|
||||
}
|
||||
|
||||
for (const synthetic of this.streamSettings.storageSettings.values.synthenticStreams) {
|
||||
const id = `synthetic:${synthetic}`;
|
||||
toRemove.delete(id);
|
||||
|
||||
let session = this.sessions.get(id);
|
||||
|
||||
if (session)
|
||||
continue;
|
||||
|
||||
session = new PrebufferSession(this, {
|
||||
id: synthetic,
|
||||
}, false, false);
|
||||
this.sessions.set(id, session);
|
||||
this.console.log('stream', synthetic, 'is synthetic and will be rebroadcast on demand.');
|
||||
}
|
||||
|
||||
if (!this.sessions.has(undefined)) {
|
||||
const defaultStreamName = this.streamSettings.storageSettings.values.defaultStream;
|
||||
let defaultSession = this.sessions.get(msos?.find(mso => mso.name === defaultStreamName)?.id);
|
||||
@@ -1617,30 +1616,39 @@ function millisUntilMidnight() {
|
||||
return (midnight.getTime() - new Date().getTime());
|
||||
}
|
||||
|
||||
export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinProvider, BufferConverter, Settings, DeviceProvider {
|
||||
export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinProvider, BufferConverter, Settings, Settings {
|
||||
// no longer in use, but kept for future use.
|
||||
storageSettings = new StorageSettings(this, {});
|
||||
storageSettings = new StorageSettings(this, {
|
||||
defaultRtspParser: {
|
||||
group: 'Advanced',
|
||||
title: 'Default RTSP Parser',
|
||||
description: `Experimental: The Default parser used to read RTSP streams. The default is "${SCRYPTED_PARSER_TCP}".`,
|
||||
defaultValue: STRING_DEFAULT,
|
||||
choices: [
|
||||
STRING_DEFAULT,
|
||||
SCRYPTED_PARSER_TCP,
|
||||
SCRYPTED_PARSER_UDP,
|
||||
FFMPEG_PARSER_TCP,
|
||||
FFMPEG_PARSER_UDP,
|
||||
],
|
||||
onPut: () => {
|
||||
this.log.a('Rebroadcast Plugin will restart momentarily.');
|
||||
sdk.deviceManager.requestRestart();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
transcodeStorageSettings = new StorageSettings(this, {
|
||||
remoteStreamingBitrate: {
|
||||
group: 'Advanced',
|
||||
title: 'Remote Streaming Bitrate',
|
||||
type: 'number',
|
||||
defaultValue: 1000000,
|
||||
defaultValue: 500000,
|
||||
description: 'The bitrate to use when remote streaming. This setting will only be used when transcoding or adaptive bitrate is enabled on a camera.',
|
||||
onPut() {
|
||||
sdk.deviceManager.onDeviceEvent('transcode', ScryptedInterface.Settings, undefined);
|
||||
},
|
||||
},
|
||||
h264EncoderArguments: {
|
||||
title: 'H264 Encoder Arguments',
|
||||
description: 'FFmpeg arguments used to encode h264 video. This is not camera specific and is used to setup the hardware accelerated encoder on your Scrypted server. This setting will only be used when transcoding is enabled on a camera.',
|
||||
choices: Object.keys(getH264EncoderArgs()),
|
||||
defaultValue: getDebugModeH264EncoderArgs().join(' '),
|
||||
combobox: true,
|
||||
mapPut: (oldValue, newValue) => getH264EncoderArgs()[newValue]?.join(' ') || newValue || getDebugModeH264EncoderArgs().join(' '),
|
||||
onPut() {
|
||||
sdk.deviceManager.onDeviceEvent('transcode', ScryptedInterface.Settings, undefined);
|
||||
},
|
||||
}
|
||||
});
|
||||
currentMixins = new Map<PrebufferMixin, {
|
||||
worker: ForkWorker,
|
||||
@@ -1650,6 +1658,8 @@ export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinP
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
|
||||
this.log.clearAlerts();
|
||||
|
||||
this.fromMimeType = 'x-scrypted/x-rfc4571';
|
||||
this.toMimeType = ScryptedMimeTypes.FFmpegInput;
|
||||
|
||||
@@ -1669,40 +1679,24 @@ export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinP
|
||||
}
|
||||
});
|
||||
|
||||
// schedule restarts at 2am
|
||||
// removed as the mp4 containerization leak used way back when is defunct.
|
||||
// const midnight = millisUntilMidnight();
|
||||
// const twoAM = midnight + 2 * 60 * 60 * 1000;
|
||||
// this.log.i(`Rebroadcaster scheduled for restart at 2AM: ${Math.round(twoAM / 1000 / 60)} minutes`)
|
||||
// setTimeout(() => deviceManager.requestRestart(), twoAM);
|
||||
|
||||
process.nextTick(() => {
|
||||
deviceManager.onDeviceDiscovered({
|
||||
nativeId: TRANSCODE_MIXIN_PROVIDER_NATIVE_ID,
|
||||
name: 'Transcoding',
|
||||
interfaces: [
|
||||
"SystemSettings",
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.MixinProvider,
|
||||
],
|
||||
type: ScryptedDeviceType.API,
|
||||
// legacy transcode extension that needs to be removed.
|
||||
if (sdk.deviceManager.getNativeIds().includes('transcode')) {
|
||||
process.nextTick(() => {
|
||||
deviceManager.onDeviceRemoved('transcode');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) {
|
||||
if (nativeId === TRANSCODE_MIXIN_PROVIDER_NATIVE_ID)
|
||||
return new TranscodeMixinProvider(this);
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
...await this.storageSettings.getSettings(),
|
||||
...await this.transcodeStorageSettings.getSettings(),
|
||||
];
|
||||
}
|
||||
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
if (this.transcodeStorageSettings.keys[key])
|
||||
return this.transcodeStorageSettings.putSetting(key, value);
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getH264DecoderArgs } from "@scrypted/common/src/ffmpeg-hardware-acceleration";
|
||||
import { StorageSetting, StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { MixinDeviceBase, ResponseMediaStreamOptions, VideoCamera } from "@scrypted/sdk";
|
||||
import { getTranscodeMixinProviderId } from "./transcode-settings";
|
||||
import { StorageSetting, StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
|
||||
export type StreamStorageSetting = StorageSetting & {
|
||||
prefersPrebuffer: boolean,
|
||||
@@ -102,45 +101,16 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
type: 'number',
|
||||
hide: false,
|
||||
},
|
||||
transcodeStreams: {
|
||||
group: 'Transcoding',
|
||||
title: 'Transcode Streams',
|
||||
description: 'The media streams to transcode. Transcoding audio and video is not recommended and should only be used when necessary. The Rebroadcast Plugin manages the system-wide Transcode settings. See the Rebroadcast Readme for optimal configuration.',
|
||||
synthenticStreams: {
|
||||
subgroup,
|
||||
title: 'Synthetic Streams',
|
||||
description: 'Create additional streams by transcoding the existing streams. This can be useful for creating streams with different resolutions or bitrates.',
|
||||
immediate: true,
|
||||
multiple: true,
|
||||
choices: Object.values(streamTypes).map(st => st.title),
|
||||
hide: true,
|
||||
},
|
||||
videoDecoderArguments: {
|
||||
group: 'Transcoding',
|
||||
title: 'Video Decoder Arguments',
|
||||
description: 'FFmpeg arguments used to decode input video when transcoding a stream.',
|
||||
placeholder: '-hwaccel auto',
|
||||
choices: Object.keys(getH264DecoderArgs()),
|
||||
combobox: true,
|
||||
mapPut: (oldValue, newValue) => getH264DecoderArgs()[newValue]?.join(' ') || newValue || '',
|
||||
hide: true,
|
||||
},
|
||||
videoFilterArguments: {
|
||||
group: 'Transcoding',
|
||||
title: 'Video Filter Arguments',
|
||||
description: 'FFmpeg arguments used to filter input video when transcoding a stream. This can be used to crops, scale, rotates, etc.',
|
||||
placeholder: 'transpose=1',
|
||||
hide: true,
|
||||
},
|
||||
// 3/6/2022
|
||||
// Ran into an issue where the RTSP source had SPS/PPS in the SDP,
|
||||
// and none in the bitstream. Codec copy will not add SPS/PPS before IDR frames
|
||||
// unless this flag is used.
|
||||
// 3/7/2022
|
||||
// This flag was enabled by default, but I believe this is causing issues with some users.
|
||||
// Make it a setting.
|
||||
missingCodecParameters: {
|
||||
group: 'Transcoding',
|
||||
title: 'Out of Band Codec Parameters',
|
||||
description: 'Some cameras do not include H264 codec parameters in the stream and this causes live streaming to always fail (but recordings may be working). This is a inexpensive video filter and does not perform a transcode. Enable this setting only as necessary.',
|
||||
type: 'boolean',
|
||||
hide: true,
|
||||
},
|
||||
choices: [],
|
||||
defaultValue: [],
|
||||
}
|
||||
});
|
||||
|
||||
function getDefaultPrebufferedStreams(msos: ResponseMediaStreamOptions[]) {
|
||||
@@ -177,10 +147,18 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
const v: StreamStorageSetting = storageSettings.settings[key];
|
||||
const value = storageSettings.values[key];
|
||||
let isDefault = value === 'Default';
|
||||
|
||||
let stream = msos?.find(mso => mso.name === value);
|
||||
if (isDefault || !stream) {
|
||||
isDefault = true;
|
||||
stream = getDefaultMediaStream(v, msos);
|
||||
if (storageSettings.values.synthenticStreams.includes(value)) {
|
||||
stream = {
|
||||
id: `synthetic:${value}`,
|
||||
};
|
||||
}
|
||||
else {
|
||||
if (isDefault || !stream) {
|
||||
isDefault = true;
|
||||
stream = getDefaultMediaStream(v, msos);
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: streamTypes[key].title,
|
||||
@@ -193,6 +171,7 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
const choices = [
|
||||
'Default',
|
||||
...msos.map(mso => mso.name),
|
||||
...storageSettings.values.synthenticStreams,
|
||||
];
|
||||
const defaultValue = getDefaultMediaStream(v, msos).name;
|
||||
|
||||
@@ -209,16 +188,6 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
onGet: async () => {
|
||||
let enabledStreams: StorageSetting;
|
||||
|
||||
const hideTranscode = device.mixins?.includes(getTranscodeMixinProviderId()) ? {
|
||||
hide: false,
|
||||
} : {};
|
||||
const hideTranscodes = {
|
||||
transcodeStreams: hideTranscode,
|
||||
missingCodecParameters: hideTranscode,
|
||||
videoDecoderArguments: hideTranscode,
|
||||
videoFilterArguments: hideTranscode,
|
||||
};
|
||||
|
||||
try {
|
||||
const msos = await device.mixinDevice.getVideoStreamOptions();
|
||||
|
||||
@@ -236,13 +205,11 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
lowResolutionStream: createStreamOptions(streamTypes.lowResolutionStream, msos),
|
||||
recordingStream: createStreamOptions(streamTypes.recordingStream, msos),
|
||||
remoteRecordingStream: createStreamOptions(streamTypes.remoteRecordingStream, msos),
|
||||
...hideTranscodes,
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
enabledStreams,
|
||||
...hideTranscodes,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,7 +218,6 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
}
|
||||
|
||||
return {
|
||||
...hideTranscodes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import sdk, { MixinProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
||||
import { RebroadcastPlugin } from "./main";
|
||||
import { REBROADCAST_MIXIN_INTERFACE_TOKEN } from "./rebroadcast-mixin-token";
|
||||
const { deviceManager } = sdk;
|
||||
|
||||
export const TRANSCODE_MIXIN_PROVIDER_NATIVE_ID = 'transcode';
|
||||
|
||||
export function getTranscodeMixinProviderId() {
|
||||
if (!deviceManager.getNativeIds().includes(TRANSCODE_MIXIN_PROVIDER_NATIVE_ID))
|
||||
return;
|
||||
const transcodeMixin = deviceManager.getDeviceState(TRANSCODE_MIXIN_PROVIDER_NATIVE_ID);
|
||||
return transcodeMixin?.id;
|
||||
}
|
||||
|
||||
export class TranscodeMixinProvider extends ScryptedDeviceBase implements MixinProvider, Settings {
|
||||
constructor(public plugin: RebroadcastPlugin) {
|
||||
super(TRANSCODE_MIXIN_PROVIDER_NATIVE_ID);
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.plugin.transcodeStorageSettings.getSettings();
|
||||
}
|
||||
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.plugin.transcodeStorageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
|
||||
if (!interfaces.includes(REBROADCAST_MIXIN_INTERFACE_TOKEN))
|
||||
return;
|
||||
return [
|
||||
ScryptedInterface.Settings,
|
||||
];
|
||||
}
|
||||
|
||||
invalidateSettings(id: string) {
|
||||
process.nextTick(async () => {
|
||||
for (const [mixin, v] of this.plugin.currentMixins.entries()) {
|
||||
if (v.id === id)
|
||||
mixin?.onDeviceEvent(ScryptedInterface.Settings, undefined)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
|
||||
this.invalidateSettings(mixinDeviceState.id);
|
||||
return mixinDevice;
|
||||
}
|
||||
|
||||
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
|
||||
this.invalidateSettings(id);
|
||||
}
|
||||
}
|
||||
4
plugins/webrtc/package-lock.json
generated
4
plugins/webrtc/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.2.57",
|
||||
"version": "0.2.58",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.2.57",
|
||||
"version": "0.2.58",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.2.57",
|
||||
"version": "0.2.58",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
4
plugins/wyze/package-lock.json
generated
4
plugins/wyze/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/wyze",
|
||||
"version": "0.0.54",
|
||||
"version": "0.0.57",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/wyze",
|
||||
"version": "0.0.54",
|
||||
"version": "0.0.57",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -28,10 +28,15 @@
|
||||
],
|
||||
"pluginDependencies": [
|
||||
"@scrypted/prebuffer-mixin"
|
||||
]
|
||||
],
|
||||
"labels": {
|
||||
"require": [
|
||||
"linux"
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.54"
|
||||
"version": "0.0.57"
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ class WyzeCamera(scrypted_sdk.ScryptedDeviceBase, VideoCamera, Settings, PanTilt
|
||||
except:
|
||||
if default:
|
||||
return "Default"
|
||||
return 120 if self.camera.is_2k else 60
|
||||
return 240 if self.camera.is_2k else 160
|
||||
|
||||
async def getSettings(self):
|
||||
ret: List[Setting] = []
|
||||
@@ -152,8 +152,9 @@ class WyzeCamera(scrypted_sdk.ScryptedDeviceBase, VideoCamera, Settings, PanTilt
|
||||
"500",
|
||||
"750",
|
||||
"1000",
|
||||
"1200",
|
||||
"1400",
|
||||
"1800",
|
||||
"2000",
|
||||
],
|
||||
}
|
||||
)
|
||||
@@ -305,11 +306,12 @@ class WyzeCamera(scrypted_sdk.ScryptedDeviceBase, VideoCamera, Settings, PanTilt
|
||||
pkill(aprocess)
|
||||
|
||||
async def ensureServer(self, cb) -> int:
|
||||
server = await asyncio.start_server(cb, "127.0.0.1", 0)
|
||||
host = os.environ.get("SCRYPTED_CLUSTER_ADDRESS", None) or "127.0.0.1"
|
||||
server = await asyncio.start_server(cb, host, 0)
|
||||
sock = server.sockets[0]
|
||||
host, port = sock.getsockname()
|
||||
asyncio.ensure_future(server.serve_forever())
|
||||
return port
|
||||
return host, port
|
||||
|
||||
async def probeCodec(self, substream: bool):
|
||||
sps: bytes = None
|
||||
@@ -414,7 +416,7 @@ class WyzeCamera(scrypted_sdk.ScryptedDeviceBase, VideoCamera, Settings, PanTilt
|
||||
print_exception(self.print, e)
|
||||
raise
|
||||
|
||||
rfcPort = await self.rfcSubServer if substream else await self.rfcServer
|
||||
rfcHost, rfcPort = await self.rfcSubServer if substream else await self.rfcServer
|
||||
|
||||
msos = self.getVideoStreamOptionsInternal()
|
||||
mso = msos[1] if substream else msos[0]
|
||||
@@ -441,7 +443,7 @@ b=AS:128
|
||||
a=rtpmap:97 {audioCodecName}/{info.audioSampleRate}/1
|
||||
"""
|
||||
rfc = {
|
||||
"url": f"tcp://127.0.0.1:{rfcPort}",
|
||||
"url": f"tcp://{rfcHost}:{rfcPort}",
|
||||
"sdp": sdp,
|
||||
"mediaStreamOptions": mso,
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ const axios = require('axios').create({
|
||||
const process = require('process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
|
||||
|
||||
function getUserHome() {
|
||||
const ret = process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'];
|
||||
@@ -127,7 +125,7 @@ exports.deploy = function (debugHost, noRebind) {
|
||||
.catch((err) => {
|
||||
console.error(err.message);
|
||||
if (err.response && err.response.data) {
|
||||
console.log(chalk.red(err.response.data));
|
||||
console.log('\x1b[31m%s\x1b[0m', err.response.data);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
@@ -160,7 +158,7 @@ exports.debug = function (debugHost, entryPoint) {
|
||||
.catch((err) => {
|
||||
console.error(err.message);
|
||||
if (err.response && err.response.data) {
|
||||
console.log(chalk.red(err.response.data));
|
||||
console.log('\x1b[31m%s\x1b[0m', err.response.data);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
|
||||
4
sdk/package-lock.json
generated
4
sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.102",
|
||||
"version": "0.3.106",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.102",
|
||||
"version": "0.3.106",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.102",
|
||||
"version": "0.3.106",
|
||||
"description": "",
|
||||
"main": "dist/src/index.js",
|
||||
"exports": {
|
||||
|
||||
@@ -266,7 +266,7 @@ try {
|
||||
}
|
||||
|
||||
try {
|
||||
(systemManager as any).setScryptedInterfaceDescriptors?.(TYPES_VERSION, ScryptedInterfaceDescriptors)?.catch(() => { });
|
||||
(sdk.systemManager as any).setScryptedInterfaceDescriptors?.(TYPES_VERSION, ScryptedInterfaceDescriptors)?.catch(() => { });
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
4
sdk/types/package-lock.json
generated
4
sdk/types/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.94",
|
||||
"version": "0.3.98",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.94",
|
||||
"version": "0.3.98",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.94",
|
||||
"version": "0.3.98",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"author": "",
|
||||
|
||||
@@ -676,7 +676,7 @@ class NotifierOptions(TypedDict):
|
||||
requireInteraction: bool
|
||||
silent: bool
|
||||
subtitle: str
|
||||
tag: str
|
||||
tag: str # Collapse key/id.
|
||||
timestamp: float
|
||||
vibrate: VibratePattern
|
||||
|
||||
@@ -951,7 +951,7 @@ class TamperState(TypedDict):
|
||||
pass
|
||||
|
||||
|
||||
TYPES_VERSION = "0.3.94"
|
||||
TYPES_VERSION = "0.3.98"
|
||||
|
||||
|
||||
class AirPurifier:
|
||||
|
||||
@@ -234,6 +234,9 @@ export interface NotifierOptions {
|
||||
renotify?: boolean;
|
||||
requireInteraction?: boolean;
|
||||
silent?: boolean;
|
||||
/**
|
||||
* Collapse key/id.
|
||||
*/
|
||||
tag?: string;
|
||||
timestamp?: number;
|
||||
vibrate?: VibratePattern;
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.123.73",
|
||||
"version": "0.125.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.123.73",
|
||||
"version": "0.125.3",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.124.0",
|
||||
"version": "0.127.1",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@scrypted/ffmpeg-static": "^6.1.0-build3",
|
||||
|
||||
@@ -6,6 +6,7 @@ import inspect
|
||||
import multiprocessing
|
||||
import multiprocessing.connection
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import random
|
||||
import sys
|
||||
@@ -472,11 +473,39 @@ class MediaManager:
|
||||
) -> scrypted_python.scrypted_sdk.types.MediaObject:
|
||||
return await self.mediaManager.createMediaObjectFromUrl(data, options)
|
||||
|
||||
async def getFFmpegPath(self) -> str:
|
||||
async def getFFmpegPath(self):
|
||||
# try to get the ffmpeg path as a value of another variable
|
||||
# ie, in docker builds:
|
||||
# export SCRYPTED_FFMPEG_PATH_ENV_VARIABLE=SCRYPTED_RASPBIAN_FFMPEG_PATH
|
||||
v = os.getenv('SCRYPTED_FFMPEG_PATH_ENV_VARIABLE')
|
||||
if v:
|
||||
f = os.getenv(v)
|
||||
if f and Path(f).exists():
|
||||
return f
|
||||
|
||||
# try to get the ffmpeg path from a variable
|
||||
# ie:
|
||||
# export SCRYPTED_FFMPEG_PATH=/usr/local/bin/ffmpeg
|
||||
f = os.getenv('SCRYPTED_FFMPEG_PATH')
|
||||
if f and Path(f).exists():
|
||||
return f
|
||||
|
||||
return await self.mediaManager.getFFmpegPath()
|
||||
|
||||
async def getFilesPath(self) -> str:
|
||||
return await self.mediaManager.getFilesPath()
|
||||
async def getFilesPath(self):
|
||||
# Get the value of the SCRYPTED_PLUGIN_VOLUME environment variable
|
||||
files_path = os.getenv('SCRYPTED_PLUGIN_VOLUME')
|
||||
if not files_path:
|
||||
raise ValueError('SCRYPTED_PLUGIN_VOLUME env variable not set?')
|
||||
|
||||
# Construct the path for the 'files' directory
|
||||
ret = Path(files_path) / 'files'
|
||||
|
||||
# Ensure the directory exists
|
||||
await asyncio.to_thread(ret.mkdir, parents=True, exist_ok=True)
|
||||
|
||||
# Return the constructed directory path as a string
|
||||
return str(ret)
|
||||
|
||||
|
||||
class DeviceState(scrypted_python.scrypted_sdk.types.DeviceState):
|
||||
|
||||
37
server/src/plugin/ffmpeg-path.ts
Normal file
37
server/src/plugin/ffmpeg-path.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { getFfmpegPath } from '@scrypted/ffmpeg-static';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
|
||||
export async function getScryptedFFmpegPath(): Promise<string> {
|
||||
// try to get the ffmpeg path as a value of another variable
|
||||
// ie, in docker builds:
|
||||
// export SCRYPTED_FFMPEG_PATH_ENV_VARIABLE=SCRYPTED_RASPBIAN_FFMPEG_PATH
|
||||
const v = process.env.SCRYPTED_FFMPEG_PATH_ENV_VARIABLE;
|
||||
if (v) {
|
||||
const f = process.env[v];
|
||||
if (f && fs.existsSync(f))
|
||||
return f;
|
||||
}
|
||||
|
||||
// strange behavior on synology and possibly unraid
|
||||
// where environment variables are not necessarily kept
|
||||
// in their container manager thing.
|
||||
// so even if the Dockerfile sets SCRYPTED_FFMPEG_PATH,
|
||||
// it is not gauranteed to be present in the environment.
|
||||
// this causes issues with @scrypted/ffmpeg-static,
|
||||
// which looks at that environment variable at build time
|
||||
// to determine whether to install ffmpeg.
|
||||
|
||||
// try to get the ffmpeg path from a variable
|
||||
// ie:
|
||||
// export SCRYPTED_FFMPEG_PATH=/usr/local/bin/ffmpeg
|
||||
const f = process.env.SCRYPTED_FFMPEG_PATH;
|
||||
if (f && fs.existsSync(f))
|
||||
return f;
|
||||
|
||||
const defaultPath = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
|
||||
const scryptedFfmpegStatic = getFfmpegPath();
|
||||
if (scryptedFfmpegStatic && fs.existsSync(scryptedFfmpegStatic))
|
||||
return scryptedFfmpegStatic;
|
||||
return defaultPath;
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { getFfmpegPath } from '@scrypted/ffmpeg-static';
|
||||
import { BufferConverter, DeviceManager, FFmpegInput, MediaConverter, MediaManager, MediaObjectCreateOptions, MediaObject as MediaObjectInterface, MediaStreamUrl, ScryptedDevice, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, SystemDeviceState, SystemManager } from "@scrypted/types";
|
||||
import fs from 'fs';
|
||||
import https from 'https';
|
||||
import Graph from 'node-dijkstra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import send from 'send';
|
||||
import MimeType from 'whatwg-mimetype';
|
||||
import { getScryptedFFmpegPath } from './ffmpeg-path';
|
||||
import { MediaObject } from "./mediaobject";
|
||||
import { MediaObjectRemote } from "./plugin-api";
|
||||
|
||||
@@ -174,25 +173,7 @@ export abstract class MediaManagerBase implements MediaManager {
|
||||
abstract getMixinConsole(mixinId: string, nativeId: ScryptedNativeId): Console;
|
||||
|
||||
async getFFmpegPath(): Promise<string> {
|
||||
// try to get the ffmpeg path as a value of another variable
|
||||
// ie, in docker builds:
|
||||
// export SCRYPTED_FFMPEG_PATH_ENV_VARIABLE=SCRYPTED_RASPBIAN_FFMPEG_PATH
|
||||
const v = process.env.SCRYPTED_FFMPEG_PATH_ENV_VARIABLE;
|
||||
if (v) {
|
||||
const f = process.env[v];
|
||||
if (f && fs.existsSync(f))
|
||||
return f;
|
||||
}
|
||||
|
||||
// try to get the ffmpeg path from a variable
|
||||
// ie:
|
||||
// export SCRYPTED_FFMPEG_PATH=/usr/local/bin/ffmpeg
|
||||
const f = process.env.SCRYPTED_FFMPEG_PATH;
|
||||
if (f && fs.existsSync(f))
|
||||
return f;
|
||||
|
||||
const defaultPath = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
|
||||
return getFfmpegPath() || defaultPath;
|
||||
return getScryptedFFmpegPath();
|
||||
}
|
||||
|
||||
async getFilesPath(): Promise<string> {
|
||||
|
||||
@@ -132,7 +132,7 @@ export class PythonRuntimeWorker extends ChildProcessWorker {
|
||||
|
||||
const strippedPythonVersion = pluginPythonVersion.replace('.', '');
|
||||
const envPython = !process.env.SCRYPTED_PORTABLE_PYTHON && process.env[`SCRYPTED_PYTHON${strippedPythonVersion}_PATH`];
|
||||
if (envPython) {
|
||||
if (envPython && fs.existsSync(envPython)) {
|
||||
pythonPath = envPython;
|
||||
setup();
|
||||
this.peerin = this.worker.stdio[3] as Writable;
|
||||
|
||||
@@ -23,6 +23,7 @@ import { EnvControl } from './services/env';
|
||||
import { Info } from './services/info';
|
||||
import { ServiceControl } from './services/service-control';
|
||||
import { sleep } from './sleep';
|
||||
import { getScryptedFFmpegPath } from './plugin/ffmpeg-path';
|
||||
|
||||
installSourceMapSupport({
|
||||
environment: 'node',
|
||||
@@ -143,6 +144,7 @@ function createClusterForkParam(mainFilename: string, clusterId: string, cluster
|
||||
...runtimeWorkerOptions.env,
|
||||
SCRYPTED_VOLUME: volume,
|
||||
SCRYPTED_PLUGIN_VOLUME: pluginVolume,
|
||||
SCRYPTED_FFMPEG_PATH: process.env.SCRYPTED_FFMPEG_PATH || await getScryptedFFmpegPath(),
|
||||
};
|
||||
|
||||
runtimeWorker = rt(mainFilename, runtimeWorkerOptions, undefined);
|
||||
@@ -369,6 +371,7 @@ export function createClusterServer(mainFilename: string, scryptedRuntime: Scryp
|
||||
console.log('Cluster client authenticated.', socket.remoteAddress, socket.remotePort, properties);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Cluster client authentication failed.', socket.remoteAddress, socket.remotePort, e);
|
||||
peer.kill(e);
|
||||
socket.destroy();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user