How Did We Discover the Expo Video PiP Discrepancy?
When companies hire app developer to create a mobile app, they often expect frameworks like React Native and Expo to provide a unified abstraction over native OS capabilities. However, native abstraction leaks are common, particularly when dealing with media playback and OS-level window management. During a recent project involving a SaaS platform’s mobile application, we encountered a fascinating architectural challenge regarding Picture-in-Picture (PiP) behavior.
While working on an interactive onboarding flow for the application (built on Expo SDK 54 and React Native 0.81.5), we designed an experience where a small PiP instructional video would remain visible while the user navigated a native system share sheet. On iOS, the startPictureInPicture() method from expo-video worked perfectly—the video popped out, and the user could interact with the share sheet within the app context. But on Android, triggering the exact same function caused the entire application to minimize, exposing the Android home screen behind it. This abruptly broke the onboarding flow and disrupted our share extension detection logic.
We realized that while the Expo documentation presented a unified API, the underlying native implementations of PiP on iOS and Android have fundamentally different semantics. This article explores why this happens, how we approached the architectural fix, and what you can learn to avoid similar cross-platform pitfalls.
What Was the Business Context Behind the PiP Feature?
The SaaS application features a sophisticated onboarding workflow designed to teach users how to utilize the app as a “share target” from other applications. The intended user experience (UX) was carefully mapped out:
- The user taps a share icon located on an onboarding link preview card.
- The native share sheet opens.
- A small PiP instructional video continues playing, guiding the user through the share flow.
- The user selects our application from the native share options.
- Our custom share extension processes the payload.
- The user returns to the primary UI, and the onboarding state advances seamlessly.
The business requirement was strict: this entire flow had to feel cohesive and keep the user contextually anchored within the application. Exiting the app or disrupting the visual hierarchy would lead to drop-offs during a critical acquisition phase.
Why Did the PiP Implementation Fail on Android?
Despite using identical JavaScript code across platforms, the Android experience derailed completely. Instead of a floating video, the sequence looked like this:
- The user tapped the share icon.
- The entire application immediately entered PiP mode.
- The user was unexpectedly dropped onto the Android home screen.
- The native share sheet was lost or buried beneath the app’s PiP window.
- When the user eventually navigated back, the state synchronization between the share extension and the primary onboarding screen frequently failed, trapping the user in a retry loop.
The root cause of this failure lies in how the underlying operating systems handle windowing, and how the expo-video library maps to those native APIs. On iOS, expo-video leverages AVPictureInPictureController. This API is strictly video-scoped. It detaches the AVPlayer layer from the app’s view hierarchy and renders it in a system-managed floating window, leaving the app’s main UI completely intact and interactive.
Android, conversely, does not have a native, video-only PiP mode that works seamlessly over the app’s own foreground UI via the standard PiP APIs. Android’s PiP API (Activity.enterPictureInPictureMode()) is Activity-scoped. Because React Native applications typically run inside a single MainActivity, instructing the system to enter PiP mode tells Android to shrink the entire application screen into a floating window. When the Activity shrinks, the OS reveals whatever task is underneath it—usually the home screen. This meant the expected native share sheet was either dismissed or attached to a minimized window, breaking the UX.
What Alternate Approaches Did We Consider?
Once we understood the architectural discrepancy, we had to rethink our strategy. We considered these solutions as well:
Can We Force Android to Act Like iOS?
Our initial thought was to dig into the native Android bridging and see if we could isolate the video surface into a secondary Activity or use Android’s WindowManager to simulate a floating window. While technically possible using the SYSTEM_ALERT_WINDOW permission, this approach is highly intrusive. It requires explicit user consent (which is a major UX friction point for onboarding) and introduces significant complexity in managing React context across multiple native entry points.
Should We Hide UI Elements During Android PiP?
Another approach we considered was subscribing to PiP lifecycle events. When Android entered PiP mode, we could dynamically hide all UI components except the video player. While this makes the minimized window look like a video-scoped PiP, it does not solve the primary business problem: the application is still minimized to the home screen, meaning the user cannot interact with the native share sheet within our app’s visual context.
Can We Build a Custom In-App Floating Video Component?
Instead of relying on OS-level PiP for Android’s foreground state, we considered building a custom React Native component using absolute positioning, react-native-reanimated, and react-native-gesture-handler. This custom view would detach the video visually and allow the user to drag it around the screen while the underlying UI (and the share sheet overlay) remained active. This bypasses the native Android PiP API entirely while the app is in the foreground.
How Did We Finally Implement the Cross-Platform Fix?
The most robust solution was to adopt a hybrid architecture. We maintained the native expo-video PiP for iOS, as it natively fulfilled the requirements. For Android, we bypassed the OS-level PiP entirely during the in-app share flow and instead implemented an In-App Floating Video mechanism.
Here is a conceptual look at how we abstracted the implementation. We wrapped the video playback logic in a custom hook and component that behaves conditionally based on the platform.
// Pseudo-code for cross-platform floating video handling
import React, { useState } from 'react';
import { Platform, View, StyleSheet } from 'react-native';
import { useVideoPlayer, VideoView } from 'expo-video';
const CrossPlatformOnboardingVideo = ({ videoSource, isShareActive }) => {
const player = useVideoPlayer(videoSource);
const [isInAppFloating, setIsInAppFloating] = useState(false);
const handleShareStart = () => {
if (Platform.OS === 'ios') {
// iOS correctly isolates the video; trigger native PiP
player.startPictureInPicture();
} else if (Platform.OS === 'android') {
// Android shrinks the whole app; use custom in-app floating state instead
setIsInAppFloating(true);
}
// Trigger native share sheet logic here...
};
const handleShareComplete = () => {
if (Platform.OS === 'ios') {
player.stopPictureInPicture();
} else {
setIsInAppFloating(false);
}
};
return (
<View style={StyleSheet.absoluteFill}>
{/* Main Onboarding UI */}
<OnboardingContent onShareTap={handleShareStart} />
{/* Conditionally style the VideoView for Android floating state */}
<View style={isInAppFloating ? styles.floatingAndroidVideo : styles.inlineVideo}>
<VideoView player={player} style={styles.videoSurface} />
</View>
</View>
);
};
const styles = StyleSheet.create({
inlineVideo: {
width: '100%',
height: 250,
},
floatingAndroidVideo: {
position: 'absolute',
bottom: 20,
right: 20,
width: 150,
height: 100,
zIndex: 999,
elevation: 10,
shadowColor: '#000',
shadowOpacity: 0.3,
},
videoSurface: {
flex: 1,
}
});
This implementation ensures that Android users keep the app fully in the foreground while interacting with the native share sheet. The video continues to play in an elevated, absolutely positioned View, achieving the visual requirement without triggering Android’s Activity-scoped PiP behavior. For teams looking to scale similar solutions, knowing when to bypass an abstraction in favor of platform-specific UX is exactly why enterprise leaders hire react native developers for cross platform apps.
What Can Engineering Teams Learn From This Mobile App Challenge?
Abstraction leaks are inevitable in cross-platform development. If you plan to hire software developer resources for your mobile team, it is critical they understand the native behaviors beneath the JavaScript bridges. Here are key takeaways from this implementation:
- Native APIs Map Differently: Just because a library exposes a unified method like startPictureInPicture() doesn’t mean the OS will interpret it the same way. Always research how the abstraction maps to underlying OS capabilities (AVPlayer vs Activity).
- Contextual PiP vs Background PiP: Understand the difference between in-app floating media and background OS-level PiP. On Android, true OS PiP is almost exclusively designed for backgrounding the app while keeping media alive.
- Test Native Integrations Concurrently: Issues like this only surface when combining two native events—in this case, PiP and a native Share Sheet. Isolated component testing will miss these UX collisions.
- Embrace Platform-Specific UX: Sometimes the best cross-platform architecture is acknowledging that the platforms aren’t identical. Building an in-app floating view for Android while using native PiP for iOS provided the best user experience.
- State Synchronization is Fragile: Relying on an app to go into a background PiP state, trigger an external extension, and resume cleanly requires meticulous state management and retry logic. Keep the app in the foreground whenever possible to simplify state.
How Can You Prevent Similar Cross-Platform Issues?
Modern mobile development requires a deep understanding of both JavaScript ecosystems and native OS behaviors. While tools like Expo significantly accelerate development, encountering an abstraction leak like the expo-video PiP discrepancy is common when pushing the boundaries of native integrations. By recognizing the difference between iOS video-scoped PiP and Android Activity-scoped PiP, we successfully engineered a hybrid solution that preserved the business onboarding flow without compromising the user experience.
Complex cross-platform challenges require experienced architects who understand when to use standard APIs and when to build custom native implementations. If your organization is looking to build resilient, enterprise-grade mobile experiences and needs to hire mobile app developers for enterprise mobility, our pre-vetted remote engineering teams are ready to deliver structured, high-quality code. Contact us to discuss your technical challenges.
Social Hashtags
#ReactNative #Expo #ExpoVideo #PictureInPicture #AndroidDevelopment #iOSDevelopment #MobileDevelopment #CrossPlatform #JavaScript #TypeScript #ReactJS #SoftwareEngineering #AppDevelopment #MobileApps #DevCommunity
Frequently Asked Questions
Android's PiP mode is designed at the Activity level. Because React Native wraps your entire application in a single MainActivity, triggering PiP shrinks the entire app window. Consequently, whatever is beneath the app (typically the home screen) becomes visible.
Not natively using the standard Activity PiP APIs within a standard single-Activity React Native app. To achieve a video-only floating effect while the rest of the app remains usable in the foreground, you must build a custom absolutely positioned React Native component.
They serve different architectural purposes. iOS handles media playback (AVPlayer) separately from the UI hierarchy, making video-scoped PiP easy. Android's model is heavily tied to window management, making it better suited for task-switching but less flexible for in-app floating media.
As of SDK 54, expo-video provides direct access to the native PiP capabilities. The behavior described is an expected result of how the underlying Android and iOS operating systems are designed, rather than an explicit bug in the Expo framework.
Because Android shrinks the Activity during PiP, any modal or native bottom sheet tied to that Activity's window context (like a share sheet) can be dismissed, visually distorted, or lost. Keeping the app in the foreground is crucial when triggering native share dialogs on Android.
Success Stories That Inspire
See how our team takes complex business challenges and turns them into powerful, scalable digital solutions. From custom software and web applications to automation, integrations, and cloud-ready systems, each project reflects our commitment to innovation, performance, and long-term value.

California-based SMB Hired Dedicated Developers to Build a Photography SaaS Platform

Swedish Agency Built a Laravel-Based Staffing System by Hiring a Dedicated Remote Team

















