expo-video 2.2.1 → 2.3.0-canary-20250713-8f814f8

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.
Files changed (121) hide show
  1. package/CHANGELOG.md +16 -2
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/AndroidManifest.xml +2 -2
  4. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +51 -5
  5. package/android/src/main/java/expo/modules/video/VideoExceptions.kt +3 -0
  6. package/android/src/main/java/expo/modules/video/VideoModule.kt +15 -1
  7. package/android/src/main/java/expo/modules/video/VideoView.kt +48 -31
  8. package/android/src/main/java/expo/modules/video/enums/FullscreenOrientation.kt +25 -0
  9. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +2 -1
  10. package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +3 -0
  11. package/android/src/main/java/expo/modules/video/records/FullscreenOptions.kt +12 -0
  12. package/android/src/main/java/expo/modules/video/utils/FullscreenActivityOrientationHelper.kt +120 -0
  13. package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +36 -3
  14. package/android/src/main/res/layout/fullscreen_player_activity.xml +2 -1
  15. package/build/NativeVideoAirPlayButtonView.d.ts +3 -0
  16. package/build/NativeVideoAirPlayButtonView.d.ts.map +1 -0
  17. package/build/NativeVideoAirPlayButtonView.js +3 -0
  18. package/build/NativeVideoAirPlayButtonView.js.map +1 -0
  19. package/build/NativeVideoModule.web.d.ts +3 -1
  20. package/build/NativeVideoModule.web.d.ts.map +1 -1
  21. package/build/NativeVideoModule.web.js +4 -1
  22. package/build/NativeVideoModule.web.js.map +1 -1
  23. package/build/VideoAirPlayButton.d.ts +10 -0
  24. package/build/VideoAirPlayButton.d.ts.map +1 -0
  25. package/build/VideoAirPlayButton.ios.d.ts +3 -0
  26. package/build/VideoAirPlayButton.ios.d.ts.map +1 -0
  27. package/build/VideoAirPlayButton.ios.js +5 -0
  28. package/build/VideoAirPlayButton.ios.js.map +1 -0
  29. package/build/VideoAirPlayButton.js +12 -0
  30. package/build/VideoAirPlayButton.js.map +1 -0
  31. package/build/VideoAirPlayButton.types.d.ts +35 -0
  32. package/build/VideoAirPlayButton.types.d.ts.map +1 -0
  33. package/build/VideoAirPlayButton.types.js +2 -0
  34. package/build/VideoAirPlayButton.types.js.map +1 -0
  35. package/build/VideoPlayer.types.d.ts +6 -0
  36. package/build/VideoPlayer.types.d.ts.map +1 -1
  37. package/build/VideoPlayer.types.js.map +1 -1
  38. package/build/VideoPlayer.web.d.ts +1 -0
  39. package/build/VideoPlayer.web.d.ts.map +1 -1
  40. package/build/VideoPlayer.web.js +1 -0
  41. package/build/VideoPlayer.web.js.map +1 -1
  42. package/build/VideoPlayerEvents.types.d.ts +16 -0
  43. package/build/VideoPlayerEvents.types.d.ts.map +1 -1
  44. package/build/VideoPlayerEvents.types.js.map +1 -1
  45. package/build/VideoView.d.ts.map +1 -1
  46. package/build/VideoView.js +3 -0
  47. package/build/VideoView.js.map +1 -1
  48. package/build/VideoView.types.d.ts +47 -0
  49. package/build/VideoView.types.d.ts.map +1 -1
  50. package/build/VideoView.types.js.map +1 -1
  51. package/build/index.d.ts +6 -4
  52. package/build/index.d.ts.map +1 -1
  53. package/build/index.js +1 -2
  54. package/build/index.js.map +1 -1
  55. package/expo-module.config.json +1 -1
  56. package/ios/Enums/FullscreenOrientation.swift +32 -0
  57. package/ios/ExpoVideo.podspec +1 -1
  58. package/ios/OrientationAVPlayerViewController.swift +215 -0
  59. package/ios/Records/FullscreenOptions.swift +12 -0
  60. package/ios/Records/VideoEventPayloads.swift +5 -0
  61. package/ios/VideoAirPlayButtonView.swift +70 -0
  62. package/ios/VideoManager.swift +2 -2
  63. package/ios/VideoModule.swift +31 -0
  64. package/ios/VideoPlayer.swift +9 -1
  65. package/ios/VideoPlayerItem.swift +5 -1
  66. package/ios/VideoPlayerObserver.swift +26 -0
  67. package/ios/VideoView.swift +13 -46
  68. package/local-maven-repo/host/exp/exponent/expo.modules.video/{2.2.1/expo.modules.video-2.2.1-sources.jar → 2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8-sources.jar} +0 -0
  69. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8-sources.jar.md5 +1 -0
  70. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8-sources.jar.sha1 +1 -0
  71. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8-sources.jar.sha256 +1 -0
  72. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8-sources.jar.sha512 +1 -0
  73. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.aar +0 -0
  74. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.aar.md5 +1 -0
  75. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.aar.sha1 +1 -0
  76. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.aar.sha256 +1 -0
  77. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.aar.sha512 +1 -0
  78. package/local-maven-repo/host/exp/exponent/expo.modules.video/{2.2.1/expo.modules.video-2.2.1.module → 2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.module} +24 -24
  79. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.module.md5 +1 -0
  80. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.module.sha1 +1 -0
  81. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.module.sha256 +1 -0
  82. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.module.sha512 +1 -0
  83. package/local-maven-repo/host/exp/exponent/expo.modules.video/{2.2.1/expo.modules.video-2.2.1.pom → 2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.pom} +2 -2
  84. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.pom.md5 +1 -0
  85. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.pom.sha1 +1 -0
  86. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.pom.sha256 +1 -0
  87. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.3.0-canary-20250713-8f814f8/expo.modules.video-2.3.0-canary-20250713-8f814f8.pom.sha512 +1 -0
  88. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml +4 -4
  89. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml.md5 +1 -1
  90. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml.sha1 +1 -1
  91. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml.sha256 +1 -1
  92. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml.sha512 +1 -1
  93. package/package.json +4 -5
  94. package/src/NativeVideoAirPlayButtonView.ts +3 -0
  95. package/src/NativeVideoModule.web.ts +4 -1
  96. package/src/VideoAirPlayButton.ios.tsx +8 -0
  97. package/src/VideoAirPlayButton.tsx +14 -0
  98. package/src/VideoAirPlayButton.types.ts +39 -0
  99. package/src/VideoPlayer.types.ts +7 -0
  100. package/src/VideoPlayer.web.tsx +1 -0
  101. package/src/VideoPlayerEvents.types.ts +19 -0
  102. package/src/VideoView.tsx +6 -0
  103. package/src/VideoView.types.ts +57 -0
  104. package/src/index.ts +6 -14
  105. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1-sources.jar.md5 +0 -1
  106. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1-sources.jar.sha1 +0 -1
  107. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1-sources.jar.sha256 +0 -1
  108. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1-sources.jar.sha512 +0 -1
  109. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.aar +0 -0
  110. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.aar.md5 +0 -1
  111. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.aar.sha1 +0 -1
  112. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.aar.sha256 +0 -1
  113. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.aar.sha512 +0 -1
  114. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.module.md5 +0 -1
  115. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.module.sha1 +0 -1
  116. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.module.sha256 +0 -1
  117. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.module.sha512 +0 -1
  118. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.pom.md5 +0 -1
  119. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.pom.sha1 +0 -1
  120. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.pom.sha256 +0 -1
  121. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.2.1/expo.modules.video-2.2.1.pom.sha512 +0 -1
package/CHANGELOG.md CHANGED
@@ -6,15 +6,29 @@
6
6
 
7
7
  ### 🎉 New features
8
8
 
9
+ - [iOS] Add complete support for AirPlay streaming. Add a device selection button and `VideoPlayer.isExternalPlaybackActive` property and appropriate listeners. ([#37207](https://github.com/expo/expo/pull/37207) by [@behenate](https://github.com/behenate))
10
+ - Add fullscreen orientation and auto-exit functionality. ([#36910](https://github.com/expo/expo/pull/36910) by [@behenate](https://github.com/behenate))
11
+
9
12
  ### 🐛 Bug fixes
10
13
 
14
+ - [Android] Fix accessing player.loop causes app to crash. ([#37928](https://github.com/expo/expo/pull/37928) by [@Wenszel](https://github.com/Wenszel))
15
+ - [iOS] Setting `player.currentTime` doesn't seek to the correct time on some videos. ([#37672](https://github.com/expo/expo/pull/37300) by [@petrkonecny2](https://github.com/petrkonecny2))
16
+
11
17
  ### 💡 Others
12
18
 
13
- ## 2.2.1 2025-06-10
19
+ - Export types using the `export type` syntax. ([#37396](https://github.com/expo/expo/pull/37396) by [@behenate](https://github.com/behenate))
20
+
21
+ ## 2.2.2 - 2025-06-18
22
+
23
+ ### 🐛 Bug fixes
24
+
25
+ - [Android] Fix aspect ratio of the Picture in Picture window when auto-entering for sources with ratio different from 16:9. ([#37225](https://github.com/expo/expo/pull/37225) by [@behenate](https://github.com/behenate))
26
+
27
+ ## 2.2.1 - 2025-06-10
14
28
 
15
29
  _This version does not introduce any user-facing changes._
16
30
 
17
- ## 2.2.0 2025-06-04
31
+ ## 2.2.0 - 2025-06-04
18
32
 
19
33
  ### 🎉 New features
20
34
 
@@ -4,13 +4,13 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'host.exp.exponent'
7
- version = '2.2.1'
7
+ version = '2.3.0-canary-20250713-8f814f8'
8
8
 
9
9
  android {
10
10
  namespace "expo.modules.video"
11
11
  defaultConfig {
12
12
  versionCode 1
13
- versionName '2.2.1'
13
+ versionName '2.3.0-canary-20250713-8f814f8'
14
14
  }
15
15
  }
16
16
 
@@ -6,8 +6,8 @@
6
6
  <application>
7
7
  <activity android:name=".FullscreenPlayerActivity"
8
8
  android:supportsPictureInPicture="true"
9
- android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
10
- android:theme="@style/Fullscreen"/>
9
+ android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|navigation"
10
+ android:theme="@style/Fullscreen" />
11
11
  <service
12
12
  android:name=".playbackService.ExpoVideoPlaybackService"
13
13
  android:exported="false"
@@ -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,8 +13,11 @@ 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
15
- import expo.modules.video.utils.applyAutoEnterPiP
16
+ import expo.modules.video.records.FullscreenOptions
17
+ import expo.modules.video.utils.FullscreenActivityOrientationHelper
18
+ import expo.modules.video.utils.applyPiPParams
16
19
  import expo.modules.video.utils.applyRectHint
20
+ import expo.modules.video.utils.calculatePiPAspectRatio
17
21
  import expo.modules.video.utils.calculateRectHint
18
22
 
19
23
  @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
@@ -25,26 +29,56 @@ class FullscreenPlayerActivity : Activity() {
25
29
  private lateinit var videoView: VideoView
26
30
  private var didFinish = false
27
31
  private var wasAutoPaused = false
32
+ private lateinit var options: FullscreenOptions
33
+ private lateinit var orientationHelper: FullscreenActivityOrientationHelper
28
34
 
29
35
  override fun onCreate(savedInstanceState: Bundle?) {
30
36
  super.onCreate(savedInstanceState)
31
- setContentView(R.layout.fullscreen_player_activity)
32
- mContentView = findViewById(R.id.enclosing_layout)
33
- playerView = findViewById(R.id.player_view)
34
37
 
35
38
  try {
36
39
  videoViewId = intent.getStringExtra(VideoManager.INTENT_PLAYER_KEY)
37
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
+ ?: throw FullScreenOptionsNotFoundException()
45
+ } else {
46
+ @Suppress("DEPRECATION")
47
+ intent.getSerializableExtra(INTENT_FULLSCREEN_OPTIONS_KEY) as? FullscreenOptions
48
+ ?: throw FullScreenOptionsNotFoundException()
49
+ }
50
+
38
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()
39
64
  } catch (e: CodedException) {
40
65
  Log.e("ExpoVideo", "${e.message}", e)
41
66
  finish()
42
67
  return
43
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
+
44
75
  videoPlayer = videoView.videoPlayer
45
76
  videoPlayer?.changePlayerView(playerView)
46
77
  VideoManager.registerFullscreenPlayerActivity(hashCode().toString(), this)
47
- applyAutoEnterPiP(this, videoView.autoEnterPiP)
78
+ playerView.player?.let {
79
+ val aspectRatio = calculatePiPAspectRatio(it.videoSize, playerView.width, playerView.height, videoView.contentFit)
80
+ applyPiPParams(this, videoView.autoEnterPiP, aspectRatio)
81
+ }
48
82
  }
49
83
 
50
84
  override fun onPostCreate(savedInstanceState: Bundle?) {
@@ -79,6 +113,7 @@ class FullscreenPlayerActivity : Activity() {
79
113
  }
80
114
 
81
115
  override fun onResume() {
116
+ orientationHelper.startOrientationEventListener()
82
117
  playerView.useController = videoView.useNativeControls
83
118
  super.onResume()
84
119
  }
@@ -91,6 +126,7 @@ class FullscreenPlayerActivity : Activity() {
91
126
  videoPlayer?.player?.pause()
92
127
  }
93
128
  }
129
+ orientationHelper.stopOrientationEventListener()
94
130
  super.onPause()
95
131
  }
96
132
 
@@ -98,6 +134,7 @@ class FullscreenPlayerActivity : Activity() {
98
134
  super.onDestroy()
99
135
  videoView.exitFullscreen()
100
136
  VideoManager.unregisterFullscreenPlayerActivity(hashCode().toString())
137
+ orientationHelper.stopOrientationEventListener()
101
138
  }
102
139
 
103
140
  private fun setupFullscreenButton() {
@@ -138,4 +175,13 @@ class FullscreenPlayerActivity : Activity() {
138
175
  )
139
176
  }
140
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
+ }
141
187
  }
@@ -6,6 +6,9 @@ import expo.modules.video.enums.DRMType
6
6
  internal class FullScreenVideoViewNotFoundException :
7
7
  CodedException("VideoView id wasn't passed to the activity")
8
8
 
9
+ internal class FullScreenOptionsNotFoundException :
10
+ CodedException("Fullscreen options were not passed to the activity")
11
+
9
12
  internal class VideoViewNotFoundException(id: String) :
10
13
  CodedException("VideoView with id: $id not found")
11
14
 
@@ -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
@@ -225,7 +226,9 @@ class VideoModule : Module() {
225
226
 
226
227
  Property("loop")
227
228
  .get { ref: VideoPlayer ->
228
- ref.player.repeatMode == REPEAT_MODE_ONE
229
+ runBlocking(appContext.mainQueue.coroutineContext) {
230
+ ref.player.repeatMode == REPEAT_MODE_ONE
231
+ }
229
232
  }
230
233
  .set { ref: VideoPlayer, loop: Boolean ->
231
234
  appContext.mainQueue.launch {
@@ -253,6 +256,12 @@ class VideoModule : Module() {
253
256
  ref.bufferOptions = bufferOptions
254
257
  }
255
258
 
259
+ Property("isExternalPlaybackActive")
260
+ .get { ref: VideoPlayer ->
261
+ // isExternalPlaybackActive is not supported on Android as of now. Return false.
262
+ false
263
+ }
264
+
256
265
  Function("play") { ref: VideoPlayer ->
257
266
  appContext.mainQueue.launch {
258
267
  ref.player.play()
@@ -381,6 +390,11 @@ private inline fun <reified T : VideoView> ViewDefinitionBuilder<T>.VideoViewCom
381
390
  Prop("allowsFullscreen") { view: T, allowsFullscreen: Boolean? ->
382
391
  view.allowsFullscreen = allowsFullscreen ?: true
383
392
  }
393
+ Prop("fullscreenOptions") { view: T, fullscreenOptions: FullscreenOptions? ->
394
+ if (fullscreenOptions != null) {
395
+ view.fullscreenOptions = fullscreenOptions
396
+ }
397
+ }
384
398
  Prop("requiresLinearPlayback") { view: T, requiresLinearPlayback: Boolean? ->
385
399
  val linearPlayback = requiresLinearPlayback ?: false
386
400
  view.playerView.applyRequiresLinearPlayback(linearPlayback)
@@ -15,6 +15,7 @@ import android.widget.FrameLayout
15
15
  import android.widget.ImageButton
16
16
  import androidx.fragment.app.FragmentActivity
17
17
  import androidx.media3.common.Tracks
18
+ import androidx.media3.common.VideoSize
18
19
  import androidx.media3.ui.PlayerView
19
20
  import com.facebook.react.bridge.ReactContext
20
21
  import com.facebook.react.uimanager.UIManagerHelper
@@ -27,8 +28,14 @@ import expo.modules.video.delegates.IgnoreSameSet
27
28
  import expo.modules.video.enums.ContentFit
28
29
  import expo.modules.video.player.VideoPlayer
29
30
  import expo.modules.video.player.VideoPlayerListener
30
- import expo.modules.video.utils.applyAutoEnterPiP
31
+ import expo.modules.video.records.AudioTrack
32
+ import expo.modules.video.records.SubtitleTrack
33
+ import expo.modules.video.records.VideoSource
34
+ import expo.modules.video.records.VideoTrack
35
+ import expo.modules.video.utils.applyPiPParams
36
+ import expo.modules.video.records.FullscreenOptions
31
37
  import expo.modules.video.utils.applyRectHint
38
+ import expo.modules.video.utils.calculatePiPAspectRatio
32
39
  import expo.modules.video.utils.calculateRectHint
33
40
  import expo.modules.video.utils.dispatchMotionEvent
34
41
  import java.util.UUID
@@ -82,7 +89,7 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
82
89
  }
83
90
 
84
91
  var autoEnterPiP: Boolean by IgnoreSameSet(false) { new, _ ->
85
- applyAutoEnterPiP(currentActivity, new)
92
+ applyPiPParams(currentActivity, new, calculateCurrentPipAspectRatio())
86
93
  }
87
94
 
88
95
  var contentFit: ContentFit = ContentFit.CONTAIN
@@ -126,6 +133,17 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
126
133
  field = value
127
134
  }
128
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
+
129
147
  private val mLayoutRunnable = Runnable {
130
148
  measure(
131
149
  MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
@@ -166,6 +184,7 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
166
184
  fun enterFullscreen() {
167
185
  val intent = Intent(context, FullscreenPlayerActivity::class.java)
168
186
  intent.putExtra(VideoManager.INTENT_PLAYER_KEY, videoViewId)
187
+ intent.putExtra(FullscreenPlayerActivity.INTENT_FULLSCREEN_OPTIONS_KEY, fullscreenOptions)
169
188
  // Set before starting the activity to avoid entering PiP unintentionally
170
189
  isInFullscreen = true
171
190
  currentActivity.startActivity(intent)
@@ -178,7 +197,7 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
178
197
  currentActivity.overridePendingTransition(0, 0)
179
198
  }
180
199
  onFullscreenEnter(Unit)
181
- applyAutoEnterPiP(currentActivity, false)
200
+ applyPiPParams(currentActivity, false, calculateCurrentPipAspectRatio())
182
201
  }
183
202
 
184
203
  fun attachPlayer() {
@@ -192,7 +211,7 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
192
211
  attachPlayer()
193
212
  onFullscreenExit(Unit)
194
213
  isInFullscreen = false
195
- applyAutoEnterPiP(currentActivity, autoEnterPiP)
214
+ applyPiPParams(currentActivity, autoEnterPiP, calculateCurrentPipAspectRatio())
196
215
  }
197
216
 
198
217
  fun enterPictureInPicture() {
@@ -203,32 +222,9 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
203
222
  val player = playerView.player
204
223
  ?: throw PictureInPictureEnterException("No player attached to the VideoView")
205
224
  playerView.useController = false
206
-
207
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
208
- var aspectRatio = if (contentFit == ContentFit.CONTAIN) {
209
- Rational(player.videoSize.width, player.videoSize.height)
210
- } else {
211
- Rational(width, height)
212
- }
213
- // AspectRatio for the activity in picture-in-picture, must be between 2.39:1 and 1:2.39 (inclusive).
214
- // https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)
215
- val maximumRatio = Rational(239, 100)
216
- val minimumRatio = Rational(100, 239)
217
- if (aspectRatio.toFloat() > maximumRatio.toFloat()) {
218
- aspectRatio = maximumRatio
219
- } else if (aspectRatio.toFloat() < minimumRatio.toFloat()) {
220
- aspectRatio = minimumRatio
221
- }
222
-
223
- currentActivity.setPictureInPictureParams(
224
- PictureInPictureParams
225
- .Builder()
226
- .setAspectRatio(aspectRatio)
227
- .build()
228
- )
229
- }
230
-
225
+ applyPiPParams(currentActivity, autoEnterPiP, calculateCurrentPipAspectRatio())
231
226
  willEnterPiP = true
227
+
232
228
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
233
229
  currentActivity.enterPictureInPictureMode(PictureInPictureParams.Builder().build())
234
230
  } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -237,6 +233,11 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
237
233
  }
238
234
  }
239
235
 
236
+ private fun calculateCurrentPipAspectRatio(): Rational? {
237
+ val player = videoPlayer?.player ?: return null
238
+ return calculatePiPAspectRatio(player.videoSize, this.width, this.height, contentFit)
239
+ }
240
+
240
241
  /**
241
242
  * For optimal picture in picture experience it's best to only have one view. This method
242
243
  * hides all children of the root view and makes the player the only visible child of the rootView.
@@ -263,6 +264,22 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
263
264
  this.addView(playerView)
264
265
  }
265
266
 
267
+ override fun onVideoSourceLoaded(
268
+ player: VideoPlayer,
269
+ videoSource: VideoSource?,
270
+ duration: Double?,
271
+ availableVideoTracks: List<VideoTrack>,
272
+ availableSubtitleTracks: List<SubtitleTrack>,
273
+ availableAudioTracks: List<AudioTrack>
274
+ ) {
275
+ availableVideoTracks.firstOrNull()?.let {
276
+ val videoSize = VideoSize(it.size.width, it.size.height)
277
+ val aspectRatio = calculatePiPAspectRatio(videoSize, this.width, this.height, contentFit)
278
+ applyPiPParams(currentActivity, autoEnterPiP, aspectRatio)
279
+ }
280
+ super.onVideoSourceLoaded(player, videoSource, duration, availableVideoTracks, availableSubtitleTracks, availableAudioTracks)
281
+ }
282
+
266
283
  override fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {
267
284
  showsSubtitlesButton = player.subtitles.availableSubtitleTracks.isNotEmpty()
268
285
  showsAudioTracksButton = player.audioTracks.availableAudioTracks.size > 1
@@ -302,7 +319,7 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
302
319
  .add(fragment, fragment.id)
303
320
  .commitAllowingStateLoss()
304
321
  }
305
- applyAutoEnterPiP(currentActivity, autoEnterPiP)
322
+ applyPiPParams(currentActivity, autoEnterPiP)
306
323
  }
307
324
 
308
325
  override fun onDetachedFromWindow() {
@@ -314,7 +331,7 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
314
331
  .remove(fragment)
315
332
  .commitAllowingStateLoss()
316
333
  }
317
- applyAutoEnterPiP(currentActivity, false)
334
+ applyPiPParams(currentActivity, false)
318
335
  }
319
336
 
320
337
  // After adding the `PlayerView` to the hierarchy the touch events stop being emitted to the JS side.
@@ -0,0 +1,25 @@
1
+ package expo.modules.video.enums
2
+ import expo.modules.kotlin.types.Enumerable
3
+ import android.content.pm.ActivityInfo
4
+
5
+ enum class FullscreenOrientation(val value: String) : Enumerable {
6
+ LANDSCAPE("landscape"),
7
+ PORTRAIT("portrait"),
8
+ LANDSCAPE_LEFT("landscapeLeft"),
9
+ LANDSCAPE_RIGHT("landscapeRight"),
10
+ PORTRAIT_UP("portraitUp"),
11
+ PORTRAIT_DOWN("portraitDown"),
12
+ DEFAULT("default");
13
+
14
+ fun toActivityOrientation(): Int {
15
+ return when (this) {
16
+ LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
17
+ PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
18
+ LANDSCAPE_LEFT -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
19
+ LANDSCAPE_RIGHT -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
20
+ PORTRAIT_UP -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
21
+ PORTRAIT_DOWN -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
22
+ DEFAULT -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
23
+ }
24
+ }
25
+ }
@@ -156,7 +156,8 @@ sealed class PlayerEvent {
156
156
  is AudioMixingModeChanged -> listeners.forEach { it.onAudioMixingModeChanged(player, audioMixingMode, oldAudioMixingMode) }
157
157
  is VideoTrackChanged -> listeners.forEach { it.onVideoTrackChanged(player, videoTrack, oldVideoTrack) }
158
158
  is RenderedFirstFrame -> listeners.forEach { it.onRenderedFirstFrame(player) }
159
- // JS-only events - VideoSourceLoaded, SubtitleTrackChanged - In the native events the TracksChanged can be used instead
159
+ is VideoSourceLoaded -> listeners.forEach { it.onVideoSourceLoaded(player, videoSource, duration, availableVideoTracks, availableSubtitleTracks, availableAudioTracks) }
160
+ // JS-only events - SubtitleTrackChanged - In the native events the TracksChanged can be used instead
160
161
  else -> Unit
161
162
  }
162
163
  }
@@ -6,7 +6,9 @@ import androidx.media3.common.Tracks
6
6
  import androidx.media3.common.util.UnstableApi
7
7
  import expo.modules.video.enums.AudioMixingMode
8
8
  import expo.modules.video.enums.PlayerStatus
9
+ import expo.modules.video.records.AudioTrack
9
10
  import expo.modules.video.records.PlaybackError
11
+ import expo.modules.video.records.SubtitleTrack
10
12
  import expo.modules.video.records.VideoSource
11
13
  import expo.modules.video.records.TimeUpdate
12
14
  import expo.modules.video.records.VideoTrack
@@ -25,5 +27,6 @@ interface VideoPlayerListener {
25
27
  fun onPlayedToEnd(player: VideoPlayer) {}
26
28
  fun onAudioMixingModeChanged(player: VideoPlayer, audioMixingMode: AudioMixingMode, oldAudioMixingMode: AudioMixingMode?) {}
27
29
  fun onVideoTrackChanged(player: VideoPlayer, videoTrack: VideoTrack?, oldVideoTrack: VideoTrack?) {}
30
+ fun onVideoSourceLoaded(player: VideoPlayer, videoSource: VideoSource?, duration: Double?, availableVideoTracks: List<VideoTrack>, availableSubtitleTracks: List<SubtitleTrack>, availableAudioTracks: List<AudioTrack>) {}
28
31
  fun onRenderedFirstFrame(player: VideoPlayer) {}
29
32
  }
@@ -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
+ }
@@ -5,11 +5,14 @@ import android.app.PictureInPictureParams
5
5
  import android.graphics.Rect
6
6
  import android.os.Build
7
7
  import android.util.Log
8
+ import android.util.Rational
8
9
  import androidx.annotation.OptIn
10
+ import androidx.media3.common.VideoSize
9
11
  import androidx.media3.common.util.UnstableApi
10
12
  import androidx.media3.ui.PlayerView
11
13
  import expo.modules.video.PictureInPictureConfigurationException
12
14
  import expo.modules.video.VideoView.Companion.isPictureInPictureSupported
15
+ import expo.modules.video.enums.ContentFit
13
16
 
14
17
  @OptIn(UnstableApi::class)
15
18
  internal fun calculateRectHint(playerView: PlayerView): Rect {
@@ -34,6 +37,25 @@ internal fun calculateRectHint(playerView: PlayerView): Rect {
34
37
  return hint
35
38
  }
36
39
 
40
+ internal fun calculatePiPAspectRatio(videoSize: VideoSize, viewWidth: Int, viewHeight: Int, contentFit: ContentFit): Rational {
41
+ var aspectRatio = if (contentFit == ContentFit.CONTAIN) {
42
+ Rational(videoSize.width, videoSize.height)
43
+ } else {
44
+ Rational(viewWidth, viewHeight)
45
+ }
46
+ // AspectRatio for the activity in picture-in-picture, must be between 2.39:1 and 1:2.39 (inclusive).
47
+ // https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)
48
+ val maximumRatio = Rational(239, 100)
49
+ val minimumRatio = Rational(100, 239)
50
+
51
+ if (aspectRatio.toFloat() > maximumRatio.toFloat()) {
52
+ aspectRatio = maximumRatio
53
+ } else if (aspectRatio.toFloat() < minimumRatio.toFloat()) {
54
+ aspectRatio = minimumRatio
55
+ }
56
+ return aspectRatio
57
+ }
58
+
37
59
  internal fun applyRectHint(activity: Activity, rectHint: Rect) {
38
60
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPictureSupported(activity)) {
39
61
  runWithPiPMisconfigurationSoftHandling {
@@ -54,10 +76,21 @@ internal fun runWithPiPMisconfigurationSoftHandling(shouldThrow: Boolean = false
54
76
  }
55
77
  }
56
78
 
57
- internal fun applyAutoEnterPiP(activity: Activity, autoEnterPiP: Boolean) {
58
- if (Build.VERSION.SDK_INT >= 31 && isPictureInPictureSupported(activity)) {
79
+ internal fun applyPiPParams(activity: Activity, autoEnterPiP: Boolean, aspectRatio: Rational? = null) {
80
+ // If the aspect ratio exceeds the limits, the app will crash
81
+ val safeAspectRatio = aspectRatio?.takeIf { it.toFloat() in 0.41841..2.39 }
82
+
83
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPictureSupported(activity)) {
84
+ val paramsBuilder = PictureInPictureParams.Builder()
85
+
86
+ safeAspectRatio?.let {
87
+ paramsBuilder.setAspectRatio(it)
88
+ }
89
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
90
+ paramsBuilder.setAutoEnterEnabled(autoEnterPiP)
91
+ }
59
92
  runWithPiPMisconfigurationSoftHandling {
60
- activity.setPictureInPictureParams(PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterPiP).build())
93
+ activity.setPictureInPictureParams(paramsBuilder.build())
61
94
  }
62
95
  }
63
96
  }
@@ -11,6 +11,7 @@
11
11
  <androidx.media3.ui.PlayerView
12
12
  android:id="@+id/player_view"
13
13
  android:layout_width="match_parent"
14
- android:layout_height="match_parent" />
14
+ android:layout_height="match_parent"
15
+ android:background="@android:color/black"/>
15
16
 
16
17
  </FrameLayout>
@@ -0,0 +1,3 @@
1
+ declare const _default: import("react").ComponentType<any>;
2
+ export default _default;
3
+ //# sourceMappingURL=NativeVideoAirPlayButtonView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeVideoAirPlayButtonView.d.ts","sourceRoot":"","sources":["../src/NativeVideoAirPlayButtonView.ts"],"names":[],"mappings":";AAEA,wBAAwE"}