mirror of
https://github.com/koush/scrypted.git
synced 2026-02-09 00:39:56 +00:00
core: initial refactor to new webrtc signaling
This commit is contained in:
31
plugins/core/package-lock.json
generated
31
plugins/core/package-lock.json
generated
@@ -10,7 +10,6 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@koush/wrtc": "^0.5.2",
|
||||
"@scrypted/rpc": "^1.0.3",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"mime": "^3.0.0",
|
||||
"router": "^1.3.6",
|
||||
@@ -73,16 +72,10 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.167",
|
||||
"version": "0.0.169",
|
||||
"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",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
@@ -92,7 +85,6 @@
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"webpack": "^5.59.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -164,11 +156,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/rpc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/rpc/-/rpc-1.0.3.tgz",
|
||||
"integrity": "sha512-luEigc8gIMoKv26t2123KKUno3W7o4ze6SMv7ZPnRnlpYrgo9CmjDhezptTfQuHm6c/0eiIRPh6qGSBWOjelGw=="
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
@@ -1388,21 +1375,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@scrypted/rpc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/rpc/-/rpc-1.0.3.tgz",
|
||||
"integrity": "sha512-luEigc8gIMoKv26t2123KKUno3W7o4ze6SMv7ZPnRnlpYrgo9CmjDhezptTfQuHm6c/0eiIRPh6qGSBWOjelGw=="
|
||||
},
|
||||
"@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",
|
||||
@@ -1415,7 +1391,6 @@
|
||||
"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",
|
||||
|
||||
@@ -12,11 +12,11 @@ import { Automation } from './automation';
|
||||
import { AggregateDevice, createAggregateDevice } from './aggregate';
|
||||
import net from 'net';
|
||||
import { Script } from './script';
|
||||
import { addBuiltins } from "../../../common/src/wrtc-convertors";
|
||||
import { addBuiltins } from "../../../common/src/ffmpeg-to-wrtc";
|
||||
import { updatePluginsData } from './update-plugins';
|
||||
import { MediaCore } from './media-core';
|
||||
|
||||
addBuiltins(console, mediaManager);
|
||||
addBuiltins(mediaManager);
|
||||
|
||||
const { pluginHostAPI } = sdk;
|
||||
|
||||
|
||||
51
plugins/core/ui/package-lock.json
generated
51
plugins/core/ui/package-lock.json
generated
@@ -5,7 +5,6 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ui",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
||||
@@ -14,8 +13,8 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||
"@fortawesome/vue-fontawesome": "^0.1.10",
|
||||
"@radial-color-picker/vue-color-picker": "^2.3.0",
|
||||
"@scrypted/client": "file:../client",
|
||||
"@scrypted/sdk": "file:../../../sdk",
|
||||
"@scrypted/types": "file:../../../sdk/types",
|
||||
"apexcharts": "^3.28.3",
|
||||
"axios": "^0.19.2",
|
||||
"core-js": "^2.6.12",
|
||||
@@ -96,16 +95,10 @@
|
||||
},
|
||||
"../../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.167",
|
||||
"version": "0.0.173",
|
||||
"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",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
@@ -115,7 +108,6 @@
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"webpack": "^5.59.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -136,12 +128,19 @@
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"../../../sdk/types": {
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.0.9",
|
||||
"license": "ISC",
|
||||
"devDependencies": {}
|
||||
},
|
||||
"../../sdk": {
|
||||
"extraneous": true
|
||||
},
|
||||
"../client": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "0.0.7",
|
||||
"extraneous": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../../../sdk",
|
||||
@@ -2160,14 +2159,14 @@
|
||||
"vue": "^2.5.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client": {
|
||||
"resolved": "../client",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"resolved": "../../../sdk/types",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@soda/friendly-errors-webpack-plugin": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz",
|
||||
@@ -21064,26 +21063,10 @@
|
||||
"@radial-color-picker/rotator": "2.1.0"
|
||||
}
|
||||
},
|
||||
"@scrypted/client": {
|
||||
"version": "file:../client",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../../../sdk",
|
||||
"@types/engine.io-client": "^3.1.5",
|
||||
"@types/lodash.clonedeep": "^4.5.6",
|
||||
"engine.io-client": "^5.2.0",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
@@ -21096,7 +21079,6 @@
|
||||
"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",
|
||||
@@ -21104,6 +21086,9 @@
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"@scrypted/types": {
|
||||
"version": "file:../../../sdk/types"
|
||||
},
|
||||
"@soda/friendly-errors-webpack-plugin": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz",
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
"serve": "vue-cli-service serve --open",
|
||||
"serve-server": "cd ../../../server && npm run serve",
|
||||
"build": "vue-cli-service build --dest ../fs/dist",
|
||||
"lint": "vue-cli-service lint",
|
||||
"postinstall": "(cd ../../../server && npm install); (cd ../client && npm install)"
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
@@ -17,7 +16,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/client": "file:../client",
|
||||
"@scrypted/types": "file:../../../sdk/types",
|
||||
"@scrypted/sdk": "file:../../../sdk",
|
||||
"apexcharts": "^3.28.3",
|
||||
"axios": "^0.19.2",
|
||||
|
||||
@@ -1,165 +1,33 @@
|
||||
import { ScryptedDevice, ScryptedMimeTypes, RTCAVMessage, MediaManager, VideoCamera, MediaObject, RTCAVSignalingOfferSetup, RequestMediaStreamOptions } from '@scrypted/sdk/types';
|
||||
import { RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedInterface, ScryptedDevice, ScryptedMimeTypes, RTCAVMessage, MediaManager, VideoCamera, MediaObject, RTCAVSignalingSetup, RequestMediaStreamOptions, RTCSignalingChannel } from '@scrypted/types';
|
||||
|
||||
export async function streamCamera(mediaManager: MediaManager, device: ScryptedDevice & VideoCamera, getVideo: () => HTMLVideoElement, createPeerConnection: (configuration: RTCConfiguration) => RTCPeerConnection) {
|
||||
async function startCameraLegacy(mediaManager: MediaManager, device: ScryptedDevice & VideoCamera & RTCSignalingChannel) {
|
||||
let selectedStream: RequestMediaStreamOptions;
|
||||
try {
|
||||
const streams = await device.getVideoStreamOptions();
|
||||
selectedStream = streams.find(stream => stream.container?.startsWith(ScryptedMimeTypes.RTCAVSignalingPrefix));
|
||||
if (selectedStream)
|
||||
selectedStream.directMediaStream = true;
|
||||
else
|
||||
selectedStream = streams.find(stream => stream.container === 'rawvideo');
|
||||
selectedStream = streams.find(stream => stream.container === 'rawvideo');
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
const videoStream = await device.getVideoStream(selectedStream);
|
||||
|
||||
let trickle = true;
|
||||
let pc: RTCPeerConnection;
|
||||
let json: RTCAVMessage;
|
||||
let sentSdp = false;
|
||||
if (videoStream.mimeType.startsWith(ScryptedMimeTypes.RTCAVSignalingPrefix)) {
|
||||
trickle = false;
|
||||
|
||||
const buffer = await mediaManager.convertMediaObjectToBuffer(
|
||||
videoStream,
|
||||
videoStream.mimeType,
|
||||
);
|
||||
const avsource: RTCAVSignalingOfferSetup = JSON.parse(buffer.toString());
|
||||
const offer = await mediaManager.convertMediaObjectToBuffer(
|
||||
videoStream,
|
||||
ScryptedMimeTypes.RTCAVOffer
|
||||
);
|
||||
json = JSON.parse(offer.toString());
|
||||
let pc = new RTCPeerConnection(json.configuration);
|
||||
|
||||
pc = createPeerConnection({})
|
||||
if (avsource.datachannel)
|
||||
pc.createDataChannel(avsource.datachannel.label, avsource.datachannel.dict);
|
||||
// it's possible to do talkback to ring.
|
||||
let useAudioTransceiver = false;
|
||||
try {
|
||||
if (avsource.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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
catch (e) {
|
||||
useAudioTransceiver = true;
|
||||
}
|
||||
if (useAudioTransceiver)
|
||||
pc.addTransceiver("audio", avsource.audio);
|
||||
pc.addTransceiver("video", avsource.video);
|
||||
|
||||
const offer = await pc.createOffer({
|
||||
offerToReceiveVideo: true,
|
||||
offerToReceiveAudio: true,
|
||||
});
|
||||
await pc.setLocalDescription(offer);
|
||||
}
|
||||
else {
|
||||
const offer = await mediaManager.convertMediaObjectToBuffer(
|
||||
videoStream,
|
||||
ScryptedMimeTypes.RTCAVOffer
|
||||
);
|
||||
json = JSON.parse(offer.toString());
|
||||
pc = createPeerConnection(json.configuration);
|
||||
}
|
||||
try {
|
||||
|
||||
pc.onconnectionstatechange = async () => {
|
||||
console.log(pc.connectionState);
|
||||
|
||||
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)?.candidateType === 'relay'
|
||||
console.log('isLocal', isLocal);
|
||||
};
|
||||
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 processCandidates = (result: Buffer) => {
|
||||
const message: RTCAVMessage = JSON.parse(result.toString());
|
||||
for (const candidate of message.candidates) {
|
||||
// console.log('remote candidate', candidate);
|
||||
pc.addIceCandidate(candidate);
|
||||
}
|
||||
};
|
||||
|
||||
pc.onicecandidate = async (evt) => {
|
||||
if (!evt.candidate) {
|
||||
if (!trickle) {
|
||||
if (sentSdp)
|
||||
return;
|
||||
sentSdp = true;
|
||||
const offer = await pc.createOffer({
|
||||
offerToReceiveAudio: true,
|
||||
offerToReceiveVideo: true,
|
||||
});
|
||||
console.log('offer', offer);
|
||||
await pc.setLocalDescription(offer);
|
||||
|
||||
const offerWithCandidates: RTCAVMessage = {
|
||||
id: undefined,
|
||||
candidates: [],
|
||||
description: {
|
||||
sdp: offer.sdp,
|
||||
type: 'offer',
|
||||
},
|
||||
configuration: {},
|
||||
};
|
||||
const mo = await mediaManager.createMediaObject(
|
||||
Buffer.from(JSON.stringify(offerWithCandidates)),
|
||||
ScryptedMimeTypes.RTCAVOffer
|
||||
);
|
||||
const result = await mediaManager.convertMediaObjectToBuffer(
|
||||
mo,
|
||||
videoStream.mimeType
|
||||
);
|
||||
const answer: RTCAVMessage = JSON.parse(result.toString())
|
||||
console.log('answer', answer);
|
||||
await pc.setRemoteDescription(answer.description);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!trickle) {
|
||||
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);
|
||||
};
|
||||
|
||||
if (!trickle)
|
||||
return pc;
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await pc.setRemoteDescription(json.description);
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
@@ -180,26 +48,169 @@ export async function streamCamera(mediaManager: MediaManager, device: ScryptedD
|
||||
);
|
||||
processCandidates(result);
|
||||
|
||||
(async () => {
|
||||
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);
|
||||
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<RTCSessionDescriptionInit> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
})();
|
||||
console.log("done av offer");
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
pc.onconnectionstatechange = async () => {
|
||||
console.log(pc.connectionState);
|
||||
|
||||
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');
|
||||
};
|
||||
|
||||
return pc;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, SystemManager } from "@scrypted/sdk/types";
|
||||
import { MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, SystemManager } from "@scrypted/types";
|
||||
|
||||
export async function setMixin(systemManager: SystemManager, device: ScryptedDevice, mixinId: string, enabled: boolean) {
|
||||
const plugins = await systemManager.getComponent(
|
||||
|
||||
@@ -398,7 +398,7 @@ import {
|
||||
hasFixedPhysicalLocation,
|
||||
getInterfaceFriendlyName,
|
||||
} from "./helpers";
|
||||
import { ScryptedInterface } from "@scrypted/sdk/types";
|
||||
import { ScryptedInterface } from "@scrypted/types";
|
||||
import Notifier from "../interfaces/Notifier.vue";
|
||||
import OnOff from "../interfaces/OnOff.vue";
|
||||
import Brightness from "../interfaces/Brightness.vue";
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
<script>
|
||||
import InterfacesPicker from "./InterfacesPicker.vue";
|
||||
import EventsPicker from "./EventsPicker.vue";
|
||||
import { ScryptedInterface } from "@scrypted/sdk/types";
|
||||
import { ScryptedInterface } from "@scrypted/types";
|
||||
import { actionableEvents, actionableInterfaces } from "./interfaces";
|
||||
|
||||
const includeContextual = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ScryptedInterface } from '@scrypted/sdk/types';
|
||||
import { ScryptedInterface } from '@scrypted/types';
|
||||
|
||||
export const actionableInterfaces = [
|
||||
ScryptedInterface.OnOff,
|
||||
|
||||
@@ -340,7 +340,7 @@ import {
|
||||
Card,
|
||||
} from "./layout";
|
||||
import { Menu } from "../../store";
|
||||
import { Settings as SettingsInterface, Setting, SettingValue } from "@scrypted/sdk/types";
|
||||
import { Settings as SettingsInterface, Setting, SettingValue } from "@scrypted/types";
|
||||
|
||||
class CardComponentSettings implements SettingsInterface {
|
||||
cardComponent: CardComponent;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { ScryptedInterface } from "@scrypted/sdk/types";
|
||||
import { ScryptedInterface } from "@scrypted/types";
|
||||
import DashboardBase from "./DashboardBase";
|
||||
import { createBlobUrl, streamCamera } from "../../common/camera";
|
||||
|
||||
@@ -51,11 +51,10 @@ export default {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await streamCamera(
|
||||
this.pc = await streamCamera(
|
||||
this.$scrypted.mediaManager,
|
||||
this.device,
|
||||
() => this.$refs.video,
|
||||
(configuration) => (this.pc = new RTCPeerConnection(configuration))
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import DashboardBase from "./DashboardBase";
|
||||
import { ScryptedInterface } from "@scrypted/sdk/types";
|
||||
import { ScryptedInterface } from "@scrypted/types";
|
||||
import colors from "vuetify/es5/util/colors";
|
||||
import { getDeviceViewPath, typeToIcon } from "../helpers";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { getDeviceViewPath } from "../helpers";
|
||||
import { ThermostatMode } from "@scrypted/sdk/types";
|
||||
import { ThermostatMode } from "@scrypted/types";
|
||||
import DashboardBase from "./DashboardBase";
|
||||
import colors from "vuetify/es5/util/colors";
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ import DashboardPopupToggle from "./DashboardPopupToggle.vue";
|
||||
import ClickOutside from "vue-click-outside";
|
||||
import VueSlider from "vue-slider-component";
|
||||
import "vue-slider-component/theme/default.css";
|
||||
import { ScryptedInterface } from "@scrypted/sdk/types";
|
||||
import { ScryptedInterface } from "@scrypted/types";
|
||||
import throttle from "lodash/throttle";
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SystemManager, ScryptedDeviceType, ScryptedDevice, ScryptedInterface, Setting } from "@scrypted/sdk/types";
|
||||
import { SystemManager, ScryptedDeviceType, ScryptedDevice, ScryptedInterface, Setting } from "@scrypted/types";
|
||||
import DashboardMap from "./DashboardMap.vue";
|
||||
import DashboardToggle from "./DashboardToggle.vue";
|
||||
import DashboardCamera from "./DashboardCamera.vue";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk/types";
|
||||
import { ScryptedDeviceType, ScryptedInterface } from "@scrypted/types";
|
||||
|
||||
export function typeToIcon(type) {
|
||||
switch (type) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" width="1024" :disabled="disabled">
|
||||
<template v-slot:activator="{ on }">
|
||||
<a v-on="on" v-if="!hidePreview"
|
||||
><v-img :src="src"></v-img
|
||||
></a>
|
||||
<a v-on="on" v-if="!hidePreview"><v-img :src="src"></v-img></a>
|
||||
</template>
|
||||
<div style="position: relative; overflow: hidden">
|
||||
<video
|
||||
@@ -51,7 +49,7 @@
|
||||
<script>
|
||||
import RPCInterface from "./RPCInterface.vue";
|
||||
import { createBlobUrl, streamCamera } from "../common/camera";
|
||||
import { ScryptedInterface } from "@scrypted/sdk/types";
|
||||
import { ScryptedInterface } from "@scrypted/types";
|
||||
import ClipPathEditor from "../components/clippath/ClipPathEditor.vue";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
|
||||
@@ -139,11 +137,10 @@ export default {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await streamCamera(
|
||||
this.pc = await streamCamera(
|
||||
this.$scrypted.mediaManager,
|
||||
this.device,
|
||||
() => this.$refs.video,
|
||||
(configuration) => (this.pc = new RTCPeerConnection(configuration))
|
||||
() => this.$refs.video
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<script>
|
||||
import RPCInterface from "./RPCInterface.vue";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { ScryptedInterface } from "@scrypted/sdk/types";
|
||||
import { ScryptedInterface } from "@scrypted/types";
|
||||
|
||||
const supportedMediaInterfaces = [
|
||||
ScryptedInterface.VideoCamera,
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
import RPCInterface from "./RPCInterface.vue";
|
||||
import throttle from "lodash/throttle";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { ThermostatMode } from "@scrypted/sdk/types";
|
||||
import { ThermostatMode } from "@scrypted/types";
|
||||
|
||||
export default {
|
||||
mixins: [RPCInterface],
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
<script>
|
||||
import RPCInterface from "../RPCInterface.vue";
|
||||
import types from "!!raw-loader!@scrypted/sdk/types/index.d.ts";
|
||||
import types from "!!raw-loader!@scrypted/types/index.d.ts";
|
||||
import sdk from "!!raw-loader!@scrypted/sdk/index.d.ts";
|
||||
import * as monaco from "monaco-editor";
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
"-profile:a", "aac_low",
|
||||
"-threads", "0",
|
||||
"-avioflags", "direct",
|
||||
"-max_delay", "1000000",
|
||||
"-flush_packets", "1",
|
||||
"-flags", "+global_header",
|
||||
"-ar", camera.talkbackSettings.samplingRate.toString(),
|
||||
@@ -319,7 +318,6 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
url: u,
|
||||
inputArguments: [
|
||||
"-rtsp_transport", "tcp",
|
||||
"-max_delay", "1000000",
|
||||
"-i", u,
|
||||
],
|
||||
mediaStreamOptions: this.createMediaStreamOptions(rtspChannel),
|
||||
|
||||
Reference in New Issue
Block a user