diff --git a/android/src/main/java/com/theoplayer/presentation/PresentationManager.kt b/android/src/main/java/com/theoplayer/presentation/PresentationManager.kt index 3abf678c3..b4b058abc 100644 --- a/android/src/main/java/com/theoplayer/presentation/PresentationManager.kt +++ b/android/src/main/java/com/theoplayer/presentation/PresentationManager.kt @@ -40,6 +40,8 @@ class PresentationManager( var currentPresentationMode: PresentationMode = PresentationMode.INLINE private set + var currentPresentationModeChangeContext: PresentationModeChangeContext? = null + private set var pipConfig: PipConfig = PipConfig() @@ -54,12 +56,14 @@ class PresentationManager( } onPictureInPictureModeChanged = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { - // Dispatch event on every PiP mode change + val transitioningToPip = intent + ?.getBooleanExtra("isTransitioningToPip", false) ?: false val inPip = intent?.getBooleanExtra("isInPictureInPictureMode", false) ?: false - if (inPip) { - onEnterPip() - } else { - onExitPip() + // Dispatch event on every PiP mode change + when { + transitioningToPip -> onEnterPip(true) + inPip -> onEnterPip() + else -> onExitPip() } } } @@ -104,9 +108,11 @@ class PresentationManager( PresentationMode.INLINE -> { setFullscreen(false) } + PresentationMode.FULLSCREEN -> { setFullscreen(true) } + PresentationMode.PICTURE_IN_PICTURE -> { setFullscreen(false) enterPip() @@ -144,8 +150,13 @@ class PresentationManager( } } - private fun onEnterPip() { - updatePresentationMode(PresentationMode.PICTURE_IN_PICTURE) + private fun onEnterPip(transitioningToPip: Boolean = false) { + updatePresentationMode( + PresentationMode.PICTURE_IN_PICTURE, + if (transitioningToPip) + PresentationModeChangeContext(PresentationModeChangePipContext.TRANSITIONING_TO_PIP) + else null + ) } private fun onExitPip() { @@ -241,11 +252,14 @@ class PresentationManager( presentationMode: PresentationMode, context: PresentationModeChangeContext? = null ) { - if (presentationMode == currentPresentationMode) { + if (presentationMode == currentPresentationMode && + context == currentPresentationModeChangeContext + ) { return } val prevPresentationMode = currentPresentationMode currentPresentationMode = presentationMode + currentPresentationModeChangeContext = context eventEmitter.emitPresentationModeChange(presentationMode, prevPresentationMode, context) // Resume playing when going to PiP and player was playing diff --git a/android/src/main/java/com/theoplayer/presentation/PresentationModeChangeContext.kt b/android/src/main/java/com/theoplayer/presentation/PresentationModeChangeContext.kt index cc660670a..b91426559 100644 --- a/android/src/main/java/com/theoplayer/presentation/PresentationModeChangeContext.kt +++ b/android/src/main/java/com/theoplayer/presentation/PresentationModeChangeContext.kt @@ -9,7 +9,10 @@ enum class PresentationModeChangePipContext { CLOSED, // The PiP window transitioned back into the app. - RESTORED + RESTORED, + + // The app transitioning to PiP frame + TRANSITIONING_TO_PIP } data class PresentationModeChangeContext( diff --git a/android/src/main/java/com/theoplayer/util/PayloadBuilder.kt b/android/src/main/java/com/theoplayer/util/PayloadBuilder.kt index 0148d9800..7414de406 100644 --- a/android/src/main/java/com/theoplayer/util/PayloadBuilder.kt +++ b/android/src/main/java/com/theoplayer/util/PayloadBuilder.kt @@ -109,6 +109,7 @@ class PayloadBuilder { val contextPayload = Arguments.createMap() context.pip?.let { pipCtx -> contextPayload.putString(EVENT_PROP_PIP, when (pipCtx) { + PresentationModeChangePipContext.TRANSITIONING_TO_PIP -> "transitioning-to-pip" PresentationModeChangePipContext.RESTORED -> "restored" else -> "closed" }) diff --git a/doc/pip.md b/doc/pip.md index 4266b0359..f0a7b1878 100644 --- a/doc/pip.md +++ b/doc/pip.md @@ -96,6 +96,27 @@ override fun onPictureInPictureModeChanged( } ``` +### Enabling early transitioning to PiP event + +You might want to enable [transitioning to PiP]() event for Android 15+ (API 35+). To enable it make sure that +the compile SDK version is set to 35 in your build.gradle `compileSdkVersion = 35`. Also the appropriate intent should be sent from the `MainActivity` to let react-native know when the app starts the PiP animation: + +```kotlin +override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) { + super.onPictureInPictureUiStateChanged(pipState) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && + pipState.isTransitioningToPip + ) { + Intent("onPictureInPictureModeChanged").also { + it.putExtra("isTransitioningToPip", true) + sendBroadcast(it) + } + } +} +``` + +Then it should set the `context?.pip` to `PresentationModeChangePipContext.TRANSITIONING_TO_PIP` in the event `PlayerEventType.PRESENTATIONMODE_CHANGE`. + ### PiP controls The PiP window will show the default controls to configure, maximize and close the PiP window. diff --git a/example/android/app/src/main/java/com/reactnativetheoplayer/MainActivity.kt b/example/android/app/src/main/java/com/reactnativetheoplayer/MainActivity.kt index 7876d2a21..2a178f4dc 100644 --- a/example/android/app/src/main/java/com/reactnativetheoplayer/MainActivity.kt +++ b/example/android/app/src/main/java/com/reactnativetheoplayer/MainActivity.kt @@ -1,5 +1,6 @@ package com.reactnativetheoplayer +import android.app.PictureInPictureUiState import android.content.Intent import android.content.res.Configuration import android.media.AudioManager @@ -39,11 +40,18 @@ open class MainActivity : ReactActivity() { override fun createReactActivityDelegate(): ReactActivityDelegate = DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) + /** + * Called as part of the activity lifecycle when an activity is about to go into the background + * as the result of user choice. + */ public override fun onUserLeaveHint() { this.sendBroadcast(Intent("onUserLeaveHint")) super.onUserLeaveHint() } + /** + * Called by the system when the activity changes to and from picture-in-picture mode. + */ override fun onPictureInPictureModeChanged( isInPictureInPictureMode: Boolean, newConfig: Configuration @@ -55,4 +63,21 @@ open class MainActivity : ReactActivity() { intent.putExtra("isInPictureInPictureMode", isInPictureInPictureMode) this.sendBroadcast(intent) } + + /** + * Called by the system when the activity is in PiP and has state changes. Compare to + * onPictureInPictureModeChanged, which is only called when PiP mode changes (meaning, enters + * or exits PiP), this can be called at any time while the activity is in PiP mode. + */ + override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) { + super.onPictureInPictureUiStateChanged(pipState) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && + pipState.isTransitioningToPip + ) { + Intent("onPictureInPictureModeChanged").also { + it.putExtra("isTransitioningToPip", true) + sendBroadcast(it) + } + } + } } diff --git a/example/android/build.gradle b/example/android/build.gradle index f89e8c722..5f7e19ccf 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -6,8 +6,8 @@ buildscript { // React Native 0.74+ needs minSdkVersion 23+ // supportsPictureInPicture and networkSecurityConfig need minSdkVersion 24+ minSdkVersion = 24 - compileSdkVersion = 34 - targetSdkVersion = 34 + compileSdkVersion = 35 + targetSdkVersion = 35 ndkVersion = "26.1.10909125" castFrameworkVersion = "21.4.0" kotlinVersion = "1.9.22" diff --git a/src/api/event/PlayerEvent.ts b/src/api/event/PlayerEvent.ts index 7c1ddd367..ff1795012 100644 --- a/src/api/event/PlayerEvent.ts +++ b/src/api/event/PlayerEvent.ts @@ -57,6 +57,14 @@ export enum PresentationModeChangePipContext { * The PiP window was restored/maximized. */ RESTORED = 'restored', + + /** + * The app is transitioning to the PiP frame. + * + * @remarks + *
- This property only applies to Android platforms. + */ + TRANSITIONING_TO_PIP = 'transitioning-to-pip', } export interface PresentationModeChangeContext {