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
This commit is contained in:
Stanislav Dmitrenko
2024-12-07 17:09:00 +00:00
committed by GitHub
parent 7d6c7c58d7
commit 307211a47f
14 changed files with 283 additions and 137 deletions
@@ -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() {}
@@ -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()
@@ -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
@@ -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))
}
@@ -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 {
@@ -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;
@@ -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;