Understanding the Android 14 WebRTC Picture-in-Picture (PiP) Bug

Developers implementing video calls in React Native often encounter subtle, platform-specific bugs during operating system upgrades. A particularly frustrating issue occurs on Android 14 (API level 34): when entering Picture-in-Picture (PiP) mode during a WebRTC call, tapping the PiP window causes the local (or remote) RTCVideoView to abruptly resize or stretch to full-screen dimensions inside the miniature PiP window.

This behavior works flawlessly on Android 12 and 13, but breaks on Android 14. Here is an in-depth breakdown of why this happens and how to resolve it permanently.

Why Android 14 Triggers the Fullscreen Layout Glitch

The root cause lies in a combination of Android 14 layout lifecycle updates, React Native thread scheduling in PiP, and WebRTC's SurfaceViewRenderer calculation:

  • Android 14 Overlay & Focus Changes: In Android 14, tapping a PiP window brings up system PiP action controls and dispatches window focus changes differently. This triggers a layout invalidation pass across the view hierarchy.
  • Window Metric vs. DecorView Bounds: When the PiP window is tapped, the native Android view hierarchy triggers a layout pass. If React Native or the native WebRTC container queries WindowManager or root DecorView metrics instead of the PiP viewport bounds, the native SurfaceViewRenderer assumes standard activity dimensions.
  • JavaScript Bridge Throttling: When an app is in PiP, Android deprioritizes background threads, including the React Native JavaScript bridge. If your styling relies on JS-driven state updates (such as dimensions from Dimensions.get('window')), these events are delayed until the user taps the window, causing an out-of-sync layout recalculation.
  • requestLayout() Side Effects: Calling window.decorView.requestLayout() while in PiP forces the root decor view to remeasure its children, often using stale full-screen constraints on Android 14.

How to Fix the WebRTC PiP Resize Issue

1. Ensure Proper AndroidManifest.xml Configuration

Ensure your MainActivity has all required PiP configuration flags so that Android does not recreate the activity or dispatch erratic layout passes when transitioning into PiP or receiving user interaction:

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:supportsPictureInPicture="true"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden|uiMode"
    android:launchMode="singleTask">
</activity>

2. Avoid JS-Driven Dimensions inside the PiP Container

Never rely on Dimensions.get('window') or useWindowDimensions() inside views meant to be visible during PiP. On Android 14, these values may reflect the pre-PiP device screen width/height rather than the PiP window.

Use pure Flexbox constraints (flex: 1) with absolute fill rules:

import React from 'react';
import { StyleSheet, View } from 'react-native';
import { RTCView } from 'react-native-webrtc';

interface VideoCallProps {
  localStreamUrl?: string;
  remoteStreamUrl?: string;
  isPipMode: boolean;
}

export const VideoCallScreen: React.FC<VideoCallProps> = ({
  localStreamUrl,
  remoteStreamUrl,
  isPipMode,
}) => {
  return (
    <View style={styles.container}>
      {/* Remote Full View */}
      {remoteStreamUrl && (
        <RTCView
          streamURL={remoteStreamUrl}
          style={StyleSheet.absoluteFillObject}
          objectFit="cover"
          zOrder={0}
        />
      )}

      {/* Local Video Thumbnail / Inset View */}
      {localStreamUrl && (
        <View
          style={[
            styles.localVideoContainer,
            isPipMode && styles.localVideoPipHidden,
          ]}>
          <RTCView
            streamURL={localStreamUrl}
            style={styles.localVideo}
            objectFit="cover"
            zOrder={1}
          />
        </View>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#000000',
  },
  localVideoContainer: {
    position: 'absolute',
    bottom: 24,
    right: 24,
    width: 100,
    height: 150,
    borderRadius: 8,
    overflow: 'hidden',
    elevation: 4,
  },
  localVideoPipHidden: {
    // If you prefer to hide local view inside small PiP window
    display: 'none',
  },
  localVideo: {
    width: '100%',
    height: '100%',
  },
});

3. Handle PiP and Avoid Manual requestLayout() in Kotlin

Remove calls to window.decorView.requestLayout() inside onPictureInPictureModeChanged. Instead, let Android's native windowing system manage the surface aspect ratio via PictureInPictureParams.

package com.yourapp

import android.app.PictureInPictureParams
import android.content.res.Configuration
import android.os.Build
import android.util.Rational
import com.facebook.react.ReactActivity

class MainActivity : ReactActivity() {

    override fun onPictureInPictureModeChanged(
        isInPictureInPictureMode: Boolean,
        newConfig: Configuration
    ) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
        // Notify React Native bridge
        PipAndroidModule.pipModeChanged(isInPictureInPictureMode)
    }

    fun updatePipParams(aspectRatio: Rational = Rational(9, 16)) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val builder = PictureInPictureParams.Builder()
                .setAspectRatio(aspectRatio)

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                builder.setAutoEnterEnabled(true)
                builder.setSeamlessResizeEnabled(true)
            }
            setPictureInPictureParams(builder.build())
        }
    }
}

4. Switch from SurfaceView to TextureView if Distortion Persists

WebRTC renders via SurfaceViewRenderer by default. In complex multi-view or PiP scenarios, SurfaceView uses a separate dedicated drawing surface behind or in front of the window, which can detach from parent React Native layout bounds during Android 14 tap-to-focus interactions.

If you encounter persistent aspect ratio stretching:

  • Pass mirror={false} and ensure objectFit="cover" is set explicitly on the RTCView component.
  • If using a custom native WebRTC module, consider wrapping the video inside a TextureView instead of a SurfaceView, which participates directly in the normal Android View hierarchy layout passes.

Summary

The Android 14 PiP resizing bug in React Native WebRTC stems from deferred layout passes and decor view measurements triggered when the PiP window is focused. By keeping layouts bound to strict Flexbox constraints (avoiding JS-based pixel calculations), preventing manual decor view layout passes, and configuring seamless PiP parameters natively, your WebRTC video streams will render seamlessly across all Android versions.