mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
terminal: support to connect via webrtc (#659)
* terminal: support to connect via webrtc * npm package * update webrtc npm package * remove console.log * fix test
This commit is contained in:
committed by
GitHub
parent
84bf815e5c
commit
295cec7c53
@@ -1,17 +1,30 @@
|
||||
{
|
||||
"name": "simplex-chat-webrtc",
|
||||
"version": "0.0.1",
|
||||
"description": "WebRTC call in browser and webview",
|
||||
"name": "@simplex-chat/webrtc",
|
||||
"version": "0.0.2",
|
||||
"description": "WebRTC call in browser and webview for SimpleX Chat clients",
|
||||
"main": "dist/call.js",
|
||||
"types": "dist/call.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "prettier --write --ignore-unknown . && tsc && ./copy"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/simplex-chat/simplex-chat.git"
|
||||
},
|
||||
"keywords": [
|
||||
"SimpleX",
|
||||
"WebRTC"
|
||||
],
|
||||
"author": "",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"homepage": "https://github.com/simplex-chat/simplex-chat/packages/simplex-chat-webrtc#readme",
|
||||
"author": "SimpleX Chat",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@types/lz-string": "^1.3.34",
|
||||
|
||||
@@ -569,8 +569,8 @@ interface CallCrypto {
|
||||
decodeAesKey: (aesKey: string) => Promise<CryptoKey>
|
||||
encodeAscii: (s: string) => Uint8Array
|
||||
decodeAscii: (a: Uint8Array) => string
|
||||
encodeBase64: (a: Uint8Array) => Uint8Array
|
||||
decodeBase64: (b64: Uint8Array) => Uint8Array | undefined
|
||||
encodeBase64url: (a: Uint8Array) => Uint8Array
|
||||
decodeBase64url: (b64: Uint8Array) => Uint8Array | undefined
|
||||
}
|
||||
|
||||
interface RTCEncodedVideoFrame {
|
||||
@@ -624,8 +624,8 @@ function callCryptoFunction(): CallCrypto {
|
||||
}
|
||||
|
||||
function decodeAesKey(aesKey: string): Promise<CryptoKey> {
|
||||
const keyData = callCrypto.decodeBase64(callCrypto.encodeAscii(aesKey))
|
||||
return crypto.subtle.importKey("raw", keyData!, {name: "AES-GCM", length: 256}, false, ["encrypt", "decrypt"])
|
||||
const keyData = callCrypto.decodeBase64url(callCrypto.encodeAscii(aesKey))
|
||||
return crypto.subtle.importKey("raw", keyData!, {name: "AES-GCM", length: 256}, true, ["encrypt", "decrypt"])
|
||||
}
|
||||
|
||||
function concatN(...bs: Uint8Array[]): Uint8Array {
|
||||
@@ -641,12 +641,12 @@ function callCryptoFunction(): CallCrypto {
|
||||
return crypto.getRandomValues(new Uint8Array(IV_LENGTH))
|
||||
}
|
||||
|
||||
const base64chars = new Uint8Array(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("").map((c) => c.charCodeAt(0))
|
||||
const base64urlChars = new Uint8Array(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".split("").map((c) => c.charCodeAt(0))
|
||||
)
|
||||
|
||||
const base64lookup = new Array(256) as (number | undefined)[]
|
||||
base64chars.forEach((c, i) => (base64lookup[c] = i))
|
||||
const base64urlLookup = new Array(256) as (number | undefined)[]
|
||||
base64urlChars.forEach((c, i) => (base64urlLookup[c] = i))
|
||||
|
||||
const char_equal = "=".charCodeAt(0)
|
||||
|
||||
@@ -663,17 +663,17 @@ function callCryptoFunction(): CallCrypto {
|
||||
return s
|
||||
}
|
||||
|
||||
function encodeBase64(a: Uint8Array): Uint8Array {
|
||||
function encodeBase64url(a: Uint8Array): Uint8Array {
|
||||
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++] = base64chars[a[i] >> 2]
|
||||
b64[j++] = base64chars[((a[i] & 3) << 4) | (a[i + 1] >> 4)]
|
||||
b64[j++] = base64chars[((a[i + 1] & 15) << 2) | (a[i + 2] >> 6)]
|
||||
b64[j++] = base64chars[a[i + 2] & 63]
|
||||
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
|
||||
@@ -682,7 +682,7 @@ function callCryptoFunction(): CallCrypto {
|
||||
return b64
|
||||
}
|
||||
|
||||
function decodeBase64(b64: Uint8Array): Uint8Array | undefined {
|
||||
function decodeBase64url(b64: Uint8Array): Uint8Array | undefined {
|
||||
let len = b64.length
|
||||
if (len % 4) return
|
||||
let bLen = (len * 3) / 4
|
||||
@@ -701,10 +701,10 @@ function callCryptoFunction(): CallCrypto {
|
||||
let i = 0
|
||||
let pos = 0
|
||||
while (i < len) {
|
||||
const enc1 = base64lookup[b64[i++]]
|
||||
const enc2 = i < len ? base64lookup[b64[i++]] : 0
|
||||
const enc3 = i < len ? base64lookup[b64[i++]] : 0
|
||||
const enc4 = i < len ? base64lookup[b64[i++]] : 0
|
||||
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)
|
||||
@@ -719,8 +719,8 @@ function callCryptoFunction(): CallCrypto {
|
||||
decodeAesKey,
|
||||
encodeAscii,
|
||||
decodeAscii,
|
||||
encodeBase64,
|
||||
decodeBase64,
|
||||
encodeBase64url,
|
||||
decodeBase64url,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,103 +1,113 @@
|
||||
;(async function run() {
|
||||
const START_E2EE_CALL_BTN = "start-e2ee-call"
|
||||
const START_CALL_BTN = "start-call"
|
||||
// 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 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 COPY_DATA_FOR_PEER_BTN = "copy-data-for-peer"
|
||||
const PASS_DATA_TO_PEER_TEXT = "pass-data-to-peer"
|
||||
const CHAT_COMMAND_FOR_PEER = "chat-command-for-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 chatCommandForPeer = document.getElementById(CHAT_COMMAND_FOR_PEER)
|
||||
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 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 apiCallStr = parsed.get("command")
|
||||
if (apiCallStr) {
|
||||
startE2EECallButton.style.display = "none"
|
||||
startCallButton.style.display = "none"
|
||||
await sendCommand(JSON.parse(decodeURIComponent(apiCallStr)))
|
||||
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.encodeBase64(new Uint8Array(keyBytes)))
|
||||
}
|
||||
sendCommand({command: {type: "start", media: "video", aesKey, useWorker: true}})
|
||||
startE2EECallButton.style.display = "none"
|
||||
startCallButton.style.display = "none"
|
||||
}
|
||||
}
|
||||
// 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(apiCall) {
|
||||
async function sendCommand(command) {
|
||||
try {
|
||||
console.log(apiCall)
|
||||
const {command} = apiCall
|
||||
const {resp} = await processCommand(apiCall)
|
||||
console.log(command)
|
||||
const {resp} = await processCommand({command})
|
||||
console.log(resp)
|
||||
switch (resp.type) {
|
||||
case "offer": {
|
||||
const {media, aesKey} = command
|
||||
const {media} = command
|
||||
const {offer, iceCandidates, capabilities} = resp
|
||||
const peerWCommand = {
|
||||
command: {type: "offer", offer, iceCandidates, media, aesKey: capabilities.encryption ? aesKey : undefined, useWorker: true},
|
||||
}
|
||||
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)
|
||||
copyUrlButton.style.display = ""
|
||||
copyDataButton.style.display = ""
|
||||
|
||||
// const webRTCCallOffer = {callType: {media, capabilities}, rtcSession: {rtcSession: offer, rtcIceCandidates: iceCandidates}}
|
||||
// const peerChatCommand = `/_call @${parsed.contact} offer ${JSON.stringify(webRTCCallOffer)}`
|
||||
// chatCommandForPeer.innerText = peerChatCommand
|
||||
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 = {command: {type: "answer", answer, iceCandidates}}
|
||||
const peerWCommand = {type: "answer", answer, iceCandidates}
|
||||
dataForPeer.innerText = JSON.stringify(peerWCommand)
|
||||
copyUrlButton.style.display = "none"
|
||||
copyDataButton.style.display = ""
|
||||
|
||||
// const webRTCSession = {rtcSession: answer, rtcIceCandidates: iceCandidates}
|
||||
// const peerChatCommand = `/_call @${parsed.contact} answer ${JSON.stringify(webRTCSession)}`
|
||||
// chatCommandForPeer.innerText = peerChatCommand
|
||||
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":
|
||||
|
||||
@@ -21,10 +21,11 @@
|
||||
<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-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-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>
|
||||
@@ -41,7 +42,7 @@
|
||||
</div>
|
||||
<div id="url-for-peer" style="display: none"></div>
|
||||
<div id="data-for-peer" style="display: none"></div>
|
||||
<div id="chat-command-for-peer" style="display: none"></div>
|
||||
<div id="simplex-chat-command" style="display: none"></div>
|
||||
</div>
|
||||
</body>
|
||||
<footer>
|
||||
|
||||
Reference in New Issue
Block a user