mirror of
https://github.com/koush/scrypted.git
synced 2026-09-17 17:20:39 +01:00
webrtc-sink: initial commit
This commit is contained in:
5
plugins/webrtc-sink/.gitignore
vendored
Normal file
5
plugins/webrtc-sink/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
dist/
|
||||
.venv
|
||||
9
plugins/webrtc-sink/.npmignore
Normal file
9
plugins/webrtc-sink/.npmignore
Normal file
@@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
*.map
|
||||
fs
|
||||
src
|
||||
.vscode
|
||||
dist/*.js
|
||||
.venv
|
||||
23
plugins/webrtc-sink/.vscode/launch.json
vendored
Normal file
23
plugins/webrtc-sink/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Scrypted Debugger",
|
||||
"address": "${config:scrypted.debugHost}",
|
||||
"port": 10081,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"**/plugin-remote-worker.*",
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"preLaunchTask": "scrypted: deploy+debug",
|
||||
"sourceMaps": true,
|
||||
"localRoot": "${workspaceFolder}/out",
|
||||
"remoteRoot": "/plugin/",
|
||||
"type": "pwa-node"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
plugins/webrtc-sink/.vscode/settings.json
vendored
Normal file
4
plugins/webrtc-sink/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
20
plugins/webrtc-sink/.vscode/tasks.json
vendored
Normal file
20
plugins/webrtc-sink/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "scrypted: deploy+debug",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "silent",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
},
|
||||
"command": "npm run scrypted-vscode-launch ${config:scrypted.debugHost}",
|
||||
},
|
||||
]
|
||||
}
|
||||
3
plugins/webrtc-sink/README.md
Normal file
3
plugins/webrtc-sink/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# WebRTC Sink Plugin for Scrypted
|
||||
|
||||
This plugin sends video feeds from Scrypted cameras to WebRTC clients. It must be installed and enabled for any video camera you want to use with Chromecast, Google Home, Alexa, and others.
|
||||
180
plugins/webrtc-sink/package-lock.json
generated
Normal file
180
plugins/webrtc-sink/package-lock.json
generated
Normal file
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc-sink",
|
||||
"version": "0.0.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/webrtc-sink",
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"@types/node": "^16.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@koush/werift": "file:../external/werift/packages/webrtc",
|
||||
"@koush/wrtc": "^0.5.3",
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"../../external/werift/packages/webrtc": {
|
||||
"name": "@koush/werift",
|
||||
"version": "0.14.5-beta7",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fidm/x509": "^1.2.1",
|
||||
"@minhducsun2002/leb128": "^0.2.0",
|
||||
"@peculiar/webcrypto": "^1.1.6",
|
||||
"@peculiar/x509": "^1.2.2",
|
||||
"@shinyoshiaki/ebml-builder": "^0.0.1",
|
||||
"aes-js": "^3.1.2",
|
||||
"binary-data": "^0.6.0",
|
||||
"buffer-crc32": "^0.2.13",
|
||||
"date-fns": "^2.27.0",
|
||||
"debug": "^4.3.3",
|
||||
"elliptic": "^6.5.3",
|
||||
"int64-buffer": "^1.0.1",
|
||||
"ip": "^1.1.5",
|
||||
"jspack": "^0.0.4",
|
||||
"lodash": "^4.17.20",
|
||||
"nano-time": "^1.0.0",
|
||||
"p-cancelable": "^2.1.1",
|
||||
"rx.mini": "^1.1.0",
|
||||
"turbo-crc32": "^1.0.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/aes-js": "^3.1.1",
|
||||
"@types/buffer-crc32": "^0.2.0",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/elliptic": "^6.4.14",
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/node": "^17.0.0",
|
||||
"@types/uuid": "^8.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
||||
"@typescript-eslint/parser": "^5.7.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^27.4.5",
|
||||
"node-actionlint": "^1.2.1",
|
||||
"prettier": "^2.5.1",
|
||||
"ts-jest": "^27.1.1",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.22.10",
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=15"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.177",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
"resolved": "../../common",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
|
||||
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA=="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@koush/werift": "file:../external/werift/packages/webrtc",
|
||||
"@koush/wrtc": "^0.5.3",
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@types/node": "^16.9.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"@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": "16.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
|
||||
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
35
plugins/webrtc-sink/package.json
Normal file
35
plugins/webrtc-sink/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc-sink",
|
||||
"version": "0.0.2",
|
||||
"scripts": {
|
||||
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
|
||||
"prescrypted-vscode-launch": "scrypted-webpack",
|
||||
"scrypted-vscode-launch": "scrypted-deploy-debug",
|
||||
"scrypted-deploy-debug": "scrypted-deploy-debug",
|
||||
"scrypted-debug": "scrypted-debug",
|
||||
"scrypted-deploy": "scrypted-deploy",
|
||||
"scrypted-webpack": "scrypted-webpack"
|
||||
},
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
"plugin",
|
||||
"webrtc",
|
||||
"ring",
|
||||
"google",
|
||||
"nest"
|
||||
],
|
||||
"scrypted": {
|
||||
"name": "WebRTC Sink",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"MixinProvider"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^16.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
}
|
||||
115
plugins/webrtc-sink/src/main.ts
Normal file
115
plugins/webrtc-sink/src/main.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Settings, RTCSignalingChannel, ScryptedDeviceType, ScryptedInterface, VideoCamera, Setting, SettingValue, RTCSessionControl, RTCSignalingClientOptions, RTCSignalingSession, FFMpegInput, ScryptedMimeTypes, RTCAVSignalingSetup } from '@scrypted/sdk';
|
||||
import sdk from '@scrypted/sdk';
|
||||
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
|
||||
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from '@scrypted/common/src/settings-mixin';
|
||||
import { StorageSettings } from '@scrypted/common/src/settings';
|
||||
import { startRTCPeerConnectionFFmpegInput } from '@scrypted/common/src/ffmpeg-to-wrtc';
|
||||
import { BrowserSignalingSession, connectRTCSignalingClients, startRTCSignalingSession } from '@scrypted/common/src/rtc-signaling';
|
||||
|
||||
|
||||
const { mediaManager, systemManager } = sdk;
|
||||
|
||||
const supportedTypes = [
|
||||
ScryptedDeviceType.Camera,
|
||||
ScryptedDeviceType.Doorbell,
|
||||
];
|
||||
|
||||
|
||||
function createSetup(type: 'offer' | 'answer'): RTCAVSignalingSetup {
|
||||
return {
|
||||
type,
|
||||
audio: {
|
||||
direction: 'recvonly',
|
||||
},
|
||||
video: {
|
||||
direction: 'recvonly',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
class WebRTCMixin extends SettingsMixinDeviceBase<VideoCamera & RTCSignalingChannel> implements RTCSignalingChannel {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
|
||||
});
|
||||
|
||||
constructor(options: SettingsMixinDeviceOptions<RTCSignalingChannel & Settings & VideoCamera>) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
async startRTCSignalingSession(session: RTCSignalingSession, options?: RTCSignalingClientOptions): Promise<RTCSessionControl> {
|
||||
// if the camera natively has RTCSignalingChannel and the client is not a weird non-browser
|
||||
// thing like Alexa, etc, pass through. Otherwise proxy/transcode.
|
||||
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.RTCSignalingChannel) && !options?.proxy)
|
||||
return this.mixinDevice.startRTCSignalingSession(session, options);
|
||||
|
||||
const device = systemManager.getDeviceById<VideoCamera>(this.id);
|
||||
const mo = await device.getVideoStream();
|
||||
const ffInput = await mediaManager.convertMediaObjectToJSON<FFMpegInput>(mo, ScryptedMimeTypes.FFmpegInput);
|
||||
const pc = await startRTCPeerConnectionFFmpegInput(ffInput, {
|
||||
maxWidth: 960,
|
||||
});
|
||||
|
||||
const answerSession = new BrowserSignalingSession(pc);
|
||||
answerSession.options = undefined;
|
||||
answerSession.hasSetup = true;
|
||||
|
||||
setTimeout(() => {
|
||||
pc.onicecandidate({
|
||||
candidate: undefined,
|
||||
} as any)
|
||||
}, 2000)
|
||||
|
||||
connectRTCSignalingClients(session, createSetup('offer'),
|
||||
answerSession, createSetup('answer'), !!options?.offer);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getMixinSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
|
||||
putMixinSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
class WebRTCSinkPlugin extends AutoenableMixinProvider {
|
||||
constructor() {
|
||||
super();
|
||||
this.on = this.on || false;
|
||||
}
|
||||
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
|
||||
if (!supportedTypes.includes(type))
|
||||
return;
|
||||
|
||||
if (!interfaces.includes(ScryptedInterface.VideoCamera))
|
||||
return;
|
||||
|
||||
// if (interfaces.includes(ScryptedInterface.RTCSignalingChannel))
|
||||
// return;
|
||||
|
||||
return [
|
||||
'@scrypted/webrtc-sink',
|
||||
ScryptedInterface.RTCSignalingChannel,
|
||||
ScryptedInterface.Settings,
|
||||
];
|
||||
}
|
||||
|
||||
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
|
||||
return new WebRTCMixin({
|
||||
mixinDevice,
|
||||
mixinDeviceInterfaces,
|
||||
mixinDeviceState,
|
||||
group: 'WebRTC',
|
||||
groupKey: 'webrtc-sink',
|
||||
mixinProviderNativeId: this.nativeId,
|
||||
})
|
||||
}
|
||||
|
||||
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
|
||||
await mixinDevice.release();
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebRTCSinkPlugin();
|
||||
Reference in New Issue
Block a user