@scrypted/server 0.1.14 → 0.2.1

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.

Files changed (57) hide show
  1. package/dist/event-registry.js +3 -4
  2. package/dist/event-registry.js.map +1 -1
  3. package/dist/http-interfaces.js +11 -0
  4. package/dist/http-interfaces.js.map +1 -1
  5. package/dist/plugin/media.js +78 -67
  6. package/dist/plugin/media.js.map +1 -1
  7. package/dist/plugin/plugin-api.js +1 -1
  8. package/dist/plugin/plugin-api.js.map +1 -1
  9. package/dist/plugin/plugin-device.js +25 -11
  10. package/dist/plugin/plugin-device.js.map +1 -1
  11. package/dist/plugin/plugin-host-api.js.map +1 -1
  12. package/dist/plugin/plugin-host.js +11 -6
  13. package/dist/plugin/plugin-host.js.map +1 -1
  14. package/dist/plugin/plugin-http.js +1 -1
  15. package/dist/plugin/plugin-http.js.map +1 -1
  16. package/dist/plugin/plugin-remote-worker.js +170 -17
  17. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  18. package/dist/plugin/plugin-remote.js +25 -85
  19. package/dist/plugin/plugin-remote.js.map +1 -1
  20. package/dist/plugin/runtime/node-fork-worker.js +11 -3
  21. package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
  22. package/dist/plugin/socket-serializer.js +17 -0
  23. package/dist/plugin/socket-serializer.js.map +1 -0
  24. package/dist/rpc-serializer.js +23 -10
  25. package/dist/rpc-serializer.js.map +1 -1
  26. package/dist/rpc.js +3 -3
  27. package/dist/rpc.js.map +1 -1
  28. package/dist/runtime.js +36 -33
  29. package/dist/runtime.js.map +1 -1
  30. package/dist/scrypted-plugin-main.js +4 -1
  31. package/dist/scrypted-plugin-main.js.map +1 -1
  32. package/dist/scrypted-server-main.js +53 -12
  33. package/dist/scrypted-server-main.js.map +1 -1
  34. package/dist/server-settings.js +5 -1
  35. package/dist/server-settings.js.map +1 -1
  36. package/dist/state.js +2 -1
  37. package/dist/state.js.map +1 -1
  38. package/package.json +5 -12
  39. package/src/event-registry.ts +3 -4
  40. package/src/http-interfaces.ts +13 -0
  41. package/src/plugin/media.ts +94 -75
  42. package/src/plugin/plugin-api.ts +5 -4
  43. package/src/plugin/plugin-device.ts +25 -11
  44. package/src/plugin/plugin-host-api.ts +1 -1
  45. package/src/plugin/plugin-host.ts +6 -5
  46. package/src/plugin/plugin-http.ts +2 -2
  47. package/src/plugin/plugin-remote-worker.ts +211 -23
  48. package/src/plugin/plugin-remote.ts +31 -95
  49. package/src/plugin/runtime/node-fork-worker.ts +11 -3
  50. package/src/plugin/runtime/runtime-worker.ts +1 -1
  51. package/src/plugin/socket-serializer.ts +15 -0
  52. package/src/rpc-serializer.ts +30 -13
  53. package/src/rpc.ts +3 -2
  54. package/src/runtime.ts +37 -38
  55. package/src/scrypted-plugin-main.ts +4 -1
  56. package/src/scrypted-server-main.ts +59 -13
  57. package/src/state.ts +2 -1
@@ -1,8 +1,6 @@
1
- import pathToFfmpeg from 'ffmpeg-static';
2
- import { BufferConverter, BufferConvertorOptions, DeviceManager, FFmpegInput, MediaManager, MediaObject, MediaObjectOptions, MediaStreamUrl, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, SystemDeviceState, SystemManager } from "@scrypted/types";
1
+ import ffmpegInstaller from '@ffmpeg-installer/ffmpeg';
2
+ import { BufferConverter, DeviceManager, FFmpegInput, MediaManager, MediaObject, MediaObjectOptions, MediaStreamUrl, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, SystemDeviceState, SystemManager } from "@scrypted/types";
3
3
  import axios from 'axios';
4
- import child_process from 'child_process';
5
- import { once } from 'events';
6
4
  import fs from 'fs';
7
5
  import https from 'https';
8
6
  import mimeType from 'mime';
@@ -10,8 +8,6 @@ import mkdirp from "mkdirp";
10
8
  import Graph from 'node-dijkstra';
11
9
  import os from 'os';
12
10
  import path from 'path';
13
- import rimraf from "rimraf";
14
- import tmp from 'tmp';
15
11
  import MimeType from 'whatwg-mimetype';
16
12
  import { MediaObjectRemote } from "./plugin-api";
17
13
 
@@ -28,14 +24,28 @@ function mimeMatches(target: MimeType, candidate: MimeType) {
28
24
 
29
25
  const httpsAgent = new https.Agent({
30
26
  rejectUnauthorized: false
31
- })
27
+ });
28
+
29
+ type IdBufferConverter = BufferConverter & {
30
+ id: string;
31
+ };
32
+
33
+ function getBuiltinId(n: number) {
34
+ return 'builtin-' + n;
35
+ }
36
+
37
+ function getExtraId(n: number) {
38
+ return 'extra-' + n;
39
+ }
32
40
 
33
41
  export abstract class MediaManagerBase implements MediaManager {
34
- builtinConverters: BufferConverter[] = [];
42
+ builtinConverters: IdBufferConverter[] = [];
43
+ extraConverters: IdBufferConverter[] = [];
35
44
 
36
45
  constructor() {
37
46
  for (const h of ['http', 'https']) {
38
47
  this.builtinConverters.push({
48
+ id: getBuiltinId(this.builtinConverters.length),
39
49
  fromMimeType: ScryptedMimeTypes.SchemePrefix + h,
40
50
  toMimeType: ScryptedMimeTypes.MediaObject,
41
51
  convert: async (data, fromMimeType, toMimeType) => {
@@ -51,18 +61,32 @@ export abstract class MediaManagerBase implements MediaManager {
51
61
  }
52
62
 
53
63
  this.builtinConverters.push({
64
+ id: getBuiltinId(this.builtinConverters.length),
54
65
  fromMimeType: ScryptedMimeTypes.SchemePrefix + 'file',
55
66
  toMimeType: ScryptedMimeTypes.MediaObject,
56
67
  convert: async (data, fromMimeType, toMimeType) => {
57
- const filename = data.toString();
68
+ const url = data.toString();
69
+ const filename = url.substring('file:'.length);
70
+
71
+ if (toMimeType === ScryptedMimeTypes.FFmpegInput) {
72
+ const ffmpegInput: FFmpegInput = {
73
+ url,
74
+ inputArguments: [
75
+ '-i', filename,
76
+ ]
77
+ };
78
+ return this.createFFmpegMediaObject(ffmpegInput);
79
+ }
80
+
58
81
  const ab = await fs.promises.readFile(filename);
59
- const mt = mimeType.lookup(data.toString());
82
+ const mt = mimeType.getType(data.toString());
60
83
  const mo = this.createMediaObject(ab, mt);
61
84
  return mo;
62
85
  }
63
86
  });
64
87
 
65
88
  this.builtinConverters.push({
89
+ id: getBuiltinId(this.builtinConverters.length),
66
90
  fromMimeType: ScryptedMimeTypes.Url,
67
91
  toMimeType: ScryptedMimeTypes.FFmpegInput,
68
92
  async convert(data, fromMimeType): Promise<Buffer> {
@@ -79,6 +103,7 @@ export abstract class MediaManagerBase implements MediaManager {
79
103
  });
80
104
 
81
105
  this.builtinConverters.push({
106
+ id: getBuiltinId(this.builtinConverters.length),
82
107
  fromMimeType: ScryptedMimeTypes.FFmpegInput,
83
108
  toMimeType: ScryptedMimeTypes.MediaStreamUrl,
84
109
  async convert(data: Buffer, fromMimeType: string): Promise<Buffer> {
@@ -87,6 +112,7 @@ export abstract class MediaManagerBase implements MediaManager {
87
112
  });
88
113
 
89
114
  this.builtinConverters.push({
115
+ id: getBuiltinId(this.builtinConverters.length),
90
116
  fromMimeType: ScryptedMimeTypes.MediaStreamUrl,
91
117
  toMimeType: ScryptedMimeTypes.FFmpegInput,
92
118
  async convert(data, fromMimeType: string): Promise<Buffer> {
@@ -111,52 +137,24 @@ export abstract class MediaManagerBase implements MediaManager {
111
137
  }
112
138
  });
113
139
 
140
+ // todo: move this to snapshot plugin
114
141
  this.builtinConverters.push({
142
+ id: getBuiltinId(this.builtinConverters.length),
115
143
  fromMimeType: 'image/*',
116
144
  toMimeType: 'image/*',
117
145
  convert: async (data, fromMimeType: string): Promise<Buffer> => {
118
146
  return data as Buffer;
119
147
  }
120
148
  });
149
+ }
121
150
 
122
- this.builtinConverters.push({
123
- fromMimeType: ScryptedMimeTypes.FFmpegInput,
124
- toMimeType: 'image/jpeg',
125
- convert: async (data, fromMimeType: string, toMimeType: string, options?: BufferConvertorOptions): Promise<Buffer> => {
126
- const console = this.getMixinConsole(options?.sourceId, undefined);
127
-
128
- const ffInput: FFmpegInput = JSON.parse(data.toString());
151
+ async addConverter(converter: IdBufferConverter): Promise<void> {
152
+ converter.id = getExtraId(this.extraConverters.length);
153
+ this.extraConverters.push(converter);
154
+ }
129
155
 
130
- const args = [
131
- '-hide_banner',
132
- ];
133
- args.push(...ffInput.inputArguments);
134
-
135
- const tmpfile = tmp.fileSync();
136
- try {
137
- args.push('-y', "-vframes", "1", '-f', 'image2', tmpfile.name);
138
-
139
- const cp = child_process.spawn(await this.getFFmpegPath(), args);
140
- console.log('converting ffmpeg input to image.');
141
- // ffmpegLogInitialOutput(console, cp);
142
- cp.on('error', (code) => {
143
- console.error('ffmpeg error code', code);
144
- })
145
- const to = setTimeout(() => {
146
- console.log('ffmpeg stream to image convesion timed out.');
147
- cp.kill('SIGKILL');
148
- }, 10000);
149
- clearTimeout(to);
150
- const [exitCode] = await once(cp, 'exit');
151
- if (exitCode)
152
- throw new Error(`ffmpeg stream to image convesion failed with exit code: ${exitCode}`);
153
- return fs.readFileSync(tmpfile.name);
154
- }
155
- finally {
156
- rimraf.sync(tmpfile.name);
157
- }
158
- }
159
- });
156
+ async clearConverters(): Promise<void> {
157
+ this.extraConverters = [];
160
158
  }
161
159
 
162
160
  async convertMediaObjectToJSON<T>(mediaObject: MediaObject, toMimeType: string): Promise<T> {
@@ -188,7 +186,7 @@ export abstract class MediaManagerBase implements MediaManager {
188
186
  return f;
189
187
 
190
188
  const defaultPath = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
191
- return pathToFfmpeg || defaultPath;
189
+ return ffmpegInstaller.path || defaultPath;
192
190
  }
193
191
 
194
192
  async getFilesPath(): Promise<string> {
@@ -200,17 +198,24 @@ export abstract class MediaManagerBase implements MediaManager {
200
198
  return ret;
201
199
  }
202
200
 
203
- getConverters(): BufferConverter[] {
201
+ getConverters(): IdBufferConverter[] {
204
202
  const converters = Object.entries(this.getSystemState())
205
203
  .filter(([id, state]) => state[ScryptedInterfaceProperty.interfaces]?.value?.includes(ScryptedInterface.BufferConverter))
206
- .map(([id]) => this.getDeviceById<BufferConverter>(id));
204
+ .map(([id]) => this.getDeviceById<IdBufferConverter>(id));
205
+
206
+ // builtins should be after system converters. these should not be overriden by system,
207
+ // as it could cause system instability with misconfiguration.
207
208
  converters.push(...this.builtinConverters);
209
+
210
+ // extra converters are added last and do allow overriding builtins, as
211
+ // the instability would be confined to a single plugin.
212
+ converters.push(...this.extraConverters);
208
213
  return converters;
209
214
  }
210
215
 
211
216
  ensureMediaObjectRemote(mediaObject: string | MediaObject): MediaObjectRemote {
212
217
  if (typeof mediaObject === 'string') {
213
- const mime = mimeType.lookup(mediaObject);
218
+ const mime = mimeType.getType(mediaObject);
214
219
  return this.createMediaObjectRemote(mediaObject, mime);
215
220
  }
216
221
  return mediaObject as MediaObjectRemote;
@@ -301,7 +306,7 @@ export abstract class MediaManagerBase implements MediaManager {
301
306
  return this.createMediaObjectRemote(data, mimeType, options);
302
307
  }
303
308
 
304
- async convert(converters: BufferConverter[], mediaObject: MediaObjectRemote, toMimeType: string): Promise<{ data: Buffer | string | any, mimeType: string }> {
309
+ async convert(converters: IdBufferConverter[], mediaObject: MediaObjectRemote, toMimeType: string): Promise<{ data: Buffer | string | any, mimeType: string }> {
305
310
  // console.log('converting', mediaObject.mimeType, toMimeType);
306
311
  const mediaMime = new MimeType(mediaObject.mimeType);
307
312
  const outputMime = new MimeType(toMimeType);
@@ -318,13 +323,9 @@ export abstract class MediaManagerBase implements MediaManager {
318
323
  sourceId = this.getPluginDeviceId();
319
324
  const console = this.getMixinConsole(sourceId, undefined);
320
325
 
321
- const converterIds = new Map<BufferConverter, string>();
322
- const converterReverseids = new Map<string, BufferConverter>();
323
- let id = 0;
324
- for (const converter of converters) {
325
- const cid = (id++).toString();
326
- converterIds.set(converter, cid);
327
- converterReverseids.set(cid, converter);
326
+ const converterMap = new Map<string, IdBufferConverter>();
327
+ for (const c of converters) {
328
+ converterMap.set(c.id, c);
328
329
  }
329
330
 
330
331
  const nodes: any = {};
@@ -335,31 +336,36 @@ export abstract class MediaManagerBase implements MediaManager {
335
336
  try {
336
337
  const inputMime = new MimeType(converter.fromMimeType);
337
338
  const convertedMime = new MimeType(converter.toMimeType);
338
- const targetId = converterIds.get(converter);
339
+ // catch all converters should be heavily weighted so as not to use them.
340
+ const inputWeight = parseFloat(inputMime.parameters.get('converter-weight')) || (inputMime.essence === '*/*' ? 1000 : 1);
341
+ // const convertedWeight = parseFloat(convertedMime.parameters.get('converter-weight')) || (convertedMime.essence === ScryptedMimeTypes.MediaObject ? 1000 : 1);
342
+ // const conversionWeight = inputWeight + convertedWeight;
343
+ const targetId = converter.id;
339
344
  const node: any = nodes[targetId] = {};
345
+
346
+ // edge matches
340
347
  for (const candidate of converters) {
341
348
  try {
342
349
  const candidateMime = new MimeType(candidate.fromMimeType);
343
350
  if (!mimeMatches(convertedMime, candidateMime))
344
351
  continue;
345
- const candidateId = converterIds.get(candidate);
346
- node[candidateId] = 1;
352
+ const outputWeight = parseFloat(candidateMime.parameters.get('converter-weight')) || (candidateMime.essence === '*/*' ? 1000 : 1);
353
+ const candidateId = candidate.id;
354
+ node[candidateId] = inputWeight + outputWeight;
347
355
  }
348
356
  catch (e) {
349
357
  console.warn('skipping converter due to error', e)
350
358
  }
351
359
  }
352
360
 
353
- // edge matches
361
+ // source matches
354
362
  if (mimeMatches(mediaMime, inputMime)) {
355
- // catch all converters should be heavily weighted so as not to use them.
356
- mediaNode[targetId] = inputMime.essence === '*/*' ? 1000 : 1;
363
+ mediaNode[targetId] = inputWeight;
357
364
  }
358
365
 
359
366
  // target output matches
360
367
  if (mimeMatches(outputMime, convertedMime) || converter.toMimeType === ScryptedMimeTypes.MediaObject) {
361
- // catch all converters should be heavily weighted so as not to use them.
362
- node['output'] = converter.toMimeType === ScryptedMimeTypes.MediaObject ? 1000 : 1;
368
+ node['output'] = inputWeight;
363
369
  }
364
370
  }
365
371
  catch (e) {
@@ -382,13 +388,21 @@ export abstract class MediaManagerBase implements MediaManager {
382
388
  let value = await mediaObject.getData();
383
389
  let valueMime = new MimeType(mediaObject.mimeType);
384
390
 
385
- for (const node of route) {
386
- const converter = converterReverseids.get(node);
391
+ while (route.length) {
392
+ const node = route.shift();
393
+ const converter = converterMap.get(node);
387
394
  const converterToMimeType = new MimeType(converter.toMimeType);
388
395
  const converterFromMimeType = new MimeType(converter.fromMimeType);
389
396
  const type = converterToMimeType.type === '*' ? valueMime.type : converterToMimeType.type;
390
397
  const subtype = converterToMimeType.subtype === '*' ? valueMime.subtype : converterToMimeType.subtype;
391
- const targetMimeType = `${type}/${subtype}`;
398
+ let targetMimeType = `${type}/${subtype}`;
399
+ if (!route.length && outputMime.parameters.size) {
400
+ const withParameters = new MimeType(targetMimeType);
401
+ for (const k of outputMime.parameters.keys()) {
402
+ withParameters.parameters.set(k, outputMime.parameters.get(k));
403
+ }
404
+ targetMimeType = outputMime.toString();
405
+ }
392
406
 
393
407
  if (converter.toMimeType === ScryptedMimeTypes.MediaObject) {
394
408
  const mo = await converter.convert(value, valueMime.essence, toMimeType, { sourceId }) as MediaObject;
@@ -413,6 +427,15 @@ export abstract class MediaManagerBase implements MediaManager {
413
427
  export class MediaManagerImpl extends MediaManagerBase {
414
428
  constructor(public systemManager: SystemManager, public deviceManager: DeviceManager) {
415
429
  super();
430
+
431
+ this.builtinConverters.push({
432
+ id: getBuiltinId(this.builtinConverters.length),
433
+ fromMimeType: ScryptedMimeTypes.ScryptedDeviceId,
434
+ toMimeType: ScryptedMimeTypes.ScryptedDevice,
435
+ convert: async (data, fromMimeType, toMimeType) => {
436
+ return this.getDeviceById(data.toString());
437
+ }
438
+ });
416
439
  }
417
440
 
418
441
  getSystemState(): { [id: string]: { [property: string]: SystemDeviceState; }; } {
@@ -436,16 +459,12 @@ export class MediaManagerImpl extends MediaManagerBase {
436
459
 
437
460
  export class MediaManagerHostImpl extends MediaManagerBase {
438
461
  constructor(public pluginDeviceId: string,
439
- public systemState: { [id: string]: { [property: string]: SystemDeviceState } },
462
+ public getSystemState: () => { [id: string]: { [property: string]: SystemDeviceState } },
440
463
  public console: Console,
441
464
  public getDeviceById: (id: string) => any) {
442
465
  super();
443
466
  }
444
467
 
445
- getSystemState(): { [id: string]: { [property: string]: SystemDeviceState; }; } {
446
- return this.systemState;
447
- }
448
-
449
468
  getPluginDeviceId(): string {
450
469
  return this.pluginDeviceId;
451
470
  }
@@ -11,8 +11,8 @@ export interface PluginAPI {
11
11
  setState(nativeId: ScryptedNativeId, key: string, value: any): Promise<void>;
12
12
  onDevicesChanged(deviceManifest: DeviceManifest): Promise<void>;
13
13
  onDeviceDiscovered(device: Device): Promise<string>;
14
- onDeviceEvent(nativeId: ScryptedNativeId, eventInterface: any, eventData?: any): Promise<void>;
15
- onMixinEvent(id: string, nativeId: ScryptedNativeId, eventInterface: any, eventData?: any): Promise<void>;
14
+ onDeviceEvent(nativeId: ScryptedNativeId, eventInterface: string, eventData?: any): Promise<void>;
15
+ onMixinEvent(id: string, nativeId: ScryptedNativeId, eventInterface: string, eventData?: any): Promise<void>;
16
16
  onDeviceRemoved(nativeId: string): Promise<void>;
17
17
  setStorage(nativeId: string, storage: {[key: string]: any}): Promise<void>;
18
18
 
@@ -89,8 +89,8 @@ export class PluginAPIProxy extends PluginAPIManagedListeners implements PluginA
89
89
  onDeviceEvent(nativeId: ScryptedNativeId, eventInterface: any, eventData?: any): Promise<void> {
90
90
  return this.api.onDeviceEvent(nativeId, eventInterface, eventData);
91
91
  }
92
- onMixinEvent(id: string, nativeId: ScryptedNativeId, eventInterface: any, eventData?: any): Promise<void> {
93
- return this.api.onMixinEvent(nativeId, eventInterface, eventData);
92
+ onMixinEvent(id: string, nativeId: ScryptedNativeId, eventInterface: string, eventData?: any): Promise<void> {
93
+ return this.api.onMixinEvent(id, nativeId, eventInterface, eventData);
94
94
  }
95
95
  onDeviceRemoved(nativeId: string): Promise<void> {
96
96
  return this.api.onDeviceRemoved(nativeId);
@@ -147,6 +147,7 @@ export interface PluginRemoteLoadZipOptions {
147
147
  * exist on the "remote", if it is not the same machine.
148
148
  */
149
149
  unzippedPath?: string;
150
+ fork?: boolean;
150
151
  }
151
152
 
152
153
  export interface PluginRemote {
@@ -216,15 +216,24 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
216
216
  let { allInterfaces } = await previousEntry;
217
217
  try {
218
218
  const mixinProvider = this.scrypted.getDevice(mixinId) as ScryptedDevice & MixinProvider;
219
- const interfaces = mixinProvider?.interfaces?.includes(ScryptedInterface.MixinProvider) && await mixinProvider?.canMixin(type, allInterfaces) as any as ScryptedInterface[];
219
+ const isMixinProvider = mixinProvider?.interfaces?.includes(ScryptedInterface.MixinProvider);
220
+ const interfaces = isMixinProvider && await mixinProvider?.canMixin(type, allInterfaces) as any as ScryptedInterface[];
220
221
  if (!interfaces) {
221
222
  // this is not an error
222
223
  // do not advertise interfaces so it is skipped during
223
224
  // vtable lookup.
224
- console.log(`mixin provider ${mixinId} can no longer mixin ${this.id}`);
225
- const mixins: string[] = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
226
- this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.mixins, mixins.filter(mid => mid !== mixinId))
227
- this.scrypted.datastore.upsert(pluginDevice);
225
+ if (!mixinProvider || (isMixinProvider && !interfaces)) {
226
+ console.log(`Mixin provider ${mixinId} can no longer mixin ${this.id}. Removing.`, {
227
+ mixinProvider: !!mixinProvider,
228
+ interfaces,
229
+ });
230
+ const mixins: string[] = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
231
+ this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.mixins, mixins.filter(mid => mid !== mixinId))
232
+ this.scrypted.datastore.upsert(pluginDevice);
233
+ }
234
+ else {
235
+ console.log(`Mixin provider ${mixinId} can not mixin ${this.id}. It is no longer a MixinProvider. This may be temporary. Passing through.`);
236
+ }
228
237
  return {
229
238
  passthrough: true,
230
239
  allInterfaces,
@@ -353,12 +362,17 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
353
362
  for (const mixin of this.mixinTable) {
354
363
  const entry = await mixin.entry;
355
364
  if (!entry.methods) {
356
- const pluginDevice = this.scrypted.findPluginDeviceById(mixin.mixinProviderId || this.id);
357
- const plugin = this.scrypted.plugins[pluginDevice.pluginId];
358
- let methods = new Set<string>(getInterfaceMethods(ScryptedInterfaceDescriptors, entry.interfaces))
359
- if (plugin.api.descriptors)
360
- methods = new Set<string>([...methods, ...getInterfaceMethods(plugin.api.descriptors, entry.interfaces)]);
361
- entry.methods = methods;
365
+ if (entry.interfaces.size) {
366
+ const pluginDevice = this.scrypted.findPluginDeviceById(mixin.mixinProviderId || this.id);
367
+ const plugin = this.scrypted.plugins[pluginDevice.pluginId];
368
+ let methods = new Set<string>(getInterfaceMethods(ScryptedInterfaceDescriptors, entry.interfaces))
369
+ if (plugin.api.descriptors)
370
+ methods = new Set<string>([...methods, ...getInterfaceMethods(plugin.api.descriptors, entry.interfaces)]);
371
+ entry.methods = methods;
372
+ }
373
+ else {
374
+ entry.methods = new Set();
375
+ }
362
376
  }
363
377
  if (entry.methods.has(method)) {
364
378
  return { mixin, entry };
@@ -47,7 +47,7 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
47
47
 
48
48
  // do we care about mixin validation here?
49
49
  // maybe to prevent/notify errant dangling events?
50
- async onMixinEvent(id: string, nativeIdOrMixinDevice: ScryptedNativeId | any, eventInterface: any, eventData?: any) {
50
+ async onMixinEvent(id: string, nativeIdOrMixinDevice: ScryptedNativeId | any, eventInterface: string, eventData?: any) {
51
51
  // nativeId code path has been deprecated in favor of mixin object 12/10/2021
52
52
  const device = this.scrypted.findPluginDeviceById(id);
53
53
 
@@ -135,7 +135,7 @@ export class PluginHost {
135
135
  this.io.on('connection', async (socket) => {
136
136
  try {
137
137
  try {
138
- if (socket.request.url.indexOf('/api') !== -1) {
138
+ if (socket.request.url.indexOf('/engine.io/api') !== -1) {
139
139
  if (socket.request.url.indexOf('/public') !== -1) {
140
140
  socket.close();
141
141
  return;
@@ -179,7 +179,7 @@ export class PluginHost {
179
179
 
180
180
  const { runtime } = this.packageJson.scrypted;
181
181
  const mediaManager = runtime === 'python'
182
- ? new MediaManagerHostImpl(pluginDeviceId, scrypted.stateManager.getSystemState(), console, id => scrypted.getDevice(id))
182
+ ? new MediaManagerHostImpl(pluginDeviceId, () => scrypted.stateManager.getSystemState(), console, id => scrypted.getDevice(id))
183
183
  : undefined;
184
184
 
185
185
  this.api = new PluginHostAPI(scrypted, this.pluginId, this, mediaManager);
@@ -214,7 +214,6 @@ export class PluginHost {
214
214
  await remote.setNativeId(pluginDevice.nativeId, pluginDevice._id, pluginDevice.storage || {});
215
215
  }
216
216
 
217
- await remote.setSystemState(scrypted.stateManager.getSystemState());
218
217
  const waitDebug = pluginDebug?.waitDebug;
219
218
  if (waitDebug) {
220
219
  console.info('waiting for debugger...');
@@ -374,7 +373,8 @@ export class PluginHost {
374
373
  serializer.setupRpcPeer(rpcPeer);
375
374
 
376
375
  // wrap the host api with a connection specific api that can be torn down on disconnect
377
- const api = new PluginAPIProxy(this.api, await this.peer.getParam('mediaManager'));
376
+ const createMediaManager = await this.peer.getParam('createMediaManager');
377
+ const api = new PluginAPIProxy(this.api, await createMediaManager());
378
378
  const kill = () => {
379
379
  serializer.onDisconnected();
380
380
  api.removeListeners();
@@ -389,7 +389,8 @@ export class PluginHost {
389
389
  const rpcPeer = createDuplexRpcPeer(`api/${this.pluginId}`, 'duplex', duplex, duplex);
390
390
 
391
391
  // wrap the host api with a connection specific api that can be torn down on disconnect
392
- const api = new PluginAPIProxy(this.api, await this.peer.getParam('mediaManager'));
392
+ const createMediaManager = await this.peer.getParam('createMediaManager');
393
+ const api = new PluginAPIProxy(this.api, await createMediaManager());
393
394
  const kill = () => {
394
395
  api.removeListeners();
395
396
  };
@@ -38,7 +38,7 @@ export abstract class PluginHttp<T> {
38
38
 
39
39
  abstract handleEngineIOEndpoint(req: Request, res: ServerResponse, endpointRequest: HttpRequest, pluginData: T): void;
40
40
  abstract handleRequestEndpoint(req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T): void;
41
- abstract getEndpointPluginData(endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<T>;
41
+ abstract getEndpointPluginData(req: Request, endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<T>;
42
42
  abstract handleWebSocket(endpoint: string, httpRequest: HttpRequest, ws: WebSocket, pluginData: T): Promise<void>;
43
43
 
44
44
  async endpointHandler(req: Request, res: Response, isPublicEndpoint: boolean, isEngineIOEndpoint: boolean,
@@ -75,7 +75,7 @@ export abstract class PluginHttp<T> {
75
75
  return;
76
76
  }
77
77
 
78
- const pluginData = await this.getEndpointPluginData(endpoint, isUpgrade, isEngineIOEndpoint);
78
+ const pluginData = await this.getEndpointPluginData(req, endpoint, isUpgrade, isEngineIOEndpoint);
79
79
  if (!pluginData) {
80
80
  end(404, 'Not Found');
81
81
  return;