-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: implement enterPictureInPictureOnLeave prop for both platform(Android, iOS) #3385
Merged
KrzysztofMoch
merged 83 commits into
TheWidlarzGroup:master
from
YangJonghun:feat/android-pip
Jan 4, 2025
Merged
Changes from 80 commits
Commits
Show all changes
83 commits
Select commit
Hold shift + click to select a range
29c35e9
docs: enable Android PIP
YangJonghun e5fd0c7
chore: change comments
YangJonghun b554034
feat(android): implement Android PictureInPicture
YangJonghun e6797fd
Merge branch 'master' into feat/android-pip
YangJonghun 3ba6a30
refactor: minor refactor code and apply lint
YangJonghun 4938bd0
fix: rewrite pip action intent code for Android14
YangJonghun 6e8307b
fix: remove redundant codes
YangJonghun 0c06c2f
feat: add isInPictureInPicture flag for lifecycle handling
YangJonghun f3066a4
feat: add pipFullscreenPlayerView for makes PIP include video only
YangJonghun 60657eb
Merge branch 'master' into feat/android-pip
YangJonghun 2872689
fix: add manifest value checker for prevent crash
YangJonghun 9723313
docs: add pictureInPicture prop's Android guide
YangJonghun 8269546
fix: sync controller visibility
YangJonghun d10f624
refactor: refining variable name
YangJonghun 05e704d
fix: check multi window mode when host pause
YangJonghun d742b96
fix: handling when onStop is called while in multi-window mode
YangJonghun ac4ca79
refactor: enhance PIP util codes
YangJonghun 7419a81
Merge tag 'v6.0.0-beta.5' into feat/android-pip
YangJonghun 996cb2f
Merge branch 'master' into feat/android-pip
YangJonghun 5c5ab63
Merge branch 'master' into feat/android-pip
YangJonghun c1c7625
Merge tag 'v6.0.0-beta.8' into feat/android-pip
YangJonghun 78c2321
Merge branch 'master' into feat/android-pip
YangJonghun cf57475
fix: fix FullscreenPlayerView constructor
YangJonghun ad06910
refactor: add enterPictureInPictureOnLeave prop and pip methods
YangJonghun f75489c
fix: fix lint error
YangJonghun e0795ad
fix: prevent audio play in background without playInBackground prop
YangJonghun 5ccc997
Merge branch 'master' into feat/android-pip
YangJonghun 2c99b61
fix: fix onDetachedFromWindow
YangJonghun ed77c4c
docs: update docs for pip
YangJonghun bf07348
fix(android): sync pip controller with external controller state
YangJonghun f0647da
Merge branch 'master' into feat/android-pip
YangJonghun 6313b0e
fix(ios): fix pip active fn variable reference
YangJonghun 6cff2bd
refactor(ios): refactor code
YangJonghun 8f1490a
Merge branch 'master' into feat/android-pip
YangJonghun 8cf55f3
refactor(android): refactor codes
YangJonghun 39988c0
Merge branch 'master' into feat/android-pip
YangJonghun 013a69c
fix(android): fix lint error
YangJonghun 5c153ac
Merge branch 'master' into feat/android-pip
YangJonghun 2e3b13b
refactor(android): refactor android pip logics
YangJonghun 5fbede2
fix(android): fix flickering issue when stop picture in picture
YangJonghun 15cd0e8
fix(android): fix import
YangJonghun dc6a64e
fix(android): fix picture in picture with fullscreen mode
YangJonghun 4a79fd7
Merge branch 'master' into feat/android-pip
YangJonghun edc91d0
fix(ios): fix syntax error
YangJonghun c552487
Merge tag 'v6.2.0' into feat/android-pip
YangJonghun 9c9fe4c
fix(android): fix Fragment managed code
YangJonghun ebb7fbe
refactor(android): remove redundant override lifecycle
YangJonghun ec0c172
Merge tag 'v6.3.0' into feat/android-pip
YangJonghun b17a7bd
Merge branch 'master' into feat/android-pip
YangJonghun b4db6da
Merge tag 'v6.4.1' into feat/android-pip
YangJonghun a5bff13
fix(js): add PIP type definition for codegen
YangJonghun 1cd78b4
fix(android): fix syntax
YangJonghun c5d9a28
chore(android): fix lint error
YangJonghun 053f84c
fix(ios): fix enter background handler
YangJonghun ae933cb
refactor(ios): remove redundant code
YangJonghun 6bbfe82
fix(ios): fix applicationDidEnterBackground for PIP
YangJonghun d0f521a
fix(android): fix onPictureInPictureStatusChanged
YangJonghun ff25e83
fix(ios): fix RCTPictureInPicture
YangJonghun b392b75
refactor(android): Ignore exception for some device ignore pip checker
YangJonghun a40e66b
Merge branch 'master' into feat/android-pip
YangJonghun 7952275
fix(android): add hideWithoutPlayer fn into Kotlin ver
YangJonghun 8308baa
refactor(android): remove redundant code
YangJonghun 5455d16
Merge tag 'v6.4.3' into feat/android-pip
YangJonghun 96126be
fix(android): fix pip ratio to be calculated with correct ratio value
YangJonghun f376b53
Merge tag 'v6.6.2' into feat/android-pip
YangJonghun 362459d
fix(android): fix crash issue when unmounting in PIP mode
YangJonghun 7e8a0b8
fix(android): fix lint error
YangJonghun 2c78d41
Merge tag 'v6.6.3' into feat/android-pip
74abd7f
Merge branch 'master' into feat/android-pip
freeboub 9c805d9
Update android/src/main/java/com/brentvatne/react/VideoManagerModule.kt
freeboub 8e132c3
Merge branch 'master' into feat/android-pip
YangJonghun d712690
fix(android): fix lint error
YangJonghun 441fb56
fix(ios): fix lint error
YangJonghun c431403
Merge branch 'master' into feat/android-pip
YangJonghun 42b2477
fix(ios): fix lint error
YangJonghun a4381e4
feat(expo): add android picture in picture config within expo plugin
YangJonghun 87bc4ab
Merge branch 'master' into feat/android-pip
YangJonghun 3c0c24c
Merge branch 'master' into feat/android-pip
YangJonghun a27d098
Merge branch 'master' into feat/android-pip
YangJonghun 3521d74
fix: Replace Fragment with androidx.activity
YangJonghun a86189d
fix: fix lint error
YangJonghun 846412e
fix(android): disable auto enter when player released
YangJonghun 5e244e8
fix(android): fix event handler to check based on Activity it's bound to
YangJonghun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
202 changes: 202 additions & 0 deletions
202
android/src/main/java/com/brentvatne/exoplayer/PictureInPictureUtil.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
package com.brentvatne.exoplayer | ||
|
||
import android.annotation.SuppressLint | ||
import android.app.AppOpsManager | ||
import android.app.PictureInPictureParams | ||
import android.app.RemoteAction | ||
import android.content.Context | ||
import android.content.ContextWrapper | ||
import android.content.pm.PackageManager | ||
import android.graphics.Rect | ||
import android.graphics.drawable.Icon | ||
import android.os.Build | ||
import android.os.Process | ||
import android.util.Rational | ||
import androidx.activity.ComponentActivity | ||
import androidx.annotation.ChecksSdkIntAtLeast | ||
import androidx.annotation.RequiresApi | ||
import androidx.core.app.AppOpsManagerCompat | ||
import androidx.core.app.PictureInPictureModeChangedInfo | ||
import androidx.lifecycle.Lifecycle | ||
import androidx.media3.exoplayer.ExoPlayer | ||
import com.brentvatne.common.toolbox.DebugLog | ||
import com.brentvatne.receiver.PictureInPictureReceiver | ||
import com.facebook.react.uimanager.ThemedReactContext | ||
|
||
internal fun Context.findActivity(): ComponentActivity { | ||
var context = this | ||
while (context is ContextWrapper) { | ||
if (context is ComponentActivity) return context | ||
context = context.baseContext | ||
} | ||
throw IllegalStateException("Picture in picture should be called in the context of an Activity") | ||
} | ||
|
||
object PictureInPictureUtil { | ||
private const val FLAG_SUPPORTS_PICTURE_IN_PICTURE = 0x400000 | ||
private const val TAG = "PictureInPictureUtil" | ||
|
||
@JvmStatic | ||
fun addLifecycleEventListener(context: ThemedReactContext, view: ReactExoplayerView): Runnable { | ||
val activity = context.findActivity() | ||
|
||
val onPictureInPictureModeChanged: (info: PictureInPictureModeChangedInfo) -> Unit = { info: PictureInPictureModeChangedInfo -> | ||
view.setIsInPictureInPicture(info.isInPictureInPictureMode) | ||
if (!info.isInPictureInPictureMode && context.findActivity().lifecycle.currentState == Lifecycle.State.CREATED) { | ||
// when user click close button of PIP | ||
if (!view.playInBackground) view.setPausedModifier(true) | ||
} | ||
} | ||
|
||
val onUserLeaveHintCallback = { | ||
if (view.enterPictureInPictureOnLeave) { | ||
view.enterPictureInPictureMode() | ||
} | ||
} | ||
|
||
activity.addOnPictureInPictureModeChangedListener(onPictureInPictureModeChanged) | ||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { | ||
activity.addOnUserLeaveHintListener(onUserLeaveHintCallback) | ||
} | ||
|
||
// @TODO convert to lambda when ReactExoplayerView migrated | ||
return object: Runnable { | ||
override fun run() { | ||
context.findActivity().removeOnPictureInPictureModeChangedListener(onPictureInPictureModeChanged) | ||
context.findActivity().removeOnUserLeaveHintListener(onUserLeaveHintCallback) | ||
} | ||
} | ||
} | ||
|
||
@JvmStatic | ||
fun enterPictureInPictureMode(context: ThemedReactContext, pictureInPictureParams: PictureInPictureParams?) { | ||
if (!isSupportPictureInPicture(context)) return | ||
if (isSupportPictureInPictureAction() && pictureInPictureParams != null) { | ||
try { | ||
context.findActivity().enterPictureInPictureMode(pictureInPictureParams) | ||
} catch (e: IllegalStateException) { | ||
DebugLog.e(TAG, e.toString()) | ||
} | ||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | ||
try { | ||
@Suppress("DEPRECATION") | ||
context.findActivity().enterPictureInPictureMode() | ||
} catch (e: IllegalStateException) { | ||
DebugLog.e(TAG, e.toString()) | ||
} | ||
} | ||
} | ||
|
||
@JvmStatic | ||
fun applyPlayingStatus(context: ThemedReactContext, pipParamsBuilder: PictureInPictureParams.Builder, receiver: PictureInPictureReceiver, isPaused: Boolean) { | ||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; | ||
val actions = getPictureInPictureActions(context, isPaused, receiver) | ||
pipParamsBuilder.setActions(actions) | ||
updatePictureInPictureActions(context, pipParamsBuilder.build()) | ||
} | ||
|
||
@JvmStatic | ||
fun applyAutoEnterEnabled(context: ThemedReactContext, pipParamsBuilder: PictureInPictureParams.Builder, autoEnterEnabled: Boolean) { | ||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return; | ||
pipParamsBuilder.setAutoEnterEnabled(autoEnterEnabled) | ||
updatePictureInPictureActions(context, pipParamsBuilder.build()) | ||
} | ||
|
||
@JvmStatic | ||
fun applySourceRectHint(context: ThemedReactContext, pipParamsBuilder: PictureInPictureParams.Builder, playerView: ExoPlayerView) { | ||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; | ||
pipParamsBuilder.setSourceRectHint(calcRectHint(playerView)) | ||
updatePictureInPictureActions(context, pipParamsBuilder.build()) | ||
} | ||
|
||
private fun updatePictureInPictureActions(context: ThemedReactContext, pipParams: PictureInPictureParams) { | ||
if (!isSupportPictureInPictureAction()) return | ||
if (!isSupportPictureInPicture(context)) return | ||
try { | ||
context.findActivity().setPictureInPictureParams(pipParams) | ||
} catch (e: IllegalStateException) { | ||
DebugLog.e(TAG, e.toString()) | ||
} | ||
} | ||
|
||
@JvmStatic | ||
@RequiresApi(Build.VERSION_CODES.O) | ||
fun getPictureInPictureActions(context: ThemedReactContext, isPaused: Boolean, receiver: PictureInPictureReceiver): ArrayList<RemoteAction> { | ||
val intent = receiver.getPipActionIntent(isPaused) | ||
val resource = | ||
if (isPaused) androidx.media3.ui.R.drawable.exo_icon_play else androidx.media3.ui.R.drawable.exo_icon_pause | ||
val icon = Icon.createWithResource(context, resource) | ||
val title = if (isPaused) "play" else "pause" | ||
return arrayListOf(RemoteAction(icon, title, title, intent)) | ||
} | ||
|
||
@JvmStatic | ||
@RequiresApi(Build.VERSION_CODES.O) | ||
private fun calcRectHint(playerView: ExoPlayerView): Rect { | ||
val hint = Rect() | ||
playerView.surfaceView?.getGlobalVisibleRect(hint) | ||
val location = IntArray(2) | ||
playerView.surfaceView?.getLocationOnScreen(location) | ||
|
||
val height = hint.bottom - hint.top | ||
hint.top = location[1] | ||
hint.bottom = hint.top + height | ||
return hint | ||
} | ||
|
||
@JvmStatic | ||
@RequiresApi(Build.VERSION_CODES.O) | ||
fun calcPictureInPictureAspectRatio(player: ExoPlayer): Rational { | ||
var aspectRatio = Rational(player.videoSize.width, player.videoSize.height) | ||
// AspectRatio for the activity in picture-in-picture, must be between 2.39:1 and 1:2.39 (inclusive). | ||
// https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational) | ||
val maximumRatio = Rational(239, 100) | ||
val minimumRatio = Rational(100, 239) | ||
if (aspectRatio.toFloat() > maximumRatio.toFloat()) { | ||
aspectRatio = maximumRatio | ||
} else if (aspectRatio.toFloat() < minimumRatio.toFloat()) { | ||
aspectRatio = minimumRatio | ||
} | ||
return aspectRatio | ||
} | ||
|
||
private fun isSupportPictureInPicture(context: ThemedReactContext): Boolean = | ||
checkIsApiSupport() && checkIsSystemSupportPIP(context) && checkIsUserAllowPIP(context) | ||
|
||
private fun isSupportPictureInPictureAction(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O | ||
|
||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N) | ||
private fun checkIsApiSupport(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N | ||
|
||
@RequiresApi(Build.VERSION_CODES.N) | ||
private fun checkIsSystemSupportPIP(context: ThemedReactContext): Boolean { | ||
val activity = context.findActivity() ?: return false | ||
|
||
val activityInfo = activity.packageManager.getActivityInfo(activity.componentName, PackageManager.GET_META_DATA) | ||
// detect current activity's android:supportsPictureInPicture value defined within AndroidManifest.xml | ||
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/ActivityInfo.java;l=1090-1093;drc=7651f0a4c059a98f32b0ba30cd64500bf135385f | ||
val isActivitySupportPip = activityInfo.flags and FLAG_SUPPORTS_PICTURE_IN_PICTURE != 0 | ||
|
||
// PIP might be disabled on devices that have low RAM. | ||
val isPipAvailable = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) | ||
|
||
return isActivitySupportPip && isPipAvailable | ||
} | ||
|
||
private fun checkIsUserAllowPIP(context: ThemedReactContext): Boolean { | ||
val activity = context.currentActivity ?: return false | ||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||
@SuppressLint("InlinedApi") | ||
val result = AppOpsManagerCompat.noteOpNoThrow( | ||
activity, | ||
AppOpsManager.OPSTR_PICTURE_IN_PICTURE, | ||
Process.myUid(), | ||
activity.packageName | ||
) | ||
AppOpsManager.MODE_ALLOWED == result | ||
} else { | ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.O && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N | ||
} | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it possible Activity is different between when bind eventListeners and when
onPictureInPictureModeChanged
is called?🤔There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes more sense to fix it as you said! We need to only handle Activity where the Video is visible
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
5e244e8
I've fixed it in above commit :)