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.
- package/CHANGELOG.md +17 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +59 -11
- package/android/src/main/java/expo/modules/video/VideoModule.kt +27 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +1 -16
- package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +32 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +44 -4
- package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +4 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
- package/android/src/main/java/expo/modules/video/records/SubtitleTrack.kt +28 -0
- package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +10 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +2 -1
- package/build/VideoPlayer.d.ts +8 -3
- package/build/VideoPlayer.d.ts.map +1 -1
- package/build/VideoPlayer.js +9 -1
- package/build/VideoPlayer.js.map +1 -1
- package/build/VideoPlayer.types.d.ts +53 -0
- package/build/VideoPlayer.types.d.ts.map +1 -1
- package/build/VideoPlayer.types.js.map +1 -1
- package/build/VideoPlayer.web.d.ts +5 -1
- package/build/VideoPlayer.web.d.ts.map +1 -1
- package/build/VideoPlayer.web.js +7 -0
- package/build/VideoPlayer.web.js.map +1 -1
- package/build/VideoPlayerEvents.types.d.ts +30 -1
- package/build/VideoPlayerEvents.types.d.ts.map +1 -1
- package/build/VideoPlayerEvents.types.js.map +1 -1
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/ios/Enums/AudioMixingMode.swift +37 -0
- package/ios/NowPlayingManager.swift +8 -6
- package/ios/Records/SubtitleTrack.swift +21 -0
- package/ios/Records/VideoEventPayloads.swift +10 -0
- package/ios/VideoManager.swift +39 -17
- package/ios/VideoModule.swift +18 -0
- package/ios/VideoPlayer.swift +30 -0
- package/ios/VideoPlayerObserver.swift +38 -0
- package/ios/VideoPlayerSubtitles.swift +71 -0
- package/package.json +2 -2
- package/src/VideoPlayer.tsx +11 -3
- package/src/VideoPlayer.types.ts +60 -0
- package/src/VideoPlayer.web.tsx +11 -0
- package/src/VideoPlayerEvents.types.ts +35 -1
- 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
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
34
|
-
|
|
50
|
+
currentFocusRequest?.let {
|
|
51
|
+
if (it.focusGain == audioFocusType) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
35
54
|
}
|
|
36
55
|
|
|
37
|
-
val newFocusRequest = AudioFocusRequest.Builder(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
} ?:
|
|
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 =
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
268
|
+
val newSource = uncommittedSource
|
|
269
|
+
val mediaSource = newSource?.toMediaSource(context)
|
|
270
|
+
mediaSource?.let {
|
|
271
|
+
player.setMediaSource(it)
|
|
232
272
|
player.prepare()
|
|
233
|
-
lastLoadedSource =
|
|
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
|
|