mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 12:19:54 +00:00
website: add call page back (#1121)
This commit is contained in:
parent
96b8f0e979
commit
bce8af16de
11 changed files with 894 additions and 9 deletions
|
@ -11,7 +11,7 @@ module.exports = function (ty) {
|
||||||
ty.addPassthroughCopy("src/css")
|
ty.addPassthroughCopy("src/css")
|
||||||
ty.addPassthroughCopy("src/js")
|
ty.addPassthroughCopy("src/js")
|
||||||
ty.addPassthroughCopy("src/contact")
|
ty.addPassthroughCopy("src/contact")
|
||||||
ty.addPassthroughCopy("src/app-demo")
|
ty.addPassthroughCopy("src/call")
|
||||||
ty.addPassthroughCopy("src/blog/images")
|
ty.addPassthroughCopy("src/blog/images")
|
||||||
ty.addPassthroughCopy("src/images")
|
ty.addPassthroughCopy("src/images")
|
||||||
ty.addPassthroughCopy("src/CNAME")
|
ty.addPassthroughCopy("src/CNAME")
|
||||||
|
|
9
website/copy_call.sh
Executable file
9
website/copy_call.sh
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
mkdir -p ./src/call
|
||||||
|
cp ./node_modules/@simplex-chat/webrtc/dist/call.js ./src/call/
|
||||||
|
cp ./node_modules/@simplex-chat/webrtc/dist/call.js.map ./src/call/
|
||||||
|
cp ./node_modules/@simplex-chat/webrtc/dist/ui.js ./src/call/
|
||||||
|
cp ./node_modules/@simplex-chat/webrtc/dist/style.css ./src/call/
|
||||||
|
cp ./node_modules/@simplex-chat/webrtc/dist/webcall.html ./src/call/index.html
|
||||||
|
cp ./node_modules/@simplex-chat/webrtc/dist/lz-string.min.js ./src/call/
|
|
@ -4,24 +4,26 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "eleventy",
|
"build": "npm run build:js && npm run build:eleventy && npm run build:tailwind",
|
||||||
"start": "npx eleventy --serve",
|
"start": "npx eleventy --serve",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"watch-tailwind": "npx tailwindcss -i ./tailwind.css -o ./_site/css/tailwind.css --watch",
|
"build:js": "cp ./node_modules/qrcode/build/qrcode.js ./src/contact/ && ./copy_call.sh",
|
||||||
"build-tailwind": "npx tailwindcss -i ./tailwind.css -o ./_site/css/tailwind.css"
|
"build:eleventy": "eleventy",
|
||||||
|
"build:tailwind": "npx tailwindcss -i ./tailwind.css -o ./_site/css/tailwind.css",
|
||||||
|
"watch:tailwind": "npx tailwindcss -i ./tailwind.css -o ./_site/css/tailwind.css --watch"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@11ty/eleventy": "^1.0.1",
|
"@11ty/eleventy": "^1.0.1",
|
||||||
|
"@simplex-chat/webrtc": "^0.1.1",
|
||||||
"common-tags": "^1.8.2",
|
"common-tags": "^1.8.2",
|
||||||
"fast-uri": "^2.1.0",
|
"fast-uri": "^2.1.0",
|
||||||
"markdown-it-anchor": "^8.6.4",
|
"markdown-it-anchor": "^8.6.4",
|
||||||
|
"markdown-it-replace-link": "^1.1.0",
|
||||||
|
"qrcode": "^1.5.1",
|
||||||
"slugify": "^1.6.5",
|
"slugify": "^1.6.5",
|
||||||
"tailwindcss": "^3.0.24"
|
"tailwindcss": "^3.0.24"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"markdown-it-replace-link": "^1.1.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
655
website/src/call/call.js
Normal file
655
website/src/call/call.js
Normal file
|
@ -0,0 +1,655 @@
|
||||||
|
"use strict";
|
||||||
|
// Inspired by
|
||||||
|
// https://github.com/webrtc/samples/blob/gh-pages/src/content/insertable-streams/endtoend-encryption
|
||||||
|
var CallMediaType;
|
||||||
|
(function (CallMediaType) {
|
||||||
|
CallMediaType["Audio"] = "audio";
|
||||||
|
CallMediaType["Video"] = "video";
|
||||||
|
})(CallMediaType || (CallMediaType = {}));
|
||||||
|
var VideoCamera;
|
||||||
|
(function (VideoCamera) {
|
||||||
|
VideoCamera["User"] = "user";
|
||||||
|
VideoCamera["Environment"] = "environment";
|
||||||
|
})(VideoCamera || (VideoCamera = {}));
|
||||||
|
// for debugging
|
||||||
|
// var sendMessageToNative = ({resp}: WVApiMessage) => console.log(JSON.stringify({command: resp}))
|
||||||
|
var sendMessageToNative = (msg) => console.log(JSON.stringify(msg));
|
||||||
|
// Global object with cryptrographic/encoding functions
|
||||||
|
const callCrypto = callCryptoFunction();
|
||||||
|
var TransformOperation;
|
||||||
|
(function (TransformOperation) {
|
||||||
|
TransformOperation["Encrypt"] = "encrypt";
|
||||||
|
TransformOperation["Decrypt"] = "decrypt";
|
||||||
|
})(TransformOperation || (TransformOperation = {}));
|
||||||
|
let activeCall;
|
||||||
|
const processCommand = (function () {
|
||||||
|
const defaultIceServers = [
|
||||||
|
{ urls: ["stun:stun.simplex.im:443"] },
|
||||||
|
{ urls: ["turn:turn.simplex.im:443"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
|
||||||
|
];
|
||||||
|
function getCallConfig(encodedInsertableStreams, iceServers, relay) {
|
||||||
|
return {
|
||||||
|
peerConnectionConfig: {
|
||||||
|
iceServers: iceServers !== null && iceServers !== void 0 ? iceServers : defaultIceServers,
|
||||||
|
iceCandidatePoolSize: 10,
|
||||||
|
encodedInsertableStreams,
|
||||||
|
iceTransportPolicy: relay ? "relay" : "all",
|
||||||
|
},
|
||||||
|
iceCandidates: {
|
||||||
|
delay: 3000,
|
||||||
|
extrasInterval: 2000,
|
||||||
|
extrasTimeout: 8000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function getIceCandidates(conn, config) {
|
||||||
|
return new Promise((resolve, _) => {
|
||||||
|
let candidates = [];
|
||||||
|
let resolved = false;
|
||||||
|
let extrasInterval;
|
||||||
|
let extrasTimeout;
|
||||||
|
const delay = setTimeout(() => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolveIceCandidates();
|
||||||
|
extrasInterval = setInterval(() => {
|
||||||
|
sendIceCandidates();
|
||||||
|
}, config.iceCandidates.extrasInterval);
|
||||||
|
extrasTimeout = setTimeout(() => {
|
||||||
|
clearInterval(extrasInterval);
|
||||||
|
sendIceCandidates();
|
||||||
|
}, config.iceCandidates.extrasTimeout);
|
||||||
|
}
|
||||||
|
}, config.iceCandidates.delay);
|
||||||
|
conn.onicecandidate = ({ candidate: c }) => c && candidates.push(c);
|
||||||
|
conn.onicegatheringstatechange = () => {
|
||||||
|
if (conn.iceGatheringState == "complete") {
|
||||||
|
if (resolved) {
|
||||||
|
if (extrasInterval)
|
||||||
|
clearInterval(extrasInterval);
|
||||||
|
if (extrasTimeout)
|
||||||
|
clearTimeout(extrasTimeout);
|
||||||
|
sendIceCandidates();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolveIceCandidates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function resolveIceCandidates() {
|
||||||
|
if (delay)
|
||||||
|
clearTimeout(delay);
|
||||||
|
resolved = true;
|
||||||
|
const iceCandidates = serialize(candidates);
|
||||||
|
candidates = [];
|
||||||
|
resolve(iceCandidates);
|
||||||
|
}
|
||||||
|
function sendIceCandidates() {
|
||||||
|
if (candidates.length === 0)
|
||||||
|
return;
|
||||||
|
const iceCandidates = serialize(candidates);
|
||||||
|
candidates = [];
|
||||||
|
sendMessageToNative({ resp: { type: "ice", iceCandidates } });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function initializeCall(config, mediaType, aesKey, useWorker) {
|
||||||
|
const pc = new RTCPeerConnection(config.peerConnectionConfig);
|
||||||
|
const remoteStream = new MediaStream();
|
||||||
|
const localCamera = VideoCamera.User;
|
||||||
|
const localStream = await getLocalMediaStream(mediaType, localCamera);
|
||||||
|
const iceCandidates = getIceCandidates(pc, config);
|
||||||
|
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker };
|
||||||
|
await setupMediaStreams(call);
|
||||||
|
pc.addEventListener("connectionstatechange", connectionStateChange);
|
||||||
|
return call;
|
||||||
|
async function connectionStateChange() {
|
||||||
|
sendMessageToNative({
|
||||||
|
resp: {
|
||||||
|
type: "connection",
|
||||||
|
state: {
|
||||||
|
connectionState: pc.connectionState,
|
||||||
|
iceConnectionState: pc.iceConnectionState,
|
||||||
|
iceGatheringState: pc.iceGatheringState,
|
||||||
|
signalingState: pc.signalingState,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (pc.connectionState == "disconnected" || pc.connectionState == "failed") {
|
||||||
|
pc.removeEventListener("connectionstatechange", connectionStateChange);
|
||||||
|
if (activeCall) {
|
||||||
|
setTimeout(() => sendMessageToNative({ resp: { type: "ended" } }), 0);
|
||||||
|
}
|
||||||
|
endCall();
|
||||||
|
}
|
||||||
|
else if (pc.connectionState == "connected") {
|
||||||
|
const stats = (await pc.getStats());
|
||||||
|
for (const stat of stats.values()) {
|
||||||
|
const { type, state } = stat;
|
||||||
|
if (type === "candidate-pair" && state === "succeeded") {
|
||||||
|
const iceCandidatePair = stat;
|
||||||
|
const resp = {
|
||||||
|
type: "connected",
|
||||||
|
connectionInfo: {
|
||||||
|
iceCandidatePair,
|
||||||
|
localCandidate: stats.get(iceCandidatePair.localCandidateId),
|
||||||
|
remoteCandidate: stats.get(iceCandidatePair.remoteCandidateId),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setTimeout(() => sendMessageToNative({ resp }), 500);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function serialize(x) {
|
||||||
|
return LZString.compressToBase64(JSON.stringify(x));
|
||||||
|
}
|
||||||
|
function parse(s) {
|
||||||
|
return JSON.parse(LZString.decompressFromBase64(s));
|
||||||
|
}
|
||||||
|
async function processCommand(body) {
|
||||||
|
const { corrId, command } = body;
|
||||||
|
const pc = activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection;
|
||||||
|
let resp;
|
||||||
|
try {
|
||||||
|
switch (command.type) {
|
||||||
|
case "capabilities":
|
||||||
|
console.log("starting outgoing call - capabilities");
|
||||||
|
if (activeCall)
|
||||||
|
endCall();
|
||||||
|
// This request for local media stream is made to prompt for camera/mic permissions on call start
|
||||||
|
if (command.media)
|
||||||
|
await getLocalMediaStream(command.media, VideoCamera.User);
|
||||||
|
const encryption = supportsInsertableStreams(command.useWorker);
|
||||||
|
resp = { type: "capabilities", capabilities: { encryption } };
|
||||||
|
break;
|
||||||
|
case "start": {
|
||||||
|
console.log("starting incoming call - create webrtc session");
|
||||||
|
if (activeCall)
|
||||||
|
endCall();
|
||||||
|
const { media, useWorker, iceServers, relay } = command;
|
||||||
|
const encryption = supportsInsertableStreams(useWorker);
|
||||||
|
const aesKey = encryption ? command.aesKey : undefined;
|
||||||
|
activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey, useWorker);
|
||||||
|
const pc = activeCall.connection;
|
||||||
|
const offer = await pc.createOffer();
|
||||||
|
await pc.setLocalDescription(offer);
|
||||||
|
// for debugging, returning the command for callee to use
|
||||||
|
// resp = {
|
||||||
|
// type: "offer",
|
||||||
|
// offer: serialize(offer),
|
||||||
|
// iceCandidates: await activeCall.iceCandidates,
|
||||||
|
// capabilities: {encryption},
|
||||||
|
// media,
|
||||||
|
// iceServers,
|
||||||
|
// relay,
|
||||||
|
// aesKey,
|
||||||
|
// useWorker,
|
||||||
|
// }
|
||||||
|
resp = {
|
||||||
|
type: "offer",
|
||||||
|
offer: serialize(offer),
|
||||||
|
iceCandidates: await activeCall.iceCandidates,
|
||||||
|
capabilities: { encryption },
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "offer":
|
||||||
|
if (activeCall) {
|
||||||
|
resp = { type: "error", message: "accept: call already started" };
|
||||||
|
}
|
||||||
|
else if (!supportsInsertableStreams(command.useWorker) && command.aesKey) {
|
||||||
|
resp = { type: "error", message: "accept: encryption is not supported" };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const offer = parse(command.offer);
|
||||||
|
const remoteIceCandidates = parse(command.iceCandidates);
|
||||||
|
const { media, aesKey, useWorker, iceServers, relay } = command;
|
||||||
|
activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey, useWorker);
|
||||||
|
const pc = activeCall.connection;
|
||||||
|
await pc.setRemoteDescription(new RTCSessionDescription(offer));
|
||||||
|
const answer = await pc.createAnswer();
|
||||||
|
await pc.setLocalDescription(answer);
|
||||||
|
addIceCandidates(pc, remoteIceCandidates);
|
||||||
|
// same as command for caller to use
|
||||||
|
resp = {
|
||||||
|
type: "answer",
|
||||||
|
answer: serialize(answer),
|
||||||
|
iceCandidates: await activeCall.iceCandidates,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "answer":
|
||||||
|
if (!pc) {
|
||||||
|
resp = { type: "error", message: "answer: call not started" };
|
||||||
|
}
|
||||||
|
else if (!pc.localDescription) {
|
||||||
|
resp = { type: "error", message: "answer: local description is not set" };
|
||||||
|
}
|
||||||
|
else if (pc.currentRemoteDescription) {
|
||||||
|
resp = { type: "error", message: "answer: remote description already set" };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const answer = parse(command.answer);
|
||||||
|
const remoteIceCandidates = parse(command.iceCandidates);
|
||||||
|
await pc.setRemoteDescription(new RTCSessionDescription(answer));
|
||||||
|
addIceCandidates(pc, remoteIceCandidates);
|
||||||
|
resp = { type: "ok" };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "ice":
|
||||||
|
if (pc) {
|
||||||
|
const remoteIceCandidates = parse(command.iceCandidates);
|
||||||
|
addIceCandidates(pc, remoteIceCandidates);
|
||||||
|
resp = { type: "ok" };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resp = { type: "error", message: "ice: call not started" };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "media":
|
||||||
|
if (!activeCall) {
|
||||||
|
resp = { type: "error", message: "media: call not started" };
|
||||||
|
}
|
||||||
|
else if (activeCall.localMedia == CallMediaType.Audio && command.media == CallMediaType.Video) {
|
||||||
|
resp = { type: "error", message: "media: no video" };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
enableMedia(activeCall.localStream, command.media, command.enable);
|
||||||
|
resp = { type: "ok" };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "camera":
|
||||||
|
if (!activeCall || !pc) {
|
||||||
|
resp = { type: "error", message: "camera: call not started" };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await replaceMedia(activeCall, command.camera);
|
||||||
|
resp = { type: "ok" };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "end":
|
||||||
|
endCall();
|
||||||
|
resp = { type: "ok" };
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
resp = { type: "error", message: "unknown command" };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
resp = { type: "error", message: `${command.type}: ${e.message}` };
|
||||||
|
}
|
||||||
|
const apiResp = { corrId, resp, command };
|
||||||
|
sendMessageToNative(apiResp);
|
||||||
|
return apiResp;
|
||||||
|
}
|
||||||
|
function endCall() {
|
||||||
|
var _a;
|
||||||
|
try {
|
||||||
|
(_a = activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection) === null || _a === void 0 ? void 0 : _a.close();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
activeCall = undefined;
|
||||||
|
resetVideoElements();
|
||||||
|
}
|
||||||
|
function addIceCandidates(conn, iceCandidates) {
|
||||||
|
for (const c of iceCandidates) {
|
||||||
|
conn.addIceCandidate(new RTCIceCandidate(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function setupMediaStreams(call) {
|
||||||
|
const videos = getVideoElements();
|
||||||
|
if (!videos)
|
||||||
|
throw Error("no video elements");
|
||||||
|
await setupEncryptionWorker(call);
|
||||||
|
setupLocalStream(call);
|
||||||
|
setupRemoteStream(call);
|
||||||
|
setupCodecPreferences(call);
|
||||||
|
// setupVideoElement(videos.local)
|
||||||
|
// setupVideoElement(videos.remote)
|
||||||
|
videos.local.srcObject = call.localStream;
|
||||||
|
videos.remote.srcObject = call.remoteStream;
|
||||||
|
}
|
||||||
|
async function setupEncryptionWorker(call) {
|
||||||
|
if (call.aesKey) {
|
||||||
|
if (!call.key)
|
||||||
|
call.key = await callCrypto.decodeAesKey(call.aesKey);
|
||||||
|
if (call.useWorker && !call.worker) {
|
||||||
|
const workerCode = `const callCrypto = (${callCryptoFunction.toString()})(); (${workerFunction.toString()})()`;
|
||||||
|
call.worker = new Worker(URL.createObjectURL(new Blob([workerCode], { type: "text/javascript" })));
|
||||||
|
call.worker.onerror = ({ error, filename, lineno, message }) => console.log(JSON.stringify({ error, filename, lineno, message }));
|
||||||
|
call.worker.onmessage = ({ data }) => console.log(JSON.stringify({ message: data }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setupLocalStream(call) {
|
||||||
|
const videos = getVideoElements();
|
||||||
|
if (!videos)
|
||||||
|
throw Error("no video elements");
|
||||||
|
const pc = call.connection;
|
||||||
|
let { localStream } = call;
|
||||||
|
for (const track of localStream.getTracks()) {
|
||||||
|
pc.addTrack(track, localStream);
|
||||||
|
}
|
||||||
|
if (call.aesKey && call.key) {
|
||||||
|
console.log("set up encryption for sending");
|
||||||
|
for (const sender of pc.getSenders()) {
|
||||||
|
setupPeerTransform(TransformOperation.Encrypt, sender, call.worker, call.aesKey, call.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setupRemoteStream(call) {
|
||||||
|
// Pull tracks from remote stream as they arrive add them to remoteStream video
|
||||||
|
const pc = call.connection;
|
||||||
|
pc.ontrack = (event) => {
|
||||||
|
try {
|
||||||
|
if (call.aesKey && call.key) {
|
||||||
|
console.log("set up decryption for receiving");
|
||||||
|
setupPeerTransform(TransformOperation.Decrypt, event.receiver, call.worker, call.aesKey, call.key);
|
||||||
|
}
|
||||||
|
for (const stream of event.streams) {
|
||||||
|
for (const track of stream.getTracks()) {
|
||||||
|
call.remoteStream.addTrack(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`ontrack success`);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(`ontrack error: ${e.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function setupCodecPreferences(call) {
|
||||||
|
// We assume VP8 encoding in the decode/encode stages to get the initial
|
||||||
|
// bytes to pass as plaintext so we enforce that here.
|
||||||
|
// VP8 is supported by all supports of webrtc.
|
||||||
|
// Use of VP8 by default may also reduce depacketisation issues.
|
||||||
|
// We do not encrypt the first couple of bytes of the payload so that the
|
||||||
|
// video elements can work by determining video keyframes and the opus mode
|
||||||
|
// being used. This appears to be necessary for any video feed at all.
|
||||||
|
// For VP8 this is the content described in
|
||||||
|
// https://tools.ietf.org/html/rfc6386#section-9.1
|
||||||
|
// which is 10 bytes for key frames and 3 bytes for delta frames.
|
||||||
|
// For opus (where encodedFrame.type is not set) this is the TOC byte from
|
||||||
|
// https://tools.ietf.org/html/rfc6716#section-3.1
|
||||||
|
var _a;
|
||||||
|
const capabilities = RTCRtpSender.getCapabilities("video");
|
||||||
|
if (capabilities) {
|
||||||
|
const { codecs } = capabilities;
|
||||||
|
const selectedCodecIndex = codecs.findIndex((c) => c.mimeType === "video/VP8");
|
||||||
|
const selectedCodec = codecs[selectedCodecIndex];
|
||||||
|
codecs.splice(selectedCodecIndex, 1);
|
||||||
|
codecs.unshift(selectedCodec);
|
||||||
|
for (const t of call.connection.getTransceivers()) {
|
||||||
|
if (((_a = t.sender.track) === null || _a === void 0 ? void 0 : _a.kind) === "video") {
|
||||||
|
t.setCodecPreferences(codecs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function replaceMedia(call, camera) {
|
||||||
|
const videos = getVideoElements();
|
||||||
|
if (!videos)
|
||||||
|
throw Error("no video elements");
|
||||||
|
const pc = call.connection;
|
||||||
|
for (const t of call.localStream.getTracks())
|
||||||
|
t.stop();
|
||||||
|
call.localCamera = camera;
|
||||||
|
const localStream = await getLocalMediaStream(call.localMedia, camera);
|
||||||
|
replaceTracks(pc, localStream.getVideoTracks());
|
||||||
|
replaceTracks(pc, localStream.getAudioTracks());
|
||||||
|
call.localStream = localStream;
|
||||||
|
videos.local.srcObject = localStream;
|
||||||
|
}
|
||||||
|
function replaceTracks(pc, tracks) {
|
||||||
|
if (!tracks.length)
|
||||||
|
return;
|
||||||
|
const sender = pc.getSenders().find((s) => { var _a; return ((_a = s.track) === null || _a === void 0 ? void 0 : _a.kind) === tracks[0].kind; });
|
||||||
|
if (sender)
|
||||||
|
for (const t of tracks)
|
||||||
|
sender.replaceTrack(t);
|
||||||
|
}
|
||||||
|
function setupPeerTransform(operation, peer, worker, aesKey, key) {
|
||||||
|
if (worker && "RTCRtpScriptTransform" in window) {
|
||||||
|
console.log(`${operation} with worker & RTCRtpScriptTransform`);
|
||||||
|
peer.transform = new RTCRtpScriptTransform(worker, { operation, aesKey });
|
||||||
|
}
|
||||||
|
else if ("createEncodedStreams" in peer) {
|
||||||
|
const { readable, writable } = peer.createEncodedStreams();
|
||||||
|
if (worker) {
|
||||||
|
console.log(`${operation} with worker`);
|
||||||
|
worker.postMessage({ operation, readable, writable, aesKey }, [readable, writable]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(`${operation} without worker`);
|
||||||
|
const transform = callCrypto.transformFrame[operation](key);
|
||||||
|
readable.pipeThrough(new TransformStream({ transform })).pipeTo(writable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(`no ${operation}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getLocalMediaStream(mediaType, facingMode) {
|
||||||
|
const constraints = callMediaConstraints(mediaType, facingMode);
|
||||||
|
return navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
}
|
||||||
|
function callMediaConstraints(mediaType, facingMode) {
|
||||||
|
switch (mediaType) {
|
||||||
|
case CallMediaType.Audio:
|
||||||
|
return { audio: true, video: false };
|
||||||
|
case CallMediaType.Video:
|
||||||
|
return {
|
||||||
|
audio: true,
|
||||||
|
video: {
|
||||||
|
frameRate: 24,
|
||||||
|
width: {
|
||||||
|
min: 480,
|
||||||
|
ideal: 720,
|
||||||
|
max: 1280,
|
||||||
|
},
|
||||||
|
aspectRatio: 1.33,
|
||||||
|
facingMode,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function supportsInsertableStreams(useWorker) {
|
||||||
|
return (("createEncodedStreams" in RTCRtpSender.prototype && "createEncodedStreams" in RTCRtpReceiver.prototype) ||
|
||||||
|
(!!useWorker && "RTCRtpScriptTransform" in window));
|
||||||
|
}
|
||||||
|
function resetVideoElements() {
|
||||||
|
const videos = getVideoElements();
|
||||||
|
if (!videos)
|
||||||
|
return;
|
||||||
|
videos.local.srcObject = null;
|
||||||
|
videos.remote.srcObject = null;
|
||||||
|
}
|
||||||
|
function getVideoElements() {
|
||||||
|
const local = document.getElementById("local-video-stream");
|
||||||
|
const remote = document.getElementById("remote-video-stream");
|
||||||
|
if (!(local && remote && local instanceof HTMLMediaElement && remote instanceof HTMLMediaElement))
|
||||||
|
return;
|
||||||
|
return { local, remote };
|
||||||
|
}
|
||||||
|
// function setupVideoElement(video: HTMLElement) {
|
||||||
|
// // TODO use display: none
|
||||||
|
// video.style.opacity = "0"
|
||||||
|
// video.onplaying = () => {
|
||||||
|
// video.style.opacity = "1"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
function enableMedia(s, media, enable) {
|
||||||
|
const tracks = media == CallMediaType.Video ? s.getVideoTracks() : s.getAudioTracks();
|
||||||
|
for (const t of tracks)
|
||||||
|
t.enabled = enable;
|
||||||
|
}
|
||||||
|
return processCommand;
|
||||||
|
})();
|
||||||
|
// Cryptography function - it is loaded both in the main window and in worker context (if the worker is used)
|
||||||
|
function callCryptoFunction() {
|
||||||
|
const initialPlainTextRequired = {
|
||||||
|
key: 10,
|
||||||
|
delta: 3,
|
||||||
|
empty: 1,
|
||||||
|
};
|
||||||
|
const IV_LENGTH = 12;
|
||||||
|
function encryptFrame(key) {
|
||||||
|
return async (frame, controller) => {
|
||||||
|
const data = new Uint8Array(frame.data);
|
||||||
|
const n = initialPlainTextRequired[frame.type] || 1;
|
||||||
|
const iv = randomIV();
|
||||||
|
const initial = data.subarray(0, n);
|
||||||
|
const plaintext = data.subarray(n, data.byteLength);
|
||||||
|
try {
|
||||||
|
const ciphertext = plaintext.length
|
||||||
|
? new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv.buffer }, key, plaintext))
|
||||||
|
: new Uint8Array(0);
|
||||||
|
frame.data = concatN(initial, ciphertext, iv).buffer;
|
||||||
|
controller.enqueue(frame);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(`encryption error ${e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function decryptFrame(key) {
|
||||||
|
return async (frame, controller) => {
|
||||||
|
const data = new Uint8Array(frame.data);
|
||||||
|
const n = initialPlainTextRequired[frame.type] || 1;
|
||||||
|
const initial = data.subarray(0, n);
|
||||||
|
const ciphertext = data.subarray(n, data.byteLength - IV_LENGTH);
|
||||||
|
const iv = data.subarray(data.byteLength - IV_LENGTH, data.byteLength);
|
||||||
|
try {
|
||||||
|
const plaintext = ciphertext.length
|
||||||
|
? new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext))
|
||||||
|
: new Uint8Array(0);
|
||||||
|
frame.data = concatN(initial, plaintext).buffer;
|
||||||
|
controller.enqueue(frame);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(`decryption error ${e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function decodeAesKey(aesKey) {
|
||||||
|
const keyData = callCrypto.decodeBase64url(callCrypto.encodeAscii(aesKey));
|
||||||
|
return crypto.subtle.importKey("raw", keyData, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
|
||||||
|
}
|
||||||
|
function concatN(...bs) {
|
||||||
|
const a = new Uint8Array(bs.reduce((size, b) => size + b.byteLength, 0));
|
||||||
|
bs.reduce((offset, b) => {
|
||||||
|
a.set(b, offset);
|
||||||
|
return offset + b.byteLength;
|
||||||
|
}, 0);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
function randomIV() {
|
||||||
|
return crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
||||||
|
}
|
||||||
|
const base64urlChars = new Uint8Array("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".split("").map((c) => c.charCodeAt(0)));
|
||||||
|
const base64urlLookup = new Array(256);
|
||||||
|
base64urlChars.forEach((c, i) => (base64urlLookup[c] = i));
|
||||||
|
const char_equal = "=".charCodeAt(0);
|
||||||
|
function encodeAscii(s) {
|
||||||
|
const a = new Uint8Array(s.length);
|
||||||
|
let i = s.length;
|
||||||
|
while (i--)
|
||||||
|
a[i] = s.charCodeAt(i);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
function decodeAscii(a) {
|
||||||
|
let s = "";
|
||||||
|
for (let i = 0; i < a.length; i++)
|
||||||
|
s += String.fromCharCode(a[i]);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
function encodeBase64url(a) {
|
||||||
|
const len = a.length;
|
||||||
|
const b64len = Math.ceil(len / 3) * 4;
|
||||||
|
const b64 = new Uint8Array(b64len);
|
||||||
|
let j = 0;
|
||||||
|
for (let i = 0; i < len; i += 3) {
|
||||||
|
b64[j++] = base64urlChars[a[i] >> 2];
|
||||||
|
b64[j++] = base64urlChars[((a[i] & 3) << 4) | (a[i + 1] >> 4)];
|
||||||
|
b64[j++] = base64urlChars[((a[i + 1] & 15) << 2) | (a[i + 2] >> 6)];
|
||||||
|
b64[j++] = base64urlChars[a[i + 2] & 63];
|
||||||
|
}
|
||||||
|
if (len % 3)
|
||||||
|
b64[b64len - 1] = char_equal;
|
||||||
|
if (len % 3 === 1)
|
||||||
|
b64[b64len - 2] = char_equal;
|
||||||
|
return b64;
|
||||||
|
}
|
||||||
|
function decodeBase64url(b64) {
|
||||||
|
let len = b64.length;
|
||||||
|
if (len % 4)
|
||||||
|
return;
|
||||||
|
let bLen = (len * 3) / 4;
|
||||||
|
if (b64[len - 1] === char_equal) {
|
||||||
|
len--;
|
||||||
|
bLen--;
|
||||||
|
if (b64[len - 1] === char_equal) {
|
||||||
|
len--;
|
||||||
|
bLen--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bytes = new Uint8Array(bLen);
|
||||||
|
let i = 0;
|
||||||
|
let pos = 0;
|
||||||
|
while (i < len) {
|
||||||
|
const enc1 = base64urlLookup[b64[i++]];
|
||||||
|
const enc2 = i < len ? base64urlLookup[b64[i++]] : 0;
|
||||||
|
const enc3 = i < len ? base64urlLookup[b64[i++]] : 0;
|
||||||
|
const enc4 = i < len ? base64urlLookup[b64[i++]] : 0;
|
||||||
|
if (enc1 === undefined || enc2 === undefined || enc3 === undefined || enc4 === undefined)
|
||||||
|
return;
|
||||||
|
bytes[pos++] = (enc1 << 2) | (enc2 >> 4);
|
||||||
|
bytes[pos++] = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||||||
|
bytes[pos++] = ((enc3 & 3) << 6) | (enc4 & 63);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
transformFrame: { encrypt: encryptFrame, decrypt: decryptFrame },
|
||||||
|
decodeAesKey,
|
||||||
|
encodeAscii,
|
||||||
|
decodeAscii,
|
||||||
|
encodeBase64url,
|
||||||
|
decodeBase64url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// If the worker is used for decryption, this function code (as string) is used to load the worker via Blob
|
||||||
|
// We have to use worker optionally, as it crashes in Android web view, regardless of how it is loaded
|
||||||
|
function workerFunction() {
|
||||||
|
// encryption with createEncodedStreams support
|
||||||
|
self.addEventListener("message", async ({ data }) => {
|
||||||
|
await setupTransform(data);
|
||||||
|
});
|
||||||
|
// encryption using RTCRtpScriptTransform.
|
||||||
|
if ("RTCTransformEvent" in self) {
|
||||||
|
self.addEventListener("rtctransform", async ({ transformer }) => {
|
||||||
|
try {
|
||||||
|
const { operation, aesKey } = transformer.options;
|
||||||
|
const { readable, writable } = transformer;
|
||||||
|
await setupTransform({ operation, aesKey, readable, writable });
|
||||||
|
self.postMessage({ result: "setupTransform success" });
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
self.postMessage({ message: `setupTransform error: ${e.message}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function setupTransform({ operation, aesKey, readable, writable }) {
|
||||||
|
const key = await callCrypto.decodeAesKey(aesKey);
|
||||||
|
const transform = callCrypto.transformFrame[operation](key);
|
||||||
|
readable.pipeThrough(new TransformStream({ transform })).pipeTo(writable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=call.js.map
|
1
website/src/call/call.js.map
Normal file
1
website/src/call/call.js.map
Normal file
File diff suppressed because one or more lines are too long
52
website/src/call/index.html
Normal file
52
website/src/call/index.html
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link href="./style.css" rel="stylesheet" />
|
||||||
|
<script src="./lz-string.min.js"></script>
|
||||||
|
<style>
|
||||||
|
#data-for-peer,
|
||||||
|
#url-for-peer,
|
||||||
|
#chat-command-for-peer,
|
||||||
|
#pass-data-to-peer {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
#ui-overlay {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<video id="remote-video-stream" autoplay playsinline></video>
|
||||||
|
<video id="local-video-stream" muted autoplay playsinline></video>
|
||||||
|
<div id="ui-overlay">
|
||||||
|
<div>
|
||||||
|
<!-- <button id="start-e2ee-call" type="submit">Start call with e2ee</button>
|
||||||
|
<button id="start-call" type="submit">Start call</button>
|
||||||
|
<button id="copy-url-for-peer" type="submit" style="display: none">Copy url for your contact</button>
|
||||||
|
<button id="copy-data-for-peer" type="submit" style="display: none">Copy data for your contact</button> -->
|
||||||
|
<button id="copy-simplex-chat-command" type="submit" style="display: none">Copy SimpleX Chat command</button>
|
||||||
|
<p id="pass-data-to-peer" style="display: none">Send copied data back to your contact</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
id="command-to-process"
|
||||||
|
cols="30"
|
||||||
|
rows="10"
|
||||||
|
style="display: none"
|
||||||
|
placeholder="Pass copied URL to your contact. Once they send you back the data, paste it here and click Connect"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="process-command" type="submit" style="display: none">Connect</button>
|
||||||
|
</div>
|
||||||
|
<div id="url-for-peer" style="display: none"></div>
|
||||||
|
<div id="data-for-peer" style="display: none"></div>
|
||||||
|
<div id="simplex-chat-command" style="display: none"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<footer>
|
||||||
|
<script src="./call.js"></script>
|
||||||
|
<script src="./ui.js"></script>
|
||||||
|
</footer>
|
||||||
|
</html>
|
1
website/src/call/lz-string.min.js
vendored
Normal file
1
website/src/call/lz-string.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
var LZString=function(){function o(o,r){if(!t[o]){t[o]={};for(var n=0;n<o.length;n++)t[o][o.charAt(n)]=n}return t[o][r]}var r=String.fromCharCode,n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",t={},i={compressToBase64:function(o){if(null==o)return"";var r=i._compress(o,6,function(o){return n.charAt(o)});switch(r.length%4){default:case 0:return r;case 1:return r+"===";case 2:return r+"==";case 3:return r+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(e){return o(n,r.charAt(e))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(o){return null==o?"":""==o?null:i._decompress(o.length,16384,function(r){return o.charCodeAt(r)-32})},compressToUint8Array:function(o){for(var r=i.compress(o),n=new Uint8Array(2*r.length),e=0,t=r.length;t>e;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return"";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<o.length;i+=1)if(u=o.charAt(i),Object.prototype.hasOwnProperty.call(s,u)||(s[u]=f++,p[u]=!0),c=a+u,Object.prototype.hasOwnProperty.call(s,c))a=c;else{if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u)}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var t,i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(t=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return"";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else{if(l!==d)return null;v=s+s.charAt(0)}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module&&(module.exports=LZString);
|
41
website/src/call/style.css
Normal file
41
website/src/call/style.css
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#remote-video-stream {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
#local-video-stream {
|
||||||
|
position: absolute;
|
||||||
|
width: 30%;
|
||||||
|
max-width: 30%;
|
||||||
|
object-fit: cover;
|
||||||
|
margin: 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-media-controls {
|
||||||
|
display: none !important;
|
||||||
|
-webkit-appearance: none !important;
|
||||||
|
}
|
||||||
|
*::-webkit-media-controls-panel {
|
||||||
|
display: none !important;
|
||||||
|
-webkit-appearance: none !important;
|
||||||
|
}
|
||||||
|
*::-webkit-media-controls-play-button {
|
||||||
|
display: none !important;
|
||||||
|
-webkit-appearance: none !important;
|
||||||
|
}
|
||||||
|
*::-webkit-media-controls-start-playback-button {
|
||||||
|
display: none !important;
|
||||||
|
-webkit-appearance: none !important;
|
||||||
|
}
|
125
website/src/call/ui.js
Normal file
125
website/src/call/ui.js
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
;(async function run() {
|
||||||
|
// const START_E2EE_CALL_BTN = "start-e2ee-call"
|
||||||
|
// const START_CALL_BTN = "start-call"
|
||||||
|
const URL_FOR_PEER = "url-for-peer"
|
||||||
|
// const COPY_URL_FOR_PEER_BTN = "copy-url-for-peer"
|
||||||
|
const DATA_FOR_PEER = "data-for-peer"
|
||||||
|
// const COPY_DATA_FOR_PEER_BTN = "copy-data-for-peer"
|
||||||
|
const PASS_DATA_TO_PEER_TEXT = "pass-data-to-peer"
|
||||||
|
const SIMPLEX_CHAT_COMMAND = "simplex-chat-command"
|
||||||
|
const COPY_SIMPLEX_CHAT_COMMAND_BTN = "copy-simplex-chat-command"
|
||||||
|
const COMMAND_TO_PROCESS = "command-to-process"
|
||||||
|
const PROCESS_COMMAND_BTN = "process-command"
|
||||||
|
const urlForPeer = document.getElementById(URL_FOR_PEER)
|
||||||
|
const dataForPeer = document.getElementById(DATA_FOR_PEER)
|
||||||
|
const passDataToPeerText = document.getElementById(PASS_DATA_TO_PEER_TEXT)
|
||||||
|
const simplexChatCommand = document.getElementById(SIMPLEX_CHAT_COMMAND)
|
||||||
|
const commandToProcess = document.getElementById(COMMAND_TO_PROCESS)
|
||||||
|
const processCommandButton = document.getElementById(PROCESS_COMMAND_BTN)
|
||||||
|
// const startE2EECallButton = document.getElementById(START_E2EE_CALL_BTN)
|
||||||
|
// const {resp} = await processCommand({command: {type: "capabilities", useWorker: true}})
|
||||||
|
// if (resp?.capabilities?.encryption) {
|
||||||
|
// startE2EECallButton.onclick = startCall(true)
|
||||||
|
// } else {
|
||||||
|
// startE2EECallButton.style.display = "none"
|
||||||
|
// }
|
||||||
|
// const startCallButton = document.getElementById(START_CALL_BTN)
|
||||||
|
// startCallButton.onclick = startCall()
|
||||||
|
// const copyUrlButton = document.getElementById(COPY_URL_FOR_PEER_BTN)
|
||||||
|
// copyUrlButton.onclick = () => {
|
||||||
|
// navigator.clipboard.writeText(urlForPeer.innerText)
|
||||||
|
// commandToProcess.style.display = ""
|
||||||
|
// processCommandButton.style.display = ""
|
||||||
|
// }
|
||||||
|
// const copyDataButton = document.getElementById(COPY_DATA_FOR_PEER_BTN)
|
||||||
|
// copyDataButton.onclick = () => {
|
||||||
|
// navigator.clipboard.writeText(dataForPeer.innerText)
|
||||||
|
// commandToProcess.style.display = ""
|
||||||
|
// processCommandButton.style.display = ""
|
||||||
|
// }
|
||||||
|
const copySimplexChatCommandButton = document.getElementById(COPY_SIMPLEX_CHAT_COMMAND_BTN)
|
||||||
|
copySimplexChatCommandButton.onclick = () => {
|
||||||
|
navigator.clipboard.writeText(simplexChatCommand.innerText)
|
||||||
|
if (simplexChatCommand.innerText.startsWith("/_call offer")) {
|
||||||
|
commandToProcess.style.display = ""
|
||||||
|
processCommandButton.style.display = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processCommandButton.onclick = () => {
|
||||||
|
sendCommand(JSON.parse(commandToProcess.value))
|
||||||
|
commandToProcess.value = ""
|
||||||
|
}
|
||||||
|
const parsed = new URLSearchParams(document.location.hash.substring(1))
|
||||||
|
let commandStr = parsed.get("command")
|
||||||
|
if (commandStr) {
|
||||||
|
// startE2EECallButton.style.display = "none"
|
||||||
|
// startCallButton.style.display = "none"
|
||||||
|
await sendCommand(JSON.parse(decodeURIComponent(commandStr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// function startCall(encryption) {
|
||||||
|
// return async () => {
|
||||||
|
// let aesKey
|
||||||
|
// if (encryption) {
|
||||||
|
// const key = await crypto.subtle.generateKey({name: "AES-GCM", length: 256}, true, ["encrypt", "decrypt"])
|
||||||
|
// const keyBytes = await crypto.subtle.exportKey("raw", key)
|
||||||
|
// aesKey = callCrypto.decodeAscii(callCrypto.encodeBase64url(new Uint8Array(keyBytes)))
|
||||||
|
// }
|
||||||
|
// startE2EECallButton.style.display = "none"
|
||||||
|
// startCallButton.style.display = "none"
|
||||||
|
// await sendCommand({type: "start", media: "video", aesKey, useWorker: true})
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
async function sendCommand(command) {
|
||||||
|
try {
|
||||||
|
console.log(command)
|
||||||
|
const {resp} = await processCommand({command})
|
||||||
|
console.log(resp)
|
||||||
|
switch (resp.type) {
|
||||||
|
case "offer": {
|
||||||
|
const {media} = command
|
||||||
|
const {offer, iceCandidates, capabilities} = resp
|
||||||
|
const aesKey = capabilities.encryption ? command.aesKey : undefined
|
||||||
|
const peerWCommand = {type: "offer", offer, iceCandidates, media, aesKey, useWorker: true}
|
||||||
|
const url = new URL(document.location)
|
||||||
|
parsed.set("command", encodeURIComponent(JSON.stringify(peerWCommand)))
|
||||||
|
url.hash = parsed.toString()
|
||||||
|
urlForPeer.innerText = url.toString()
|
||||||
|
dataForPeer.innerText = JSON.stringify(peerWCommand)
|
||||||
|
|
||||||
|
const webRTCCallOffer = {callType: {media, capabilities}, rtcSession: {rtcSession: offer, rtcIceCandidates: iceCandidates}}
|
||||||
|
const peerChatCommand = `/_call offer @${parsed.get("contact_id")} ${JSON.stringify(webRTCCallOffer)}`
|
||||||
|
simplexChatCommand.innerText = peerChatCommand
|
||||||
|
|
||||||
|
// copyUrlButton.style.display = ""
|
||||||
|
// copyDataButton.style.display = ""
|
||||||
|
copySimplexChatCommandButton.style.display = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "answer": {
|
||||||
|
const {answer, iceCandidates} = resp
|
||||||
|
const peerWCommand = {type: "answer", answer, iceCandidates}
|
||||||
|
dataForPeer.innerText = JSON.stringify(peerWCommand)
|
||||||
|
|
||||||
|
const webRTCSession = {rtcSession: answer, rtcIceCandidates: iceCandidates}
|
||||||
|
const peerChatCommand = `/_call answer @${parsed.get("contact_id")} ${JSON.stringify(webRTCSession)}`
|
||||||
|
// copyUrlButton.style.display = "none"
|
||||||
|
// copyDataButton.style.display = ""
|
||||||
|
copySimplexChatCommandButton.style.display = ""
|
||||||
|
simplexChatCommand.innerText = peerChatCommand
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "ok":
|
||||||
|
if ((command.type = "answer")) {
|
||||||
|
console.log("connecting")
|
||||||
|
commandToProcess.style.display = "none"
|
||||||
|
processCommandButton.style.display = "none"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error: ", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
File diff suppressed because one or more lines are too long
|
@ -6,4 +6,3 @@ rm website/src/blog/README.md
|
||||||
cd website
|
cd website
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
npm run build-tailwind
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue