Compare commits
1 Commits
master
...
ivan/on-se
Author | SHA1 | Date | |
---|---|---|---|
75d4eec823 |
12
.github/scripts/validate.js
vendored
12
.github/scripts/validate.js
vendored
@ -31,7 +31,7 @@ const BOT_LABELS = [
|
||||
const SKIP_LABEL = 'No Validation';
|
||||
|
||||
const MESSAGE = {
|
||||
FEATURE_REQUEST: `Thanks for the feature request! Check out our roadmap [here](https://github.com/TheWidlarzGroup/react-native-video/discussions/3351). If your request is already there – great! If not, give us some time, and we'll get back to you with information on when TheWidlarzGroup can address it as part of our free open-source support. Alternatively, [contact us](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=feature-request#Contact) to discuss ways to speed up the process.`,
|
||||
FEATURE_REQUEST: `Thank you for your feature request. We will review it and get back to you if we need more information.`,
|
||||
BUG_REPORT: `Thank you for your bug report. We will review it and get back to you if we need more information.`,
|
||||
MISSING_INFO: (missingFields) => {
|
||||
return `Thank you for your issue report. Please note that the following information is missing or incomplete:\n\n${missingFields
|
||||
@ -267,15 +267,11 @@ const hidePreviousComments = async ({github, context}) => {
|
||||
issue_number: context.payload.issue.number,
|
||||
});
|
||||
|
||||
// Filter for bot comments that aren't already hidden
|
||||
const unhiddenBotComments = comments.data.filter(
|
||||
(comment) =>
|
||||
comment.user.type === 'Bot' &&
|
||||
!comment.body.includes('<details>') &&
|
||||
!comment.body.includes('Previous bot comment')
|
||||
const botComments = comments.data.filter(
|
||||
(comment) => comment.user.type === 'Bot',
|
||||
);
|
||||
|
||||
for (const comment of unhiddenBotComments) {
|
||||
for (const comment of botComments) {
|
||||
// Don't format string - it will broke the markdown
|
||||
const hiddenBody = `
|
||||
<details>
|
||||
|
24
CHANGELOG.md
24
CHANGELOG.md
@ -1,29 +1,5 @@
|
||||
|
||||
|
||||
## [6.9.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.9.0...v6.9.1) (2025-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid memory leak on iOS ([#4355](https://github.com/TheWidlarzGroup/react-native-video/issues/4355)) ([424f4ee](https://github.com/TheWidlarzGroup/react-native-video/commit/424f4eeddea989392e25c52f45a9a0281ead6fe1))
|
||||
* NPE in setEnterPictureInPictureOnLeave for unsupported Android versions ([#4362](https://github.com/TheWidlarzGroup/react-native-video/issues/4362)) ([3924b5e](https://github.com/TheWidlarzGroup/react-native-video/commit/3924b5e295ed64c97284f4665bc294066a83574a))
|
||||
|
||||
# [6.9.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.2...v6.9.0) (2025-01-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **android:** disable caching on local asset files ([#4304](https://github.com/TheWidlarzGroup/react-native-video/issues/4304)) ([63c592f](https://github.com/TheWidlarzGroup/react-native-video/commit/63c592f7cd897caf918fd3bd5f129c72432d2b55))
|
||||
* **docs:** bump `next.js` version & fix meta warnings ([#4327](https://github.com/TheWidlarzGroup/react-native-video/issues/4327)) ([7b4bd9a](https://github.com/TheWidlarzGroup/react-native-video/commit/7b4bd9a0169fc2ea6f277dd7ed904bada98bc63a))
|
||||
* hiding poster ([#4308](https://github.com/TheWidlarzGroup/react-native-video/issues/4308)) ([621a802](https://github.com/TheWidlarzGroup/react-native-video/commit/621a80299c690c07846f3fcd8a6c73b7ecde39bf))
|
||||
* **ios:** `_paused` is updated when video playback pause ([#4320](https://github.com/TheWidlarzGroup/react-native-video/issues/4320)) ([3da4f1c](https://github.com/TheWidlarzGroup/react-native-video/commit/3da4f1ca979058b387b1be2c2141f6b93fd084a7))
|
||||
* **ios:** disables subtitles for `none` and `empty` track types ([#4319](https://github.com/TheWidlarzGroup/react-native-video/issues/4319)) ([1033c9d](https://github.com/TheWidlarzGroup/react-native-video/commit/1033c9d4f3db7042a96e7108a7fe9f1567d69ded))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* implement enterPictureInPictureOnLeave prop for both platform (Android, iOS) ([#3385](https://github.com/TheWidlarzGroup/react-native-video/issues/3385)) ([69a7bc2](https://github.com/TheWidlarzGroup/react-native-video/commit/69a7bc2d265f2cf4985f8d81054c46f47ee3bae2))
|
||||
|
||||
## [6.8.2](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.1...v6.8.2) (2024-11-25)
|
||||
|
||||
|
||||
|
@ -43,8 +43,7 @@ enum class EventTypes(val eventName: String) {
|
||||
|
||||
EVENT_TEXT_TRACK_DATA_CHANGED("onTextTrackDataChanged"),
|
||||
EVENT_VIDEO_TRACKS("onVideoTracks"),
|
||||
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent"),
|
||||
EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED("onPictureInPictureStatusChanged");
|
||||
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent");
|
||||
|
||||
companion object {
|
||||
fun toMap() =
|
||||
@ -93,7 +92,6 @@ 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)
|
||||
@ -287,11 +285,6 @@ class VideoEventEmitter {
|
||||
)
|
||||
}
|
||||
}
|
||||
onPictureInPictureStatusChanged = { isActive ->
|
||||
event.dispatch(EventTypes.EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED) {
|
||||
putBoolean("isActive", isActive)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,8 +30,7 @@ class ExoPlayerView(private val context: Context) :
|
||||
FrameLayout(context, null, 0),
|
||||
AdViewProvider {
|
||||
|
||||
var surfaceView: View? = null
|
||||
private set
|
||||
private var surfaceView: View? = null
|
||||
private var shutterView: View
|
||||
private var subtitleLayout: SubtitleView
|
||||
private var layout: AspectRatioFrameLayout
|
||||
|
@ -5,7 +5,6 @@ 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
|
||||
@ -126,14 +125,6 @@ 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
|
||||
|
@ -1,207 +0,0 @@
|
||||
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 && activity.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 (pipParamsBuilder == null || 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 (pipParamsBuilder == null || 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 (pipParamsBuilder == null || 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
|
||||
}
|
||||
}
|
||||
}
|
@ -10,8 +10,6 @@ import static androidx.media3.common.C.TIME_END_OF_SOURCE;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.PictureInPictureParams;
|
||||
import android.app.RemoteAction;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
@ -27,7 +25,6 @@ import android.os.Message;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.CaptioningManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
@ -126,7 +123,6 @@ import com.brentvatne.react.R;
|
||||
import com.brentvatne.react.ReactNativeVideoManager;
|
||||
import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
|
||||
import com.brentvatne.receiver.BecomingNoisyListener;
|
||||
import com.brentvatne.receiver.PictureInPictureReceiver;
|
||||
import com.facebook.react.bridge.LifecycleEventListener;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
@ -207,8 +203,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
private boolean isPaused;
|
||||
private boolean isBuffering;
|
||||
private boolean muted = false;
|
||||
public boolean enterPictureInPictureOnLeave = false;
|
||||
private PictureInPictureParams.Builder pictureInPictureParamsBuilder;
|
||||
private boolean hasAudioFocus = false;
|
||||
private float rate = 1f;
|
||||
private AudioOutput audioOutput = AudioOutput.SPEAKER;
|
||||
@ -219,10 +213,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
private boolean selectTrackWhenReady = false;
|
||||
private final Handler mainHandler;
|
||||
private Runnable mainRunnable;
|
||||
private Runnable pipListenerUnsubscribe;
|
||||
private boolean useCache = false;
|
||||
private ControlsConfig controlsConfig = new ControlsConfig();
|
||||
private ArrayList<Integer> rootViewChildrenOriginalVisibility = new ArrayList<Integer>();
|
||||
|
||||
/*
|
||||
* When user is seeking first called is on onPositionDiscontinuity -> DISCONTINUITY_REASON_SEEK
|
||||
@ -247,7 +239,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
private boolean disableDisconnectError;
|
||||
private boolean preventsDisplaySleepDuringVideoPlayback = true;
|
||||
private float mProgressUpdateInterval = 250.0f;
|
||||
protected boolean playInBackground = false;
|
||||
private boolean playInBackground = false;
|
||||
private boolean mReportBandwidth = false;
|
||||
private boolean controls;
|
||||
|
||||
@ -258,7 +250,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
private final ThemedReactContext themedReactContext;
|
||||
private final AudioManager audioManager;
|
||||
private final AudioBecomingNoisyReceiver audioBecomingNoisyReceiver;
|
||||
private final PictureInPictureReceiver pictureInPictureReceiver;
|
||||
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
||||
|
||||
// store last progress event values to avoid sending unnecessary messages
|
||||
@ -335,19 +326,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
this.eventEmitter = new VideoEventEmitter();
|
||||
this.config = config;
|
||||
this.bandwidthMeter = config.getBandwidthMeter();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && pictureInPictureParamsBuilder == null) {
|
||||
this.pictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
|
||||
}
|
||||
mainHandler = new Handler();
|
||||
|
||||
createViews();
|
||||
|
||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
themedReactContext.addLifecycleEventListener(this);
|
||||
pipListenerUnsubscribe = PictureInPictureUtil.addLifecycleEventListener(context, this);
|
||||
audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext);
|
||||
audioFocusChangeListener = new OnAudioFocusChangedListener(this, themedReactContext);
|
||||
pictureInPictureReceiver = new PictureInPictureReceiver(this, themedReactContext);
|
||||
}
|
||||
|
||||
private boolean isPlayingAd() {
|
||||
@ -363,21 +348,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT);
|
||||
exoPlayerView = new ExoPlayerView(getContext());
|
||||
exoPlayerView.addOnLayoutChangeListener( (View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) ->
|
||||
PictureInPictureUtil.applySourceRectHint(themedReactContext, pictureInPictureParamsBuilder, exoPlayerView)
|
||||
);
|
||||
exoPlayerView.setLayoutParams(layoutParams);
|
||||
addView(exoPlayerView, 0, layoutParams);
|
||||
|
||||
exoPlayerView.setFocusable(this.focusable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
cleanupPlaybackService();
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
// LifecycleEventListener implementation
|
||||
@Override
|
||||
public void onHostResume() {
|
||||
@ -390,10 +366,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
@Override
|
||||
public void onHostPause() {
|
||||
isInBackground = true;
|
||||
Activity activity = themedReactContext.getCurrentActivity();
|
||||
boolean isInPictureInPicture = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInPictureInPictureMode();
|
||||
boolean isInMultiWindowMode = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInMultiWindowMode();
|
||||
if (playInBackground || isInPictureInPicture || isInMultiWindowMode) {
|
||||
if (playInBackground) {
|
||||
return;
|
||||
}
|
||||
setPlayWhenReady(false);
|
||||
@ -404,6 +377,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
cleanUpResources();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
cleanupPlaybackService();
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
public void cleanUpResources() {
|
||||
stopPlayback();
|
||||
themedReactContext.removeLifecycleEventListener(this);
|
||||
@ -551,12 +530,21 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
|
||||
builder.setSingleChoiceItems(speedOptions, selectedSpeedIndex, (dialog, which) -> {
|
||||
selectedSpeedIndex = which;
|
||||
float speed = switch (which) {
|
||||
case 0 -> 0.5f;
|
||||
case 2 -> 1.5f;
|
||||
case 3 -> 2.0f;
|
||||
default -> 1.0f;
|
||||
};
|
||||
float speed;
|
||||
switch (which) {
|
||||
case 0:
|
||||
speed = 0.5f;
|
||||
break;
|
||||
case 1:
|
||||
speed = 1.0f;
|
||||
break;
|
||||
case 2:
|
||||
speed = 1.5f;
|
||||
break;
|
||||
default:
|
||||
speed = 1.0f;
|
||||
break;
|
||||
}
|
||||
setRateModifier(speed);
|
||||
});
|
||||
builder.show();
|
||||
@ -884,7 +872,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
exoPlayerView.setPlayer(player);
|
||||
|
||||
audioBecomingNoisyReceiver.setListener(self);
|
||||
pictureInPictureReceiver.setListener();
|
||||
bandwidthMeter.addEventListener(new Handler(), self);
|
||||
setPlayWhenReady(!isPaused);
|
||||
playerNeedsSource = true;
|
||||
@ -1309,10 +1296,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
updateResumePosition();
|
||||
player.release();
|
||||
player.removeListener(this);
|
||||
PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, false);
|
||||
if (pipListenerUnsubscribe != null) {
|
||||
new Handler().post(pipListenerUnsubscribe);
|
||||
}
|
||||
trackSelector = null;
|
||||
|
||||
ReactNativeVideoManager.Companion.getInstance().onInstanceRemoved(instanceId, player);
|
||||
@ -1325,7 +1308,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
progressHandler.removeMessages(SHOW_PROGRESS);
|
||||
audioBecomingNoisyReceiver.removeListener();
|
||||
pictureInPictureReceiver.removeListener();
|
||||
bandwidthMeter.removeEventListener(this);
|
||||
|
||||
if (mainHandler != null && mainRunnable != null) {
|
||||
@ -1850,7 +1832,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
if (isPlaying && isSeeking) {
|
||||
eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition);
|
||||
}
|
||||
PictureInPictureUtil.applyPlayingStatus(themedReactContext, pictureInPictureParamsBuilder, pictureInPictureReceiver, !isPlaying);
|
||||
|
||||
eventEmitter.onVideoPlaybackStateChanged.invoke(isPlaying, isSeeking);
|
||||
|
||||
if (isPlaying) {
|
||||
@ -1945,6 +1927,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
}
|
||||
|
||||
// ReactExoplayerViewManager public api
|
||||
|
||||
public void setSrc(Source source) {
|
||||
if (source.getUri() != null) {
|
||||
clearResumePosition();
|
||||
@ -2262,84 +2246,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
}
|
||||
|
||||
public void setEnterPictureInPictureOnLeave(boolean enterPictureInPictureOnLeave) {
|
||||
this.enterPictureInPictureOnLeave = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && enterPictureInPictureOnLeave;
|
||||
PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, this.enterPictureInPictureOnLeave);
|
||||
}
|
||||
|
||||
protected void setIsInPictureInPicture(boolean isInPictureInPicture) {
|
||||
eventEmitter.onPictureInPictureStatusChanged.invoke(isInPictureInPicture);
|
||||
|
||||
if (fullScreenPlayerView != null && fullScreenPlayerView.isShowing()) {
|
||||
if (isInPictureInPicture) fullScreenPlayerView.hideWithoutPlayer();
|
||||
return;
|
||||
}
|
||||
|
||||
Activity currentActivity = themedReactContext.getCurrentActivity();
|
||||
if (currentActivity == null) return;
|
||||
|
||||
View decorView = currentActivity.getWindow().getDecorView();
|
||||
ViewGroup rootView = decorView.findViewById(android.R.id.content);
|
||||
|
||||
LayoutParams layoutParams = new LayoutParams(
|
||||
LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT);
|
||||
|
||||
if (isInPictureInPicture) {
|
||||
ViewGroup parent = (ViewGroup)exoPlayerView.getParent();
|
||||
if (parent != null) {
|
||||
parent.removeView(exoPlayerView);
|
||||
}
|
||||
for (int i = 0; i < rootView.getChildCount(); i++) {
|
||||
if (rootView.getChildAt(i) != exoPlayerView) {
|
||||
rootViewChildrenOriginalVisibility.add(rootView.getChildAt(i).getVisibility());
|
||||
rootView.getChildAt(i).setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
rootView.addView(exoPlayerView, layoutParams);
|
||||
} else {
|
||||
rootView.removeView(exoPlayerView);
|
||||
if (!rootViewChildrenOriginalVisibility.isEmpty()) {
|
||||
for (int i = 0; i < rootView.getChildCount(); i++) {
|
||||
rootView.getChildAt(i).setVisibility(rootViewChildrenOriginalVisibility.get(i));
|
||||
}
|
||||
addView(exoPlayerView, 0, layoutParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void enterPictureInPictureMode() {
|
||||
PictureInPictureParams _pipParams = null;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ArrayList<RemoteAction> actions = PictureInPictureUtil.getPictureInPictureActions(themedReactContext, isPaused, pictureInPictureReceiver);
|
||||
pictureInPictureParamsBuilder.setActions(actions);
|
||||
_pipParams = pictureInPictureParamsBuilder
|
||||
.setAspectRatio(PictureInPictureUtil.calcPictureInPictureAspectRatio(player))
|
||||
.build();
|
||||
}
|
||||
PictureInPictureUtil.enterPictureInPictureMode(themedReactContext, _pipParams);
|
||||
}
|
||||
|
||||
public void exitPictureInPictureMode() {
|
||||
Activity currentActivity = themedReactContext.getCurrentActivity();
|
||||
if (currentActivity == null) return;
|
||||
|
||||
View decorView = currentActivity.getWindow().getDecorView();
|
||||
ViewGroup rootView = decorView.findViewById(android.R.id.content);
|
||||
|
||||
if (!rootViewChildrenOriginalVisibility.isEmpty()) {
|
||||
if (exoPlayerView.getParent().equals(rootView)) rootView.removeView(exoPlayerView);
|
||||
for (int i = 0; i < rootView.getChildCount(); i++) {
|
||||
rootView.getChildAt(i).setVisibility(rootViewChildrenOriginalVisibility.get(i));
|
||||
}
|
||||
rootViewChildrenOriginalVisibility.clear();
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && currentActivity.isInPictureInPictureMode()) {
|
||||
currentActivity.moveTaskToBack(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void setMutedModifier(boolean muted) {
|
||||
this.muted = muted;
|
||||
if (player != null) {
|
||||
|
@ -32,7 +32,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
private const val PROP_SELECTED_TEXT_TRACK_TYPE = "type"
|
||||
private const val PROP_SELECTED_TEXT_TRACK_VALUE = "value"
|
||||
private const val PROP_PAUSED = "paused"
|
||||
private const val PROP_ENTER_PICTURE_IN_PICTURE_ON_LEAVE = "enterPictureInPictureOnLeave"
|
||||
private const val PROP_MUTED = "muted"
|
||||
private const val PROP_AUDIO_OUTPUT = "audioOutput"
|
||||
private const val PROP_VOLUME = "volume"
|
||||
@ -70,7 +69,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
|
||||
override fun onDropViewInstance(view: ReactExoplayerView) {
|
||||
view.cleanUpResources()
|
||||
view.exitPictureInPictureMode()
|
||||
ReactNativeVideoManager.getInstance().unregisterView(this)
|
||||
}
|
||||
|
||||
@ -156,11 +154,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
videoView.setMutedModifier(muted)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_ENTER_PICTURE_IN_PICTURE_ON_LEAVE, defaultBoolean = false)
|
||||
fun setEnterPictureInPictureOnLeave(videoView: ReactExoplayerView, enterPictureInPictureOnLeave: Boolean) {
|
||||
videoView.setEnterPictureInPictureOnLeave(enterPictureInPictureOnLeave)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_AUDIO_OUTPUT)
|
||||
fun setAudioOutput(videoView: ReactExoplayerView, audioOutput: String) {
|
||||
videoView.setAudioOutput(AudioOutput.get(audioOutput))
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.brentvatne.react
|
||||
|
||||
import com.brentvatne.common.toolbox.DebugLog
|
||||
import com.brentvatne.exoplayer.ReactExoplayerViewManager
|
||||
|
||||
/**
|
||||
* ReactNativeVideoManager is a singleton class which allows to manipulate / the global state of the app
|
||||
@ -22,13 +23,13 @@ class ReactNativeVideoManager : RNVPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
private var instanceList: ArrayList<Any> = ArrayList()
|
||||
private var instanceList: ArrayList<ReactExoplayerViewManager> = ArrayList()
|
||||
private var pluginList: ArrayList<RNVPlugin> = ArrayList()
|
||||
|
||||
/**
|
||||
* register a new ReactExoplayerViewManager in the managed list
|
||||
*/
|
||||
fun registerView(newInstance: Any) {
|
||||
fun registerView(newInstance: ReactExoplayerViewManager) {
|
||||
if (instanceList.size > 2) {
|
||||
DebugLog.d(TAG, "multiple Video displayed ?")
|
||||
}
|
||||
@ -38,7 +39,7 @@ class ReactNativeVideoManager : RNVPlugin {
|
||||
/**
|
||||
* unregister existing ReactExoplayerViewManager in the managed list
|
||||
*/
|
||||
fun unregisterView(newInstance: Any) {
|
||||
fun unregisterView(newInstance: ReactExoplayerViewManager) {
|
||||
instanceList.remove(newInstance)
|
||||
}
|
||||
|
||||
|
@ -65,20 +65,6 @@ class VideoManagerModule(reactContext: ReactApplicationContext?) : ReactContextB
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun enterPictureInPictureCmd(reactTag: Int) {
|
||||
performOnPlayerView(reactTag) {
|
||||
it?.enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun exitPictureInPictureCmd(reactTag: Int) {
|
||||
performOnPlayerView(reactTag) {
|
||||
it?.exitPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun setSourceCmd(reactTag: Int, source: ReadableMap?) {
|
||||
performOnPlayerView(reactTag) {
|
||||
|
@ -1,72 +0,0 @@
|
||||
package com.brentvatne.receiver
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.brentvatne.exoplayer.ReactExoplayerView
|
||||
import com.facebook.react.uimanager.ThemedReactContext
|
||||
|
||||
class PictureInPictureReceiver(private val view: ReactExoplayerView, private val context: ThemedReactContext) : BroadcastReceiver() {
|
||||
|
||||
companion object {
|
||||
const val ACTION_MEDIA_CONTROL = "rnv_media_control"
|
||||
const val EXTRA_CONTROL_TYPE = "rnv_control_type"
|
||||
|
||||
// The request code for play action PendingIntent.
|
||||
const val REQUEST_PLAY = 1
|
||||
|
||||
// The request code for pause action PendingIntent.
|
||||
const val REQUEST_PAUSE = 2
|
||||
|
||||
// The intent extra value for play action.
|
||||
const val CONTROL_TYPE_PLAY = 1
|
||||
|
||||
// The intent extra value for pause action.
|
||||
const val CONTROL_TYPE_PAUSE = 2
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
intent ?: return
|
||||
if (intent.action == ACTION_MEDIA_CONTROL) {
|
||||
when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) {
|
||||
CONTROL_TYPE_PLAY -> view.setPausedModifier(false)
|
||||
CONTROL_TYPE_PAUSE -> view.setPausedModifier(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setListener() {
|
||||
ContextCompat.registerReceiver(context, this, IntentFilter(ACTION_MEDIA_CONTROL), ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
|
||||
fun removeListener() {
|
||||
try {
|
||||
context.unregisterReceiver(this)
|
||||
} catch (e: Exception) {
|
||||
// ignore if already unregistered
|
||||
}
|
||||
}
|
||||
|
||||
fun getPipActionIntent(isPaused: Boolean): PendingIntent {
|
||||
val requestCode = if (isPaused) REQUEST_PLAY else REQUEST_PAUSE
|
||||
val controlType = if (isPaused) CONTROL_TYPE_PLAY else CONTROL_TYPE_PAUSE
|
||||
val flag =
|
||||
if (Build.VERSION.SDK_INT >=
|
||||
Build.VERSION_CODES.M
|
||||
) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
val intent = Intent(ACTION_MEDIA_CONTROL).putExtra(
|
||||
EXTRA_CONTROL_TYPE,
|
||||
controlType
|
||||
)
|
||||
intent.setPackage(context.packageName)
|
||||
return PendingIntent.getBroadcast(context, requestCode, intent, flag)
|
||||
}
|
||||
}
|
BIN
docs/bun.lockb
BIN
docs/bun.lockb
Binary file not shown.
@ -3,6 +3,6 @@
|
||||
}
|
||||
|
||||
.spanStyle {
|
||||
font-family: var(--font-orbitron);
|
||||
font-family: 'Orbitron';
|
||||
font-weight: 800;
|
||||
}
|
||||
|
@ -1,62 +0,0 @@
|
||||
.extraContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0.5rem;
|
||||
text-align: center;
|
||||
background-color: #171717;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.extraText {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.extraButton {
|
||||
width: 100%;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
background-color: #f9d85b;
|
||||
transition: transform 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.extraButton:hover {
|
||||
transform: scale(1.05);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
:is(html[class~=dark]) .extraContainer {
|
||||
background-color: #87ccef;
|
||||
}
|
||||
|
||||
:is(html[class~=dark]) .extraText {
|
||||
color: #171717;
|
||||
}
|
||||
|
||||
:is(html[class~=dark]) .extraButton {
|
||||
background-color: #171717;
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.visibleOnLarge {
|
||||
display: inherit;
|
||||
}
|
||||
.visibleOnSmall {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1279px) {
|
||||
.visibleOnLarge {
|
||||
display: none;
|
||||
}
|
||||
.visibleOnSmall {
|
||||
display: flex;
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './TWGBadge.module.css';
|
||||
|
||||
interface TWGBadgeProps {
|
||||
visibleOnLarge?: boolean;
|
||||
}
|
||||
|
||||
const TWGBadge = ({visibleOnLarge}: TWGBadgeProps) => {
|
||||
const visibilityClass = visibleOnLarge
|
||||
? styles.visibleOnLarge
|
||||
: styles.visibleOnSmall;
|
||||
|
||||
return (
|
||||
<div className={[styles.extraContainer, visibilityClass].join(' ')}>
|
||||
<span className={styles.extraText}>We are TheWidlarzGroup</span>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs#Contact"
|
||||
className={styles.extraButton}
|
||||
rel="noreferrer">
|
||||
Premium support →
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TWGBadge;
|
@ -1,7 +0,0 @@
|
||||
import {Orbitron} from 'next/font/google';
|
||||
|
||||
export const orbitron = Orbitron({
|
||||
display: 'swap',
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '900'],
|
||||
});
|
2
docs/next-env.d.ts
vendored
2
docs/next-env.d.ts
vendored
@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
@ -8,7 +8,7 @@
|
||||
"build": "bun next build"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.2.20",
|
||||
"next": "^13.5.4",
|
||||
"nextra": "^2.13.2",
|
||||
"nextra-theme-docs": "^2.13.2",
|
||||
"react": "^18.2.0",
|
||||
|
@ -1,14 +0,0 @@
|
||||
import {orbitron} from '../font';
|
||||
|
||||
export default function Nextra({Component, pageProps}) {
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
:root {
|
||||
--font-orbitron: ${orbitron.style.fontFamily};
|
||||
}
|
||||
`}</style>
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -22,14 +22,5 @@
|
||||
"newWindow": true,
|
||||
"href": "https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples"
|
||||
},
|
||||
"projects": "Useful projects",
|
||||
"separator_enterprise": {
|
||||
"type": "separator",
|
||||
"title": ""
|
||||
},
|
||||
"enterprise_support": {
|
||||
"title": "Enterprise Support",
|
||||
"newWindow": true,
|
||||
"href": "https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs#Contact"
|
||||
}
|
||||
"projects": "Useful projects"
|
||||
}
|
@ -317,7 +317,7 @@ Example:
|
||||
|
||||
### `onPictureInPictureStatusChanged`
|
||||
|
||||
<PlatformsList types={['iOS', 'Android', 'web']} />
|
||||
<PlatformsList types={['iOS']} />
|
||||
|
||||
Callback function that is called when picture in picture becomes active or inactive.
|
||||
|
||||
|
@ -78,56 +78,6 @@ Future:
|
||||
- Will support more formats in the future through options
|
||||
- Will support custom directory and file name through options
|
||||
|
||||
### `enterPictureInPicture`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
`enterPictureInPicture()`
|
||||
|
||||
To use this feature on Android with Expo, you must set 'enableAndroidPictureInPicture' true within expo plugin config (app.json)
|
||||
|
||||
```json
|
||||
"plugins": [
|
||||
[
|
||||
"react-native-video",
|
||||
{
|
||||
"enableAndroidPictureInPicture": true,
|
||||
}
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
To use this feature on Android with Bare React Native, you must:
|
||||
|
||||
- [Declare PiP support](https://developer.android.com/develop/ui/views/picture-in-picture#declaring) in your AndroidManifest.xml
|
||||
- setting `android:supportsPictureInPicture` to `true`
|
||||
|
||||
```xml
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
...
|
||||
android:supportsPictureInPicture="true">
|
||||
```
|
||||
|
||||
NOTE: Foreground picture in picture is not supported on Android due to limitations of react native (Single Activity App). So, If you call `enterPictureInPicture`, application will switch to background on Android.
|
||||
NOTE: Video ads cannot start when you are using the PIP on iOS (more info available at [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads)). If you are using custom controls, you must hide your PIP button when you receive the `STARTED` event from `onReceiveAdEvent` and show it again when you receive the `ALL_ADS_COMPLETED` event.
|
||||
|
||||
### `exitPictureInPicture`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
`exitPictureInPicture()`
|
||||
|
||||
Exits the active picture in picture; if it is not active, the function call is ignored.
|
||||
|
||||
### `restoreUserInterfaceForPictureInPictureStopCompleted`
|
||||
|
||||
<PlatformsList types={['iOS']} />
|
||||
|
||||
`restoreUserInterfaceForPictureInPictureStopCompleted(restored)`
|
||||
|
||||
This function corresponds to the completion handler in Apple's [restoreUserInterfaceForPictureInPictureStop](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). IMPORTANT: This function must be called after `onRestoreUserInterfaceForPictureInPictureStop` is called.
|
||||
|
||||
### `seek`
|
||||
|
||||
<PlatformsList types={['All']} />
|
||||
|
@ -252,42 +252,6 @@ To setup DRM please follow [this guide](/component/drm)
|
||||
|
||||
> ⚠️ DRM is not supported on visionOS yet
|
||||
|
||||
### `enterPictureInPictureOnLeave`
|
||||
|
||||
<PlatformsList types={['iOS', 'Android']} />
|
||||
|
||||
Determine whether to play media as a picture in picture when the user goes to the background.
|
||||
|
||||
- **false (default)** - Don't not play as picture in picture
|
||||
- **true** - Play the media as picture in picture
|
||||
|
||||
To use this feature on Android with Expo, you must set 'enableAndroidPictureInPicture' true within expo plugin config (app.json)
|
||||
|
||||
```json
|
||||
"plugins": [
|
||||
[
|
||||
"react-native-video",
|
||||
{
|
||||
"enableAndroidPictureInPicture": true,
|
||||
}
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
To use this feature on Android with Bare React Native, you must:
|
||||
|
||||
- [Declare PiP support](https://developer.android.com/develop/ui/views/picture-in-picture#declaring) in your AndroidManifest.xml
|
||||
- setting `android:supportsPictureInPicture` to `true`
|
||||
|
||||
```xml
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
...
|
||||
android:supportsPictureInPicture="true">
|
||||
```
|
||||
|
||||
NOTE: Video ads cannot start when you are using the PIP on iOS (more info available at [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads)). If you are using custom controls, you must hide your PIP button when you receive the `STARTED` event from `onReceiveAdEvent` and show it again when you receive the `ALL_ADS_COMPLETED` event.
|
||||
|
||||
### `filter`
|
||||
|
||||
<PlatformsList types={['iOS', 'visionOS']} />
|
||||
@ -461,6 +425,17 @@ Controls whether the media is paused
|
||||
- **false (default)** - Don't pause the media
|
||||
- **true** - Pause the media
|
||||
|
||||
### `pictureInPicture`
|
||||
|
||||
<PlatformsList types={['iOS']} />
|
||||
|
||||
Determine whether the media should played as picture in picture.
|
||||
|
||||
- **false (default)** - Don't not play as picture in picture
|
||||
- **true** - Play the media as picture in picture
|
||||
|
||||
NOTE: Video ads cannot start when you are using the PIP on iOS (more info available at [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads)). If you are using custom controls, you must hide your PIP button when you receive the `STARTED` event from `onReceiveAdEvent` and show it again when you receive the `ALL_ADS_COMPLETED` event.
|
||||
|
||||
### `playInBackground`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||
|
@ -13,7 +13,7 @@ It allows to stream video files (m3u, mpd, mp4, ...) inside your react native ap
|
||||
- Subtitles (embeded or side loaded)
|
||||
- DRM support
|
||||
- Client side Ads insertion (via google IMA)
|
||||
- Pip
|
||||
- Pip (ios)
|
||||
- Embedded playback controls
|
||||
- And much more
|
||||
|
||||
|
@ -38,4 +38,3 @@ It's useful when you are using `expo` managed workflow (expo prebuild) as it wil
|
||||
| enableADSExtension | boolean | false | Add required changes to use ads extension for video player |
|
||||
| enableCacheExtension | boolean | false | Add required changes to use cache extension for video player on iOS |
|
||||
| androidExtensions | object | {} | You can enable/disable extensions as per your requirement - this allow to reduce library size on android |
|
||||
| enableAndroidPictureInPicture | boolean | false | Apply configs to be able to use Picture-in-picture on android |
|
@ -2,11 +2,10 @@
|
||||
This page links other open source projects which can be useful for your player implementation. <br>
|
||||
If you have a project which can be useful for other users, feel free to open a PR to add it here.
|
||||
|
||||
## Our (TheWidlarzGroup) libraries
|
||||
- [react-native-video-player](https://github.com/TheWidlarzGroup/react-native-video-player): Our video player UI library
|
||||
## UI over react-native-video
|
||||
- [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls): First reference player UI
|
||||
- [react-native-media-console](https://github.com/criszz77/react-native-media-console): React-native-video-controls updated and rewritten in typescript
|
||||
- [react-native-corner-video](https://github.com/Lg0gs/react-native-corner-video): A floating video player
|
||||
|
||||
## Community libraries
|
||||
- [react-native-corner-video](https://github.com/Lg0gs/react-native-corner-video): A floating video player
|
||||
- [react-native-track-player](https://github.com/doublesymmetry/react-native-track-player): A toolbox for audio playback
|
||||
- [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls): Video player UI
|
||||
- [react-native-media-console](https://github.com/criszz77/react-native-media-console): React-native-video-controls updated and rewritten in typescript
|
||||
## Other tools
|
||||
- [react-native-track-player](https://github.com/doublesymmetry/react-native-track-player): A toolbox to control player over media session
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import TWGBadge from './components/TWGBadge/TWGBadge';
|
||||
|
||||
export default {
|
||||
head: (
|
||||
@ -27,6 +26,12 @@ export default {
|
||||
content="https://docs.thewidlarzgroup.com/react-native-video/thumbnail.jpg"
|
||||
/>
|
||||
<meta name="twitter:image:alt" content="React Native Video" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
@ -34,15 +39,13 @@ export default {
|
||||
/>
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-4YEWQH5ZHS"
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-PM2TQQQMDN"
|
||||
/>
|
||||
<script>
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
{`window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-4YEWQH5ZHS');
|
||||
`}
|
||||
gtag('config', 'G-PM2TQQQMDN');`}
|
||||
</script>
|
||||
</>
|
||||
),
|
||||
@ -56,15 +59,6 @@ export default {
|
||||
},
|
||||
docsRepositoryBase:
|
||||
'https://github.com/TheWidlarzGroup/react-native-video/tree/master/docs/',
|
||||
main: ({children}) => (
|
||||
<>
|
||||
{children}
|
||||
<TWGBadge visibleOnLarge={false} />
|
||||
</>
|
||||
),
|
||||
toc: {
|
||||
extraContent: <TWGBadge visibleOnLarge={true} />,
|
||||
},
|
||||
footer: {
|
||||
text: (
|
||||
<span>
|
||||
@ -72,6 +66,61 @@ export default {
|
||||
</span>
|
||||
),
|
||||
},
|
||||
toc: {
|
||||
extraContent: (
|
||||
<>
|
||||
<style>{`
|
||||
:is(html[class~=dark]) .extra-container {
|
||||
background-color: #87ccef;
|
||||
}
|
||||
:is(html[class~=dark]) .extra-text {
|
||||
color: #171717;
|
||||
}
|
||||
:is(html[class~=dark]) .extra-button {
|
||||
background-color: #171717;
|
||||
}
|
||||
.extra-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0.5rem;
|
||||
text-align: center;
|
||||
background-color: #171717;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.extra-text {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
.extra-button {
|
||||
width: 100%;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
background-color: #f9d85b;
|
||||
transition: transform 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
.extra-button:hover {
|
||||
transform: scale(1.05);
|
||||
background-color: #fff;
|
||||
}
|
||||
`}</style>
|
||||
<div className="extra-container">
|
||||
<span className="extra-text">We are TheWidlarzGroup</span>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs#Contact"
|
||||
className="extra-button"
|
||||
rel="noreferrer">
|
||||
Premium support →
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
useNextSeoProps() {
|
||||
return {
|
||||
|
@ -89,10 +89,6 @@ yarn start
|
||||
|
||||
## [Expo](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples/expo)
|
||||
|
||||
> [!NOTE]
|
||||
> Additionally, there is a great example of a TV app available in the [AmazonAppDev/react-native-multi-tv-app-sample](https://github.com/AmazonAppDev/react-native-multi-tv-app-sample) repository.
|
||||
It provides a sample application for Android TV, Fire TV, tvOS, and the web. The app includes customizable drawer navigation, a content grid, a hero header, and an integrated video player. Built with Expo, it serves as a great starting point for cross-platform TV app development.
|
||||
|
||||
### Configuration
|
||||
|
||||
#### Expo Plugin
|
||||
|
@ -4,16 +4,16 @@
|
||||
|
||||
class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMALinkOpenerDelegate {
|
||||
private weak var _video: RCTVideo?
|
||||
private var _isPictureInPictureActive: () -> Bool
|
||||
private var _pipEnabled: () -> Bool
|
||||
|
||||
/* Entry point for the SDK. Used to make ad requests. */
|
||||
private var adsLoader: IMAAdsLoader!
|
||||
/* Main point of interaction with the SDK. Created by the SDK as the result of an ad request. */
|
||||
private var adsManager: IMAAdsManager!
|
||||
|
||||
init(video: RCTVideo!, isPictureInPictureActive: @escaping () -> Bool) {
|
||||
init(video: RCTVideo!, pipEnabled: @escaping () -> Bool) {
|
||||
_video = video
|
||||
_isPictureInPictureActive = isPictureInPictureActive
|
||||
_pipEnabled = pipEnabled
|
||||
|
||||
super.init()
|
||||
}
|
||||
@ -103,7 +103,7 @@
|
||||
}
|
||||
// Play each ad once it has been loaded
|
||||
if event.type == IMAAdEventType.LOADED {
|
||||
if _isPictureInPictureActive() {
|
||||
if _pipEnabled() {
|
||||
return
|
||||
}
|
||||
adsManager.start()
|
||||
|
@ -11,9 +11,7 @@ import React
|
||||
private var _onPictureInPictureExit: (() -> Void)?
|
||||
private var _onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)?
|
||||
private var _restoreUserInterfaceForPIPStopCompletionHandler: ((Bool) -> Void)?
|
||||
private var _isPictureInPictureActive: Bool {
|
||||
return _pipController?.isPictureInPictureActive ?? false
|
||||
}
|
||||
private var _isActive = false
|
||||
|
||||
init(
|
||||
_ onPictureInPictureEnter: (() -> Void)? = nil,
|
||||
@ -69,35 +67,23 @@ import React
|
||||
_pipController = nil
|
||||
}
|
||||
|
||||
func enterPictureInPicture() {
|
||||
func setPictureInPicture(_ isActive: Bool) {
|
||||
if _isActive == isActive {
|
||||
return
|
||||
}
|
||||
_isActive = isActive
|
||||
|
||||
guard let _pipController else { return }
|
||||
if !_isPictureInPictureActive {
|
||||
|
||||
if _isActive && !_pipController.isPictureInPictureActive {
|
||||
DispatchQueue.main.async {
|
||||
_pipController.startPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
func exitPictureInPicture() {
|
||||
guard let _pipController else { return }
|
||||
if _isPictureInPictureActive {
|
||||
let state = UIApplication.shared.applicationState
|
||||
if state == .background || state == .inactive {
|
||||
deinitPipController()
|
||||
_onPictureInPictureExit?()
|
||||
_onRestoreUserInterfaceForPictureInPictureStop?()
|
||||
} else {
|
||||
} else if !_isActive && _pipController.isPictureInPictureActive {
|
||||
DispatchQueue.main.async {
|
||||
_pipController.stopPictureInPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
class RCTPictureInPicture: NSObject {
|
||||
public let _pipController: NSObject? = nil
|
||||
|
||||
func setRestoreUserInterfaceForPIPStopCompletionHandler(_: Bool) {}
|
||||
func setupPipController(_: AVPlayerLayer?) {}
|
||||
func deinitPipController() {}
|
||||
func enterPictureInPicture() {}
|
||||
func exitPictureInPicture() {}
|
||||
}
|
||||
#endif
|
||||
|
@ -27,7 +27,7 @@ enum RCTPlayerOperations {
|
||||
|
||||
var selectedTrackIndex: Int = RCTVideoUnset
|
||||
|
||||
if (type == "disabled") || (type == "none") || (type == "") {
|
||||
if type == "disabled" {
|
||||
// Select the last text index which is the disabled text track
|
||||
selectedTrackIndex = trackCount - firstTextIndex
|
||||
} else if type == "language" {
|
||||
@ -92,7 +92,7 @@ enum RCTPlayerOperations {
|
||||
return
|
||||
}
|
||||
|
||||
if (type == "disabled") || (type == "none") || (type == "") {
|
||||
if type == "disabled" {
|
||||
// Do nothing. We want to ensure option is nil
|
||||
} else if (type == "language") || (type == "title") {
|
||||
let value = criteria?.value as? String
|
||||
|
@ -63,11 +63,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _showNotificationControls = false
|
||||
// Buffer last bitrate value received. Initialized to -2 to ensure -1 (sometimes reported by AVPlayer) is not missed
|
||||
private var _lastBitrate = -2.0
|
||||
private var _enterPictureInPictureOnLeave = false {
|
||||
private var _pictureInPictureEnabled = false {
|
||||
didSet {
|
||||
#if os(iOS)
|
||||
if isPictureInPictureActive() { return }
|
||||
if _enterPictureInPictureOnLeave {
|
||||
if _pictureInPictureEnabled {
|
||||
initPictureinPicture()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||
} else {
|
||||
@ -102,7 +101,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private let _videoCache: RCTVideoCachingHandler = .init()
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
private var _pip: RCTPictureInPicture?
|
||||
#endif
|
||||
|
||||
// Events
|
||||
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||
@ -166,7 +167,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
onRestoreUserInterfaceForPictureInPictureStop?([:])
|
||||
}
|
||||
|
||||
func isPictureInPictureActive() -> Bool {
|
||||
func isPipEnabled() -> Bool {
|
||||
return _pictureInPictureEnabled
|
||||
}
|
||||
|
||||
func isPipActive() -> Bool {
|
||||
#if os(iOS)
|
||||
return _pip?._pipController?.isPictureInPictureActive == true
|
||||
#else
|
||||
@ -176,7 +181,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
func initPictureinPicture() {
|
||||
#if os(iOS)
|
||||
if _pip == nil {
|
||||
_pip = RCTPictureInPicture({ [weak self] in
|
||||
self?._onPictureInPictureEnter()
|
||||
}, { [weak self] in
|
||||
@ -184,9 +188,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}, { [weak self] in
|
||||
self?.onRestoreUserInterfaceForPictureInPictureStop?([:])
|
||||
})
|
||||
}
|
||||
|
||||
if _playerLayer != nil && !_controls && _pip?._pipController == nil {
|
||||
if _playerLayer != nil && !_controls {
|
||||
_pip?.setupPipController(_playerLayer)
|
||||
}
|
||||
#else
|
||||
@ -198,14 +201,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
|
||||
ReactNativeVideoManager.shared.registerView(newInstance: self)
|
||||
#if USE_GOOGLE_IMA
|
||||
_imaAdsManager = RCTIMAAdsManager(video: self, isPictureInPictureActive: isPictureInPictureActive)
|
||||
_imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled)
|
||||
#endif
|
||||
|
||||
_eventDispatcher = eventDispatcher
|
||||
|
||||
#if os(iOS)
|
||||
if _enterPictureInPictureOnLeave {
|
||||
if _pictureInPictureEnabled {
|
||||
initPictureinPicture()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||
} else {
|
||||
_playerViewController?.allowsPictureInPicturePlayback = false
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -237,20 +243,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(screenWillLock),
|
||||
name: UIApplication.protectedDataWillBecomeUnavailableNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(screenDidUnlock),
|
||||
name: UIApplication.protectedDataDidBecomeAvailableNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(audioRouteChanged(notification:)),
|
||||
@ -266,7 +258,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
#if USE_GOOGLE_IMA
|
||||
_imaAdsManager = RCTIMAAdsManager(video: self, isPictureInPictureActive: isPictureInPictureActive)
|
||||
_imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -322,12 +314,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
@objc
|
||||
func applicationDidEnterBackground(notification _: NSNotification!) {
|
||||
if !_paused && isPictureInPictureActive() {
|
||||
_player?.play()
|
||||
_player?.rate = _rate
|
||||
}
|
||||
let isExternalPlaybackActive = getIsExternalPlaybackActive()
|
||||
if !_playInBackground || isExternalPlaybackActive || isPictureInPictureActive() { return }
|
||||
if !_playInBackground || isExternalPlaybackActive || isPipActive() { return }
|
||||
// Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html
|
||||
_playerLayer?.player = nil
|
||||
_playerViewController?.player = nil
|
||||
@ -340,24 +328,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_playerViewController?.player = _player
|
||||
}
|
||||
|
||||
@objc
|
||||
func screenWillLock() {
|
||||
let isActiveBackgroundPip = isPictureInPictureActive() && UIApplication.shared.applicationState != .active
|
||||
if _playInBackground || !_isPlaying || !isActiveBackgroundPip { return }
|
||||
|
||||
_player?.pause()
|
||||
_player?.rate = 0.0
|
||||
}
|
||||
|
||||
@objc
|
||||
func screenDidUnlock() {
|
||||
let isActiveBackgroundPip = isPictureInPictureActive() && UIApplication.shared.applicationState != .active
|
||||
if _paused || !isActiveBackgroundPip { return }
|
||||
|
||||
_player?.play()
|
||||
_player?.rate = _rate
|
||||
}
|
||||
|
||||
// MARK: - Audio events
|
||||
|
||||
@objc
|
||||
@ -741,16 +711,19 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
@objc
|
||||
func setEnterPictureInPictureOnLeave(_ enterPictureInPictureOnLeave: Bool) {
|
||||
func setPictureInPicture(_ pictureInPicture: Bool) {
|
||||
#if os(iOS)
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
do {
|
||||
try audioSession.setCategory(.playback)
|
||||
try audioSession.setActive(true, options: [])
|
||||
} catch {}
|
||||
if _enterPictureInPictureOnLeave != enterPictureInPictureOnLeave {
|
||||
_enterPictureInPictureOnLeave = enterPictureInPictureOnLeave
|
||||
if pictureInPicture {
|
||||
_pictureInPictureEnabled = true
|
||||
} else {
|
||||
_pictureInPictureEnabled = false
|
||||
}
|
||||
_pip?.setPictureInPicture(pictureInPicture)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -844,16 +817,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
self._playerObserver.addTimeObserverIfNotSet()
|
||||
if !wasPaused {
|
||||
self.setPaused(false)
|
||||
}
|
||||
self.setPaused(self._paused)
|
||||
|
||||
let currentTimeAfterSeek = CMTimeGetSeconds(item.currentTime())
|
||||
|
||||
let newCurrentTime = NSNumber(value: Float(currentTimeAfterSeek))
|
||||
let newCurrentTime = NSNumber(value: Float(CMTimeGetSeconds(item.currentTime())))
|
||||
self.onVideoSeekComplete?(["currentTime": newCurrentTime,
|
||||
"seekTime": time,
|
||||
"target": self.reactTag as Any])
|
||||
|
||||
}
|
||||
|
||||
player.seek(to: seekTime, toleranceBefore: toleranceTime, toleranceAfter: toleranceTime, completionHandler: seekCompletionHandler)
|
||||
@ -1140,11 +1110,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
viewController.view.frame = self.bounds
|
||||
viewController.player = player
|
||||
if #available(tvOS 14.0, *) {
|
||||
viewController.allowsPictureInPicturePlayback = _enterPictureInPictureOnLeave
|
||||
viewController.allowsPictureInPicturePlayback = _pictureInPictureEnabled
|
||||
}
|
||||
#if os(iOS)
|
||||
viewController.allowsPictureInPicturePlayback = _enterPictureInPictureOnLeave
|
||||
#endif
|
||||
return viewController
|
||||
}
|
||||
|
||||
@ -1164,7 +1131,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
self.layer.needsDisplayOnBoundsChange = true
|
||||
#if os(iOS)
|
||||
if _enterPictureInPictureOnLeave {
|
||||
if _pictureInPictureEnabled {
|
||||
_pip?.setupPipController(_playerLayer)
|
||||
}
|
||||
#endif
|
||||
@ -1558,7 +1525,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
guard _isPlaying != isPlaying else { return }
|
||||
_isPlaying = isPlaying
|
||||
_paused = !isPlaying
|
||||
onVideoPlaybackStateChanged?(["isPlaying": isPlaying, "isSeeking": self._pendingSeek == true, "target": reactTag as Any])
|
||||
}
|
||||
|
||||
@ -1735,31 +1701,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func enterPictureInPicture() {
|
||||
if _pip?._pipController == nil {
|
||||
initPictureinPicture()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||
}
|
||||
_pip?.enterPictureInPicture()
|
||||
}
|
||||
|
||||
@objc
|
||||
func exitPictureInPicture() {
|
||||
guard isPictureInPictureActive() else { return }
|
||||
|
||||
_pip?.exitPictureInPicture()
|
||||
#if os(iOS)
|
||||
if _enterPictureInPictureOnLeave {
|
||||
initPictureinPicture()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||
} else {
|
||||
_pip?.deinitPipController()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = false
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Workaround for #3418 - https://github.com/TheWidlarzGroup/react-native-video/issues/3418#issuecomment-2043508862
|
||||
@objc
|
||||
func setOnClick(_: Any) {}
|
||||
|
@ -23,7 +23,7 @@ RCT_EXPORT_VIEW_PROPERTY(playInBackground, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(preventsDisplaySleepDuringVideoPlayback, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(preferredForwardBufferDuration, float);
|
||||
RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(enterPictureInPictureOnLeave, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(pictureInPicture, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(mixWithOthers, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(rate, float);
|
||||
@ -74,8 +74,6 @@ RCT_EXTERN_METHOD(setLicenseResultErrorCmd : (nonnull NSNumber*)reactTag error :
|
||||
RCT_EXTERN_METHOD(setPlayerPauseStateCmd : (nonnull NSNumber*)reactTag paused : (nonnull BOOL)paused)
|
||||
RCT_EXTERN_METHOD(setVolumeCmd : (nonnull NSNumber*)reactTag volume : (nonnull float*)volume)
|
||||
RCT_EXTERN_METHOD(setFullScreenCmd : (nonnull NSNumber*)reactTag fullscreen : (nonnull BOOL)fullScreen)
|
||||
RCT_EXTERN_METHOD(enterPictureInPictureCmd : (nonnull NSNumber*)reactTag)
|
||||
RCT_EXTERN_METHOD(exitPictureInPictureCmd : (nonnull NSNumber*)reactTag)
|
||||
RCT_EXTERN_METHOD(setSourceCmd : (nonnull NSNumber*)reactTag source : (NSDictionary*)source)
|
||||
|
||||
RCT_EXTERN_METHOD(save
|
||||
|
@ -72,20 +72,6 @@ class RCTVideoManager: RCTViewManager {
|
||||
})
|
||||
}
|
||||
|
||||
@objc(enterPictureInPictureCmd:)
|
||||
func enterPictureInPictureCmd(_ reactTag: NSNumber) {
|
||||
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||
videoView?.enterPictureInPicture()
|
||||
})
|
||||
}
|
||||
|
||||
@objc(exitPictureInPictureCmd:)
|
||||
func exitPictureInPictureCmd(_ reactTag: NSNumber) {
|
||||
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||
videoView?.exitPictureInPicture()
|
||||
})
|
||||
}
|
||||
|
||||
@objc(setSourceCmd:source:)
|
||||
func setSourceCmd(_ reactTag: NSNumber, source: NSDictionary) {
|
||||
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||
|
@ -6,31 +6,33 @@
|
||||
import Foundation
|
||||
|
||||
public class ReactNativeVideoManager: RNVPlugin {
|
||||
private let expectedMaxVideoCount = 2
|
||||
private let expectedMaxVideoCount = 10
|
||||
|
||||
// create a private initializer
|
||||
private init() {}
|
||||
|
||||
public static let shared: ReactNativeVideoManager = .init()
|
||||
|
||||
private var instanceCount = 0
|
||||
private var pluginList: [RNVPlugin] = Array()
|
||||
var instanceList: [RCTVideo] = Array()
|
||||
var pluginList: [RNVPlugin] = Array()
|
||||
|
||||
/**
|
||||
* register a new view
|
||||
* register a new ReactExoplayerViewManager in the managed list
|
||||
*/
|
||||
func registerView(newInstance _: RCTVideo) {
|
||||
if instanceCount > expectedMaxVideoCount {
|
||||
func registerView(newInstance: RCTVideo) {
|
||||
if instanceList.count > expectedMaxVideoCount {
|
||||
DebugLog("multiple Video displayed ?")
|
||||
}
|
||||
instanceCount += 1
|
||||
instanceList.append(newInstance)
|
||||
}
|
||||
|
||||
/**
|
||||
* unregister existing view
|
||||
* unregister existing ReactExoplayerViewManager in the managed list
|
||||
*/
|
||||
func unregisterView(newInstance _: RCTVideo) {
|
||||
instanceCount -= 1
|
||||
func unregisterView(newInstance: RCTVideo) {
|
||||
if let i = instanceList.firstIndex(of: newInstance) {
|
||||
instanceList.remove(at: i)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-native-video",
|
||||
"version": "6.9.1",
|
||||
"version": "6.8.2",
|
||||
"description": "A <Video /> element for react-native",
|
||||
"main": "lib/index",
|
||||
"source": "src/index.ts",
|
||||
|
@ -411,40 +411,6 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
[setFullScreen],
|
||||
);
|
||||
|
||||
const enterPictureInPicture = useCallback(async () => {
|
||||
if (!nativeRef.current) {
|
||||
console.warn('Video Component is not mounted');
|
||||
return;
|
||||
}
|
||||
|
||||
const _enterPictureInPicture = () => {
|
||||
NativeVideoManager.enterPictureInPictureCmd(getReactTag(nativeRef));
|
||||
};
|
||||
|
||||
Platform.select({
|
||||
ios: _enterPictureInPicture,
|
||||
android: _enterPictureInPicture,
|
||||
default: () => {},
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const exitPictureInPicture = useCallback(async () => {
|
||||
if (!nativeRef.current) {
|
||||
console.warn('Video Component is not mounted');
|
||||
return;
|
||||
}
|
||||
|
||||
const _exitPictureInPicture = () => {
|
||||
NativeVideoManager.exitPictureInPictureCmd(getReactTag(nativeRef));
|
||||
};
|
||||
|
||||
Platform.select({
|
||||
ios: _exitPictureInPicture,
|
||||
android: _exitPictureInPicture,
|
||||
default: () => {},
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const save = useCallback((options: object) => {
|
||||
// VideoManager.save can be null on android & windows
|
||||
if (Platform.OS !== 'ios') {
|
||||
@ -700,8 +666,6 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
setVolume,
|
||||
getCurrentPosition,
|
||||
setFullScreen,
|
||||
enterPictureInPicture,
|
||||
exitPictureInPicture,
|
||||
setSource,
|
||||
}),
|
||||
[
|
||||
@ -715,8 +679,6 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
setVolume,
|
||||
getCurrentPosition,
|
||||
setFullScreen,
|
||||
enterPictureInPicture,
|
||||
exitPictureInPicture,
|
||||
setSource,
|
||||
],
|
||||
);
|
||||
|
@ -50,7 +50,6 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
onVolumeChange,
|
||||
onEnd,
|
||||
onPlaybackStateChanged,
|
||||
onPictureInPictureStatusChanged,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@ -181,31 +180,6 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
[setFullScreen],
|
||||
);
|
||||
|
||||
const enterPictureInPicture = useCallback(() => {
|
||||
try {
|
||||
if (!nativeRef.current) {
|
||||
console.error('Video Component is not mounted');
|
||||
} else {
|
||||
nativeRef.current.requestPictureInPicture();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const exitPictureInPicture = useCallback(() => {
|
||||
if (
|
||||
nativeRef.current &&
|
||||
nativeRef.current === document.pictureInPictureElement
|
||||
) {
|
||||
try {
|
||||
document.exitPictureInPicture();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
@ -219,8 +193,6 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
dismissFullscreenPlayer,
|
||||
setFullScreen,
|
||||
save: unsupported,
|
||||
enterPictureInPicture,
|
||||
exitPictureInPicture,
|
||||
restoreUserInterfaceForPictureInPictureStopCompleted: unsupported,
|
||||
nativeHtmlVideoRef: nativeRef,
|
||||
}),
|
||||
@ -236,8 +208,6 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
presentFullscreenPlayer,
|
||||
dismissFullscreenPlayer,
|
||||
setFullScreen,
|
||||
enterPictureInPicture,
|
||||
exitPictureInPicture,
|
||||
],
|
||||
);
|
||||
|
||||
@ -282,27 +252,6 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
nativeRef.current.playbackRate = rate;
|
||||
}, [rate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
typeof onPictureInPictureStatusChanged !== 'function' ||
|
||||
!nativeRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const onEnterPip = () =>
|
||||
onPictureInPictureStatusChanged({isActive: true});
|
||||
const onLeavePip = () =>
|
||||
onPictureInPictureStatusChanged({isActive: false});
|
||||
|
||||
const video = nativeRef.current;
|
||||
video.addEventListener('enterpictureinpicture', onEnterPip);
|
||||
video.addEventListener('leavepictureinpicture', onLeavePip);
|
||||
return () => {
|
||||
video.removeEventListener('enterpictureinpicture', onEnterPip);
|
||||
video.removeEventListener('leavepictureinpicture', onLeavePip);
|
||||
};
|
||||
}, [onPictureInPictureStatusChanged]);
|
||||
|
||||
useMediaSession(src?.metadata, nativeRef, showNotificationControls);
|
||||
|
||||
return (
|
||||
|
@ -5,11 +5,6 @@ export type ConfigProps = {
|
||||
* @default false
|
||||
*/
|
||||
enableNotificationControls?: boolean;
|
||||
/**
|
||||
* Apply configs to be able to use Picture-in-picture on Android.
|
||||
* @default false
|
||||
*/
|
||||
enableAndroidPictureInPicture?: boolean;
|
||||
/**
|
||||
* Whether to enable background audio feature.
|
||||
* @default false
|
||||
|
@ -1,31 +0,0 @@
|
||||
import {
|
||||
AndroidConfig,
|
||||
withAndroidManifest,
|
||||
type ConfigPlugin,
|
||||
} from '@expo/config-plugins';
|
||||
|
||||
export const withAndroidPictureInPicture: ConfigPlugin<boolean> = (
|
||||
config,
|
||||
enableAndroidPictureInPicture,
|
||||
) => {
|
||||
return withAndroidManifest(config, (_config) => {
|
||||
if (!enableAndroidPictureInPicture) {
|
||||
return _config;
|
||||
}
|
||||
|
||||
const mainActivity = AndroidConfig.Manifest.getMainActivity(
|
||||
_config.modResults,
|
||||
);
|
||||
|
||||
if (!mainActivity) {
|
||||
console.warn(
|
||||
'AndroidManifest.xml is missing an <activity android:name=".MainActivity" /> element - skipping adding Picture-In-Picture related config.',
|
||||
);
|
||||
return _config;
|
||||
}
|
||||
|
||||
mainActivity.$['android:supportsPictureInPicture'] = 'true';
|
||||
|
||||
return _config;
|
||||
});
|
||||
};
|
@ -2,7 +2,6 @@ import {type ConfigPlugin, createRunOncePlugin} from '@expo/config-plugins';
|
||||
import type {ConfigProps} from './@types';
|
||||
import {withNotificationControls} from './withNotificationControls';
|
||||
import {withAndroidExtensions} from './withAndroidExtensions';
|
||||
import {withAndroidPictureInPicture} from './withAndroidPictureInPicture';
|
||||
import {withAds} from './withAds';
|
||||
import {withBackgroundAudio} from './withBackgroundAudio';
|
||||
import {withPermissions} from '@expo/config-plugins/build/android/Permissions';
|
||||
@ -22,13 +21,6 @@ const withRNVideo: ConfigPlugin<ConfigProps> = (config, props = {}) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (props.enableAndroidPictureInPicture) {
|
||||
config = withAndroidPictureInPicture(
|
||||
config,
|
||||
props.enableAndroidPictureInPicture,
|
||||
);
|
||||
}
|
||||
|
||||
if (props.androidExtensions != null) {
|
||||
config = withAndroidExtensions(config, props.androidExtensions);
|
||||
}
|
||||
|
@ -23,8 +23,6 @@ export interface VideoManagerType {
|
||||
setFullScreenCmd: (reactTag: Int32, fullScreen: boolean) => Promise<void>;
|
||||
setSourceCmd: (reactTag: Int32, source?: UnsafeObject) => Promise<void>;
|
||||
setVolumeCmd: (reactTag: Int32, volume: number) => Promise<void>;
|
||||
enterPictureInPictureCmd: (reactTag: number) => Promise<void>;
|
||||
exitPictureInPictureCmd: (reactTag: number) => Promise<void>;
|
||||
save: (reactTag: Int32, option: UnsafeObject) => Promise<VideoSaveData>;
|
||||
getCurrentPosition: (reactTag: Int32) => Promise<Int32>;
|
||||
}
|
||||
|
@ -355,7 +355,7 @@ export interface VideoNativeProps extends ViewProps {
|
||||
preventsDisplaySleepDuringVideoPlayback?: boolean;
|
||||
preferredForwardBufferDuration?: Float; //ios, 0
|
||||
playWhenInactive?: boolean; // ios, false
|
||||
enterPictureInPictureOnLeave?: boolean; // default false
|
||||
pictureInPicture?: boolean; // ios, false
|
||||
ignoreSilentSwitch?: WithDefault<string, 'inherit'>; // ios, 'inherit'
|
||||
mixWithOthers?: WithDefault<string, 'inherit'>; // ios, 'inherit'
|
||||
rate?: Float;
|
||||
|
@ -253,7 +253,7 @@ export interface ReactVideoEvents {
|
||||
onLoadStart?: (e: OnLoadStartData) => void; //All
|
||||
onPictureInPictureStatusChanged?: (
|
||||
e: OnPictureInPictureStatusChangedData,
|
||||
) => void; //Android, iOS
|
||||
) => void; //iOS
|
||||
onPlaybackRateChange?: (e: OnPlaybackRateChangeData) => void; //All
|
||||
onVolumeChange?: (e: OnVolumeChangeData) => void; //Android, iOS
|
||||
onProgress?: (e: OnProgressData) => void; //All
|
||||
|
@ -19,7 +19,5 @@ export interface VideoRef {
|
||||
getCurrentPosition: () => Promise<number>;
|
||||
setFullScreen: (fullScreen: boolean) => void;
|
||||
setSource: (source?: ReactVideoSource) => void;
|
||||
enterPictureInPicture: () => void;
|
||||
exitPictureInPicture: () => void;
|
||||
nativeHtmlVideoRef?: RefObject<HTMLVideoElement>; // web only
|
||||
}
|
||||
|
@ -315,7 +315,7 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps {
|
||||
mixWithOthers?: EnumValues<MixWithOthersType>; // iOS
|
||||
muted?: boolean;
|
||||
paused?: boolean;
|
||||
enterPictureInPictureOnLeave?: boolean;
|
||||
pictureInPicture?: boolean; // iOS
|
||||
playInBackground?: boolean;
|
||||
playWhenInactive?: boolean; // iOS
|
||||
poster?: string | ReactVideoPoster; // string is deprecated
|
||||
|
Loading…
x
Reference in New Issue
Block a user