@scrypted/server 0.0.132 → 0.0.137
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @scrypted/server might be problematic. Click here for more details.
- package/.vscode/settings.json +1 -1
- package/dist/event-registry.js +1 -1
- package/dist/event-registry.js.map +1 -1
- package/dist/infer-defaults.js +1 -1
- package/dist/infer-defaults.js.map +1 -1
- package/dist/media-helpers.js +20 -15
- package/dist/media-helpers.js.map +1 -1
- package/dist/plugin/descriptor.js +1 -1
- package/dist/plugin/descriptor.js.map +1 -1
- package/dist/plugin/media.js +207 -101
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-device.js +5 -1
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +1 -1
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +15 -6
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-remote.js +10 -3
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/system.js +1 -1
- package/dist/plugin/system.js.map +1 -1
- package/dist/runtime.js +36 -8
- package/dist/runtime.js.map +1 -1
- package/dist/services/plugin.js +2 -10
- package/dist/services/plugin.js.map +1 -1
- package/dist/state.js +1 -1
- package/dist/state.js.map +1 -1
- package/package.json +7 -5
- package/python/plugin-remote.py +5 -5
- package/src/db-types.ts +1 -1
- package/src/event-registry.ts +1 -1
- package/src/http-interfaces.ts +1 -1
- package/src/infer-defaults.ts +1 -1
- package/src/media-helpers.ts +16 -15
- package/src/plugin/descriptor.ts +1 -1
- package/src/plugin/media.ts +232 -116
- package/src/plugin/plugin-api.ts +1 -1
- package/src/plugin/plugin-console.ts +1 -1
- package/src/plugin/plugin-device.ts +7 -2
- package/src/plugin/plugin-host-api.ts +1 -1
- package/src/plugin/plugin-host.ts +17 -7
- package/src/plugin/plugin-http.ts +1 -1
- package/src/plugin/plugin-lazy-remote.ts +1 -1
- package/src/plugin/plugin-remote-worker.ts +1 -1
- package/src/plugin/plugin-remote.ts +12 -4
- package/src/plugin/plugin-repl.ts +1 -1
- package/src/plugin/system.ts +1 -1
- package/src/runtime.ts +42 -10
- package/src/services/plugin.ts +2 -10
- package/src/state.ts +1 -1
- package/dist/convert.js +0 -117
- package/dist/convert.js.map +0 -1
- package/src/convert.ts +0 -122
package/src/plugin/media.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { ScryptedInterfaceProperty, SystemDeviceState, MediaStreamUrl, VideoCamera, Camera, BufferConverter, FFMpegInput, MediaManager, MediaObject, ScryptedDevice, ScryptedInterface, ScryptedMimeTypes, SystemManager
|
|
2
|
-
import { convert, ensureBuffer } from "../convert";
|
|
1
|
+
import { ScryptedInterfaceProperty, SystemDeviceState, MediaStreamUrl, VideoCamera, Camera, BufferConverter, FFMpegInput, MediaManager, MediaObject, ScryptedDevice, ScryptedInterface, ScryptedMimeTypes, SystemManager } from "@scrypted/types";
|
|
3
2
|
import { MediaObjectRemote } from "./plugin-api";
|
|
4
3
|
import mimeType from 'mime'
|
|
5
4
|
import child_process from 'child_process';
|
|
@@ -9,106 +8,132 @@ import tmp from 'tmp';
|
|
|
9
8
|
import os from 'os';
|
|
10
9
|
import { getInstalledFfmpeg } from '@scrypted/ffmpeg'
|
|
11
10
|
import { ffmpegLogInitialOutput } from "../media-helpers";
|
|
11
|
+
import Graph from 'node-dijkstra';
|
|
12
|
+
import MimeType from 'whatwg-mimetype';
|
|
13
|
+
import axios from 'axios';
|
|
14
|
+
import https from 'https';
|
|
15
|
+
|
|
16
|
+
function typeMatches(target: string, candidate: string): boolean {
|
|
17
|
+
// candidate will accept anything
|
|
18
|
+
if (candidate === '*')
|
|
19
|
+
return true;
|
|
20
|
+
return target === candidate;
|
|
21
|
+
}
|
|
12
22
|
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
toMimeType: ScryptedMimeTypes.FFmpegInput,
|
|
17
|
-
async convert(data: string | Buffer, fromMimeType: string): Promise<Buffer | string> {
|
|
18
|
-
const url = data.toString();
|
|
19
|
-
const args: FFMpegInput = {
|
|
20
|
-
url,
|
|
21
|
-
inputArguments: [
|
|
22
|
-
'-i',
|
|
23
|
-
url,
|
|
24
|
-
],
|
|
25
|
-
}
|
|
23
|
+
function mimeMatches(target: MimeType, candidate: MimeType) {
|
|
24
|
+
return typeMatches(target.type, candidate.type) && typeMatches(target.subtype, candidate.subtype);
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const httpsAgent = new https.Agent({
|
|
28
|
+
rejectUnauthorized: false
|
|
29
|
+
})
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
export abstract class MediaManagerBase implements MediaManager {
|
|
32
|
+
builtinConverters: BufferConverter[] = [];
|
|
33
|
+
|
|
34
|
+
constructor(public console: Console) {
|
|
35
|
+
for (const h of ['http', 'https']) {
|
|
36
|
+
this.builtinConverters.push({
|
|
37
|
+
fromMimeType: ScryptedMimeTypes.SchemePrefix + h,
|
|
38
|
+
toMimeType: ScryptedMimeTypes.MediaObject,
|
|
39
|
+
convert: async (data, fromMimeType, toMimeType) => {
|
|
40
|
+
const ab = await axios.get(data.toString(), {
|
|
41
|
+
responseType: 'arraybuffer',
|
|
42
|
+
httpsAgent,
|
|
43
|
+
});
|
|
44
|
+
const mimeType = ab.headers['content-type'] || toMimeType;
|
|
45
|
+
const mo = this.createMediaObject(Buffer.from(ab.data), mimeType);
|
|
46
|
+
return mo;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
36
49
|
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
mediaManager.builtinConverters.push({
|
|
40
|
-
fromMimeType: ScryptedMimeTypes.MediaStreamUrl,
|
|
41
|
-
toMimeType: ScryptedMimeTypes.FFmpegInput,
|
|
42
|
-
async convert(data: string | Buffer, fromMimeType: string): Promise<Buffer | string> {
|
|
43
|
-
const mediaUrl: MediaStreamUrl = JSON.parse(data.toString());
|
|
44
|
-
|
|
45
|
-
const inputArguments: string[] = [
|
|
46
|
-
'-i',
|
|
47
|
-
mediaUrl.url,
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
if (mediaUrl.url.startsWith('rtsp://')) {
|
|
51
|
-
inputArguments.unshift(
|
|
52
|
-
// should this be set here? configurable?
|
|
53
|
-
// do we ever want udp?
|
|
54
|
-
"-rtsp_transport",
|
|
55
|
-
"tcp",
|
|
56
|
-
// 10 seconds
|
|
57
|
-
'-analyzeduration', '10000000',
|
|
58
|
-
// 20mb
|
|
59
|
-
'-probesize', '20000000',
|
|
60
|
-
"-reorder_queue_size",
|
|
61
|
-
"1024",
|
|
62
|
-
"-max_delay",
|
|
63
|
-
// 10 second delay
|
|
64
|
-
"10000000",
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
50
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
51
|
+
this.builtinConverters.push({
|
|
52
|
+
fromMimeType: `${ScryptedMimeTypes.Url};${ScryptedMimeTypes.AcceptUrlParameter}=true`,
|
|
53
|
+
toMimeType: ScryptedMimeTypes.FFmpegInput,
|
|
54
|
+
async convert(data, fromMimeType): Promise<Buffer> {
|
|
55
|
+
const url = data.toString();
|
|
56
|
+
const args: FFMpegInput = {
|
|
57
|
+
url,
|
|
58
|
+
inputArguments: [
|
|
59
|
+
'-i', url,
|
|
60
|
+
],
|
|
61
|
+
}
|
|
71
62
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
mediaManager.builtinConverters.push({
|
|
77
|
-
fromMimeType: ScryptedMimeTypes.FFmpegInput,
|
|
78
|
-
toMimeType: 'image/jpeg',
|
|
79
|
-
async convert(data: string | Buffer, fromMimeType: string): Promise<Buffer | string> {
|
|
80
|
-
const ffInput: FFMpegInput = JSON.parse(data.toString());
|
|
81
|
-
|
|
82
|
-
const args = [
|
|
83
|
-
'-hide_banner',
|
|
84
|
-
];
|
|
85
|
-
args.push(...ffInput.inputArguments);
|
|
86
|
-
|
|
87
|
-
const tmpfile = tmp.fileSync();
|
|
88
|
-
args.push('-y', "-vframes", "1", '-f', 'image2', tmpfile.name);
|
|
89
|
-
|
|
90
|
-
const cp = child_process.spawn(await mediaManager.getFFmpegPath(), args);
|
|
91
|
-
ffmpegLogInitialOutput(console, cp);
|
|
92
|
-
cp.on('error', (code) => {
|
|
93
|
-
console.error('ffmpeg error code', code);
|
|
94
|
-
})
|
|
95
|
-
const to = setTimeout(() => {
|
|
96
|
-
console.log('ffmpeg stream to image convesion timed out.');
|
|
97
|
-
cp.kill('SIGKILL');
|
|
98
|
-
}, 10000);
|
|
99
|
-
await once(cp, 'exit');
|
|
100
|
-
clearTimeout(to);
|
|
101
|
-
const ret = fs.readFileSync(tmpfile.name);
|
|
102
|
-
return ret;
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
63
|
+
return Buffer.from(JSON.stringify(args));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
106
66
|
|
|
107
|
-
|
|
108
|
-
|
|
67
|
+
this.builtinConverters.push({
|
|
68
|
+
fromMimeType: ScryptedMimeTypes.FFmpegInput,
|
|
69
|
+
toMimeType: ScryptedMimeTypes.MediaStreamUrl,
|
|
70
|
+
async convert(data: Buffer, fromMimeType: string): Promise<Buffer> {
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.builtinConverters.push({
|
|
76
|
+
fromMimeType: ScryptedMimeTypes.MediaStreamUrl,
|
|
77
|
+
toMimeType: ScryptedMimeTypes.FFmpegInput,
|
|
78
|
+
async convert(data, fromMimeType: string): Promise<Buffer> {
|
|
79
|
+
const mediaUrl: MediaStreamUrl = JSON.parse(data.toString());
|
|
80
|
+
|
|
81
|
+
const inputArguments: string[] = [
|
|
82
|
+
'-i', mediaUrl.url,
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
if (mediaUrl.url.startsWith('rtsp://')) {
|
|
86
|
+
inputArguments.unshift(
|
|
87
|
+
"-rtsp_transport", "tcp",
|
|
88
|
+
"-max_delay", "1000000",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const ret: FFMpegInput = Object.assign({
|
|
93
|
+
inputArguments,
|
|
94
|
+
}, mediaUrl);
|
|
95
|
+
|
|
96
|
+
return Buffer.from(JSON.stringify(ret));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
109
99
|
|
|
110
|
-
|
|
111
|
-
|
|
100
|
+
this.builtinConverters.push({
|
|
101
|
+
fromMimeType: 'image/*',
|
|
102
|
+
toMimeType: 'image/*',
|
|
103
|
+
convert: async (data, fromMimeType: string): Promise<Buffer> => {
|
|
104
|
+
return data as Buffer;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this.builtinConverters.push({
|
|
109
|
+
fromMimeType: ScryptedMimeTypes.FFmpegInput,
|
|
110
|
+
toMimeType: 'image/jpeg',
|
|
111
|
+
convert: async (data, fromMimeType: string): Promise<Buffer> => {
|
|
112
|
+
const ffInput: FFMpegInput = JSON.parse(data.toString());
|
|
113
|
+
|
|
114
|
+
const args = [
|
|
115
|
+
'-hide_banner',
|
|
116
|
+
];
|
|
117
|
+
args.push(...ffInput.inputArguments);
|
|
118
|
+
|
|
119
|
+
const tmpfile = tmp.fileSync();
|
|
120
|
+
args.push('-y', "-vframes", "1", '-f', 'image2', tmpfile.name);
|
|
121
|
+
|
|
122
|
+
const cp = child_process.spawn(await this.getFFmpegPath(), args);
|
|
123
|
+
ffmpegLogInitialOutput(console, cp);
|
|
124
|
+
cp.on('error', (code) => {
|
|
125
|
+
console.error('ffmpeg error code', code);
|
|
126
|
+
})
|
|
127
|
+
const to = setTimeout(() => {
|
|
128
|
+
console.log('ffmpeg stream to image convesion timed out.');
|
|
129
|
+
cp.kill('SIGKILL');
|
|
130
|
+
}, 10000);
|
|
131
|
+
await once(cp, 'exit');
|
|
132
|
+
clearTimeout(to);
|
|
133
|
+
const ret = fs.readFileSync(tmpfile.name);
|
|
134
|
+
return ret;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
112
137
|
}
|
|
113
138
|
|
|
114
139
|
abstract getSystemState(): { [id: string]: { [property: string]: SystemDeviceState } };
|
|
@@ -153,26 +178,26 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
|
153
178
|
}
|
|
154
179
|
|
|
155
180
|
async convertMediaObjectToInsecureLocalUrl(mediaObject: string | MediaObject, toMimeType: string): Promise<string> {
|
|
156
|
-
const intermediate = await convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
|
|
181
|
+
const intermediate = await this.convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
|
|
157
182
|
const converted = this.createMediaObject(intermediate.data, intermediate.mimeType);
|
|
158
|
-
const url = await convert(this.getConverters(), converted, ScryptedMimeTypes.InsecureLocalUrl);
|
|
183
|
+
const url = await this.convert(this.getConverters(), converted, ScryptedMimeTypes.InsecureLocalUrl);
|
|
159
184
|
return url.data.toString();
|
|
160
185
|
}
|
|
161
186
|
|
|
162
187
|
async convertMediaObjectToBuffer(mediaObject: MediaObject, toMimeType: string): Promise<Buffer> {
|
|
163
|
-
const intermediate = await convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
|
|
164
|
-
return
|
|
188
|
+
const intermediate = await this.convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
|
|
189
|
+
return intermediate.data as Buffer;
|
|
165
190
|
}
|
|
166
191
|
async convertMediaObjectToLocalUrl(mediaObject: string | MediaObject, toMimeType: string): Promise<string> {
|
|
167
|
-
const intermediate = await convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
|
|
192
|
+
const intermediate = await this.convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
|
|
168
193
|
const converted = this.createMediaObject(intermediate.data, intermediate.mimeType);
|
|
169
|
-
const url = await convert(this.getConverters(), converted, ScryptedMimeTypes.LocalUrl);
|
|
194
|
+
const url = await this.convert(this.getConverters(), converted, ScryptedMimeTypes.LocalUrl);
|
|
170
195
|
return url.data.toString();
|
|
171
196
|
}
|
|
172
197
|
async convertMediaObjectToUrl(mediaObject: string | MediaObject, toMimeType: string): Promise<string> {
|
|
173
|
-
const intermediate = await convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
|
|
198
|
+
const intermediate = await this.convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
|
|
174
199
|
const converted = this.createMediaObject(intermediate.data, intermediate.mimeType);
|
|
175
|
-
const url = await convert(this.getConverters(), converted, ScryptedMimeTypes.Url);
|
|
200
|
+
const url = await this.convert(this.getConverters(), converted, ScryptedMimeTypes.Url);
|
|
176
201
|
return url.data.toString();
|
|
177
202
|
}
|
|
178
203
|
|
|
@@ -194,25 +219,116 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
|
194
219
|
return new MediaObjectImpl();
|
|
195
220
|
}
|
|
196
221
|
|
|
197
|
-
async createMediaObjectFromUrl(data: string
|
|
198
|
-
|
|
199
|
-
|
|
222
|
+
async createMediaObjectFromUrl(data: string): Promise<MediaObject> {
|
|
223
|
+
const url = new URL(data);
|
|
224
|
+
const scheme = url.protocol.slice(0, -1);
|
|
225
|
+
const fromMimeType = ScryptedMimeTypes.SchemePrefix + scheme;
|
|
226
|
+
return this.createMediaObject(data, fromMimeType);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async convert(converters: BufferConverter[], mediaObject: MediaObjectRemote, toMimeType: string): Promise<{ data: Buffer | string, mimeType: string }> {
|
|
230
|
+
// console.log('converting', mediaObject.mimeType, toMimeType);
|
|
231
|
+
const mediaMime = new MimeType(mediaObject.mimeType);
|
|
232
|
+
const outputMime = new MimeType(toMimeType);
|
|
200
233
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
mo = await this.getDeviceById<VideoCamera>(id).getVideoStream();
|
|
234
|
+
if (mimeMatches(mediaMime, outputMime)) {
|
|
235
|
+
return {
|
|
236
|
+
mimeType: outputMime.essence,
|
|
237
|
+
data: await mediaObject.getData(),
|
|
238
|
+
}
|
|
207
239
|
}
|
|
208
|
-
|
|
209
|
-
|
|
240
|
+
|
|
241
|
+
const converterIds = new Map<BufferConverter, string>();
|
|
242
|
+
const converterReverseids = new Map<string, BufferConverter>();
|
|
243
|
+
let id = 0;
|
|
244
|
+
for (const converter of converters) {
|
|
245
|
+
const cid = (id++).toString();
|
|
246
|
+
converterIds.set(converter, cid);
|
|
247
|
+
converterReverseids.set(cid, converter);
|
|
210
248
|
}
|
|
211
|
-
|
|
212
|
-
|
|
249
|
+
|
|
250
|
+
const nodes: any = {};
|
|
251
|
+
const mediaNode: any = {};
|
|
252
|
+
nodes['mediaObject'] = mediaNode;
|
|
253
|
+
nodes['output'] = {};
|
|
254
|
+
for (const converter of converters) {
|
|
255
|
+
try {
|
|
256
|
+
const inputMime = new MimeType(converter.fromMimeType);
|
|
257
|
+
const convertedMime = new MimeType(converter.toMimeType);
|
|
258
|
+
const targetId = converterIds.get(converter);
|
|
259
|
+
const node: any = nodes[targetId] = {};
|
|
260
|
+
for (const candidate of converters) {
|
|
261
|
+
try {
|
|
262
|
+
const candidateMime = new MimeType(candidate.fromMimeType);
|
|
263
|
+
if (!mimeMatches(convertedMime, candidateMime))
|
|
264
|
+
continue;
|
|
265
|
+
const candidateId = converterIds.get(candidate);
|
|
266
|
+
node[candidateId] = 1;
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
console.warn('skipping converter due to error', e)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (mimeMatches(mediaMime, inputMime)) {
|
|
274
|
+
mediaNode[targetId] = 1;
|
|
275
|
+
}
|
|
276
|
+
if (mimeMatches(outputMime, convertedMime) || converter.toMimeType === ScryptedMimeTypes.MediaObject) {
|
|
277
|
+
node['output'] = 1;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
console.warn('skipping converter due to error', e)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const graph = new Graph();
|
|
286
|
+
for (const id of Object.keys(nodes)) {
|
|
287
|
+
graph.addNode(id, nodes[id]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const route = graph.path('mediaObject', 'output') as Array<string>;
|
|
291
|
+
if (!route || !route.length)
|
|
292
|
+
throw new Error('no converter found');
|
|
293
|
+
// pop off the mediaObject start node, no conversion necessary.
|
|
294
|
+
route.shift();
|
|
295
|
+
// also remove the output node.
|
|
296
|
+
route.splice(route.length - 1);
|
|
297
|
+
let value = await mediaObject.getData();
|
|
298
|
+
let valueMime = new MimeType(mediaObject.mimeType);
|
|
299
|
+
for (const node of route) {
|
|
300
|
+
const converter = converterReverseids.get(node);
|
|
301
|
+
const converterToMimeType = new MimeType(converter.toMimeType);
|
|
302
|
+
const converterFromMimeType = new MimeType(converter.fromMimeType);
|
|
303
|
+
const type = converterToMimeType.type === '*' ? valueMime.type : converterToMimeType.type;
|
|
304
|
+
const subtype = converterToMimeType.subtype === '*' ? valueMime.subtype : converterToMimeType.subtype;
|
|
305
|
+
const targetMimeType = `${type}/${subtype}`;
|
|
306
|
+
|
|
307
|
+
if (typeof value === 'string' && !converterFromMimeType.parameters.has(ScryptedMimeTypes.AcceptUrlParameter)) {
|
|
308
|
+
const url = new URL(value);
|
|
309
|
+
const scheme = url.protocol.slice(0, -1);
|
|
310
|
+
const fromMimeType = ScryptedMimeTypes.SchemePrefix + scheme;
|
|
311
|
+
for (const converter of this.getConverters()) {
|
|
312
|
+
if (converter.fromMimeType !== fromMimeType || converter.toMimeType !== ScryptedMimeTypes.MediaObject)
|
|
313
|
+
continue;
|
|
314
|
+
|
|
315
|
+
const mo = await converter.convert(value, fromMimeType, toMimeType) as MediaObject;
|
|
316
|
+
const found = await this.convertMediaObjectToBuffer(mo, toMimeType);
|
|
317
|
+
return {
|
|
318
|
+
data: found,
|
|
319
|
+
mimeType: mo.mimeType,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
throw new Error(`no ${ScryptedInterface.BufferConverter} exists for scheme: ${scheme}`);
|
|
323
|
+
}
|
|
324
|
+
value = await converter.convert(value, valueMime.essence, targetMimeType) as string | Buffer;
|
|
325
|
+
valueMime = new MimeType(targetMimeType);
|
|
213
326
|
}
|
|
214
327
|
|
|
215
|
-
return
|
|
328
|
+
return {
|
|
329
|
+
data: value,
|
|
330
|
+
mimeType: valueMime.essence,
|
|
331
|
+
};
|
|
216
332
|
}
|
|
217
333
|
}
|
|
218
334
|
|
package/src/plugin/plugin-api.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ScryptedNativeId, ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterfaceProperty, MediaObject, SystemDeviceState, MediaManager, HttpRequest } from '@scrypted/
|
|
1
|
+
import { ScryptedNativeId, ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterfaceProperty, MediaObject, SystemDeviceState, MediaManager, HttpRequest } from '@scrypted/types'
|
|
2
2
|
|
|
3
3
|
export interface PluginLogger {
|
|
4
4
|
log(level: string, message: string): Promise<void>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { DeviceProvider, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedInterfaceProperty } from "@scrypted/
|
|
1
|
+
import { DeviceProvider, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedInterfaceProperty } from "@scrypted/types";
|
|
2
2
|
import { ScryptedRuntime } from "../runtime";
|
|
3
3
|
import { PluginDevice } from "../db-types";
|
|
4
|
-
import { MixinProvider } from "@scrypted/
|
|
4
|
+
import { MixinProvider } from "@scrypted/types";
|
|
5
5
|
import { handleFunctionInvocations, PrimitiveProxyHandler } from "../rpc";
|
|
6
6
|
import { getState } from "../state";
|
|
7
7
|
import { getDisplayType } from "../infer-defaults";
|
|
@@ -402,6 +402,11 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
|
|
|
402
402
|
}
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
if (method === 'getPluginJson'
|
|
406
|
+
&& getState(pluginDevice, ScryptedInterfaceProperty.providedInterfaces)?.includes(ScryptedInterface.ScryptedPlugin)) {
|
|
407
|
+
return this.scrypted.getPackageJson(pluginDevice.pluginId);
|
|
408
|
+
}
|
|
409
|
+
|
|
405
410
|
if (!isValidInterfaceMethod(pluginDevice.state.interfaces.value, method))
|
|
406
411
|
throw new PluginError(`device ${this.id} does not support method ${method}`);
|
|
407
412
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ScryptedNativeId, ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterfaceProperty, MediaManager, HttpRequest } from '@scrypted/
|
|
1
|
+
import { ScryptedNativeId, ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterfaceProperty, MediaManager, HttpRequest } from '@scrypted/types'
|
|
2
2
|
import { ScryptedRuntime } from '../runtime';
|
|
3
3
|
import { Plugin } from '../db-types';
|
|
4
4
|
import { PluginAPI, PluginAPIManagedListeners } from './plugin-api';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RpcPeer } from '../rpc';
|
|
2
2
|
import AdmZip from 'adm-zip';
|
|
3
|
-
import { Device, EngineIOHandler } from '@scrypted/
|
|
3
|
+
import { Device, EngineIOHandler } from '@scrypted/types'
|
|
4
4
|
import { ScryptedRuntime } from '../runtime';
|
|
5
5
|
import { Plugin } from '../db-types';
|
|
6
6
|
import io, { Socket } from 'engine.io';
|
|
@@ -27,6 +27,8 @@ import rimraf from 'rimraf';
|
|
|
27
27
|
|
|
28
28
|
export class PluginHost {
|
|
29
29
|
static sharedWorker: child_process.ChildProcess;
|
|
30
|
+
static sharedWorkerImmediateRestart = false;
|
|
31
|
+
|
|
30
32
|
worker: child_process.ChildProcess;
|
|
31
33
|
peer: RpcPeer;
|
|
32
34
|
pluginId: string;
|
|
@@ -51,6 +53,9 @@ export class PluginHost {
|
|
|
51
53
|
kill() {
|
|
52
54
|
this.killed = true;
|
|
53
55
|
this.api.removeListeners();
|
|
56
|
+
// things might get a bit race prone, so clear out the shared worker before killing.
|
|
57
|
+
if (this.worker === PluginHost.sharedWorker)
|
|
58
|
+
PluginHost.sharedWorker = undefined;
|
|
54
59
|
this.worker.kill('SIGKILL');
|
|
55
60
|
this.io.close();
|
|
56
61
|
for (const s of Object.values(this.ws)) {
|
|
@@ -254,7 +259,7 @@ export class PluginHost {
|
|
|
254
259
|
// stdin, stdout, stderr, peer in, peer out
|
|
255
260
|
stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
|
|
256
261
|
env: Object.assign({
|
|
257
|
-
PYTHONPATH: path.join(process.cwd(), 'node_modules/@scrypted/
|
|
262
|
+
PYTHONPATH: path.join(process.cwd(), 'node_modules/@scrypted/types'),
|
|
258
263
|
}, process.env, env),
|
|
259
264
|
});
|
|
260
265
|
|
|
@@ -293,17 +298,22 @@ export class PluginHost {
|
|
|
293
298
|
Object.keys(this.packageJson.optionalDependencies || {}).length === 0;
|
|
294
299
|
if (useSharedWorker) {
|
|
295
300
|
if (!PluginHost.sharedWorker) {
|
|
296
|
-
|
|
301
|
+
const worker = child_process.fork(require.main.filename, ['child', '@scrypted/shared'], {
|
|
297
302
|
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
|
298
303
|
env: Object.assign({}, process.env, env),
|
|
299
304
|
serialization: 'advanced',
|
|
300
305
|
execArgv,
|
|
301
306
|
});
|
|
307
|
+
PluginHost.sharedWorker = worker;
|
|
302
308
|
PluginHost.sharedWorker.setMaxListeners(100);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
309
|
+
const clearSharedWorker = () => {
|
|
310
|
+
if (worker === PluginHost.sharedWorker)
|
|
311
|
+
PluginHost.sharedWorker = undefined;
|
|
312
|
+
};
|
|
313
|
+
PluginHost.sharedWorker.on('close', () => clearSharedWorker);
|
|
314
|
+
PluginHost.sharedWorker.on('error', () => clearSharedWorker);
|
|
315
|
+
PluginHost.sharedWorker.on('exit', () => clearSharedWorker);
|
|
316
|
+
PluginHost.sharedWorker.on('disconnect', () => clearSharedWorker);
|
|
307
317
|
}
|
|
308
318
|
PluginHost.sharedWorker.send({
|
|
309
319
|
type: 'start',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Request, Response, Router } from 'express';
|
|
2
2
|
import bodyParser from 'body-parser';
|
|
3
|
-
import { HttpRequest } from '@scrypted/
|
|
3
|
+
import { HttpRequest } from '@scrypted/types';
|
|
4
4
|
import WebSocket, { Server as WebSocketServer } from "ws";
|
|
5
5
|
import { ServerResponse } from 'http';
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RpcMessage, RpcPeer } from '../rpc';
|
|
2
|
-
import { SystemManager, DeviceManager, ScryptedNativeId } from '@scrypted/
|
|
2
|
+
import { SystemManager, DeviceManager, ScryptedNativeId } from '@scrypted/types'
|
|
3
3
|
import { attachPluginRemote, PluginReader } from './plugin-remote';
|
|
4
4
|
import { PluginAPI } from './plugin-api';
|
|
5
5
|
import { MediaManagerImpl } from './media';
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import AdmZip from 'adm-zip';
|
|
2
2
|
import { Volume } from 'memfs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { ScryptedNativeId, DeviceManager, Logger, Device, DeviceManifest, DeviceState, EndpointManager, SystemDeviceState, ScryptedStatic, SystemManager, MediaManager, ScryptedMimeTypes, ScryptedInterface, ScryptedInterfaceProperty, HttpRequest } from '@scrypted/
|
|
4
|
+
import { ScryptedNativeId, DeviceManager, Logger, Device, DeviceManifest, DeviceState, EndpointManager, SystemDeviceState, ScryptedStatic, SystemManager, MediaManager, ScryptedMimeTypes, ScryptedInterface, ScryptedInterfaceProperty, HttpRequest } from '@scrypted/types'
|
|
5
5
|
import { PluginAPI, PluginLogger, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
|
6
6
|
import { SystemManagerImpl } from './system';
|
|
7
7
|
import { RpcPeer, RPCResultError, PROPERTY_PROXY_ONEWAY_METHODS, PROPERTY_JSON_DISABLE_SERIALIZATION } from '../rpc';
|
|
8
8
|
import { BufferSerializer } from './buffer-serializer';
|
|
9
9
|
import { createWebSocketClass, WebSocketConnectCallbacks, WebSocketMethods } from './plugin-remote-websocket';
|
|
10
10
|
import fs from 'fs';
|
|
11
|
-
const {link} = require('linkfs');
|
|
11
|
+
const { link } = require('linkfs');
|
|
12
12
|
|
|
13
13
|
class DeviceLogger implements Logger {
|
|
14
14
|
nativeId: ScryptedNativeId;
|
|
@@ -530,10 +530,18 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
|
530
530
|
peer.evalLocal(script, zipOptions?.filename || '/plugin/main.nodejs.js', params);
|
|
531
531
|
pluginConsole?.log('plugin successfully loaded');
|
|
532
532
|
await options?.onPluginReady?.(ret, params, exports.default);
|
|
533
|
-
|
|
533
|
+
|
|
534
|
+
const defaultExport = exports.default;
|
|
535
|
+
// support exporting a plugin class, plugin main function,
|
|
536
|
+
// or a plugin instance
|
|
537
|
+
if (defaultExport.toString().startsWith('class '))
|
|
538
|
+
return new defaultExport();
|
|
539
|
+
if (typeof defaultExport === 'function')
|
|
540
|
+
return await defaultExport();
|
|
541
|
+
return defaultExport;
|
|
534
542
|
}
|
|
535
543
|
catch (e) {
|
|
536
|
-
pluginConsole?.error('plugin failed to
|
|
544
|
+
pluginConsole?.error('plugin failed to start', e);
|
|
537
545
|
throw e;
|
|
538
546
|
}
|
|
539
547
|
},
|
|
@@ -2,7 +2,7 @@ import { listenZero } from './listen-zero';
|
|
|
2
2
|
import { Server } from 'net';
|
|
3
3
|
import { once } from 'events';
|
|
4
4
|
import repl from 'repl';
|
|
5
|
-
import { ScryptedStatic } from '@scrypted/
|
|
5
|
+
import { ScryptedStatic } from '@scrypted/types';
|
|
6
6
|
|
|
7
7
|
export async function createREPLServer(scrypted: ScryptedStatic, params: any, plugin: any): Promise<number> {
|
|
8
8
|
const { deviceManager, systemManager } = scrypted;
|
package/src/plugin/system.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventListenerOptions, EventDetails, EventListenerRegister, ScryptedDevice, ScryptedInterface, ScryptedInterfaceDescriptors, SystemDeviceState, SystemManager, ScryptedInterfaceProperty, ScryptedDeviceType, Logger } from "@scrypted/
|
|
1
|
+
import { EventListenerOptions, EventDetails, EventListenerRegister, ScryptedDevice, ScryptedInterface, ScryptedInterfaceDescriptors, SystemDeviceState, SystemManager, ScryptedInterfaceProperty, ScryptedDeviceType, Logger } from "@scrypted/types";
|
|
2
2
|
import { PluginAPI } from "./plugin-api";
|
|
3
3
|
import { handleFunctionInvocations, PrimitiveProxyHandler, PROPERTY_PROXY_ONEWAY_METHODS } from '../rpc';
|
|
4
4
|
import { EventRegistry } from "../event-registry";
|