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;