mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
ios: webrtc calls started (#594)
* building towards ios calls * minor ios changes * linting * RPC calls with webview * tidy up * more types * ios: webrtc types, call works * remove comment * prettier * add prettier to build Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
@@ -1 +1,3 @@
|
||||
Do NOT edit call.js here, it is compiled from call.ts in packages/simplex-chat-webrtc
|
||||
# WebView for WebRTC calls in SimpleX Chat
|
||||
|
||||
Do NOT edit call.js here, it is compiled abd copied here from call.ts in packages/simplex-chat-webrtc
|
||||
|
||||
@@ -6,15 +6,9 @@ var CallMediaType;
|
||||
CallMediaType["Audio"] = "audio";
|
||||
CallMediaType["Video"] = "video";
|
||||
})(CallMediaType || (CallMediaType = {}));
|
||||
// STUN servers
|
||||
const peerConnectionConfig = {
|
||||
iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }],
|
||||
iceCandidatePoolSize: 10,
|
||||
encodedInsertableStreams: true,
|
||||
};
|
||||
const keyAlgorithm = {
|
||||
name: "AES-GCM",
|
||||
length: 256
|
||||
length: 256,
|
||||
};
|
||||
const keyUsages = ["encrypt", "decrypt"];
|
||||
let pc;
|
||||
@@ -24,17 +18,24 @@ const initialPlainTextRequired = {
|
||||
delta: 3,
|
||||
undefined: 1,
|
||||
};
|
||||
const defaultCallConfig = {
|
||||
iceCandidates: {
|
||||
delay: 2000,
|
||||
extrasInterval: 2000,
|
||||
extrasTimeout: 8000
|
||||
}
|
||||
};
|
||||
function defaultCallConfig(encodedInsertableStreams) {
|
||||
return {
|
||||
peerConnectionConfig: {
|
||||
iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }],
|
||||
iceCandidatePoolSize: 10,
|
||||
encodedInsertableStreams,
|
||||
},
|
||||
iceCandidates: {
|
||||
delay: 2000,
|
||||
extrasInterval: 2000,
|
||||
extrasTimeout: 8000,
|
||||
},
|
||||
};
|
||||
}
|
||||
async function initializeCall(config, mediaType, aesKey) {
|
||||
const conn = new RTCPeerConnection(peerConnectionConfig);
|
||||
const conn = new RTCPeerConnection(config.peerConnectionConfig);
|
||||
const remoteStream = new MediaStream();
|
||||
const localStream = await navigator.mediaDevices.getUserMedia(callMediaContraints(mediaType));
|
||||
const localStream = await navigator.mediaDevices.getUserMedia(callMediaConstraints(mediaType));
|
||||
await setUpMediaStreams(conn, localStream, remoteStream, aesKey);
|
||||
conn.addEventListener("connectionstatechange", connectionStateChange);
|
||||
const iceCandidates = new Promise((resolve, _) => {
|
||||
@@ -73,141 +74,144 @@ async function initializeCall(config, mediaType, aesKey) {
|
||||
if (delay)
|
||||
clearTimeout(delay);
|
||||
resolved = true;
|
||||
const iceCandidates = candidates.slice();
|
||||
const iceCandidates = candidates.map((c) => JSON.stringify(c));
|
||||
candidates = [];
|
||||
resolve(iceCandidates);
|
||||
}
|
||||
function sendIceCandidates() {
|
||||
if (candidates.length === 0)
|
||||
return;
|
||||
const iceCandidates = candidates.slice();
|
||||
const iceCandidates = candidates.map((c) => JSON.stringify(c));
|
||||
candidates = [];
|
||||
sendMessageToNative({ type: "ice", iceCandidates });
|
||||
sendMessageToNative({ resp: { type: "ice", iceCandidates } });
|
||||
}
|
||||
});
|
||||
return { connection: conn, iceCandidates };
|
||||
function connectionStateChange() {
|
||||
sendMessageToNative({
|
||||
type: "connection",
|
||||
state: {
|
||||
connectionState: conn.connectionState,
|
||||
iceConnectionState: conn.iceConnectionState,
|
||||
iceGatheringState: conn.iceGatheringState,
|
||||
signalingState: conn.signalingState,
|
||||
}
|
||||
resp: {
|
||||
type: "connection",
|
||||
state: {
|
||||
connectionState: conn.connectionState,
|
||||
iceConnectionState: conn.iceConnectionState,
|
||||
iceGatheringState: conn.iceGatheringState,
|
||||
signalingState: conn.signalingState,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (conn.connectionState == "disconnected" || conn.connectionState == "failed") {
|
||||
conn.removeEventListener("connectionstatechange", connectionStateChange);
|
||||
sendMessageToNative({ type: "ended" });
|
||||
sendMessageToNative({ resp: { type: "ended" } });
|
||||
conn.close();
|
||||
pc = undefined;
|
||||
resetVideoElements();
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO remove WCallCommand from parameter type
|
||||
function sendMessageToNative(msg) {
|
||||
console.log(JSON.stringify(msg));
|
||||
}
|
||||
// TODO remove WCallCommand from result type
|
||||
async function processCommand(command) {
|
||||
async function processCommand(body) {
|
||||
const { command, corrId } = body;
|
||||
let resp;
|
||||
switch (command.type) {
|
||||
case "capabilities":
|
||||
const encryption = supportsInsertableStreams();
|
||||
resp = { type: "capabilities", capabilities: { encryption } };
|
||||
break;
|
||||
case "start":
|
||||
console.log("starting call");
|
||||
if (pc) {
|
||||
resp = { type: "error", message: "start: call already started" };
|
||||
}
|
||||
else if (!supportsInsertableStreams() && command.aesKey) {
|
||||
resp = { type: "error", message: "start: encryption is not supported" };
|
||||
}
|
||||
else {
|
||||
try {
|
||||
try {
|
||||
switch (command.type) {
|
||||
case "capabilities":
|
||||
const encryption = supportsInsertableStreams();
|
||||
resp = { type: "capabilities", capabilities: { encryption } };
|
||||
break;
|
||||
case "start":
|
||||
console.log("starting call");
|
||||
if (pc) {
|
||||
resp = { type: "error", message: "start: call already started" };
|
||||
}
|
||||
else if (!supportsInsertableStreams() && command.aesKey) {
|
||||
resp = { type: "error", message: "start: encryption is not supported" };
|
||||
}
|
||||
else {
|
||||
const { media, aesKey } = command;
|
||||
const call = await initializeCall(defaultCallConfig, media, aesKey);
|
||||
const call = await initializeCall(defaultCallConfig(!!aesKey), media, aesKey);
|
||||
const { connection, iceCandidates } = call;
|
||||
pc = connection;
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
// for debugging, returning the command for callee to use
|
||||
resp = { type: "accept", offer, iceCandidates: await iceCandidates, media, aesKey };
|
||||
resp = { type: "accept", offer: JSON.stringify(offer), iceCandidates: await iceCandidates, media, aesKey };
|
||||
// resp = {type: "offer", offer, iceCandidates: await iceCandidates}
|
||||
}
|
||||
catch (e) {
|
||||
resp = { type: "error", message: e.message };
|
||||
break;
|
||||
case "accept":
|
||||
if (pc) {
|
||||
resp = { type: "error", message: "accept: call already started" };
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "accept":
|
||||
if (pc) {
|
||||
resp = { type: "error", message: "accept: call already started" };
|
||||
}
|
||||
else if (!supportsInsertableStreams() && command.aesKey) {
|
||||
resp = { type: "error", message: "accept: encryption is not supported" };
|
||||
}
|
||||
else {
|
||||
try {
|
||||
const call = await initializeCall(defaultCallConfig, command.media, command.aesKey);
|
||||
else if (!supportsInsertableStreams() && command.aesKey) {
|
||||
resp = { type: "error", message: "accept: encryption is not supported" };
|
||||
}
|
||||
else {
|
||||
const offer = JSON.parse(command.offer);
|
||||
const remoteIceCandidates = command.iceCandidates.map((c) => JSON.parse(c));
|
||||
const call = await initializeCall(defaultCallConfig(!!command.aesKey), command.media, command.aesKey);
|
||||
const { connection, iceCandidates } = call;
|
||||
pc = connection;
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(command.offer));
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(offer));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
addIceCandidates(pc, command.iceCandidates);
|
||||
addIceCandidates(pc, remoteIceCandidates);
|
||||
// same as command for caller to use
|
||||
resp = { type: "answer", answer, iceCandidates: await iceCandidates };
|
||||
resp = { type: "answer", answer: JSON.stringify(answer), iceCandidates: await iceCandidates };
|
||||
}
|
||||
catch (e) {
|
||||
resp = { type: "error", message: e.message };
|
||||
break;
|
||||
case "answer":
|
||||
if (!pc) {
|
||||
resp = { type: "error", message: "answer: call not started" };
|
||||
}
|
||||
}
|
||||
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 {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(command.answer));
|
||||
addIceCandidates(pc, command.iceCandidates);
|
||||
resp = { type: "ok" };
|
||||
}
|
||||
break;
|
||||
case "ice":
|
||||
if (pc) {
|
||||
addIceCandidates(pc, command.iceCandidates);
|
||||
resp = { type: "ok" };
|
||||
}
|
||||
else {
|
||||
resp = { type: "error", message: "ice: call not started" };
|
||||
}
|
||||
break;
|
||||
case "end":
|
||||
if (pc) {
|
||||
pc.close();
|
||||
pc = undefined;
|
||||
resetVideoElements();
|
||||
resp = { type: "ok" };
|
||||
}
|
||||
else {
|
||||
resp = { type: "error", message: "end: call not started" };
|
||||
}
|
||||
break;
|
||||
default:
|
||||
resp = { type: "error", message: "unknown command" };
|
||||
break;
|
||||
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 = JSON.parse(command.answer);
|
||||
const remoteIceCandidates = command.iceCandidates.map((c) => JSON.parse(c));
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(answer));
|
||||
addIceCandidates(pc, remoteIceCandidates);
|
||||
resp = { type: "ok" };
|
||||
}
|
||||
break;
|
||||
case "ice":
|
||||
if (pc) {
|
||||
const remoteIceCandidates = command.iceCandidates.map((c) => JSON.parse(c));
|
||||
addIceCandidates(pc, remoteIceCandidates);
|
||||
resp = { type: "ok" };
|
||||
}
|
||||
else {
|
||||
resp = { type: "error", message: "ice: call not started" };
|
||||
}
|
||||
break;
|
||||
case "end":
|
||||
if (pc) {
|
||||
pc.close();
|
||||
pc = undefined;
|
||||
resetVideoElements();
|
||||
resp = { type: "ok" };
|
||||
}
|
||||
else {
|
||||
resp = { type: "error", message: "end: call not started" };
|
||||
}
|
||||
break;
|
||||
default:
|
||||
resp = { type: "error", message: "unknown command" };
|
||||
break;
|
||||
}
|
||||
}
|
||||
sendMessageToNative(resp);
|
||||
return resp;
|
||||
catch (e) {
|
||||
resp = { type: "error", message: e.message };
|
||||
}
|
||||
const apiResp = { resp, corrId };
|
||||
sendMessageToNative(apiResp);
|
||||
return apiResp;
|
||||
}
|
||||
function addIceCandidates(conn, iceCandidates) {
|
||||
for (const c of iceCandidates) {
|
||||
@@ -274,7 +278,7 @@ async function setUpMediaStreams(pc, localStream, remoteStream, aesKey) {
|
||||
videos.local.srcObject = localStream;
|
||||
videos.remote.srcObject = remoteStream;
|
||||
}
|
||||
function callMediaContraints(mediaType) {
|
||||
function callMediaConstraints(mediaType) {
|
||||
switch (mediaType) {
|
||||
case CallMediaType.Audio:
|
||||
return { audio: true, video: false };
|
||||
@@ -294,8 +298,7 @@ function callMediaContraints(mediaType) {
|
||||
}
|
||||
}
|
||||
function supportsInsertableStreams() {
|
||||
return ("createEncodedStreams" in RTCRtpSender.prototype)
|
||||
&& ("createEncodedStreams" in RTCRtpReceiver.prototype);
|
||||
return "createEncodedStreams" in RTCRtpSender.prototype && "createEncodedStreams" in RTCRtpReceiver.prototype;
|
||||
}
|
||||
function resetVideoElements() {
|
||||
const videos = getVideoElements();
|
||||
@@ -326,10 +329,6 @@ function getVideoElements() {
|
||||
// localStream.getVideoTracks()[0].enabled = false
|
||||
// }
|
||||
// }
|
||||
function f() {
|
||||
console.log("Debug Function");
|
||||
return "Debugging Return";
|
||||
}
|
||||
/* Stream Transforms */
|
||||
function setupPeerTransform(peer, transform) {
|
||||
const streams = peer.createEncodedStreams();
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
video::-webkit-media-controls {
|
||||
display: none;
|
||||
}
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
#remote-video-stream {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
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;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
max-width: 30%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user