@stepincto/expo-video 1.0.0

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 (180) hide show
  1. package/README.md +45 -0
  2. package/android/build.gradle +32 -0
  3. package/android/src/main/AndroidManifest.xml +20 -0
  4. package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
  5. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
  6. package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
  7. package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
  8. package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
  9. package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
  10. package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
  11. package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
  12. package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
  13. package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
  14. package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
  15. package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
  16. package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
  17. package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
  18. package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
  19. package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
  20. package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
  21. package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
  22. package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
  23. package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
  24. package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
  25. package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
  26. package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
  27. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
  28. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
  29. package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
  30. package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
  31. package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
  32. package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
  33. package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
  34. package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
  35. package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
  36. package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
  37. package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
  38. package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
  39. package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
  40. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
  41. package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
  42. package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
  43. package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
  44. package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
  45. package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
  46. package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
  47. package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
  48. package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
  49. package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
  50. package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
  51. package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
  52. package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
  53. package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
  54. package/android/src/main/res/layout/surface_player_view.xml +7 -0
  55. package/android/src/main/res/layout/texture_player_view.xml +7 -0
  56. package/android/src/main/res/values/styles.xml +9 -0
  57. package/app.plugin.js +1 -0
  58. package/build/NativeVideoModule.d.ts +16 -0
  59. package/build/NativeVideoModule.d.ts.map +1 -0
  60. package/build/NativeVideoModule.js +3 -0
  61. package/build/NativeVideoModule.js.map +1 -0
  62. package/build/NativeVideoModule.web.d.ts +3 -0
  63. package/build/NativeVideoModule.web.d.ts.map +1 -0
  64. package/build/NativeVideoModule.web.js +2 -0
  65. package/build/NativeVideoModule.web.js.map +1 -0
  66. package/build/NativeVideoView.d.ts +4 -0
  67. package/build/NativeVideoView.d.ts.map +1 -0
  68. package/build/NativeVideoView.js +6 -0
  69. package/build/NativeVideoView.js.map +1 -0
  70. package/build/VideoModule.d.ts +38 -0
  71. package/build/VideoModule.d.ts.map +1 -0
  72. package/build/VideoModule.js +53 -0
  73. package/build/VideoModule.js.map +1 -0
  74. package/build/VideoPlayer.d.ts +15 -0
  75. package/build/VideoPlayer.d.ts.map +1 -0
  76. package/build/VideoPlayer.js +52 -0
  77. package/build/VideoPlayer.js.map +1 -0
  78. package/build/VideoPlayer.types.d.ts +532 -0
  79. package/build/VideoPlayer.types.d.ts.map +1 -0
  80. package/build/VideoPlayer.types.js +2 -0
  81. package/build/VideoPlayer.types.js.map +1 -0
  82. package/build/VideoPlayer.web.d.ts +75 -0
  83. package/build/VideoPlayer.web.d.ts.map +1 -0
  84. package/build/VideoPlayer.web.js +376 -0
  85. package/build/VideoPlayer.web.js.map +1 -0
  86. package/build/VideoPlayerEvents.types.d.ts +262 -0
  87. package/build/VideoPlayerEvents.types.d.ts.map +1 -0
  88. package/build/VideoPlayerEvents.types.js +2 -0
  89. package/build/VideoPlayerEvents.types.js.map +1 -0
  90. package/build/VideoThumbnail.d.ts +29 -0
  91. package/build/VideoThumbnail.d.ts.map +1 -0
  92. package/build/VideoThumbnail.js +3 -0
  93. package/build/VideoThumbnail.js.map +1 -0
  94. package/build/VideoView.d.ts +44 -0
  95. package/build/VideoView.d.ts.map +1 -0
  96. package/build/VideoView.js +76 -0
  97. package/build/VideoView.js.map +1 -0
  98. package/build/VideoView.types.d.ts +147 -0
  99. package/build/VideoView.types.d.ts.map +1 -0
  100. package/build/VideoView.types.js +2 -0
  101. package/build/VideoView.types.js.map +1 -0
  102. package/build/VideoView.web.d.ts +9 -0
  103. package/build/VideoView.web.d.ts.map +1 -0
  104. package/build/VideoView.web.js +180 -0
  105. package/build/VideoView.web.js.map +1 -0
  106. package/build/index.d.ts +9 -0
  107. package/build/index.d.ts.map +1 -0
  108. package/build/index.js +7 -0
  109. package/build/index.js.map +1 -0
  110. package/build/resolveAssetSource.d.ts +3 -0
  111. package/build/resolveAssetSource.d.ts.map +1 -0
  112. package/build/resolveAssetSource.js +3 -0
  113. package/build/resolveAssetSource.js.map +1 -0
  114. package/build/resolveAssetSource.web.d.ts +4 -0
  115. package/build/resolveAssetSource.web.d.ts.map +1 -0
  116. package/build/resolveAssetSource.web.js +16 -0
  117. package/build/resolveAssetSource.web.js.map +1 -0
  118. package/expo-module.config.json +9 -0
  119. package/ios/Cache/CachableRequest.swift +44 -0
  120. package/ios/Cache/CachedResource.swift +97 -0
  121. package/ios/Cache/CachingHelpers.swift +92 -0
  122. package/ios/Cache/MediaFileHandle.swift +94 -0
  123. package/ios/Cache/MediaInfo.swift +147 -0
  124. package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
  125. package/ios/Cache/SynchronizedHashTable.swift +23 -0
  126. package/ios/Cache/VideoCacheManager.swift +338 -0
  127. package/ios/ContentKeyDelegate.swift +214 -0
  128. package/ios/ContentKeyManager.swift +21 -0
  129. package/ios/Enums/AudioMixingMode.swift +37 -0
  130. package/ios/Enums/ContentType.swift +12 -0
  131. package/ios/Enums/DRMType.swift +20 -0
  132. package/ios/Enums/PlayerStatus.swift +10 -0
  133. package/ios/Enums/VideoContentFit.swift +39 -0
  134. package/ios/ExpoVideo.podspec +29 -0
  135. package/ios/NowPlayingManager.swift +296 -0
  136. package/ios/Records/BufferOptions.swift +12 -0
  137. package/ios/Records/DRMOptions.swift +24 -0
  138. package/ios/Records/PlaybackError.swift +10 -0
  139. package/ios/Records/Tracks.swift +176 -0
  140. package/ios/Records/VideoEventPayloads.swift +76 -0
  141. package/ios/Records/VideoMetadata.swift +16 -0
  142. package/ios/Records/VideoSize.swift +15 -0
  143. package/ios/Records/VideoSource.swift +25 -0
  144. package/ios/Thumbnails/VideoThumbnail.swift +27 -0
  145. package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
  146. package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
  147. package/ios/VideoAsset.swift +123 -0
  148. package/ios/VideoExceptions.swift +53 -0
  149. package/ios/VideoItem.swift +11 -0
  150. package/ios/VideoManager.swift +140 -0
  151. package/ios/VideoModule.swift +383 -0
  152. package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
  153. package/ios/VideoPlayer.swift +435 -0
  154. package/ios/VideoPlayerAudioTracks.swift +72 -0
  155. package/ios/VideoPlayerItem.swift +97 -0
  156. package/ios/VideoPlayerObserver.swift +523 -0
  157. package/ios/VideoPlayerSubtitles.swift +71 -0
  158. package/ios/VideoSourceLoader.swift +89 -0
  159. package/ios/VideoSourceLoaderListener.swift +34 -0
  160. package/ios/VideoView.swift +224 -0
  161. package/package.json +59 -0
  162. package/plugin/build/tsconfig.tsbuildinfo +1 -0
  163. package/plugin/build/withExpoVideo.d.ts +7 -0
  164. package/plugin/build/withExpoVideo.js +38 -0
  165. package/src/NativeVideoModule.ts +20 -0
  166. package/src/NativeVideoModule.web.ts +1 -0
  167. package/src/NativeVideoView.ts +8 -0
  168. package/src/VideoModule.ts +59 -0
  169. package/src/VideoPlayer.tsx +67 -0
  170. package/src/VideoPlayer.types.ts +613 -0
  171. package/src/VideoPlayer.web.tsx +451 -0
  172. package/src/VideoPlayerEvents.types.ts +313 -0
  173. package/src/VideoThumbnail.ts +31 -0
  174. package/src/VideoView.tsx +86 -0
  175. package/src/VideoView.types.ts +165 -0
  176. package/src/VideoView.web.tsx +214 -0
  177. package/src/index.ts +46 -0
  178. package/src/resolveAssetSource.ts +2 -0
  179. package/src/resolveAssetSource.web.ts +17 -0
  180. package/src/ts-declarations/react-native-assets.d.ts +1 -0
@@ -0,0 +1,460 @@
1
+ package expo.modules.video.player
2
+
3
+ import android.content.Context
4
+ import android.media.MediaMetadataRetriever
5
+ import androidx.media3.common.C
6
+ import android.webkit.URLUtil
7
+ import androidx.annotation.OptIn
8
+ import androidx.media3.common.Format
9
+ import androidx.media3.common.MediaItem
10
+ import androidx.media3.common.MimeTypes
11
+ import androidx.media3.common.PlaybackException
12
+ import androidx.media3.common.PlaybackParameters
13
+ import androidx.media3.common.Player
14
+ import androidx.media3.common.Player.STATE_BUFFERING
15
+ import androidx.media3.common.Timeline
16
+ import androidx.media3.common.TrackSelectionParameters
17
+ import androidx.media3.common.Tracks
18
+ import androidx.media3.common.util.UnstableApi
19
+ import androidx.media3.exoplayer.DecoderReuseEvaluation
20
+ import androidx.media3.exoplayer.DefaultRenderersFactory
21
+ import androidx.media3.exoplayer.ExoPlayer
22
+ import androidx.media3.exoplayer.analytics.AnalyticsListener
23
+ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
24
+ import androidx.media3.ui.PlayerView
25
+ import expo.modules.kotlin.AppContext
26
+ import expo.modules.kotlin.sharedobjects.SharedObject
27
+ import expo.modules.video.IntervalUpdateClock
28
+ import expo.modules.video.IntervalUpdateEmitter
29
+ import expo.modules.video.VideoManager
30
+ import expo.modules.video.delegates.IgnoreSameSet
31
+ import expo.modules.video.enums.AudioMixingMode
32
+ import expo.modules.video.enums.PlayerStatus
33
+ import expo.modules.video.enums.PlayerStatus.*
34
+ import expo.modules.video.playbackService.ExpoVideoPlaybackService
35
+ import expo.modules.video.playbackService.PlaybackServiceConnection
36
+ import expo.modules.video.records.BufferOptions
37
+ import expo.modules.video.records.PlaybackError
38
+ import expo.modules.video.records.TimeUpdate
39
+ import expo.modules.video.records.VideoSource
40
+ import expo.modules.video.utils.MutableWeakReference
41
+ import expo.modules.video.records.VideoTrack
42
+ import kotlinx.coroutines.launch
43
+ import java.io.FileInputStream
44
+ import java.lang.ref.WeakReference
45
+
46
+ // https://developer.android.com/guide/topics/media/media3/getting-started/migration-guide#improvements_in_media3
47
+ @UnstableApi
48
+ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSource?) : AutoCloseable, SharedObject(appContext), IntervalUpdateEmitter {
49
+ // This improves the performance of playing DRM-protected content
50
+ private var renderersFactory = DefaultRenderersFactory(context)
51
+ .forceEnableMediaCodecAsynchronousQueueing()
52
+ .setEnableDecoderFallback(true)
53
+ private var listeners: MutableList<WeakReference<VideoPlayerListener>> = mutableListOf()
54
+ private var currentPlayerView = MutableWeakReference<PlayerView?>(null)
55
+ val loadControl: VideoPlayerLoadControl = VideoPlayerLoadControl.Builder().build()
56
+ val subtitles: VideoPlayerSubtitles = VideoPlayerSubtitles(this)
57
+ val audioTracks: VideoPlayerAudioTracks = VideoPlayerAudioTracks(this)
58
+ val trackSelector = DefaultTrackSelector(context)
59
+
60
+ val player = ExoPlayer
61
+ .Builder(context, renderersFactory)
62
+ .setLooper(context.mainLooper)
63
+ .setLoadControl(loadControl)
64
+ .build()
65
+
66
+ private val firstFrameEventGenerator = createFirstFrameEventGenerator()
67
+ val serviceConnection = PlaybackServiceConnection(WeakReference(this))
68
+ val intervalUpdateClock = IntervalUpdateClock(this)
69
+
70
+ var playing by IgnoreSameSet(false) { new, old ->
71
+ sendEvent(PlayerEvent.IsPlayingChanged(new, old))
72
+ }
73
+
74
+ var uncommittedSource: VideoSource? = source
75
+ private var commitedSource by IgnoreSameSet<VideoSource?>(null) { new, old ->
76
+ sendEvent(PlayerEvent.SourceChanged(new, old))
77
+ }
78
+
79
+ // Volume of the player if there was no mute applied.
80
+ var userVolume = 1f
81
+ var status: PlayerStatus = IDLE
82
+ var requiresLinearPlayback = false
83
+ var staysActiveInBackground = false
84
+ var preservesPitch = false
85
+ set(preservesPitch) {
86
+ field = preservesPitch
87
+ playbackParameters = applyPitchCorrection(playbackParameters)
88
+ }
89
+ var showNowPlayingNotification = false
90
+ set(value) {
91
+ field = value
92
+ serviceConnection.playbackServiceBinder?.service?.setShowNotification(value, this.player)
93
+ }
94
+ var duration = 0f
95
+ var isLive = false
96
+
97
+ var volume: Float by IgnoreSameSet(1f) { new: Float, old: Float ->
98
+ player.volume = if (muted) 0f else new
99
+ userVolume = volume
100
+ sendEvent(PlayerEvent.VolumeChanged(new, old))
101
+ }
102
+
103
+ var muted: Boolean by IgnoreSameSet(false) { new: Boolean, old: Boolean ->
104
+ player.volume = if (new) 0f else userVolume
105
+ sendEvent(PlayerEvent.MutedChanged(new, old))
106
+ }
107
+
108
+ var playbackParameters by IgnoreSameSet(
109
+ PlaybackParameters.DEFAULT,
110
+ propertyMapper = { applyPitchCorrection(it) }
111
+ ) { new: PlaybackParameters, old: PlaybackParameters ->
112
+ player.playbackParameters = new
113
+
114
+ if (old.speed != new.speed) {
115
+ sendEvent(PlayerEvent.PlaybackRateChanged(new.speed, old.speed))
116
+ }
117
+ }
118
+
119
+ val currentOffsetFromLive: Float?
120
+ get() {
121
+ return if (player.currentLiveOffset == C.TIME_UNSET) {
122
+ null
123
+ } else {
124
+ player.currentLiveOffset / 1000f
125
+ }
126
+ }
127
+
128
+ val currentLiveTimestamp: Long?
129
+ get() {
130
+ val window = Timeline.Window()
131
+ if (!player.currentTimeline.isEmpty) {
132
+ player.currentTimeline.getWindow(player.currentMediaItemIndex, window)
133
+ }
134
+ if (window.windowStartTimeMs == C.TIME_UNSET) {
135
+ return null
136
+ }
137
+ return window.windowStartTimeMs + player.currentPosition
138
+ }
139
+
140
+ var bufferOptions: BufferOptions = BufferOptions()
141
+ set(value) {
142
+ field = value
143
+ loadControl.applyBufferOptions(value)
144
+ }
145
+
146
+ val bufferedPosition: Double
147
+ get() {
148
+ if (player.currentMediaItem == null) {
149
+ return -1.0
150
+ }
151
+ if (player.playbackState == STATE_BUFFERING) {
152
+ return 0.0
153
+ }
154
+ return player.bufferedPosition / 1000.0
155
+ }
156
+
157
+ var audioMixingMode: AudioMixingMode = AudioMixingMode.AUTO
158
+ set(value) {
159
+ val old = field
160
+ field = value
161
+ sendEvent(PlayerEvent.AudioMixingModeChanged(value, old))
162
+ }
163
+
164
+ var isLoadingNewSource = false
165
+ private set
166
+
167
+ var currentVideoTrack: VideoTrack? = null
168
+ private set(value) {
169
+ val old = field
170
+ field = value
171
+ sendEvent(PlayerEvent.VideoTrackChanged(value, old))
172
+ }
173
+
174
+ var availableVideoTracks: List<VideoTrack> = emptyList()
175
+ private set
176
+
177
+ private val playerListener = object : Player.Listener {
178
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
179
+ this@VideoPlayer.playing = isPlaying
180
+ }
181
+
182
+ override fun onTracksChanged(tracks: Tracks) {
183
+ val oldSubtitleTracks = ArrayList(subtitles.availableSubtitleTracks)
184
+ val oldAudioTracks = ArrayList(audioTracks.availableAudioTracks)
185
+ val oldCurrentTrack = subtitles.currentSubtitleTrack
186
+ val oldCurrentAudioTrack = audioTracks.currentAudioTrack
187
+
188
+ // Emit the tracks change event to update the subtitles
189
+ sendEvent(PlayerEvent.TracksChanged(tracks))
190
+
191
+ val newSubtitleTracks = subtitles.availableSubtitleTracks
192
+ val newAudioTracks = audioTracks.availableAudioTracks
193
+ val newCurrentSubtitleTrack = subtitles.currentSubtitleTrack
194
+ val newCurrentAudioTrack = audioTracks.currentAudioTrack
195
+ availableVideoTracks = tracks.toVideoTracks()
196
+
197
+ if (isLoadingNewSource) {
198
+ sendEvent(
199
+ PlayerEvent.VideoSourceLoaded(
200
+ commitedSource,
201
+ this@VideoPlayer.player.duration / 1000.0,
202
+ availableVideoTracks,
203
+ newSubtitleTracks,
204
+ newAudioTracks
205
+ )
206
+ )
207
+ isLoadingNewSource = false
208
+ }
209
+
210
+ if (!oldSubtitleTracks.toArray().contentEquals(newSubtitleTracks.toArray())) {
211
+ sendEvent(PlayerEvent.AvailableSubtitleTracksChanged(newSubtitleTracks, oldSubtitleTracks))
212
+ }
213
+ if (!oldAudioTracks.toArray().contentEquals(newAudioTracks.toArray())) {
214
+ sendEvent(PlayerEvent.AvailableAudioTracksChanged(newAudioTracks, oldAudioTracks))
215
+ }
216
+ if (oldCurrentTrack != newCurrentSubtitleTrack) {
217
+ sendEvent(PlayerEvent.SubtitleTrackChanged(newCurrentSubtitleTrack, oldCurrentTrack))
218
+ }
219
+ if (oldCurrentAudioTrack != newCurrentAudioTrack) {
220
+ sendEvent(PlayerEvent.AudioTrackChanged(newCurrentAudioTrack, oldCurrentAudioTrack))
221
+ }
222
+ super.onTracksChanged(tracks)
223
+ }
224
+
225
+ override fun onTrackSelectionParametersChanged(parameters: TrackSelectionParameters) {
226
+ val oldTrack = subtitles.currentSubtitleTrack
227
+ val oldAudioTrack = audioTracks.currentAudioTrack
228
+ sendEvent(PlayerEvent.TrackSelectionParametersChanged(parameters))
229
+
230
+ val newTrack = subtitles.currentSubtitleTrack
231
+ val newAudioTrack = audioTracks.currentAudioTrack
232
+ sendEvent(PlayerEvent.SubtitleTrackChanged(newTrack, oldTrack))
233
+ sendEvent(PlayerEvent.AudioTrackChanged(newAudioTrack, oldAudioTrack))
234
+ super.onTrackSelectionParametersChanged(parameters)
235
+ }
236
+
237
+ override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
238
+ this@VideoPlayer.duration = 0f
239
+ this@VideoPlayer.isLive = false
240
+ if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT) {
241
+ sendEvent(PlayerEvent.PlayedToEnd())
242
+ }
243
+ subtitles.setSubtitlesEnabled(false)
244
+ super.onMediaItemTransition(mediaItem, reason)
245
+ }
246
+
247
+ override fun onPlaybackStateChanged(@Player.State playbackState: Int) {
248
+ if (playbackState == Player.STATE_IDLE && player.playerError != null) {
249
+ return
250
+ }
251
+ if (playbackState == Player.STATE_READY) {
252
+ this@VideoPlayer.duration = this@VideoPlayer.player.duration / 1000f
253
+ this@VideoPlayer.isLive = this@VideoPlayer.player.isCurrentMediaItemLive
254
+ }
255
+ setStatus(playerStateToPlayerStatus(playbackState), null)
256
+ super.onPlaybackStateChanged(playbackState)
257
+ }
258
+
259
+ override fun onVolumeChanged(volume: Float) {
260
+ if (!muted) {
261
+ this@VideoPlayer.volume = volume
262
+ }
263
+ }
264
+
265
+ override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {
266
+ this@VideoPlayer.playbackParameters = playbackParameters
267
+ super.onPlaybackParametersChanged(playbackParameters)
268
+ }
269
+
270
+ override fun onPlayerErrorChanged(error: PlaybackException?) {
271
+ error?.let {
272
+ this@VideoPlayer.duration = 0f
273
+ this@VideoPlayer.isLive = false
274
+ setStatus(ERROR, error)
275
+ } ?: run {
276
+ setStatus(playerStateToPlayerStatus(player.playbackState), null)
277
+ }
278
+
279
+ super.onPlayerErrorChanged(error)
280
+ }
281
+ }
282
+
283
+ private val analyticsListener = object : AnalyticsListener {
284
+ override fun onVideoInputFormatChanged(eventTime: AnalyticsListener.EventTime, format: Format, decoderReuseEvaluation: DecoderReuseEvaluation?) {
285
+ currentVideoTrack = availableVideoTracks.firstOrNull { it.format?.id == format.id }
286
+ super.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation)
287
+ }
288
+ }
289
+
290
+ init {
291
+ ExpoVideoPlaybackService.startService(appContext, context, serviceConnection)
292
+ player.addListener(playerListener)
293
+ player.addAnalyticsListener(analyticsListener)
294
+ VideoManager.registerVideoPlayer(this)
295
+
296
+ // ExoPlayer will enable subtitles automatically at the start, we want them disabled by default
297
+ appContext.mainQueue.launch {
298
+ subtitles.setSubtitlesEnabled(false)
299
+ }
300
+ }
301
+
302
+ override fun close() {
303
+ appContext?.reactContext?.unbindService(serviceConnection)
304
+ serviceConnection.playbackServiceBinder?.service?.unregisterPlayer(player)
305
+ VideoManager.unregisterVideoPlayer(this@VideoPlayer)
306
+
307
+ appContext?.mainQueue?.launch {
308
+ player.removeListener(playerListener)
309
+ player.release()
310
+ }
311
+ uncommittedSource = null
312
+ commitedSource = null
313
+ }
314
+
315
+ override fun deallocate() {
316
+ super.deallocate()
317
+ close()
318
+ }
319
+
320
+ fun changePlayerView(playerView: PlayerView?) {
321
+ PlayerView.switchTargetView(player, currentPlayerView.get(), playerView)
322
+ currentPlayerView.set(playerView)
323
+ }
324
+
325
+ fun prepare() {
326
+ availableVideoTracks = listOf()
327
+ currentVideoTrack = null
328
+
329
+ val newSource = uncommittedSource
330
+ val mediaSource = newSource?.toMediaSource(context)
331
+
332
+ mediaSource?.let {
333
+ player.setMediaSource(it)
334
+ player.prepare()
335
+ commitedSource = newSource
336
+ uncommittedSource = null
337
+ isLoadingNewSource = true
338
+ } ?: run {
339
+ player.clearMediaItems()
340
+ player.prepare()
341
+ isLoadingNewSource = false
342
+ }
343
+ }
344
+
345
+ private fun applyPitchCorrection(playbackParameters: PlaybackParameters): PlaybackParameters {
346
+ val speed = playbackParameters.speed
347
+ val pitch = if (preservesPitch) 1f else speed
348
+ return PlaybackParameters(speed, pitch)
349
+ }
350
+
351
+ private fun playerStateToPlayerStatus(@Player.State state: Int): PlayerStatus {
352
+ return when (state) {
353
+ Player.STATE_IDLE -> IDLE
354
+ Player.STATE_BUFFERING -> LOADING
355
+ Player.STATE_READY -> READY_TO_PLAY
356
+ Player.STATE_ENDED -> {
357
+ // When an error occurs, the player state changes to ENDED.
358
+ if (player.playerError != null) {
359
+ ERROR
360
+ } else {
361
+ IDLE
362
+ }
363
+ }
364
+
365
+ else -> IDLE
366
+ }
367
+ }
368
+
369
+ private fun setStatus(status: PlayerStatus, error: PlaybackException?) {
370
+ val oldStatus = this.status
371
+ this.status = status
372
+
373
+ val playbackError = error?.let {
374
+ PlaybackError(it)
375
+ }
376
+
377
+ if (playbackError == null && player.playbackState == Player.STATE_ENDED) {
378
+ sendEvent(PlayerEvent.PlayedToEnd())
379
+ }
380
+
381
+ if (this.status != oldStatus) {
382
+ sendEvent(PlayerEvent.StatusChanged(status, oldStatus, playbackError))
383
+ }
384
+ }
385
+
386
+ fun addListener(videoPlayerListener: VideoPlayerListener) {
387
+ if (listeners.all { it.get() != videoPlayerListener }) {
388
+ listeners.add(WeakReference(videoPlayerListener))
389
+ }
390
+ }
391
+
392
+ fun removeListener(videoPlayerListener: VideoPlayerListener) {
393
+ listeners.removeAll { it.get() == videoPlayerListener }
394
+ }
395
+
396
+ private fun sendEvent(event: PlayerEvent) {
397
+ // Emits to the native listeners
398
+ val listenersSnapshot = listeners.toList().mapNotNull { it.get() }
399
+
400
+ event.emit(this, listenersSnapshot)
401
+
402
+ // Emits to the JS side
403
+ if (event.emitToJS) {
404
+ emit(event.name, event.jsEventPayload)
405
+ }
406
+ }
407
+
408
+ private fun createFirstFrameEventGenerator(): FirstFrameEventGenerator {
409
+ return FirstFrameEventGenerator(player, currentPlayerView) {
410
+ sendEvent(PlayerEvent.RenderedFirstFrame())
411
+ }
412
+ }
413
+
414
+ // IntervalUpdateEmitter
415
+ override fun emitTimeUpdate() {
416
+ appContext?.mainQueue?.launch {
417
+ val updatePayload = TimeUpdate(player.currentPosition / 1000.0, currentOffsetFromLive, currentLiveTimestamp, bufferedPosition)
418
+ sendEvent(PlayerEvent.TimeUpdated(updatePayload))
419
+ }
420
+ }
421
+
422
+ fun toMetadataRetriever(): MediaMetadataRetriever {
423
+ val source = uncommittedSource ?: commitedSource
424
+ val uri = source?.uri ?: throw IllegalStateException("Video source is not set")
425
+ val stringUri = uri.toString()
426
+
427
+ val mediaMetadataRetriever = MediaMetadataRetriever()
428
+ if (URLUtil.isFileUrl(stringUri)) {
429
+ mediaMetadataRetriever.setDataSource(stringUri.replace("file://", ""))
430
+ } else if (URLUtil.isContentUrl(stringUri)) {
431
+ context.contentResolver.openFileDescriptor(uri, "r")?.use { parcelFileDescriptor ->
432
+ FileInputStream(parcelFileDescriptor.fileDescriptor).use { inputStream ->
433
+ mediaMetadataRetriever.setDataSource(inputStream.fd)
434
+ }
435
+ }
436
+ } else {
437
+ mediaMetadataRetriever.setDataSource(stringUri, source.headers ?: emptyMap())
438
+ }
439
+ return mediaMetadataRetriever
440
+ }
441
+ }
442
+
443
+ // Extension functions
444
+
445
+ @OptIn(UnstableApi::class)
446
+ private fun Tracks.toVideoTracks(): List<VideoTrack> {
447
+ val videoTracks = mutableListOf<VideoTrack?>()
448
+ for (group in this.groups) {
449
+ for (i in 0 until group.length) {
450
+ val format = group.getTrackFormat(i)
451
+ val isSupported = group.isTrackSupported(i)
452
+
453
+ if (!MimeTypes.isVideo(format.sampleMimeType)) {
454
+ continue
455
+ }
456
+ videoTracks.add(VideoTrack.fromFormat(format, isSupported))
457
+ }
458
+ }
459
+ return videoTracks.filterNotNull()
460
+ }
@@ -0,0 +1,125 @@
1
+ package expo.modules.video.player
2
+
3
+ import androidx.annotation.OptIn
4
+ import androidx.media3.common.C
5
+ import androidx.media3.common.Format
6
+ import androidx.media3.common.MimeTypes
7
+ import androidx.media3.common.TrackGroup
8
+ import androidx.media3.common.TrackSelectionOverride
9
+ import androidx.media3.common.TrackSelectionParameters
10
+ import androidx.media3.common.Tracks
11
+ import androidx.media3.common.util.UnstableApi
12
+ import expo.modules.video.records.AudioTrack
13
+ import java.lang.ref.WeakReference
14
+
15
+ @OptIn(UnstableApi::class)
16
+ class VideoPlayerAudioTracks(owner: VideoPlayer) : VideoPlayerListener {
17
+ private val owner = WeakReference(owner)
18
+ private val videoPlayer: VideoPlayer?
19
+ get() {
20
+ return owner.get()
21
+ }
22
+ private val formatsToGroups = mutableMapOf<Format, Pair<TrackGroup, Int>>()
23
+ private var currentAudioTrackFormat: Format? = null
24
+ private var currentOverride: TrackSelectionOverride? = null
25
+
26
+ var currentAudioTrack: AudioTrack?
27
+ get() {
28
+ return AudioTrack.fromFormat(currentAudioTrackFormat)
29
+ }
30
+ set(value) {
31
+ applyAudioTrack(value)
32
+ }
33
+ val availableAudioTracks = arrayListOf<AudioTrack>()
34
+
35
+ init {
36
+ owner.addListener(this)
37
+ }
38
+
39
+ fun setAudioTracksEnabled(enabled: Boolean) {
40
+ val currentParams = videoPlayer?.player?.trackSelectionParameters ?: return
41
+ var params = currentParams.buildUpon().setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, !enabled).build()
42
+ if (!enabled) {
43
+ params = params.buildUpon().clearOverridesOfType(C.TRACK_TYPE_AUDIO).build()
44
+ }
45
+ videoPlayer?.player?.trackSelectionParameters = params
46
+ }
47
+
48
+ // VideoPlayerListener
49
+ override fun onTrackSelectionParametersChanged(player: VideoPlayer, trackSelectionParameters: TrackSelectionParameters) {
50
+ currentAudioTrackFormat = findSelectedAudioFormat()
51
+ super.onTrackSelectionParametersChanged(player, trackSelectionParameters)
52
+ }
53
+
54
+ override fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {
55
+ formatsToGroups.clear()
56
+ availableAudioTracks.clear()
57
+ for (group in tracks.groups) {
58
+ for (i in 0..<group.length) {
59
+ val format: Format = group.getTrackFormat(i)
60
+
61
+ if (MimeTypes.isAudio(format.sampleMimeType)) {
62
+ formatsToGroups[format] = group.mediaTrackGroup to i
63
+ val track = AudioTrack.fromFormat(format) ?: continue
64
+ availableAudioTracks.add(track)
65
+ }
66
+ }
67
+ }
68
+ currentAudioTrackFormat = findSelectedAudioFormat()
69
+ super.onTracksChanged(player, tracks)
70
+ }
71
+
72
+ // Private methods
73
+ private fun applyAudioTrack(audioTrack: AudioTrack?) {
74
+ val player = videoPlayer?.player ?: return
75
+ var newParameters: TrackSelectionParameters = player.trackSelectionParameters
76
+ currentOverride?.let { override ->
77
+ newParameters = newParameters.buildUpon().clearOverridesOfType(C.TRACK_TYPE_AUDIO).build()
78
+ }
79
+ if (audioTrack == null) {
80
+ player.trackSelectionParameters = newParameters
81
+ setAudioTracksEnabled(false)
82
+ currentOverride = null
83
+ return
84
+ }
85
+ val format = formatsToGroups.keys.firstOrNull {
86
+ it.id == audioTrack.id
87
+ }
88
+ format?.let {
89
+ formatsToGroups[it]?.let { subtitlePair ->
90
+ val trackSelectionOverride = TrackSelectionOverride(subtitlePair.first, subtitlePair.second)
91
+ newParameters = newParameters.buildUpon().addOverride(trackSelectionOverride).build()
92
+ player.trackSelectionParameters = newParameters
93
+ setAudioTracksEnabled(true)
94
+ currentOverride = trackSelectionOverride
95
+ }
96
+ }
97
+ }
98
+
99
+ private fun findSelectedAudioFormat(): Format? {
100
+ val trackSelectionParameters = videoPlayer?.player?.trackSelectionParameters
101
+ val preferredAudioLanguages = trackSelectionParameters?.preferredAudioLanguages
102
+ val overriddenFormat: Format? = trackSelectionParameters?.overrides?.let {
103
+ for ((group, trackSelectionOverride) in it) {
104
+ if (group.type == C.TRACK_TYPE_AUDIO) {
105
+ // For audioTracks only one index will be replaced
106
+ return@let trackSelectionOverride.trackIndices.firstOrNull()?.let { index ->
107
+ group.getFormat(index)
108
+ }
109
+ }
110
+ }
111
+ return@let null
112
+ }
113
+
114
+ val preferredFormat: Format? = preferredAudioLanguages?.let { preferredAudioLanguages ->
115
+ for (preferredLanguage in preferredAudioLanguages) {
116
+ return@let formatsToGroups.keys.firstOrNull {
117
+ it.language == preferredLanguage
118
+ }
119
+ }
120
+ return@let null
121
+ }
122
+
123
+ return overriddenFormat ?: preferredFormat
124
+ }
125
+ }
@@ -0,0 +1,32 @@
1
+ package expo.modules.video.player
2
+
3
+ import androidx.annotation.OptIn
4
+ import androidx.media3.common.TrackSelectionParameters
5
+ import androidx.media3.common.Tracks
6
+ import androidx.media3.common.util.UnstableApi
7
+ import expo.modules.video.enums.AudioMixingMode
8
+ import expo.modules.video.enums.PlayerStatus
9
+ import expo.modules.video.records.AudioTrack
10
+ import expo.modules.video.records.PlaybackError
11
+ import expo.modules.video.records.SubtitleTrack
12
+ import expo.modules.video.records.VideoSource
13
+ import expo.modules.video.records.TimeUpdate
14
+ import expo.modules.video.records.VideoTrack
15
+
16
+ @OptIn(UnstableApi::class)
17
+ interface VideoPlayerListener {
18
+ fun onStatusChanged(player: VideoPlayer, status: PlayerStatus, oldStatus: PlayerStatus?, error: PlaybackError?) {}
19
+ fun onIsPlayingChanged(player: VideoPlayer, isPlaying: Boolean, oldIsPlaying: Boolean?) {}
20
+ fun onVolumeChanged(player: VideoPlayer, volume: Float, oldVolume: Float?) {}
21
+ fun onMutedChanged(player: VideoPlayer, muted: Boolean, oldMuted: Boolean?) {}
22
+ fun onSourceChanged(player: VideoPlayer, source: VideoSource?, oldSource: VideoSource?) {}
23
+ fun onPlaybackRateChanged(player: VideoPlayer, rate: Float, oldRate: Float?) {}
24
+ fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {}
25
+ fun onTrackSelectionParametersChanged(player: VideoPlayer, trackSelectionParameters: TrackSelectionParameters) {}
26
+ fun onTimeUpdate(player: VideoPlayer, timeUpdate: TimeUpdate) {}
27
+ fun onPlayedToEnd(player: VideoPlayer) {}
28
+ fun onAudioMixingModeChanged(player: VideoPlayer, audioMixingMode: AudioMixingMode, oldAudioMixingMode: AudioMixingMode?) {}
29
+ fun onVideoTrackChanged(player: VideoPlayer, videoTrack: VideoTrack?, oldVideoTrack: VideoTrack?) {}
30
+ fun onVideoSourceLoaded(player: VideoPlayer, videoSource: VideoSource?, duration: Double?, availableVideoTracks: List<VideoTrack>, availableSubtitleTracks: List<SubtitleTrack>, availableAudioTracks: List<AudioTrack>) {}
31
+ fun onRenderedFirstFrame(player: VideoPlayer) {}
32
+ }