@stepincto/expo-video 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +46 -4
- package/android/src/main/java/expo/modules/video/VideoModule.kt +6 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +13 -0
- package/android/src/main/java/expo/modules/video/enums/FullscreenOrientation.kt +26 -0
- package/android/src/main/java/expo/modules/video/records/FullscreenOptions.kt +12 -0
- package/android/src/main/java/expo/modules/video/utils/FullscreenActivityOrientationHelper.kt +120 -0
- package/build/VideoView.d.ts.map +1 -1
- package/build/VideoView.js +10 -3
- package/build/VideoView.js.map +1 -1
- package/build/VideoView.types.d.ts +47 -0
- package/build/VideoView.types.d.ts.map +1 -1
- package/build/VideoView.types.js.map +1 -1
- package/build/VideoView.web.d.ts.map +1 -1
- package/build/VideoView.web.js +3 -2
- package/build/VideoView.web.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/expo-module.config.json +7 -1
- package/ios/Enums/FullscreenOrientation.swift +34 -0
- package/ios/OrientationAVPlayerViewController.swift +231 -0
- package/ios/Records/FullscreenOptions.swift +12 -0
- package/ios/VideoModule.swift +8 -0
- package/ios/VideoView.swift +19 -50
- package/package.json +3 -4
- package/src/VideoView.tsx +28 -3
- package/src/VideoView.types.ts +57 -0
- package/src/VideoView.web.tsx +4 -2
- package/src/index.ts +7 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package expo.modules.video
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
|
+
import android.content.pm.ActivityInfo
|
|
4
5
|
import android.content.res.Configuration
|
|
5
6
|
import android.os.Build
|
|
6
7
|
import android.os.Bundle
|
|
@@ -12,6 +13,8 @@ import android.widget.ImageButton
|
|
|
12
13
|
import androidx.media3.ui.PlayerView
|
|
13
14
|
import expo.modules.kotlin.exception.CodedException
|
|
14
15
|
import expo.modules.video.player.VideoPlayer
|
|
16
|
+
import expo.modules.video.records.FullscreenOptions
|
|
17
|
+
import expo.modules.video.utils.FullscreenActivityOrientationHelper
|
|
15
18
|
import expo.modules.video.utils.applyPiPParams
|
|
16
19
|
import expo.modules.video.utils.applyRectHint
|
|
17
20
|
import expo.modules.video.utils.calculatePiPAspectRatio
|
|
@@ -26,22 +29,49 @@ class FullscreenPlayerActivity : Activity() {
|
|
|
26
29
|
private lateinit var videoView: VideoView
|
|
27
30
|
private var didFinish = false
|
|
28
31
|
private var wasAutoPaused = false
|
|
32
|
+
private lateinit var options: FullscreenOptions
|
|
33
|
+
private lateinit var orientationHelper: FullscreenActivityOrientationHelper
|
|
29
34
|
|
|
30
35
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
31
36
|
super.onCreate(savedInstanceState)
|
|
32
|
-
setContentView(R.layout.fullscreen_player_activity)
|
|
33
|
-
mContentView = findViewById(R.id.enclosing_layout)
|
|
34
|
-
playerView = findViewById(R.id.player_view)
|
|
35
37
|
|
|
36
38
|
try {
|
|
37
39
|
videoViewId = intent.getStringExtra(VideoManager.INTENT_PLAYER_KEY)
|
|
38
40
|
?: throw FullScreenVideoViewNotFoundException()
|
|
41
|
+
|
|
42
|
+
options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
43
|
+
intent.getSerializableExtra(INTENT_FULLSCREEN_OPTIONS_KEY, FullscreenOptions::class.java)
|
|
44
|
+
?: FullscreenOptions()
|
|
45
|
+
} else {
|
|
46
|
+
@Suppress("DEPRECATION")
|
|
47
|
+
(intent.getSerializableExtra(INTENT_FULLSCREEN_OPTIONS_KEY) as? FullscreenOptions)
|
|
48
|
+
?: FullscreenOptions()
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
videoView = VideoManager.getVideoView(videoViewId)
|
|
52
|
+
|
|
53
|
+
orientationHelper = FullscreenActivityOrientationHelper(
|
|
54
|
+
this,
|
|
55
|
+
options,
|
|
56
|
+
onShouldAutoExit = {
|
|
57
|
+
finish()
|
|
58
|
+
},
|
|
59
|
+
onShouldReleaseOrientation = {
|
|
60
|
+
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
orientationHelper.startOrientationEventListener()
|
|
40
64
|
} catch (e: CodedException) {
|
|
41
65
|
Log.e("ExpoVideo", "${e.message}", e)
|
|
42
66
|
finish()
|
|
43
67
|
return
|
|
44
68
|
}
|
|
69
|
+
|
|
70
|
+
setContentView(R.layout.fullscreen_player_activity)
|
|
71
|
+
mContentView = findViewById(R.id.enclosing_layout)
|
|
72
|
+
playerView = findViewById(R.id.player_view)
|
|
73
|
+
requestedOrientation = options.orientation.toActivityOrientation()
|
|
74
|
+
|
|
45
75
|
videoPlayer = videoView.videoPlayer
|
|
46
76
|
videoPlayer?.changePlayerView(playerView)
|
|
47
77
|
VideoManager.registerFullscreenPlayerActivity(hashCode().toString(), this)
|
|
@@ -83,7 +113,8 @@ class FullscreenPlayerActivity : Activity() {
|
|
|
83
113
|
}
|
|
84
114
|
|
|
85
115
|
override fun onResume() {
|
|
86
|
-
|
|
116
|
+
orientationHelper.startOrientationEventListener()
|
|
117
|
+
playerView.useController = true
|
|
87
118
|
super.onResume()
|
|
88
119
|
}
|
|
89
120
|
|
|
@@ -95,6 +126,7 @@ class FullscreenPlayerActivity : Activity() {
|
|
|
95
126
|
videoPlayer?.player?.pause()
|
|
96
127
|
}
|
|
97
128
|
}
|
|
129
|
+
orientationHelper.stopOrientationEventListener()
|
|
98
130
|
super.onPause()
|
|
99
131
|
}
|
|
100
132
|
|
|
@@ -102,6 +134,7 @@ class FullscreenPlayerActivity : Activity() {
|
|
|
102
134
|
super.onDestroy()
|
|
103
135
|
videoView.exitFullscreen()
|
|
104
136
|
VideoManager.unregisterFullscreenPlayerActivity(hashCode().toString())
|
|
137
|
+
orientationHelper.stopOrientationEventListener()
|
|
105
138
|
}
|
|
106
139
|
|
|
107
140
|
private fun setupFullscreenButton() {
|
|
@@ -142,4 +175,13 @@ class FullscreenPlayerActivity : Activity() {
|
|
|
142
175
|
)
|
|
143
176
|
}
|
|
144
177
|
}
|
|
178
|
+
|
|
179
|
+
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
180
|
+
super.onConfigurationChanged(newConfig)
|
|
181
|
+
orientationHelper.onConfigurationChanged(newConfig)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
companion object {
|
|
185
|
+
const val INTENT_FULLSCREEN_OPTIONS_KEY = "fullscreen_options"
|
|
186
|
+
}
|
|
145
187
|
}
|
|
@@ -20,6 +20,7 @@ import expo.modules.video.enums.AudioMixingMode
|
|
|
20
20
|
import expo.modules.video.enums.ContentFit
|
|
21
21
|
import expo.modules.video.player.VideoPlayer
|
|
22
22
|
import expo.modules.video.records.BufferOptions
|
|
23
|
+
import expo.modules.video.records.FullscreenOptions
|
|
23
24
|
import expo.modules.video.records.SubtitleTrack
|
|
24
25
|
import expo.modules.video.records.AudioTrack
|
|
25
26
|
import expo.modules.video.records.VideoSource
|
|
@@ -381,6 +382,11 @@ private inline fun <reified T : VideoView> ViewDefinitionBuilder<T>.VideoViewCom
|
|
|
381
382
|
Prop("allowsFullscreen") { view: T, allowsFullscreen: Boolean? ->
|
|
382
383
|
view.allowsFullscreen = allowsFullscreen ?: true
|
|
383
384
|
}
|
|
385
|
+
Prop("fullscreenOptions") { view: T, fullscreenOptions: FullscreenOptions? ->
|
|
386
|
+
if (fullscreenOptions != null) {
|
|
387
|
+
view.fullscreenOptions = fullscreenOptions
|
|
388
|
+
}
|
|
389
|
+
}
|
|
384
390
|
Prop("requiresLinearPlayback") { view: T, requiresLinearPlayback: Boolean? ->
|
|
385
391
|
val linearPlayback = requiresLinearPlayback ?: false
|
|
386
392
|
view.playerView.applyRequiresLinearPlayback(linearPlayback)
|
|
@@ -32,6 +32,7 @@ import expo.modules.video.records.AudioTrack
|
|
|
32
32
|
import expo.modules.video.records.SubtitleTrack
|
|
33
33
|
import expo.modules.video.records.VideoSource
|
|
34
34
|
import expo.modules.video.records.VideoTrack
|
|
35
|
+
import expo.modules.video.records.FullscreenOptions
|
|
35
36
|
import expo.modules.video.utils.applyPiPParams
|
|
36
37
|
import expo.modules.video.utils.applyRectHint
|
|
37
38
|
import expo.modules.video.utils.calculatePiPAspectRatio
|
|
@@ -132,6 +133,17 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
|
|
|
132
133
|
field = value
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
var fullscreenOptions: FullscreenOptions = FullscreenOptions()
|
|
137
|
+
set(value) {
|
|
138
|
+
field = value
|
|
139
|
+
if (value.enable) {
|
|
140
|
+
playerView.setFullscreenButtonClickListener { enterFullscreen() }
|
|
141
|
+
} else {
|
|
142
|
+
playerView.setFullscreenButtonClickListener(null)
|
|
143
|
+
playerView.setFullscreenButtonVisibility(false)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
135
147
|
private val mLayoutRunnable = Runnable {
|
|
136
148
|
measure(
|
|
137
149
|
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
@@ -172,6 +184,7 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
|
|
|
172
184
|
fun enterFullscreen() {
|
|
173
185
|
val intent = Intent(context, FullscreenPlayerActivity::class.java)
|
|
174
186
|
intent.putExtra(VideoManager.INTENT_PLAYER_KEY, videoViewId)
|
|
187
|
+
intent.putExtra(FullscreenPlayerActivity.INTENT_FULLSCREEN_OPTIONS_KEY, fullscreenOptions)
|
|
175
188
|
// Set before starting the activity to avoid entering PiP unintentionally
|
|
176
189
|
isInFullscreen = true
|
|
177
190
|
currentActivity.startActivity(intent)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package expo.modules.video.enums
|
|
2
|
+
|
|
3
|
+
import android.content.pm.ActivityInfo
|
|
4
|
+
import expo.modules.kotlin.types.Enumerable
|
|
5
|
+
|
|
6
|
+
enum class FullscreenOrientation(val value: String) : Enumerable {
|
|
7
|
+
LANDSCAPE("landscape"),
|
|
8
|
+
PORTRAIT("portrait"),
|
|
9
|
+
LANDSCAPE_LEFT("landscapeLeft"),
|
|
10
|
+
LANDSCAPE_RIGHT("landscapeRight"),
|
|
11
|
+
PORTRAIT_UP("portraitUp"),
|
|
12
|
+
PORTRAIT_DOWN("portraitDown"),
|
|
13
|
+
DEFAULT("default");
|
|
14
|
+
|
|
15
|
+
fun toActivityOrientation(): Int {
|
|
16
|
+
return when (this) {
|
|
17
|
+
LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
|
18
|
+
PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
|
19
|
+
LANDSCAPE_LEFT -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
20
|
+
LANDSCAPE_RIGHT -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
|
21
|
+
PORTRAIT_UP -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
22
|
+
PORTRAIT_DOWN -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
|
23
|
+
DEFAULT -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package expo.modules.video.records
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.records.Field
|
|
4
|
+
import expo.modules.kotlin.records.Record
|
|
5
|
+
import expo.modules.video.enums.FullscreenOrientation
|
|
6
|
+
import java.io.Serializable
|
|
7
|
+
|
|
8
|
+
data class FullscreenOptions(
|
|
9
|
+
@Field val enable: Boolean = true,
|
|
10
|
+
@Field val orientation: FullscreenOrientation = FullscreenOrientation.DEFAULT,
|
|
11
|
+
@Field val autoExitOnRotate: Boolean = false
|
|
12
|
+
) : Record, Serializable
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
package expo.modules.video.utils
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.res.Configuration
|
|
5
|
+
import android.hardware.SensorManager
|
|
6
|
+
import android.provider.Settings
|
|
7
|
+
import android.view.OrientationEventListener
|
|
8
|
+
import expo.modules.video.enums.FullscreenOrientation
|
|
9
|
+
import expo.modules.video.records.FullscreenOptions
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper for the auto-exit fullscreen functionality. Once the user has rotated the phone to the desired orientation, the orientation lock should be released, so that once rotation to a perpendicular orientation is detected, the fullscreen can be exited.
|
|
13
|
+
*/
|
|
14
|
+
class FullscreenActivityOrientationHelper(val context: Context, val options: FullscreenOptions, val onShouldAutoExit: (() -> Unit), val onShouldReleaseOrientation: (() -> Unit)) {
|
|
15
|
+
private var userHasRotatedToVideoOrientation = false
|
|
16
|
+
private val isLockedToLandscape = options.orientation == FullscreenOrientation.LANDSCAPE ||
|
|
17
|
+
options.orientation == FullscreenOrientation.LANDSCAPE_LEFT ||
|
|
18
|
+
options.orientation == FullscreenOrientation.LANDSCAPE_RIGHT
|
|
19
|
+
|
|
20
|
+
private val isLockedToPortrait = options.orientation == FullscreenOrientation.PORTRAIT ||
|
|
21
|
+
options.orientation == FullscreenOrientation.PORTRAIT_UP ||
|
|
22
|
+
options.orientation == FullscreenOrientation.PORTRAIT_DOWN
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Checks if the system's auto-rotation setting is currently enabled.
|
|
26
|
+
* Returns true if auto-rotation is unlocked (enabled), false otherwise (locked or error).
|
|
27
|
+
*/
|
|
28
|
+
val isAutoRotationEnabled: Boolean
|
|
29
|
+
get() {
|
|
30
|
+
return try {
|
|
31
|
+
val rotationStatus = Settings.System.getInt(
|
|
32
|
+
context.contentResolver,
|
|
33
|
+
Settings.System.ACCELEROMETER_ROTATION,
|
|
34
|
+
0
|
|
35
|
+
)
|
|
36
|
+
rotationStatus == 1
|
|
37
|
+
} catch (e: Exception) {
|
|
38
|
+
false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Orientation listener running while the activity orientation is locked. The goal of the listener is to detect if the user has rotated the phone to the desired orientation.
|
|
43
|
+
Once they have done that auto-exit can be activated. That's when we can disable the lock and wait for the device to be rotated to portrait.
|
|
44
|
+
When the screen starts rotating we receive a configuration change and can send a signal to exit fullscreen.
|
|
45
|
+
It's better to unlock and wait for config change instead of trying to detect orientation based on angles, because the angles update faster than the phone rotation.
|
|
46
|
+
*/
|
|
47
|
+
private val orientationEventListener by lazy {
|
|
48
|
+
object : OrientationEventListener(context, SensorManager.SENSOR_DELAY_NORMAL) {
|
|
49
|
+
override fun onOrientationChanged(orientation: Int) {
|
|
50
|
+
// Use narrower ranges to determine the orientation. Using a 90 degree range is too sensitive to small tilts.
|
|
51
|
+
val newPhysicalOrientation = when {
|
|
52
|
+
(orientation >= 0 && orientation <= 10) || (orientation >= 350 && orientation < 360) -> {
|
|
53
|
+
Configuration.ORIENTATION_PORTRAIT
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
(orientation >= 80 && orientation <= 100) -> {
|
|
57
|
+
Configuration.ORIENTATION_LANDSCAPE
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
(orientation >= 170 && orientation <= 190) -> {
|
|
61
|
+
Configuration.ORIENTATION_PORTRAIT
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
(orientation >= 260 && orientation <= 280) -> {
|
|
65
|
+
Configuration.ORIENTATION_LANDSCAPE
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
else -> {
|
|
69
|
+
Configuration.ORIENTATION_UNDEFINED
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!options.autoExitOnRotate) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
val canReleaseFromLandscape = newPhysicalOrientation == Configuration.ORIENTATION_PORTRAIT && isLockedToLandscape && userHasRotatedToVideoOrientation
|
|
78
|
+
val canReleaseFromPortrait = newPhysicalOrientation == Configuration.ORIENTATION_LANDSCAPE && isLockedToPortrait && userHasRotatedToVideoOrientation
|
|
79
|
+
|
|
80
|
+
if (canReleaseFromPortrait || canReleaseFromLandscape) {
|
|
81
|
+
if (!isAutoRotationEnabled) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
onShouldReleaseOrientation()
|
|
85
|
+
this@FullscreenActivityOrientationHelper.stopOrientationEventListener()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
val hasRotatedToVideoOrientationPortrait = newPhysicalOrientation == Configuration.ORIENTATION_PORTRAIT && isLockedToPortrait && !userHasRotatedToVideoOrientation
|
|
89
|
+
val hasRotatedToVideoOrientationLandscape = newPhysicalOrientation == Configuration.ORIENTATION_LANDSCAPE && isLockedToLandscape && !userHasRotatedToVideoOrientation
|
|
90
|
+
|
|
91
|
+
if (hasRotatedToVideoOrientationPortrait || hasRotatedToVideoOrientationLandscape) {
|
|
92
|
+
userHasRotatedToVideoOrientation = true
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fun onConfigurationChanged(newConfig: Configuration) {
|
|
99
|
+
val orientation = newConfig.orientation
|
|
100
|
+
if (!options.autoExitOnRotate) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isLockedToPortrait && orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
105
|
+
onShouldAutoExit()
|
|
106
|
+
} else if (isLockedToLandscape && orientation == Configuration.ORIENTATION_PORTRAIT) {
|
|
107
|
+
onShouldAutoExit()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fun startOrientationEventListener() {
|
|
112
|
+
if (orientationEventListener.canDetectOrientation()) {
|
|
113
|
+
orientationEventListener.enable()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fun stopOrientationEventListener() {
|
|
118
|
+
orientationEventListener.disable()
|
|
119
|
+
}
|
|
120
|
+
}
|
package/build/VideoView.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoView.d.ts","sourceRoot":"","sources":["../src/VideoView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAa,MAAM,OAAO,CAAC;AAK5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,IAAI,OAAO,CAErD;AAED,qBAAa,SAAU,SAAQ,aAAa,CAAC,cAAc,CAAC;IAC1D,SAAS,iCAAoB;IAE7B;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC;;;;;;;;;OASG;IACG,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5C;;;;;OAKG;IACG,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3C,MAAM,IAAI,SAAS;
|
|
1
|
+
{"version":3,"file":"VideoView.d.ts","sourceRoot":"","sources":["../src/VideoView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAa,MAAM,OAAO,CAAC;AAK5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,IAAI,OAAO,CAErD;AAED,qBAAa,SAAU,SAAQ,aAAa,CAAC,cAAc,CAAC;IAC1D,SAAS,iCAAoB;IAE7B;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC;;;;;;;;;OASG;IACG,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5C;;;;;OAKG;IACG,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3C,MAAM,IAAI,SAAS;CAkCpB"}
|
package/build/VideoView.js
CHANGED
|
@@ -52,12 +52,19 @@ export class VideoView extends PureComponent {
|
|
|
52
52
|
return await this.nativeRef.current?.stopPictureInPicture();
|
|
53
53
|
}
|
|
54
54
|
render() {
|
|
55
|
-
const { player, ...props } = this.props;
|
|
55
|
+
const { player, allowsFullscreen, ...props } = this.props;
|
|
56
56
|
const playerId = getPlayerId(player);
|
|
57
|
+
if (allowsFullscreen !== undefined) {
|
|
58
|
+
console.warn('The `allowsFullscreen` prop is deprecated and will be removed in a future release. Use `fullscreenOptions` prop instead.');
|
|
59
|
+
}
|
|
60
|
+
const fullscreenOptions = {
|
|
61
|
+
enable: allowsFullscreen,
|
|
62
|
+
...props.fullscreenOptions,
|
|
63
|
+
};
|
|
57
64
|
if (NativeTextureVideoView && this.props.surfaceType === 'textureView') {
|
|
58
|
-
return _jsx(NativeTextureVideoView, { ...props, player: playerId, ref: this.nativeRef });
|
|
65
|
+
return (_jsx(NativeTextureVideoView, { ...props, fullscreenOptions: fullscreenOptions, player: playerId, ref: this.nativeRef }));
|
|
59
66
|
}
|
|
60
|
-
return _jsx(NativeVideoView, { ...props, player: playerId, ref: this.nativeRef });
|
|
67
|
+
return (_jsx(NativeVideoView, { ...props, fullscreenOptions: fullscreenOptions, player: playerId, ref: this.nativeRef }));
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
70
|
// Temporary solution to pass the shared object ID instead of the player object.
|
package/build/VideoView.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoView.js","sourceRoot":"","sources":["../src/VideoView.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAa,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAE5D,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,eAAe,EAAE,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAI5E;;;;;;;;;GASG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,iBAAiB,CAAC,2BAA2B,EAAE,CAAC;AACzD,CAAC;AAED,MAAM,OAAO,SAAU,SAAQ,aAA6B;IAC1D,SAAS,GAAG,SAAS,EAAO,CAAC;IAE7B;;OAEG;IACH,KAAK,CAAC,eAAe;QACnB,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,CAAC;IACzD,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,cAAc;QAClB,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACxD,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,qBAAqB;QACzB,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,qBAAqB,EAAE,CAAC;IAC/D,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,oBAAoB;QACxB,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAC9D,CAAC;IAED,MAAM;QACJ,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"VideoView.js","sourceRoot":"","sources":["../src/VideoView.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAa,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAE5D,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,eAAe,EAAE,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAI5E;;;;;;;;;GASG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,iBAAiB,CAAC,2BAA2B,EAAE,CAAC;AACzD,CAAC;AAED,MAAM,OAAO,SAAU,SAAQ,aAA6B;IAC1D,SAAS,GAAG,SAAS,EAAO,CAAC;IAE7B;;OAEG;IACH,KAAK,CAAC,eAAe;QACnB,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,CAAC;IACzD,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,cAAc;QAClB,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACxD,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,qBAAqB;QACzB,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,qBAAqB,EAAE,CAAC;IAC/D,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,oBAAoB;QACxB,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAC9D,CAAC;IAED,MAAM;QACJ,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;QAC1D,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QAErC,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACnC,OAAO,CAAC,IAAI,CACV,0HAA0H,CAC3H,CAAC;QACJ,CAAC;QAED,MAAM,iBAAiB,GAAG;YACxB,MAAM,EAAE,gBAAgB;YACxB,GAAG,KAAK,CAAC,iBAAiB;SAC3B,CAAC;QAEF,IAAI,sBAAsB,IAAI,IAAI,CAAC,KAAK,CAAC,WAAW,KAAK,aAAa,EAAE,CAAC;YACvE,OAAO,CACL,KAAC,sBAAsB,OACjB,KAAK,EACT,iBAAiB,EAAE,iBAAiB,EACpC,MAAM,EAAE,QAAQ,EAChB,GAAG,EAAE,IAAI,CAAC,SAAS,GACnB,CACH,CAAC;QACJ,CAAC;QACD,OAAO,CACL,KAAC,eAAe,OACV,KAAK,EACT,iBAAiB,EAAE,iBAAiB,EACpC,MAAM,EAAE,QAAQ,EAChB,GAAG,EAAE,IAAI,CAAC,SAAS,GACnB,CACH,CAAC;IACJ,CAAC;CACF;AAED,gFAAgF;AAChF,gEAAgE;AAChE,yEAAyE;AACzE,SAAS,WAAW,CAAC,MAA4B;IAC/C,IAAI,MAAM,YAAY,iBAAiB,CAAC,WAAW,EAAE,CAAC;QACpD,mBAAmB;QACnB,OAAO,MAAM,CAAC,yBAAyB,CAAC;IAC1C,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["import { ReactNode, PureComponent, createRef } from 'react';\n\nimport NativeVideoModule from './NativeVideoModule';\nimport NativeVideoView, { NativeTextureVideoView } from './NativeVideoView';\nimport type { VideoPlayer } from './VideoPlayer.types';\nimport type { VideoViewProps } from './VideoView.types';\n\n/**\n * Returns whether the current device supports Picture in Picture (PiP) mode.\n *\n * > **Note:** All major web browsers support Picture in Picture (PiP) mode except Firefox.\n * > For more information, see [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/API/Picture-in-Picture_API#browser_compatibility).\n * @returns A `boolean` which is `true` if the device supports PiP mode, and `false` otherwise.\n * @platform android\n * @platform ios\n * @platform web\n */\nexport function isPictureInPictureSupported(): boolean {\n return NativeVideoModule.isPictureInPictureSupported();\n}\n\nexport class VideoView extends PureComponent<VideoViewProps> {\n nativeRef = createRef<any>();\n\n /**\n * Enters fullscreen mode.\n */\n async enterFullscreen(): Promise<void> {\n return await this.nativeRef.current?.enterFullscreen();\n }\n\n /**\n * Exits fullscreen mode.\n */\n async exitFullscreen(): Promise<void> {\n return await this.nativeRef.current?.exitFullscreen();\n }\n\n /**\n * Enters Picture in Picture (PiP) mode. Throws an exception if the device does not support PiP.\n * > **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n *\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-app-config)\n * > has to be configured for the PiP to work.\n * @platform android\n * @platform ios\n * @platform web\n */\n async startPictureInPicture(): Promise<void> {\n return await this.nativeRef.current?.startPictureInPicture();\n }\n\n /**\n * Exits Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios\n * @platform web\n */\n async stopPictureInPicture(): Promise<void> {\n return await this.nativeRef.current?.stopPictureInPicture();\n }\n\n render(): ReactNode {\n const { player, allowsFullscreen, ...props } = this.props;\n const playerId = getPlayerId(player);\n\n if (allowsFullscreen !== undefined) {\n console.warn(\n 'The `allowsFullscreen` prop is deprecated and will be removed in a future release. Use `fullscreenOptions` prop instead.'\n );\n }\n\n const fullscreenOptions = {\n enable: allowsFullscreen,\n ...props.fullscreenOptions,\n };\n\n if (NativeTextureVideoView && this.props.surfaceType === 'textureView') {\n return (\n <NativeTextureVideoView\n {...props}\n fullscreenOptions={fullscreenOptions}\n player={playerId}\n ref={this.nativeRef}\n />\n );\n }\n return (\n <NativeVideoView\n {...props}\n fullscreenOptions={fullscreenOptions}\n player={playerId}\n ref={this.nativeRef}\n />\n );\n }\n}\n\n// Temporary solution to pass the shared object ID instead of the player object.\n// We can't really pass it as an object in the old architecture.\n// Technically we can in the new architecture, but it's not possible yet.\nfunction getPlayerId(player: number | VideoPlayer): number | null {\n if (player instanceof NativeVideoModule.VideoPlayer) {\n // @ts-expect-error\n return player.__expo_shared_object_id__;\n }\n if (typeof player === 'number') {\n return player;\n }\n return null;\n}\n"]}
|
|
@@ -16,6 +16,47 @@ export type VideoContentFit = 'contain' | 'cover' | 'fill';
|
|
|
16
16
|
* @platform android
|
|
17
17
|
*/
|
|
18
18
|
export type SurfaceType = 'textureView' | 'surfaceView';
|
|
19
|
+
/**
|
|
20
|
+
* Describes the orientation of the video in fullscreen mode. Available values are:
|
|
21
|
+
* - `default`: The video is displayed in any of the available device rotations.
|
|
22
|
+
* - `portrait`: The video is displayed in one of two available portrait orientations and rotates between them.
|
|
23
|
+
* - `portraitUp`: The video is displayed in the portrait orientation - the notch of the phone points upwards.
|
|
24
|
+
* - `portraitDown`: The video is displayed in the portrait orientation - the notch of the phone points downwards.
|
|
25
|
+
* - `landscape`: The video is displayed in one of two available landscape orientations and rotates between them.
|
|
26
|
+
* - `landscapeLeft`: The video is displayed in the left landscape orientation - the notch of the phone is in the left palm of the user.
|
|
27
|
+
* - `landscapeRight`: The video is displayed in the right landscape orientation - the notch of the phone is in the right palm of the user.
|
|
28
|
+
*/
|
|
29
|
+
export type FullscreenOrientation = 'default' | 'portrait' | 'portraitUp' | 'portraitDown' | 'landscape' | 'landscapeLeft' | 'landscapeRight';
|
|
30
|
+
/**
|
|
31
|
+
* Describes the options for fullscreen video mode.
|
|
32
|
+
*/
|
|
33
|
+
export type FullscreenOptions = {
|
|
34
|
+
/**
|
|
35
|
+
* Specifies whether the fullscreen mode should be available to the user. When `false`, the fullscreen button will be hidden in the player.
|
|
36
|
+
* Equivalent to the `allowsFullscreen` prop.
|
|
37
|
+
* @default true
|
|
38
|
+
*/
|
|
39
|
+
enable?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Specifies the orientation of the video in fullscreen mode.
|
|
42
|
+
* @default 'default'
|
|
43
|
+
* @platform android
|
|
44
|
+
* @platform ios
|
|
45
|
+
*/
|
|
46
|
+
orientation?: FullscreenOrientation;
|
|
47
|
+
/**
|
|
48
|
+
* Specifies whether the app should exit fullscreen mode when the device is rotated to a different orientation than the one specified in the `orientation` prop.
|
|
49
|
+
* For example, if the `orientation` prop is set to `landscape` and the device is rotated to `portrait`, the app will exit fullscreen mode.
|
|
50
|
+
*
|
|
51
|
+
* > This prop will have no effect if the `orientation` prop is set to `default`.
|
|
52
|
+
* > The `VideoView` will never auto-exit fullscreen when the device auto-rotate feature has been disabled in settings.
|
|
53
|
+
*
|
|
54
|
+
* @default false
|
|
55
|
+
* @platform android
|
|
56
|
+
* @platform ios
|
|
57
|
+
*/
|
|
58
|
+
autoExitOnRotate?: boolean;
|
|
59
|
+
};
|
|
19
60
|
export interface VideoViewProps extends ViewProps {
|
|
20
61
|
/**
|
|
21
62
|
* A video player instance. Use [`useVideoPlayer()`](#usevideoplayersource-setup) hook to create one.
|
|
@@ -34,9 +75,15 @@ export interface VideoViewProps extends ViewProps {
|
|
|
34
75
|
contentFit?: VideoContentFit;
|
|
35
76
|
/**
|
|
36
77
|
* Determines whether fullscreen mode is allowed or not.
|
|
78
|
+
*
|
|
79
|
+
* > Note: This option has been deprecated in favor of the `fullscreenOptions` prop and will be disabled in the future.
|
|
37
80
|
* @default true
|
|
38
81
|
*/
|
|
39
82
|
allowsFullscreen?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Determines the fullscreen mode options.
|
|
85
|
+
*/
|
|
86
|
+
fullscreenOptions?: FullscreenOptions;
|
|
40
87
|
/**
|
|
41
88
|
* Determines whether the timecodes should be displayed or not.
|
|
42
89
|
* @default true
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoView.types.d.ts","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3D;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,aAAa,CAAC;AAExD,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C;;OAEG;IACH,MAAM,EAAE,WAAW,CAAC;IAEpB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAE7B
|
|
1
|
+
{"version":3,"file":"VideoView.types.d.ts","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3D;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,aAAa,CAAC;AAExD;;;;;;;;;GASG;AACH,MAAM,MAAM,qBAAqB,GAC7B,SAAS,GACT,UAAU,GACV,YAAY,GACZ,cAAc,GACd,WAAW,GACX,eAAe,GACf,gBAAgB,CAAC;AAErB;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C;;OAEG;IACH,MAAM,EAAE,WAAW,CAAC;IAEpB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAE7B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;OAEG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAEtC;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAE1B;;;;OAIG;IACH,eAAe,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE/C;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,MAAM,IAAI,CAAC;IAErC;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpC;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;;;;;;OAUG;IACH,mCAAmC,CAAC,EAAE,OAAO,CAAC;IAE9C;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAEnC;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAE/B;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAE9B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEhC;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,WAAW,GAAG,iBAAiB,CAAC;CAC/C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoView.types.js","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"","sourcesContent":["import { ViewProps } from 'react-native';\n\nimport type { VideoPlayer } from './VideoPlayer.types';\n\n/**\n * Describes how a video should be scaled to fit in a container.\n * - `contain`: The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing.\n * - `cover`: The video maintains its aspect ratio and covers the entire container, potentially cropping some portions.\n * - `fill`: The video stretches/squeezes to completely fill the container, potentially causing distortion.\n */\nexport type VideoContentFit = 'contain' | 'cover' | 'fill';\n\n/**\n * Describes the type of the surface used to render the video.\n * - `surfaceView`: Uses the `SurfaceView` to render the video. This value should be used in the majority of cases. Provides significantly lower power consumption, better performance, and more features.\n * - `textureView`: Uses the `TextureView` to render the video. Should be used in cases where the SurfaceView is not supported or causes issues (for example, overlapping video views).\n *\n * You can learn more about surface types in the official [ExoPlayer documentation](https://developer.android.com/media/media3/ui/playerview#surfacetype).\n * @platform android\n */\nexport type SurfaceType = 'textureView' | 'surfaceView';\n\nexport interface VideoViewProps extends ViewProps {\n /**\n * A video player instance. Use [`useVideoPlayer()`](#usevideoplayersource-setup) hook to create one.\n */\n player: VideoPlayer;\n\n /**\n * Determines whether native controls should be displayed or not.\n * @default true\n */\n nativeControls?: boolean;\n\n /**\n * Describes how the video should be scaled to fit in the container.\n * Options are `'contain'`, `'cover'`, and `'fill'`.\n * @default 'contain'\n */\n contentFit?: VideoContentFit;\n\n /**\n * Determines whether fullscreen mode is allowed or not.\n * @default true\n */\n allowsFullscreen?: boolean;\n\n /**\n * Determines whether the timecodes should be displayed or not.\n * @default true\n * @platform ios\n */\n showsTimecodes?: boolean;\n\n /**\n * Determines whether the player allows the user to skip media content.\n * @default false\n * @platform android\n * @platform ios\n */\n requiresLinearPlayback?: boolean;\n\n /**\n * Determines the type of the surface used to render the video.\n * > This prop should not be changed at runtime.\n * @default 'surfaceView'\n * @platform android\n */\n surfaceType?: SurfaceType;\n\n /**\n * Determines the position offset of the video inside the container.\n * @default { dx: 0, dy: 0 }\n * @platform ios\n */\n contentPosition?: { dx?: number; dy?: number };\n\n /**\n * A callback to call after the video player enters Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios\n * @platform web\n */\n onPictureInPictureStart?: () => void;\n\n /**\n * A callback to call after the video player exits Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios\n * @platform web\n */\n onPictureInPictureStop?: () => void;\n\n /**\n * Determines whether the player allows Picture in Picture (PiP) mode.\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-app-config)\n * > has to be configured for the PiP to work.\n * @platform android\n * @platform ios\n * @platform web\n */\n allowsPictureInPicture?: boolean;\n\n /**\n * Determines whether a video should be played \"inline\", that is, within the element's playback area.\n * @platform web\n */\n playsInline?: boolean;\n\n /**\n * Determines whether the player should start Picture in Picture (PiP) automatically when the app is in the background.\n * > **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n *\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-app-config)\n * > has to be configured for the PiP to work.\n *\n * @default false\n * @platform android 12+\n * @platform ios\n */\n startsPictureInPictureAutomatically?: boolean;\n\n /**\n * Specifies whether to perform video frame analysis (Live Text in videos).\n * Check official [Apple documentation](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/allowsvideoframeanalysis) for more details.\n * @default true\n * @platform ios 16.0+\n */\n allowsVideoFrameAnalysis?: boolean;\n\n /**\n * A callback to call after the video player enters fullscreen mode.\n */\n onFullscreenEnter?: () => void;\n\n /**\n * A callback to call after the video player exits fullscreen mode.\n */\n onFullscreenExit?: () => void;\n\n /**\n * A callback to call after the mounted `VideoPlayer` has rendered the first frame into the `VideoView`.\n * This event can be used to hide any cover images that conceal the initial loading of the player.\n * > **Note:** This event may also be called during playback when the current video track changes (for example when the player switches video quality).\n */\n onFirstFrameRender?: () => void;\n\n /**\n * Determines whether the player should use the default ExoPlayer shutter that covers the `VideoView` before the first video frame is rendered.\n * Setting this property to `false` makes the Android behavior the same as iOS.\n *\n * @platform android\n * @default false\n */\n useExoShutter?: boolean;\n\n /**\n * Determines the [cross origin policy](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/crossorigin) used by the underlying native view on web.\n * If undefined, does not use CORS at all.\n *\n * @platform web\n * @default undefined\n */\n crossOrigin?: 'anonymous' | 'use-credentials';\n}\n"]}
|
|
1
|
+
{"version":3,"file":"VideoView.types.js","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"","sourcesContent":["import { ViewProps } from 'react-native';\n\nimport type { VideoPlayer } from './VideoPlayer.types';\n\n/**\n * Describes how a video should be scaled to fit in a container.\n * - `contain`: The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing.\n * - `cover`: The video maintains its aspect ratio and covers the entire container, potentially cropping some portions.\n * - `fill`: The video stretches/squeezes to completely fill the container, potentially causing distortion.\n */\nexport type VideoContentFit = 'contain' | 'cover' | 'fill';\n\n/**\n * Describes the type of the surface used to render the video.\n * - `surfaceView`: Uses the `SurfaceView` to render the video. This value should be used in the majority of cases. Provides significantly lower power consumption, better performance, and more features.\n * - `textureView`: Uses the `TextureView` to render the video. Should be used in cases where the SurfaceView is not supported or causes issues (for example, overlapping video views).\n *\n * You can learn more about surface types in the official [ExoPlayer documentation](https://developer.android.com/media/media3/ui/playerview#surfacetype).\n * @platform android\n */\nexport type SurfaceType = 'textureView' | 'surfaceView';\n\n/**\n * Describes the orientation of the video in fullscreen mode. Available values are:\n * - `default`: The video is displayed in any of the available device rotations.\n * - `portrait`: The video is displayed in one of two available portrait orientations and rotates between them.\n * - `portraitUp`: The video is displayed in the portrait orientation - the notch of the phone points upwards.\n * - `portraitDown`: The video is displayed in the portrait orientation - the notch of the phone points downwards.\n * - `landscape`: The video is displayed in one of two available landscape orientations and rotates between them.\n * - `landscapeLeft`: The video is displayed in the left landscape orientation - the notch of the phone is in the left palm of the user.\n * - `landscapeRight`: The video is displayed in the right landscape orientation - the notch of the phone is in the right palm of the user.\n */\nexport type FullscreenOrientation =\n | 'default'\n | 'portrait'\n | 'portraitUp'\n | 'portraitDown'\n | 'landscape'\n | 'landscapeLeft'\n | 'landscapeRight';\n\n/**\n * Describes the options for fullscreen video mode.\n */\nexport type FullscreenOptions = {\n /**\n * Specifies whether the fullscreen mode should be available to the user. When `false`, the fullscreen button will be hidden in the player.\n * Equivalent to the `allowsFullscreen` prop.\n * @default true\n */\n enable?: boolean;\n /**\n * Specifies the orientation of the video in fullscreen mode.\n * @default 'default'\n * @platform android\n * @platform ios\n */\n orientation?: FullscreenOrientation;\n /**\n * Specifies whether the app should exit fullscreen mode when the device is rotated to a different orientation than the one specified in the `orientation` prop.\n * For example, if the `orientation` prop is set to `landscape` and the device is rotated to `portrait`, the app will exit fullscreen mode.\n *\n * > This prop will have no effect if the `orientation` prop is set to `default`.\n * > The `VideoView` will never auto-exit fullscreen when the device auto-rotate feature has been disabled in settings.\n *\n * @default false\n * @platform android\n * @platform ios\n */\n autoExitOnRotate?: boolean;\n};\n\nexport interface VideoViewProps extends ViewProps {\n /**\n * A video player instance. Use [`useVideoPlayer()`](#usevideoplayersource-setup) hook to create one.\n */\n player: VideoPlayer;\n\n /**\n * Determines whether native controls should be displayed or not.\n * @default true\n */\n nativeControls?: boolean;\n\n /**\n * Describes how the video should be scaled to fit in the container.\n * Options are `'contain'`, `'cover'`, and `'fill'`.\n * @default 'contain'\n */\n contentFit?: VideoContentFit;\n\n /**\n * Determines whether fullscreen mode is allowed or not.\n *\n * > Note: This option has been deprecated in favor of the `fullscreenOptions` prop and will be disabled in the future.\n * @default true\n */\n allowsFullscreen?: boolean;\n\n /**\n * Determines the fullscreen mode options.\n */\n fullscreenOptions?: FullscreenOptions;\n\n /**\n * Determines whether the timecodes should be displayed or not.\n * @default true\n * @platform ios\n */\n showsTimecodes?: boolean;\n\n /**\n * Determines whether the player allows the user to skip media content.\n * @default false\n * @platform android\n * @platform ios\n */\n requiresLinearPlayback?: boolean;\n\n /**\n * Determines the type of the surface used to render the video.\n * > This prop should not be changed at runtime.\n * @default 'surfaceView'\n * @platform android\n */\n surfaceType?: SurfaceType;\n\n /**\n * Determines the position offset of the video inside the container.\n * @default { dx: 0, dy: 0 }\n * @platform ios\n */\n contentPosition?: { dx?: number; dy?: number };\n\n /**\n * A callback to call after the video player enters Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios\n * @platform web\n */\n onPictureInPictureStart?: () => void;\n\n /**\n * A callback to call after the video player exits Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios\n * @platform web\n */\n onPictureInPictureStop?: () => void;\n\n /**\n * Determines whether the player allows Picture in Picture (PiP) mode.\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-app-config)\n * > has to be configured for the PiP to work.\n * @platform android\n * @platform ios\n * @platform web\n */\n allowsPictureInPicture?: boolean;\n\n /**\n * Determines whether a video should be played \"inline\", that is, within the element's playback area.\n * @platform web\n */\n playsInline?: boolean;\n\n /**\n * Determines whether the player should start Picture in Picture (PiP) automatically when the app is in the background.\n * > **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n *\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-app-config)\n * > has to be configured for the PiP to work.\n *\n * @default false\n * @platform android 12+\n * @platform ios\n */\n startsPictureInPictureAutomatically?: boolean;\n\n /**\n * Specifies whether to perform video frame analysis (Live Text in videos).\n * Check official [Apple documentation](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/allowsvideoframeanalysis) for more details.\n * @default true\n * @platform ios 16.0+\n */\n allowsVideoFrameAnalysis?: boolean;\n\n /**\n * A callback to call after the video player enters fullscreen mode.\n */\n onFullscreenEnter?: () => void;\n\n /**\n * A callback to call after the video player exits fullscreen mode.\n */\n onFullscreenExit?: () => void;\n\n /**\n * A callback to call after the mounted `VideoPlayer` has rendered the first frame into the `VideoView`.\n * This event can be used to hide any cover images that conceal the initial loading of the player.\n * > **Note:** This event may also be called during playback when the current video track changes (for example when the player switches video quality).\n */\n onFirstFrameRender?: () => void;\n\n /**\n * Determines whether the player should use the default ExoPlayer shutter that covers the `VideoView` before the first video frame is rendered.\n * Setting this property to `false` makes the Android behavior the same as iOS.\n *\n * @platform android\n * @default false\n */\n useExoShutter?: boolean;\n\n /**\n * Determines the [cross origin policy](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/crossorigin) used by the underlying native view on web.\n * If undefined, does not use CORS at all.\n *\n * @platform web\n * @default undefined\n */\n crossOrigin?: 'anonymous' | 'use-credentials';\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoView.web.d.ts","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6D,MAAM,OAAO,CAAC;AAGlF,OAAO,WAA6B,MAAM,mBAAmB,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAsBxD,wBAAgB,2BAA2B,IAAI,OAAO,CAErD;AAED,eAAO,MAAM,SAAS;aAAiC,WAAW;
|
|
1
|
+
{"version":3,"file":"VideoView.web.d.ts","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6D,MAAM,OAAO,CAAC;AAGlF,OAAO,WAA6B,MAAM,mBAAmB,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAsBxD,wBAAgB,2BAA2B,IAAI,OAAO,CAErD;AAED,eAAO,MAAM,SAAS;aAAiC,WAAW;kDAuLhE,CAAC;AAEH,eAAe,SAAS,CAAC"}
|
package/build/VideoView.web.js
CHANGED
|
@@ -23,6 +23,7 @@ export function isPictureInPictureSupported() {
|
|
|
23
23
|
}
|
|
24
24
|
export const VideoView = forwardRef((props, ref) => {
|
|
25
25
|
const videoRef = useRef(null);
|
|
26
|
+
const fullscreenEnabled = props.fullscreenOptions?.enable ?? props.allowsFullscreen ?? true;
|
|
26
27
|
const mediaNodeRef = useRef(null);
|
|
27
28
|
const hasToSetupAudioContext = useRef(false);
|
|
28
29
|
const fullscreenChangeListener = useRef(null);
|
|
@@ -37,7 +38,7 @@ export const VideoView = forwardRef((props, ref) => {
|
|
|
37
38
|
const zeroGainNodeRef = useRef(null);
|
|
38
39
|
useImperativeHandle(ref, () => ({
|
|
39
40
|
enterFullscreen: async () => {
|
|
40
|
-
if (!
|
|
41
|
+
if (!fullscreenEnabled) {
|
|
41
42
|
return;
|
|
42
43
|
}
|
|
43
44
|
await videoRef.current?.requestFullscreen();
|
|
@@ -157,7 +158,7 @@ export const VideoView = forwardRef((props, ref) => {
|
|
|
157
158
|
detachAudioNodes();
|
|
158
159
|
};
|
|
159
160
|
}, [props.player]);
|
|
160
|
-
return (_jsx("video", { controls: props.nativeControls ?? true, controlsList:
|
|
161
|
+
return (_jsx("video", { controls: props.nativeControls ?? true, controlsList: fullscreenEnabled ? undefined : 'nofullscreen', crossOrigin: props.crossOrigin, style: {
|
|
161
162
|
...mapStyles(props.style),
|
|
162
163
|
objectFit: props.contentFit,
|
|
163
164
|
}, onPlay: () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoView.web.js","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';\nimport { StyleSheet } from 'react-native';\n\nimport VideoPlayer, { getSourceUri } from './VideoPlayer.web';\nimport type { VideoViewProps } from './VideoView.types';\n\nfunction createAudioContext(): AudioContext | null {\n return typeof window !== 'undefined' ? new window.AudioContext() : null;\n}\n\nfunction createZeroGainNode(audioContext: AudioContext | null): GainNode | null {\n const zeroGainNode = audioContext?.createGain() ?? null;\n\n if (audioContext && zeroGainNode) {\n zeroGainNode.gain.value = 0;\n zeroGainNode.connect(audioContext.destination);\n }\n return zeroGainNode;\n}\n\nfunction mapStyles(style: VideoViewProps['style']): React.CSSProperties {\n const flattenedStyles = StyleSheet.flatten(style);\n // Looking through react-native-web source code they also just pass styles directly without further conversions, so it's just a cast.\n return flattenedStyles as React.CSSProperties;\n}\n\nexport function isPictureInPictureSupported(): boolean {\n return typeof document === 'object' && typeof document.exitPictureInPicture === 'function';\n}\n\nexport const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoViewProps, ref) => {\n const videoRef = useRef<null | HTMLVideoElement>(null);\n const mediaNodeRef = useRef<null | MediaElementAudioSourceNode>(null);\n const hasToSetupAudioContext = useRef(false);\n const fullscreenChangeListener = useRef<null | (() => void)>(null);\n const isWaitingForFirstFrame = useRef(false);\n\n /**\n * Audio context is used to mute all but one video when multiple video views are playing from one player simultaneously.\n * Using audio context nodes allows muting videos without displaying the mute icon in the video player.\n * We have to keep the context that called createMediaElementSource(videoRef), as the method can't be called\n * for the second time with another context and there is no way to unbind the video and audio context afterward.\n */\n const audioContextRef = useRef<null | AudioContext>(null);\n const zeroGainNodeRef = useRef<null | GainNode>(null);\n\n useImperativeHandle(ref, () => ({\n enterFullscreen: async () => {\n if (!props.allowsFullscreen) {\n return;\n }\n await videoRef.current?.requestFullscreen();\n },\n exitFullscreen: async () => {\n await document.exitFullscreen();\n },\n startPictureInPicture: async () => {\n await videoRef.current?.requestPictureInPicture();\n },\n stopPictureInPicture: async () => {\n try {\n await document.exitPictureInPicture();\n } catch (e) {\n if (e instanceof DOMException && e.name === 'InvalidStateError') {\n console.warn('The VideoView is not in Picture-in-Picture mode.');\n } else {\n throw e;\n }\n }\n },\n }));\n\n useEffect(() => {\n const onEnter = () => {\n props.onPictureInPictureStart?.();\n };\n const onLeave = () => {\n props.onPictureInPictureStop?.();\n };\n const onLoadStart = () => {\n isWaitingForFirstFrame.current = true;\n };\n const onCanPlay = () => {\n if (isWaitingForFirstFrame.current) {\n props.onFirstFrameRender?.();\n }\n isWaitingForFirstFrame.current = false;\n };\n videoRef.current?.addEventListener('enterpictureinpicture', onEnter);\n videoRef.current?.addEventListener('leavepictureinpicture', onLeave);\n videoRef.current?.addEventListener('loadstart', onLoadStart);\n videoRef.current?.addEventListener('loadeddata', onCanPlay);\n\n return () => {\n videoRef.current?.removeEventListener('enterpictureinpicture', onEnter);\n videoRef.current?.removeEventListener('leavepictureinpicture', onLeave);\n videoRef.current?.removeEventListener('loadstart', onLoadStart);\n videoRef.current?.removeEventListener('loadeddata', onCanPlay);\n };\n }, [videoRef, props.onPictureInPictureStop, props.onPictureInPictureStart]);\n\n // Adds the video view as a candidate for being the audio source for the player (when multiple views play from one\n // player only one will emit audio).\n function attachAudioNodes() {\n const audioContext = audioContextRef.current;\n const zeroGainNode = zeroGainNodeRef.current;\n const mediaNode = mediaNodeRef.current;\n\n if (audioContext && zeroGainNode && mediaNode) {\n props.player.mountAudioNode(audioContext, zeroGainNode, mediaNode);\n } else {\n console.warn(\n \"Couldn't mount audio node, this might affect the audio playback when using multiple video views with the same player.\"\n );\n }\n }\n\n function detachAudioNodes() {\n const audioContext = audioContextRef.current;\n const mediaNode = mediaNodeRef.current;\n if (audioContext && mediaNode && videoRef.current) {\n props.player.unmountAudioNode(videoRef.current, audioContext, mediaNode);\n }\n }\n\n function maybeSetupAudioContext() {\n if (\n !hasToSetupAudioContext.current ||\n !navigator.userActivation.hasBeenActive ||\n !videoRef.current\n ) {\n return;\n }\n const audioContext = createAudioContext();\n\n detachAudioNodes();\n audioContextRef.current = audioContext;\n zeroGainNodeRef.current = createZeroGainNode(audioContextRef.current);\n mediaNodeRef.current = audioContext\n ? audioContext.createMediaElementSource(videoRef.current)\n : null;\n attachAudioNodes();\n hasToSetupAudioContext.current = false;\n }\n\n function fullscreenListener() {\n if (document.fullscreenElement === videoRef.current) {\n props.onFullscreenEnter?.();\n } else {\n props.onFullscreenExit?.();\n }\n }\n\n function setupFullscreenListener() {\n fullscreenChangeListener.current = fullscreenListener;\n videoRef.current?.addEventListener('fullscreenchange', fullscreenChangeListener.current);\n }\n\n function cleanupFullscreenListener() {\n if (fullscreenChangeListener.current) {\n videoRef.current?.removeEventListener('fullscreenchange', fullscreenChangeListener.current);\n fullscreenChangeListener.current = null;\n }\n }\n\n useEffect(() => {\n if (videoRef.current) {\n props.player?.mountVideoView(videoRef.current);\n }\n setupFullscreenListener();\n attachAudioNodes();\n\n return () => {\n if (videoRef.current) {\n props.player?.unmountVideoView(videoRef.current);\n }\n cleanupFullscreenListener();\n detachAudioNodes();\n };\n }, [props.player]);\n\n return (\n <video\n controls={props.nativeControls ?? true}\n controlsList={props.allowsFullscreen ? undefined : 'nofullscreen'}\n crossOrigin={props.crossOrigin}\n style={{\n ...mapStyles(props.style),\n objectFit: props.contentFit,\n }}\n onPlay={() => {\n maybeSetupAudioContext();\n }}\n // The player can autoplay when muted, unmuting by a user should create the audio context\n onVolumeChange={() => {\n maybeSetupAudioContext();\n }}\n ref={(newRef) => {\n // This is called with a null value before `player.unmountVideoView` is called,\n // we can't assign null to videoRef if we want to unmount it from the player.\n if (newRef && !newRef.isEqualNode(videoRef.current)) {\n videoRef.current = newRef;\n hasToSetupAudioContext.current = true;\n maybeSetupAudioContext();\n }\n }}\n disablePictureInPicture={!props.allowsPictureInPicture}\n playsInline={props.playsInline}\n src={getSourceUri(props.player?.src) ?? ''}\n />\n );\n});\n\nexport default VideoView;\n"]}
|
|
1
|
+
{"version":3,"file":"VideoView.web.js","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';\nimport { StyleSheet } from 'react-native';\n\nimport VideoPlayer, { getSourceUri } from './VideoPlayer.web';\nimport type { VideoViewProps } from './VideoView.types';\n\nfunction createAudioContext(): AudioContext | null {\n return typeof window !== 'undefined' ? new window.AudioContext() : null;\n}\n\nfunction createZeroGainNode(audioContext: AudioContext | null): GainNode | null {\n const zeroGainNode = audioContext?.createGain() ?? null;\n\n if (audioContext && zeroGainNode) {\n zeroGainNode.gain.value = 0;\n zeroGainNode.connect(audioContext.destination);\n }\n return zeroGainNode;\n}\n\nfunction mapStyles(style: VideoViewProps['style']): React.CSSProperties {\n const flattenedStyles = StyleSheet.flatten(style);\n // Looking through react-native-web source code they also just pass styles directly without further conversions, so it's just a cast.\n return flattenedStyles as React.CSSProperties;\n}\n\nexport function isPictureInPictureSupported(): boolean {\n return typeof document === 'object' && typeof document.exitPictureInPicture === 'function';\n}\n\nexport const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoViewProps, ref) => {\n const videoRef = useRef<null | HTMLVideoElement>(null);\n const fullscreenEnabled =\n props.fullscreenOptions?.enable ?? props.allowsFullscreen ?? true;\n const mediaNodeRef = useRef<null | MediaElementAudioSourceNode>(null);\n const hasToSetupAudioContext = useRef(false);\n const fullscreenChangeListener = useRef<null | (() => void)>(null);\n const isWaitingForFirstFrame = useRef(false);\n\n /**\n * Audio context is used to mute all but one video when multiple video views are playing from one player simultaneously.\n * Using audio context nodes allows muting videos without displaying the mute icon in the video player.\n * We have to keep the context that called createMediaElementSource(videoRef), as the method can't be called\n * for the second time with another context and there is no way to unbind the video and audio context afterward.\n */\n const audioContextRef = useRef<null | AudioContext>(null);\n const zeroGainNodeRef = useRef<null | GainNode>(null);\n\n useImperativeHandle(ref, () => ({\n enterFullscreen: async () => {\n if (!fullscreenEnabled) {\n return;\n }\n await videoRef.current?.requestFullscreen();\n },\n exitFullscreen: async () => {\n await document.exitFullscreen();\n },\n startPictureInPicture: async () => {\n await videoRef.current?.requestPictureInPicture();\n },\n stopPictureInPicture: async () => {\n try {\n await document.exitPictureInPicture();\n } catch (e) {\n if (e instanceof DOMException && e.name === 'InvalidStateError') {\n console.warn('The VideoView is not in Picture-in-Picture mode.');\n } else {\n throw e;\n }\n }\n },\n }));\n\n useEffect(() => {\n const onEnter = () => {\n props.onPictureInPictureStart?.();\n };\n const onLeave = () => {\n props.onPictureInPictureStop?.();\n };\n const onLoadStart = () => {\n isWaitingForFirstFrame.current = true;\n };\n const onCanPlay = () => {\n if (isWaitingForFirstFrame.current) {\n props.onFirstFrameRender?.();\n }\n isWaitingForFirstFrame.current = false;\n };\n videoRef.current?.addEventListener('enterpictureinpicture', onEnter);\n videoRef.current?.addEventListener('leavepictureinpicture', onLeave);\n videoRef.current?.addEventListener('loadstart', onLoadStart);\n videoRef.current?.addEventListener('loadeddata', onCanPlay);\n\n return () => {\n videoRef.current?.removeEventListener('enterpictureinpicture', onEnter);\n videoRef.current?.removeEventListener('leavepictureinpicture', onLeave);\n videoRef.current?.removeEventListener('loadstart', onLoadStart);\n videoRef.current?.removeEventListener('loadeddata', onCanPlay);\n };\n }, [videoRef, props.onPictureInPictureStop, props.onPictureInPictureStart]);\n\n // Adds the video view as a candidate for being the audio source for the player (when multiple views play from one\n // player only one will emit audio).\n function attachAudioNodes() {\n const audioContext = audioContextRef.current;\n const zeroGainNode = zeroGainNodeRef.current;\n const mediaNode = mediaNodeRef.current;\n\n if (audioContext && zeroGainNode && mediaNode) {\n props.player.mountAudioNode(audioContext, zeroGainNode, mediaNode);\n } else {\n console.warn(\n \"Couldn't mount audio node, this might affect the audio playback when using multiple video views with the same player.\"\n );\n }\n }\n\n function detachAudioNodes() {\n const audioContext = audioContextRef.current;\n const mediaNode = mediaNodeRef.current;\n if (audioContext && mediaNode && videoRef.current) {\n props.player.unmountAudioNode(videoRef.current, audioContext, mediaNode);\n }\n }\n\n function maybeSetupAudioContext() {\n if (\n !hasToSetupAudioContext.current ||\n !navigator.userActivation.hasBeenActive ||\n !videoRef.current\n ) {\n return;\n }\n const audioContext = createAudioContext();\n\n detachAudioNodes();\n audioContextRef.current = audioContext;\n zeroGainNodeRef.current = createZeroGainNode(audioContextRef.current);\n mediaNodeRef.current = audioContext\n ? audioContext.createMediaElementSource(videoRef.current)\n : null;\n attachAudioNodes();\n hasToSetupAudioContext.current = false;\n }\n\n function fullscreenListener() {\n if (document.fullscreenElement === videoRef.current) {\n props.onFullscreenEnter?.();\n } else {\n props.onFullscreenExit?.();\n }\n }\n\n function setupFullscreenListener() {\n fullscreenChangeListener.current = fullscreenListener;\n videoRef.current?.addEventListener('fullscreenchange', fullscreenChangeListener.current);\n }\n\n function cleanupFullscreenListener() {\n if (fullscreenChangeListener.current) {\n videoRef.current?.removeEventListener('fullscreenchange', fullscreenChangeListener.current);\n fullscreenChangeListener.current = null;\n }\n }\n\n useEffect(() => {\n if (videoRef.current) {\n props.player?.mountVideoView(videoRef.current);\n }\n setupFullscreenListener();\n attachAudioNodes();\n\n return () => {\n if (videoRef.current) {\n props.player?.unmountVideoView(videoRef.current);\n }\n cleanupFullscreenListener();\n detachAudioNodes();\n };\n }, [props.player]);\n\n return (\n <video\n controls={props.nativeControls ?? true}\n controlsList={fullscreenEnabled ? undefined : 'nofullscreen'}\n crossOrigin={props.crossOrigin}\n style={{\n ...mapStyles(props.style),\n objectFit: props.contentFit,\n }}\n onPlay={() => {\n maybeSetupAudioContext();\n }}\n // The player can autoplay when muted, unmuting by a user should create the audio context\n onVolumeChange={() => {\n maybeSetupAudioContext();\n }}\n ref={(newRef) => {\n // This is called with a null value before `player.unmountVideoView` is called,\n // we can't assign null to videoRef if we want to unmount it from the player.\n if (newRef && !newRef.isEqualNode(videoRef.current)) {\n videoRef.current = newRef;\n hasToSetupAudioContext.current = true;\n maybeSetupAudioContext();\n }\n }}\n disablePictureInPicture={!props.allowsPictureInPicture}\n playsInline={props.playsInline}\n src={getSourceUri(props.player?.src) ?? ''}\n />\n );\n});\n\nexport default VideoView;\n"]}
|
package/build/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { isPictureInPictureSupported, clearVideoCacheAsync, setVideoCacheSizeAsync, getCurrentVideoCacheSize, preCacheVideoPartialAsync, preCacheVideoAsync, isVideoCachedAsync, } from './VideoModule';
|
|
2
2
|
export { VideoView } from './VideoView';
|
|
3
3
|
export { useVideoPlayer } from './VideoPlayer';
|
|
4
|
-
export { VideoContentFit, VideoViewProps, SurfaceType } from './VideoView.types';
|
|
4
|
+
export { VideoContentFit, VideoViewProps, SurfaceType, FullscreenOptions, FullscreenOrientation, } from './VideoView.types';
|
|
5
5
|
export { VideoThumbnail } from './VideoThumbnail';
|
|
6
6
|
export { createVideoPlayer } from './VideoPlayer';
|
|
7
7
|
export { VideoPlayer, VideoPlayerStatus, VideoSource, PlayerError, VideoMetadata, DRMType, DRMOptions, BufferOptions, AudioMixingMode, VideoThumbnailOptions, VideoSize, SubtitleTrack, AudioTrack, VideoTrack, ContentType, } from './VideoPlayer.types';
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,EACL,eAAe,EACf,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,WAAW,EACX,WAAW,EACX,aAAa,EACb,OAAO,EACP,UAAU,EACV,aAAa,EACb,eAAe,EACf,qBAAqB,EACrB,SAAS,EACT,aAAa,EACb,UAAU,EACV,UAAU,EACV,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,yBAAyB,EACzB,8BAA8B,EAC9B,wBAAwB,EACxB,uBAAuB,EACvB,sBAAsB,EACtB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC"}
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAS/C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EACL,WAAW,GAeZ,MAAM,qBAAqB,CAAC","sourcesContent":["export {\n isPictureInPictureSupported,\n clearVideoCacheAsync,\n setVideoCacheSizeAsync,\n getCurrentVideoCacheSize,\n preCacheVideoPartialAsync,\n preCacheVideoAsync,\n isVideoCachedAsync,\n} from './VideoModule';\nexport { VideoView } from './VideoView';\nexport { useVideoPlayer } from './VideoPlayer';\n\nexport {\n VideoContentFit,\n VideoViewProps,\n SurfaceType,\n FullscreenOptions,\n FullscreenOrientation,\n} from './VideoView.types';\nexport { VideoThumbnail } from './VideoThumbnail';\n\nexport { createVideoPlayer } from './VideoPlayer';\n\nexport {\n VideoPlayer,\n VideoPlayerStatus,\n VideoSource,\n PlayerError,\n VideoMetadata,\n DRMType,\n DRMOptions,\n BufferOptions,\n AudioMixingMode,\n VideoThumbnailOptions,\n VideoSize,\n SubtitleTrack,\n AudioTrack,\n VideoTrack,\n ContentType,\n} from './VideoPlayer.types';\n\nexport {\n VideoPlayerEvents,\n StatusChangeEventPayload,\n PlayingChangeEventPayload,\n PlaybackRateChangeEventPayload,\n VolumeChangeEventPayload,\n MutedChangeEventPayload,\n TimeUpdateEventPayload,\n SourceChangeEventPayload,\n SourceLoadEventPayload,\n} from './VideoPlayerEvents.types';\n"]}
|
package/expo-module.config.json
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
"modules": ["VideoModule"]
|
|
5
5
|
},
|
|
6
6
|
"android": {
|
|
7
|
-
"modules": ["expo.modules.video.VideoModule"]
|
|
7
|
+
"modules": ["expo.modules.video.VideoModule"],
|
|
8
|
+
"publication": {
|
|
9
|
+
"groupId": "host.exp.exponent",
|
|
10
|
+
"artifactId": "expo.modules.video",
|
|
11
|
+
"version": "1.0.6",
|
|
12
|
+
"repository": "local-maven-repo"
|
|
13
|
+
}
|
|
8
14
|
}
|
|
9
15
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
|
|
5
|
+
internal enum FullscreenOrientation: String, Enumerable {
|
|
6
|
+
case landscape
|
|
7
|
+
case portrait
|
|
8
|
+
case landscapeLeft
|
|
9
|
+
case landscapeRight
|
|
10
|
+
case portraitUp
|
|
11
|
+
case portraitDown
|
|
12
|
+
case `default`
|
|
13
|
+
|
|
14
|
+
#if !os(tvOS)
|
|
15
|
+
func toUIInterfaceOrientationMask() -> UIInterfaceOrientationMask {
|
|
16
|
+
switch self {
|
|
17
|
+
case .landscape:
|
|
18
|
+
return .landscape
|
|
19
|
+
case .portrait:
|
|
20
|
+
return [.portrait, .portraitUpsideDown]
|
|
21
|
+
case .landscapeLeft:
|
|
22
|
+
return .landscapeLeft
|
|
23
|
+
case .landscapeRight:
|
|
24
|
+
return .landscapeRight
|
|
25
|
+
case .portraitUp:
|
|
26
|
+
return .portrait
|
|
27
|
+
case .portraitDown:
|
|
28
|
+
return .portraitUpsideDown
|
|
29
|
+
case .default:
|
|
30
|
+
return .all
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
#endif
|
|
34
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import AVKit
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AVPlayerViewController with support for locking the fullscreen orientation, and other expo-video utility methods such as `enterPictureInPicture`
|
|
8
|
+
*/
|
|
9
|
+
internal class OrientationAVPlayerViewController: AVPlayerViewController, AVPlayerViewControllerDelegate {
|
|
10
|
+
weak var forwardDelegate: AVPlayerViewControllerDelegate?
|
|
11
|
+
#if !os(tvOS)
|
|
12
|
+
var fullscreenOrientation: UIInterfaceOrientationMask = UIDevice.current.userInterfaceIdiom == .phone ? .allButUpsideDown : .all
|
|
13
|
+
#endif
|
|
14
|
+
var autoExitOnRotate: Bool = false
|
|
15
|
+
|
|
16
|
+
// Used to determine whether the user has rotated the device to the target orientation. Useful for auto-exit for example:
|
|
17
|
+
// Device portrait, fullscreenOrientation - landscape
|
|
18
|
+
// We could auto-exit but we would do it right away causing ugly animations and confusion, instead we wait for the user to rotate to landscape.
|
|
19
|
+
// When they do, we know they acknowledged the orientaiton of the app and we can auto-exit once they rotate to a different orientation.
|
|
20
|
+
// In case of: device landscape, fullscreenOrientation - landscape
|
|
21
|
+
// We can set this to `true` right away.
|
|
22
|
+
private var hasRotatedToTargetOrientation = false
|
|
23
|
+
var isInPictureInPicture = false
|
|
24
|
+
|
|
25
|
+
var isFullscreen: Bool = false {
|
|
26
|
+
didSet {
|
|
27
|
+
guard oldValue == isFullscreen else {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
if !isFullscreen {
|
|
31
|
+
hasRotatedToTargetOrientation = false
|
|
32
|
+
}
|
|
33
|
+
#if os(tvOS)
|
|
34
|
+
hasRotatedToTargetOrientation = true
|
|
35
|
+
#else
|
|
36
|
+
// Check if the current device orientation lines up with target orientation right away after entering fullscreen
|
|
37
|
+
guard let deviceOrientationMask = UIDevice.current.orientation.toInterfaceOrientationMask(), isFullscreen else {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
hasRotatedToTargetOrientation = fullscreenOrientation.contains(deviceOrientationMask)
|
|
41
|
+
#endif
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#if !os(tvOS)
|
|
46
|
+
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
|
47
|
+
// Always remove the observer to avoid adding it multiple times
|
|
48
|
+
NotificationCenter.default.removeObserver(
|
|
49
|
+
self,
|
|
50
|
+
name: UIDevice.orientationDidChangeNotification,
|
|
51
|
+
object: nil
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if isFullscreen {
|
|
55
|
+
// Only add the observer when fullscreen, it's useful only for auto-exit
|
|
56
|
+
NotificationCenter.default.addObserver(
|
|
57
|
+
self,
|
|
58
|
+
selector: #selector(deviceOrientationDidChange(_:)),
|
|
59
|
+
name: UIDevice.orientationDidChangeNotification,
|
|
60
|
+
object: nil
|
|
61
|
+
)
|
|
62
|
+
return fullscreenOrientation
|
|
63
|
+
}
|
|
64
|
+
return super.supportedInterfaceOrientations
|
|
65
|
+
}
|
|
66
|
+
#endif
|
|
67
|
+
|
|
68
|
+
convenience init(delegate: AVPlayerViewControllerDelegate?) {
|
|
69
|
+
self.init()
|
|
70
|
+
self.forwardDelegate = delegate
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
deinit {
|
|
74
|
+
#if !os(tvOS)
|
|
75
|
+
NotificationCenter.default.removeObserver(
|
|
76
|
+
self,
|
|
77
|
+
name: UIDevice.orientationDidChangeNotification,
|
|
78
|
+
object: nil
|
|
79
|
+
)
|
|
80
|
+
#endif
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func enterFullscreen(selectorUnsupportedFallback: (() -> Void)?) {
|
|
84
|
+
let selectorName = "enterFullScreenAnimated:completionHandler:"
|
|
85
|
+
let selectorToEnterFullScreenMode = NSSelectorFromString(selectorName)
|
|
86
|
+
|
|
87
|
+
if self.responds(to: selectorToEnterFullScreenMode) {
|
|
88
|
+
self.perform(selectorToEnterFullScreenMode, with: true, with: nil)
|
|
89
|
+
} else {
|
|
90
|
+
selectorUnsupportedFallback?()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func exitFullscreen() {
|
|
95
|
+
if !isFullscreen {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let selectorName = "exitFullScreenAnimated:completionHandler:"
|
|
100
|
+
let selectorToExitFullScreenMode = NSSelectorFromString(selectorName)
|
|
101
|
+
|
|
102
|
+
if self.responds(to: selectorToExitFullScreenMode) {
|
|
103
|
+
self.perform(selectorToExitFullScreenMode, with: true, with: nil)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func startPictureInPicture() throws {
|
|
108
|
+
if isInPictureInPicture {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if !AVPictureInPictureController.isPictureInPictureSupported() {
|
|
112
|
+
throw PictureInPictureUnsupportedException()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let selectorName = "startPictureInPicture"
|
|
116
|
+
let selectorToStartPictureInPicture = NSSelectorFromString(selectorName)
|
|
117
|
+
|
|
118
|
+
if self.responds(to: selectorToStartPictureInPicture) {
|
|
119
|
+
self.perform(selectorToStartPictureInPicture)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func stopPictureInPicture() {
|
|
124
|
+
if !isInPictureInPicture {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
let selectorName = "stopPictureInPicture"
|
|
128
|
+
let selectorToStopPictureInPicture = NSSelectorFromString(selectorName)
|
|
129
|
+
|
|
130
|
+
if self.responds(to: selectorToStopPictureInPicture) {
|
|
131
|
+
self.perform(selectorToStopPictureInPicture)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
override func viewDidLoad() {
|
|
136
|
+
super.viewDidLoad()
|
|
137
|
+
self.delegate = self
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#if !os(tvOS)
|
|
141
|
+
@objc private func deviceOrientationDidChange(_ notification: Notification) {
|
|
142
|
+
guard let deviceOrientationMask = UIDevice.current.orientation.toInterfaceOrientationMask(), isFullscreen else {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
// IPhones generally don't support portraitUpsideDown, in that case we never want to exit, becasuse we would exit into an invalid app UI orientation
|
|
146
|
+
let isPortraitUpsideDownAndUnsupported = UIDevice.current.orientation == .portraitUpsideDown && UIDevice.current.userInterfaceIdiom == .phone
|
|
147
|
+
if isPortraitUpsideDownAndUnsupported {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
hasRotatedToTargetOrientation = fullscreenOrientation.contains(deviceOrientationMask) || hasRotatedToTargetOrientation
|
|
152
|
+
|
|
153
|
+
if autoExitOnRotate && !fullscreenOrientation.contains(deviceOrientationMask) && hasRotatedToTargetOrientation {
|
|
154
|
+
self.exitFullscreen()
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// MARK: - AVPlayerViewControllerDelegate
|
|
159
|
+
// TODO: Forward more methods to the forward delegate as needed
|
|
160
|
+
func playerViewController(
|
|
161
|
+
_ playerViewController: AVPlayerViewController,
|
|
162
|
+
willBeginFullScreenPresentationWithAnimationCoordinator coordinator: any UIViewControllerTransitionCoordinator
|
|
163
|
+
) {
|
|
164
|
+
forwardDelegate?.playerViewController?(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator)
|
|
165
|
+
coordinator.animate(alongsideTransition: nil) { [weak self] context in
|
|
166
|
+
if !context.isCancelled {
|
|
167
|
+
self?.isFullscreen = true
|
|
168
|
+
self?.forceRotationUpdate()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func playerViewController(
|
|
174
|
+
_ playerViewController: AVPlayerViewController,
|
|
175
|
+
willEndFullScreenPresentationWithAnimationCoordinator coordinator: any UIViewControllerTransitionCoordinator
|
|
176
|
+
) {
|
|
177
|
+
forwardDelegate?.playerViewController?(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator)
|
|
178
|
+
coordinator.animate(alongsideTransition: nil) { [weak self] context in
|
|
179
|
+
if !context.isCancelled {
|
|
180
|
+
self?.isFullscreen = false
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
#endif
|
|
185
|
+
|
|
186
|
+
func playerViewControllerDidStartPictureInPicture(_ playerViewController: AVPlayerViewController) {
|
|
187
|
+
isInPictureInPicture = true
|
|
188
|
+
forwardDelegate?.playerViewControllerDidStartPictureInPicture?(playerViewController)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) {
|
|
192
|
+
isInPictureInPicture = false
|
|
193
|
+
forwardDelegate?.playerViewControllerDidStopPictureInPicture?(playerViewController)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#if os(tvOS)
|
|
197
|
+
func playerViewControllerWillBeginDismissalTransition(_ playerViewController: AVPlayerViewController) {
|
|
198
|
+
forwardDelegate?.playerViewControllerWillBeginDismissalTransition?(playerViewController)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func playerViewControllerDidEndDismissalTransition(_ playerViewController: AVPlayerViewController) {
|
|
202
|
+
forwardDelegate?.playerViewControllerDidEndDismissalTransition?(playerViewController)
|
|
203
|
+
}
|
|
204
|
+
#endif
|
|
205
|
+
|
|
206
|
+
#if !os(tvOS)
|
|
207
|
+
private func forceRotationUpdate() {
|
|
208
|
+
if #available(iOS 16.0, *) {
|
|
209
|
+
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
|
|
210
|
+
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: fullscreenOrientation))
|
|
211
|
+
} else {
|
|
212
|
+
UIViewController.attemptRotationToDeviceOrientation()
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
#endif
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#if !os(tvOS)
|
|
219
|
+
fileprivate extension UIDeviceOrientation {
|
|
220
|
+
func toInterfaceOrientationMask() -> UIInterfaceOrientationMask? {
|
|
221
|
+
switch self {
|
|
222
|
+
case .portrait: return .portrait
|
|
223
|
+
case .portraitUpsideDown: return .portraitUpsideDown
|
|
224
|
+
case .landscapeLeft: return .landscapeLeft
|
|
225
|
+
case .landscapeRight: return .landscapeRight
|
|
226
|
+
case .unknown, .faceUp, .faceDown: return nil
|
|
227
|
+
@unknown default: return nil
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
#endif
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
|
|
5
|
+
internal struct FullscreenOptions: Record {
|
|
6
|
+
@Field
|
|
7
|
+
var enable: Bool = true
|
|
8
|
+
@Field
|
|
9
|
+
var orientation: FullscreenOrientation = FullscreenOrientation.default
|
|
10
|
+
@Field
|
|
11
|
+
var autoExitOnRotate: Bool = false
|
|
12
|
+
}
|
package/ios/VideoModule.swift
CHANGED
|
@@ -85,6 +85,14 @@ public final class VideoModule: Module {
|
|
|
85
85
|
)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
Prop("fullscreenOptions") { (view, options: FullscreenOptions?) in
|
|
89
|
+
#if !os(tvOS)
|
|
90
|
+
view.playerViewController.fullscreenOrientation = options?.orientation.toUIInterfaceOrientationMask() ?? .all
|
|
91
|
+
view.playerViewController.autoExitOnRotate = options?.autoExitOnRotate ?? false
|
|
92
|
+
view.playerViewController.setValue(options?.enable ?? true, forKey: "allowsEnteringFullScreen")
|
|
93
|
+
#endif
|
|
94
|
+
}
|
|
95
|
+
|
|
88
96
|
Prop("allowsFullscreen") { (view, allowsFullscreen: Bool?) in
|
|
89
97
|
#if !os(tvOS)
|
|
90
98
|
view.playerViewController.setValue(allowsFullscreen ?? true, forKey: "allowsEnteringFullScreen")
|
package/ios/VideoView.swift
CHANGED
|
@@ -4,7 +4,7 @@ import AVKit
|
|
|
4
4
|
import ExpoModulesCore
|
|
5
5
|
|
|
6
6
|
public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
7
|
-
lazy var playerViewController =
|
|
7
|
+
lazy var playerViewController = OrientationAVPlayerViewController(delegate: self)
|
|
8
8
|
|
|
9
9
|
weak var player: VideoPlayer? {
|
|
10
10
|
didSet {
|
|
@@ -14,11 +14,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
|
14
14
|
|
|
15
15
|
#if os(tvOS)
|
|
16
16
|
var wasPlaying: Bool = false
|
|
17
|
-
#endif
|
|
18
|
-
var isFullscreen: Bool = false
|
|
19
|
-
var isInPictureInPicture = false
|
|
20
|
-
#if os(tvOS)
|
|
21
17
|
let startPictureInPictureAutomatically = false
|
|
18
|
+
var isFullscreen: Bool = false
|
|
22
19
|
#else
|
|
23
20
|
var startPictureInPictureAutomatically = false {
|
|
24
21
|
didSet {
|
|
@@ -55,7 +52,6 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
|
55
52
|
VideoManager.shared.register(videoView: self)
|
|
56
53
|
|
|
57
54
|
clipsToBounds = true
|
|
58
|
-
playerViewController.delegate = self
|
|
59
55
|
playerViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
60
56
|
playerViewController.view.backgroundColor = .clear
|
|
61
57
|
// Now playing is managed by the `NowPlayingManager`
|
|
@@ -73,60 +69,34 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
func enterFullscreen() {
|
|
76
|
-
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
let selectorName = "enterFullScreenAnimated:completionHandler:"
|
|
80
|
-
let selectorToForceFullScreenMode = NSSelectorFromString(selectorName)
|
|
81
|
-
|
|
82
|
-
if playerViewController.responds(to: selectorToForceFullScreenMode) {
|
|
83
|
-
playerViewController.perform(selectorToForceFullScreenMode, with: true, with: nil)
|
|
84
|
-
} else {
|
|
72
|
+
let tvOSFallback = {
|
|
85
73
|
#if os(tvOS)
|
|
86
74
|
// For TV, save the currently playing state,
|
|
87
75
|
// remove the view controller from its superview,
|
|
88
76
|
// and present the view controller normally
|
|
89
|
-
wasPlaying = player?.isPlaying == true
|
|
77
|
+
self.wasPlaying = self.player?.isPlaying == true
|
|
90
78
|
self.playerViewController.view.removeFromSuperview()
|
|
91
79
|
self.reactViewController().present(self.playerViewController, animated: true)
|
|
92
|
-
onFullscreenEnter()
|
|
93
|
-
isFullscreen = true
|
|
80
|
+
self.onFullscreenEnter()
|
|
81
|
+
self.isFullscreen = true
|
|
94
82
|
#endif
|
|
95
83
|
}
|
|
84
|
+
playerViewController.enterFullscreen(selectorUnsupportedFallback: tvOSFallback)
|
|
96
85
|
}
|
|
97
86
|
|
|
98
87
|
func exitFullscreen() {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
let selectorToExitFullScreenMode = NSSelectorFromString(selectorName)
|
|
104
|
-
|
|
105
|
-
if playerViewController.responds(to: selectorToExitFullScreenMode) {
|
|
106
|
-
playerViewController.perform(selectorToExitFullScreenMode, with: true, with: nil)
|
|
107
|
-
}
|
|
88
|
+
playerViewController.exitFullscreen()
|
|
89
|
+
#if os(tvOS)
|
|
90
|
+
self.isFullscreen = false
|
|
91
|
+
#endif
|
|
108
92
|
}
|
|
109
93
|
|
|
110
94
|
func startPictureInPicture() throws {
|
|
111
|
-
|
|
112
|
-
throw PictureInPictureUnsupportedException()
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
let selectorName = "startPictureInPicture"
|
|
116
|
-
let selectorToStartPictureInPicture = NSSelectorFromString(selectorName)
|
|
117
|
-
|
|
118
|
-
if playerViewController.responds(to: selectorToStartPictureInPicture) {
|
|
119
|
-
playerViewController.perform(selectorToStartPictureInPicture)
|
|
120
|
-
}
|
|
95
|
+
try playerViewController.startPictureInPicture()
|
|
121
96
|
}
|
|
122
97
|
|
|
123
98
|
func stopPictureInPicture() {
|
|
124
|
-
|
|
125
|
-
let selectorToStopPictureInPicture = NSSelectorFromString(selectorName)
|
|
126
|
-
|
|
127
|
-
if playerViewController.responds(to: selectorToStopPictureInPicture) {
|
|
128
|
-
playerViewController.perform(selectorToStopPictureInPicture)
|
|
129
|
-
}
|
|
99
|
+
playerViewController.stopPictureInPicture()
|
|
130
100
|
}
|
|
131
101
|
|
|
132
102
|
// MARK: - AVPlayerViewControllerDelegate
|
|
@@ -162,7 +132,6 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
|
162
132
|
willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
|
163
133
|
) {
|
|
164
134
|
onFullscreenEnter()
|
|
165
|
-
isFullscreen = true
|
|
166
135
|
}
|
|
167
136
|
|
|
168
137
|
public func playerViewController(
|
|
@@ -171,27 +140,27 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
|
171
140
|
) {
|
|
172
141
|
// Platform's behavior is to pause the player when exiting the fullscreen mode.
|
|
173
142
|
// It seems better to continue playing, so we resume the player once the dismissing animation finishes.
|
|
174
|
-
let wasPlaying = player?.
|
|
143
|
+
let wasPlaying = player?.isPlaying ?? false
|
|
175
144
|
|
|
176
145
|
coordinator.animate(alongsideTransition: nil) { context in
|
|
177
|
-
if !context.isCancelled {
|
|
178
|
-
|
|
146
|
+
if !context.isCancelled && wasPlaying {
|
|
147
|
+
DispatchQueue.main.async {
|
|
179
148
|
self.player?.ref.play()
|
|
180
149
|
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if !context.isCancelled {
|
|
181
153
|
self.onFullscreenExit()
|
|
182
|
-
self.isFullscreen = false
|
|
183
154
|
}
|
|
184
155
|
}
|
|
185
156
|
}
|
|
186
157
|
#endif
|
|
187
158
|
|
|
188
159
|
public func playerViewControllerDidStartPictureInPicture(_ playerViewController: AVPlayerViewController) {
|
|
189
|
-
isInPictureInPicture = true
|
|
190
160
|
onPictureInPictureStart()
|
|
191
161
|
}
|
|
192
162
|
|
|
193
163
|
public func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) {
|
|
194
|
-
isInPictureInPicture = false
|
|
195
164
|
onPictureInPictureStop()
|
|
196
165
|
}
|
|
197
166
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stepincto/expo-video",
|
|
3
3
|
"title": "Expo Video",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.6",
|
|
5
5
|
"originalUpstreamVersion": "2.2.2",
|
|
6
6
|
"description": "A cross-platform, performant video component for React Native and Expo with Web support",
|
|
7
7
|
"main": "build/index.js",
|
|
@@ -36,12 +36,11 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/react": "^19.1.9",
|
|
38
38
|
"@types/react-dom": "^19.1.7",
|
|
39
|
-
"expo-module-scripts": "^
|
|
39
|
+
"expo-module-scripts": "^5.0.8",
|
|
40
40
|
"typescript": "^5.9.2"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"expo": "^
|
|
44
|
-
"expo-modules-core": ">=2.4 <2.6",
|
|
43
|
+
"expo": "^54.0.0",
|
|
45
44
|
"react": ">=18",
|
|
46
45
|
"react-native": ">=0.75"
|
|
47
46
|
},
|
package/src/VideoView.tsx
CHANGED
|
@@ -61,13 +61,38 @@ export class VideoView extends PureComponent<VideoViewProps> {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
render(): ReactNode {
|
|
64
|
-
const { player, ...props } = this.props;
|
|
64
|
+
const { player, allowsFullscreen, ...props } = this.props;
|
|
65
65
|
const playerId = getPlayerId(player);
|
|
66
66
|
|
|
67
|
+
if (allowsFullscreen !== undefined) {
|
|
68
|
+
console.warn(
|
|
69
|
+
'The `allowsFullscreen` prop is deprecated and will be removed in a future release. Use `fullscreenOptions` prop instead.'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const fullscreenOptions = {
|
|
74
|
+
enable: allowsFullscreen,
|
|
75
|
+
...props.fullscreenOptions,
|
|
76
|
+
};
|
|
77
|
+
|
|
67
78
|
if (NativeTextureVideoView && this.props.surfaceType === 'textureView') {
|
|
68
|
-
return
|
|
79
|
+
return (
|
|
80
|
+
<NativeTextureVideoView
|
|
81
|
+
{...props}
|
|
82
|
+
fullscreenOptions={fullscreenOptions}
|
|
83
|
+
player={playerId}
|
|
84
|
+
ref={this.nativeRef}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
69
87
|
}
|
|
70
|
-
return
|
|
88
|
+
return (
|
|
89
|
+
<NativeVideoView
|
|
90
|
+
{...props}
|
|
91
|
+
fullscreenOptions={fullscreenOptions}
|
|
92
|
+
player={playerId}
|
|
93
|
+
ref={this.nativeRef}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
71
96
|
}
|
|
72
97
|
}
|
|
73
98
|
|
package/src/VideoView.types.ts
CHANGED
|
@@ -20,6 +20,56 @@ export type VideoContentFit = 'contain' | 'cover' | 'fill';
|
|
|
20
20
|
*/
|
|
21
21
|
export type SurfaceType = 'textureView' | 'surfaceView';
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Describes the orientation of the video in fullscreen mode. Available values are:
|
|
25
|
+
* - `default`: The video is displayed in any of the available device rotations.
|
|
26
|
+
* - `portrait`: The video is displayed in one of two available portrait orientations and rotates between them.
|
|
27
|
+
* - `portraitUp`: The video is displayed in the portrait orientation - the notch of the phone points upwards.
|
|
28
|
+
* - `portraitDown`: The video is displayed in the portrait orientation - the notch of the phone points downwards.
|
|
29
|
+
* - `landscape`: The video is displayed in one of two available landscape orientations and rotates between them.
|
|
30
|
+
* - `landscapeLeft`: The video is displayed in the left landscape orientation - the notch of the phone is in the left palm of the user.
|
|
31
|
+
* - `landscapeRight`: The video is displayed in the right landscape orientation - the notch of the phone is in the right palm of the user.
|
|
32
|
+
*/
|
|
33
|
+
export type FullscreenOrientation =
|
|
34
|
+
| 'default'
|
|
35
|
+
| 'portrait'
|
|
36
|
+
| 'portraitUp'
|
|
37
|
+
| 'portraitDown'
|
|
38
|
+
| 'landscape'
|
|
39
|
+
| 'landscapeLeft'
|
|
40
|
+
| 'landscapeRight';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Describes the options for fullscreen video mode.
|
|
44
|
+
*/
|
|
45
|
+
export type FullscreenOptions = {
|
|
46
|
+
/**
|
|
47
|
+
* Specifies whether the fullscreen mode should be available to the user. When `false`, the fullscreen button will be hidden in the player.
|
|
48
|
+
* Equivalent to the `allowsFullscreen` prop.
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
enable?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Specifies the orientation of the video in fullscreen mode.
|
|
54
|
+
* @default 'default'
|
|
55
|
+
* @platform android
|
|
56
|
+
* @platform ios
|
|
57
|
+
*/
|
|
58
|
+
orientation?: FullscreenOrientation;
|
|
59
|
+
/**
|
|
60
|
+
* Specifies whether the app should exit fullscreen mode when the device is rotated to a different orientation than the one specified in the `orientation` prop.
|
|
61
|
+
* For example, if the `orientation` prop is set to `landscape` and the device is rotated to `portrait`, the app will exit fullscreen mode.
|
|
62
|
+
*
|
|
63
|
+
* > This prop will have no effect if the `orientation` prop is set to `default`.
|
|
64
|
+
* > The `VideoView` will never auto-exit fullscreen when the device auto-rotate feature has been disabled in settings.
|
|
65
|
+
*
|
|
66
|
+
* @default false
|
|
67
|
+
* @platform android
|
|
68
|
+
* @platform ios
|
|
69
|
+
*/
|
|
70
|
+
autoExitOnRotate?: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
23
73
|
export interface VideoViewProps extends ViewProps {
|
|
24
74
|
/**
|
|
25
75
|
* A video player instance. Use [`useVideoPlayer()`](#usevideoplayersource-setup) hook to create one.
|
|
@@ -41,10 +91,17 @@ export interface VideoViewProps extends ViewProps {
|
|
|
41
91
|
|
|
42
92
|
/**
|
|
43
93
|
* Determines whether fullscreen mode is allowed or not.
|
|
94
|
+
*
|
|
95
|
+
* > Note: This option has been deprecated in favor of the `fullscreenOptions` prop and will be disabled in the future.
|
|
44
96
|
* @default true
|
|
45
97
|
*/
|
|
46
98
|
allowsFullscreen?: boolean;
|
|
47
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Determines the fullscreen mode options.
|
|
102
|
+
*/
|
|
103
|
+
fullscreenOptions?: FullscreenOptions;
|
|
104
|
+
|
|
48
105
|
/**
|
|
49
106
|
* Determines whether the timecodes should be displayed or not.
|
|
50
107
|
* @default true
|
package/src/VideoView.web.tsx
CHANGED
|
@@ -30,6 +30,8 @@ export function isPictureInPictureSupported(): boolean {
|
|
|
30
30
|
|
|
31
31
|
export const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoViewProps, ref) => {
|
|
32
32
|
const videoRef = useRef<null | HTMLVideoElement>(null);
|
|
33
|
+
const fullscreenEnabled =
|
|
34
|
+
props.fullscreenOptions?.enable ?? props.allowsFullscreen ?? true;
|
|
33
35
|
const mediaNodeRef = useRef<null | MediaElementAudioSourceNode>(null);
|
|
34
36
|
const hasToSetupAudioContext = useRef(false);
|
|
35
37
|
const fullscreenChangeListener = useRef<null | (() => void)>(null);
|
|
@@ -46,7 +48,7 @@ export const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoView
|
|
|
46
48
|
|
|
47
49
|
useImperativeHandle(ref, () => ({
|
|
48
50
|
enterFullscreen: async () => {
|
|
49
|
-
if (!
|
|
51
|
+
if (!fullscreenEnabled) {
|
|
50
52
|
return;
|
|
51
53
|
}
|
|
52
54
|
await videoRef.current?.requestFullscreen();
|
|
@@ -182,7 +184,7 @@ export const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoView
|
|
|
182
184
|
return (
|
|
183
185
|
<video
|
|
184
186
|
controls={props.nativeControls ?? true}
|
|
185
|
-
controlsList={
|
|
187
|
+
controlsList={fullscreenEnabled ? undefined : 'nofullscreen'}
|
|
186
188
|
crossOrigin={props.crossOrigin}
|
|
187
189
|
style={{
|
|
188
190
|
...mapStyles(props.style),
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,13 @@ export {
|
|
|
10
10
|
export { VideoView } from './VideoView';
|
|
11
11
|
export { useVideoPlayer } from './VideoPlayer';
|
|
12
12
|
|
|
13
|
-
export {
|
|
13
|
+
export {
|
|
14
|
+
VideoContentFit,
|
|
15
|
+
VideoViewProps,
|
|
16
|
+
SurfaceType,
|
|
17
|
+
FullscreenOptions,
|
|
18
|
+
FullscreenOrientation,
|
|
19
|
+
} from './VideoView.types';
|
|
14
20
|
export { VideoThumbnail } from './VideoThumbnail';
|
|
15
21
|
|
|
16
22
|
export { createVideoPlayer } from './VideoPlayer';
|