Picture in picture

Picture-in-picture (PiP) mode displays calls in a movable small window while using other apps.

iOS

Setup

CallContent activates PiP automatically when backgrounding during active calls. Disable with disablePictureInPicture prop.

For custom components, use RTCViewPipIOS:

import { RTCViewPipIOS } from "@stream-io/video-react-native-sdk";

<>
  <RTCViewPipIOS />
  <MyComponent />
</>;

Mirroring in PiP

PiP mirroring follows the same mirror behavior as participant rendering:

  • CallContent: set mirror to force mirrored/unmirrored video in PiP.
  • RTCViewPipIOS: set mirror to force mirrored/unmirrored video when building custom PiP rendering.

For layout-level controls, see Call layouts.

<CallContent mirror={false} />
<RTCViewPipIOS mirror={false} />

If mirror is not provided, default behavior is used (local front camera mirrored, other camera tracks unmirrored). Screen share is never mirrored.

Current user camera

iOS requires multitasking camera access for background camera use. Enabled automatically with voip in UIBackgroundModes (iOS 18+) or the multitasking-camera-access entitlement.

Enabling

Enable local camera in PiP:

  • CallContent: Set iOSPiPIncludeLocalParticipantVideo to true
  • RTCViewPipIOS: Set includeLocalParticipantVideo to true

Then apply native changes:

Update AppDelegate

Add header after #import "AppDelegate.h":

#import <WebRTCModuleOptions.h>

Add to didFinishLaunchingWithOptions:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance];
  options.enableMultitaskingCameraAccess = YES;
  // the rest
}

Android

Setup

Changes to AndroidManifest

Add to MainActivity in AndroidManifest.xml:

<activity>
    ...
      android:name=".MainActivity"
      android:supportsPictureInPicture="true"
      android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
    ...
  </activity>

Changes to MainActivity

Add imports:

import android.content.res.Configuration
import com.streamvideo.reactnative.StreamVideoReactNative
import android.os.Build
import androidx.lifecycle.Lifecycle

Add functions:

fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode)
    if (isFinishing) {
      return
    }
    if (lifecycle.currentState === Lifecycle.State.CREATED) {
        // when user clicks on Close button of PIP
        finishAndRemoveTask()
    } else {
        StreamVideoReactNative.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
    }
  }

  override fun onUserLeaveHint() {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
          Build.VERSION.SDK_INT < Build.VERSION_CODES.S &&
          StreamVideoReactNative.canAutoEnterPictureInPictureMode) {
          val config = resources.configuration
          onPictureInPictureModeChanged(true,  config)
      }
  }
@Override
  public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode);
    if (isFinishing()) {
      return;
    }
    if (getLifecycle().getCurrentState() == Lifecycle.State.CREATED) {
      // when user clicks on Close button of PIP
      finishAndRemoveTask();
    } else {
      StreamVideoReactNative.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
    }
  }

  @Override
  protected void onUserLeaveHint() {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
          Build.VERSION.SDK_INT < Build.VERSION_CODES.S &&
          StreamVideoReactNative.Companion.getCanAutoEnterPictureInPictureMode()) {
          Configuration config = getResources().getConfiguration();
          onPictureInPictureModeChanged(true, config);
      }
  }

Add to app.json plugins:

{
  "plugins": [
    [
      "@stream-io/video-react-native-sdk",
      {
        "androidPictureInPicture": true
      }
    ]
  ]
}

If Expo EAS build is not used, please do npx expo prebuild --clean to generate the native directories again after adding the config plugins.

Keep call alive in the background

PiP triggers background mode on Android. Add a foreground service to keep calls alive. See Keeping Call Alive.

Entering PiP mode

CallContent activates PiP on home button press. Disable with disablePictureInPicture prop.

For custom components, use useAutoEnterPiPEffect:

import { useAutoEnterPiPEffect } from "@stream-io/video-react-native-sdk";

useAutoEnterPiPEffect();

Choosing what to render on PiP mode

PiP windows are small - render only essential content. CallContent automatically shows only the dominant speaker.

For custom rendering, use useIsInPiPMode:

import { useIsInPiPMode } from "@stream-io/video-react-native-sdk";

const isInPiPMode = useIsInPiPMode();

Use this boolean to conditionally render content during PiP mode.

When using CallContent, you can also set mirror to control whether the rendered participant video is mirrored in Android PiP:

<CallContent mirror={false} />