Compare commits

..

52 Commits

Author SHA1 Message Date
ff0f288636 Merge remote-tracking branch 'origin/master' into ivan/merging-shaka-hls 2024-12-02 22:57:41 -07:00
d7977241c9 Account for crop in progress 2024-10-18 03:25:50 -06:00
921ead0f05 Timeout actions 2024-10-17 19:21:06 -06:00
20397d32e6 More logging 2024-10-17 19:11:22 -06:00
e3900e794d What is going on 2024-10-17 19:07:39 -06:00
4dc7bf465f Typescript 2024-10-17 18:59:42 -06:00
e5f182cda9 Use an async queue 2024-10-17 18:56:38 -06:00
9138c3249d Try to fix double load 2024-10-17 18:34:34 -06:00
7a1d0e8b10 Try to fix source quality issue 2024-10-17 18:28:16 -06:00
9cbba8f95e Typescript again 2024-10-17 18:22:26 -06:00
2cfb26d51f Typescript 2024-10-17 18:20:28 -06:00
4f18e9b238 Fix never ending loading, try to respect crops 2024-10-17 18:19:05 -06:00
bd64379837 Merge pull request 'Fix android build' (#7) from volodymyr/fix-android-build into feat/shaka
Reviewed-on: #7
Reviewed-by: Ivan Malison <ivanmalison@gmail.com>
2024-10-14 07:01:12 -06:00
47151c7119 Fix android build
Some checks failed
Build Android / Build Android Example App (pull_request) Has been cancelled
Build Android / Build Android Example App Without Ads (pull_request) Has been cancelled
Check Android / Kotlin-Lint (pull_request) Has been cancelled
2024-10-14 11:02:21 +02:00
a16275b003 ts-ignores 2024-10-13 22:48:51 -06:00
56c129aa4f Don't patch package anymore 2024-10-13 22:45:06 -06:00
68cbe3c4b1 Don't require rate to be defined 2024-10-13 20:45:25 -06:00
81e864c0e1 Update shaka when nativeRef changes 2024-10-13 19:17:01 -06:00
0e9ac4d125 Shaka loggin 2024-10-13 15:14:48 -06:00
9191a06600 Simple shaka 2024-10-13 15:04:44 -06:00
dc61c3efea Fix tsc 2024-10-13 14:18:49 -06:00
11f480f206 Start muted 2024-10-13 14:15:28 -06:00
9619e7517b Try again 2024-10-13 02:06:12 -06:00
3ddc6e931a More hacks 2024-10-13 01:09:57 -06:00
6b5831dc1c Hacks 2024-10-13 01:07:48 -06:00
3fc002f3fd Fix ts issues 2024-10-13 00:44:54 -06:00
edb5c6bcfa Merge remote-tracking branch 'railbird/master' into feat/shaka 2024-10-12 23:52:26 -06:00
5bc975b2c9 Shaka? 2024-10-12 23:48:55 -06:00
d79b5c9a83 Merge remote-tracking branch 'origin/master' into feat/web 2024-10-12 23:40:00 -06:00
Zoe Roux
6768c22139
Add nativeHtmlVideoRef doc 2024-07-16 12:31:32 +07:00
Zoe Roux
2b369df57d
Fix native import on web 2024-07-16 12:31:32 +07:00
Zoe Roux
8542c8f7d1
Add isSeeking on web 2024-07-16 12:31:32 +07:00
Zoe Roux
fc5b2d4563
Add web support for fullscreen 2024-07-16 11:33:50 +07:00
Zoe Roux
ffb4631854
Remove unused errorHandler ref 2024-07-16 11:33:50 +07:00
Zoe Roux
29cf7c97c3
Update doc for web 2024-07-16 11:33:50 +07:00
Zoe Roux
491ed77a32
Renamve nativeHtmlRef to nativeHtmlVideoRef 2024-07-16 11:33:50 +07:00
Zoe Roux
5b199b52b4
Move video style to const var 2024-07-16 11:33:50 +07:00
Zoe Roux
9d19157654
Add web command in basic example 2024-07-16 11:33:50 +07:00
Zoe Roux
3dabf5f16f
Fix and improve VideoNativeComponent (canPlay/isWidewine supportd) for web 2024-07-16 11:33:50 +07:00
Zoe Roux
e610a274d5
Prevent playback state change loop on web 2024-07-16 11:33:50 +07:00
Zoe Roux
27880f5212
Update the doc for web things 2024-07-16 11:33:50 +07:00
Zoe Roux
39dd30b762
Cleanup media session handling on the web 2024-07-16 11:33:50 +07:00
Zoe Roux
edf5d0c613
Test notifications on web 2024-07-16 11:33:50 +07:00
Zoe Roux
975fc2f303
Fix web bugs 2024-07-16 11:33:49 +07:00
Zoe Roux
aa85d71b87
Make the basic example app work on web 2024-07-16 11:33:18 +07:00
Zoe Roux
cce24cd829
Add media session support 2024-07-16 11:33:18 +07:00
Zoe Roux
cad63d465d
Add most properties 2024-07-16 11:33:18 +07:00
Zoe Roux
f5fa063bc0
Add most events 2024-07-16 11:33:18 +07:00
Zoe Roux
c6abcdeb2f
Create ref handling and basics stollen from Kyoo 2024-07-16 11:33:18 +07:00
Zoe Roux
a72ab331dc
Move video ref type to its own file 2024-07-16 11:33:15 +07:00
Zoe Roux
fa126de97f
Add VideoDecoderProperties for the web 2024-07-16 11:24:52 +07:00
Zoe Roux
ca2452edb6
Add shell.nix for nix users 2024-07-16 11:20:12 +07:00
50 changed files with 10012 additions and 1016 deletions

View File

@ -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>

View File

@ -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)

View File

@ -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)
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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) {

View File

@ -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))

View File

@ -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)
}

View File

@ -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) {

View File

@ -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)
}
}

Binary file not shown.

View File

@ -3,6 +3,6 @@
}
.spanStyle {
font-family: var(--font-orbitron);
font-family: 'Orbitron';
font-weight: 800;
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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
View File

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

View File

@ -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",
@ -18,4 +18,4 @@
"devDependencies": {
"bun-types": "latest"
}
}
}

View File

@ -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} />
</>
);
}

View File

@ -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"
}

View File

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

View File

@ -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']} />

View File

@ -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']} />

View File

@ -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

View File

@ -37,5 +37,4 @@ It's useful when you are using `expo` managed workflow (expo prebuild) as it wil
| enableBackgroundAudio | boolean | false | Add required changes to play video in background on iOS |
| 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 |
| androidExtensions | object | {} | You can enable/disable extensions as per your requirement - this allow to reduce library size on android |

View File

@ -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

View File

@ -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 || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-4YEWQH5ZHS');
`}
{`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
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 {

View File

@ -87,11 +87,7 @@ yarn start
> [!TIP]
> Make sure you've already downloaded the simulator on which you want to launch the app.
## [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.
## [Expo](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples/bare)
### Configuration

View File

@ -0,0 +1,52 @@
{
"name": "VideoPlayer",
"version": "1.0.0",
"private": true,
"scripts": {
"web": "expo start --web",
"android": "expo run:android",
"ios": "expo run:ios",
"windows": "react-native run-windows",
"start": "expo start",
"test": "jest",
"lint": "eslint .",
"pod-install": "cd ios && pod install && cd ..",
"pod-install:newarch": "cd ios && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install && cd .."
},
"dependencies": {
"@expo/metro-runtime": "~3.2.1",
"@react-native-picker/picker": "2.7.5",
"expo": "^51.0.32",
"expo-asset": "~10.0.10",
"expo-image": "^1.12.15",
"expo-navigation-bar": "~3.0.7",
"react": "18.2.0",
"react-native": "0.74.5",
"react-dom": "18.2.0",
"react-native-web": "~0.19.10",
"react-native-windows": "0.74.19"
},
"devDependencies": {
"@babel/core": "^7.24.0",
"@babel/preset-env": "^7.22.10",
"@babel/runtime": "^7.22.10",
"@react-native/babel-preset": "0.74.85",
"@react-native/eslint-config": "0.74.85",
"@react-native/metro-config": "0.74.85",
"@react-native/typescript-config": "0.74.85",
"@types/react": "~18.2.79",
"@types/react-test-renderer": "^18.0.0",
"babel-jest": "^29.6.3",
"babel-plugin-module-resolver": "5.0.0",
"eslint": "^8.19.0",
"jest": "^29.6.3",
"prettier": "^2.8.8",
"typescript": "~5.3.3"
},
"resolutions": {
"@types/react": "^18.0.24"
},
"engines": {
"node": ">=18"
}
}

View File

@ -0,0 +1,4 @@
import {registerRootComponent} from 'expo';
import VideoPlayer from './VideoPlayer';
registerRootComponent(VideoPlayer);

9711
examples/basic/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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()

View File

@ -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() {
guard let _pipController else { return }
if !_isPictureInPictureActive {
_pipController.startPictureInPicture()
func setPictureInPicture(_ isActive: Bool) {
if _isActive == isActive {
return
}
}
_isActive = isActive
func exitPictureInPicture() {
guard let _pipController else { return }
if _isPictureInPictureActive {
let state = UIApplication.shared.applicationState
if state == .background || state == .inactive {
deinitPipController()
_onPictureInPictureExit?()
_onRestoreUserInterfaceForPictureInPictureStop?()
} else {
if _isActive && !_pipController.isPictureInPictureActive {
DispatchQueue.main.async {
_pipController.startPictureInPicture()
}
} 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

View File

@ -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

View File

@ -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
private var _pip: RCTPictureInPicture?
#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,17 +181,15 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
func initPictureinPicture() {
#if os(iOS)
if _pip == nil {
_pip = RCTPictureInPicture({ [weak self] in
self?._onPictureInPictureEnter()
}, { [weak self] in
self?._onPictureInPictureExit()
}, { [weak self] in
self?.onRestoreUserInterfaceForPictureInPictureStop?([:])
})
}
_pip = RCTPictureInPicture({ [weak self] in
self?._onPictureInPictureEnter()
}, { [weak self] in
self?._onPictureInPictureExit()
}, { [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) {}

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
/**

View File

@ -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",
@ -32,9 +32,12 @@
"react-native": "0.73.2",
"react-native-windows": "^0.61.0-0",
"release-it": "^16.2.1",
"typescript": "5.1.6"
"typescript": "5.1.6",
"patch-package": "^8.0.0"
},
"dependencies": {
"shaka-player": "^4.11.7"
},
"dependencies": {},
"peerDependencies": {
"react": "*",
"react-native": "*"

View File

@ -0,0 +1,39 @@
diff --git a/node_modules/shaka-player/dist/shaka-player.compiled.d.ts b/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
index 19c0930..cc0a3fd 100644
--- a/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
+++ b/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
@@ -5117,3 +5117,5 @@ declare namespace shaka.extern {
declare namespace shaka.extern {
type TransmuxerPlugin = ( ) => shaka.extern.Transmuxer ;
}
+
+export default shaka;
diff --git a/node_modules/shaka-player/dist/shaka-player.ui.d.ts b/node_modules/shaka-player/dist/shaka-player.ui.d.ts
index 1618ca0..a6076c6 100644
--- a/node_modules/shaka-player/dist/shaka-player.ui.d.ts
+++ b/node_modules/shaka-player/dist/shaka-player.ui.d.ts
@@ -5830,3 +5830,5 @@ declare namespace shaka.extern {
declare namespace shaka.extern {
type UIVolumeBarColors = { base : string , level : string } ;
}
+
+export default shaka;
diff --git a/node_modules/shaka-player/index.d.ts b/node_modules/shaka-player/index.d.ts
new file mode 100644
index 0000000..3ebfd96
--- /dev/null
+++ b/node_modules/shaka-player/index.d.ts
@@ -0,0 +1,2 @@
+/// <reference path="./dist/shaka-player.compiled.d.ts" />
+/// <reference path="./dist/shaka-player.ui.d.ts" />
\ No newline at end of file
diff --git a/node_modules/shaka-player/ui.d.ts b/node_modules/shaka-player/ui.d.ts
new file mode 100644
index 0000000..84a3be0
--- /dev/null
+++ b/node_modules/shaka-player/ui.d.ts
@@ -0,0 +1,3 @@
+import shaka from 'shaka-player/dist/shaka-player.ui'
+export * from 'shaka-player/dist/shaka-player.ui'
+export default shaka;
\ No newline at end of file

View File

@ -46,7 +46,7 @@ import {
resolveAssetSourceForVideo,
} from './utils';
import NativeVideoManager from './specs/NativeVideoManager';
import {ViewType, CmcdMode, VideoRef} from './types';
import {type VideoSaveData, ViewType, CmcdMode, VideoRef} from './types';
import type {
OnLoadData,
OnTextTracksData,
@ -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,
],
);

View File

@ -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 (

View File

@ -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

View File

@ -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;
});
};

View File

@ -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);
}

View File

@ -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>;
}

View File

@ -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;

View File

@ -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

View File

@ -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
}

View File

@ -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