[Android] Fullscreen video forcibly overrides the activity's orientation, causing an infinite portrait/landscape loop on orientation-locked screens
[Android] Fullscreen video forcibly overrides the activity's orientation, causing an infinite portrait/landscape loop on orientation-locked screens
Bug description
On Android, the fullscreen video handling in RNCWebViewManagerImpl.kt hard-codes two orientation writes around the HTML5 fullscreen custom view:
onShowCustomView:activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIEDonHideCustomView:activity.requestedOrientation = initialRequestedOrientation(captured once at WebView creation)
This has two problems for apps that manage orientation themselves (e.g. a portrait-locked app that unlocks only on specific screens):
The host app's orientation policy is silently overridden with no opt-out. Entering fullscreen always unlocks rotation, even when the hosting screen deliberately locked the activity to portrait.
The exit restore forces a rotation at the worst possible moment, which can loop forever.
onHideCustomViewreapplies the creation-time orientation immediately. If the device is still physically held in landscape when fullscreen exits, this forces a rotation back to portrait. Some pages exit and re-enter HTML5 fullscreen in response to rotation/viewport changes (we hit this with a Cookiebot consent overlay mounted in the page, but any player with orientation-reactive fullscreen logic triggers it). The result is a feedback loop:enter fullscreen → UNSPECIFIED → rotates to landscape (device is tilted) → page exits fullscreen on the rotation → restore portrait → forced rotation → page re-enters fullscreen → UNSPECIFIED → rotates to landscape → …The activity oscillates portrait ↔ landscape indefinitely, flashing the underlying page between video frames.
Steps to reproduce
- Host a
WebViewwithallowsFullscreenVideoon a screen that locks orientation (activity.requestedOrientation = SCREEN_ORIENTATION_PORTRAIT, e.g. via react-native-orientation-locker). - Load a page whose player exits/re-enters HTML5 fullscreen on orientation change (a page with an unaccepted Cookiebot overlay plus a
<video>player reproduces this reliably). - Play the video (enters fullscreen), physically rotate the device to landscape.
- Observe the activity bouncing between landscape and portrait indefinitely.
Expected behavior
Either (or both):
- The orientation writes are opt-out/configurable (e.g. a
fullscreenOrientationBehaviorprop:"unlock"today's behavior,"inherit"leaverequestedOrientationuntouched), so apps that manage orientation keep control. - The exit restore is deferred until it doesn't force a rotation: keep the current orientation on
onHideCustomViewand reapplyinitialRequestedOrientationonly once the display is physically back atROTATION_0/180(cancelling if fullscreen re-enters). This preserves today's UX for apps that rely on the unlock, while making the restore side-effect-free.
Workaround
We currently ship this as a patch-package patch (13.16.0) implementing the deferred restore; happy to turn it into a PR if maintainers agree with the direction:
val initialRequestedOrientation = activity.requestedOrientation
val orientationHandler = android.os.Handler(android.os.Looper.getMainLooper())
val webChromeClient: RNCWebChromeClient =
object : RNCWebChromeClient(webView) {
// Restore the pre-fullscreen orientation without forcing a
// rotation: wait until the device is physically portrait again.
private val restoreOrientation = object : Runnable {
override fun run() {
if (mVideoView != null || activity.isFinishing || activity.isDestroyed) {
return
}
val rotation = activity.windowManager.defaultDisplay.rotation
val isPortrait = rotation == android.view.Surface.ROTATION_0 ||
rotation == android.view.Surface.ROTATION_180
if (isPortrait) {
activity.requestedOrientation = initialRequestedOrientation
} else if (mWebView.parent != null) {
orientationHandler.postDelayed(this, 500)
}
}
}
override fun onShowCustomView(view: View, callback: CustomViewCallback) {
// ...existing body...
orientationHandler.removeCallbacks(restoreOrientation)
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
// ...
}
override fun onHideCustomView() {
// ...existing body, replacing the immediate restore with:
orientationHandler.removeCallbacks(restoreOrientation)
orientationHandler.post(restoreOrientation)
// ...
}
}Environment
- react-native-webview: 13.16.0
- react-native: 0.81.5 (new architecture enabled)
- Platform: Android (reproduced on API 34 emulator and physical devices)
- OS: Android 14
iOS is unaffected (WKWebView fullscreen respects the app's supported-orientations mask).
Source: react-native-webview/react-native-webview