From 307211a47fee8fe7cdf0a7828219077615579caa Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sun, 8 Dec 2024 00:09:00 +0700 Subject: [PATCH] android, desktop: landscape calls on Android and better local camera ratio management (#5124) * android, desktop: landscape calls on Android and better local camera ratio management The main thing is that now when exiting from CallActivity while in call audio devices are not reset to default. It allows to have landscape mode enabled * styles * fix changing calls --- .../android/src/main/AndroidManifest.xml | 1 - .../main/java/chat/simplex/app/SimplexApp.kt | 2 + .../views/call/CallAudioDeviceManager.kt | 8 +- .../common/views/call/CallView.android.kt | 113 ++++++++++-------- .../chat/simplex/common/platform/Platform.kt | 2 + .../simplex/common/views/call/CallManager.kt | 3 + .../chat/simplex/common/views/call/WebRTC.kt | 5 +- .../simplex/common/views/chat/ChatView.kt | 4 +- .../resources/assets/www/android/style.css | 105 +++++++++++----- .../commonMain/resources/assets/www/call.js | 31 +++-- .../resources/assets/www/desktop/style.css | 4 +- .../simplex-chat-webrtc/src/android/style.css | 105 +++++++++++----- packages/simplex-chat-webrtc/src/call.ts | 33 +++-- .../simplex-chat-webrtc/src/desktop/style.css | 4 +- 14 files changed, 283 insertions(+), 137 deletions(-) diff --git a/apps/multiplatform/android/src/main/AndroidManifest.xml b/apps/multiplatform/android/src/main/AndroidManifest.xml index deb5d83e5f..67bc0d70c8 100644 --- a/apps/multiplatform/android/src/main/AndroidManifest.xml +++ b/apps/multiplatform/android/src/main/AndroidManifest.xml @@ -115,7 +115,6 @@ android:launchMode="singleInstance" android:supportsPictureInPicture="true" android:autoRemoveFromRecents="true" - android:screenOrientation="portrait" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/> = Build.VERSION_CODES.S) { + callAudioDeviceManager.start() + } + } + + override fun close() { + if (closed) return + closed = true + CallSoundsPlayer.stop() + if (wasConnected) { + CallSoundsPlayer.vibrate() + } + callAudioDeviceManager.stop() + dropAudioManagerOverrides() + if (proximityLock?.isHeld == true) { + proximityLock.release() + } + } + + private fun screenOffWakeLock(): WakeLock? { val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager) - if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { + return if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock") } else { null } } - val wasConnected = rememberSaveable { mutableStateOf(false) } +} + + +@SuppressLint("SourceLockedOrientationActivity") +@Composable +actual fun ActiveCallView() { + val call = remember { chatModel.activeCall }.value + val callState = call?.androidCallState as ActiveCallState? + val scope = rememberCoroutineScope() LaunchedEffect(call) { - if (call?.callState == CallState.Connected && !wasConnected.value) { + if (call?.callState == CallState.Connected && callState != null && !callState.wasConnected) { CallSoundsPlayer.vibrate(2) - wasConnected.value = true + callState.wasConnected = true } } - val callAudioDeviceManager = remember { CallAudioDeviceManagerInterface.new() } - DisposableEffect(Unit) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - callAudioDeviceManager.start() - } - onDispose { - CallSoundsPlayer.stop() - if (wasConnected.value) { - CallSoundsPlayer.vibrate() - } - callAudioDeviceManager.stop() - dropAudioManagerOverrides() - if (proximityLock?.isHeld == true) { - proximityLock.release() - } - } - } - LaunchedEffect(chatModel.activeCallViewIsCollapsed.value) { + LaunchedEffect(callState, chatModel.activeCallViewIsCollapsed.value) { + callState ?: return@LaunchedEffect if (chatModel.activeCallViewIsCollapsed.value) { - if (proximityLock?.isHeld == true) proximityLock.release() + if (callState.proximityLock?.isHeld == true) callState.proximityLock.release() } else { delay(1000) - if (proximityLock?.isHeld == false) proximityLock.acquire() + if (callState.proximityLock?.isHeld == false) callState.proximityLock.acquire() } } Box(Modifier.fillMaxSize()) { @@ -122,6 +134,7 @@ actual fun ActiveCallView() { Log.d(TAG, "received from WebRTCView: $apiMsg") val call = chatModel.activeCall.value if (call != null) { + val callState = call.androidCallState as ActiveCallState Log.d(TAG, "has active call $call") val callRh = call.remoteHostId when (val r = apiMsg.resp) { @@ -131,9 +144,9 @@ actual fun ActiveCallView() { updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // Starting is delayed to make Android <= 11 working good with Bluetooth - callAudioDeviceManager.start() + callState.callAudioDeviceManager.start() } else { - callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) + callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) } CallSoundsPlayer.startConnectingCallSound(scope) activeCallWaitDeliveryReceipt(scope) @@ -143,9 +156,9 @@ actual fun ActiveCallView() { updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // Starting is delayed to make Android <= 11 working good with Bluetooth - callAudioDeviceManager.start() + callState.callAudioDeviceManager.start() } else { - callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) + callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) } } is WCallResponse.Answer -> withBGApi { @@ -228,14 +241,14 @@ actual fun ActiveCallView() { !chatModel.activeCallViewIsCollapsed.value -> true else -> false } - if (call != null && showOverlay) { - ActiveCallOverlay(call, chatModel, callAudioDeviceManager) + if (call != null && showOverlay && callState != null) { + ActiveCallOverlay(call, chatModel, callState.callAudioDeviceManager) } } - KeyChangeEffect(call?.localMediaSources?.hasVideo) { - if (call != null && call.hasVideo && callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { + KeyChangeEffect(callState, call?.localMediaSources?.hasVideo) { + if (call != null && call.hasVideo && callState != null && callState.callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { // enabling speaker on user action (peer action ignored) and not disabling it again - callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) + callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) } } val context = LocalContext.current @@ -243,16 +256,12 @@ actual fun ActiveCallView() { val activity = context as? Activity ?: return@DisposableEffect onDispose {} val prevVolumeControlStream = activity.volumeControlStream activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL - // Lock orientation to portrait in order to have good experience with calls - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT chatModel.activeCallViewIsVisible.value = true // After the first call, End command gets added to the list which prevents making another calls chatModel.callCommand.removeAll { it is WCallCommand.End } keepScreenOn(true) onDispose { activity.volumeControlStream = prevVolumeControlStream - // Unlock orientation - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED chatModel.activeCallViewIsVisible.value = false chatModel.callCommand.clear() keepScreenOn(false) @@ -264,8 +273,8 @@ actual fun ActiveCallView() { private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceManager: CallAudioDeviceManagerInterface) { ActiveCallOverlayLayout( call = call, - devices = remember { callAudioDeviceManager.devices }.value, - currentDevice = remember { callAudioDeviceManager.currentDevice }, + devices = remember(callAudioDeviceManager) { callAudioDeviceManager.devices }.value, + currentDevice = remember(callAudioDeviceManager) { callAudioDeviceManager.currentDevice }, dismiss = { withBGApi { chatModel.callManager.endCall(call) } }, toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) }, selectDevice = { callAudioDeviceManager.selectDevice(it.id) }, @@ -832,7 +841,8 @@ fun PreviewActiveCallOverlayVideo() { connectionInfo = ConnectionInfo( RTCIceCandidate(RTCIceCandidateType.Host, "tcp"), RTCIceCandidate(RTCIceCandidateType.Host, "tcp") - ) + ), + androidCallState = {} ), devices = emptyList(), currentDevice = remember { mutableStateOf(null) }, @@ -841,7 +851,7 @@ fun PreviewActiveCallOverlayVideo() { selectDevice = {}, toggleVideo = {}, toggleSound = {}, - flipCamera = {} + flipCamera = {}, ) } } @@ -862,7 +872,8 @@ fun PreviewActiveCallOverlayAudio() { connectionInfo = ConnectionInfo( RTCIceCandidate(RTCIceCandidateType.Host, "udp"), RTCIceCandidate(RTCIceCandidateType.Host, "udp") - ) + ), + androidCallState = {} ), devices = emptyList(), currentDevice = remember { mutableStateOf(null) }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt index e0a9e22f71..448100bc17 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt @@ -10,6 +10,7 @@ import chat.simplex.common.model.ChatId import chat.simplex.common.model.NotificationsMode import chat.simplex.common.ui.theme.CurrentColors import kotlinx.coroutines.Job +import java.io.Closeable interface PlatformInterface { suspend fun androidServiceStart() {} @@ -26,6 +27,7 @@ interface PlatformInterface { fun androidPictureInPictureAllowed(): Boolean = true fun androidCallEnded() {} fun androidRestartNetworkObserver() {} + fun androidCreateActiveCallState(): Closeable = Closeable { } fun androidIsXiaomiDevice(): Boolean = false val androidApiLevel: Int? get() = null @Composable fun androidLockPortraitOrientation() {} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt index 405094f72a..d6ab57a70d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt @@ -43,6 +43,7 @@ class CallManager(val chatModel: ChatModel) { private fun justAcceptIncomingCall(invitation: RcvCallInvitation, userProfile: Profile) { with (chatModel) { + activeCall.value?.androidCallState?.close() activeCall.value = Call( remoteHostId = invitation.remoteHostId, userProfile = userProfile, @@ -51,6 +52,7 @@ class CallManager(val chatModel: ChatModel) { callState = CallState.InvitationAccepted, initialCallType = invitation.callType.media, sharedKey = invitation.sharedKey, + androidCallState = platform.androidCreateActiveCallState() ) showCallView.value = true val useRelay = controller.appPrefs.webrtcPolicyRelay.get() @@ -78,6 +80,7 @@ class CallManager(val chatModel: ChatModel) { // Don't destroy WebView if you plan to accept next call right after this one if (!switchingCall.value) { showCallView.value = false + activeCall.value?.androidCallState?.close() activeCall.value = null activeCallViewIsCollapsed.value = false platform.androidCallEnded() 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 bbf860b39c..705fc6a28f 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 @@ -7,6 +7,7 @@ import chat.simplex.res.MR import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import java.io.Closeable import java.net.URI import kotlin.collections.ArrayList @@ -27,7 +28,9 @@ data class Call( // When a user has audio call, and then he wants to enable camera but didn't grant permissions for using camera yet, // we show permissions view without enabling camera before permissions are granted. After they are granted, enabling camera - val wantsToEnableCamera: Boolean = false + val wantsToEnableCamera: Boolean = false, + + val androidCallState: Closeable ) { val encrypted: Boolean get() = localEncrypted && sharedKey != null private val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index ddf25a6e3b..913ea87c98 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.model.CIDirection.GroupRcv import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatModel.activeCall import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* @@ -573,7 +574,8 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) if (chatInfo is ChatInfo.Direct) { val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId) val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi - chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, initialCallType = media, userProfile = profile) + activeCall.value?.androidCallState?.close() + chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, initialCallType = media, userProfile = profile, androidCallState = platform.androidCreateActiveCallState()) chatModel.showCallView.value = true chatModel.callCommand.add(WCallCommand.Capabilities(media)) } diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css b/apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css index a9d1c3785a..377458c184 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css @@ -12,26 +12,60 @@ body { object-fit: cover; } -#remote-video-stream.collapsed { - position: absolute; - max-width: 30%; - max-height: 30%; - object-fit: cover; - margin: 16px; - border-radius: 16px; - bottom: 80px; - right: 0; +@media (orientation: portrait) { + #remote-video-stream.collapsed { + position: absolute; + width: 30%; + max-width: 30%; + height: 39.9vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; + } } -#remote-video-stream.collapsed-pip { - position: absolute; - max-width: 50%; - max-height: 50%; - object-fit: cover; - margin: 8px; - border-radius: 8px; - bottom: 0; - right: 0; +@media (orientation: landscape) { + #remote-video-stream.collapsed { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; + } +} + +@media (orientation: portrait) { + #remote-video-stream.collapsed-pip { + position: absolute; + width: 50%; + max-width: 50%; + height: 66.5vw; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; + } +} + +@media (orientation: landscape) { + #remote-video-stream.collapsed-pip { + position: absolute; + width: 50%; + max-width: 50%; + height: 37.59vw; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; + } } #remote-screen-video-stream.inline { @@ -41,15 +75,32 @@ body { object-fit: cover; } -#local-video-stream.inline { - position: absolute; - width: 30%; - max-width: 30%; - object-fit: cover; - margin: 16px; - border-radius: 16px; - top: 0; - right: 0; +@media (orientation: portrait) { + #local-video-stream.inline { + position: absolute; + width: 30%; + max-width: 30%; + height: 39.9vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; + } +} + +@media (orientation: landscape) { + #local-video-stream.inline { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; + } } #local-screen-video-stream.inline { 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 4dae487d03..7ab8d6fdd6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js @@ -301,6 +301,7 @@ const processCommand = (function () { localStream = await getLocalMediaStream(true, command.media == CallMediaType.Video && (await browserHasCamera()), VideoCamera.User); const videos = getVideoElements(); if (videos) { + setupLocalVideoRatio(videos.local); videos.local.srcObject = localStream; videos.local.play().catch((e) => console.log(e)); } @@ -330,9 +331,12 @@ const processCommand = (function () { console.log("starting incoming call - create webrtc session"); if (activeCall) endCall(); + // It can be already defined on Android when switching calls (if the previous call was outgoing) + notConnectedCall = undefined; inactiveCallMediaSources.mic = true; inactiveCallMediaSources.camera = command.media == CallMediaType.Video; inactiveCallMediaSourcesChanged(inactiveCallMediaSources); + setupLocalVideoRatio(getVideoElements().local); const { media, iceServers, relay } = command; const encryption = supportsInsertableStreams(useWorker); const aesKey = encryption ? command.aesKey : undefined; @@ -547,13 +551,13 @@ const processCommand = (function () { } function endCall() { var _a; + shutdownCameraAndMic(); try { (_a = activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection) === null || _a === void 0 ? void 0 : _a.close(); } catch (e) { console.log(e); } - shutdownCameraAndMic(); activeCall = undefined; resetVideoElements(); } @@ -642,27 +646,21 @@ const processCommand = (function () { } // Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture videos.local.play().catch((e) => console.log(e)); - setupLocalVideoRatio(videos.local); } function setupLocalVideoRatio(local) { - const ratio = isDesktop ? 1.33 : 1 / 1.33; - const currentRect = local.getBoundingClientRect(); - // better to get percents from here than to hardcode values from styles (the styles can be changed) - const screenWidth = currentRect.left + currentRect.width; - const percents = currentRect.width / screenWidth; - local.style.width = `${percents * 100}%`; - local.style.height = `${(percents / ratio) * 100}vw`; local.addEventListener("loadedmetadata", function () { console.log("Local video videoWidth: " + local.videoWidth + "px, videoHeight: " + local.videoHeight + "px"); if (local.videoWidth == 0 || local.videoHeight == 0) return; - local.style.height = `${(percents / (local.videoWidth / local.videoHeight)) * 100}vw`; + const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3; + local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw`; }); local.onresize = function () { console.log("Local video size changed to " + local.videoWidth + "x" + local.videoHeight); if (local.videoWidth == 0 || local.videoHeight == 0) return; - local.style.height = `${(percents / (local.videoWidth / local.videoHeight)) * 100}vw`; + const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3; + local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw`; }; } function setupEncryptionForLocalStream(call) { @@ -1128,8 +1126,9 @@ const processCommand = (function () { (!!useWorker && "RTCRtpScriptTransform" in window)); } function shutdownCameraAndMic() { - if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localStream) { + if (activeCall) { activeCall.localStream.getTracks().forEach((track) => track.stop()); + activeCall.localScreenStream.getTracks().forEach((track) => track.stop()); } } function resetVideoElements() { @@ -1295,6 +1294,9 @@ function changeLayout(layout) { break; } videos.localScreen.style.visibility = localSources.screenVideo ? "visible" : "hidden"; + if (!isDesktop && !localSources.camera) { + resetLocalVideoElementHeight(videos.local); + } } function getVideoElements() { const local = document.getElementById("local-video-stream"); @@ -1312,6 +1314,11 @@ function getVideoElements() { return; return { local, localScreen, remote, remoteScreen }; } +// Allow CSS to figure out the size of view by itself on Android because rotating to different orientation +// without dropping override will cause the view to have not normal proportion while no video is present +function resetLocalVideoElementHeight(local) { + local.style.height = ""; +} function desktopShowPermissionsAlert(mediaType) { if (!isDesktop) return; diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css index 99050bc94f..5110c7c7d6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css @@ -15,8 +15,9 @@ body { #remote-video-stream.collapsed { position: absolute; + width: 20%; max-width: 20%; - max-height: 20%; + height: 15.03vw; object-fit: cover; margin: 16px; border-radius: 16px; @@ -47,6 +48,7 @@ body { position: absolute; width: 20%; max-width: 20%; + height: 15.03vw; object-fit: cover; margin: 16px; border-radius: 16px; diff --git a/packages/simplex-chat-webrtc/src/android/style.css b/packages/simplex-chat-webrtc/src/android/style.css index a9d1c3785a..377458c184 100644 --- a/packages/simplex-chat-webrtc/src/android/style.css +++ b/packages/simplex-chat-webrtc/src/android/style.css @@ -12,26 +12,60 @@ body { object-fit: cover; } -#remote-video-stream.collapsed { - position: absolute; - max-width: 30%; - max-height: 30%; - object-fit: cover; - margin: 16px; - border-radius: 16px; - bottom: 80px; - right: 0; +@media (orientation: portrait) { + #remote-video-stream.collapsed { + position: absolute; + width: 30%; + max-width: 30%; + height: 39.9vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; + } } -#remote-video-stream.collapsed-pip { - position: absolute; - max-width: 50%; - max-height: 50%; - object-fit: cover; - margin: 8px; - border-radius: 8px; - bottom: 0; - right: 0; +@media (orientation: landscape) { + #remote-video-stream.collapsed { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; + } +} + +@media (orientation: portrait) { + #remote-video-stream.collapsed-pip { + position: absolute; + width: 50%; + max-width: 50%; + height: 66.5vw; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; + } +} + +@media (orientation: landscape) { + #remote-video-stream.collapsed-pip { + position: absolute; + width: 50%; + max-width: 50%; + height: 37.59vw; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; + } } #remote-screen-video-stream.inline { @@ -41,15 +75,32 @@ body { object-fit: cover; } -#local-video-stream.inline { - position: absolute; - width: 30%; - max-width: 30%; - object-fit: cover; - margin: 16px; - border-radius: 16px; - top: 0; - right: 0; +@media (orientation: portrait) { + #local-video-stream.inline { + position: absolute; + width: 30%; + max-width: 30%; + height: 39.9vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; + } +} + +@media (orientation: landscape) { + #local-video-stream.inline { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; + } } #local-screen-video-stream.inline { diff --git a/packages/simplex-chat-webrtc/src/call.ts b/packages/simplex-chat-webrtc/src/call.ts index 693ad6bbe5..5f3d2bf332 100644 --- a/packages/simplex-chat-webrtc/src/call.ts +++ b/packages/simplex-chat-webrtc/src/call.ts @@ -593,6 +593,7 @@ const processCommand = (function () { ) const videos = getVideoElements() if (videos) { + setupLocalVideoRatio(videos.local) videos.local.srcObject = localStream videos.local.play().catch((e) => console.log(e)) } @@ -621,9 +622,12 @@ const processCommand = (function () { console.log("starting incoming call - create webrtc session") if (activeCall) endCall() + // It can be already defined on Android when switching calls (if the previous call was outgoing) + notConnectedCall = undefined inactiveCallMediaSources.mic = true inactiveCallMediaSources.camera = command.media == CallMediaType.Video inactiveCallMediaSourcesChanged(inactiveCallMediaSources) + setupLocalVideoRatio(getVideoElements()!.local) const {media, iceServers, relay} = command const encryption = supportsInsertableStreams(useWorker) @@ -827,12 +831,12 @@ const processCommand = (function () { } function endCall() { + shutdownCameraAndMic() try { activeCall?.connection?.close() } catch (e) { console.log(e) } - shutdownCameraAndMic() activeCall = undefined resetVideoElements() } @@ -925,28 +929,21 @@ const processCommand = (function () { } // Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture videos.local.play().catch((e) => console.log(e)) - setupLocalVideoRatio(videos.local) } function setupLocalVideoRatio(local: HTMLVideoElement) { - const ratio = isDesktop ? 1.33 : 1 / 1.33 - const currentRect = local.getBoundingClientRect() - // better to get percents from here than to hardcode values from styles (the styles can be changed) - const screenWidth = currentRect.left + currentRect.width - const percents = currentRect.width / screenWidth - local.style.width = `${percents * 100}%` - local.style.height = `${(percents / ratio) * 100}vw` - local.addEventListener("loadedmetadata", function () { console.log("Local video videoWidth: " + local.videoWidth + "px, videoHeight: " + local.videoHeight + "px") if (local.videoWidth == 0 || local.videoHeight == 0) return - local.style.height = `${(percents / (local.videoWidth / local.videoHeight)) * 100}vw` + const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3 + local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw` }) local.onresize = function () { console.log("Local video size changed to " + local.videoWidth + "x" + local.videoHeight) if (local.videoWidth == 0 || local.videoHeight == 0) return - local.style.height = `${(percents / (local.videoWidth / local.videoHeight)) * 100}vw` + const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3 + local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw` } } @@ -1441,8 +1438,9 @@ const processCommand = (function () { } function shutdownCameraAndMic() { - if (activeCall?.localStream) { + if (activeCall) { activeCall.localStream.getTracks().forEach((track) => track.stop()) + activeCall.localScreenStream.getTracks().forEach((track) => track.stop()) } } @@ -1614,6 +1612,9 @@ function changeLayout(layout: LayoutType) { break } videos.localScreen.style.visibility = localSources.screenVideo ? "visible" : "hidden" + if (!isDesktop && !localSources.camera) { + resetLocalVideoElementHeight(videos.local) + } } function getVideoElements(): VideoElements | undefined { @@ -1637,6 +1638,12 @@ function getVideoElements(): VideoElements | undefined { return {local, localScreen, remote, remoteScreen} } +// Allow CSS to figure out the size of view by itself on Android because rotating to different orientation +// without dropping override will cause the view to have not normal proportion while no video is present +function resetLocalVideoElementHeight(local: HTMLVideoElement) { + local.style.height = "" +} + function desktopShowPermissionsAlert(mediaType: CallMediaType) { if (!isDesktop) return diff --git a/packages/simplex-chat-webrtc/src/desktop/style.css b/packages/simplex-chat-webrtc/src/desktop/style.css index 99050bc94f..5110c7c7d6 100644 --- a/packages/simplex-chat-webrtc/src/desktop/style.css +++ b/packages/simplex-chat-webrtc/src/desktop/style.css @@ -15,8 +15,9 @@ body { #remote-video-stream.collapsed { position: absolute; + width: 20%; max-width: 20%; - max-height: 20%; + height: 15.03vw; object-fit: cover; margin: 16px; border-radius: 16px; @@ -47,6 +48,7 @@ body { position: absolute; width: 20%; max-width: 20%; + height: 15.03vw; object-fit: cover; margin: 16px; border-radius: 16px;