[Android] Fullscreen video forcibly overrides the activity's orientation, causing an infinite portrait/landscape loop on orientation-locked screens

Author: Rodney-WebCreated Aug 11, 2026Updated Aug 11, 2026

[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_UNSPECIFIED
  • onHideCustomView: activity.requestedOrientation = initialRequestedOrientation (captured once at WebView creation)

https://github.com/react-native-webview/react-native-webview/blob/v13.16.0/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt#L146-L210

This has two problems for apps that manage orientation themselves (e.g. a portrait-locked app that unlocks only on specific screens):

  1. 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.

  2. The exit restore forces a rotation at the worst possible moment, which can loop forever. onHideCustomView reapplies 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

  1. Host a WebView with allowsFullscreenVideo on a screen that locks orientation (activity.requestedOrientation = SCREEN_ORIENTATION_PORTRAIT, e.g. via react-native-orientation-locker).
  2. 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).
  3. Play the video (enters fullscreen), physically rotate the device to landscape.
  4. Observe the activity bouncing between landscape and portrait indefinitely.

Expected behavior

Either (or both):

  • The orientation writes are opt-out/configurable (e.g. a fullscreenOrientationBehavior prop: "unlock" today's behavior, "inherit" leave requestedOrientation untouched), so apps that manage orientation keep control.
  • The exit restore is deferred until it doesn't force a rotation: keep the current orientation on onHideCustomView and reapply initialRequestedOrientation only once the display is physically back at ROTATION_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:

kotlin
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