expo-video 2.1.8 → 2.1.10-canary-20250611-f0afe80

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 (91) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +6 -2
  4. package/android/src/main/java/expo/modules/video/VideoModule.kt +21 -1
  5. package/android/src/main/java/expo/modules/video/VideoView.kt +42 -35
  6. package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +3 -2
  7. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +22 -3
  8. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +16 -1
  9. package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
  10. package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +3 -0
  11. package/android/src/main/java/expo/modules/video/records/Tracks.kt +21 -0
  12. package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +12 -1
  13. package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +36 -3
  14. package/build/VideoPlayer.types.d.ts +30 -0
  15. package/build/VideoPlayer.types.d.ts.map +1 -1
  16. package/build/VideoPlayer.types.js.map +1 -1
  17. package/build/VideoPlayer.web.d.ts +3 -1
  18. package/build/VideoPlayer.web.d.ts.map +1 -1
  19. package/build/VideoPlayer.web.js +2 -0
  20. package/build/VideoPlayer.web.js.map +1 -1
  21. package/build/VideoPlayerEvents.types.d.ts +34 -1
  22. package/build/VideoPlayerEvents.types.d.ts.map +1 -1
  23. package/build/VideoPlayerEvents.types.js.map +1 -1
  24. package/build/VideoView.types.d.ts +9 -1
  25. package/build/VideoView.types.d.ts.map +1 -1
  26. package/build/VideoView.types.js.map +1 -1
  27. package/build/VideoView.web.js +1 -1
  28. package/build/VideoView.web.js.map +1 -1
  29. package/build/index.d.ts +1 -1
  30. package/build/index.d.ts.map +1 -1
  31. package/build/index.js.map +1 -1
  32. package/expo-module.config.json +1 -1
  33. package/ios/Records/Tracks.swift +13 -0
  34. package/ios/Records/VideoEventPayloads.swift +11 -0
  35. package/ios/VideoModule.swift +11 -0
  36. package/ios/VideoPlayer.swift +21 -2
  37. package/ios/VideoPlayerAudioTracks.swift +72 -0
  38. package/ios/VideoPlayerItem.swift +7 -1
  39. package/ios/VideoPlayerObserver.swift +89 -22
  40. package/ios/VideoSourceLoader.swift +44 -8
  41. package/ios/VideoSourceLoaderListener.swift +34 -0
  42. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80-sources.jar +0 -0
  43. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80-sources.jar.md5 +1 -0
  44. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80-sources.jar.sha1 +1 -0
  45. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80-sources.jar.sha256 +1 -0
  46. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80-sources.jar.sha512 +1 -0
  47. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.aar +0 -0
  48. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.aar.md5 +1 -0
  49. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.aar.sha1 +1 -0
  50. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.aar.sha256 +1 -0
  51. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.aar.sha512 +1 -0
  52. package/local-maven-repo/host/exp/exponent/expo.modules.video/{2.1.8/expo.modules.video-2.1.8.module → 2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.module} +24 -24
  53. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.module.md5 +1 -0
  54. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.module.sha1 +1 -0
  55. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.module.sha256 +1 -0
  56. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.module.sha512 +1 -0
  57. package/local-maven-repo/host/exp/exponent/expo.modules.video/{2.1.8/expo.modules.video-2.1.8.pom → 2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.pom} +2 -2
  58. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.pom.md5 +1 -0
  59. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.pom.sha1 +1 -0
  60. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.pom.sha256 +1 -0
  61. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.10-canary-20250611-f0afe80/expo.modules.video-2.1.10-canary-20250611-f0afe80.pom.sha512 +1 -0
  62. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml +4 -4
  63. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml.md5 +1 -1
  64. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml.sha1 +1 -1
  65. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml.sha256 +1 -1
  66. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml.sha512 +1 -1
  67. package/package.json +4 -5
  68. package/src/VideoPlayer.types.ts +35 -0
  69. package/src/VideoPlayer.web.tsx +3 -0
  70. package/src/VideoPlayerEvents.types.ts +40 -0
  71. package/src/VideoView.types.ts +10 -1
  72. package/src/VideoView.web.tsx +1 -1
  73. package/src/index.ts +1 -0
  74. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8-sources.jar +0 -0
  75. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8-sources.jar.md5 +0 -1
  76. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8-sources.jar.sha1 +0 -1
  77. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8-sources.jar.sha256 +0 -1
  78. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8-sources.jar.sha512 +0 -1
  79. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.aar +0 -0
  80. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.aar.md5 +0 -1
  81. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.aar.sha1 +0 -1
  82. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.aar.sha256 +0 -1
  83. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.aar.sha512 +0 -1
  84. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.module.md5 +0 -1
  85. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.module.sha1 +0 -1
  86. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.module.sha256 +0 -1
  87. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.module.sha512 +0 -1
  88. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.pom.md5 +0 -1
  89. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.pom.sha1 +0 -1
  90. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.pom.sha256 +0 -1
  91. package/local-maven-repo/host/exp/exponent/expo.modules.video/2.1.8/expo.modules.video-2.1.8.pom.sha512 +0 -1
package/CHANGELOG.md CHANGED
@@ -8,8 +8,36 @@
8
8
 
9
9
  ### 🐛 Bug fixes
10
10
 
11
+ - [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))
12
+
11
13
  ### 💡 Others
12
14
 
15
+ ## 2.2.1 - 2025-06-10
16
+
17
+ _This version does not introduce any user-facing changes._
18
+
19
+ ## 2.2.0 - 2025-06-04
20
+
21
+ ### 🎉 New features
22
+
23
+ - [Android][iOS] Added support for Audio Track feature. You can now set the audio track using `player.audioTrack` and list available audio tracks using `player.availableAudioTracks`. ([#36207](https://github.com/expo/expo/pull/36207) by [@HADeveloper](https://github.com/HADeveloper))
24
+
25
+ ### 🐛 Bug fixes
26
+
27
+ - [Android] Fix `onFirstFrameRender` not being emitted for sources with `pixelWidthHeightRatio` different than 1. ([#37009](https://github.com/expo/expo/pull/37009) by [@behenate](https://github.com/behenate))
28
+ - [Android] Fix `useExoShutter` prop not being exposed to the JS side. ([#37012](https://github.com/expo/expo/pull/37012) by [@behenate](https://github.com/behenate))
29
+ - [Android] Add missing `onFirstFrameRender` event to the `VideoView` definition. ([#37014](https://github.com/expo/expo/pull/37014) by [@behenate](https://github.com/behenate))
30
+ - [iOS] Fix player not entering 'error' state when loading fails on iOS. ([#37177](https://github.com/expo/expo/pull/37177) by [@behenate](https://github.com/behenate))
31
+ - [iOS] Fix player reporting status `readyToPlay` while a source is being loaded asynchronously. ([#37180](https://github.com/expo/expo/pull/37180) by [@behenate](https://github.com/behenate))
32
+ - [iOS] Fix player going into `loading` status for a single frame when unpausing with a full buffer. ([#37181](https://github.com/expo/expo/pull/37181) by [@behenate](https://github.com/behenate))
33
+ - [iOS] Fix player getting stuck in `loading` state for null sources. ([#37183](https://github.com/expo/expo/pull/37183) by [@behenate](https://github.com/behenate))
34
+
35
+ ## 2.1.9 — 2025-05-08
36
+
37
+ ### 🛠 Breaking changes
38
+
39
+ - [web] Add crossOrigin prop, change default value to no CORS. ([#36713](https://github.com/expo/expo/pull/36713) by [@aleqsio](https://github.com/aleqsio))
40
+
13
41
  ## 2.1.8 — 2025-04-30
14
42
 
15
43
  _This version does not introduce any user-facing changes._
@@ -4,13 +4,13 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'host.exp.exponent'
7
- version = '2.1.8'
7
+ version = '2.1.10-canary-20250611-f0afe80'
8
8
 
9
9
  android {
10
10
  namespace "expo.modules.video"
11
11
  defaultConfig {
12
12
  versionCode 1
13
- versionName '2.1.8'
13
+ versionName '2.1.10-canary-20250611-f0afe80'
14
14
  }
15
15
  }
16
16
 
@@ -12,8 +12,9 @@ import android.widget.ImageButton
12
12
  import androidx.media3.ui.PlayerView
13
13
  import expo.modules.kotlin.exception.CodedException
14
14
  import expo.modules.video.player.VideoPlayer
15
- import expo.modules.video.utils.applyAutoEnterPiP
15
+ import expo.modules.video.utils.applyPiPParams
16
16
  import expo.modules.video.utils.applyRectHint
17
+ import expo.modules.video.utils.calculatePiPAspectRatio
17
18
  import expo.modules.video.utils.calculateRectHint
18
19
 
19
20
  @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
@@ -44,7 +45,10 @@ class FullscreenPlayerActivity : Activity() {
44
45
  videoPlayer = videoView.videoPlayer
45
46
  videoPlayer?.changePlayerView(playerView)
46
47
  VideoManager.registerFullscreenPlayerActivity(hashCode().toString(), this)
47
- applyAutoEnterPiP(this, videoView.autoEnterPiP)
48
+ playerView.player?.let {
49
+ val aspectRatio = calculatePiPAspectRatio(it.videoSize, playerView.width, playerView.height, videoView.contentFit)
50
+ applyPiPParams(this, videoView.autoEnterPiP, aspectRatio)
51
+ }
48
52
  }
49
53
 
50
54
  override fun onPostCreate(savedInstanceState: Bundle?) {
@@ -21,6 +21,7 @@ import expo.modules.video.enums.ContentFit
21
21
  import expo.modules.video.player.VideoPlayer
22
22
  import expo.modules.video.records.BufferOptions
23
23
  import expo.modules.video.records.SubtitleTrack
24
+ import expo.modules.video.records.AudioTrack
24
25
  import expo.modules.video.records.VideoSource
25
26
  import expo.modules.video.records.VideoThumbnailOptions
26
27
  import expo.modules.video.utils.runWithPiPMisconfigurationSoftHandling
@@ -146,6 +147,21 @@ class VideoModule : Module() {
146
147
  }
147
148
  }
148
149
 
150
+ Property("availableAudioTracks")
151
+ .get { ref: VideoPlayer ->
152
+ ref.audioTracks.availableAudioTracks
153
+ }
154
+
155
+ Property("audioTrack")
156
+ .get { ref: VideoPlayer ->
157
+ ref.audioTracks.currentAudioTrack
158
+ }
159
+ .set { ref: VideoPlayer, audioTrack: AudioTrack? ->
160
+ appContext.mainQueue.launch {
161
+ ref.audioTracks.currentAudioTrack = audioTrack
162
+ }
163
+ }
164
+
149
165
  Property("currentOffsetFromLive")
150
166
  .get { ref: VideoPlayer ->
151
167
  runBlocking(appContext.mainQueue.coroutineContext) {
@@ -347,7 +363,8 @@ private inline fun <reified T : VideoView> ViewDefinitionBuilder<T>.VideoViewCom
347
363
  "onPictureInPictureStart",
348
364
  "onPictureInPictureStop",
349
365
  "onFullscreenEnter",
350
- "onFullscreenExit"
366
+ "onFullscreenExit",
367
+ "onFirstFrameRender"
351
368
  )
352
369
  Prop("player") { view: T, player: VideoPlayer ->
353
370
  view.videoPlayer = player
@@ -369,6 +386,9 @@ private inline fun <reified T : VideoView> ViewDefinitionBuilder<T>.VideoViewCom
369
386
  view.playerView.applyRequiresLinearPlayback(linearPlayback)
370
387
  view.videoPlayer?.requiresLinearPlayback = linearPlayback
371
388
  }
389
+ Prop("useExoShutter") { view: T, useExoShutter: Boolean? ->
390
+ view.useExoShutter = useExoShutter
391
+ }
372
392
  AsyncFunction("enterFullscreen") { view: T ->
373
393
  view.enterFullscreen()
374
394
  }.runOnQueue(Queues.MAIN)
@@ -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,13 @@ 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
31
36
  import expo.modules.video.utils.applyRectHint
37
+ import expo.modules.video.utils.calculatePiPAspectRatio
32
38
  import expo.modules.video.utils.calculateRectHint
33
39
  import expo.modules.video.utils.dispatchMotionEvent
34
40
  import java.util.UUID
@@ -55,6 +61,8 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
55
61
  private set
56
62
  var showsSubtitlesButton = false
57
63
  private set
64
+ var showsAudioTracksButton = false
65
+ private set
58
66
 
59
67
  private val currentActivity = appContext.throwingActivity
60
68
  private val decorView = currentActivity.window.decorView
@@ -70,17 +78,17 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
70
78
 
71
79
  var useExoShutter: Boolean? = null
72
80
  set(value) {
73
- if (value == false) {
74
- playerView.setShutterBackgroundColor(Color.TRANSPARENT)
75
- } else {
81
+ if (value == true) {
76
82
  playerView.setShutterBackgroundColor(Color.BLACK)
83
+ } else {
84
+ playerView.setShutterBackgroundColor(Color.TRANSPARENT)
77
85
  }
78
86
  applySurfaceViewVisibility()
79
87
  field = value
80
88
  }
81
89
 
82
90
  var autoEnterPiP: Boolean by IgnoreSameSet(false) { new, _ ->
83
- applyAutoEnterPiP(currentActivity, new)
91
+ applyPiPParams(currentActivity, new, calculateCurrentPipAspectRatio())
84
92
  }
85
93
 
86
94
  var contentFit: ContentFit = ContentFit.CONTAIN
@@ -154,7 +162,7 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
154
162
  }
155
163
 
156
164
  fun applySurfaceViewVisibility() {
157
- if (useExoShutter == false && shouldHideSurfaceView) {
165
+ if (useExoShutter != true && shouldHideSurfaceView) {
158
166
  playerView.videoSurfaceView?.alpha = 0f
159
167
  } else {
160
168
  playerView.videoSurfaceView?.alpha = 1f
@@ -176,7 +184,7 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
176
184
  currentActivity.overridePendingTransition(0, 0)
177
185
  }
178
186
  onFullscreenEnter(Unit)
179
- applyAutoEnterPiP(currentActivity, false)
187
+ applyPiPParams(currentActivity, false, calculateCurrentPipAspectRatio())
180
188
  }
181
189
 
182
190
  fun attachPlayer() {
@@ -190,7 +198,7 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
190
198
  attachPlayer()
191
199
  onFullscreenExit(Unit)
192
200
  isInFullscreen = false
193
- applyAutoEnterPiP(currentActivity, autoEnterPiP)
201
+ applyPiPParams(currentActivity, autoEnterPiP, calculateCurrentPipAspectRatio())
194
202
  }
195
203
 
196
204
  fun enterPictureInPicture() {
@@ -201,32 +209,9 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
201
209
  val player = playerView.player
202
210
  ?: throw PictureInPictureEnterException("No player attached to the VideoView")
203
211
  playerView.useController = false
204
-
205
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
206
- var aspectRatio = if (contentFit == ContentFit.CONTAIN) {
207
- Rational(player.videoSize.width, player.videoSize.height)
208
- } else {
209
- Rational(width, height)
210
- }
211
- // AspectRatio for the activity in picture-in-picture, must be between 2.39:1 and 1:2.39 (inclusive).
212
- // https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)
213
- val maximumRatio = Rational(239, 100)
214
- val minimumRatio = Rational(100, 239)
215
- if (aspectRatio.toFloat() > maximumRatio.toFloat()) {
216
- aspectRatio = maximumRatio
217
- } else if (aspectRatio.toFloat() < minimumRatio.toFloat()) {
218
- aspectRatio = minimumRatio
219
- }
220
-
221
- currentActivity.setPictureInPictureParams(
222
- PictureInPictureParams
223
- .Builder()
224
- .setAspectRatio(aspectRatio)
225
- .build()
226
- )
227
- }
228
-
212
+ applyPiPParams(currentActivity, autoEnterPiP, calculateCurrentPipAspectRatio())
229
213
  willEnterPiP = true
214
+
230
215
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
231
216
  currentActivity.enterPictureInPictureMode(PictureInPictureParams.Builder().build())
232
217
  } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -235,6 +220,11 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
235
220
  }
236
221
  }
237
222
 
223
+ private fun calculateCurrentPipAspectRatio(): Rational? {
224
+ val player = videoPlayer?.player ?: return null
225
+ return calculatePiPAspectRatio(player.videoSize, this.width, this.height, contentFit)
226
+ }
227
+
238
228
  /**
239
229
  * For optimal picture in picture experience it's best to only have one view. This method
240
230
  * hides all children of the root view and makes the player the only visible child of the rootView.
@@ -261,8 +251,25 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
261
251
  this.addView(playerView)
262
252
  }
263
253
 
254
+ override fun onVideoSourceLoaded(
255
+ player: VideoPlayer,
256
+ videoSource: VideoSource?,
257
+ duration: Double?,
258
+ availableVideoTracks: List<VideoTrack>,
259
+ availableSubtitleTracks: List<SubtitleTrack>,
260
+ availableAudioTracks: List<AudioTrack>
261
+ ) {
262
+ availableVideoTracks.firstOrNull()?.let {
263
+ val videoSize = VideoSize(it.size.width, it.size.height)
264
+ val aspectRatio = calculatePiPAspectRatio(videoSize, this.width, this.height, contentFit)
265
+ applyPiPParams(currentActivity, autoEnterPiP, aspectRatio)
266
+ }
267
+ super.onVideoSourceLoaded(player, videoSource, duration, availableVideoTracks, availableSubtitleTracks, availableAudioTracks)
268
+ }
269
+
264
270
  override fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {
265
271
  showsSubtitlesButton = player.subtitles.availableSubtitleTracks.isNotEmpty()
272
+ showsAudioTracksButton = player.audioTracks.availableAudioTracks.size > 1
266
273
  playerView.setShowSubtitleButton(showsSubtitlesButton)
267
274
  super.onTracksChanged(player, tracks)
268
275
  }
@@ -299,7 +306,7 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
299
306
  .add(fragment, fragment.id)
300
307
  .commitAllowingStateLoss()
301
308
  }
302
- applyAutoEnterPiP(currentActivity, autoEnterPiP)
309
+ applyPiPParams(currentActivity, autoEnterPiP)
303
310
  }
304
311
 
305
312
  override fun onDetachedFromWindow() {
@@ -311,7 +318,7 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
311
318
  .remove(fragment)
312
319
  .commitAllowingStateLoss()
313
320
  }
314
- applyAutoEnterPiP(currentActivity, false)
321
+ applyPiPParams(currentActivity, false)
315
322
  }
316
323
 
317
324
  // After adding the `PlayerView` to the hierarchy the touch events stop being emitted to the JS side.
@@ -64,7 +64,7 @@ internal class FirstFrameEventGenerator(
64
64
 
65
65
  private fun isPlayerSurfaceLayoutValid(): Boolean {
66
66
  // Sometimes the video size announced by the track will is 1px off the render size.
67
- val epsilon = 0.01
67
+ val epsilon = 0.05
68
68
  val player = playerReference.get() ?: run {
69
69
  return false
70
70
  }
@@ -75,13 +75,14 @@ internal class FirstFrameEventGenerator(
75
75
  val surfaceHeight = player.surfaceSize.height
76
76
  val sourceWidth = player.videoSize.width
77
77
  val sourceHeight = player.videoSize.height
78
+ val sourcePixelWidthHeightRatio = player.videoSize.pixelWidthHeightRatio
78
79
 
79
80
  if (surfaceWidth == 0 || surfaceHeight == 0) {
80
81
  return false
81
82
  }
82
83
 
83
84
  val surfaceAspectRatio = surfaceWidth.toFloat() / surfaceHeight
84
- val trackAspectRatio = sourceWidth.toFloat() / sourceHeight
85
+ val trackAspectRatio = sourceWidth.toFloat() / sourceHeight * sourcePixelWidthHeightRatio
85
86
 
86
87
  val videoSizeIsUnknown = sourceWidth == 0 || sourceHeight == 0
87
88
  val hasFillContentFit = currentPlayerView.resizeMode == ContentFit.FILL.toResizeMode()
@@ -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.AvailableSubtitleTracksChangedEventPayload
11
+ import expo.modules.video.records.AvailableAudioTracksChangedEventPayload
10
12
  import expo.modules.video.records.IsPlayingEventPayload
11
13
  import expo.modules.video.records.MutedChangedEventPayload
12
14
  import expo.modules.video.records.PlaybackError
@@ -15,6 +17,7 @@ import expo.modules.video.records.SourceChangedEventPayload
15
17
  import expo.modules.video.records.StatusChangedEventPayload
16
18
  import expo.modules.video.records.SubtitleTrack
17
19
  import expo.modules.video.records.SubtitleTrackChangedEventPayload
20
+ import expo.modules.video.records.AudioTrackChangedEventPayload
18
21
  import expo.modules.video.records.TimeUpdate
19
22
  import expo.modules.video.records.VideoEventPayload
20
23
  import expo.modules.video.records.VideoSource
@@ -74,6 +77,11 @@ sealed class PlayerEvent {
74
77
  override val jsEventPayload = SubtitleTrackChangedEventPayload(subtitleTrack, oldSubtitleTrack)
75
78
  }
76
79
 
80
+ data class AudioTrackChanged(val audioTrack: AudioTrack?, val oldAudioTrack: AudioTrack?) : PlayerEvent() {
81
+ override val name = "audioTrackChange"
82
+ override val jsEventPayload = AudioTrackChangedEventPayload(audioTrack, oldAudioTrack)
83
+ }
84
+
77
85
  data class VideoTrackChanged(val videoTrack: VideoTrack?, val oldVideoTrack: VideoTrack?) : PlayerEvent() {
78
86
  override val name = "videoTrackChange"
79
87
  override val jsEventPayload = VideoTrackChangedEventPayload(videoTrack, oldVideoTrack)
@@ -94,18 +102,28 @@ sealed class PlayerEvent {
94
102
  override val jsEventPayload = AvailableSubtitleTracksChangedEventPayload(availableSubtitleTracks, oldAvailableSubtitleTracks)
95
103
  }
96
104
 
105
+ data class AvailableAudioTracksChanged(
106
+ val availableAudioTracks: List<AudioTrack>,
107
+ val oldAvailableAudioTracks: List<AudioTrack>
108
+ ) : PlayerEvent() {
109
+ override val name = "availableAudioTracksChange"
110
+ override val jsEventPayload = AvailableAudioTracksChangedEventPayload(availableAudioTracks, oldAvailableAudioTracks)
111
+ }
112
+
97
113
  data class VideoSourceLoaded(
98
114
  val videoSource: VideoSource?,
99
115
  val duration: Double,
100
116
  val availableVideoTracks: List<VideoTrack>,
101
- val availableSubtitleTracks: List<SubtitleTrack>
117
+ val availableSubtitleTracks: List<SubtitleTrack>,
118
+ val availableAudioTracks: List<AudioTrack>
102
119
  ) : PlayerEvent() {
103
120
  override val name = "sourceLoad"
104
121
  override val jsEventPayload = VideoSourceLoadedEventPayload(
105
122
  videoSource,
106
123
  duration,
107
124
  availableVideoTracks,
108
- availableSubtitleTracks
125
+ availableSubtitleTracks,
126
+ availableAudioTracks
109
127
  )
110
128
  }
111
129
 
@@ -138,7 +156,8 @@ sealed class PlayerEvent {
138
156
  is AudioMixingModeChanged -> listeners.forEach { it.onAudioMixingModeChanged(player, audioMixingMode, oldAudioMixingMode) }
139
157
  is VideoTrackChanged -> listeners.forEach { it.onVideoTrackChanged(player, videoTrack, oldVideoTrack) }
140
158
  is RenderedFirstFrame -> listeners.forEach { it.onRenderedFirstFrame(player) }
141
- // 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
142
161
  else -> Unit
143
162
  }
144
163
  }
@@ -54,6 +54,7 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
54
54
  private var currentPlayerView = MutableWeakReference<PlayerView?>(null)
55
55
  val loadControl: VideoPlayerLoadControl = VideoPlayerLoadControl.Builder().build()
56
56
  val subtitles: VideoPlayerSubtitles = VideoPlayerSubtitles(this)
57
+ val audioTracks: VideoPlayerAudioTracks = VideoPlayerAudioTracks(this)
57
58
  val trackSelector = DefaultTrackSelector(context)
58
59
 
59
60
  val player = ExoPlayer
@@ -180,13 +181,17 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
180
181
 
181
182
  override fun onTracksChanged(tracks: Tracks) {
182
183
  val oldSubtitleTracks = ArrayList(subtitles.availableSubtitleTracks)
184
+ val oldAudioTracks = ArrayList(audioTracks.availableAudioTracks)
183
185
  val oldCurrentTrack = subtitles.currentSubtitleTrack
186
+ val oldCurrentAudioTrack = audioTracks.currentAudioTrack
184
187
 
185
188
  // Emit the tracks change event to update the subtitles
186
189
  sendEvent(PlayerEvent.TracksChanged(tracks))
187
190
 
188
191
  val newSubtitleTracks = subtitles.availableSubtitleTracks
192
+ val newAudioTracks = audioTracks.availableAudioTracks
189
193
  val newCurrentSubtitleTrack = subtitles.currentSubtitleTrack
194
+ val newCurrentAudioTrack = audioTracks.currentAudioTrack
190
195
  availableVideoTracks = tracks.toVideoTracks()
191
196
 
192
197
  if (isLoadingNewSource) {
@@ -195,7 +200,8 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
195
200
  commitedSource,
196
201
  this@VideoPlayer.player.duration / 1000.0,
197
202
  availableVideoTracks,
198
- newSubtitleTracks
203
+ newSubtitleTracks,
204
+ newAudioTracks
199
205
  )
200
206
  )
201
207
  isLoadingNewSource = false
@@ -204,18 +210,27 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
204
210
  if (!oldSubtitleTracks.toArray().contentEquals(newSubtitleTracks.toArray())) {
205
211
  sendEvent(PlayerEvent.AvailableSubtitleTracksChanged(newSubtitleTracks, oldSubtitleTracks))
206
212
  }
213
+ if (!oldAudioTracks.toArray().contentEquals(newAudioTracks.toArray())) {
214
+ sendEvent(PlayerEvent.AvailableAudioTracksChanged(newAudioTracks, oldAudioTracks))
215
+ }
207
216
  if (oldCurrentTrack != newCurrentSubtitleTrack) {
208
217
  sendEvent(PlayerEvent.SubtitleTrackChanged(newCurrentSubtitleTrack, oldCurrentTrack))
209
218
  }
219
+ if (oldCurrentAudioTrack != newCurrentAudioTrack) {
220
+ sendEvent(PlayerEvent.AudioTrackChanged(newCurrentAudioTrack, oldCurrentAudioTrack))
221
+ }
210
222
  super.onTracksChanged(tracks)
211
223
  }
212
224
 
213
225
  override fun onTrackSelectionParametersChanged(parameters: TrackSelectionParameters) {
214
226
  val oldTrack = subtitles.currentSubtitleTrack
227
+ val oldAudioTrack = audioTracks.currentAudioTrack
215
228
  sendEvent(PlayerEvent.TrackSelectionParametersChanged(parameters))
216
229
 
217
230
  val newTrack = subtitles.currentSubtitleTrack
231
+ val newAudioTrack = audioTracks.currentAudioTrack
218
232
  sendEvent(PlayerEvent.SubtitleTrackChanged(newTrack, oldTrack))
233
+ sendEvent(PlayerEvent.AudioTrackChanged(newAudioTrack, oldAudioTrack))
219
234
  super.onTrackSelectionParametersChanged(parameters)
220
235
  }
221
236
 
@@ -0,0 +1,125 @@
1
+ package expo.modules.video.player
2
+
3
+ import androidx.annotation.OptIn
4
+ import androidx.media3.common.C
5
+ import androidx.media3.common.Format
6
+ import androidx.media3.common.MimeTypes
7
+ import androidx.media3.common.TrackGroup
8
+ import androidx.media3.common.TrackSelectionOverride
9
+ import androidx.media3.common.TrackSelectionParameters
10
+ import androidx.media3.common.Tracks
11
+ import androidx.media3.common.util.UnstableApi
12
+ import expo.modules.video.records.AudioTrack
13
+ import java.lang.ref.WeakReference
14
+
15
+ @OptIn(UnstableApi::class)
16
+ class VideoPlayerAudioTracks(owner: VideoPlayer) : VideoPlayerListener {
17
+ private val owner = WeakReference(owner)
18
+ private val videoPlayer: VideoPlayer?
19
+ get() {
20
+ return owner.get()
21
+ }
22
+ private val formatsToGroups = mutableMapOf<Format, Pair<TrackGroup, Int>>()
23
+ private var currentAudioTrackFormat: Format? = null
24
+ private var currentOverride: TrackSelectionOverride? = null
25
+
26
+ var currentAudioTrack: AudioTrack?
27
+ get() {
28
+ return AudioTrack.fromFormat(currentAudioTrackFormat)
29
+ }
30
+ set(value) {
31
+ applyAudioTrack(value)
32
+ }
33
+ val availableAudioTracks = arrayListOf<AudioTrack>()
34
+
35
+ init {
36
+ owner.addListener(this)
37
+ }
38
+
39
+ fun setAudioTracksEnabled(enabled: Boolean) {
40
+ val currentParams = videoPlayer?.player?.trackSelectionParameters ?: return
41
+ var params = currentParams.buildUpon().setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, !enabled).build()
42
+ if (!enabled) {
43
+ params = params.buildUpon().clearOverridesOfType(C.TRACK_TYPE_AUDIO).build()
44
+ }
45
+ videoPlayer?.player?.trackSelectionParameters = params
46
+ }
47
+
48
+ // VideoPlayerListener
49
+ override fun onTrackSelectionParametersChanged(player: VideoPlayer, trackSelectionParameters: TrackSelectionParameters) {
50
+ currentAudioTrackFormat = findSelectedAudioFormat()
51
+ super.onTrackSelectionParametersChanged(player, trackSelectionParameters)
52
+ }
53
+
54
+ override fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {
55
+ formatsToGroups.clear()
56
+ availableAudioTracks.clear()
57
+ for (group in tracks.groups) {
58
+ for (i in 0..<group.length) {
59
+ val format: Format = group.getTrackFormat(i)
60
+
61
+ if (MimeTypes.isAudio(format.sampleMimeType)) {
62
+ formatsToGroups[format] = group.mediaTrackGroup to i
63
+ val track = AudioTrack.fromFormat(format) ?: continue
64
+ availableAudioTracks.add(track)
65
+ }
66
+ }
67
+ }
68
+ currentAudioTrackFormat = findSelectedAudioFormat()
69
+ super.onTracksChanged(player, tracks)
70
+ }
71
+
72
+ // Private methods
73
+ private fun applyAudioTrack(audioTrack: AudioTrack?) {
74
+ val player = videoPlayer?.player ?: return
75
+ var newParameters: TrackSelectionParameters = player.trackSelectionParameters
76
+ currentOverride?.let { override ->
77
+ newParameters = newParameters.buildUpon().clearOverridesOfType(C.TRACK_TYPE_AUDIO).build()
78
+ }
79
+ if (audioTrack == null) {
80
+ player.trackSelectionParameters = newParameters
81
+ setAudioTracksEnabled(false)
82
+ currentOverride = null
83
+ return
84
+ }
85
+ val format = formatsToGroups.keys.firstOrNull {
86
+ it.id == audioTrack.id
87
+ }
88
+ format?.let {
89
+ formatsToGroups[it]?.let { subtitlePair ->
90
+ val trackSelectionOverride = TrackSelectionOverride(subtitlePair.first, subtitlePair.second)
91
+ newParameters = newParameters.buildUpon().addOverride(trackSelectionOverride).build()
92
+ player.trackSelectionParameters = newParameters
93
+ setAudioTracksEnabled(true)
94
+ currentOverride = trackSelectionOverride
95
+ }
96
+ }
97
+ }
98
+
99
+ private fun findSelectedAudioFormat(): Format? {
100
+ val trackSelectionParameters = videoPlayer?.player?.trackSelectionParameters
101
+ val preferredAudioLanguages = trackSelectionParameters?.preferredAudioLanguages
102
+ val overriddenFormat: Format? = trackSelectionParameters?.overrides?.let {
103
+ for ((group, trackSelectionOverride) in it) {
104
+ if (group.type == C.TRACK_TYPE_AUDIO) {
105
+ // For audioTracks only one index will be replaced
106
+ return@let trackSelectionOverride.trackIndices.firstOrNull()?.let { index ->
107
+ group.getFormat(index)
108
+ }
109
+ }
110
+ }
111
+ return@let null
112
+ }
113
+
114
+ val preferredFormat: Format? = preferredAudioLanguages?.let { preferredAudioLanguages ->
115
+ for (preferredLanguage in preferredAudioLanguages) {
116
+ return@let formatsToGroups.keys.firstOrNull {
117
+ it.language == preferredLanguage
118
+ }
119
+ }
120
+ return@let null
121
+ }
122
+
123
+ return overriddenFormat ?: preferredFormat
124
+ }
125
+ }
@@ -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
  }
@@ -29,6 +29,27 @@ class SubtitleTrack(
29
29
  }
30
30
  }
31
31
 
32
+ class AudioTrack(
33
+ @Field val id: String,
34
+ @Field val language: String?,
35
+ @Field val label: String?
36
+ ) : Record, Serializable {
37
+ companion object {
38
+ fun fromFormat(format: Format?): AudioTrack? {
39
+ format ?: return null
40
+ val id = format.id ?: return null
41
+ val language = format.language
42
+ val label = language?.let { Locale(it).displayLanguage } ?: "Unknown"
43
+
44
+ return AudioTrack(
45
+ id = id,
46
+ language = language,
47
+ label = label
48
+ )
49
+ }
50
+ }
51
+ }
52
+
32
53
  @OptIn(UnstableApi::class)
33
54
  class VideoTrack(
34
55
  @Field val id: String,
@@ -50,6 +50,11 @@ class SubtitleTrackChangedEventPayload(
50
50
  @Field val oldSubtitleTrack: SubtitleTrack?
51
51
  ) : VideoEventPayload
52
52
 
53
+ class AudioTrackChangedEventPayload(
54
+ @Field val audioTrack: AudioTrack?,
55
+ @Field val oldAudioTrack: AudioTrack?
56
+ ) : VideoEventPayload
57
+
53
58
  class VideoTrackChangedEventPayload(
54
59
  @Field val videoTrack: VideoTrack?,
55
60
  @Field val oldVideoTrack: VideoTrack?
@@ -60,9 +65,15 @@ class AvailableSubtitleTracksChangedEventPayload(
60
65
  @Field val oldAvailableSubtitleTracks: List<SubtitleTrack>
61
66
  ) : VideoEventPayload
62
67
 
68
+ class AvailableAudioTracksChangedEventPayload(
69
+ @Field val availableAudioTracks: List<AudioTrack>,
70
+ @Field val oldAvailableAudioTracks: List<AudioTrack>
71
+ ) : VideoEventPayload
72
+
63
73
  class VideoSourceLoadedEventPayload(
64
74
  @Field val videoSource: VideoSource?,
65
75
  @Field val duration: Double,
66
76
  @Field val availableVideoTracks: List<VideoTrack>,
67
- @Field val availableSubtitleTracks: List<SubtitleTrack>
77
+ @Field val availableSubtitleTracks: List<SubtitleTrack>,
78
+ @Field val availableAudioTracks: List<AudioTrack>
68
79
  ) : VideoEventPayload