diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt index 89883e1bf8..bbf860b39c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt @@ -83,6 +83,7 @@ enum class CallState { @Serializable sealed class WCallCommand { @Serializable @SerialName("capabilities") data class Capabilities(val media: CallMediaType): WCallCommand() + @Serializable @SerialName("permission") data class Permission(val title: String, val chrome: String, val safari: String): WCallCommand() @Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List? = null, val relay: Boolean? = null): WCallCommand() @Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List? = null, val relay: Boolean? = null): WCallCommand() @Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand() diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index c344382423..2f6c601558 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1047,6 +1047,9 @@ Call already ended! video call audio call + To make calls, allow to use your microphone. End the call and try to call again. + Click info button near address field to allow using microphone. + Open Safari Settings / Websites / Microphone, then choose Allow for localhost. Audio & video calls diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js b/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js index f042859270..0e58050fcf 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js @@ -31,6 +31,7 @@ var sendMessageToNative = (msg) => console.log(JSON.stringify(msg)); var toggleScreenShare = async () => { }; var localOrPeerMediaSourcesChanged = (_call) => { }; var inactiveCallMediaSourcesChanged = (_inactiveCallMediaSources) => { }; +var failedToGetPermissions = (_title, _description) => { }; // Global object with cryptrographic/encoding functions const callCrypto = callCryptoFunction(); var TransformOperation; @@ -159,7 +160,7 @@ const processCommand = (function () { try { localStream = (notConnectedCall === null || notConnectedCall === void 0 ? void 0 : notConnectedCall.localStream) ? notConnectedCall.localStream - : await getLocalMediaStream(inactiveCallMediaSources.mic, inactiveCallMediaSources.camera, localCamera); + : await getLocalMediaStream(inactiveCallMediaSources.mic, inactiveCallMediaSources.camera && (await browserHasCamera()), localCamera); } catch (e) { console.log("Error while getting local media stream", e); @@ -296,7 +297,7 @@ const processCommand = (function () { endCall(); let localStream = null; try { - localStream = await getLocalMediaStream(true, command.media == CallMediaType.Video, VideoCamera.User); + localStream = await getLocalMediaStream(true, command.media == CallMediaType.Video && (await browserHasCamera()), VideoCamera.User); const videos = getVideoElements(); if (videos) { videos.local.srcObject = localStream; @@ -304,6 +305,10 @@ const processCommand = (function () { } } catch (e) { + console.log(e); + // Do not allow to continue the call without audio permission + resp = { type: "error", message: "capabilities: no permissions were granted for mic and/or camera" }; + break; localStream = new MediaStream(); // Will be shown on the next stage of call estabilishing, can work without any streams //desktopShowPermissionsAlert(command.media) @@ -437,6 +442,11 @@ const processCommand = (function () { break; case "media": if (!activeCall) { + if (!notConnectedCall) { + // call can have a slow startup and be in this place even before "capabilities" stage + resp = { type: "error", message: "media: call has not yet pass capabilities stage" }; + break; + } switch (command.source) { case CallMediaSource.Mic: inactiveCallMediaSources.mic = command.enable; @@ -484,8 +494,11 @@ const processCommand = (function () { if (!activeCall || !pc) { if (notConnectedCall) { recreateLocalStreamWhileNotConnected(command.camera); + resp = { type: "ok" }; + } + else { + resp = { type: "error", message: "camera: call has not yet pass capabilities stage" }; } - resp = { type: "ok" }; } else { if (await replaceMedia(activeCall, CallMediaSource.Camera, true, command.camera)) { @@ -515,6 +528,10 @@ const processCommand = (function () { endCall(); resp = { type: "ok" }; break; + case "permission": + failedToGetPermissions(command.title, permissionDescription(command)); + resp = { type: "ok" }; + break; default: resp = { type: "error", message: "unknown command" }; break; @@ -884,7 +901,7 @@ const processCommand = (function () { localStream.getVideoTracks().forEach((elem) => elem.stop()); localStream.getVideoTracks().forEach((elem) => localStream.removeTrack(elem)); } - await getLocalMediaStream(inactiveCallMediaSources.mic && localStream.getAudioTracks().length == 0, inactiveCallMediaSources.camera && (localStream.getVideoTracks().length == 0 || oldCamera != newCamera), newCamera) + await getLocalMediaStream(inactiveCallMediaSources.mic && localStream.getAudioTracks().length == 0, inactiveCallMediaSources.camera && (localStream.getVideoTracks().length == 0 || oldCamera != newCamera) && (await browserHasCamera()), newCamera) .then((stream) => { stream.getTracks().forEach((elem) => { localStream.addTrack(elem); @@ -1074,6 +1091,18 @@ const processCommand = (function () { }; return navigator.mediaDevices.getDisplayMedia(constraints); } + async function browserHasCamera() { + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + const hasCamera = devices.some((elem) => elem.kind == "videoinput"); + console.log("Camera is available: " + hasCamera); + return hasCamera; + } + catch (error) { + console.log("Error while enumerating devices: " + error, error); + return false; + } + } function callMediaConstraints(mic, camera, facingMode) { return { audio: mic, @@ -1287,6 +1316,18 @@ function desktopShowPermissionsAlert(mediaType) { window.alert("Permissions denied. Please, allow access to mic and camera to make the call working and hit unmute/camera button. Don't reload the page."); } } +function permissionDescription(command) { + if (window.safari) { + return command.safari; + } + else if ((navigator.userAgent.includes("Chrome") && navigator.vendor.includes("Google Inc")) || + navigator.userAgent.includes("Firefox")) { + return command.chrome; + } + else { + return ""; + } +} // Cryptography function - it is loaded both in the main window and in worker context (if the worker is used) function callCryptoFunction() { const initialPlainTextRequired = { diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html index 0432b1f475..8ea76ed488 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html @@ -50,6 +50,12 @@
+ +
+

+

+
+