mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-03-14 09:45:42 +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:
parent
7d6c7c58d7
commit
307211a47f
14 changed files with 283 additions and 137 deletions
|
@ -115,7 +115,6 @@
|
|||
android:launchMode="singleInstance"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/>
|
||||
|
||||
<provider
|
||||
|
|
|
@ -360,6 +360,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||
return true
|
||||
}
|
||||
|
||||
override fun androidCreateActiveCallState(): Closeable = ActiveCallState()
|
||||
|
||||
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,8 +71,12 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
|
|||
}
|
||||
|
||||
override fun stop() {
|
||||
am.unregisterAudioDeviceCallback(audioCallback)
|
||||
am.removeOnCommunicationDeviceChangedListener(listener)
|
||||
try {
|
||||
am.unregisterAudioDeviceCallback(audioCallback)
|
||||
am.removeOnCommunicationDeviceChangedListener(listener)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyExternal: Boolean) {
|
||||
|
|
|
@ -6,12 +6,12 @@ import android.Manifest
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.*
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK
|
||||
import android.os.PowerManager.WakeLock
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.*
|
||||
|
@ -23,7 +23,6 @@ import androidx.compose.foundation.shape.CircleShape
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -47,7 +46,6 @@ import chat.simplex.common.model.ChatController.appPrefs
|
|||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import chat.simplex.res.MR
|
||||
import com.google.accompanist.permissions.*
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
|
@ -58,6 +56,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.Closeable
|
||||
|
||||
// Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
|
@ -72,49 +71,62 @@ fun activeCallDestroyWebView() = withApi {
|
|||
Log.d(TAG, "CallView: webview was destroyed")
|
||||
}
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
@Composable
|
||||
actual fun ActiveCallView() {
|
||||
val call = remember { chatModel.activeCall }.value
|
||||
val scope = rememberCoroutineScope()
|
||||
val proximityLock = remember {
|
||||
class ActiveCallState: Closeable {
|
||||
val proximityLock: WakeLock? = screenOffWakeLock()
|
||||
var wasConnected = false
|
||||
val callAudioDeviceManager = CallAudioDeviceManagerInterface.new()
|
||||
private var closed = false
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= 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) },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue