expo-video 2.0.0-preview.0 → 2.0.0-preview.2

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 (46) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +59 -11
  4. package/android/src/main/java/expo/modules/video/VideoModule.kt +27 -0
  5. package/android/src/main/java/expo/modules/video/VideoView.kt +1 -16
  6. package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
  7. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +32 -0
  8. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +44 -4
  9. package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +4 -0
  10. package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
  11. package/android/src/main/java/expo/modules/video/records/SubtitleTrack.kt +28 -0
  12. package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +10 -0
  13. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +2 -1
  14. package/build/VideoPlayer.d.ts +8 -3
  15. package/build/VideoPlayer.d.ts.map +1 -1
  16. package/build/VideoPlayer.js +9 -1
  17. package/build/VideoPlayer.js.map +1 -1
  18. package/build/VideoPlayer.types.d.ts +53 -0
  19. package/build/VideoPlayer.types.d.ts.map +1 -1
  20. package/build/VideoPlayer.types.js.map +1 -1
  21. package/build/VideoPlayer.web.d.ts +5 -1
  22. package/build/VideoPlayer.web.d.ts.map +1 -1
  23. package/build/VideoPlayer.web.js +7 -0
  24. package/build/VideoPlayer.web.js.map +1 -1
  25. package/build/VideoPlayerEvents.types.d.ts +30 -1
  26. package/build/VideoPlayerEvents.types.d.ts.map +1 -1
  27. package/build/VideoPlayerEvents.types.js.map +1 -1
  28. package/build/index.d.ts +2 -1
  29. package/build/index.d.ts.map +1 -1
  30. package/build/index.js +1 -0
  31. package/build/index.js.map +1 -1
  32. package/ios/Enums/AudioMixingMode.swift +37 -0
  33. package/ios/NowPlayingManager.swift +8 -6
  34. package/ios/Records/SubtitleTrack.swift +21 -0
  35. package/ios/Records/VideoEventPayloads.swift +10 -0
  36. package/ios/VideoManager.swift +39 -17
  37. package/ios/VideoModule.swift +18 -0
  38. package/ios/VideoPlayer.swift +30 -0
  39. package/ios/VideoPlayerObserver.swift +38 -0
  40. package/ios/VideoPlayerSubtitles.swift +71 -0
  41. package/package.json +2 -2
  42. package/src/VideoPlayer.tsx +11 -3
  43. package/src/VideoPlayer.types.ts +60 -0
  44. package/src/VideoPlayer.web.tsx +11 -0
  45. package/src/VideoPlayerEvents.types.ts +35 -1
  46. package/src/index.ts +3 -0
package/CHANGELOG.md CHANGED
@@ -10,6 +10,23 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 2.0.0-preview.2 — 2024-11-07
14
+
15
+ ### 🎉 New features
16
+
17
+ - [Android][iOS] Add support for listing and selecting closed captions. ([#32582](https://github.com/expo/expo/pull/32582) by [@behenate](https://github.com/behenate))
18
+
19
+ ## 2.0.0-preview.1 — 2024-11-05
20
+
21
+ ### 🎉 New features
22
+
23
+ - [Android][iOS] Add `audioMixingMode` property to control how the player interacts with other audio in the system. ([#32428](https://github.com/expo/expo/pull/32428) by [@behenate](https://github.com/behenate))
24
+ - Add support for creating a direct instance of `VideoPlayer`. ([#32228](https://github.com/expo/expo/pull/32228) by [@behenate](https://github.com/behenate))
25
+
26
+ ### 🐛 Bug fixes
27
+
28
+ - [Android] Fix errors when passing a source with an `undefined` `uri` field. ([#32585](https://github.com/expo/expo/pull/32585) by [@behenate](https://github.com/behenate))
29
+
13
30
  ## 2.0.0-preview.0 — 2024-10-22
14
31
 
15
32
  ### 🛠 Breaking changes
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'host.exp.exponent'
4
- version = '2.0.0-preview.0'
4
+ version = '2.0.0-preview.2'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -14,7 +14,7 @@ android {
14
14
  namespace "expo.modules.video"
15
15
  defaultConfig {
16
16
  versionCode 1
17
- versionName '2.0.0-preview.0'
17
+ versionName '2.0.0-preview.2'
18
18
  }
19
19
  }
20
20
 
@@ -7,6 +7,7 @@ import android.media.AudioManager
7
7
  import android.os.Build
8
8
  import androidx.media3.common.util.UnstableApi
9
9
  import expo.modules.kotlin.AppContext
10
+ import expo.modules.video.enums.AudioMixingMode
10
11
  import expo.modules.video.player.VideoPlayer
11
12
  import expo.modules.video.player.VideoPlayerListener
12
13
  import kotlinx.coroutines.launch
@@ -22,19 +23,37 @@ class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAud
22
23
 
23
24
  private var players: MutableList<WeakReference<VideoPlayer>> = mutableListOf()
24
25
  private var currentFocusRequest: AudioFocusRequest? = null
26
+ private var currentMixingMode: AudioMixingMode = AudioMixingMode.MIX_WITH_OTHERS
25
27
  private val anyPlayerRequiresFocus: Boolean
26
28
  get() = players.any {
27
29
  playerRequiresFocus(it)
28
30
  }
29
31
 
30
32
  private fun requestAudioFocus() {
33
+ val audioMixingMode = findAudioMixingMode()
34
+
35
+ // We don't request AudioFocus if we want to mix the audio with others
36
+ if (audioMixingMode == AudioMixingMode.MIX_WITH_OTHERS || !anyPlayerRequiresFocus) {
37
+ abandonAudioFocus()
38
+ currentMixingMode = audioMixingMode
39
+ return
40
+ }
41
+ val audioFocusType = when (audioMixingMode) {
42
+ AudioMixingMode.DUCK_OTHERS -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
43
+ AudioMixingMode.AUTO -> AudioManager.AUDIOFOCUS_GAIN
44
+ AudioMixingMode.DO_NOT_MIX -> AudioManager.AUDIOFOCUS_GAIN
45
+ else -> AudioManager.AUDIOFOCUS_GAIN
46
+ }
47
+
31
48
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
32
49
  // We already have audio focus
33
- if (currentFocusRequest != null) {
34
- return
50
+ currentFocusRequest?.let {
51
+ if (it.focusGain == audioFocusType) {
52
+ return
53
+ }
35
54
  }
36
55
 
37
- val newFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
56
+ val newFocusRequest = AudioFocusRequest.Builder(audioFocusType).run {
38
57
  setAudioAttributes(
39
58
  AudioAttributes.Builder().run {
40
59
  setUsage(AudioAttributes.USAGE_MEDIA)
@@ -44,16 +63,17 @@ class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAud
44
63
  }
45
64
  ).build()
46
65
  }
47
- this.currentFocusRequest = newFocusRequest
66
+ currentFocusRequest = newFocusRequest
48
67
  audioManager.requestAudioFocus(newFocusRequest)
49
68
  } else {
50
69
  @Suppress("DEPRECATION")
51
70
  audioManager.requestAudioFocus(
52
71
  this,
53
72
  AudioManager.STREAM_MUSIC,
54
- AudioManager.AUDIOFOCUS_GAIN
73
+ audioFocusType
55
74
  )
56
75
  }
76
+ currentMixingMode = audioMixingMode
57
77
  }
58
78
 
59
79
  private fun abandonAudioFocus() {
@@ -84,6 +104,11 @@ class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAud
84
104
 
85
105
  // VideoPlayerListener
86
106
 
107
+ override fun onAudioMixingModeChanged(player: VideoPlayer, audioMixingMode: AudioMixingMode, oldAudioMixingMode: AudioMixingMode?) {
108
+ requestAudioFocus()
109
+ super.onAudioMixingModeChanged(player, audioMixingMode, oldAudioMixingMode)
110
+ }
111
+
87
112
  override fun onIsPlayingChanged(player: VideoPlayer, isPlaying: Boolean, oldIsPlaying: Boolean?) {
88
113
  // we can't use `updateAudioFocus`, because when losing focus the videos are paused sequentially,
89
114
  // which can lead to unexpected results.
@@ -116,6 +141,12 @@ class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAud
116
141
  }
117
142
 
118
143
  AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
144
+ // W could pause/mix the players here individually, but we will keep the behaviour in line with iOS,
145
+ // find the dominant audioMixingMode and apply it to all players.
146
+ val audioMixingMode = findAudioMixingMode()
147
+ if (audioMixingMode == AudioMixingMode.MIX_WITH_OTHERS) {
148
+ return
149
+ }
119
150
  appContext.mainQueue.launch {
120
151
  players.forEach {
121
152
  pausePlayerIfUnmuted(it)
@@ -125,9 +156,15 @@ class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAud
125
156
  }
126
157
 
127
158
  AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
159
+ val audioMixingMode = findAudioMixingMode()
160
+
128
161
  appContext.mainQueue.launch {
129
162
  players.forEach {
130
- duckPlayer(it)
163
+ if (audioMixingMode == AudioMixingMode.DO_NOT_MIX) {
164
+ pausePlayerIfUnmuted(it)
165
+ } else {
166
+ duckPlayer(it)
167
+ }
131
168
  }
132
169
  }
133
170
  }
@@ -148,10 +185,8 @@ class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAud
148
185
 
149
186
  private fun playerRequiresFocus(weakPlayer: WeakReference<VideoPlayer>): Boolean {
150
187
  return weakPlayer.get()?.let {
151
- !it.muted && it.playing && it.volume > 0
152
- } ?: run {
153
- false
154
- }
188
+ (!it.muted && it.playing && it.volume > 0) || it.audioMixingMode == AudioMixingMode.DO_NOT_MIX
189
+ } ?: false
155
190
  }
156
191
 
157
192
  private fun pausePlayerIfUnmuted(weakPlayer: WeakReference<VideoPlayer>) {
@@ -183,10 +218,23 @@ class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAud
183
218
  }
184
219
 
185
220
  private fun updateAudioFocus() {
186
- if (anyPlayerRequiresFocus) {
221
+ if (anyPlayerRequiresFocus || findAudioMixingMode() != currentMixingMode) {
187
222
  requestAudioFocus()
188
223
  } else {
189
224
  abandonAudioFocus()
190
225
  }
191
226
  }
227
+
228
+ private fun findAudioMixingMode(): AudioMixingMode {
229
+ val mixingModes = players.mapNotNull { player ->
230
+ player.get()?.takeIf { it.playing }?.audioMixingMode
231
+ }
232
+ if (mixingModes.isEmpty()) {
233
+ return AudioMixingMode.MIX_WITH_OTHERS
234
+ }
235
+
236
+ return mixingModes.reduce { currentAudioMixingMode, next ->
237
+ next.takeIf { it.priority > currentAudioMixingMode.priority } ?: currentAudioMixingMode
238
+ }
239
+ }
192
240
  }
@@ -16,9 +16,11 @@ import expo.modules.kotlin.functions.Coroutine
16
16
  import expo.modules.kotlin.modules.Module
17
17
  import expo.modules.kotlin.modules.ModuleDefinition
18
18
  import expo.modules.kotlin.types.Either
19
+ import expo.modules.video.enums.AudioMixingMode
19
20
  import expo.modules.video.enums.ContentFit
20
21
  import expo.modules.video.player.VideoPlayer
21
22
  import expo.modules.video.records.BufferOptions
23
+ import expo.modules.video.records.SubtitleTrack
22
24
  import expo.modules.video.records.VideoSource
23
25
  import expo.modules.video.utils.ifYogaDefinedUse
24
26
  import expo.modules.video.utils.makeYogaUndefinedIfNegative
@@ -207,6 +209,21 @@ class VideoModule : Module() {
207
209
  }
208
210
  }
209
211
 
212
+ Property("availableSubtitleTracks")
213
+ .get { ref: VideoPlayer ->
214
+ ref.subtitles.availableSubtitleTracks
215
+ }
216
+
217
+ Property("subtitleTrack")
218
+ .get { ref: VideoPlayer ->
219
+ ref.subtitles.currentSubtitleTrack
220
+ }
221
+ .set { ref: VideoPlayer, subtitleTrack: SubtitleTrack? ->
222
+ appContext.mainQueue.launch {
223
+ ref.subtitles.currentSubtitleTrack = subtitleTrack
224
+ }
225
+ }
226
+
210
227
  Property("currentOffsetFromLive")
211
228
  .get { ref: VideoPlayer ->
212
229
  runBlocking(appContext.mainQueue.coroutineContext) {
@@ -318,6 +335,16 @@ class VideoModule : Module() {
318
335
  ref.intervalUpdateClock.interval = (intervalSeconds * 1000).toLong()
319
336
  }
320
337
 
338
+ Property("audioMixingMode")
339
+ .get { ref: VideoPlayer ->
340
+ ref.audioMixingMode
341
+ }
342
+ .set { ref: VideoPlayer, audioMixingMode: AudioMixingMode ->
343
+ appContext.mainQueue.launch {
344
+ ref.audioMixingMode = audioMixingMode
345
+ }
346
+ }
347
+
321
348
  Function("replace") { ref: VideoPlayer, source: Either<Uri, VideoSource>? ->
322
349
  val videoSource = source?.let {
323
350
  if (it.`is`(VideoSource::class)) {
@@ -12,8 +12,6 @@ import android.view.ViewGroup
12
12
  import android.widget.FrameLayout
13
13
  import android.widget.ImageButton
14
14
  import androidx.fragment.app.FragmentActivity
15
- import androidx.media3.common.Format
16
- import androidx.media3.common.MimeTypes
17
15
  import androidx.media3.common.Tracks
18
16
  import androidx.media3.ui.PlayerView
19
17
  import com.facebook.react.common.annotations.UnstableReactNativeAPI
@@ -254,24 +252,11 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
254
252
  }
255
253
 
256
254
  override fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {
257
- showsSubtitlesButton = hasSubtitles(tracks)
255
+ showsSubtitlesButton = player.subtitles.availableSubtitleTracks.isNotEmpty()
258
256
  playerView.setShowSubtitleButton(showsSubtitlesButton)
259
257
  super.onTracksChanged(player, tracks)
260
258
  }
261
259
 
262
- private fun hasSubtitles(tracks: Tracks): Boolean {
263
- for (group in tracks.groups) {
264
- for (i in 0..<group.length) {
265
- val format: Format = group.getTrackFormat(i)
266
-
267
- if (MimeTypes.isText(format.sampleMimeType)) {
268
- return true
269
- }
270
- }
271
- }
272
- return false
273
- }
274
-
275
260
  override fun requestLayout() {
276
261
  super.requestLayout()
277
262
 
@@ -0,0 +1,20 @@
1
+ package expo.modules.video.enums
2
+
3
+ import expo.modules.kotlin.types.Enumerable
4
+
5
+ enum class AudioMixingMode(val value: String) : Enumerable {
6
+ MIX_WITH_OTHERS("mixWithOthers"),
7
+ DUCK_OTHERS("duckOthers"),
8
+ AUTO("auto"),
9
+ DO_NOT_MIX("doNotMix");
10
+
11
+ val priority: Int
12
+ get() {
13
+ return when (this) {
14
+ DO_NOT_MIX -> 3
15
+ AUTO -> 2
16
+ DUCK_OTHERS -> 1
17
+ MIX_WITH_OTHERS -> 0
18
+ }
19
+ }
20
+ }
@@ -1,15 +1,20 @@
1
1
  package expo.modules.video.player
2
2
 
3
3
  import androidx.annotation.OptIn
4
+ import androidx.media3.common.TrackSelectionParameters
4
5
  import androidx.media3.common.Tracks
5
6
  import androidx.media3.common.util.UnstableApi
7
+ import expo.modules.video.enums.AudioMixingMode
6
8
  import expo.modules.video.enums.PlayerStatus
9
+ import expo.modules.video.records.AvailableSubtitleTracksChangedEventPayload
7
10
  import expo.modules.video.records.IsPlayingEventPayload
8
11
  import expo.modules.video.records.MutedChangedEventPayload
9
12
  import expo.modules.video.records.PlaybackError
10
13
  import expo.modules.video.records.PlaybackRateChangedEventPayload
11
14
  import expo.modules.video.records.SourceChangedEventPayload
12
15
  import expo.modules.video.records.StatusChangedEventPayload
16
+ import expo.modules.video.records.SubtitleTrack
17
+ import expo.modules.video.records.SubtitleTrackChangedEventPayload
13
18
  import expo.modules.video.records.TimeUpdate
14
19
  import expo.modules.video.records.VideoEventPayload
15
20
  import expo.modules.video.records.VideoSource
@@ -56,11 +61,34 @@ sealed class PlayerEvent {
56
61
  override val emitToJS = false
57
62
  }
58
63
 
64
+ data class TrackSelectionParametersChanged(val trackSelectionParameters: TrackSelectionParameters) : PlayerEvent() {
65
+ override val name = "trackSelectionParametersChange"
66
+ override val emitToJS = false
67
+ }
68
+
69
+ data class SubtitleTrackChanged(val subtitleTrack: SubtitleTrack?, val oldSubtitleTrack: SubtitleTrack?) : PlayerEvent() {
70
+ override val name = "subtitleTrackChange"
71
+ override val jsEventPayload = SubtitleTrackChangedEventPayload(subtitleTrack, oldSubtitleTrack)
72
+ }
73
+
74
+ data class AvailableSubtitleTracksChanged(
75
+ val availableSubtitleTracks: List<SubtitleTrack>,
76
+ val oldAvailableSubtitleTracks: List<SubtitleTrack>
77
+ ) : PlayerEvent() {
78
+ override val name = "availableSubtitleTracksChange"
79
+ override val jsEventPayload = AvailableSubtitleTracksChangedEventPayload(availableSubtitleTracks, oldAvailableSubtitleTracks)
80
+ }
81
+
59
82
  data class TimeUpdated(val timeUpdate: TimeUpdate) : PlayerEvent() {
60
83
  override val name = "timeUpdate"
61
84
  override val jsEventPayload = timeUpdate
62
85
  }
63
86
 
87
+ data class AudioMixingModeChanged(val audioMixingMode: AudioMixingMode, val oldAudioMixingMode: AudioMixingMode?) : PlayerEvent() {
88
+ override val name = "audioMixingModeChange"
89
+ override val emitToJS = false
90
+ }
91
+
64
92
  class PlayedToEnd : PlayerEvent() {
65
93
  override val name = "playToEnd"
66
94
  }
@@ -73,9 +101,13 @@ sealed class PlayerEvent {
73
101
  is SourceChanged -> listeners.forEach { it.onSourceChanged(player, source, oldSource) }
74
102
  is PlaybackRateChanged -> listeners.forEach { it.onPlaybackRateChanged(player, rate, oldRate) }
75
103
  is TracksChanged -> listeners.forEach { it.onTracksChanged(player, tracks) }
104
+ is TrackSelectionParametersChanged -> listeners.forEach { it.onTrackSelectionParametersChanged(player, trackSelectionParameters) }
76
105
  is TimeUpdated -> listeners.forEach { it.onTimeUpdate(player, timeUpdate) }
77
106
  is PlayedToEnd -> listeners.forEach { it.onPlayedToEnd(player) }
78
107
  is MutedChanged -> listeners.forEach { it.onMutedChanged(player, muted, oldMuted) }
108
+ is AudioMixingModeChanged -> listeners.forEach { it.onAudioMixingModeChanged(player, audioMixingMode, oldAudioMixingMode) }
109
+ // JS-only events
110
+ else -> Unit
79
111
  }
80
112
  }
81
113
  }
@@ -11,10 +11,12 @@ import androidx.media3.common.PlaybackParameters
11
11
  import androidx.media3.common.Player
12
12
  import androidx.media3.common.Player.STATE_BUFFERING
13
13
  import androidx.media3.common.Timeline
14
+ import androidx.media3.common.TrackSelectionParameters
14
15
  import androidx.media3.common.Tracks
15
16
  import androidx.media3.common.util.UnstableApi
16
17
  import androidx.media3.exoplayer.DefaultRenderersFactory
17
18
  import androidx.media3.exoplayer.ExoPlayer
19
+ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
18
20
  import androidx.media3.ui.PlayerView
19
21
  import expo.modules.kotlin.AppContext
20
22
  import expo.modules.kotlin.sharedobjects.SharedObject
@@ -22,6 +24,7 @@ import expo.modules.video.IntervalUpdateClock
22
24
  import expo.modules.video.IntervalUpdateEmitter
23
25
  import expo.modules.video.VideoManager
24
26
  import expo.modules.video.delegates.IgnoreSameSet
27
+ import expo.modules.video.enums.AudioMixingMode
25
28
  import expo.modules.video.enums.PlayerStatus
26
29
  import expo.modules.video.enums.PlayerStatus.*
27
30
  import expo.modules.video.playbackService.ExpoVideoPlaybackService
@@ -43,6 +46,8 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
43
46
  .setEnableDecoderFallback(true)
44
47
  private var listeners: MutableList<WeakReference<VideoPlayerListener>> = mutableListOf()
45
48
  val loadControl: VideoPlayerLoadControl = VideoPlayerLoadControl.Builder().build()
49
+ val subtitles: VideoPlayerSubtitles = VideoPlayerSubtitles(this)
50
+ val trackSelector = DefaultTrackSelector(context)
46
51
 
47
52
  val player = ExoPlayer
48
53
  .Builder(context, renderersFactory)
@@ -140,22 +145,51 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
140
145
  return player.bufferedPosition / 1000.0
141
146
  }
142
147
 
148
+ var audioMixingMode: AudioMixingMode = AudioMixingMode.AUTO
149
+ set(value) {
150
+ val old = field
151
+ field = value
152
+ sendEvent(PlayerEvent.AudioMixingModeChanged(value, old))
153
+ }
154
+
143
155
  private val playerListener = object : Player.Listener {
144
156
  override fun onIsPlayingChanged(isPlaying: Boolean) {
145
157
  this@VideoPlayer.playing = isPlaying
146
158
  }
147
159
 
148
160
  override fun onTracksChanged(tracks: Tracks) {
161
+ val oldSubtitleTracks = ArrayList(subtitles.availableSubtitleTracks)
162
+ val oldCurrentTrack = subtitles.currentSubtitleTrack
149
163
  sendEvent(PlayerEvent.TracksChanged(tracks))
164
+
165
+ val newSubtitleTracks = subtitles.availableSubtitleTracks
166
+ val newCurrentSubtitleTrack = subtitles.currentSubtitleTrack
167
+
168
+ if (!oldSubtitleTracks.toArray().contentEquals(newSubtitleTracks.toArray())) {
169
+ sendEvent(PlayerEvent.AvailableSubtitleTracksChanged(newSubtitleTracks, oldSubtitleTracks))
170
+ }
171
+ if (oldCurrentTrack != newCurrentSubtitleTrack) {
172
+ sendEvent(PlayerEvent.SubtitleTrackChanged(newCurrentSubtitleTrack, oldCurrentTrack))
173
+ }
150
174
  super.onTracksChanged(tracks)
151
175
  }
152
176
 
177
+ override fun onTrackSelectionParametersChanged(parameters: TrackSelectionParameters) {
178
+ val oldTrack = subtitles.currentSubtitleTrack
179
+ sendEvent(PlayerEvent.TrackSelectionParametersChanged(parameters))
180
+
181
+ val newTrack = subtitles.currentSubtitleTrack
182
+ sendEvent(PlayerEvent.SubtitleTrackChanged(newTrack, oldTrack))
183
+ super.onTrackSelectionParametersChanged(parameters)
184
+ }
185
+
153
186
  override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
154
187
  this@VideoPlayer.duration = 0f
155
188
  this@VideoPlayer.isLive = false
156
189
  if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT) {
157
190
  sendEvent(PlayerEvent.PlayedToEnd())
158
191
  }
192
+ subtitles.setSubtitlesEnabled(false)
159
193
  super.onMediaItemTransition(mediaItem, reason)
160
194
  }
161
195
 
@@ -199,6 +233,11 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
199
233
  ExpoVideoPlaybackService.startService(appContext, context, serviceConnection)
200
234
  player.addListener(playerListener)
201
235
  VideoManager.registerVideoPlayer(this)
236
+
237
+ // ExoPlayer will enable subtitles automatically at the start, we want them disabled by default
238
+ appContext.mainQueue.launch {
239
+ subtitles.setSubtitlesEnabled(false)
240
+ }
202
241
  }
203
242
 
204
243
  override fun close() {
@@ -226,11 +265,12 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
226
265
  }
227
266
 
228
267
  fun prepare() {
229
- uncommittedSource?.let { videoSource ->
230
- val mediaSource = videoSource.toMediaSource(context)
231
- player.setMediaSource(mediaSource)
268
+ val newSource = uncommittedSource
269
+ val mediaSource = newSource?.toMediaSource(context)
270
+ mediaSource?.let {
271
+ player.setMediaSource(it)
232
272
  player.prepare()
233
- lastLoadedSource = videoSource
273
+ lastLoadedSource = newSource
234
274
  uncommittedSource = null
235
275
  } ?: run {
236
276
  player.clearMediaItems()
@@ -1,8 +1,10 @@
1
1
  package expo.modules.video.player
2
2
 
3
3
  import androidx.annotation.OptIn
4
+ import androidx.media3.common.TrackSelectionParameters
4
5
  import androidx.media3.common.Tracks
5
6
  import androidx.media3.common.util.UnstableApi
7
+ import expo.modules.video.enums.AudioMixingMode
6
8
  import expo.modules.video.enums.PlayerStatus
7
9
  import expo.modules.video.records.PlaybackError
8
10
  import expo.modules.video.records.VideoSource
@@ -17,6 +19,8 @@ interface VideoPlayerListener {
17
19
  fun onSourceChanged(player: VideoPlayer, source: VideoSource?, oldSource: VideoSource?) {}
18
20
  fun onPlaybackRateChanged(player: VideoPlayer, rate: Float, oldRate: Float?) {}
19
21
  fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {}
22
+ fun onTrackSelectionParametersChanged(player: VideoPlayer, trackSelectionParameters: TrackSelectionParameters) {}
20
23
  fun onTimeUpdate(player: VideoPlayer, timeUpdate: TimeUpdate) {}
21
24
  fun onPlayedToEnd(player: VideoPlayer) {}
25
+ fun onAudioMixingModeChanged(player: VideoPlayer, audioMixingMode: AudioMixingMode, oldAudioMixingMode: AudioMixingMode?) {}
22
26
  }
@@ -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.SubtitleTrack
13
+ import java.lang.ref.WeakReference
14
+
15
+ @OptIn(UnstableApi::class)
16
+ class VideoPlayerSubtitles(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 currentSubtitleFormat: Format? = null
24
+ private var currentOverride: TrackSelectionOverride? = null
25
+
26
+ var currentSubtitleTrack: SubtitleTrack?
27
+ get() {
28
+ return SubtitleTrack.fromFormat(currentSubtitleFormat)
29
+ }
30
+ set(value) {
31
+ applySubtitleTrack(value)
32
+ }
33
+ val availableSubtitleTracks = arrayListOf<SubtitleTrack>()
34
+
35
+ init {
36
+ owner.addListener(this)
37
+ }
38
+
39
+ fun setSubtitlesEnabled(enabled: Boolean) {
40
+ val currentParams = videoPlayer?.player?.trackSelectionParameters ?: return
41
+ var params = currentParams.buildUpon().setTrackTypeDisabled(C.TRACK_TYPE_TEXT, !enabled).build()
42
+ if (!enabled) {
43
+ params = params.buildUpon().clearOverridesOfType(C.TRACK_TYPE_TEXT).build()
44
+ }
45
+ videoPlayer?.player?.trackSelectionParameters = params
46
+ }
47
+
48
+ // VideoPlayerListener
49
+ override fun onTrackSelectionParametersChanged(player: VideoPlayer, trackSelectionParameters: TrackSelectionParameters) {
50
+ currentSubtitleFormat = findSelectedSubtitleFormat()
51
+ super.onTrackSelectionParametersChanged(player, trackSelectionParameters)
52
+ }
53
+
54
+ override fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {
55
+ formatsToGroups.clear()
56
+ availableSubtitleTracks.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.isText(format.sampleMimeType)) {
62
+ formatsToGroups[format] = Pair(group.mediaTrackGroup, i)
63
+ val track = SubtitleTrack.fromFormat(format) ?: continue
64
+ availableSubtitleTracks.add(track)
65
+ }
66
+ }
67
+ }
68
+ currentSubtitleFormat = findSelectedSubtitleFormat()
69
+ super.onTracksChanged(player, tracks)
70
+ }
71
+
72
+ // Private methods
73
+ private fun applySubtitleTrack(subtitleTrack: SubtitleTrack?) {
74
+ val player = videoPlayer?.player ?: return
75
+ var newParameters: TrackSelectionParameters = player.trackSelectionParameters
76
+ currentOverride?.let { override ->
77
+ newParameters = newParameters.buildUpon().clearOverridesOfType(C.TRACK_TYPE_TEXT).build()
78
+ }
79
+ if (subtitleTrack == null) {
80
+ player.trackSelectionParameters = newParameters
81
+ setSubtitlesEnabled(false)
82
+ currentOverride = null
83
+ return
84
+ }
85
+ val format = formatsToGroups.keys.firstOrNull {
86
+ it.id == subtitleTrack.id
87
+ }
88
+ format?.let {
89
+ formatsToGroups[it]?.let { subtitlePair ->
90
+ val override = TrackSelectionOverride(subtitlePair.first, subtitlePair.second)
91
+ newParameters = newParameters.buildUpon().addOverride(override).build()
92
+ player.trackSelectionParameters = newParameters
93
+ setSubtitlesEnabled(true)
94
+ currentOverride = override
95
+ }
96
+ }
97
+ }
98
+
99
+ private fun findSelectedSubtitleFormat(): Format? {
100
+ val trackSelectionParameters = videoPlayer?.player?.trackSelectionParameters
101
+ val preferredTextLanguages = trackSelectionParameters?.preferredTextLanguages
102
+ val overriddenFormat: Format? = trackSelectionParameters?.overrides?.let {
103
+ for ((group, override) in it) {
104
+ if (group.type == C.TRACK_TYPE_TEXT) {
105
+ // For subtitles only one index will be replaced
106
+ return@let override.trackIndices.firstOrNull()?.let { index ->
107
+ group.getFormat(index)
108
+ }
109
+ }
110
+ }
111
+ return@let null
112
+ }
113
+
114
+ val preferredFormat: Format? = preferredTextLanguages?.let { preferredTextLanguages ->
115
+ for (preferredLanguage in preferredTextLanguages) {
116
+ return@let formatsToGroups.keys.firstOrNull {
117
+ it.language == preferredLanguage
118
+ }
119
+ }
120
+ return@let null
121
+ }
122
+
123
+ return overriddenFormat ?: preferredFormat
124
+ }
125
+ }
@@ -0,0 +1,28 @@
1
+ package expo.modules.video.records
2
+
3
+ import androidx.media3.common.Format
4
+ import expo.modules.kotlin.records.Field
5
+ import expo.modules.kotlin.records.Record
6
+ import java.io.Serializable
7
+ import java.util.Locale
8
+
9
+ class SubtitleTrack(
10
+ @Field var id: String,
11
+ @Field var language: String?,
12
+ @Field var label: String?
13
+ ) : Record, Serializable {
14
+ companion object {
15
+ fun fromFormat(format: Format?): SubtitleTrack? {
16
+ format ?: return null
17
+ val id = format.id ?: return null
18
+ val language = format.language ?: return null
19
+ val label = Locale(language).displayLanguage
20
+
21
+ return SubtitleTrack(
22
+ id = id,
23
+ language = language,
24
+ label = label
25
+ )
26
+ }
27
+ }
28
+ }
@@ -44,3 +44,13 @@ class TimeUpdate(
44
44
  @Field var currentLiveTimestamp: Long?,
45
45
  @Field var bufferedPosition: Double = .0
46
46
  ) : VideoEventPayload
47
+
48
+ class SubtitleTrackChangedEventPayload(
49
+ @Field val subtitleTrack: SubtitleTrack?,
50
+ @Field val oldSubtitleTrack: SubtitleTrack?
51
+ ) : VideoEventPayload
52
+
53
+ class AvailableSubtitleTracksChangedEventPayload(
54
+ @Field val availableSubtitleTracks: List<SubtitleTrack>,
55
+ @Field val oldAvailableSubtitleTracks: List<SubtitleTrack>
56
+ ) : VideoEventPayload
@@ -38,7 +38,8 @@ class VideoSource(
38
38
  "NotificationDataArtwork:${this.metadata?.artwork?.path}"
39
39
  }
40
40
 
41
- fun toMediaSource(context: Context): MediaSource {
41
+ fun toMediaSource(context: Context): MediaSource? {
42
+ this.uri ?: return null
42
43
  return buildMediaSourceWithHeaders(context, this)
43
44
  }
44
45