@scrypted/server 0.0.134 → 0.0.135

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 (53) hide show
  1. package/.vscode/settings.json +1 -1
  2. package/dist/event-registry.js +1 -1
  3. package/dist/event-registry.js.map +1 -1
  4. package/dist/infer-defaults.js +1 -1
  5. package/dist/infer-defaults.js.map +1 -1
  6. package/dist/media-helpers.js +9 -25
  7. package/dist/media-helpers.js.map +1 -1
  8. package/dist/plugin/descriptor.js +1 -1
  9. package/dist/plugin/descriptor.js.map +1 -1
  10. package/dist/plugin/media.js +207 -101
  11. package/dist/plugin/media.js.map +1 -1
  12. package/dist/plugin/plugin-device.js +1 -1
  13. package/dist/plugin/plugin-device.js.map +1 -1
  14. package/dist/plugin/plugin-host-api.js +1 -1
  15. package/dist/plugin/plugin-host-api.js.map +1 -1
  16. package/dist/plugin/plugin-host.js +15 -6
  17. package/dist/plugin/plugin-host.js.map +1 -1
  18. package/dist/plugin/plugin-remote.js +1 -1
  19. package/dist/plugin/plugin-remote.js.map +1 -1
  20. package/dist/plugin/system.js +1 -1
  21. package/dist/plugin/system.js.map +1 -1
  22. package/dist/runtime.js +9 -3
  23. package/dist/runtime.js.map +1 -1
  24. package/dist/services/plugin.js +1 -1
  25. package/dist/services/plugin.js.map +1 -1
  26. package/dist/state.js +1 -1
  27. package/dist/state.js.map +1 -1
  28. package/package.json +5 -5
  29. package/python/plugin-remote.py +5 -5
  30. package/src/db-types.ts +1 -1
  31. package/src/event-registry.ts +1 -1
  32. package/src/http-interfaces.ts +1 -1
  33. package/src/infer-defaults.ts +1 -1
  34. package/src/media-helpers.ts +6 -27
  35. package/src/plugin/descriptor.ts +1 -1
  36. package/src/plugin/media.ts +232 -116
  37. package/src/plugin/plugin-api.ts +1 -1
  38. package/src/plugin/plugin-console.ts +1 -1
  39. package/src/plugin/plugin-device.ts +2 -2
  40. package/src/plugin/plugin-host-api.ts +1 -1
  41. package/src/plugin/plugin-host.ts +17 -7
  42. package/src/plugin/plugin-http.ts +1 -1
  43. package/src/plugin/plugin-lazy-remote.ts +1 -1
  44. package/src/plugin/plugin-remote-worker.ts +1 -1
  45. package/src/plugin/plugin-remote.ts +1 -1
  46. package/src/plugin/plugin-repl.ts +1 -1
  47. package/src/plugin/system.ts +1 -1
  48. package/src/runtime.ts +11 -3
  49. package/src/services/plugin.ts +1 -1
  50. package/src/state.ts +1 -1
  51. package/dist/convert.js +0 -117
  52. package/dist/convert.js.map +0 -1
  53. package/src/convert.ts +0 -122
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@scrypted/server",
3
- "version": "0.0.134",
3
+ "version": "0.0.135",
4
4
  "description": "",
5
5
  "dependencies": {
6
- "@mapbox/node-pre-gyp": "^1.0.5",
6
+ "@mapbox/node-pre-gyp": "^1.0.8",
7
7
  "@scrypted/ffmpeg": "^1.0.9",
8
- "@scrypted/sdk": "^0.0.151",
8
+ "@scrypted/types": "^0.0.3",
9
9
  "adm-zip": "^0.5.3",
10
10
  "axios": "^0.21.1",
11
11
  "body-parser": "^1.19.0",
@@ -20,6 +20,7 @@
20
20
  "memfs": "^3.2.2",
21
21
  "mime-db": "^1.51.0",
22
22
  "mkdirp": "^1.0.4",
23
+ "nan": "^2.15.0",
23
24
  "node-dijkstra": "^2.5.0",
24
25
  "node-forge": "^1.2.0",
25
26
  "node-pty": "^0.10.1",
@@ -29,8 +30,7 @@
29
30
  "tar": "^6.1.11",
30
31
  "tmp": "^0.2.1",
31
32
  "tslib": "^2.3.1",
32
- "typescript": "^4.4.4",
33
- "werift": "^0.14.5",
33
+ "typescript": "^4.5.5",
34
34
  "whatwg-mimetype": "^2.3.0",
35
35
  "ws": "^8.2.3"
36
36
  },
@@ -15,7 +15,7 @@ import asyncio
15
15
  import rpc
16
16
  from collections.abc import Mapping
17
17
  from scrypted_python.scrypted_sdk.types import DeviceManifest, MediaManager, ScryptedInterfaceProperty
18
- import scrypted_python.scrypted_sdk
18
+ import scrypted_python.scrypted_sdk.types
19
19
  from asyncio.futures import Future
20
20
  from asyncio.streams import StreamReader, StreamWriter
21
21
  import os
@@ -30,7 +30,7 @@ class SystemDeviceState(TypedDict):
30
30
  value: any
31
31
 
32
32
 
33
- class SystemManager(scrypted_python.scrypted_sdk.SystemManager):
33
+ class SystemManager(scrypted_python.scrypted_sdk.types.SystemManager):
34
34
  def __init__(self, api: Any, systemState: Mapping[str, Mapping[str, SystemDeviceState]]) -> None:
35
35
  super().__init__()
36
36
  self.api = api
@@ -39,8 +39,8 @@ class SystemManager(scrypted_python.scrypted_sdk.SystemManager):
39
39
  async def getComponent(self, id: str) -> Any:
40
40
  return await self.api.getComponent(id)
41
41
 
42
- class DeviceState(scrypted_python.scrypted_sdk.DeviceState):
43
- def __init__(self, id: str, nativeId: str, systemManager: SystemManager, deviceManager: scrypted_python.scrypted_sdk.DeviceManager) -> None:
42
+ class DeviceState(scrypted_python.scrypted_sdk.types.DeviceState):
43
+ def __init__(self, id: str, nativeId: str, systemManager: SystemManager, deviceManager: scrypted_python.scrypted_sdk.types.DeviceManager) -> None:
44
44
  super().__init__()
45
45
  self._id = id
46
46
  self.nativeId = nativeId
@@ -80,7 +80,7 @@ class DeviceStorage:
80
80
  storage: Mapping[str, str] = {}
81
81
 
82
82
 
83
- class DeviceManager(scrypted_python.scrypted_sdk.DeviceManager):
83
+ class DeviceManager(scrypted_python.scrypted_sdk.types.DeviceManager):
84
84
  def __init__(self, nativeIds: Mapping[str, DeviceStorage], systemManager: SystemManager) -> None:
85
85
  super().__init__()
86
86
  self.nativeIds = nativeIds
package/src/db-types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { LevelDocument } from "./level";
2
- import { ScryptedNativeId, SystemDeviceState } from "@scrypted/sdk/types";
2
+ import { ScryptedNativeId, SystemDeviceState } from "@scrypted/types";
3
3
 
4
4
  export class ScryptedDocument implements LevelDocument {
5
5
  _id?: string;
@@ -1,4 +1,4 @@
1
- import { EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterface, SystemDeviceState } from "@scrypted/sdk/types";
1
+ import { EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterface, SystemDeviceState } from "@scrypted/types";
2
2
 
3
3
  export class EventListenerRegisterImpl implements EventListenerRegister {
4
4
  removeListener: () => void;
@@ -1,4 +1,4 @@
1
- import { HttpResponse, HttpResponseOptions } from "@scrypted/sdk/types";
1
+ import { HttpResponse, HttpResponseOptions } from "@scrypted/types";
2
2
  import { Response } from "express";
3
3
  import mime from "mime";
4
4
  import { PROPERTY_PROXY_ONEWAY_METHODS } from "./rpc";
@@ -1,4 +1,4 @@
1
- import { ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceProperty } from "@scrypted/sdk/types";
1
+ import { ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceProperty } from "@scrypted/types";
2
2
  import { PluginDevice } from "./db-types";
3
3
  import { getState } from "./state";
4
4
 
@@ -1,6 +1,5 @@
1
1
  import { ChildProcess } from "child_process";
2
- import { MediaStreamOptions, VideoCamera } from "@scrypted/sdk";
3
-
2
+ import process from 'process';
4
3
 
5
4
  const filtered = [
6
5
  'decode_slice_header error',
@@ -8,7 +7,9 @@ const filtered = [
8
7
  'non-existing PPS',
9
8
  ];
10
9
 
11
- export function ffmpegLogInitialOutput(console: Console, cp: ChildProcess, forever?: boolean) {
10
+ export function ffmpegLogInitialOutput(console: Console, cp: ChildProcess, forever?: boolean, storage?: Storage) {
11
+ const SCRYPTED_FFMPEG_NOISY = !!process.env.SCRYPTED_FFMPEG_NOISY || !!storage?.getItem('SCRYPTED_FFMPEG_NOISY');
12
+
12
13
  function logger(log: (str: string) => void): (buffer: Buffer) => void {
13
14
  const ret = (buffer: Buffer) => {
14
15
  const str = buffer.toString();
@@ -18,7 +19,7 @@ export function ffmpegLogInitialOutput(console: Console, cp: ChildProcess, forev
18
19
  return;
19
20
  }
20
21
 
21
- if (!forever && (str.indexOf('frame=') !== -1 || str.indexOf('size=') !== -1)) {
22
+ if (!SCRYPTED_FFMPEG_NOISY && !forever && (str.indexOf('frame=') !== -1 || str.indexOf('size=') !== -1)) {
22
23
  log(str);
23
24
  log('video/audio detected, discarding further input');
24
25
  cp.stdout.removeListener('data', ret);
@@ -36,34 +37,12 @@ export function ffmpegLogInitialOutput(console: Console, cp: ChildProcess, forev
36
37
  cp.on('exit', () => console.log('ffmpeg exited'));
37
38
  }
38
39
 
39
- export async function probeVideoCamera(device: VideoCamera) {
40
- let options: MediaStreamOptions[];
41
- try {
42
- options = await device.getVideoStreamOptions() || [];
43
- }
44
- catch (e) {
45
- }
46
-
47
- // todo: uses the first stream to determine audio availability. buggy! fix this!
48
- const noAudio = options && options.length && options[0].audio === null;
49
- return {
50
- options,
51
- noAudio,
52
- };
53
- }
54
-
55
40
  export function safePrintFFmpegArguments(console: Console, args: string[]) {
56
41
  const ret = [];
57
42
  for (const arg of args) {
58
43
  try {
59
44
  const url = new URL(arg);
60
- if (url.password) {
61
- url.password = 'REDACTED';
62
- ret.push(url.toString());
63
- }
64
- else {
65
- ret.push(arg);
66
- }
45
+ ret.push(`${url.protocol}[REDACTED]`)
67
46
  }
68
47
  catch (e) {
69
48
  ret.push(arg);
@@ -1,4 +1,4 @@
1
- import { ScryptedInterface, ScryptedInterfaceDescriptors } from "@scrypted/sdk/types";
1
+ import { ScryptedInterface, ScryptedInterfaceDescriptors } from "@scrypted/types";
2
2
 
3
3
  export const allInterfaceMethods: string[] = [].concat(...Object.values(ScryptedInterfaceDescriptors).map((type: any) => type.methods));
4
4
  export const allInterfaceProperties: string[] = [].concat(...Object.values(ScryptedInterfaceDescriptors).map((type: any) => type.properties));
@@ -1,5 +1,4 @@
1
- import { ScryptedInterfaceProperty, SystemDeviceState, MediaStreamUrl, VideoCamera, Camera, BufferConverter, FFMpegInput, MediaManager, MediaObject, ScryptedDevice, ScryptedInterface, ScryptedMimeTypes, SystemManager, SCRYPTED_MEDIA_SCHEME } from "@scrypted/sdk/types";
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 addBuiltins(console: Console, mediaManager: MediaManager) {
14
- mediaManager.builtinConverters.push({
15
- fromMimeType: `${ScryptedMimeTypes.Url};${ScryptedMimeTypes.AcceptUrlParameter}=true`,
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
- return Buffer.from(JSON.stringify(args));
28
- }
29
- });
27
+ const httpsAgent = new https.Agent({
28
+ rejectUnauthorized: false
29
+ })
30
30
 
31
- mediaManager.builtinConverters.push({
32
- fromMimeType: ScryptedMimeTypes.FFmpegInput,
33
- toMimeType: ScryptedMimeTypes.MediaStreamUrl,
34
- async convert(data: string | Buffer, fromMimeType: string): Promise<Buffer | string> {
35
- return data;
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
- const ret: FFMpegInput = Object.assign({
69
- inputArguments,
70
- }, mediaUrl);
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
- return Buffer.from(JSON.stringify(ret));
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
- export abstract class MediaManagerBase implements MediaManager {
108
- builtinConverters: BufferConverter[] = [];
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
- constructor(public console: Console) {
111
- addBuiltins(this.console, this);
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 ensureBuffer(intermediate.data);
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, mimeType?: string): Promise<MediaObject> {
198
- if (!data.startsWith(SCRYPTED_MEDIA_SCHEME))
199
- return this.createMediaObject(data, mimeType || ScryptedMimeTypes.Url);
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
- const url = new URL(data.toString());
202
- const id = url.hostname;
203
- const path = url.pathname.split('/')[1];
204
- let mo: MediaObject;
205
- if (path === ScryptedInterface.VideoCamera) {
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
- else if (path === ScryptedInterface.Camera) {
209
- mo = await this.getDeviceById<Camera>(id).takePicture() as any;
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
- else {
212
- throw new Error('Unrecognized Scrypted Media interface.')
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 mo;
328
+ return {
329
+ data: value,
330
+ mimeType: valueMime.essence,
331
+ };
216
332
  }
217
333
  }
218
334
 
@@ -1,4 +1,4 @@
1
- import { ScryptedNativeId, ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterfaceProperty, MediaObject, SystemDeviceState, MediaManager, HttpRequest } from '@scrypted/sdk/types'
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,4 +1,4 @@
1
- import { ScryptedNativeId } from '@scrypted/sdk/types'
1
+ import { ScryptedNativeId } from '@scrypted/types'
2
2
  import { listenZero } from './listen-zero';
3
3
  import { Server } from 'net';
4
4
  import { once } from 'events';
@@ -1,7 +1,7 @@
1
- import { DeviceProvider, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedInterfaceProperty } from "@scrypted/sdk/types";
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/sdk/types";
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";
@@ -1,4 +1,4 @@
1
- import { ScryptedNativeId, ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterfaceProperty, MediaManager, HttpRequest } from '@scrypted/sdk/types'
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';