From 2c9cdbf65532343d63f89dd806bbcf83bb4bfabc Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 21 Feb 2022 12:39:55 -0800 Subject: [PATCH] webrtc: refactor entire pipeline to handle trickle and consolidate code --- common/src/ffmpeg-to-wrtc.ts | 109 ++-- common/src/rtc-signaling.ts | 118 +++++ common/src/wrtc-to-rtsp.ts | 21 +- .../google-home/cast-receiver/.gitignore | 2 + .../plugins/google-home/cast-receiver/cast.js | 101 +--- .../google-home/cast-receiver/dist/rpc.js | 470 ++++++++++++++++++ .../cast-receiver/dist/rtc-signaling.js | 104 ++++ .../google-home/cast-receiver/index.html | 2 +- .../cast-receiver/package-lock.json | 97 ++++ .../google-home/cast-receiver/package.json | 8 + .../google-home/cast-receiver/src/rpc.ts | 1 + .../cast-receiver/src/rtc-signaling.ts | 1 + .../google-home/cast-receiver/tsconfig.json | 17 + packages/client/package-lock.json | 21 +- packages/client/package.json | 1 - packages/client/src/index.ts | 4 +- plugins/core/package-lock.json | 110 +++- plugins/core/package.json | 1 + plugins/core/src/aggregate.ts | 4 +- plugins/core/src/main.ts | 26 +- plugins/core/src/script.ts | 2 +- plugins/core/src/scrypted-eval.ts | 2 +- plugins/core/ui/package-lock.json | 47 ++ plugins/core/ui/package.json | 3 +- plugins/core/ui/src/client.ts | 4 +- plugins/core/ui/src/common/camera.ts | 255 ++-------- .../google-device-access/package-lock.json | 4 +- plugins/google-device-access/package.json | 2 +- plugins/google-device-access/src/main.ts | 3 +- plugins/google-home/.npmignore | 1 + plugins/google-home/package-lock.json | 132 +++-- plugins/google-home/package.json | 1 + .../google-home/src/commands/camerastream.ts | 7 +- plugins/google-home/src/main.ts | 57 +-- plugins/ring/src/main.ts | 5 +- sdk/gen/types.input.ts | 2 +- sdk/types/index.d.ts | 2 +- sdk/types/index.ts | 2 +- server/src/rpc.ts | 14 +- 39 files changed, 1257 insertions(+), 506 deletions(-) create mode 100644 common/src/rtc-signaling.ts create mode 100644 docs/plugins/google-home/cast-receiver/.gitignore create mode 100644 docs/plugins/google-home/cast-receiver/dist/rpc.js create mode 100644 docs/plugins/google-home/cast-receiver/dist/rtc-signaling.js create mode 100644 docs/plugins/google-home/cast-receiver/package-lock.json create mode 100644 docs/plugins/google-home/cast-receiver/package.json create mode 120000 docs/plugins/google-home/cast-receiver/src/rpc.ts create mode 120000 docs/plugins/google-home/cast-receiver/src/rtc-signaling.ts create mode 100644 docs/plugins/google-home/cast-receiver/tsconfig.json diff --git a/common/src/ffmpeg-to-wrtc.ts b/common/src/ffmpeg-to-wrtc.ts index 645c03672..6c843f6cc 100644 --- a/common/src/ffmpeg-to-wrtc.ts +++ b/common/src/ffmpeg-to-wrtc.ts @@ -1,9 +1,9 @@ -import { RTCAVMessage, FFMpegInput, MediaManager, ScryptedMimeTypes, MediaObject } from "@scrypted/sdk/types"; import child_process from 'child_process'; import net from 'net'; import { listenZero } from "./listen-cluster"; import { ffmpegLogInitialOutput } from "./media-helpers"; -import sdk from "@scrypted/sdk"; +import sdk, { RTCAVMessage, FFMpegInput, MediaManager, ScryptedMimeTypes, MediaObject, RTCAVSignalingSetup, RTCSignalingChannel, RTCSignalingChannelOptions, RTCSignalingSession, ScryptedDevice, ScryptedInterface, VideoCamera } from "@scrypted/sdk"; +import { RpcPeer } from "../../server/src/rpc"; const { mediaManager } = sdk; @@ -38,6 +38,7 @@ interface RTCSession { resolve?: (value: any) => void; } +// todo: remove this legacy path export function addBuiltins(mediaManager: MediaManager) { // older scrypted runtime won't have this property, and wrtc will be built in. if (!mediaManager.builtinConverters) @@ -129,11 +130,6 @@ export function addBuiltins(mediaManager: MediaManager) { }) } -export interface RTCPeerConnectionMediaObjectSession { - pc: RTCPeerConnection; - answer: RTCAVMessage; -} - export async function startRTCPeerConnectionFFmpegInput(ffInput: FFMpegInput, options?: { maxWidth: number, }): Promise { @@ -333,46 +329,79 @@ export async function startRTCPeerConnectionFFmpegInput(ffInput: FFMpegInput, op return pc; } -export async function startRTCPeerConnection(mediaObject: MediaObject, offer: RTCAVMessage, options?: { +export async function startRTCPeerConnection(console: Console, mediaObject: MediaObject, session: RTCSignalingSession, options?: RTCSignalingChannelOptions & { maxWidth: number, -}): Promise { - const configuration: RTCConfiguration = { - iceServers: [ - { - urls: ["turn:turn0.clockworkmod.com", "turn:n0.clockworkmod.com", "turn:n1.clockworkmod.com"], - username: "foo", - credential: "bar", - }, - ], - }; - +}) { const buffer = await mediaManager.convertMediaObjectToBuffer(mediaObject, ScryptedMimeTypes.FFmpegInput); const ffInput = JSON.parse(buffer.toString()); const pc = await startRTCPeerConnectionFFmpegInput(ffInput, options); - const done = new Promise(resolve => { + try { pc.onicecandidate = ev => { - if (!ev.candidate) - resolve(undefined); + if (ev.candidate) { + console.log('local candidate', ev.candidate); + session.addIceCandidate(JSON.parse(JSON.stringify(ev.candidate))); + } } - }) - await pc.setRemoteDescription(offer.description); - for (const c of offer.candidates || []) { - pc.addIceCandidate(c); + + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + const setup: RTCAVSignalingSetup = { + type: 'offer', + audio: { + direction: 'recvonly', + }, + video: { + direction: 'recvonly', + } + }; + await session.setRemoteDescription(offer, setup); + + const answer = await session.createLocalDescription('answer', setup, async (candidate) => { + console.log('remote candidate', candidate); + pc.addIceCandidate(candidate); + }); + + await pc.setRemoteDescription(answer); + } + catch (e) { + pc.close(); + throw e; + } +} + +export async function startBrowserRTCSignaling(camera: ScryptedDevice & RTCSignalingChannel & VideoCamera, ws: WebSocket, console: Console) { + try { + const peer = new RpcPeer("google-home", "cast-receiver", (message, reject) => { + const json = JSON.stringify(message); + try { + ws.send(json); + } + catch (e) { + reject?.(e); + } + }); + ws.onmessage = message => { + const json = JSON.parse(message.data); + peer.handleMessage(json); + }; + + const session: RTCSignalingSession = await peer.getParam('session'); + const options: RTCSignalingChannelOptions = await peer.getParam('options'); + + if (camera.interfaces.includes(ScryptedInterface.RTCSignalingChannel)) { + camera.startRTCSignalingSession(session, options); + } + else { + startRTCPeerConnection(console, await camera.getVideoStream(), session, Object.assign({ + maxWidth: 960, + }, options)); + } + } + catch (e) { + console.error("error negotiating browser RTCC signaling", e); + ws.close(); + throw e; } - let answer = await pc.createAnswer(); - await pc.setLocalDescription(answer); - await done; - - - return { - pc, - answer: { - id: undefined, - candidates: undefined, - description: pc.currentLocalDescription, - configuration, - } - }; } diff --git a/common/src/rtc-signaling.ts b/common/src/rtc-signaling.ts new file mode 100644 index 000000000..ebe527e52 --- /dev/null +++ b/common/src/rtc-signaling.ts @@ -0,0 +1,118 @@ +import type { RTCSignalingSession, RTCAVSignalingSetup, ScryptedDevice, RTCSignalingChannel } from "@scrypted/sdk/types"; +import type { RTCSignalingChannelOptions, RTCSignalingSendIceCandidate } from "@scrypted/sdk"; + +export async function startRTCSignalingSession(session: RTCSignalingSession, offer: RTCSessionDescriptionInit, + createSetup: () => Promise, + setRemoteDescription: (remoteDescription: RTCSessionDescriptionInit) => Promise, + addIceCandidate?: (candidate: RTCIceCandidate) => Promise) { + const setup = await createSetup(); + if (!offer) { + const offer = await session.createLocalDescription('offer', setup, addIceCandidate); + const answer = await setRemoteDescription(offer); + await session.setRemoteDescription(answer, setup); + } + else { + await session.setRemoteDescription(offer, setup); + const answer = await session.createLocalDescription('answer', setup, addIceCandidate); + await setRemoteDescription(answer); + } +} + +export class BrowserSignalingSession implements RTCSignalingSession { + hasSetup = false; + options: RTCSignalingChannelOptions = { + capabilities: { + audio: RTCRtpReceiver.getCapabilities('audio'), + video: RTCRtpReceiver.getCapabilities('video'), + } + }; + + constructor(public pc: RTCPeerConnection, cleanup: () => void) { + const checkConn = () => { + if (pc.iceConnectionState === 'disconnected' + || pc.iceConnectionState === 'failed' + || pc.iceConnectionState === 'closed') { + cleanup(); + } + if (pc.connectionState === 'closed' + || pc.connectionState === 'disconnected' + || pc.connectionState === 'failed') { + cleanup(); + } + } + + pc.addEventListener('connectionstatechange', checkConn); + pc.addEventListener('iceconnectionstatechange', checkConn); + } + + createPeerConnection(setup: RTCAVSignalingSetup) { + if (this.hasSetup) + return; + this.hasSetup = true; + if (setup.datachannel) + this.pc.createDataChannel(setup.datachannel.label, setup.datachannel.dict); + this.pc.addTransceiver('audio', setup.audio); + this.pc.addTransceiver('video', setup.video); + } + + async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate) { + this.createPeerConnection(setup); + + const gatheringPromise = new Promise(resolve => this.pc.onicegatheringstatechange = () => { + if (this.pc.iceGatheringState === 'complete') + resolve(undefined); + }); + + if (sendIceCandidate) { + this.pc.onicecandidate = ev => { + if (ev.candidate) { + console.log("local candidate", ev.candidate); + sendIceCandidate(JSON.parse(JSON.stringify(ev.candidate))); + } + } + } + + const toDescription = (init: RTCSessionDescriptionInit) => { + return { + type: init.type, + sdp: init.sdp, + } + } + + if (type === 'offer') { + let offer = await this.pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }); + const set = this.pc.setLocalDescription(offer); + if (sendIceCandidate) + return toDescription(offer); + await set; + await gatheringPromise; + offer = await this.pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }); + return toDescription(offer); + } + else { + let answer = await this.pc.createAnswer(); + const set = this.pc.setLocalDescription(answer); + if (sendIceCandidate) + return toDescription(answer); + await set; + await gatheringPromise; + answer = this.pc.currentLocalDescription || answer; + return toDescription(answer); + } + } + + async setRemoteDescription(description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup) { + await this.pc.setRemoteDescription(description); + + } + async addIceCandidate(candidate: RTCIceCandidateInit) { + console.log("remote candidate", candidate); + await this.pc.addIceCandidate(candidate); + } +} diff --git a/common/src/wrtc-to-rtsp.ts b/common/src/wrtc-to-rtsp.ts index 956d864ce..32f27d066 100644 --- a/common/src/wrtc-to-rtsp.ts +++ b/common/src/wrtc-to-rtsp.ts @@ -1,4 +1,4 @@ -import { RTCSignalingSession, RTCAVSignalingSetup, RTCSignalingChannel, FFMpegInput, MediaStreamOptions } from "@scrypted/sdk/types"; +import { RTCAVSignalingSetup, RTCSignalingChannel, FFMpegInput, MediaStreamOptions } from "@scrypted/sdk/types"; import { listenZeroSingleClient } from "./listen-cluster"; import { RTCPeerConnection, RTCRtpCodecParameters } from "@koush/werift"; import dgram from 'dgram'; @@ -55,23 +55,6 @@ export function getRTCMediaStreamOptions(id: string, name: string): MediaStreamO }; } -export async function startRTCSignalingSession(session: RTCSignalingSession, offer: RTCSessionDescriptionInit, - createSetup: () => Promise, - sendRemoteDescription: (remoteDescription: RTCSessionDescriptionInit) => Promise, - sendCandidate?: (candidate: RTCIceCandidate) => Promise) { - const setup = await createSetup(); - if (!offer) { - const offer = await session.createLocalDescription('offer', setup, sendCandidate); - const answer = await sendRemoteDescription(offer); - await session.setRemoteDescription(answer, setup); - } - else { - await session.setRemoteDescription(offer, setup); - const answer = await session.createLocalDescription('answer', setup, sendCandidate); - await sendRemoteDescription(answer); - } -} - export async function createRTCPeerConnectionSource(channel: ScryptedDeviceBase & RTCSignalingChannel, id: string): Promise { const { console, name } = channel; const videoPort = Math.round(Math.random() * 10000 + 30000); @@ -228,7 +211,7 @@ export async function createRTCPeerConnectionSource(channel: ScryptedDeviceBase rtspServer.sdp = createSdpInput(audioPort, videoPort, description.sdp); await rtspServer.handleSetup(); }, - onIceCandidate: async (candidate: RTCIceCandidateInit) => { + addIceCandidate: async (candidate: RTCIceCandidateInit) => { await pc.addIceCandidate(candidate as RTCIceCandidate); } }); diff --git a/docs/plugins/google-home/cast-receiver/.gitignore b/docs/plugins/google-home/cast-receiver/.gitignore new file mode 100644 index 000000000..02547828b --- /dev/null +++ b/docs/plugins/google-home/cast-receiver/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist/*.map diff --git a/docs/plugins/google-home/cast-receiver/cast.js b/docs/plugins/google-home/cast-receiver/cast.js index 4fcc69eb0..425adbd13 100644 --- a/docs/plugins/google-home/cast-receiver/cast.js +++ b/docs/plugins/google-home/cast-receiver/cast.js @@ -1,7 +1,5 @@ - -async function sleep(ms) { - await new Promise((resolve) => setTimeout(resolve, ms)); -} +import { RpcPeer } from './dist/rpc.js'; +import { BrowserSignalingSession } from './dist/rtc-signaling.js'; document.addEventListener("DOMContentLoaded", function (event) { const options = new cast.framework.CastReceiverOptions(); @@ -37,92 +35,23 @@ document.addEventListener("DOMContentLoaded", function (event) { token, })); - socket.once('message', async (data) => { - const avsource = JSON.parse(data); - console.log(avsource); - - const pc = new RTCPeerConnection(); - - const iceDone = new Promise(resolve => { - pc.onicecandidate = evt => { - if (!evt.candidate) { - resolve(undefined); - } - } - }); - - if (avsource.datachannel) - pc.createDataChannel(avsource.datachannel.label, avsource.datachannel.dict); - // it's possible to do talkback to ring. - let useAudioTransceiver = false; - if (avsource.audio?.direction === 'sendrecv') { - try { - // doing sendrecv on safari requires a mic be attached, or it fails to connect. - const mic = await navigator.mediaDevices.getUserMedia({ video: false, audio: true }) - for (const track of mic.getTracks()) { - pc.addTrack(track); - } - } - catch (e) { - let silence = () => { - let ctx = new AudioContext(), oscillator = ctx.createOscillator(); - let dst = oscillator.connect(ctx.createMediaStreamDestination()); - oscillator.start(); - return Object.assign(dst.stream.getAudioTracks()[0], { enabled: false }); - } - pc.addTrack(silence()); - } + const rpcPeer = new RpcPeer('cast-receiver', 'scrypted-server', (message, reject) => { + try { + socket.send(JSON.stringify(message)); } - else { - useAudioTransceiver = true; + catch (e) { + reject?.(e); } - if (useAudioTransceiver) - pc.addTransceiver("audio", avsource.audio); - pc.addTransceiver("video", avsource.video); + }); + socket.on('message', data => { + rpcPeer.handleMessage(JSON.parse(data)); + }); - const checkConn = () => { - console.log(pc.connectionState, pc.iceConnectionState); - if (pc.iceConnectionState === 'failed' || pc.connectionState === 'failed') { - window.close(); - } - } + const pc = new RTCPeerConnection(); - pc.onconnectionstatechange = checkConn; - pc.onsignalingstatechange = checkConn; - pc.ontrack = () => { - const mediaStream = new MediaStream( - pc.getReceivers().map((receiver) => receiver.track) - ); - video.srcObject = mediaStream; - const remoteAudio = document.createElement("audio"); - remoteAudio.srcObject = mediaStream; - remoteAudio.play(); - }; - - let offer = await pc.createOffer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true - }); - await pc.setLocalDescription(offer); - await iceDone; - offer = await pc.createOffer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true - }); - await pc.setLocalDescription(offer); - const message = { - token, - offer: { - description: offer, - } - }; - socket.send(JSON.stringify(message)); - - socket.once('message', async (data) => { - const json = JSON.parse(data); - await pc.setRemoteDescription(json.description); - }) - }) + const session = new BrowserSignalingSession(pc, () => window.close()); + rpcPeer.params['session'] = session; + rpcPeer.params['options'] = session.options; }); return null; diff --git a/docs/plugins/google-home/cast-receiver/dist/rpc.js b/docs/plugins/google-home/cast-receiver/dist/rpc.js new file mode 100644 index 000000000..23138a661 --- /dev/null +++ b/docs/plugins/google-home/cast-receiver/dist/rpc.js @@ -0,0 +1,470 @@ +export function startPeriodicGarbageCollection() { + if (!global.gc) { + console.warn('rpc peer garbage collection not available: global.gc is not exposed.'); + return; + } + try { + const g = global; + if (g.gc) { + return setInterval(() => { + g.gc(); + }, 10000); + } + } + catch (e) { + } +} +class RpcProxy { + peer; + entry; + constructorName; + proxyProps; + proxyOneWayMethods; + constructor(peer, entry, constructorName, proxyProps, proxyOneWayMethods) { + this.peer = peer; + this.entry = entry; + this.constructorName = constructorName; + this.proxyProps = proxyProps; + this.proxyOneWayMethods = proxyOneWayMethods; + } + toPrimitive() { + const peer = this.peer; + return `RpcProxy-${peer.selfName}:${peer.peerName}: ${this.constructorName}`; + } + get(target, p, receiver) { + if (p === '__proxy_id') + return this.entry.id; + if (p === '__proxy_constructor') + return this.constructorName; + if (p === '__proxy_peer') + return this.peer; + if (p === RpcPeer.PROPERTY_PROXY_PROPERTIES) + return this.proxyProps; + if (p === RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS) + return this.proxyOneWayMethods; + if (p === RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION || p === RpcPeer.PROPERTY_JSON_COPY_SERIALIZE_CHILDREN) + return; + if (p === 'then') + return; + if (p === 'constructor') + return; + if (this.proxyProps?.[p] !== undefined) + return this.proxyProps?.[p]; + const handled = RpcPeer.handleFunctionInvocations(this, target, p, receiver); + if (handled) + return handled; + return new Proxy(() => p, this); + } + set(target, p, value, receiver) { + if (p === RpcPeer.finalizerIdSymbol) + this.entry.finalizerId = value; + return true; + } + apply(target, thisArg, argArray) { + if (Object.isFrozen(this.peer.pendingResults)) + return Promise.reject(new RPCResultError(this.peer, 'RpcPeer has been killed')); + // rpc objects can be functions. if the function is a oneway method, + // it will have a null in the oneway method list. this is because + // undefined is not JSON serializable. + const method = target() || null; + const args = []; + for (const arg of (argArray || [])) { + args.push(this.peer.serialize(arg)); + } + const rpcApply = { + type: "apply", + id: undefined, + proxyId: this.entry.id, + args, + method, + }; + if (this.proxyOneWayMethods?.includes?.(method)) { + rpcApply.oneway = true; + this.peer.send(rpcApply); + return Promise.resolve(); + } + return this.peer.createPendingResult((id, reject) => { + rpcApply.id = id; + this.peer.send(rpcApply, reject); + }); + } +} +// todo: error constructor adds a "cause" variable in Chrome 93, Node v?? +export class RPCResultError extends Error { + cause; + constructor(peer, message, cause, options) { + super(`${peer.selfName}:${peer.peerName}: ${message}`); + this.cause = cause; + if (options?.name) { + this.name = options?.name; + } + if (options?.stack) { + this.stack = `${peer.peerName}:${peer.selfName}\n${cause?.stack || options.stack}`; + } + } +} +function compileFunction(code, params, options) { + params = params || []; + const f = `(function(${params.join(',')}) {;${code};})`; + return eval(f); +} +try { + const fr = FinalizationRegistry; +} +catch (e) { + window.WeakRef = class WeakRef { + target; + constructor(target) { + this.target = target; + } + deref() { + return this.target; + } + }; + window.FinalizationRegistry = class FinalizationRegistry { + register() { + } + }; +} +export class RpcPeer { + selfName; + peerName; + send; + idCounter = 1; + onOob; + params = {}; + pendingResults = {}; + proxyCounter = 1; + localProxied = new Map(); + localProxyMap = {}; + remoteWeakProxies = {}; + finalizers = new FinalizationRegistry(entry => this.finalize(entry)); + nameDeserializerMap = new Map(); + constructorSerializerMap = new Map(); + transportSafeArgumentTypes = RpcPeer.getDefaultTransportSafeArgumentTypes(); + static finalizerIdSymbol = Symbol('rpcFinalizerId'); + static getDefaultTransportSafeArgumentTypes() { + const jsonSerializable = new Set(); + jsonSerializable.add(Number.name); + jsonSerializable.add(String.name); + jsonSerializable.add(Object.name); + jsonSerializable.add(Boolean.name); + jsonSerializable.add(Array.name); + return jsonSerializable; + } + static handleFunctionInvocations(thiz, target, p, receiver) { + if (p === 'apply') { + return (thisArg, args) => { + return thiz.apply(target, thiz, args); + }; + } + else if (p === 'call') { + return (thisArg, ...args) => { + return thiz.apply(target, thiz, args); + }; + } + else if (p === 'toString' || p === Symbol.toPrimitive) { + return (thisArg, ...args) => { + return thiz.toPrimitive(); + }; + } + } + static PROPERTY_PROXY_ONEWAY_METHODS = '__proxy_oneway_methods'; + static PROPERTY_JSON_DISABLE_SERIALIZATION = '__json_disable_serialization'; + static PROPERTY_PROXY_PROPERTIES = '__proxy_props'; + static PROPERTY_JSON_COPY_SERIALIZE_CHILDREN = '__json_copy_serialize_children'; + constructor(selfName, peerName, send) { + this.selfName = selfName; + this.peerName = peerName; + this.send = send; + } + createPendingResult(cb) { + if (Object.isFrozen(this.pendingResults)) + return Promise.reject(new RPCResultError(this, 'RpcPeer has been killed')); + const promise = new Promise((resolve, reject) => { + const id = (this.idCounter++).toString(); + this.pendingResults[id] = { resolve, reject }; + cb(id, e => reject(new RPCResultError(this, e.message, e))); + }); + // todo: make this an option so rpc doesn't nuke the process if uncaught? + promise.catch(() => { }); + return promise; + } + kill(message) { + const error = new RPCResultError(this, message || 'peer was killed'); + for (const result of Object.values(this.pendingResults)) { + result.reject(error); + } + this.pendingResults = Object.freeze({}); + this.remoteWeakProxies = Object.freeze({}); + this.localProxyMap = Object.freeze({}); + this.localProxied.clear(); + } + // need a name/constructor map due to babel name mangling? fix somehow? + addSerializer(ctr, name, serializer) { + this.nameDeserializerMap.set(name, serializer); + this.constructorSerializerMap.set(ctr, name); + } + finalize(entry) { + delete this.remoteWeakProxies[entry.id]; + const rpcFinalize = { + __local_proxy_id: entry.id, + __local_proxy_finalizer_id: entry.finalizerId, + type: 'finalize', + }; + this.send(rpcFinalize); + } + async getParam(param) { + return this.createPendingResult((id, reject) => { + const paramMessage = { + id, + type: 'param', + param, + }; + this.send(paramMessage, reject); + }); + } + sendOob(oob) { + this.send({ + type: 'oob', + oob, + }); + } + evalLocal(script, filename, coercedParams) { + const params = Object.assign({}, this.params, coercedParams); + let compile; + try { + compile = require('vm').compileFunction; + ; + } + catch (e) { + compile = compileFunction; + } + const f = compile(script, Object.keys(params), { + filename, + }); + const value = f(...Object.values(params)); + return value; + } + createErrorResult(result, e) { + result.stack = e.stack || 'no stack'; + result.result = e.name || 'no name'; + result.message = e.message || 'no message'; + } + deserialize(value) { + if (!value) + return value; + const copySerializeChildren = value[RpcPeer.PROPERTY_JSON_COPY_SERIALIZE_CHILDREN]; + if (copySerializeChildren) { + const ret = {}; + for (const [key, val] of Object.entries(value)) { + ret[key] = this.deserialize(val); + } + return ret; + } + const { __remote_proxy_id, __remote_proxy_finalizer_id, __local_proxy_id, __remote_constructor_name, __serialized_value, __remote_proxy_props, __remote_proxy_oneway_methods } = value; + if (__remote_proxy_id) { + let proxy = this.remoteWeakProxies[__remote_proxy_id]?.deref(); + if (!proxy) + proxy = this.newProxy(__remote_proxy_id, __remote_constructor_name, __remote_proxy_props, __remote_proxy_oneway_methods); + proxy[RpcPeer.finalizerIdSymbol] = __remote_proxy_finalizer_id; + return proxy; + } + if (__local_proxy_id) { + const ret = this.localProxyMap[__local_proxy_id]; + if (!ret) + throw new RPCResultError(this, `invalid local proxy id ${__local_proxy_id}`); + return ret; + } + const deserializer = this.nameDeserializerMap.get(__remote_constructor_name); + if (deserializer) { + return deserializer.deserialize(__serialized_value); + } + return value; + } + serialize(value) { + if (value?.[RpcPeer.PROPERTY_JSON_COPY_SERIALIZE_CHILDREN] === true) { + const ret = {}; + for (const [key, val] of Object.entries(value)) { + ret[key] = this.serialize(val); + } + return ret; + } + if (!value || (!value[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION] && this.transportSafeArgumentTypes.has(value.constructor?.name))) { + return value; + } + let __remote_constructor_name = value.__proxy_constructor || value.constructor?.name?.toString(); + let proxiedEntry = this.localProxied.get(value); + if (proxiedEntry) { + const __remote_proxy_finalizer_id = (this.proxyCounter++).toString(); + proxiedEntry.finalizerId = __remote_proxy_finalizer_id; + const ret = { + __remote_proxy_id: proxiedEntry.id, + __remote_proxy_finalizer_id, + __remote_constructor_name, + __remote_proxy_props: value?.[RpcPeer.PROPERTY_PROXY_PROPERTIES], + __remote_proxy_oneway_methods: value?.[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS], + }; + return ret; + } + const { __proxy_id, __proxy_peer } = value; + if (__proxy_id && __proxy_peer === this) { + const ret = { + __local_proxy_id: __proxy_id, + }; + return ret; + } + const serializerMapName = this.constructorSerializerMap.get(value.constructor); + if (serializerMapName) { + __remote_constructor_name = serializerMapName; + const serializer = this.nameDeserializerMap.get(serializerMapName); + if (!serializer) + throw new Error('serializer not found for ' + serializerMapName); + const serialized = serializer.serialize(value); + const ret = { + __remote_proxy_id: undefined, + __remote_proxy_finalizer_id: undefined, + __remote_constructor_name, + __remote_proxy_props: value?.[RpcPeer.PROPERTY_PROXY_PROPERTIES], + __remote_proxy_oneway_methods: value?.[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS], + __serialized_value: serialized, + }; + return ret; + } + const __remote_proxy_id = (this.proxyCounter++).toString(); + proxiedEntry = { + id: __remote_proxy_id, + finalizerId: __remote_proxy_id, + }; + this.localProxied.set(value, proxiedEntry); + this.localProxyMap[__remote_proxy_id] = value; + const ret = { + __remote_proxy_id, + __remote_proxy_finalizer_id: __remote_proxy_id, + __remote_constructor_name, + __remote_proxy_props: value?.[RpcPeer.PROPERTY_PROXY_PROPERTIES], + __remote_proxy_oneway_methods: value?.[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS], + }; + return ret; + } + newProxy(proxyId, proxyConstructorName, proxyProps, proxyOneWayMethods) { + const localProxiedEntry = { + id: proxyId, + finalizerId: undefined, + }; + const rpc = new RpcProxy(this, localProxiedEntry, proxyConstructorName, proxyProps, proxyOneWayMethods); + const target = proxyConstructorName === 'Function' || proxyConstructorName === 'AsyncFunction' ? function () { } : rpc; + const proxy = new Proxy(target, rpc); + const weakref = new WeakRef(proxy); + this.remoteWeakProxies[proxyId] = weakref; + this.finalizers.register(rpc, localProxiedEntry); + return proxy; + } + async handleMessage(message) { + try { + switch (message.type) { + case 'param': { + const rpcParam = message; + const result = { + type: 'result', + id: rpcParam.id, + result: this.serialize(this.params[rpcParam.param]) + }; + this.send(result); + break; + } + case 'apply': { + const rpcApply = message; + const result = { + type: 'result', + id: rpcApply.id || '', + }; + try { + const target = this.localProxyMap[rpcApply.proxyId]; + if (!target) + throw new Error(`proxy id ${rpcApply.proxyId} not found`); + const args = []; + for (const arg of (rpcApply.args || [])) { + args.push(this.deserialize(arg)); + } + let value; + if (rpcApply.method) { + const method = target[rpcApply.method]; + if (!method) + throw new Error(`target ${target?.constructor?.name} does not have method ${rpcApply.method}`); + value = await target[rpcApply.method](...args); + } + else { + value = await target(...args); + } + result.result = this.serialize(value); + } + catch (e) { + console.error('failure', rpcApply.method, e); + this.createErrorResult(result, e); + } + if (!rpcApply.oneway) + this.send(result); + break; + } + case 'result': { + const rpcResult = message; + const deferred = this.pendingResults[rpcResult.id]; + delete this.pendingResults[rpcResult.id]; + if (!deferred) + throw new Error(`unknown result ${rpcResult.id}`); + if (rpcResult.message || rpcResult.stack) { + const e = new RPCResultError(this, rpcResult.message || 'no message', undefined, { + name: rpcResult.result, + stack: rpcResult.stack, + }); + deferred.reject(e); + return; + } + deferred.resolve(this.deserialize(rpcResult.result)); + break; + } + case 'finalize': { + const rpcFinalize = message; + const local = this.localProxyMap[rpcFinalize.__local_proxy_id]; + if (local) { + const localProxiedEntry = this.localProxied.get(local); + // if a finalizer id is specified, it must match. + if (rpcFinalize.__local_proxy_finalizer_id && rpcFinalize.__local_proxy_finalizer_id !== localProxiedEntry?.finalizerId) { + break; + } + delete this.localProxyMap[rpcFinalize.__local_proxy_id]; + this.localProxied.delete(local); + } + break; + } + case 'oob': { + const rpcOob = message; + this.onOob?.(rpcOob.oob); + break; + } + default: + throw new Error(`unknown rpc message type ${message.type}`); + } + } + catch (e) { + console.error('unhandled rpc error', this.peerName, e); + return; + } + } +} +export function getEvalSource() { + return ` + (() => { + ${RpcProxy} + + ${RpcPeer} + + return { + RpcPeer, + RpcProxy, + }; + })(); + `; +} +//# sourceMappingURL=rpc.js.map \ No newline at end of file diff --git a/docs/plugins/google-home/cast-receiver/dist/rtc-signaling.js b/docs/plugins/google-home/cast-receiver/dist/rtc-signaling.js new file mode 100644 index 000000000..30c4e71f0 --- /dev/null +++ b/docs/plugins/google-home/cast-receiver/dist/rtc-signaling.js @@ -0,0 +1,104 @@ +export async function startRTCSignalingSession(session, offer, createSetup, setRemoteDescription, addIceCandidate) { + const setup = await createSetup(); + if (!offer) { + const offer = await session.createLocalDescription('offer', setup, addIceCandidate); + const answer = await setRemoteDescription(offer); + await session.setRemoteDescription(answer, setup); + } + else { + await session.setRemoteDescription(offer, setup); + const answer = await session.createLocalDescription('answer', setup, addIceCandidate); + await setRemoteDescription(answer); + } +} +export class BrowserSignalingSession { + pc; + hasSetup = false; + options = { + capabilities: { + audio: RTCRtpReceiver.getCapabilities('audio'), + video: RTCRtpReceiver.getCapabilities('video'), + } + }; + constructor(pc, cleanup) { + this.pc = pc; + const checkConn = () => { + if (pc.iceConnectionState === 'disconnected' + || pc.iceConnectionState === 'failed' + || pc.iceConnectionState === 'closed') { + cleanup(); + } + if (pc.connectionState === 'closed' + || pc.connectionState === 'disconnected' + || pc.connectionState === 'failed') { + cleanup(); + } + }; + pc.addEventListener('connectionstatechange', checkConn); + pc.addEventListener('iceconnectionstatechange', checkConn); + } + createPeerConnection(setup) { + if (this.hasSetup) + return; + this.hasSetup = true; + if (setup.datachannel) + this.pc.createDataChannel(setup.datachannel.label, setup.datachannel.dict); + this.pc.addTransceiver('audio', setup.audio); + this.pc.addTransceiver('video', setup.video); + } + async createLocalDescription(type, setup, sendIceCandidate) { + this.createPeerConnection(setup); + const gatheringPromise = new Promise(resolve => this.pc.onicegatheringstatechange = () => { + if (this.pc.iceGatheringState === 'complete') + resolve(undefined); + }); + if (sendIceCandidate) { + this.pc.onicecandidate = ev => { + if (ev.candidate) { + console.log("local candidate", ev.candidate); + sendIceCandidate(JSON.parse(JSON.stringify(ev.candidate))); + } + }; + } + const toDescription = (init) => { + return { + type: init.type, + sdp: init.sdp, + }; + }; + if (type === 'offer') { + let offer = await this.pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }); + const set = this.pc.setLocalDescription(offer); + if (sendIceCandidate) + return toDescription(offer); + await set; + await gatheringPromise; + offer = await this.pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }); + return toDescription(offer); + } + else { + let answer = await this.pc.createAnswer(); + const set = this.pc.setLocalDescription(answer); + if (sendIceCandidate) + return toDescription(answer); + await set; + await gatheringPromise; + answer = this.pc.currentLocalDescription || answer; + return toDescription(answer); + } + } + async setRemoteDescription(description, setup) { + await this.pc.setRemoteDescription(description); + } + async addIceCandidate(candidate) { + console.log("remote candidate", candidate); + await this.pc.addIceCandidate(candidate); + } +} +//# sourceMappingURL=rtc-signaling.js.map \ No newline at end of file diff --git a/docs/plugins/google-home/cast-receiver/index.html b/docs/plugins/google-home/cast-receiver/index.html index 6835afbd9..d5f6b0ae4 100644 --- a/docs/plugins/google-home/cast-receiver/index.html +++ b/docs/plugins/google-home/cast-receiver/index.html @@ -4,7 +4,7 @@ src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"> - + diff --git a/docs/plugins/google-home/cast-receiver/package-lock.json b/docs/plugins/google-home/cast-receiver/package-lock.json new file mode 100644 index 000000000..3e22188f1 --- /dev/null +++ b/docs/plugins/google-home/cast-receiver/package-lock.json @@ -0,0 +1,97 @@ +{ + "name": "cast-receiver", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "@scrypted/sdk": "file:../../../../sdk" + }, + "devDependencies": { + "@types/node": "^17.0.18" + } + }, + "../../../../sdk": { + "version": "0.0.173", + "license": "ISC", + "dependencies": { + "@babel/preset-typescript": "^7.16.7", + "adm-zip": "^0.4.13", + "axios": "^0.21.4", + "babel-loader": "^8.2.3", + "babel-plugin-const-enum": "^1.1.0", + "esbuild": "^0.13.8", + "ncp": "^2.0.0", + "raw-loader": "^4.0.2", + "rimraf": "^3.0.2", + "tmp": "^0.2.1", + "webpack": "^5.59.0" + }, + "bin": { + "scrypted-debug": "bin/scrypted-debug.js", + "scrypted-deploy": "bin/scrypted-deploy.js", + "scrypted-deploy-debug": "bin/scrypted-deploy-debug.js", + "scrypted-package-json": "bin/scrypted-package-json.js", + "scrypted-readme": "bin/scrypted-readme.js", + "scrypted-webpack": "bin/scrypted-webpack.js" + }, + "devDependencies": { + "@types/node": "^16.11.1", + "@types/stringify-object": "^4.0.0", + "stringify-object": "^3.3.0", + "ts-node": "^10.4.0", + "typedoc": "^0.22.8", + "typescript-json-schema": "^0.50.1", + "webpack-bundle-analyzer": "^4.5.0" + } + }, + "../../../../sdk/types": { + "name": "@scrypted/types", + "version": "0.0.9", + "extraneous": true, + "license": "ISC", + "devDependencies": {} + }, + "node_modules/@scrypted/sdk": { + "resolved": "../../../../sdk", + "link": true + }, + "node_modules/@types/node": { + "version": "17.0.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz", + "integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==", + "dev": true + } + }, + "dependencies": { + "@scrypted/sdk": { + "version": "file:../../../../sdk", + "requires": { + "@babel/preset-typescript": "^7.16.7", + "@types/node": "^16.11.1", + "@types/stringify-object": "^4.0.0", + "adm-zip": "^0.4.13", + "axios": "^0.21.4", + "babel-loader": "^8.2.3", + "babel-plugin-const-enum": "^1.1.0", + "esbuild": "^0.13.8", + "ncp": "^2.0.0", + "raw-loader": "^4.0.2", + "rimraf": "^3.0.2", + "stringify-object": "^3.3.0", + "tmp": "^0.2.1", + "ts-node": "^10.4.0", + "typedoc": "^0.22.8", + "typescript-json-schema": "^0.50.1", + "webpack": "^5.59.0", + "webpack-bundle-analyzer": "^4.5.0" + } + }, + "@types/node": { + "version": "17.0.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz", + "integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==", + "dev": true + } + } +} diff --git a/docs/plugins/google-home/cast-receiver/package.json b/docs/plugins/google-home/cast-receiver/package.json new file mode 100644 index 000000000..4fa078ebc --- /dev/null +++ b/docs/plugins/google-home/cast-receiver/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "@scrypted/sdk": "file:../../../../sdk" + }, + "devDependencies": { + "@types/node": "^17.0.18" + } +} diff --git a/docs/plugins/google-home/cast-receiver/src/rpc.ts b/docs/plugins/google-home/cast-receiver/src/rpc.ts new file mode 120000 index 000000000..51bea0432 --- /dev/null +++ b/docs/plugins/google-home/cast-receiver/src/rpc.ts @@ -0,0 +1 @@ +../../../../../server/src/rpc.ts \ No newline at end of file diff --git a/docs/plugins/google-home/cast-receiver/src/rtc-signaling.ts b/docs/plugins/google-home/cast-receiver/src/rtc-signaling.ts new file mode 120000 index 000000000..8d53ebef5 --- /dev/null +++ b/docs/plugins/google-home/cast-receiver/src/rtc-signaling.ts @@ -0,0 +1 @@ +../../../../../common/src/rtc-signaling.ts \ No newline at end of file diff --git a/docs/plugins/google-home/cast-receiver/tsconfig.json b/docs/plugins/google-home/cast-receiver/tsconfig.json new file mode 100644 index 000000000..fc0abdced --- /dev/null +++ b/docs/plugins/google-home/cast-receiver/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "module": "ESNext", + "target": "esnext", + "noImplicitAny": true, + "outDir": "./dist", + "esModuleInterop": true, + // skip error: Interface 'WebGL2RenderingContext' incorrectly extends interface 'WebGL2RenderingContextBase'. + // https://github.com/tensorflow/tfjs/issues/4201 + "skipLibCheck": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], +} \ No newline at end of file diff --git a/packages/client/package-lock.json b/packages/client/package-lock.json index 2077e504e..ee7696e19 100644 --- a/packages/client/package-lock.json +++ b/packages/client/package-lock.json @@ -10,7 +10,6 @@ "license": "ISC", "dependencies": { "@scrypted/rpc": "^1.0.4", - "@scrypted/types": "^0.0.6", "adm-zip": "^0.5.9", "axios": "^0.25.0", "engine.io-client": "^5.2.0", @@ -24,17 +23,22 @@ "@types/node": "^17.0.17" } }, + "../../sdk/types": { + "name": "@scrypted/types", + "version": "0.0.9", + "extraneous": true, + "license": "ISC", + "devDependencies": {} + }, + "../sdk/types": { + "extraneous": true + }, "node_modules/@scrypted/rpc": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@scrypted/rpc/-/rpc-1.0.5.tgz", "integrity": "sha512-bHnJKmCXs0Hjunc0XceNsVCagX/VplMo18XlMZm5OHMyCZB55AzUkadrICA5xt/oYK6JXCjZH2FSNWVTVg6tGQ==", "deprecated": "deprecated" }, - "node_modules/@scrypted/types": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.0.6.tgz", - "integrity": "sha512-r/attybPcJvBNll3g+k8i2jQwQiu0izoBazZ+Kvsdeayr3Mbzm1NaBkwbUPICroWJKY+jlfoaZSQt4eGTX+vog==" - }, "node_modules/@types/adm-zip": { "version": "0.4.34", "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz", @@ -335,11 +339,6 @@ "resolved": "https://registry.npmjs.org/@scrypted/rpc/-/rpc-1.0.5.tgz", "integrity": "sha512-bHnJKmCXs0Hjunc0XceNsVCagX/VplMo18XlMZm5OHMyCZB55AzUkadrICA5xt/oYK6JXCjZH2FSNWVTVg6tGQ==" }, - "@scrypted/types": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.0.6.tgz", - "integrity": "sha512-r/attybPcJvBNll3g+k8i2jQwQiu0izoBazZ+Kvsdeayr3Mbzm1NaBkwbUPICroWJKY+jlfoaZSQt4eGTX+vog==" - }, "@types/adm-zip": { "version": "0.4.34", "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz", diff --git a/packages/client/package.json b/packages/client/package.json index 7d35e6242..11aeb4b31 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -18,7 +18,6 @@ }, "dependencies": { "@scrypted/rpc": "^1.0.4", - "@scrypted/types": "^0.0.6", "adm-zip": "^0.5.9", "axios": "^0.25.0", "engine.io-client": "^5.2.0", diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index ac262c4d3..af7911356 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1,5 +1,5 @@ -import { ScryptedStatic } from "@scrypted/types"; -export * from "@scrypted/types"; +import { ScryptedStatic } from "../../../sdk/types/index"; +export * from "../../../sdk/types/index"; import { SocketOptions } from 'engine.io-client'; import eio from 'engine.io-client'; import { attachPluginRemote } from '../../../server/src/plugin/plugin-remote'; diff --git a/plugins/core/package-lock.json b/plugins/core/package-lock.json index c7291931d..5ca35ca3b 100644 --- a/plugins/core/package-lock.json +++ b/plugins/core/package-lock.json @@ -10,8 +10,10 @@ "license": "Apache-2.0", "dependencies": { "@koush/wrtc": "^0.5.2", + "@scrypted/common": "../../common", "@scrypted/sdk": "file:../../sdk", "mime": "^3.0.0", + "query-string": "^7.1.1", "router": "^1.3.6", "typescript": "^4.5.5" }, @@ -70,9 +72,22 @@ "babel-plugin-minify-dead-code-elimination": "^0.5.1" } }, + "../../common": { + "name": "@scrypted/common", + "version": "1.0.1", + "license": "ISC", + "dependencies": { + "@koush/werift": "file:../external/werift/packages/webrtc", + "@scrypted/sdk": "file:../sdk", + "typescript": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^16.9.0" + } + }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.0.169", + "version": "0.0.173", "license": "ISC", "dependencies": { "@babel/preset-typescript": "^7.16.7", @@ -156,6 +171,10 @@ "node": ">=10" } }, + "node_modules/@scrypted/common": { + "resolved": "../../common", + "link": true + }, "node_modules/@scrypted/sdk": { "resolved": "../../sdk", "link": true @@ -320,6 +339,14 @@ "node": ">=0.10.0" } }, + "node_modules/decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "engines": { + "node": ">=0.10" + } + }, "node_modules/define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -369,6 +396,14 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", @@ -960,6 +995,23 @@ "node": ">=0.10.0" } }, + "node_modules/query-string": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", + "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", + "dependencies": { + "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -1137,6 +1189,22 @@ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz", "integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==" }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", + "engines": { + "node": ">=4" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1375,6 +1443,15 @@ } } }, + "@scrypted/common": { + "version": "file:../../common", + "requires": { + "@koush/werift": "file:../external/werift/packages/webrtc", + "@scrypted/sdk": "file:../sdk", + "@types/node": "^16.9.0", + "typescript": "^4.4.3" + } + }, "@scrypted/sdk": { "version": "file:../../sdk", "requires": { @@ -1523,6 +1600,11 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -1563,6 +1645,11 @@ "is-arrayish": "^0.2.1" } }, + "filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=" + }, "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", @@ -2003,6 +2090,17 @@ "pinkie": "^2.0.0" } }, + "query-string": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", + "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", + "requires": { + "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -2141,6 +2239,16 @@ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz", "integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==" }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", diff --git a/plugins/core/package.json b/plugins/core/package.json index 4395bf70a..a728daff2 100644 --- a/plugins/core/package.json +++ b/plugins/core/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@koush/wrtc": "^0.5.2", + "@scrypted/common": "../../common", "@scrypted/sdk": "file:../../sdk", "mime": "^3.0.0", "router": "^1.3.6", diff --git a/plugins/core/src/aggregate.ts b/plugins/core/src/aggregate.ts index 9d2c7ed60..296465e0c 100644 --- a/plugins/core/src/aggregate.ts +++ b/plugins/core/src/aggregate.ts @@ -1,7 +1,7 @@ import { EventListener, EventListenerRegister, FFMpegInput, LockState, MediaObject, MediaStreamOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes, VideoCamera } from "@scrypted/sdk"; import sdk from "@scrypted/sdk"; -import { startParserSession, ParserSession, createParserRebroadcaster, Rebroadcaster, createRebroadcaster } from "../../../common/src/ffmpeg-rebroadcast"; -import { createMpegTsParser, StreamParser } from "../../../common/src/stream-parser"; +import { startParserSession, ParserSession, createParserRebroadcaster, Rebroadcaster, createRebroadcaster } from "@scrypted/common/src/ffmpeg-rebroadcast"; +import { createMpegTsParser, StreamParser } from "@scrypted/common/src/stream-parser"; const { systemManager, mediaManager } = sdk; export interface AggregateDevice extends ScryptedDeviceBase { diff --git a/plugins/core/src/main.ts b/plugins/core/src/main.ts index dbc6fc4e0..768d72b8c 100644 --- a/plugins/core/src/main.ts +++ b/plugins/core/src/main.ts @@ -1,4 +1,4 @@ -import { ScryptedDeviceBase, HttpRequestHandler, HttpRequest, HttpResponse, EngineIOHandler, Device, DeviceProvider, ScryptedInterface, ScryptedDeviceType } from '@scrypted/sdk'; +import { ScryptedDeviceBase, HttpRequestHandler, HttpRequest, HttpResponse, EngineIOHandler, Device, DeviceProvider, ScryptedInterface, ScryptedDeviceType, RTCSignalingChannel, VideoCamera } from '@scrypted/sdk'; import sdk from '@scrypted/sdk'; const { systemManager, deviceManager, mediaManager, endpointManager } = sdk; import Router from 'router'; @@ -12,9 +12,10 @@ import { Automation } from './automation'; import { AggregateDevice, createAggregateDevice } from './aggregate'; import net from 'net'; import { Script } from './script'; -import { addBuiltins } from "../../../common/src/ffmpeg-to-wrtc"; +import { addBuiltins } from "@scrypted/common/src/ffmpeg-to-wrtc"; import { updatePluginsData } from './update-plugins'; import { MediaCore } from './media-core'; +import { startBrowserRTCSignaling } from "@scrypted/common/src/ffmpeg-to-wrtc"; addBuiltins(mediaManager); @@ -169,12 +170,16 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng return this.scripts.get(nativeId); } - async discoverDevices(duration: number) { + checkEngineIoEndpoint(request: HttpRequest, name: string) { + const check = `/endpoint/@scrypted/core/engine.io/${name}/`; + if (!request.url.startsWith(check)) + return null; + return check; } async checkService(request: HttpRequest, ws: WebSocket, name: string): Promise { - const check = `/endpoint/@scrypted/core/engine.io/${name}/`; - if (!request.url.startsWith(check)) + const check = this.checkEngineIoEndpoint(request, name); + if (!check) return false; const deviceId = request.url.substr(check.length).split('/')[0]; const plugins = await systemManager.getComponent('plugins'); @@ -197,6 +202,17 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng return; } + if (this.checkEngineIoEndpoint(request, 'videocamera')) { + const url = new URL(`http://localhost${request.url}`); + const deviceId = url.searchParams.get('deviceId'); + const camera = systemManager.getDeviceById(deviceId); + if (!camera) + ws.close(); + else + startBrowserRTCSignaling(camera, ws, this.console); + return; + } + if (request.isPublicEndpoint) { ws.close(); return; diff --git a/plugins/core/src/script.ts b/plugins/core/src/script.ts index 5d31b238c..7e0b8a061 100644 --- a/plugins/core/src/script.ts +++ b/plugins/core/src/script.ts @@ -1,5 +1,5 @@ import { Scriptable, Program, ScryptedDeviceBase, ScriptSource } from "@scrypted/sdk"; -import { createMonacoEvalDefaults } from "../../../common/src/scrypted-eval"; +import { createMonacoEvalDefaults } from "@scrypted/common/src/scrypted-eval"; import { scryptedEval } from "./scrypted-eval"; const monacoEvalDefaults = createMonacoEvalDefaults({}); diff --git a/plugins/core/src/scrypted-eval.ts b/plugins/core/src/scrypted-eval.ts index 2c2c4c860..f371120b0 100644 --- a/plugins/core/src/scrypted-eval.ts +++ b/plugins/core/src/scrypted-eval.ts @@ -1,5 +1,5 @@ import { ScryptedDeviceBase } from "@scrypted/sdk"; -import { scryptedEval as scryptedEvalBase } from "../../../common/src/scrypted-eval"; +import { scryptedEval as scryptedEvalBase } from "@scrypted/common/src/scrypted-eval"; export async function scryptedEval(device: ScryptedDeviceBase, script: string, params: { [name: string]: any }) { return scryptedEvalBase(device, script, {}, params); diff --git a/plugins/core/ui/package-lock.json b/plugins/core/ui/package-lock.json index 633dd77a6..1511f9c69 100644 --- a/plugins/core/ui/package-lock.json +++ b/plugins/core/ui/package-lock.json @@ -13,6 +13,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.2", "@fortawesome/vue-fontawesome": "^0.1.10", "@radial-color-picker/vue-color-picker": "^2.3.0", + "@scrypted/common": "file:../../../common", "@scrypted/sdk": "file:../../../sdk", "@scrypted/types": "file:../../../sdk/types", "apexcharts": "^3.28.3", @@ -93,6 +94,39 @@ "worker-loader": "^3.0.8" } }, + "../../../common": { + "name": "@scrypted/common", + "version": "1.0.1", + "license": "ISC", + "dependencies": { + "@koush/werift": "file:../external/werift/packages/webrtc", + "@scrypted/sdk": "file:../sdk", + "typescript": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^16.9.0" + } + }, + "../../../packages/client": { + "name": "@scrypted/client", + "version": "1.0.9", + "extraneous": true, + "license": "ISC", + "dependencies": { + "@scrypted/rpc": "^1.0.4", + "adm-zip": "^0.5.9", + "axios": "^0.25.0", + "engine.io-client": "^5.2.0", + "linkfs": "^2.1.0", + "memfs": "^3.4.1", + "rimraf": "^3.0.2" + }, + "devDependencies": { + "@types/adm-zip": "^0.4.34", + "@types/engine.io-client": "^3.1.5", + "@types/node": "^17.0.17" + } + }, "../../../sdk": { "name": "@scrypted/sdk", "version": "0.0.173", @@ -2159,6 +2193,10 @@ "vue": "^2.5.21" } }, + "node_modules/@scrypted/common": { + "resolved": "../../../common", + "link": true + }, "node_modules/@scrypted/sdk": { "resolved": "../../../sdk", "link": true @@ -21063,6 +21101,15 @@ "@radial-color-picker/rotator": "2.1.0" } }, + "@scrypted/common": { + "version": "file:../../../common", + "requires": { + "@koush/werift": "file:../external/werift/packages/webrtc", + "@scrypted/sdk": "file:../sdk", + "@types/node": "^16.9.0", + "typescript": "^4.4.3" + } + }, "@scrypted/sdk": { "version": "file:../../../sdk", "requires": { diff --git a/plugins/core/ui/package.json b/plugins/core/ui/package.json index ce0546223..4be6cf1b2 100644 --- a/plugins/core/ui/package.json +++ b/plugins/core/ui/package.json @@ -16,8 +16,9 @@ "@fortawesome/free-solid-svg-icons": "^5.15.2", "@fortawesome/vue-fontawesome": "^0.1.10", "@radial-color-picker/vue-color-picker": "^2.3.0", - "@scrypted/types": "file:../../../sdk/types", + "@scrypted/common": "file:../../../common", "@scrypted/sdk": "file:../../../sdk", + "@scrypted/types": "file:../../../sdk/types", "apexcharts": "^3.28.3", "axios": "^0.19.2", "core-js": "^2.6.12", diff --git a/plugins/core/ui/src/client.ts b/plugins/core/ui/src/client.ts index 9794cd216..42669ab58 100644 --- a/plugins/core/ui/src/client.ts +++ b/plugins/core/ui/src/client.ts @@ -3,11 +3,11 @@ import {connectScryptedClient} from '../../../../packages/client/src/index'; import axios from 'axios'; import store from './store'; -function hasValue(state, property) { +function hasValue(state: any, property: string) { return state[property] && state[property].value; } -function isValidDevice(id) { +function isValidDevice(id: string) { const state = store.state.systemState[id]; for (const property of [ "name", diff --git a/plugins/core/ui/src/common/camera.ts b/plugins/core/ui/src/common/camera.ts index ab9c24e53..dbb146934 100644 --- a/plugins/core/ui/src/common/camera.ts +++ b/plugins/core/ui/src/common/camera.ts @@ -1,223 +1,50 @@ -import { RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedInterface, ScryptedDevice, ScryptedMimeTypes, RTCAVMessage, MediaManager, VideoCamera, MediaObject, RTCAVSignalingSetup, RequestMediaStreamOptions, RTCSignalingChannel } from '@scrypted/types'; - -async function startCameraLegacy(mediaManager: MediaManager, device: ScryptedDevice & VideoCamera & RTCSignalingChannel) { - let selectedStream: RequestMediaStreamOptions; - try { - const streams = await device.getVideoStreamOptions(); - selectedStream = streams.find(stream => stream.container === 'rawvideo'); - } - catch (e) { - } - const videoStream = await device.getVideoStream(selectedStream); - - let json: RTCAVMessage; - - const offer = await mediaManager.convertMediaObjectToBuffer( - videoStream, - ScryptedMimeTypes.RTCAVOffer - ); - json = JSON.parse(offer.toString()); - let pc = new RTCPeerConnection(json.configuration); - - const processCandidates = (result: Buffer) => { - const message: RTCAVMessage = JSON.parse(result.toString()); - for (const candidate of message.candidates) { - // console.log('remote candidate', candidate); - pc.addIceCandidate(candidate); - } - }; - - (async () => { - await pc.setRemoteDescription(json.description); - const answer = await pc.createAnswer(); - await pc.setLocalDescription(answer); - const answerObject: RTCAVMessage = { - id: json.id, - candidates: [], - description: null, - configuration: json.configuration, - }; - answerObject.description = answer; - const mo = await mediaManager.createMediaObject( - Buffer.from(JSON.stringify(answerObject)), - ScryptedMimeTypes.RTCAVAnswer - ); - const result = await mediaManager.convertMediaObjectToBuffer( - mo, - ScryptedMimeTypes.RTCAVOffer - ); - processCandidates(result); - - const emptyObject: RTCAVMessage = { - id: json.id, - candidates: [], - description: null, - configuration: null, - }; - while (true) { - const mo = await mediaManager.createMediaObject( - Buffer.from(JSON.stringify(emptyObject)), - ScryptedMimeTypes.RTCAVAnswer - ); - const result = await mediaManager.convertMediaObjectToBuffer( - mo, - ScryptedMimeTypes.RTCAVOffer - ); - processCandidates(result); - } - })(); - console.log("done av offer"); - - pc.onicecandidate = async (evt) => { - if (!evt.candidate) { - return; - } - // console.log('local candidate', evt.candidate); - const candidateObject: RTCAVMessage = { - id: json.id, - candidates: [evt.candidate], - description: null, - configuration: null, - }; - const mo = await mediaManager.createMediaObject( - Buffer.from(JSON.stringify(candidateObject)), - ScryptedMimeTypes.RTCAVAnswer - ); - const result = await mediaManager.convertMediaObjectToBuffer( - mo, - ScryptedMimeTypes.RTCAVOffer - ); - processCandidates(result); - }; - - return pc; -} - -async function startCameraRtc(mediaManager: MediaManager, device: ScryptedDevice & VideoCamera & RTCSignalingChannel) { - const pc = new RTCPeerConnection(); - const gatheringPromise = new Promise(resolve => pc.onicegatheringstatechange = () => { - if (pc.iceGatheringState === 'complete') - resolve(undefined); - }); - - class SignalingSession implements RTCSignalingSession { - async onIceCandidate(candidate: RTCIceCandidate) { - await pc.addIceCandidate(candidate); - } - async createLocalDescription(type: 'offer' | 'answer', setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise { - if (setup.datachannel) - pc.createDataChannel(setup.datachannel.label, setup.datachannel.dict); - // it's possible to do talkback to ring. - let useAudioTransceiver = false; - try { - if (setup.audio?.direction === 'sendrecv') { - // doing sendrecv on safari requires a mic be attached, or it fails to connect. - const mic = await navigator.mediaDevices.getUserMedia({ video: false, audio: true }); - for (const track of mic.getTracks()) { - pc.addTrack(track); - } - } - else { - useAudioTransceiver = true; - } - } - catch (e) { - useAudioTransceiver = true; - } - if (useAudioTransceiver) - pc.addTransceiver("audio", setup.audio); - pc.addTransceiver("video", setup.video); - - pc.onicecandidate = ev => { - sendIceCandidate?.(ev.candidate as any); - }; - - const toDescription = (init: RTCSessionDescriptionInit) => { - return { - type: init.type, - sdp: init.sdp, - } - } - - if (type === 'offer') { - let offer = await pc.createOffer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true, - }); - const set = pc.setLocalDescription(offer); - if (sendIceCandidate) - return toDescription(offer); - await set; - await gatheringPromise; - offer = await pc.createOffer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true, - }); - return toDescription(offer); - } - else { - let answer = await pc.createAnswer(); - const set = pc.setLocalDescription(answer); - if (sendIceCandidate) - return toDescription(answer); - await set; - await gatheringPromise; - answer = pc.currentLocalDescription || answer; - return toDescription(answer); - } - } - async setRemoteDescription(description: RTCSessionDescription) { - await pc.setRemoteDescription(description); - } - } - - device.startRTCSignalingSession(new SignalingSession()); - return pc; -} +import { ScryptedDevice, MediaManager, VideoCamera, MediaObject, RTCSignalingChannel } from '@scrypted/types'; +import { BrowserSignalingSession } from "@scrypted/common/src/rtc-signaling"; +import eio from "engine.io-client"; +import { RpcPeer } from '../../../../../server/src/rpc'; export async function streamCamera(mediaManager: MediaManager, device: ScryptedDevice & VideoCamera & RTCSignalingChannel, getVideo: () => HTMLVideoElement) { - let pc: RTCPeerConnection; - if (device.interfaces.includes(ScryptedInterface.RTCSignalingChannel)) { - pc = await startCameraRtc(mediaManager, device); - } - else { - // todo: stop using the weird buffer convertor as a shim a signaling channel. - pc = await startCameraLegacy(mediaManager, device); - } + const pluginId = '@scrypted/core'; + const endpointPath = `/endpoint/${pluginId}` + const options: any = { + path: `${endpointPath}/engine.io/videocamera/`, + query: { + deviceId: device.id, + }, + rejectUnauthorized: false, + }; + const rootLocation = `${window.location.protocol}//${window.location.host}`; + const socket = eio(rootLocation, options); - try { - pc.onconnectionstatechange = async () => { - console.log(pc.connectionState); + const rpcPeer = new RpcPeer('cast-receiver', 'scrypted-server', (message, reject) => { + try { + socket.send(JSON.stringify(message)); + } + catch (e) { + reject?.(e); + } + }); + socket.on('message', data => { + rpcPeer.handleMessage(JSON.parse(data.toString())); + }); - const stats = await pc.getStats() - let selectedLocalCandidate - for (const { type, state, localCandidateId } of stats.values()) - if (type === 'candidate-pair' && state === 'succeeded' && localCandidateId) { - selectedLocalCandidate = localCandidateId - break - } - const isLocal = !!selectedLocalCandidate && stats.get(selectedLocalCandidate)?.type === "local-candidate"; - console.log('isLocal', isLocal, stats.get(selectedLocalCandidate)); - }; - pc.onsignalingstatechange = () => console.log(pc.connectionState); - pc.ontrack = () => { - const mediaStream = new MediaStream( - pc.getReceivers().map((receiver) => receiver.track) - ); - getVideo().srcObject = mediaStream; - const remoteAudio = document.createElement("audio"); - remoteAudio.srcObject = mediaStream; - remoteAudio.play(); - console.log('done tracks'); - }; + const pc = new RTCPeerConnection(); - return pc; - } - catch (e) { - pc.close(); - throw e; - } + const session = new BrowserSignalingSession(pc, () => socket.close()); + rpcPeer.params['session'] = session; + rpcPeer.params['options'] = session.options; + + pc.ontrack = ev => { + const mediaStream = new MediaStream( + pc.getReceivers().map((receiver) => receiver.track) + ); + getVideo().srcObject = mediaStream; + const remoteAudio = document.createElement("audio"); + remoteAudio.srcObject = mediaStream; + remoteAudio.play(); + console.log('received track', ev.track); + }; } export async function createBlobUrl(mediaManager: MediaManager, mediaObject: MediaObject): Promise { diff --git a/plugins/google-device-access/package-lock.json b/plugins/google-device-access/package-lock.json index 4248e52a3..2243a7975 100644 --- a/plugins/google-device-access/package-lock.json +++ b/plugins/google-device-access/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/google-device-access", - "version": "0.0.87", + "version": "0.0.88", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/google-device-access", - "version": "0.0.87", + "version": "0.0.88", "dependencies": { "@googleapis/smartdevicemanagement": "^0.2.0", "@koush/werift": "^0.14.5-beta5", diff --git a/plugins/google-device-access/package.json b/plugins/google-device-access/package.json index 1e5becde5..484d9289e 100644 --- a/plugins/google-device-access/package.json +++ b/plugins/google-device-access/package.json @@ -44,5 +44,5 @@ "@types/node": "^14.17.11", "@types/url-parse": "^1.4.3" }, - "version": "0.0.87" + "version": "0.0.88" } diff --git a/plugins/google-device-access/src/main.ts b/plugins/google-device-access/src/main.ts index 9d7adc441..89e5eec19 100644 --- a/plugins/google-device-access/src/main.ts +++ b/plugins/google-device-access/src/main.ts @@ -5,7 +5,8 @@ import ClientOAuth2 from 'client-oauth2'; import { URL } from 'url'; import axios from 'axios'; import throttle from 'lodash/throttle'; -import { createRTCPeerConnectionSource, getRTCMediaStreamOptions as getRtcMediaStreamOptions, startRTCSignalingSession } from '../../../common/src/wrtc-to-rtsp'; +import { createRTCPeerConnectionSource, getRTCMediaStreamOptions as getRtcMediaStreamOptions } from '../../../common/src/wrtc-to-rtsp'; +import { startRTCSignalingSession } from '../../../common/src/rtc-signaling'; import { sleep } from '../../../common/src/sleep'; import fs from 'fs'; import { randomBytes } from 'crypto'; diff --git a/plugins/google-home/.npmignore b/plugins/google-home/.npmignore index ff2824293..dd9986396 100644 --- a/plugins/google-home/.npmignore +++ b/plugins/google-home/.npmignore @@ -6,3 +6,4 @@ fs src .vscode dist/*.js +local-sdk-app diff --git a/plugins/google-home/package-lock.json b/plugins/google-home/package-lock.json index f661b7b62..a56319197 100644 --- a/plugins/google-home/package-lock.json +++ b/plugins/google-home/package-lock.json @@ -18,25 +18,34 @@ "url-parse": "^1.5.1" }, "devDependencies": { + "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", "@types/debug": "^4.1.5", "@types/lodash": "^4.14.168", "@types/url-parse": "^1.4.3" } }, - "../../sdk": { - "name": "@scrypted/sdk", - "version": "0.0.134", + "../../common": { + "name": "@scrypted/common", + "version": "1.0.1", "dev": true, "license": "ISC", "dependencies": { - "@babel/plugin-proposal-class-properties": "^7.14.5", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", - "@babel/plugin-proposal-numeric-separator": "^7.14.5", - "@babel/plugin-proposal-optional-chaining": "^7.14.5", - "@babel/plugin-transform-modules-commonjs": "^7.15.4", - "@babel/plugin-transform-typescript": "^7.15.8", - "@babel/preset-typescript": "^7.15.0", + "@koush/werift": "file:../external/werift/packages/webrtc", + "@scrypted/sdk": "file:../sdk", + "typescript": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^16.9.0" + } + }, + "../../sdk": { + "name": "@scrypted/sdk", + "version": "0.0.173", + "dev": true, + "license": "ISC", + "dependencies": { + "@babel/preset-typescript": "^7.16.7", "adm-zip": "^0.4.13", "axios": "^0.21.4", "babel-loader": "^8.2.3", @@ -46,7 +55,6 @@ "raw-loader": "^4.0.2", "rimraf": "^3.0.2", "tmp": "^0.2.1", - "ts-loader": "^9.2.6", "webpack": "^5.59.0" }, "bin": { @@ -63,7 +71,8 @@ "stringify-object": "^3.3.0", "ts-node": "^10.4.0", "typedoc": "^0.22.8", - "typescript-json-schema": "^0.50.1" + "typescript-json-schema": "^0.50.1", + "webpack-bundle-analyzer": "^4.5.0" } }, "../sdk": { @@ -131,6 +140,10 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@scrypted/common": { + "resolved": "../../common", + "link": true + }, "node_modules/@scrypted/sdk": { "resolved": "../../sdk", "link": true @@ -599,9 +612,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz", - "integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==", + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", "funding": [ { "type": "individual", @@ -741,11 +754,11 @@ } }, "node_modules/google-p12-pem": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.2.tgz", - "integrity": "sha512-tjf3IQIt7tWCDsa0ofDQ1qqSCNzahXDxdAGJDbruWqu3eCg5CKLYKN+hi0s6lfvzYZ1GDVr+oDF9OOWlDSdf0A==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz", + "integrity": "sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==", "dependencies": { - "node-forge": "^0.10.0" + "node-forge": "^1.0.0" }, "bin": { "gp12-pem": "build/src/bin/gp12-pem.js" @@ -1076,22 +1089,30 @@ } }, "node_modules/node-fetch": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", - "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dependencies": { "whatwg-url": "^5.0.0" }, "engines": { "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", + "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==", "engines": { - "node": ">= 6.0.0" + "node": ">= 6.13.0" } }, "node_modules/nopt": { @@ -1553,9 +1574,9 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/url-parse": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", - "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.9.tgz", + "integrity": "sha512-HpOvhKBvre8wYez+QhHcYiVvVmeF6DVnuSOOPhe3cTum3BnqHhvKaZm8FU5yTiOu/Jut2ZpB2rA/SbBA1JIGlQ==", "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -1817,16 +1838,19 @@ "tar": "^6.1.11" } }, + "@scrypted/common": { + "version": "file:../../common", + "requires": { + "@koush/werift": "file:../external/werift/packages/webrtc", + "@scrypted/sdk": "file:../sdk", + "@types/node": "^16.9.0", + "typescript": "^4.4.3" + } + }, "@scrypted/sdk": { "version": "file:../../sdk", "requires": { - "@babel/plugin-proposal-class-properties": "^7.14.5", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", - "@babel/plugin-proposal-numeric-separator": "^7.14.5", - "@babel/plugin-proposal-optional-chaining": "^7.14.5", - "@babel/plugin-transform-modules-commonjs": "^7.15.4", - "@babel/plugin-transform-typescript": "^7.15.8", - "@babel/preset-typescript": "^7.15.0", + "@babel/preset-typescript": "^7.16.7", "@types/node": "^16.11.1", "@types/stringify-object": "^4.0.0", "adm-zip": "^0.4.13", @@ -1839,11 +1863,11 @@ "rimraf": "^3.0.2", "stringify-object": "^3.3.0", "tmp": "^0.2.1", - "ts-loader": "^9.2.6", "ts-node": "^10.4.0", "typedoc": "^0.22.8", "typescript-json-schema": "^0.50.1", - "webpack": "^5.59.0" + "webpack": "^5.59.0", + "webpack-bundle-analyzer": "^4.5.0" } }, "@types/aws-lambda": { @@ -2226,9 +2250,9 @@ } }, "follow-redirects": { - "version": "1.14.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz", - "integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==" + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" }, "fs-minipass": { "version": "2.1.0", @@ -2330,11 +2354,11 @@ } }, "google-p12-pem": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.2.tgz", - "integrity": "sha512-tjf3IQIt7tWCDsa0ofDQ1qqSCNzahXDxdAGJDbruWqu3eCg5CKLYKN+hi0s6lfvzYZ1GDVr+oDF9OOWlDSdf0A==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz", + "integrity": "sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==", "requires": { - "node-forge": "^0.10.0" + "node-forge": "^1.0.0" } }, "googleapis": { @@ -2589,17 +2613,17 @@ } }, "node-fetch": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", - "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "requires": { "whatwg-url": "^5.0.0" } }, "node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", + "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==" }, "nopt": { "version": "5.0.0", @@ -2946,9 +2970,9 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "url-parse": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", - "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.9.tgz", + "integrity": "sha512-HpOvhKBvre8wYez+QhHcYiVvVmeF6DVnuSOOPhe3cTum3BnqHhvKaZm8FU5yTiOu/Jut2ZpB2rA/SbBA1JIGlQ==", "requires": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" diff --git a/plugins/google-home/package.json b/plugins/google-home/package.json index 63f9e3e9b..954f10b56 100644 --- a/plugins/google-home/package.json +++ b/plugins/google-home/package.json @@ -38,6 +38,7 @@ }, "devDependencies": { "@scrypted/sdk": "file:../../sdk", + "@scrypted/common": "file:../../common", "@types/debug": "^4.1.5", "@types/lodash": "^4.14.168", "@types/url-parse": "^1.4.3" diff --git a/plugins/google-home/src/commands/camerastream.ts b/plugins/google-home/src/commands/camerastream.ts index f7911b948..5de906769 100644 --- a/plugins/google-home/src/commands/camerastream.ts +++ b/plugins/google-home/src/commands/camerastream.ts @@ -1,4 +1,4 @@ -import { Brightness, OnOff, ScryptedDevice, ScryptedMimeTypes, VideoCamera } from "@scrypted/sdk"; +import { Brightness, OnOff, RTCSignalingChannel, ScryptedDevice, ScryptedMimeTypes, VideoCamera } from "@scrypted/sdk"; import { executeResponse } from "../common"; import { commandHandlers } from "../handlers"; @@ -7,9 +7,9 @@ const {mediaManager, endpointManager, systemManager } = sdk; const tokens: { [token: string]: string } = {}; -export function canAccess(token: string): ScryptedDevice & VideoCamera { +export function canAccess(token: string): ScryptedDevice & VideoCamera & RTCSignalingChannel { const id = tokens[token]; - return systemManager.getDeviceById(id) as ScryptedDevice & VideoCamera; + return systemManager.getDeviceById(id) as ScryptedDevice & VideoCamera & RTCSignalingChannel; } commandHandlers['action.devices.commands.GetCameraStream'] = async (device: ScryptedDevice & VideoCamera, execution) => { @@ -24,6 +24,7 @@ commandHandlers['action.devices.commands.GetCameraStream'] = async (device: Scry ret.states = { cameraStreamAccessUrl, + // cameraStreamReceiverAppId: "9E3714BD", cameraStreamReceiverAppId: "00F7C5DD", cameraStreamAuthToken, } diff --git a/plugins/google-home/src/main.ts b/plugins/google-home/src/main.ts index 41f056a3e..92eb7bccf 100644 --- a/plugins/google-home/src/main.ts +++ b/plugins/google-home/src/main.ts @@ -1,4 +1,4 @@ -import { EngineIOHandler, HttpRequest, HttpRequestHandler, HttpResponse, MediaObject, MixinProvider, Refresh, RequestMediaStreamOptions, RTCAVMessage, RTCAVSignalingSetup, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes } from '@scrypted/sdk'; +import { EngineIOHandler, HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, Refresh, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceProperty } from '@scrypted/sdk'; import sdk from '@scrypted/sdk'; import type { SmartHomeV1DisconnectRequest, SmartHomeV1DisconnectResponse, SmartHomeV1ExecuteRequest, SmartHomeV1ExecuteResponse, SmartHomeV1ExecuteResponseCommands } from 'actions-on-google/dist/service/smarthome/api/v1'; import { supportedTypes } from './common'; @@ -16,14 +16,13 @@ import { canAccess } from './commands/camerastream'; import { URL } from 'url'; import { homegraph } from '@googleapis/homegraph'; import type { JSONClient } from 'google-auth-library/build/src/auth/googleauth'; -import { addBuiltins, startRTCPeerConnection } from "../../../common/src/ffmpeg-to-wrtc"; +import { startBrowserRTCSignaling } from "@scrypted/common/src/ffmpeg-to-wrtc"; import ciao, { Protocol } from '@homebridge/ciao'; const responder = ciao.getResponder(); -const { systemManager, mediaManager, endpointManager, deviceManager } = sdk; -addBuiltins(mediaManager); +const { systemManager, endpointManager } = sdk; function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { @@ -90,12 +89,12 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin this.defaultIncluded = {}; } - systemManager.listen((source, details, data) => { + systemManager.listen((source, details) => { if (source && details.changed && details.property) this.queueReportState(source); }); - systemManager.listen((eventSource, eventDetails, eventData) => { + systemManager.listen((eventSource, eventDetails) => { if (eventDetails.eventInterface !== ScryptedInterface.ScryptedDevice) return; @@ -193,7 +192,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin ws.onmessage = async (message) => { const json = JSON.parse(message.data as string); - const { token, offer } = json; + const { token } = json; const camera = canAccess(token); if (!camera) { @@ -201,49 +200,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin return; } - let setup: RTCAVSignalingSetup; - - const msos = await camera.getVideoStreamOptions(); - const found = msos.find(mso => mso.container?.startsWith(ScryptedMimeTypes.RTCAVSignalingPrefix)) as RequestMediaStreamOptions; - if (found) { - found.directMediaStream = true; - const mediaObject = await camera.getVideoStream(found); - const buffer = await mediaManager.convertMediaObjectToBuffer(mediaObject, mediaObject.mimeType); - setup = JSON.parse(buffer.toString()); - - ws.onmessage = async (message) => { - ws.onmessage = undefined; - const json = JSON.parse(message.data as string); - const { offer } = json; - - const mo = mediaManager.createMediaObject(Buffer.from(JSON.stringify(offer)), ScryptedMimeTypes.RTCAVOffer) - const answer = await mediaManager.convertMediaObjectToBuffer(mo, undefined); - ws.send(answer.toString()); - } - } - else { - setup = { - type: 'offer', - audio: { - direction: 'recvonly', - }, - video: { - direction: 'recvonly', - }, - } - - ws.onmessage = async (message) => { - ws.onmessage = undefined; - const json = JSON.parse(message.data as string); - const { offer } = json; - // chromecast and nest hub are super underpowered so cap the width - const { pc, answer } = await startRTCPeerConnection(await camera.getVideoStream(), offer, { - maxWidth: 960, - }); - ws.send(JSON.stringify(answer)); - } - } - ws.send(JSON.stringify(setup)); + await startBrowserRTCSignaling(camera, ws, this.console); } } diff --git a/plugins/ring/src/main.ts b/plugins/ring/src/main.ts index ef7bac152..3c69653cd 100644 --- a/plugins/ring/src/main.ts +++ b/plugins/ring/src/main.ts @@ -3,7 +3,8 @@ import sdk from '@scrypted/sdk'; import { RingApi, RingCamera, RingRestClient } from './ring-client-api'; import { StorageSettings } from '../../..//common/src/settings'; import { ChildProcess } from 'child_process'; -import { createRTCPeerConnectionSource, startRTCSignalingSession } from '../../../common/src/wrtc-to-rtsp'; +import { createRTCPeerConnectionSource } from '../../../common/src/wrtc-to-rtsp'; +import { startRTCSignalingSession } from '../../../common/src/rtc-signaling'; import { generateUuid } from './ring-client-api'; import fs from 'fs'; import { clientApi } from './ring-client-api'; @@ -104,7 +105,7 @@ class RingCameraDevice extends ScryptedDeviceBase implements BufferConverter, De ) } else if (message.method === 'ice') { - session.onIceCandidate({ + session.addIceCandidate({ candidate: message.ice, sdpMLineIndex: message.mlineindex, }) diff --git a/sdk/gen/types.input.ts b/sdk/gen/types.input.ts index 980d120b7..6decca71b 100644 --- a/sdk/gen/types.input.ts +++ b/sdk/gen/types.input.ts @@ -1285,7 +1285,7 @@ export type RTCSignalingSendIceCandidate = (candidate: RTCIceCandidate) => Promi export interface RTCSignalingSession { createLocalDescription: (type: 'offer' | 'answer', setup: RTCAVSignalingSetup, sendIceCandidate: undefined|RTCSignalingSendIceCandidate) => Promise; setRemoteDescription: (description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup) => Promise; - onIceCandidate: (candidate: RTCIceCandidateInit) => Promise; + addIceCandidate: (candidate: RTCIceCandidateInit) => Promise; } export interface RTCSignalingChannelOptions { diff --git a/sdk/types/index.d.ts b/sdk/types/index.d.ts index 8c86540c3..9a1c8cef2 100644 --- a/sdk/types/index.d.ts +++ b/sdk/types/index.d.ts @@ -1325,7 +1325,7 @@ export declare type RTCSignalingSendIceCandidate = (candidate: RTCIceCandidate) export interface RTCSignalingSession { createLocalDescription: (type: 'offer' | 'answer', setup: RTCAVSignalingSetup, sendIceCandidate: undefined | RTCSignalingSendIceCandidate) => Promise; setRemoteDescription: (description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup) => Promise; - onIceCandidate: (candidate: RTCIceCandidateInit) => Promise; + addIceCandidate: (candidate: RTCIceCandidateInit) => Promise; } export interface RTCSignalingChannelOptions { capabilities?: { diff --git a/sdk/types/index.ts b/sdk/types/index.ts index 6378baacf..ed82d89ca 100644 --- a/sdk/types/index.ts +++ b/sdk/types/index.ts @@ -1941,7 +1941,7 @@ export type RTCSignalingSendIceCandidate = (candidate: RTCIceCandidate) => Promi export interface RTCSignalingSession { createLocalDescription: (type: 'offer' | 'answer', setup: RTCAVSignalingSetup, sendIceCandidate: undefined|RTCSignalingSendIceCandidate) => Promise; setRemoteDescription: (description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup) => Promise; - onIceCandidate: (candidate: RTCIceCandidateInit) => Promise; + addIceCandidate: (candidate: RTCIceCandidateInit) => Promise; } export interface RTCSignalingChannelOptions { diff --git a/server/src/rpc.ts b/server/src/rpc.ts index b488b2121..fbb6bc6fb 100644 --- a/server/src/rpc.ts +++ b/server/src/rpc.ts @@ -1,4 +1,5 @@ -import vm from 'vm'; +import type { CompileFunctionOptions } from 'vm'; +type CompileFunction = (code: string, params?: ReadonlyArray, options?: CompileFunctionOptions) => Function; export function startPeriodicGarbageCollection() { if (!global.gc) { @@ -165,7 +166,7 @@ export class RPCResultError extends Error { } } -function compileFunction(code: string, params?: ReadonlyArray, options?: vm.CompileFunctionOptions): any { +function compileFunction(code: string, params?: ReadonlyArray, options?: CompileFunctionOptions): any { params = params || []; const f = `(function(${params.join(',')}) {;${code};})`; return eval(f); @@ -318,7 +319,14 @@ export class RpcPeer { evalLocal(script: string, filename?: string, coercedParams?: { [name: string]: any }): T { const params = Object.assign({}, this.params, coercedParams); - const f = (vm.compileFunction || compileFunction)(script, Object.keys(params), { + let compile: CompileFunction; + try { + compile = require('vm').compileFunction;; + } + catch (e) { + compile = compileFunction; + } + const f = compile(script, Object.keys(params), { filename, }); const value = f(...Object.values(params));