Skip to content
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

Open
wants to merge 83 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
29c35e9
docs: enable Android PIP
YangJonghun Nov 26, 2023
e5fd0c7
chore: change comments
YangJonghun Nov 26, 2023
b554034
feat(android): implement Android PictureInPicture
YangJonghun Nov 26, 2023
e6797fd
Merge branch 'master' into feat/android-pip
YangJonghun Jan 18, 2024
3ba6a30
refactor: minor refactor code and apply lint
YangJonghun Jan 19, 2024
4938bd0
fix: rewrite pip action intent code for Android14
YangJonghun Jan 19, 2024
6e8307b
fix: remove redundant codes
YangJonghun Jan 19, 2024
0c06c2f
feat: add isInPictureInPicture flag for lifecycle handling
YangJonghun Jan 19, 2024
f3066a4
feat: add pipFullscreenPlayerView for makes PIP include video only
YangJonghun Jan 19, 2024
60657eb
Merge branch 'master' into feat/android-pip
YangJonghun Jan 20, 2024
2872689
fix: add manifest value checker for prevent crash
YangJonghun Jan 20, 2024
9723313
docs: add pictureInPicture prop's Android guide
YangJonghun Jan 20, 2024
8269546
fix: sync controller visibility
YangJonghun Jan 20, 2024
d10f624
refactor: refining variable name
YangJonghun Jan 23, 2024
05e704d
fix: check multi window mode when host pause
YangJonghun Jan 23, 2024
d742b96
fix: handling when onStop is called while in multi-window mode
YangJonghun Jan 23, 2024
ac4ca79
refactor: enhance PIP util codes
YangJonghun Jan 25, 2024
7419a81
Merge tag 'v6.0.0-beta.5' into feat/android-pip
YangJonghun Mar 8, 2024
996cb2f
Merge branch 'master' into feat/android-pip
YangJonghun Mar 8, 2024
5c5ab63
Merge branch 'master' into feat/android-pip
YangJonghun Mar 18, 2024
c1c7625
Merge tag 'v6.0.0-beta.8' into feat/android-pip
YangJonghun Apr 10, 2024
78c2321
Merge branch 'master' into feat/android-pip
YangJonghun Apr 10, 2024
cf57475
fix: fix FullscreenPlayerView constructor
YangJonghun Apr 10, 2024
ad06910
refactor: add enterPictureInPictureOnLeave prop and pip methods
YangJonghun Apr 11, 2024
f75489c
fix: fix lint error
YangJonghun Apr 12, 2024
e0795ad
fix: prevent audio play in background without playInBackground prop
YangJonghun Apr 17, 2024
5ccc997
Merge branch 'master' into feat/android-pip
YangJonghun May 9, 2024
2c99b61
fix: fix onDetachedFromWindow
YangJonghun May 10, 2024
ed77c4c
docs: update docs for pip
YangJonghun May 10, 2024
bf07348
fix(android): sync pip controller with external controller state
YangJonghun May 10, 2024
f0647da
Merge branch 'master' into feat/android-pip
YangJonghun May 15, 2024
6313b0e
fix(ios): fix pip active fn variable reference
YangJonghun May 17, 2024
6cff2bd
refactor(ios): refactor code
YangJonghun May 21, 2024
8f1490a
Merge branch 'master' into feat/android-pip
YangJonghun May 22, 2024
8cf55f3
refactor(android): refactor codes
YangJonghun May 22, 2024
39988c0
Merge branch 'master' into feat/android-pip
YangJonghun May 23, 2024
013a69c
fix(android): fix lint error
YangJonghun May 23, 2024
5c153ac
Merge branch 'master' into feat/android-pip
YangJonghun May 24, 2024
2e3b13b
refactor(android): refactor android pip logics
YangJonghun May 24, 2024
5fbede2
fix(android): fix flickering issue when stop picture in picture
YangJonghun May 27, 2024
15cd0e8
fix(android): fix import
YangJonghun May 27, 2024
dc6a64e
fix(android): fix picture in picture with fullscreen mode
YangJonghun May 30, 2024
4a79fd7
Merge branch 'master' into feat/android-pip
YangJonghun Jun 1, 2024
edc91d0
fix(ios): fix syntax error
YangJonghun Jun 1, 2024
c552487
Merge tag 'v6.2.0' into feat/android-pip
YangJonghun Jun 19, 2024
9c9fe4c
fix(android): fix Fragment managed code
YangJonghun Jun 20, 2024
ebb7fbe
refactor(android): remove redundant override lifecycle
YangJonghun Jun 20, 2024
ec0c172
Merge tag 'v6.3.0' into feat/android-pip
YangJonghun Jul 3, 2024
b17a7bd
Merge branch 'master' into feat/android-pip
YangJonghun Jul 12, 2024
b4db6da
Merge tag 'v6.4.1' into feat/android-pip
YangJonghun Jul 12, 2024
a5bff13
fix(js): add PIP type definition for codegen
YangJonghun Jul 12, 2024
1cd78b4
fix(android): fix syntax
YangJonghun Jul 12, 2024
c5d9a28
chore(android): fix lint error
YangJonghun Jul 12, 2024
053f84c
fix(ios): fix enter background handler
YangJonghun Jul 12, 2024
ae933cb
refactor(ios): remove redundant code
YangJonghun Jul 12, 2024
6bbfe82
fix(ios): fix applicationDidEnterBackground for PIP
YangJonghun Jul 12, 2024
d0f521a
fix(android): fix onPictureInPictureStatusChanged
YangJonghun Jul 12, 2024
ff25e83
fix(ios): fix RCTPictureInPicture
YangJonghun Jul 12, 2024
b392b75
refactor(android): Ignore exception for some device ignore pip checker
YangJonghun Jul 14, 2024
a40e66b
Merge branch 'master' into feat/android-pip
YangJonghun Jul 16, 2024
7952275
fix(android): add hideWithoutPlayer fn into Kotlin ver
YangJonghun Jul 17, 2024
8308baa
refactor(android): remove redundant code
YangJonghun Jul 18, 2024
5455d16
Merge tag 'v6.4.3' into feat/android-pip
YangJonghun Jul 28, 2024
96126be
fix(android): fix pip ratio to be calculated with correct ratio value
YangJonghun Sep 7, 2024
f376b53
Merge tag 'v6.6.2' into feat/android-pip
YangJonghun Sep 29, 2024
362459d
fix(android): fix crash issue when unmounting in PIP mode
YangJonghun Sep 29, 2024
7e8a0b8
fix(android): fix lint error
YangJonghun Sep 29, 2024
2c78d41
Merge tag 'v6.6.3' into feat/android-pip
Oct 1, 2024
74abd7f
Merge branch 'master' into feat/android-pip
freeboub Oct 10, 2024
9c805d9
Update android/src/main/java/com/brentvatne/react/VideoManagerModule.kt
freeboub Oct 10, 2024
8e132c3
Merge branch 'master' into feat/android-pip
YangJonghun Oct 11, 2024
d712690
fix(android): fix lint error
YangJonghun Oct 11, 2024
441fb56
fix(ios): fix lint error
YangJonghun Oct 11, 2024
c431403
Merge branch 'master' into feat/android-pip
YangJonghun Oct 13, 2024
42b2477
fix(ios): fix lint error
YangJonghun Oct 13, 2024
a4381e4
feat(expo): add android picture in picture config within expo plugin
YangJonghun Oct 13, 2024
87bc4ab
Merge branch 'master' into feat/android-pip
YangJonghun Nov 17, 2024
3c0c24c
Merge branch 'master' into feat/android-pip
YangJonghun Nov 29, 2024
a27d098
Merge branch 'master' into feat/android-pip
YangJonghun Dec 1, 2024
3521d74
fix: Replace Fragment with androidx.activity
YangJonghun Dec 15, 2024
a86189d
fix: fix lint error
YangJonghun Dec 15, 2024
846412e
fix(android): disable auto enter when player released
YangJonghun Dec 15, 2024
5e244e8
fix(android): fix event handler to check based on Activity it's bound to
YangJonghun Dec 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ enum class EventTypes(val eventName: String) {

EVENT_TEXT_TRACK_DATA_CHANGED("onTextTrackDataChanged"),
EVENT_VIDEO_TRACKS("onVideoTracks"),
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent");
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent"),
EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED("onPictureInPictureStatusChanged");

companion object {
fun toMap() =
Expand Down Expand Up @@ -90,6 +91,7 @@ class VideoEventEmitter {
lateinit var onVideoTracks: (videoTracks: ArrayList<VideoTrack>?) -> Unit
lateinit var onTextTrackDataChanged: (textTrackData: String) -> Unit
lateinit var onReceiveAdEvent: (adEvent: String, adData: Map<String?, String?>?) -> Unit
lateinit var onPictureInPictureStatusChanged: (isActive: Boolean) -> Unit

fun addEventEmitters(reactContext: ThemedReactContext, view: ReactExoplayerView) {
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
Expand Down Expand Up @@ -278,6 +280,11 @@ class VideoEventEmitter {
)
}
}
onPictureInPictureStatusChanged = { isActive ->
event.dispatch(EventTypes.EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED) {
putBoolean("isActive", isActive)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.app.Dialog
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
Expand Down Expand Up @@ -125,6 +126,14 @@ class FullScreenPlayerView(
}
}

fun hideWithoutPlayer() {
for (i in 0 until containerView.childCount) {
if (containerView.getChildAt(i) !== exoPlayerView) {
containerView.getChildAt(i).visibility = View.GONE
}
}
}

private fun getFullscreenIconResource(isFullscreen: Boolean): Int =
if (isFullscreen) {
androidx.media3.ui.R.drawable.exo_icon_fullscreen_exit
Expand Down
119 changes: 119 additions & 0 deletions android/src/main/java/com/brentvatne/exoplayer/PictureInPictureUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.brentvatne.exoplayer

import android.annotation.SuppressLint
import android.app.AppOpsManager
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.pm.PackageManager
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Process
import android.util.Rational
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.RequiresApi
import androidx.core.app.AppOpsManagerCompat
import androidx.media3.exoplayer.ExoPlayer
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.receiver.PictureInPictureReceiver
import com.facebook.react.uimanager.ThemedReactContext

object PictureInPictureUtil {
private const val FLAG_SUPPORTS_PICTURE_IN_PICTURE = 0x400000
private const val TAG = "PictureInPictureUtil"

@JvmStatic
fun enterPictureInPictureMode(context: ThemedReactContext, pictureInPictureParams: PictureInPictureParams?) {
if (!isSupportPictureInPicture(context)) return
if (isSupportPictureInPictureAction() && pictureInPictureParams != null) {
try {
context.currentActivity?.enterPictureInPictureMode(pictureInPictureParams)
} catch (e: IllegalStateException) {
DebugLog.e(TAG, e.toString())
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
@Suppress("DEPRECATION")
context.currentActivity?.enterPictureInPictureMode()
} catch (e: IllegalStateException) {
DebugLog.e(TAG, e.toString())
}
}
}

@JvmStatic
fun updatePictureInPictureActions(context: ThemedReactContext, pictureInPictureParams: PictureInPictureParams) {
if (!isSupportPictureInPictureAction()) return
if (!isSupportPictureInPicture(context)) return
try {
context.currentActivity?.setPictureInPictureParams(pictureInPictureParams)
} 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)
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.currentActivity ?: 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
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.brentvatne.exoplayer

import android.os.Build
import androidx.fragment.app.Fragment
import java.util.UUID

class ReactExoplayerFragment(private val view: ReactExoplayerView) : Fragment() {
val id = "${ReactExoplayerFragment::class.java.simpleName}_${UUID.randomUUID()}"
private var mIsOnStopCalled = false

override fun onStart() {
super.onStart()
mIsOnStopCalled = false
}

override fun onStop() {
// On entering Picture-in-Picture mode, onPause is called, but not onStop.
// For this reason, this is the place where we should pause the video playback.
super.onStop()
if (!view.playInBackground) view.setPausedModifier(true)
mIsOnStopCalled = true

// Handling when onStop is called while in multi-window mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
freeboub marked this conversation as resolved.
Show resolved Hide resolved
activity?.isInMultiWindowMode == true &&
activity?.isInPictureInPictureMode != true &&
!view.playInBackground
) {
view.setPausedModifier(true)
}
}

override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
view.setIsInPictureInPicture(isInPictureInPictureMode)
// To pause player when closing PIP window.
if (!isInPictureInPictureMode && mIsOnStopCalled && !view.playInBackground) {
view.setPausedModifier(true)
}
}
}
Loading
Loading