mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +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:launchMode="singleInstance"
|
||||||
android:supportsPictureInPicture="true"
|
android:supportsPictureInPicture="true"
|
||||||
android:autoRemoveFromRecents="true"
|
android:autoRemoveFromRecents="true"
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/>
|
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|
|
@ -360,6 +360,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun androidCreateActiveCallState(): Closeable = ActiveCallState()
|
||||||
|
|
||||||
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
|
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,8 +71,12 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
am.unregisterAudioDeviceCallback(audioCallback)
|
try {
|
||||||
am.removeOnCommunicationDeviceChangedListener(listener)
|
am.unregisterAudioDeviceCallback(audioCallback)
|
||||||
|
am.removeOnCommunicationDeviceChangedListener(listener)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, e.stackTraceToString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyExternal: Boolean) {
|
override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyExternal: Boolean) {
|
||||||
|
|
|
@ -6,12 +6,12 @@ import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.media.*
|
import android.media.*
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK
|
import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK
|
||||||
|
import android.os.PowerManager.WakeLock
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.webkit.*
|
import android.webkit.*
|
||||||
|
@ -23,7 +23,6 @@ import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import com.google.accompanist.permissions.*
|
import com.google.accompanist.permissions.*
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
@ -58,6 +56,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.serialization.encodeToString
|
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
|
// Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
@ -72,49 +71,62 @@ fun activeCallDestroyWebView() = withApi {
|
||||||
Log.d(TAG, "CallView: webview was destroyed")
|
Log.d(TAG, "CallView: webview was destroyed")
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SourceLockedOrientationActivity")
|
class ActiveCallState: Closeable {
|
||||||
@Composable
|
val proximityLock: WakeLock? = screenOffWakeLock()
|
||||||
actual fun ActiveCallView() {
|
var wasConnected = false
|
||||||
val call = remember { chatModel.activeCall }.value
|
val callAudioDeviceManager = CallAudioDeviceManagerInterface.new()
|
||||||
val scope = rememberCoroutineScope()
|
private var closed = false
|
||||||
val proximityLock = remember {
|
|
||||||
|
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)
|
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")
|
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock")
|
||||||
} else {
|
} else {
|
||||||
null
|
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) {
|
LaunchedEffect(call) {
|
||||||
if (call?.callState == CallState.Connected && !wasConnected.value) {
|
if (call?.callState == CallState.Connected && callState != null && !callState.wasConnected) {
|
||||||
CallSoundsPlayer.vibrate(2)
|
CallSoundsPlayer.vibrate(2)
|
||||||
wasConnected.value = true
|
callState.wasConnected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val callAudioDeviceManager = remember { CallAudioDeviceManagerInterface.new() }
|
LaunchedEffect(callState, chatModel.activeCallViewIsCollapsed.value) {
|
||||||
DisposableEffect(Unit) {
|
callState ?: return@LaunchedEffect
|
||||||
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) {
|
|
||||||
if (chatModel.activeCallViewIsCollapsed.value) {
|
if (chatModel.activeCallViewIsCollapsed.value) {
|
||||||
if (proximityLock?.isHeld == true) proximityLock.release()
|
if (callState.proximityLock?.isHeld == true) callState.proximityLock.release()
|
||||||
} else {
|
} else {
|
||||||
delay(1000)
|
delay(1000)
|
||||||
if (proximityLock?.isHeld == false) proximityLock.acquire()
|
if (callState.proximityLock?.isHeld == false) callState.proximityLock.acquire()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
@ -122,6 +134,7 @@ actual fun ActiveCallView() {
|
||||||
Log.d(TAG, "received from WebRTCView: $apiMsg")
|
Log.d(TAG, "received from WebRTCView: $apiMsg")
|
||||||
val call = chatModel.activeCall.value
|
val call = chatModel.activeCall.value
|
||||||
if (call != null) {
|
if (call != null) {
|
||||||
|
val callState = call.androidCallState as ActiveCallState
|
||||||
Log.d(TAG, "has active call $call")
|
Log.d(TAG, "has active call $call")
|
||||||
val callRh = call.remoteHostId
|
val callRh = call.remoteHostId
|
||||||
when (val r = apiMsg.resp) {
|
when (val r = apiMsg.resp) {
|
||||||
|
@ -131,9 +144,9 @@ actual fun ActiveCallView() {
|
||||||
updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) }
|
updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) }
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
// Starting is delayed to make Android <= 11 working good with Bluetooth
|
// Starting is delayed to make Android <= 11 working good with Bluetooth
|
||||||
callAudioDeviceManager.start()
|
callState.callAudioDeviceManager.start()
|
||||||
} else {
|
} else {
|
||||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
|
callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
|
||||||
}
|
}
|
||||||
CallSoundsPlayer.startConnectingCallSound(scope)
|
CallSoundsPlayer.startConnectingCallSound(scope)
|
||||||
activeCallWaitDeliveryReceipt(scope)
|
activeCallWaitDeliveryReceipt(scope)
|
||||||
|
@ -143,9 +156,9 @@ actual fun ActiveCallView() {
|
||||||
updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) }
|
updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) }
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
// Starting is delayed to make Android <= 11 working good with Bluetooth
|
// Starting is delayed to make Android <= 11 working good with Bluetooth
|
||||||
callAudioDeviceManager.start()
|
callState.callAudioDeviceManager.start()
|
||||||
} else {
|
} else {
|
||||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
|
callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is WCallResponse.Answer -> withBGApi {
|
is WCallResponse.Answer -> withBGApi {
|
||||||
|
@ -228,14 +241,14 @@ actual fun ActiveCallView() {
|
||||||
!chatModel.activeCallViewIsCollapsed.value -> true
|
!chatModel.activeCallViewIsCollapsed.value -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
if (call != null && showOverlay) {
|
if (call != null && showOverlay && callState != null) {
|
||||||
ActiveCallOverlay(call, chatModel, callAudioDeviceManager)
|
ActiveCallOverlay(call, chatModel, callState.callAudioDeviceManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyChangeEffect(call?.localMediaSources?.hasVideo) {
|
KeyChangeEffect(callState, call?.localMediaSources?.hasVideo) {
|
||||||
if (call != null && call.hasVideo && callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
|
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
|
// 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
|
val context = LocalContext.current
|
||||||
|
@ -243,16 +256,12 @@ actual fun ActiveCallView() {
|
||||||
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
|
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
|
||||||
val prevVolumeControlStream = activity.volumeControlStream
|
val prevVolumeControlStream = activity.volumeControlStream
|
||||||
activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL
|
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
|
chatModel.activeCallViewIsVisible.value = true
|
||||||
// After the first call, End command gets added to the list which prevents making another calls
|
// After the first call, End command gets added to the list which prevents making another calls
|
||||||
chatModel.callCommand.removeAll { it is WCallCommand.End }
|
chatModel.callCommand.removeAll { it is WCallCommand.End }
|
||||||
keepScreenOn(true)
|
keepScreenOn(true)
|
||||||
onDispose {
|
onDispose {
|
||||||
activity.volumeControlStream = prevVolumeControlStream
|
activity.volumeControlStream = prevVolumeControlStream
|
||||||
// Unlock orientation
|
|
||||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
chatModel.activeCallViewIsVisible.value = false
|
chatModel.activeCallViewIsVisible.value = false
|
||||||
chatModel.callCommand.clear()
|
chatModel.callCommand.clear()
|
||||||
keepScreenOn(false)
|
keepScreenOn(false)
|
||||||
|
@ -264,8 +273,8 @@ actual fun ActiveCallView() {
|
||||||
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceManager: CallAudioDeviceManagerInterface) {
|
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceManager: CallAudioDeviceManagerInterface) {
|
||||||
ActiveCallOverlayLayout(
|
ActiveCallOverlayLayout(
|
||||||
call = call,
|
call = call,
|
||||||
devices = remember { callAudioDeviceManager.devices }.value,
|
devices = remember(callAudioDeviceManager) { callAudioDeviceManager.devices }.value,
|
||||||
currentDevice = remember { callAudioDeviceManager.currentDevice },
|
currentDevice = remember(callAudioDeviceManager) { callAudioDeviceManager.currentDevice },
|
||||||
dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
|
dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
|
||||||
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) },
|
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) },
|
||||||
selectDevice = { callAudioDeviceManager.selectDevice(it.id) },
|
selectDevice = { callAudioDeviceManager.selectDevice(it.id) },
|
||||||
|
@ -832,7 +841,8 @@ fun PreviewActiveCallOverlayVideo() {
|
||||||
connectionInfo = ConnectionInfo(
|
connectionInfo = ConnectionInfo(
|
||||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
|
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
|
||||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp")
|
RTCIceCandidate(RTCIceCandidateType.Host, "tcp")
|
||||||
)
|
),
|
||||||
|
androidCallState = {}
|
||||||
),
|
),
|
||||||
devices = emptyList(),
|
devices = emptyList(),
|
||||||
currentDevice = remember { mutableStateOf(null) },
|
currentDevice = remember { mutableStateOf(null) },
|
||||||
|
@ -841,7 +851,7 @@ fun PreviewActiveCallOverlayVideo() {
|
||||||
selectDevice = {},
|
selectDevice = {},
|
||||||
toggleVideo = {},
|
toggleVideo = {},
|
||||||
toggleSound = {},
|
toggleSound = {},
|
||||||
flipCamera = {}
|
flipCamera = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -862,7 +872,8 @@ fun PreviewActiveCallOverlayAudio() {
|
||||||
connectionInfo = ConnectionInfo(
|
connectionInfo = ConnectionInfo(
|
||||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
|
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
|
||||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp")
|
RTCIceCandidate(RTCIceCandidateType.Host, "udp")
|
||||||
)
|
),
|
||||||
|
androidCallState = {}
|
||||||
),
|
),
|
||||||
devices = emptyList(),
|
devices = emptyList(),
|
||||||
currentDevice = remember { mutableStateOf(null) },
|
currentDevice = remember { mutableStateOf(null) },
|
||||||
|
|
|
@ -10,6 +10,7 @@ import chat.simplex.common.model.ChatId
|
||||||
import chat.simplex.common.model.NotificationsMode
|
import chat.simplex.common.model.NotificationsMode
|
||||||
import chat.simplex.common.ui.theme.CurrentColors
|
import chat.simplex.common.ui.theme.CurrentColors
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import java.io.Closeable
|
||||||
|
|
||||||
interface PlatformInterface {
|
interface PlatformInterface {
|
||||||
suspend fun androidServiceStart() {}
|
suspend fun androidServiceStart() {}
|
||||||
|
@ -26,6 +27,7 @@ interface PlatformInterface {
|
||||||
fun androidPictureInPictureAllowed(): Boolean = true
|
fun androidPictureInPictureAllowed(): Boolean = true
|
||||||
fun androidCallEnded() {}
|
fun androidCallEnded() {}
|
||||||
fun androidRestartNetworkObserver() {}
|
fun androidRestartNetworkObserver() {}
|
||||||
|
fun androidCreateActiveCallState(): Closeable = Closeable { }
|
||||||
fun androidIsXiaomiDevice(): Boolean = false
|
fun androidIsXiaomiDevice(): Boolean = false
|
||||||
val androidApiLevel: Int? get() = null
|
val androidApiLevel: Int? get() = null
|
||||||
@Composable fun androidLockPortraitOrientation() {}
|
@Composable fun androidLockPortraitOrientation() {}
|
||||||
|
|
|
@ -43,6 +43,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||||
|
|
||||||
private fun justAcceptIncomingCall(invitation: RcvCallInvitation, userProfile: Profile) {
|
private fun justAcceptIncomingCall(invitation: RcvCallInvitation, userProfile: Profile) {
|
||||||
with (chatModel) {
|
with (chatModel) {
|
||||||
|
activeCall.value?.androidCallState?.close()
|
||||||
activeCall.value = Call(
|
activeCall.value = Call(
|
||||||
remoteHostId = invitation.remoteHostId,
|
remoteHostId = invitation.remoteHostId,
|
||||||
userProfile = userProfile,
|
userProfile = userProfile,
|
||||||
|
@ -51,6 +52,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||||
callState = CallState.InvitationAccepted,
|
callState = CallState.InvitationAccepted,
|
||||||
initialCallType = invitation.callType.media,
|
initialCallType = invitation.callType.media,
|
||||||
sharedKey = invitation.sharedKey,
|
sharedKey = invitation.sharedKey,
|
||||||
|
androidCallState = platform.androidCreateActiveCallState()
|
||||||
)
|
)
|
||||||
showCallView.value = true
|
showCallView.value = true
|
||||||
val useRelay = controller.appPrefs.webrtcPolicyRelay.get()
|
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
|
// Don't destroy WebView if you plan to accept next call right after this one
|
||||||
if (!switchingCall.value) {
|
if (!switchingCall.value) {
|
||||||
showCallView.value = false
|
showCallView.value = false
|
||||||
|
activeCall.value?.androidCallState?.close()
|
||||||
activeCall.value = null
|
activeCall.value = null
|
||||||
activeCallViewIsCollapsed.value = false
|
activeCallViewIsCollapsed.value = false
|
||||||
platform.androidCallEnded()
|
platform.androidCallEnded()
|
||||||
|
|
|
@ -7,6 +7,7 @@ import chat.simplex.res.MR
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.io.Closeable
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.collections.ArrayList
|
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,
|
// 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
|
// 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
|
val encrypted: Boolean get() = localEncrypted && sharedKey != null
|
||||||
private val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
|
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.*
|
||||||
import chat.simplex.common.model.CIDirection.GroupRcv
|
import chat.simplex.common.model.CIDirection.GroupRcv
|
||||||
import chat.simplex.common.model.ChatController.appPrefs
|
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.controller
|
||||||
import chat.simplex.common.model.ChatModel.withChats
|
import chat.simplex.common.model.ChatModel.withChats
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
|
@ -573,7 +574,8 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
|
||||||
if (chatInfo is ChatInfo.Direct) {
|
if (chatInfo is ChatInfo.Direct) {
|
||||||
val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId)
|
val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId)
|
||||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
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.showCallView.value = true
|
||||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,26 +12,60 @@ body {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
#remote-video-stream.collapsed {
|
@media (orientation: portrait) {
|
||||||
position: absolute;
|
#remote-video-stream.collapsed {
|
||||||
max-width: 30%;
|
position: absolute;
|
||||||
max-height: 30%;
|
width: 30%;
|
||||||
object-fit: cover;
|
max-width: 30%;
|
||||||
margin: 16px;
|
height: 39.9vw;
|
||||||
border-radius: 16px;
|
object-fit: cover;
|
||||||
bottom: 80px;
|
margin: 16px;
|
||||||
right: 0;
|
border-radius: 16px;
|
||||||
|
bottom: 80px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#remote-video-stream.collapsed-pip {
|
@media (orientation: landscape) {
|
||||||
position: absolute;
|
#remote-video-stream.collapsed {
|
||||||
max-width: 50%;
|
position: absolute;
|
||||||
max-height: 50%;
|
width: 20%;
|
||||||
object-fit: cover;
|
max-width: 20%;
|
||||||
margin: 8px;
|
height: 15.03vw;
|
||||||
border-radius: 8px;
|
object-fit: cover;
|
||||||
bottom: 0;
|
margin: 16px;
|
||||||
right: 0;
|
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 {
|
#remote-screen-video-stream.inline {
|
||||||
|
@ -41,15 +75,32 @@ body {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
#local-video-stream.inline {
|
@media (orientation: portrait) {
|
||||||
position: absolute;
|
#local-video-stream.inline {
|
||||||
width: 30%;
|
position: absolute;
|
||||||
max-width: 30%;
|
width: 30%;
|
||||||
object-fit: cover;
|
max-width: 30%;
|
||||||
margin: 16px;
|
height: 39.9vw;
|
||||||
border-radius: 16px;
|
object-fit: cover;
|
||||||
top: 0;
|
margin: 16px;
|
||||||
right: 0;
|
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 {
|
#local-screen-video-stream.inline {
|
||||||
|
|
|
@ -301,6 +301,7 @@ const processCommand = (function () {
|
||||||
localStream = await getLocalMediaStream(true, command.media == CallMediaType.Video && (await browserHasCamera()), VideoCamera.User);
|
localStream = await getLocalMediaStream(true, command.media == CallMediaType.Video && (await browserHasCamera()), VideoCamera.User);
|
||||||
const videos = getVideoElements();
|
const videos = getVideoElements();
|
||||||
if (videos) {
|
if (videos) {
|
||||||
|
setupLocalVideoRatio(videos.local);
|
||||||
videos.local.srcObject = localStream;
|
videos.local.srcObject = localStream;
|
||||||
videos.local.play().catch((e) => console.log(e));
|
videos.local.play().catch((e) => console.log(e));
|
||||||
}
|
}
|
||||||
|
@ -330,9 +331,12 @@ const processCommand = (function () {
|
||||||
console.log("starting incoming call - create webrtc session");
|
console.log("starting incoming call - create webrtc session");
|
||||||
if (activeCall)
|
if (activeCall)
|
||||||
endCall();
|
endCall();
|
||||||
|
// It can be already defined on Android when switching calls (if the previous call was outgoing)
|
||||||
|
notConnectedCall = undefined;
|
||||||
inactiveCallMediaSources.mic = true;
|
inactiveCallMediaSources.mic = true;
|
||||||
inactiveCallMediaSources.camera = command.media == CallMediaType.Video;
|
inactiveCallMediaSources.camera = command.media == CallMediaType.Video;
|
||||||
inactiveCallMediaSourcesChanged(inactiveCallMediaSources);
|
inactiveCallMediaSourcesChanged(inactiveCallMediaSources);
|
||||||
|
setupLocalVideoRatio(getVideoElements().local);
|
||||||
const { media, iceServers, relay } = command;
|
const { media, iceServers, relay } = command;
|
||||||
const encryption = supportsInsertableStreams(useWorker);
|
const encryption = supportsInsertableStreams(useWorker);
|
||||||
const aesKey = encryption ? command.aesKey : undefined;
|
const aesKey = encryption ? command.aesKey : undefined;
|
||||||
|
@ -547,13 +551,13 @@ const processCommand = (function () {
|
||||||
}
|
}
|
||||||
function endCall() {
|
function endCall() {
|
||||||
var _a;
|
var _a;
|
||||||
|
shutdownCameraAndMic();
|
||||||
try {
|
try {
|
||||||
(_a = activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection) === null || _a === void 0 ? void 0 : _a.close();
|
(_a = activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection) === null || _a === void 0 ? void 0 : _a.close();
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
shutdownCameraAndMic();
|
|
||||||
activeCall = undefined;
|
activeCall = undefined;
|
||||||
resetVideoElements();
|
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
|
// 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));
|
videos.local.play().catch((e) => console.log(e));
|
||||||
setupLocalVideoRatio(videos.local);
|
|
||||||
}
|
}
|
||||||
function setupLocalVideoRatio(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 () {
|
local.addEventListener("loadedmetadata", function () {
|
||||||
console.log("Local video videoWidth: " + local.videoWidth + "px, videoHeight: " + local.videoHeight + "px");
|
console.log("Local video videoWidth: " + local.videoWidth + "px, videoHeight: " + local.videoHeight + "px");
|
||||||
if (local.videoWidth == 0 || local.videoHeight == 0)
|
if (local.videoWidth == 0 || local.videoHeight == 0)
|
||||||
return;
|
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 () {
|
local.onresize = function () {
|
||||||
console.log("Local video size changed to " + local.videoWidth + "x" + local.videoHeight);
|
console.log("Local video size changed to " + local.videoWidth + "x" + local.videoHeight);
|
||||||
if (local.videoWidth == 0 || local.videoHeight == 0)
|
if (local.videoWidth == 0 || local.videoHeight == 0)
|
||||||
return;
|
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) {
|
function setupEncryptionForLocalStream(call) {
|
||||||
|
@ -1128,8 +1126,9 @@ const processCommand = (function () {
|
||||||
(!!useWorker && "RTCRtpScriptTransform" in window));
|
(!!useWorker && "RTCRtpScriptTransform" in window));
|
||||||
}
|
}
|
||||||
function shutdownCameraAndMic() {
|
function shutdownCameraAndMic() {
|
||||||
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localStream) {
|
if (activeCall) {
|
||||||
activeCall.localStream.getTracks().forEach((track) => track.stop());
|
activeCall.localStream.getTracks().forEach((track) => track.stop());
|
||||||
|
activeCall.localScreenStream.getTracks().forEach((track) => track.stop());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function resetVideoElements() {
|
function resetVideoElements() {
|
||||||
|
@ -1295,6 +1294,9 @@ function changeLayout(layout) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
videos.localScreen.style.visibility = localSources.screenVideo ? "visible" : "hidden";
|
videos.localScreen.style.visibility = localSources.screenVideo ? "visible" : "hidden";
|
||||||
|
if (!isDesktop && !localSources.camera) {
|
||||||
|
resetLocalVideoElementHeight(videos.local);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function getVideoElements() {
|
function getVideoElements() {
|
||||||
const local = document.getElementById("local-video-stream");
|
const local = document.getElementById("local-video-stream");
|
||||||
|
@ -1312,6 +1314,11 @@ function getVideoElements() {
|
||||||
return;
|
return;
|
||||||
return { local, localScreen, remote, remoteScreen };
|
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) {
|
function desktopShowPermissionsAlert(mediaType) {
|
||||||
if (!isDesktop)
|
if (!isDesktop)
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -15,8 +15,9 @@ body {
|
||||||
|
|
||||||
#remote-video-stream.collapsed {
|
#remote-video-stream.collapsed {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
width: 20%;
|
||||||
max-width: 20%;
|
max-width: 20%;
|
||||||
max-height: 20%;
|
height: 15.03vw;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
@ -47,6 +48,7 @@ body {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 20%;
|
width: 20%;
|
||||||
max-width: 20%;
|
max-width: 20%;
|
||||||
|
height: 15.03vw;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|
|
@ -12,26 +12,60 @@ body {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
#remote-video-stream.collapsed {
|
@media (orientation: portrait) {
|
||||||
position: absolute;
|
#remote-video-stream.collapsed {
|
||||||
max-width: 30%;
|
position: absolute;
|
||||||
max-height: 30%;
|
width: 30%;
|
||||||
object-fit: cover;
|
max-width: 30%;
|
||||||
margin: 16px;
|
height: 39.9vw;
|
||||||
border-radius: 16px;
|
object-fit: cover;
|
||||||
bottom: 80px;
|
margin: 16px;
|
||||||
right: 0;
|
border-radius: 16px;
|
||||||
|
bottom: 80px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#remote-video-stream.collapsed-pip {
|
@media (orientation: landscape) {
|
||||||
position: absolute;
|
#remote-video-stream.collapsed {
|
||||||
max-width: 50%;
|
position: absolute;
|
||||||
max-height: 50%;
|
width: 20%;
|
||||||
object-fit: cover;
|
max-width: 20%;
|
||||||
margin: 8px;
|
height: 15.03vw;
|
||||||
border-radius: 8px;
|
object-fit: cover;
|
||||||
bottom: 0;
|
margin: 16px;
|
||||||
right: 0;
|
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 {
|
#remote-screen-video-stream.inline {
|
||||||
|
@ -41,15 +75,32 @@ body {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
#local-video-stream.inline {
|
@media (orientation: portrait) {
|
||||||
position: absolute;
|
#local-video-stream.inline {
|
||||||
width: 30%;
|
position: absolute;
|
||||||
max-width: 30%;
|
width: 30%;
|
||||||
object-fit: cover;
|
max-width: 30%;
|
||||||
margin: 16px;
|
height: 39.9vw;
|
||||||
border-radius: 16px;
|
object-fit: cover;
|
||||||
top: 0;
|
margin: 16px;
|
||||||
right: 0;
|
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 {
|
#local-screen-video-stream.inline {
|
||||||
|
|
|
@ -593,6 +593,7 @@ const processCommand = (function () {
|
||||||
)
|
)
|
||||||
const videos = getVideoElements()
|
const videos = getVideoElements()
|
||||||
if (videos) {
|
if (videos) {
|
||||||
|
setupLocalVideoRatio(videos.local)
|
||||||
videos.local.srcObject = localStream
|
videos.local.srcObject = localStream
|
||||||
videos.local.play().catch((e) => console.log(e))
|
videos.local.play().catch((e) => console.log(e))
|
||||||
}
|
}
|
||||||
|
@ -621,9 +622,12 @@ const processCommand = (function () {
|
||||||
console.log("starting incoming call - create webrtc session")
|
console.log("starting incoming call - create webrtc session")
|
||||||
if (activeCall) endCall()
|
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.mic = true
|
||||||
inactiveCallMediaSources.camera = command.media == CallMediaType.Video
|
inactiveCallMediaSources.camera = command.media == CallMediaType.Video
|
||||||
inactiveCallMediaSourcesChanged(inactiveCallMediaSources)
|
inactiveCallMediaSourcesChanged(inactiveCallMediaSources)
|
||||||
|
setupLocalVideoRatio(getVideoElements()!.local)
|
||||||
|
|
||||||
const {media, iceServers, relay} = command
|
const {media, iceServers, relay} = command
|
||||||
const encryption = supportsInsertableStreams(useWorker)
|
const encryption = supportsInsertableStreams(useWorker)
|
||||||
|
@ -827,12 +831,12 @@ const processCommand = (function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function endCall() {
|
function endCall() {
|
||||||
|
shutdownCameraAndMic()
|
||||||
try {
|
try {
|
||||||
activeCall?.connection?.close()
|
activeCall?.connection?.close()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
}
|
}
|
||||||
shutdownCameraAndMic()
|
|
||||||
activeCall = undefined
|
activeCall = undefined
|
||||||
resetVideoElements()
|
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
|
// 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))
|
videos.local.play().catch((e) => console.log(e))
|
||||||
setupLocalVideoRatio(videos.local)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupLocalVideoRatio(local: HTMLVideoElement) {
|
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 () {
|
local.addEventListener("loadedmetadata", function () {
|
||||||
console.log("Local video videoWidth: " + local.videoWidth + "px, videoHeight: " + local.videoHeight + "px")
|
console.log("Local video videoWidth: " + local.videoWidth + "px, videoHeight: " + local.videoHeight + "px")
|
||||||
if (local.videoWidth == 0 || local.videoHeight == 0) return
|
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 () {
|
local.onresize = function () {
|
||||||
console.log("Local video size changed to " + local.videoWidth + "x" + local.videoHeight)
|
console.log("Local video size changed to " + local.videoWidth + "x" + local.videoHeight)
|
||||||
if (local.videoWidth == 0 || local.videoHeight == 0) return
|
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() {
|
function shutdownCameraAndMic() {
|
||||||
if (activeCall?.localStream) {
|
if (activeCall) {
|
||||||
activeCall.localStream.getTracks().forEach((track) => track.stop())
|
activeCall.localStream.getTracks().forEach((track) => track.stop())
|
||||||
|
activeCall.localScreenStream.getTracks().forEach((track) => track.stop())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1614,6 +1612,9 @@ function changeLayout(layout: LayoutType) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
videos.localScreen.style.visibility = localSources.screenVideo ? "visible" : "hidden"
|
videos.localScreen.style.visibility = localSources.screenVideo ? "visible" : "hidden"
|
||||||
|
if (!isDesktop && !localSources.camera) {
|
||||||
|
resetLocalVideoElementHeight(videos.local)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVideoElements(): VideoElements | undefined {
|
function getVideoElements(): VideoElements | undefined {
|
||||||
|
@ -1637,6 +1638,12 @@ function getVideoElements(): VideoElements | undefined {
|
||||||
return {local, localScreen, remote, remoteScreen}
|
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) {
|
function desktopShowPermissionsAlert(mediaType: CallMediaType) {
|
||||||
if (!isDesktop) return
|
if (!isDesktop) return
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,9 @@ body {
|
||||||
|
|
||||||
#remote-video-stream.collapsed {
|
#remote-video-stream.collapsed {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
width: 20%;
|
||||||
max-width: 20%;
|
max-width: 20%;
|
||||||
max-height: 20%;
|
height: 15.03vw;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
@ -47,6 +48,7 @@ body {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 20%;
|
width: 20%;
|
||||||
max-width: 20%;
|
max-width: 20%;
|
||||||
|
height: 15.03vw;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue