mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-17 01:35:08 +00:00
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:
+2
@@ -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() {}
|
||||
|
||||
+3
@@ -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()
|
||||
|
||||
+4
-1
@@ -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
|
||||
|
||||
+3
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user