fix: PiP aspect ratio matches remote video when available

- Track remote video aspect ratio in CallController via VideoFrame
  rotatedWidth/rotatedHeight
- PiP uses video's aspect ratio when remote video is active, falls
  back to 9:16 portrait for audio-only calls
- Observe isRemoteVideoActive to update PiP params when video
  starts/stops during a call

https://claude.ai/code/session_01Ak5tTkujpjNG1r5ASuPipZ
This commit is contained in:
Claude
2026-04-02 20:30:36 +00:00
parent 47ce858481
commit 7024994b9a
2 changed files with 38 additions and 2 deletions
@@ -82,11 +82,18 @@ class CallController(
// Remote video frame monitoring — detects when peer stops sending video
private val _isRemoteVideoActive = MutableStateFlow(false)
val isRemoteVideoActive: StateFlow<Boolean> = _isRemoteVideoActive.asStateFlow()
private val _remoteVideoAspectRatio = MutableStateFlow<Float?>(null)
val remoteVideoAspectRatio: StateFlow<Float?> = _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
@@ -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<RemoteAction> {
val actions = mutableListOf<RemoteAction>()