React Native fullscreen double audio: pause the hidden player
Fullscreen opened and the live commentary played twice. The overlay looked like an expanded version of the inline stream, but its HLS branch mounted a separate Video instance. The original player remained underneath it.
Covering a player changes what the viewer sees. Playback follows the player's own state. When fullscreen takes playback, pause the hidden player.
Count the playback instances
The project has an inline player on the detail screen, a floating mini player, and a fullscreen overlay above the navigator. For HLS, each active Video instance can own a separate playback path. I start by inspecting which instances remain mounted and their paused values when fullscreen opens.
The original inline condition paused for a visible mini player. It also paused for fullscreen on Android, with an exception for Agora. That platform condition left the inline HLS player active under the custom iOS fullscreen overlay.
Pause inline HLS on both platforms
The source repair removes the Android-only restriction:
const embeddedPaused =
isPopupVisible ||
(isFullscreenVisible && streamType !== 'agora');
// Inside the existing inline HLS Video:
paused={embeddedPaused || hlsUserPaused}
These are source-derived integration excerpts. isPopupVisible and isFullscreenVisible come from the app's shared live-stream state. hlsUserPaused belongs to the inline custom controls. The second condition explicitly transfers playback away from the inline player while fullscreen owns a non-Agora stream.
The deliberate user pause remains separate. An ownership transition should not silently clear a pause that the user chose.
The fullscreen player has its own visibility and pause state:
// Inside the existing fullscreen HLS Video:
paused={!visible || hlsPaused}
visible is the fullscreen intent. The overlay can remain mounted briefly for its exit animation, while this expression pauses its player as soon as visibility becomes false. View lifetime and playback intent serve different purposes.
The mini player renders a poster
Stopping the inline player leaves another surface to audit. In the repair, the mini player renders an image while fullscreen is visible:
if (!isPlaying || isFullscreenVisible) {
return (
<FastImage
source={{uri: activeStream.image}}
style={styles.poster}
/>
);
}
FastImage is the project's image component. This shortened excerpt belongs inside its existing mini-player renderer. The image preserves a visible placeholder without running another HLS Video there. After fullscreen closes, the mini player follows the existing playback state again.
For a new HLS integration I would make the owner explicit, for example inline | mini | fullscreen, and derive each player's pause state from that owner plus shared user intent. The accompanying teaching component, PlaybackOwnership.tsx, demonstrates that policy with controlled props. It leaves navigation, native audio configuration, reconnect handling and custom controls to the host application.
Why the Agora path differs
The Agora renderers in this project share one engine. Keeping inline and fullscreen rendering surfaces alive during a transition does not create a second independent audio engine in that architecture. The inline condition therefore excludes Agora from the fullscreen ownership pause.
A pair of mounted views is a starting point for investigation. Confirm whether those views create separate players or attach to the same engine before changing playback policy.
The rotation follow-up has a broader policy
The same repair records HLS opening paused during the iOS orientation transition. Its overlay enables these props:
ignoreSilentSwitch="ignore"
playInBackground={true}
playWhenInactive={Platform.OS === 'ios'}
These control background and inactive playback, so their effect extends beyond rotation. Enable them only when that product behavior is intended. The version-six props documentation describes their behavior and the required iOS Background Audio configuration. The overlay already sets ignoreSilentSwitch to ignore.
Test real backgrounding and interruption behavior too. Do not treat these props as a universal fullscreen fix.
Apply the orientation lock to its window
The native change uses the project's existing AppDelegate orientation hook to distinguish the app window from other windows:
if let window = window, window !== self.window {
return .allButUpsideDown
}
return Orientation.getOrientation()
This is an excerpt from the historical hook, with Orientation supplied by the app's orientation-locker integration. It leaves the app window under its own lock while avoiding that lock for system-presented fullscreen windows. Supported orientations still depend on the app and presented controller agreeing. Current UIKit documentation deprecates this older delegate method; check the API supported by your deployment target before adapting it.
The same source change unifies HLS custom controls. The ownership condition and mini-player poster are the changes that address duplicate playback.
Verify each handoff
I reviewed the original source changes and matching library documentation; I did not reproduce the behavior on a device. The project declares React Native 0.79.6 and react-native-video ^6.10.2. Exact installed versions and operating-system builds are unverified.
- Play one HLS stream inline, then enter fullscreen on iOS and Android. Inspect each instance's active and paused state while listening for duplicate output.
- Open the mini player, then fullscreen. Confirm that the mini-player branch renders an image during fullscreen ownership.
- Pause deliberately before changing surfaces and verify the product's chosen pause policy.
- Exit with the close action, rotation and hardware back. Repeat rapid open/close transitions through the exit animation.
- Exercise backgrounding, interruptions and picture in picture when supported, then repeat separately for the shared Agora engine.
Separate Video instances may reconnect or buffer at different playback positions. Giving one instance ownership does not transfer position or guarantee continuous playback.
Previous lesson: The tvOS launch crash before React.
Comments