mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 12:19:54 +00:00
android, desktop: checking for camera in calls and handle uninitialized call (#4997)
* android, desktop: checking for camera in calls and handle uninitialized call * explanation for situation without permission * reorder * reorder * strings * font --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
parent
35fc0544a0
commit
54b40a5838
11 changed files with 201 additions and 11 deletions
|
@ -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<RTCIceServer>? = 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<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
|
||||
|
|
|
@ -1047,6 +1047,9 @@
|
|||
<string name="call_already_ended">Call already ended!</string>
|
||||
<string name="icon_descr_video_call">video call</string>
|
||||
<string name="icon_descr_audio_call">audio call</string>
|
||||
<string name="call_desktop_permission_denied_title">To make calls, allow to use your microphone. End the call and try to call again.</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Click info button near address field to allow using microphone.</string>
|
||||
<string name="call_desktop_permission_denied_safari">Open Safari Settings / Websites / Microphone, then choose Allow for localhost.</string>
|
||||
|
||||
<!-- Call settings -->
|
||||
<string name="settings_audio_video_calls">Audio & video calls</string>
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -50,6 +50,12 @@
|
|||
<div id="audio-call-icon">
|
||||
<img src="/desktop/images/ic_phone_in_talk.svg" />
|
||||
</div>
|
||||
|
||||
<div id="permission-denied">
|
||||
<p id="permission-denied-title"></p>
|
||||
<p id="permission-denied-desc"></p>
|
||||
</div>
|
||||
|
||||
<p id="manage-call">
|
||||
<button id="toggle-screen" onclick="javascript:toggleScreenManually()">
|
||||
<img src="/desktop/images/ic_screen_share.svg" />
|
||||
|
|
|
@ -3,6 +3,7 @@ body {
|
|||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: black;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
#remote-video-stream.inline {
|
||||
|
@ -157,6 +158,23 @@ body {
|
|||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
#permission-denied {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
align-content: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#permission-denied-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#permission-denied-desc {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
|
@ -180,7 +198,6 @@ body {
|
|||
color: white;
|
||||
opacity: 0.8;
|
||||
width: 200px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
#info-block.audio {
|
||||
|
|
|
@ -75,6 +75,16 @@ inactiveCallMediaSourcesChanged = (inactiveCallMediaSources) => {
|
|||
document.getElementById("info-block").className = className;
|
||||
// document.getElementById("media-sources")!.innerText = inactiveCallMediaSourcesStatus(inactiveCallMediaSources)
|
||||
};
|
||||
failedToGetPermissions = (title, description) => {
|
||||
document.getElementById("info-block").style.visibility = "hidden";
|
||||
document.getElementById("progress").style.visibility = "hidden";
|
||||
document.getElementById("permission-denied-title").innerText = title;
|
||||
document.getElementById("permission-denied-desc").innerText = description;
|
||||
document.getElementById("toggle-mic").style.visibility = "hidden";
|
||||
document.getElementById("toggle-camera").style.visibility = "hidden";
|
||||
document.getElementById("toggle-screen").style.visibility = "hidden";
|
||||
document.getElementById("toggle-speaker").style.visibility = "hidden";
|
||||
};
|
||||
function enableMicIcon(enabled) {
|
||||
document.getElementById("toggle-mic").innerHTML = enabled
|
||||
? '<img src="/desktop/images/ic_mic.svg" />'
|
||||
|
|
|
@ -105,6 +105,14 @@ actual fun ActiveCallView() {
|
|||
else -> {}
|
||||
}
|
||||
is WCallResponse.Error -> {
|
||||
when (apiMsg.command) {
|
||||
is WCallCommand.Capabilities -> chatModel.callCommand.add(WCallCommand.Permission(
|
||||
title = generalGetString(MR.strings.call_desktop_permission_denied_title),
|
||||
chrome = generalGetString(MR.strings.call_desktop_permission_denied_chrome),
|
||||
safari = generalGetString(MR.strings.call_desktop_permission_denied_safari)
|
||||
))
|
||||
else -> {}
|
||||
}
|
||||
Log.e(TAG, "ActiveCallView: command error ${r.message}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ interface WVApiMessage {
|
|||
|
||||
type WCallCommand =
|
||||
| WCCapabilities
|
||||
| WCPermission
|
||||
| WCStartCall
|
||||
| WCAcceptOffer
|
||||
| WCallAnswer
|
||||
|
@ -33,7 +34,18 @@ type WCallResponse =
|
|||
| WRError
|
||||
| WCAcceptOffer
|
||||
|
||||
type WCallCommandTag = "capabilities" | "start" | "offer" | "answer" | "ice" | "media" | "camera" | "description" | "layout" | "end"
|
||||
type WCallCommandTag =
|
||||
| "capabilities"
|
||||
| "permission"
|
||||
| "start"
|
||||
| "offer"
|
||||
| "answer"
|
||||
| "ice"
|
||||
| "media"
|
||||
| "camera"
|
||||
| "description"
|
||||
| "layout"
|
||||
| "end"
|
||||
|
||||
type WCallResponseTag =
|
||||
| "capabilities"
|
||||
|
@ -154,6 +166,13 @@ interface WCLayout extends IWCallCommand {
|
|||
layout: LayoutType
|
||||
}
|
||||
|
||||
interface WCPermission extends IWCallCommand {
|
||||
type: "permission"
|
||||
title: string
|
||||
chrome: string
|
||||
safari: string
|
||||
}
|
||||
|
||||
interface WRCapabilities extends IWCallResponse {
|
||||
type: "capabilities"
|
||||
capabilities: CallCapabilities
|
||||
|
@ -220,6 +239,7 @@ var sendMessageToNative = (msg: WVApiMessage) => console.log(JSON.stringify(msg)
|
|||
var toggleScreenShare = async () => {}
|
||||
var localOrPeerMediaSourcesChanged = (_call: Call) => {}
|
||||
var inactiveCallMediaSourcesChanged = (_inactiveCallMediaSources: CallMediaSources) => {}
|
||||
var failedToGetPermissions = (_title: string, _description: string) => {}
|
||||
|
||||
// Global object with cryptrographic/encoding functions
|
||||
const callCrypto = callCryptoFunction()
|
||||
|
@ -421,7 +441,11 @@ const processCommand = (function () {
|
|||
try {
|
||||
localStream = 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)
|
||||
if (isDesktop) {
|
||||
|
@ -560,13 +584,21 @@ const processCommand = (function () {
|
|||
|
||||
let localStream: MediaStream | null = 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
|
||||
videos.local.play().catch((e) => console.log(e))
|
||||
}
|
||||
} 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)
|
||||
|
@ -698,6 +730,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
|
||||
|
@ -741,8 +778,10 @@ 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)) {
|
||||
resp = {type: "ok"}
|
||||
|
@ -769,6 +808,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
|
||||
|
@ -1166,7 +1209,7 @@ const processCommand = (function () {
|
|||
}
|
||||
await getLocalMediaStream(
|
||||
inactiveCallMediaSources.mic && localStream.getAudioTracks().length == 0,
|
||||
inactiveCallMediaSources.camera && (localStream.getVideoTracks().length == 0 || oldCamera != newCamera),
|
||||
inactiveCallMediaSources.camera && (localStream.getVideoTracks().length == 0 || oldCamera != newCamera) && (await browserHasCamera()),
|
||||
newCamera
|
||||
)
|
||||
.then((stream) => {
|
||||
|
@ -1358,6 +1401,18 @@ const processCommand = (function () {
|
|||
return navigator.mediaDevices.getDisplayMedia(constraints)
|
||||
}
|
||||
|
||||
async function browserHasCamera(): Promise<boolean> {
|
||||
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: boolean, camera: boolean, facingMode: VideoCamera): MediaStreamConstraints {
|
||||
return {
|
||||
audio: mic,
|
||||
|
@ -1590,6 +1645,19 @@ function desktopShowPermissionsAlert(mediaType: CallMediaType) {
|
|||
}
|
||||
}
|
||||
|
||||
function permissionDescription(command: WCPermission): string {
|
||||
if ((window as any).safari) {
|
||||
return command.safari
|
||||
} else if (
|
||||
(navigator.userAgent.includes("Chrome") && navigator.vendor.includes("Google Inc")) ||
|
||||
navigator.userAgent.includes("Firefox")
|
||||
) {
|
||||
return command.chrome
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type TransformFrameFunc = (key: CryptoKey) => (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void>
|
||||
|
||||
interface CallCrypto {
|
||||
|
|
|
@ -50,6 +50,12 @@
|
|||
<div id="audio-call-icon">
|
||||
<img src="/desktop/images/ic_phone_in_talk.svg" />
|
||||
</div>
|
||||
|
||||
<div id="permission-denied">
|
||||
<p id="permission-denied-title"></p>
|
||||
<p id="permission-denied-desc"></p>
|
||||
</div>
|
||||
|
||||
<p id="manage-call">
|
||||
<button id="toggle-screen" onclick="javascript:toggleScreenManually()">
|
||||
<img src="/desktop/images/ic_screen_share.svg" />
|
||||
|
|
|
@ -3,6 +3,7 @@ body {
|
|||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: black;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
#remote-video-stream.inline {
|
||||
|
@ -157,6 +158,23 @@ body {
|
|||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
#permission-denied {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
align-content: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#permission-denied-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#permission-denied-desc {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
|
@ -180,7 +198,6 @@ body {
|
|||
color: white;
|
||||
opacity: 0.8;
|
||||
width: 200px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
#info-block.audio {
|
||||
|
|
|
@ -91,6 +91,19 @@ inactiveCallMediaSourcesChanged = (inactiveCallMediaSources: CallMediaSources) =
|
|||
// document.getElementById("media-sources")!.innerText = inactiveCallMediaSourcesStatus(inactiveCallMediaSources)
|
||||
}
|
||||
|
||||
failedToGetPermissions = (title: string, description: string) => {
|
||||
document.getElementById("info-block")!.style.visibility = "hidden"
|
||||
document.getElementById("progress")!.style.visibility = "hidden"
|
||||
|
||||
document.getElementById("permission-denied-title")!.innerText = title
|
||||
document.getElementById("permission-denied-desc")!.innerText = description
|
||||
|
||||
document.getElementById("toggle-mic")!.style.visibility = "hidden"
|
||||
document.getElementById("toggle-camera")!.style.visibility = "hidden"
|
||||
document.getElementById("toggle-screen")!.style.visibility = "hidden"
|
||||
document.getElementById("toggle-speaker")!.style.visibility = "hidden"
|
||||
}
|
||||
|
||||
function enableMicIcon(enabled: boolean) {
|
||||
document.getElementById("toggle-mic")!.innerHTML = enabled
|
||||
? '<img src="/desktop/images/ic_mic.svg" />'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue