@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.
@@ -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
- playerView.useController = videoView.useNativeControls
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
+ }
@@ -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;CASpB"}
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"}
@@ -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.
@@ -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;QACxC,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QAErC,IAAI,sBAAsB,IAAI,IAAI,CAAC,KAAK,CAAC,WAAW,KAAK,aAAa,EAAE,CAAC;YACvE,OAAO,KAAC,sBAAsB,OAAK,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,GAAI,CAAC;QACtF,CAAC;QACD,OAAO,KAAC,eAAe,OAAK,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,GAAI,CAAC;IAC/E,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, ...props } = this.props;\n const playerId = getPlayerId(player);\n\n if (NativeTextureVideoView && this.props.surfaceType === 'textureView') {\n return <NativeTextureVideoView {...props} player={playerId} ref={this.nativeRef} />;\n }\n return <NativeVideoView {...props} player={playerId} ref={this.nativeRef} />;\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"]}
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;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;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
+ {"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;kDAqLhE,CAAC;AAEH,eAAe,SAAS,CAAC"}
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"}
@@ -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 (!props.allowsFullscreen) {
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: props.allowsFullscreen ? undefined : 'nofullscreen', crossOrigin: props.crossOrigin, style: {
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';
@@ -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,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACjF,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"}
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"}
@@ -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;AAG/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 { VideoContentFit, VideoViewProps, SurfaceType } 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"]}
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"]}
@@ -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
+ }
@@ -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")
@@ -4,7 +4,7 @@ import AVKit
4
4
  import ExpoModulesCore
5
5
 
6
6
  public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
7
- lazy var playerViewController = AVPlayerViewController()
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
- if isFullscreen {
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
- if !isFullscreen {
100
- return
101
- }
102
- let selectorName = "exitFullScreenAnimated:completionHandler:"
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
- if !AVPictureInPictureController.isPictureInPictureSupported() {
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
- let selectorName = "stopPictureInPicture"
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?.ref.timeControlStatus == .playing
143
+ let wasPlaying = player?.isPlaying ?? false
175
144
 
176
145
  coordinator.animate(alongsideTransition: nil) { context in
177
- if !context.isCancelled {
178
- if wasPlaying {
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.5",
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": "^4.1.10",
39
+ "expo-module-scripts": "^5.0.8",
40
40
  "typescript": "^5.9.2"
41
41
  },
42
42
  "peerDependencies": {
43
- "expo": "^53.0.0",
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 <NativeTextureVideoView {...props} player={playerId} ref={this.nativeRef} />;
79
+ return (
80
+ <NativeTextureVideoView
81
+ {...props}
82
+ fullscreenOptions={fullscreenOptions}
83
+ player={playerId}
84
+ ref={this.nativeRef}
85
+ />
86
+ );
69
87
  }
70
- return <NativeVideoView {...props} player={playerId} ref={this.nativeRef} />;
88
+ return (
89
+ <NativeVideoView
90
+ {...props}
91
+ fullscreenOptions={fullscreenOptions}
92
+ player={playerId}
93
+ ref={this.nativeRef}
94
+ />
95
+ );
71
96
  }
72
97
  }
73
98
 
@@ -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
@@ -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 (!props.allowsFullscreen) {
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={props.allowsFullscreen ? undefined : 'nofullscreen'}
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 { VideoContentFit, VideoViewProps, SurfaceType } from './VideoView.types';
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';