expo-video 55.0.5 → 55.0.7

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 +30 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +16 -12
  4. package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +51 -0
  5. package/android/src/main/java/expo/modules/video/VideoModule.kt +8 -8
  6. package/android/src/main/java/expo/modules/video/VideoView.kt +26 -4
  7. package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +5 -7
  8. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +42 -12
  9. package/android/src/main/java/expo/modules/video/records/ButtonOptions.kt +15 -0
  10. package/android/src/main/java/expo/modules/video/records/PlayerBuilderOptions.kt +13 -0
  11. package/android/src/main/java/expo/modules/video/records/Tracks.kt +26 -5
  12. package/build/VideoPlayer.d.ts +6 -4
  13. package/build/VideoPlayer.d.ts.map +1 -1
  14. package/build/VideoPlayer.js +9 -6
  15. package/build/VideoPlayer.js.map +1 -1
  16. package/build/VideoPlayer.types.d.ts +60 -1
  17. package/build/VideoPlayer.types.d.ts.map +1 -1
  18. package/build/VideoPlayer.types.js.map +1 -1
  19. package/build/VideoView.types.d.ts +61 -0
  20. package/build/VideoView.types.d.ts.map +1 -1
  21. package/build/VideoView.types.js.map +1 -1
  22. package/build/index.d.ts +1 -1
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js.map +1 -1
  25. package/expo-module.config.json +1 -1
  26. package/ios/Cache/CachedResource.swift +74 -7
  27. package/ios/Cache/CachingHelpers.swift +24 -9
  28. package/ios/Cache/MediaFileHandle.swift +54 -9
  29. package/ios/Cache/MediaInfo.swift +65 -22
  30. package/ios/Cache/ResourceLoaderDelegate.swift +38 -6
  31. package/ios/Cache/VideoCacheManager.swift +1 -1
  32. package/ios/NowPlayingManager.swift +2 -2
  33. package/ios/Records/Tracks.swift +66 -85
  34. package/ios/Utils/AVAssetVariant+VideoTracks.swift +17 -0
  35. package/ios/Utils/AvAssetTrack+VideoTracks.swift +52 -0
  36. package/ios/Utils/FourCharCode+toString.swift +25 -0
  37. package/ios/Utils/HlsUriUtils.swift +27 -0
  38. package/ios/VideoAsset.swift +7 -7
  39. package/ios/VideoManager.swift +2 -2
  40. package/ios/VideoModule.swift +1 -1
  41. package/ios/VideoPlayerAudioTracks.swift +2 -2
  42. package/ios/VideoPlayerItem.swift +55 -18
  43. package/ios/VideoPlayerObserver.swift +1 -12
  44. package/ios/VideoPlayerSubtitles.swift +2 -2
  45. package/local-maven-repo/host/exp/exponent/expo.modules.video/{55.0.5/expo.modules.video-55.0.5-sources.jar → 55.0.7/expo.modules.video-55.0.7-sources.jar} +0 -0
  46. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7-sources.jar.md5 +1 -0
  47. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7-sources.jar.sha1 +1 -0
  48. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7-sources.jar.sha256 +1 -0
  49. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7-sources.jar.sha512 +1 -0
  50. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.aar +0 -0
  51. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.aar.md5 +1 -0
  52. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.aar.sha1 +1 -0
  53. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.aar.sha256 +1 -0
  54. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.aar.sha512 +1 -0
  55. package/local-maven-repo/host/exp/exponent/expo.modules.video/{55.0.5/expo.modules.video-55.0.5.module → 55.0.7/expo.modules.video-55.0.7.module} +22 -22
  56. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.module.md5 +1 -0
  57. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.module.sha1 +1 -0
  58. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.module.sha256 +1 -0
  59. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.module.sha512 +1 -0
  60. package/local-maven-repo/host/exp/exponent/expo.modules.video/{55.0.5/expo.modules.video-55.0.5.pom → 55.0.7/expo.modules.video-55.0.7.pom} +13 -1
  61. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.pom.md5 +1 -0
  62. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.pom.sha1 +1 -0
  63. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.pom.sha256 +1 -0
  64. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.7/expo.modules.video-55.0.7.pom.sha512 +1 -0
  65. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml +4 -4
  66. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml.md5 +1 -1
  67. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml.sha1 +1 -1
  68. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml.sha256 +1 -1
  69. package/local-maven-repo/host/exp/exponent/expo.modules.video/maven-metadata.xml.sha512 +1 -1
  70. package/package.json +2 -2
  71. package/src/VideoPlayer.tsx +14 -7
  72. package/src/VideoPlayer.types.ts +73 -1
  73. package/src/VideoView.types.ts +63 -0
  74. package/src/index.ts +6 -1
  75. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5-sources.jar.md5 +0 -1
  76. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5-sources.jar.sha1 +0 -1
  77. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5-sources.jar.sha256 +0 -1
  78. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5-sources.jar.sha512 +0 -1
  79. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.aar +0 -0
  80. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.aar.md5 +0 -1
  81. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.aar.sha1 +0 -1
  82. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.aar.sha256 +0 -1
  83. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.aar.sha512 +0 -1
  84. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.module.md5 +0 -1
  85. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.module.sha1 +0 -1
  86. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.module.sha256 +0 -1
  87. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.module.sha512 +0 -1
  88. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.pom.md5 +0 -1
  89. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.pom.sha1 +0 -1
  90. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.pom.sha256 +0 -1
  91. package/local-maven-repo/host/exp/exponent/expo.modules.video/55.0.5/expo.modules.video-55.0.5.pom.sha512 +0 -1
package/CHANGELOG.md CHANGED
@@ -10,12 +10,40 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 55.0.7 — 2026-02-20
14
+
15
+ ### 🎉 New features
16
+
17
+ - [Android][iOS] Add `url` field to HLS video tracks. ([#41681](https://github.com/expo/expo/pull/41681) by [@behenate](https://github.com/behenate))
18
+ - [Android][iOS] Add `name`, `isDefault` and `autoSelect` fields to `AudioTrack` and `SubtitleTrack`. ([#43250](https://github.com/expo/expo/pull/43250) by [@behenate](https://github.com/behenate))
19
+
20
+ ### 💡 Others
21
+
22
+ - Update HLS video track fetching for iOS 26+. ([#41681](https://github.com/expo/expo/pull/41681) by [@behenate](https://github.com/behenate))
23
+
24
+ ## 55.0.6 — 2026-02-16
25
+
26
+ ### 🛠 Breaking changes
27
+
28
+ - [Android] The next and previous buttons are now hidden by default in the native `VideoView` controls. ([#42875](https://github.com/expo/expo/pull/42875) by [@behenate](https://github.com/behenate))
29
+
30
+ ### 🎉 New features
31
+
32
+ - [Android] Add `buttonConfiguration` prop to `VideoView`. ([#42875](https://github.com/expo/expo/pull/42875) by [@behenate](https://github.com/behenate))
33
+ - [Android] Add `PlayerBuilderOptions` parameter to `useVideoPlayer` hook to configure seek backward/forward increments. ([#43043](https://github.com/expo/expo/pull/43043) by [@behenate](https://github.com/behenate))
34
+
35
+ ### 🐛 Bug fixes
36
+
37
+ - [Android] Fix only one player getting released when reloading with multiple players present. ([#42780](https://github.com/expo/expo/pull/42780) by [@behenate](https://github.com/behenate))
38
+ - [iOS] Fix data getting corrupted when caching is enabled. ([#42621](https://github.com/expo/expo/pull/42621) by [@behenate](https://github.com/behenate))
39
+
13
40
  ## 55.0.5 — 2026-02-08
14
41
 
15
42
  ### 🐛 Bug fixes
16
43
 
17
44
  - [iOS] Prevents blocking main thread when loading asset tracks for non-HSL tracks ([#42037](https://github.com/expo/expo/pull/42037) by [@santitopo](https://github.com/santitopo))
18
45
  - [Android] Fix crash due to `SimpleCache` directory lock conflicts. ([#42723](https://github.com/expo/expo/pull/42723) by [@santitopo](https://github.com/santitopo))
46
+ - [Android] Avoid crash when FullscreenPlayerActivity init fails. ([#42943](https://github.com/expo/expo/pull/42943) by [@amyu](https://github.com/amyu))
19
47
 
20
48
  ## 55.0.4 — 2026-02-03
21
49
 
@@ -46,6 +74,7 @@ _This version does not introduce any user-facing changes._
46
74
  - [Android][iOS] Add `seek tolerance` and `scrubbingModeOptions` properties to the player. ([#40203](https://github.com/expo/expo/pull/40203) by [@behenate](https://github.com/behenate))
47
75
  - Allow assigning `null` value to the `player` prop of the `VideoView` ([#40860](https://github.com/expo/expo/pull/40860) by [@behenate](https://github.com/behenate))
48
76
  - [Android][iOS] Add `averageBitrate` and `peakBitrate` for video tracks. ([#41532](https://github.com/expo/expo/pull/41532) by [@behenate](https://github.com/behenate))
77
+ - [Android][iOS] Add `url` field to HLS video tracks. ([#41681](https://github.com/expo/expo/pull/41681) by [@behenate](https://github.com/behenate))
49
78
 
50
79
  ### 🐛 Bug fixes
51
80
 
@@ -65,6 +94,7 @@ _This version does not introduce any user-facing changes._
65
94
  - Add extract the object `VideoSource` type into separate `VideoSourceObject` type. ([#41514](https://github.com/expo/expo/pull/41514) by [@behenate](https://github.com/behenate))
66
95
  - [Android] Set property values on calling thread. ([#41533](https://github.com/expo/expo/pull/41533) by [@behenate](https://github.com/behenate))
67
96
  - Mark the video track `bitrate` field as deprecated. ([#41532](https://github.com/expo/expo/pull/41532) by [@behenate](https://github.com/behenate))
97
+ - Update HLS video track fetching for iOS 26+. ([#41681](https://github.com/expo/expo/pull/41681) by [@behenate](https://github.com/behenate))
68
98
 
69
99
  ## 3.0.15 - 2025-12-05
70
100
 
@@ -4,13 +4,13 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'host.exp.exponent'
7
- version = '55.0.5'
7
+ version = '55.0.7'
8
8
 
9
9
  android {
10
10
  namespace "expo.modules.video"
11
11
  defaultConfig {
12
12
  versionCode 1
13
- versionName '55.0.5'
13
+ versionName '55.0.7'
14
14
  }
15
15
  }
16
16
 
@@ -27,14 +27,14 @@ import expo.modules.video.managers.VideoManager
27
27
  @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
28
28
  class FullscreenPlayerActivity : Activity() {
29
29
  private lateinit var mContentView: View
30
- private lateinit var videoViewId: String
30
+ private var videoViewId: String? = null
31
31
  private var videoPlayer: VideoPlayer? = null
32
32
  private lateinit var playerView: PlayerView
33
33
  private lateinit var videoView: VideoView
34
34
  private var didFinish = false
35
35
  private var wasAutoPaused = false
36
36
  private lateinit var options: FullscreenOptions
37
- private lateinit var orientationHelper: FullscreenActivityOrientationHelper
37
+ private var orientationHelper: FullscreenActivityOrientationHelper? = null
38
38
  private var captioningChangeListener: CaptioningManager.CaptioningChangeListener? = null
39
39
 
40
40
  override fun onCreate(savedInstanceState: Bundle?) {
@@ -53,7 +53,8 @@ class FullscreenPlayerActivity : Activity() {
53
53
  ?: throw FullScreenOptionsNotFoundException()
54
54
  }
55
55
 
56
- videoView = VideoManager.getVideoView(videoViewId)
56
+ videoView = videoViewId?.let { VideoManager.getVideoView(it) }
57
+ ?: throw FullScreenVideoViewNotFoundException()
57
58
 
58
59
  orientationHelper = FullscreenActivityOrientationHelper(
59
60
  this,
@@ -65,7 +66,7 @@ class FullscreenPlayerActivity : Activity() {
65
66
  requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
66
67
  }
67
68
  )
68
- orientationHelper.startOrientationEventListener()
69
+ orientationHelper?.startOrientationEventListener()
69
70
  } catch (e: CodedException) {
70
71
  Log.e("ExpoVideo", "${e.message}", e)
71
72
  finish()
@@ -94,13 +95,16 @@ class FullscreenPlayerActivity : Activity() {
94
95
  super.onPostCreate(savedInstanceState)
95
96
  hideStatusBar()
96
97
  setupFullscreenButton()
97
- playerView.applyRequiresLinearPlayback(videoPlayer?.requiresLinearPlayback ?: false)
98
+ val requiresLinearPlayback = videoPlayer?.requiresLinearPlayback ?: false
99
+ val buttonConfig = videoView.buttonOptions.copy(showBottomBar = true) // Always show bottom bar in fullscreen mode so user can exit
100
+ playerView.applyButtonOptions(buttonConfig, requiresLinearPlayback)
101
+ playerView.setTimeBarInteractive(requiresLinearPlayback)
98
102
  playerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
99
103
  // On every re-layout ExoPlayer makes the timeBar interactive.
100
104
  // We need to disable it to keep scrubbing off.
101
- playerView.setTimeBarInteractive(videoPlayer?.requiresLinearPlayback ?: true)
105
+ playerView.setTimeBarInteractive(requiresLinearPlayback)
102
106
  }
103
- playerView.setShowSubtitleButton(videoView.showsSubtitlesButton)
107
+ playerView.setShowSubtitleButton(videoView.buttonOptions.showSubtitles ?: videoView.currentTrackHasSubtitles)
104
108
 
105
109
  // Configure subtitle view to fix sizing issues with embedded styles (same as VideoView)
106
110
  SubtitleUtils.configureSubtitleView(playerView, this)
@@ -116,7 +120,7 @@ class FullscreenPlayerActivity : Activity() {
116
120
  override fun finish() {
117
121
  super.finish()
118
122
  didFinish = true
119
- VideoManager.getVideoView(videoViewId).attachPlayer()
123
+ videoViewId?.let { VideoManager.getVideoView(it).attachPlayer() }
120
124
 
121
125
  // Disable the exit transition
122
126
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
@@ -128,7 +132,7 @@ class FullscreenPlayerActivity : Activity() {
128
132
  }
129
133
 
130
134
  override fun onResume() {
131
- orientationHelper.startOrientationEventListener()
135
+ orientationHelper?.startOrientationEventListener()
132
136
  playerView.useController = true
133
137
  // Reconfigure subtitles when resuming (handles returning from settings)
134
138
  SubtitleUtils.configureSubtitleView(playerView, this)
@@ -143,7 +147,7 @@ class FullscreenPlayerActivity : Activity() {
143
147
  videoPlayer?.player?.pause()
144
148
  }
145
149
  }
146
- orientationHelper.stopOrientationEventListener()
150
+ orientationHelper?.stopOrientationEventListener()
147
151
  super.onPause()
148
152
  }
149
153
 
@@ -159,7 +163,7 @@ class FullscreenPlayerActivity : Activity() {
159
163
 
160
164
  videoView.exitFullscreen()
161
165
  VideoManager.unregisterFullscreenPlayerActivity(hashCode().toString())
162
- orientationHelper.stopOrientationEventListener()
166
+ orientationHelper?.stopOrientationEventListener()
163
167
  }
164
168
 
165
169
  private fun setupFullscreenButton() {
@@ -213,7 +217,7 @@ class FullscreenPlayerActivity : Activity() {
213
217
 
214
218
  override fun onConfigurationChanged(newConfig: Configuration) {
215
219
  super.onConfigurationChanged(newConfig)
216
- orientationHelper.onConfigurationChanged(newConfig)
220
+ orientationHelper?.onConfigurationChanged(newConfig)
217
221
  }
218
222
 
219
223
  companion object {
@@ -34,3 +34,54 @@ internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
34
34
  android.view.View.GONE
35
35
  }
36
36
  }
37
+
38
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
39
+ internal fun PlayerView.setSettingsButtonVisibility(visible: Boolean) {
40
+ val settingsButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_settings)
41
+ settingsButton?.visibility = if (visible) {
42
+ android.view.View.VISIBLE
43
+ } else {
44
+ android.view.View.GONE
45
+ }
46
+ }
47
+
48
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
49
+ internal fun PlayerView.setPlayPauseButtonVisibility(visible: Boolean) {
50
+ val playPauseButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_play_pause)
51
+ playPauseButton?.visibility = if (visible) {
52
+ android.view.View.VISIBLE
53
+ } else {
54
+ android.view.View.GONE
55
+ }
56
+ }
57
+
58
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
59
+ internal fun PlayerView.setBottomBarVisibility(visible: Boolean) {
60
+ val bottomBar = findViewById<android.view.ViewGroup>(androidx.media3.ui.R.id.exo_bottom_bar)
61
+ bottomBar?.visibility = if (visible) {
62
+ android.view.View.VISIBLE
63
+ } else {
64
+ android.view.View.GONE
65
+ }
66
+
67
+ val progressBar = findViewById<DefaultTimeBar>(androidx.media3.ui.R.id.exo_progress)
68
+ progressBar?.visibility = if (visible) {
69
+ android.view.View.VISIBLE
70
+ } else {
71
+ android.view.View.GONE
72
+ }
73
+ }
74
+
75
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
76
+ internal fun PlayerView.applyButtonOptions(
77
+ config: expo.modules.video.records.ButtonOptions,
78
+ requiresLinearPlayback: Boolean = false
79
+ ) {
80
+ setShowFastForwardButton(!requiresLinearPlayback && config.showSeekForward)
81
+ setShowRewindButton(!requiresLinearPlayback && config.showSeekBackward)
82
+ setShowPreviousButton(!requiresLinearPlayback && config.showPrevious)
83
+ setShowNextButton(!requiresLinearPlayback && config.showNext)
84
+ setSettingsButtonVisibility(config.showSettings)
85
+ setPlayPauseButtonVisibility(config.showPlayPause)
86
+ setBottomBarVisibility(config.showBottomBar)
87
+ }
@@ -1,5 +1,3 @@
1
- @file:OptIn(EitherType::class)
2
-
3
1
  package expo.modules.video
4
2
 
5
3
  import android.net.Uri
@@ -8,7 +6,6 @@ import androidx.media3.common.Player.REPEAT_MODE_OFF
8
6
  import androidx.media3.common.Player.REPEAT_MODE_ONE
9
7
  import androidx.media3.common.util.UnstableApi
10
8
  import expo.modules.kotlin.Promise
11
- import expo.modules.kotlin.apifeatures.EitherType
12
9
  import expo.modules.kotlin.functions.Coroutine
13
10
  import expo.modules.kotlin.functions.Queues
14
11
  import expo.modules.kotlin.modules.Module
@@ -19,6 +16,8 @@ import expo.modules.video.enums.AudioMixingMode
19
16
  import expo.modules.video.enums.ContentFit
20
17
  import expo.modules.video.player.VideoPlayer
21
18
  import expo.modules.video.records.BufferOptions
19
+ import expo.modules.video.records.PlayerBuilderOptions
20
+ import expo.modules.video.records.ButtonOptions
22
21
  import expo.modules.video.records.FullscreenOptions
23
22
  import expo.modules.video.records.SubtitleTrack
24
23
  import expo.modules.video.records.AudioTrack
@@ -72,8 +71,8 @@ class VideoModule : Module() {
72
71
  }
73
72
 
74
73
  Class(VideoPlayer::class) {
75
- Constructor { source: VideoSource? ->
76
- val player = VideoPlayer(appContext.throwingActivity.applicationContext, appContext, source)
74
+ Constructor { source: VideoSource?, /* useSynchronousReplace - iOS-only */ _: Boolean?, playerBuilderOptions: PlayerBuilderOptions? ->
75
+ val player = VideoPlayer(appContext.throwingActivity.applicationContext, appContext, source, playerBuilderOptions)
77
76
  appContext.mainQueue.launch {
78
77
  player.prepare()
79
78
  }
@@ -410,9 +409,10 @@ private inline fun <reified T : VideoView> ViewDefinitionBuilder<T>.VideoViewCom
410
409
  }
411
410
  }
412
411
  Prop("requiresLinearPlayback") { view: T, requiresLinearPlayback: Boolean? ->
413
- val linearPlayback = requiresLinearPlayback ?: false
414
- view.playerView.applyRequiresLinearPlayback(linearPlayback)
415
- view.videoPlayer?.requiresLinearPlayback = linearPlayback
412
+ view.requiresLinearPlayback = requiresLinearPlayback ?: false
413
+ }
414
+ Prop("buttonOptions") { view: T, buttonOptions: ButtonOptions? ->
415
+ view.buttonOptions = buttonOptions ?: ButtonOptions()
416
416
  }
417
417
  Prop("useExoShutter") { view: T, useExoShutter: Boolean? ->
418
418
  view.useExoShutter = useExoShutter
@@ -26,6 +26,7 @@ import expo.modules.video.player.VideoPlayer
26
26
  import expo.modules.video.listeners.VideoPlayerListener
27
27
  import expo.modules.video.listeners.VideoViewListener
28
28
  import expo.modules.video.records.AudioTrack
29
+ import expo.modules.video.records.ButtonOptions
29
30
  import expo.modules.video.records.SubtitleTrack
30
31
  import expo.modules.video.records.VideoSource
31
32
  import expo.modules.video.records.VideoTrack
@@ -57,11 +58,25 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
57
58
  var wasAutoPaused: Boolean = false
58
59
  var isInFullscreen: Boolean = false
59
60
  private set
60
- var showsSubtitlesButton = false
61
+ var currentTrackHasSubtitles = false
61
62
  private set
62
63
  var showsAudioTracksButton = false
63
64
  private set
64
65
 
66
+ var requiresLinearPlayback: Boolean = false
67
+ set(value) {
68
+ field = value
69
+ videoPlayer?.requiresLinearPlayback = value
70
+ playerView.applyRequiresLinearPlayback(value)
71
+ applyButtonSettings()
72
+ }
73
+
74
+ var buttonOptions: ButtonOptions = ButtonOptions()
75
+ set(value) {
76
+ field = value
77
+ applyButtonSettings()
78
+ }
79
+
65
80
  private var listeners = mutableListOf<WeakReference<VideoViewListener>>()
66
81
  private val currentActivity = appContext.throwingActivity
67
82
  private val decorView = currentActivity.window.decorView
@@ -131,8 +146,9 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
131
146
 
132
147
  var useNativeControls: Boolean = true
133
148
  set(value) {
149
+ val shouldShowSubtitle = value && (buttonOptions.showSubtitles ?: currentTrackHasSubtitles)
134
150
  playerView.useController = value
135
- playerView.setShowSubtitleButton(value)
151
+ playerView.setShowSubtitleButton(shouldShowSubtitle)
136
152
  field = value
137
153
  }
138
154
 
@@ -291,9 +307,9 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
291
307
  }
292
308
 
293
309
  override fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {
294
- showsSubtitlesButton = player.subtitles.availableSubtitleTracks.isNotEmpty()
310
+ currentTrackHasSubtitles = player.subtitles.availableSubtitleTracks.isNotEmpty()
295
311
  showsAudioTracksButton = player.audioTracks.availableAudioTracks.size > 1
296
- playerView.setShowSubtitleButton(showsSubtitlesButton)
312
+ playerView.setShowSubtitleButton(buttonOptions.showSubtitles ?: currentTrackHasSubtitles)
297
313
  super.onTracksChanged(player, tracks)
298
314
  }
299
315
 
@@ -407,6 +423,12 @@ open class VideoView(context: Context, appContext: AppContext, useTextureView: B
407
423
  }
408
424
  }
409
425
 
426
+ private fun applyButtonSettings() {
427
+ val shouldShowSubtitle = buttonOptions.showSubtitles ?: currentTrackHasSubtitles
428
+ playerView.applyButtonOptions(buttonOptions, requiresLinearPlayback)
429
+ playerView.setShowSubtitleButton(shouldShowSubtitle)
430
+ }
431
+
410
432
  companion object {
411
433
  fun isPictureInPictureSupported(currentActivity: Activity): Boolean {
412
434
  return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && currentActivity.packageManager.hasSystemFeature(
@@ -28,10 +28,9 @@ internal class FirstFrameEventGenerator(
28
28
  appContext: AppContext,
29
29
  videoPlayer: VideoPlayer,
30
30
  private val currentViewReference: MutableWeakReference<VideoView?>,
31
- private val onFirstFrameRendered: () -> Unit
31
+ private var onFirstFrameRendered: (() -> Unit)?
32
32
  ) : Player.Listener, VideoPlayerListener {
33
33
  private val videoPlayerReference = WeakReference(videoPlayer)
34
- private val weakAppContext = WeakReference(appContext)
35
34
  private var hasPendingOnFirstFrame = false
36
35
  internal var hasSentFirstFrameForCurrentMediaItem = false
37
36
  private set
@@ -45,12 +44,11 @@ internal class FirstFrameEventGenerator(
45
44
  }
46
45
  }
47
46
 
47
+ @MainThread
48
48
  fun release() {
49
49
  videoPlayerReference.get()?.removeListener(this)
50
-
51
- weakAppContext.get()?.mainQueue?.launch {
52
- videoPlayerReference.get()?.player?.removeListener(this@FirstFrameEventGenerator)
53
- }
50
+ videoPlayerReference.get()?.player?.removeListener(this)
51
+ onFirstFrameRendered = null
54
52
  }
55
53
 
56
54
  override fun onRenderedFirstFrame() {
@@ -80,7 +78,7 @@ internal class FirstFrameEventGenerator(
80
78
  // We want to match the behavior across platforms, so we limit the number of event emissions.
81
79
  private fun maybeCallOnFirstFrameRendered() {
82
80
  if (!hasSentFirstFrameForCurrentMediaItem || !hasSentFirstFrameForCurrentVideoView) {
83
- onFirstFrameRendered()
81
+ onFirstFrameRendered?.invoke()
84
82
  }
85
83
  hasPendingOnFirstFrame = false
86
84
  hasSentFirstFrameForCurrentMediaItem = true
@@ -20,6 +20,7 @@ import androidx.media3.exoplayer.DecoderReuseEvaluation
20
20
  import androidx.media3.exoplayer.DefaultRenderersFactory
21
21
  import androidx.media3.exoplayer.ExoPlayer
22
22
  import androidx.media3.exoplayer.analytics.AnalyticsListener
23
+ import androidx.media3.exoplayer.hls.HlsManifest
23
24
  import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
24
25
  import androidx.media3.session.MediaSession
25
26
  import androidx.media3.ui.PlayerView
@@ -47,13 +48,17 @@ import expo.modules.video.records.VideoSource
47
48
  import expo.modules.video.utils.MutableWeakReference
48
49
  import expo.modules.video.records.VideoTrack
49
50
  import expo.modules.video.utils.buildBasicMediaSession
51
+ import kotlinx.coroutines.DelicateCoroutinesApi
52
+ import kotlinx.coroutines.Dispatchers
53
+ import kotlinx.coroutines.GlobalScope
50
54
  import kotlinx.coroutines.launch
51
55
  import java.io.FileInputStream
52
56
  import java.lang.ref.WeakReference
57
+ import kotlin.time.DurationUnit
53
58
 
54
59
  // https://developer.android.com/guide/topics/media/media3/getting-started/migration-guide#improvements_in_media3
55
60
  @UnstableApi
56
- class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSource?) : AutoCloseable, SharedObject(appContext), IntervalUpdateEmitter {
61
+ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSource?, playerBuilderOptions: expo.modules.video.records.PlayerBuilderOptions? = null) : AutoCloseable, SharedObject(appContext), IntervalUpdateEmitter {
57
62
  // This improves the performance of playing DRM-protected content
58
63
  private var renderersFactory = DefaultRenderersFactory(context)
59
64
  .forceEnableMediaCodecAsynchronousQueueing()
@@ -70,9 +75,16 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
70
75
 
71
76
  val player = ExoPlayer
72
77
  .Builder(context, renderersFactory)
73
- .setLooper(context.mainLooper)
74
- .setLoadControl(loadControl)
75
- .build()
78
+ .apply {
79
+ setLooper(context.mainLooper)
80
+ setLoadControl(loadControl)
81
+ playerBuilderOptions?.seekBackwardIncrement?.let {
82
+ setSeekBackIncrementMs((it).toLong(DurationUnit.MILLISECONDS).coerceIn(1, 999_000))
83
+ }
84
+ playerBuilderOptions?.seekForwardIncrement?.let {
85
+ setSeekForwardIncrementMs((it).toLong(DurationUnit.MILLISECONDS).coerceIn(1, 999_000))
86
+ }
87
+ }.build()
76
88
 
77
89
  internal val firstFrameEventGenerator: FirstFrameEventGenerator
78
90
  val serviceConnection = PlaybackServiceConnection(WeakReference(this), appContext)
@@ -238,7 +250,8 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
238
250
  val newAudioTracks = audioTracks.availableAudioTracks
239
251
  val newCurrentSubtitleTrack = subtitles.currentSubtitleTrack
240
252
  val newCurrentAudioTrack = audioTracks.currentAudioTrack
241
- availableVideoTracks = tracks.toVideoTracks()
253
+ val hlsManifest = player.currentManifest as? HlsManifest
254
+ availableVideoTracks = tracks.toVideoTracks(hlsManifest)
242
255
  refreshPlaybackInfo()
243
256
 
244
257
  if (isLoadingNewSource) {
@@ -346,7 +359,17 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
346
359
  }
347
360
  }
348
361
 
362
+ @kotlin.OptIn(DelicateCoroutinesApi::class)
349
363
  override fun close() {
364
+ // Releases the listeners from VideoPlayerKeepAwake
365
+ keepScreenOnWhilePlaying = false
366
+
367
+ intervalUpdateClock.interval = 0L
368
+
369
+ synchronized(listeners) {
370
+ listeners.clear()
371
+ }
372
+
350
373
  if (serviceConnection.isConnected) {
351
374
  appContext?.reactContext?.unbindService(serviceConnection)
352
375
  }
@@ -355,19 +378,20 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
355
378
 
356
379
  VideoManager.unregisterVideoPlayer(this@VideoPlayer)
357
380
 
358
- appContext?.mainQueue?.launch {
381
+ // Run on global scope (not appContext.mainQueue) so that reloading doesn't cancel the release process
382
+ // https://github.com/expo/expo/blob/cdf592a7fea56fc01b0149e9b2e5dbd294bcdc4c/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/AppContext.kt#L277-L279
383
+ GlobalScope.launch(Dispatchers.Main) {
359
384
  firstFrameEventGenerator.release()
360
385
  player.removeListener(playerListener)
386
+ player.removeAnalyticsListener(analyticsListener)
361
387
  player.release()
362
388
  }
363
389
  uncommittedSource = null
364
390
  commitedSource = null
365
- // Releases the listeners from VideoPlayerKeepAwake
366
- keepScreenOnWhilePlaying = false
367
391
  }
368
392
 
369
- override fun deallocate() {
370
- super.deallocate()
393
+ override fun sharedObjectDidRelease() {
394
+ super.sharedObjectDidRelease()
371
395
  close()
372
396
  }
373
397
 
@@ -548,17 +572,23 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
548
572
  // Extension functions
549
573
 
550
574
  @OptIn(UnstableApi::class)
551
- private fun Tracks.toVideoTracks(): List<VideoTrack> {
575
+ private fun Tracks.toVideoTracks(sourceManifest: HlsManifest?): List<VideoTrack> {
552
576
  val videoTracks = mutableListOf<VideoTrack?>()
553
577
  for (group in this.groups) {
554
578
  for (i in 0 until group.length) {
555
579
  val format = group.getTrackFormat(i)
556
580
  val isSupported = group.isTrackSupported(i)
581
+ val hlsVariant = sourceManifest?.multivariantPlaylist?.variants?.firstOrNull {
582
+ it.format.id == format.id
583
+ }
584
+
585
+ // We provide the variant url only for HLS sources
586
+ val variantUrl = hlsVariant?.url
557
587
 
558
588
  if (!MimeTypes.isVideo(format.sampleMimeType)) {
559
589
  continue
560
590
  }
561
- videoTracks.add(VideoTrack.fromFormat(format, isSupported))
591
+ videoTracks.add(VideoTrack.fromFormat(format, isSupported, variantUrl))
562
592
  }
563
593
  }
564
594
  return videoTracks.filterNotNull()
@@ -0,0 +1,15 @@
1
+ package expo.modules.video.records
2
+
3
+ import expo.modules.kotlin.records.Field
4
+ import expo.modules.kotlin.records.Record
5
+
6
+ data class ButtonOptions(
7
+ @Field val showNext: Boolean = false,
8
+ @Field val showPrevious: Boolean = false,
9
+ @Field val showSeekForward: Boolean = true,
10
+ @Field val showSeekBackward: Boolean = true,
11
+ @Field val showSubtitles: Boolean? = null,
12
+ @Field val showSettings: Boolean = true,
13
+ @Field val showPlayPause: Boolean = true,
14
+ @Field val showBottomBar: Boolean = true
15
+ ) : Record
@@ -0,0 +1,13 @@
1
+ package expo.modules.video.records
2
+
3
+ import androidx.media3.common.util.UnstableApi
4
+ import expo.modules.kotlin.records.Field
5
+ import expo.modules.kotlin.records.Record
6
+ import java.io.Serializable
7
+ import kotlin.time.Duration
8
+
9
+ @UnstableApi
10
+ class PlayerBuilderOptions(
11
+ @Field var seekBackwardIncrement: Duration? = null,
12
+ @Field var seekForwardIncrement: Duration? = null
13
+ ) : Record, Serializable
@@ -1,5 +1,6 @@
1
1
  package expo.modules.video.records
2
2
 
3
+ import android.net.Uri
3
4
  import androidx.annotation.OptIn
4
5
  import androidx.media3.common.Format
5
6
  import androidx.media3.common.util.UnstableApi
@@ -11,7 +12,10 @@ import java.util.Locale
11
12
  class SubtitleTrack(
12
13
  @Field val id: String,
13
14
  @Field val language: String?,
14
- @Field val label: String?
15
+ @Field val label: String?,
16
+ @Field val name: String?,
17
+ @Field val isDefault: Boolean,
18
+ @Field val autoSelect: Boolean
15
19
  ) : Record, Serializable {
16
20
  companion object {
17
21
  fun fromFormat(format: Format?): SubtitleTrack? {
@@ -19,11 +23,17 @@ class SubtitleTrack(
19
23
  val id = format.id ?: return null
20
24
  val language = format.language ?: return null
21
25
  val label = Locale(language).displayLanguage
26
+ val name = format.label
27
+ val isDefault = (format.selectionFlags and androidx.media3.common.C.SELECTION_FLAG_DEFAULT) != 0
28
+ val autoSelect = (format.selectionFlags and androidx.media3.common.C.SELECTION_FLAG_AUTOSELECT) != 0
22
29
 
23
30
  return SubtitleTrack(
24
31
  id = id,
25
32
  language = language,
26
- label = label
33
+ label = label,
34
+ name = name,
35
+ isDefault = isDefault,
36
+ autoSelect = autoSelect
27
37
  )
28
38
  }
29
39
  }
@@ -32,7 +42,10 @@ class SubtitleTrack(
32
42
  class AudioTrack(
33
43
  @Field val id: String,
34
44
  @Field val language: String?,
35
- @Field val label: String?
45
+ @Field val label: String?,
46
+ @Field val name: String?,
47
+ @Field val isDefault: Boolean,
48
+ @Field val autoSelect: Boolean
36
49
  ) : Record, Serializable {
37
50
  companion object {
38
51
  fun fromFormat(format: Format?): AudioTrack? {
@@ -40,11 +53,17 @@ class AudioTrack(
40
53
  val id = format.id ?: return null
41
54
  val language = format.language
42
55
  val label = language?.let { Locale(it).displayLanguage } ?: "Unknown"
56
+ val name = format.label
57
+ val isDefault = (format.selectionFlags and androidx.media3.common.C.SELECTION_FLAG_DEFAULT) != 0
58
+ val autoSelect = (format.selectionFlags and androidx.media3.common.C.SELECTION_FLAG_AUTOSELECT) != 0
43
59
 
44
60
  return AudioTrack(
45
61
  id = id,
46
62
  language = language,
47
- label = label
63
+ label = label,
64
+ name = name,
65
+ isDefault = isDefault,
66
+ autoSelect = autoSelect
48
67
  )
49
68
  }
50
69
  }
@@ -53,6 +72,7 @@ class AudioTrack(
53
72
  @OptIn(UnstableApi::class)
54
73
  class VideoTrack(
55
74
  @Field val id: String,
75
+ @Field val url: Uri?,
56
76
  @Field val size: VideoSize,
57
77
  @Field val mimeType: String?,
58
78
  @Field val isSupported: Boolean = true,
@@ -63,7 +83,7 @@ class VideoTrack(
63
83
  var format: Format? = null
64
84
  ) : Record, Serializable {
65
85
  companion object {
66
- fun fromFormat(format: Format?, isSupported: Boolean): VideoTrack? {
86
+ fun fromFormat(format: Format?, isSupported: Boolean, variantUrl: Uri?): VideoTrack? {
67
87
  val id = format?.id ?: return null
68
88
  val size = VideoSize(format)
69
89
  val mimeType = format.sampleMimeType
@@ -73,6 +93,7 @@ class VideoTrack(
73
93
 
74
94
  return VideoTrack(
75
95
  id = id,
96
+ url = variantUrl,
76
97
  size = size,
77
98
  mimeType = mimeType,
78
99
  isSupported = isSupported,
@@ -1,15 +1,17 @@
1
- import { VideoSource, VideoPlayer } from './VideoPlayer.types';
1
+ import { VideoSource, VideoPlayer, PlayerBuilderOptions } from './VideoPlayer.types';
2
2
  /**
3
3
  * Creates a direct instance of `VideoPlayer` that doesn't release automatically.
4
4
  *
5
5
  * > **info** For most use cases you should use the [`useVideoPlayer`](#usevideoplayer) hook instead. See the [Using the VideoPlayer Directly](#using-the-videoplayer-directly) section for more details.
6
- * @param source
6
+ * @param source - A video source that is used to initialize the player.
7
+ * @param playerBuilderOptions - Options to apply to the Android player builder before the native constructor is invoked.
7
8
  */
8
- export declare function createVideoPlayer(source: VideoSource): VideoPlayer;
9
+ export declare function createVideoPlayer(source: VideoSource, playerBuilderOptions?: PlayerBuilderOptions): VideoPlayer;
9
10
  /**
10
11
  * Creates a `VideoPlayer`, which will be automatically cleaned up when the component is unmounted.
11
12
  * @param source - A video source that is used to initialize the player.
12
13
  * @param setup - A function that allows setting up the player. It will run after the player is created.
14
+ * @param playerBuilderOptions - Options to apply to the Android player builder before the native constructor is invoked.
13
15
  */
14
- export declare function useVideoPlayer(source: VideoSource, setup?: (player: VideoPlayer) => void): VideoPlayer;
16
+ export declare function useVideoPlayer(source: VideoSource, setup?: (player: VideoPlayer) => void, playerBuilderOptions?: PlayerBuilderOptions): VideoPlayer;
15
17
  //# sourceMappingURL=VideoPlayer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAsB/D;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAElE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACpC,WAAW,CAQb"}
1
+ {"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAsBrF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EACnB,oBAAoB,CAAC,EAAE,oBAAoB,GAC1C,WAAW,CAGb;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,EACrC,oBAAoB,CAAC,EAAE,oBAAoB,GAC1C,WAAW,CAQb"}