diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt index 11205527d..488500434 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt @@ -82,11 +82,18 @@ class CallController( // Remote video frame monitoring — detects when peer stops sending video private val _isRemoteVideoActive = MutableStateFlow(false) val isRemoteVideoActive: StateFlow = _isRemoteVideoActive.asStateFlow() + private val _remoteVideoAspectRatio = MutableStateFlow(null) + val remoteVideoAspectRatio: StateFlow = _remoteVideoAspectRatio.asStateFlow() private val lastRemoteFrameTimeMs = AtomicLong(0L) private var remoteVideoMonitorJob: kotlinx.coroutines.Job? = null private val remoteFrameSink = - VideoSink { _: VideoFrame -> + VideoSink { frame: VideoFrame -> lastRemoteFrameTimeMs.set(System.currentTimeMillis()) + val w = frame.rotatedWidth + val h = frame.rotatedHeight + if (w > 0 && h > 0) { + _remoteVideoAspectRatio.value = w.toFloat() / h.toFloat() + } } // Audio/video toggle state (UI concerns, not domain state) @@ -358,6 +365,7 @@ class CallController( _isAudioMuted.value = false _isVideoEnabled.value = false _isRemoteVideoActive.value = false + _remoteVideoAspectRatio.value = null videoPausedByProximity = false webRtcSession?.dispose() webRtcSession = null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallActivity.kt index 03a4e78f9..c94a32329 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallActivity.kt @@ -35,6 +35,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.lifecycleScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.call.CallState import com.vitorpamplona.amethyst.ui.theme.AmethystTheme @@ -79,6 +80,7 @@ class CallActivity : AppCompatActivity() { } registerPipReceiver() + observeVideoStateForPip(callController) setContent { AmethystTheme { @@ -140,6 +142,15 @@ class CallActivity : AppCompatActivity() { super.onDestroy() } + private fun observeVideoStateForPip(controller: com.vitorpamplona.amethyst.service.call.CallController?) { + controller ?: return + lifecycleScope.launch { + controller.isRemoteVideoActive.collect { + updatePipParams() + } + } + } + private fun enterPipIfActive() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return val callManager = ActiveCallHolder.callManager ?: return @@ -168,10 +179,11 @@ class CallActivity : AppCompatActivity() { } private fun buildPipParams(): PictureInPictureParams { + val aspectRatio = computePipAspectRatio() val builder = PictureInPictureParams .Builder() - .setAspectRatio(Rational(9, 16)) + .setAspectRatio(aspectRatio) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { builder.setActions(buildPipActions()) @@ -180,6 +192,22 @@ class CallActivity : AppCompatActivity() { return builder.build() } + private fun computePipAspectRatio(): Rational { + val controller = ActiveCallHolder.callController ?: return Rational(9, 16) + val isVideoActive = controller.isRemoteVideoActive.value + val videoRatio = controller.remoteVideoAspectRatio.value + + if (isVideoActive && videoRatio != null && videoRatio > 0f) { + // Clamp to Android's allowed PiP range (roughly 1:2.39 to 2.39:1) + val clamped = videoRatio.coerceIn(0.42f, 2.39f) + val num = (clamped * 1000).toInt() + return Rational(num, 1000) + } + + // No active video — use portrait ratio + return Rational(9, 16) + } + private fun buildPipActions(): List { val actions = mutableListOf()