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:
IanRDavies
2022-05-04 23:07:26 +01:00
committed by GitHub
parent 3e19e495de
commit f2036236f6
12 changed files with 715 additions and 347 deletions
@@ -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
+114 -115
View File
@@ -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();
+16 -15
View File
@@ -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;
}