expo-native-track-player 0.1.1
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/ExpoNativeTrackPlayer.podspec +21 -0
- package/LICENSE +20 -0
- package/README.md +267 -0
- package/android/build.gradle +71 -0
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/java/com/exponativetrackplayer/ExpoNativeTrackPlayerModule.kt +528 -0
- package/android/src/main/java/com/exponativetrackplayer/ExpoNativeTrackPlayerPackage.kt +34 -0
- package/android/src/main/java/com/exponativetrackplayer/ExpoNativeTrackPlayerService.kt +118 -0
- package/ios/ExpoNativeTrackPlayer.h +6 -0
- package/ios/ExpoNativeTrackPlayer.mm +475 -0
- package/lib/module/NativeExpoNativeTrackPlayer.js +5 -0
- package/lib/module/NativeExpoNativeTrackPlayer.js.map +1 -0
- package/lib/module/constants.js +31 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/events.js +56 -0
- package/lib/module/events.js.map +1 -0
- package/lib/module/hooks.js +105 -0
- package/lib/module/hooks.js.map +1 -0
- package/lib/module/index.js +150 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types/ResourceObject.js +2 -0
- package/lib/module/types/ResourceObject.js.map +1 -0
- package/lib/module/types/Track.js +4 -0
- package/lib/module/types/Track.js.map +1 -0
- package/lib/module/types/TrackMetadataBase.js +2 -0
- package/lib/module/types/TrackMetadataBase.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeExpoNativeTrackPlayer.d.ts +57 -0
- package/lib/typescript/src/NativeExpoNativeTrackPlayer.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +23 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/events.d.ts +163 -0
- package/lib/typescript/src/events.d.ts.map +1 -0
- package/lib/typescript/src/hooks.d.ts +12 -0
- package/lib/typescript/src/hooks.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +65 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types/ResourceObject.d.ts +5 -0
- package/lib/typescript/src/types/ResourceObject.d.ts.map +1 -0
- package/lib/typescript/src/types/Track.d.ts +22 -0
- package/lib/typescript/src/types/Track.d.ts.map +1 -0
- package/lib/typescript/src/types/TrackMetadataBase.d.ts +18 -0
- package/lib/typescript/src/types/TrackMetadataBase.d.ts.map +1 -0
- package/package.json +172 -0
- package/src/NativeExpoNativeTrackPlayer.ts +70 -0
- package/src/constants.ts +40 -0
- package/src/events.ts +136 -0
- package/src/hooks.ts +149 -0
- package/src/index.tsx +250 -0
- package/src/types/ResourceObject.ts +4 -0
- package/src/types/Track.ts +23 -0
- package/src/types/TrackMetadataBase.ts +17 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
package com.exponativetrackplayer
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import androidx.media3.common.AudioAttributes
|
|
6
|
+
import androidx.media3.common.C
|
|
7
|
+
import androidx.media3.common.MediaItem
|
|
8
|
+
import androidx.media3.common.MediaMetadata
|
|
9
|
+
import androidx.media3.common.PlaybackException
|
|
10
|
+
import androidx.media3.common.PlaybackParameters
|
|
11
|
+
import androidx.media3.common.Player
|
|
12
|
+
import androidx.media3.exoplayer.ExoPlayer
|
|
13
|
+
import android.util.Log
|
|
14
|
+
import com.facebook.react.bridge.Arguments
|
|
15
|
+
import com.facebook.react.bridge.ReadableArray
|
|
16
|
+
import com.facebook.react.bridge.ReadableMap
|
|
17
|
+
import com.facebook.react.bridge.ReadableType
|
|
18
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
19
|
+
import com.facebook.react.bridge.WritableArray
|
|
20
|
+
import com.facebook.react.bridge.WritableMap
|
|
21
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
22
|
+
import java.util.concurrent.CountDownLatch
|
|
23
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
24
|
+
|
|
25
|
+
class ExpoNativeTrackPlayerModule(reactContext: ReactApplicationContext) :
|
|
26
|
+
NativeExpoNativeTrackPlayerSpec(reactContext) {
|
|
27
|
+
|
|
28
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
29
|
+
private val queue = mutableListOf<Map<String, Any?>>()
|
|
30
|
+
private var currentIndex: Int = -1
|
|
31
|
+
private var repeatMode: String = "off"
|
|
32
|
+
private var loopStartMs: Long? = null
|
|
33
|
+
private var loopEndMs: Long? = null
|
|
34
|
+
private var playbackState: String = "stopped"
|
|
35
|
+
private var playbackRate: Float = 1f
|
|
36
|
+
|
|
37
|
+
private val eventPlaybackState = "playback-state"
|
|
38
|
+
private val eventPlaybackPosition = "playback-progress-updated"
|
|
39
|
+
private val eventTrackChanged = "playback-active-track-changed"
|
|
40
|
+
private val eventPlaybackQueueEnded = "playback-queue-ended"
|
|
41
|
+
private val eventQueueUpdated = "queue-updated"
|
|
42
|
+
|
|
43
|
+
private val player: ExoPlayer by lazy {
|
|
44
|
+
val audioAttributes = AudioAttributes.Builder()
|
|
45
|
+
.setUsage(C.USAGE_MEDIA)
|
|
46
|
+
.setContentType(C.CONTENT_TYPE_MUSIC)
|
|
47
|
+
.build()
|
|
48
|
+
ExoPlayer.Builder(reactContext)
|
|
49
|
+
.setAudioAttributes(audioAttributes, true)
|
|
50
|
+
.setHandleAudioBecomingNoisy(true)
|
|
51
|
+
.build()
|
|
52
|
+
.apply {
|
|
53
|
+
addListener(object : Player.Listener {
|
|
54
|
+
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
55
|
+
currentIndex = currentMediaItemIndex
|
|
56
|
+
emitTrackChanged()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override fun onPlaybackStateChanged(state: Int) {
|
|
60
|
+
if (state == Player.STATE_ENDED) {
|
|
61
|
+
handleTrackEnded()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override fun onPlayerError(error: PlaybackException) {
|
|
66
|
+
Log.e(TAG, "playerError code=${error.errorCode}", error)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
init {
|
|
73
|
+
startLoopWatcher()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override fun addToQueue(track: ReadableMap) {
|
|
77
|
+
val storedTrack = toStoredMap(track)
|
|
78
|
+
queue.add(storedTrack)
|
|
79
|
+
runOnMainBlocking {
|
|
80
|
+
player.addMediaItem(mediaItemFor(storedTrack))
|
|
81
|
+
if (player.mediaItemCount == 1) {
|
|
82
|
+
player.prepare()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
emitQueueUpdated()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override fun addQueue(tracks: ReadableArray) {
|
|
89
|
+
val storedTracks = mutableListOf<Map<String, Any?>>()
|
|
90
|
+
for (index in 0 until tracks.size()) {
|
|
91
|
+
val track = tracks.getMap(index) ?: continue
|
|
92
|
+
storedTracks.add(toStoredMap(track))
|
|
93
|
+
}
|
|
94
|
+
queue.addAll(storedTracks)
|
|
95
|
+
runOnMainBlocking {
|
|
96
|
+
storedTracks.forEach { storedTrack ->
|
|
97
|
+
player.addMediaItem(mediaItemFor(storedTrack))
|
|
98
|
+
}
|
|
99
|
+
if (storedTracks.isNotEmpty()) {
|
|
100
|
+
player.prepare()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
emitQueueUpdated()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
override fun getQueue(): WritableArray {
|
|
107
|
+
val array = Arguments.createArray()
|
|
108
|
+
queue.forEach { array.pushMap(toWritableMap(it)) }
|
|
109
|
+
return array
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override fun removeFromQueue(trackId: String) {
|
|
113
|
+
val index = queue.indexOfFirst { (it["id"] as? String) == trackId }
|
|
114
|
+
if (index == -1) return
|
|
115
|
+
queue.removeAt(index)
|
|
116
|
+
runOnMainBlocking {
|
|
117
|
+
player.removeMediaItem(index)
|
|
118
|
+
if (index == currentIndex) {
|
|
119
|
+
stopInternal()
|
|
120
|
+
currentIndex = -1
|
|
121
|
+
emitTrackChanged()
|
|
122
|
+
} else if (index < currentIndex) {
|
|
123
|
+
currentIndex -= 1
|
|
124
|
+
emitTrackChanged()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
emitQueueUpdated()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
override fun clearQueue() {
|
|
131
|
+
queue.clear()
|
|
132
|
+
runOnMainBlocking {
|
|
133
|
+
player.clearMediaItems()
|
|
134
|
+
stopInternal()
|
|
135
|
+
currentIndex = -1
|
|
136
|
+
}
|
|
137
|
+
emitQueueUpdated()
|
|
138
|
+
emitTrackChanged()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
override fun play(index: Double?) {
|
|
142
|
+
runOnMainBlocking {
|
|
143
|
+
val targetIndex = index?.toInt()
|
|
144
|
+
playInternal(targetIndex)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
override fun pause() {
|
|
149
|
+
runOnMainBlocking {
|
|
150
|
+
player.pause()
|
|
151
|
+
setPlaybackState("paused")
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
override fun stop() {
|
|
156
|
+
runOnMainBlocking { stopInternal() }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
override fun skipToNext() {
|
|
160
|
+
runOnMainBlocking {
|
|
161
|
+
if (player.hasNextMediaItem()) {
|
|
162
|
+
player.seekToNext()
|
|
163
|
+
player.play()
|
|
164
|
+
setPlaybackState("playing")
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
override fun skipToPrevious() {
|
|
170
|
+
runOnMainBlocking {
|
|
171
|
+
if (player.hasPreviousMediaItem()) {
|
|
172
|
+
player.seekToPrevious()
|
|
173
|
+
player.play()
|
|
174
|
+
setPlaybackState("playing")
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
override fun skipToIndex(index: Double) {
|
|
180
|
+
runOnMainBlocking { playInternal(index.toInt()) }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
override fun seekTo(positionMs: Double) {
|
|
184
|
+
runOnMainBlocking { player.seekTo(positionMs.toLong()) }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
override fun reset() {
|
|
188
|
+
queue.clear()
|
|
189
|
+
runOnMainBlocking {
|
|
190
|
+
player.clearMediaItems()
|
|
191
|
+
stopInternal()
|
|
192
|
+
currentIndex = -1
|
|
193
|
+
}
|
|
194
|
+
emitQueueUpdated()
|
|
195
|
+
emitTrackChanged()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
override fun setRepeatMode(mode: String, startMs: Double?, endMs: Double?) {
|
|
199
|
+
runOnMainBlocking {
|
|
200
|
+
repeatMode = mode
|
|
201
|
+
loopStartMs = startMs?.toLong()
|
|
202
|
+
loopEndMs = endMs?.toLong()
|
|
203
|
+
applyRepeatMode()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
override fun getRepeatMode(): String {
|
|
208
|
+
return repeatMode
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
override fun getCurrentTrack(): WritableMap? {
|
|
212
|
+
val track = queue.getOrNull(currentIndex) ?: return null
|
|
213
|
+
return toWritableMap(track)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
override fun getCurrentTrackIndex(): Double {
|
|
217
|
+
return currentIndex.toDouble()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
override fun getPosition(): Double {
|
|
221
|
+
return runOnMainBlocking { player.currentPosition.toDouble() }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
override fun getDuration(): Double {
|
|
225
|
+
return runOnMainBlocking {
|
|
226
|
+
val duration = player.duration
|
|
227
|
+
if (duration >= 0) duration.toDouble() else 0.0
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
override fun getPlaybackState(): String {
|
|
232
|
+
return playbackState
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
override fun setVolume(volume: Double) {
|
|
236
|
+
runOnMainBlocking { player.volume = volume.toFloat() }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
override fun getVolume(): Double {
|
|
240
|
+
return runOnMainBlocking { player.volume.toDouble() }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
override fun setRate(rate: Double) {
|
|
244
|
+
runOnMainBlocking {
|
|
245
|
+
playbackRate = rate.toFloat()
|
|
246
|
+
player.playbackParameters = PlaybackParameters(playbackRate)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
override fun getRate(): Double {
|
|
251
|
+
return playbackRate.toDouble()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
override fun addListener(eventName: String) {
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
override fun removeListeners(count: Double) {
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private fun playInternal(index: Int?) {
|
|
261
|
+
if (queue.isEmpty()) return
|
|
262
|
+
if (index != null) {
|
|
263
|
+
if (index < 0 || index >= queue.size) return
|
|
264
|
+
player.seekTo(index, 0)
|
|
265
|
+
} else if (currentIndex < 0) {
|
|
266
|
+
player.seekTo(0, 0)
|
|
267
|
+
}
|
|
268
|
+
applyRepeatMode()
|
|
269
|
+
player.prepare()
|
|
270
|
+
player.play()
|
|
271
|
+
player.playbackParameters = PlaybackParameters(playbackRate)
|
|
272
|
+
setPlaybackState("playing")
|
|
273
|
+
ExpoNativeTrackPlayerService.start(reactApplicationContext, player)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private fun stopInternal() {
|
|
277
|
+
player.pause()
|
|
278
|
+
player.seekTo(0)
|
|
279
|
+
setPlaybackState("stopped")
|
|
280
|
+
ExpoNativeTrackPlayerService.stop(reactApplicationContext)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private fun handleTrackEnded() {
|
|
284
|
+
if (repeatMode == "track") {
|
|
285
|
+
player.seekTo(currentIndex, 0)
|
|
286
|
+
player.play()
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
if (repeatMode == "queue") {
|
|
290
|
+
if (currentIndex >= queue.size - 1) {
|
|
291
|
+
player.seekTo(0, 0)
|
|
292
|
+
player.play()
|
|
293
|
+
}
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
if (repeatMode == "loop_portion") {
|
|
297
|
+
player.seekTo(currentIndex, loopStartMs ?: 0L)
|
|
298
|
+
player.play()
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
if (currentIndex < queue.size - 1) {
|
|
302
|
+
player.seekTo(currentIndex + 1, 0)
|
|
303
|
+
player.play()
|
|
304
|
+
} else {
|
|
305
|
+
setPlaybackState("ended")
|
|
306
|
+
emitPlaybackQueueEnded()
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private fun applyRepeatMode() {
|
|
311
|
+
player.repeatMode = when (repeatMode) {
|
|
312
|
+
"track" -> Player.REPEAT_MODE_ONE
|
|
313
|
+
"queue" -> Player.REPEAT_MODE_ALL
|
|
314
|
+
else -> Player.REPEAT_MODE_OFF
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private fun emitEvent(name: String, payload: WritableMap?) {
|
|
319
|
+
reactApplicationContext.runOnJSQueueThread {
|
|
320
|
+
reactApplicationContext
|
|
321
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
322
|
+
.emit(name, payload)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private fun emitPlaybackState() {
|
|
327
|
+
val payload = Arguments.createMap().apply {
|
|
328
|
+
putString("state", playbackState)
|
|
329
|
+
}
|
|
330
|
+
emitEvent(eventPlaybackState, payload)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private fun emitQueueUpdated() {
|
|
334
|
+
val queueArray = Arguments.createArray().apply {
|
|
335
|
+
queue.forEach { pushMap(toWritableMap(it)) }
|
|
336
|
+
}
|
|
337
|
+
val payload = Arguments.createMap().apply {
|
|
338
|
+
putInt("count", queue.size)
|
|
339
|
+
putInt("trackIndex", currentIndex)
|
|
340
|
+
putArray("queue", queueArray)
|
|
341
|
+
}
|
|
342
|
+
emitEvent(eventQueueUpdated, payload)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private fun emitTrackChanged() {
|
|
346
|
+
val payload = Arguments.createMap().apply {
|
|
347
|
+
putInt("trackIndex", currentIndex)
|
|
348
|
+
val track = queue.getOrNull(currentIndex)
|
|
349
|
+
if (track != null) {
|
|
350
|
+
putMap("track", toWritableMap(track))
|
|
351
|
+
} else {
|
|
352
|
+
putNull("track")
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
emitEvent(eventTrackChanged, payload)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private fun emitPlaybackPosition() {
|
|
359
|
+
val duration = player.duration
|
|
360
|
+
val payload = Arguments.createMap().apply {
|
|
361
|
+
putDouble("position", player.currentPosition.toDouble())
|
|
362
|
+
putDouble("duration", if (duration > 0) duration.toDouble() else 0.0)
|
|
363
|
+
putInt("trackIndex", currentIndex)
|
|
364
|
+
}
|
|
365
|
+
emitEvent(eventPlaybackPosition, payload)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private fun emitPlaybackQueueEnded() {
|
|
369
|
+
val payload = Arguments.createMap().apply {
|
|
370
|
+
putInt("trackIndex", currentIndex)
|
|
371
|
+
putDouble("position", player.currentPosition.toDouble())
|
|
372
|
+
}
|
|
373
|
+
emitEvent(eventPlaybackQueueEnded, payload)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private fun startLoopWatcher() {
|
|
377
|
+
handler.post(object : Runnable {
|
|
378
|
+
override fun run() {
|
|
379
|
+
handleLoopPortionIfNeeded(player.currentPosition)
|
|
380
|
+
emitPlaybackPosition()
|
|
381
|
+
handler.postDelayed(this, 100)
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
@Suppress("UNCHECKED_CAST")
|
|
387
|
+
private fun <T> runOnMainBlocking(action: () -> T): T {
|
|
388
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
389
|
+
return action()
|
|
390
|
+
}
|
|
391
|
+
val latch = CountDownLatch(1)
|
|
392
|
+
val result = AtomicReference<T?>()
|
|
393
|
+
val error = AtomicReference<Throwable?>()
|
|
394
|
+
handler.post {
|
|
395
|
+
try {
|
|
396
|
+
result.set(action())
|
|
397
|
+
} catch (throwable: Throwable) {
|
|
398
|
+
error.set(throwable)
|
|
399
|
+
} finally {
|
|
400
|
+
latch.countDown()
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
latch.await()
|
|
404
|
+
error.get()?.let { throw RuntimeException(it) }
|
|
405
|
+
return result.get() as T
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private fun handleLoopPortionIfNeeded(positionMs: Long) {
|
|
409
|
+
if (repeatMode != "loop_portion") return
|
|
410
|
+
val startMs = loopStartMs ?: return
|
|
411
|
+
val endMs = loopEndMs ?: return
|
|
412
|
+
if (endMs <= startMs) return
|
|
413
|
+
if (positionMs >= endMs) {
|
|
414
|
+
player.seekTo(currentIndex, startMs)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private fun mediaItemFor(track: Map<String, Any?>): MediaItem {
|
|
419
|
+
val url = track["url"] as? String ?: ""
|
|
420
|
+
val id = track["id"] as? String ?: url
|
|
421
|
+
val title = track["title"] as? String
|
|
422
|
+
val artist = track["artist"] as? String
|
|
423
|
+
val albumName = track["albumName"] as? String
|
|
424
|
+
return MediaItem.Builder()
|
|
425
|
+
.setMediaId(id)
|
|
426
|
+
.setUri(url)
|
|
427
|
+
.setMediaMetadata(
|
|
428
|
+
MediaMetadata.Builder()
|
|
429
|
+
.setTitle(title)
|
|
430
|
+
.setArtist(artist)
|
|
431
|
+
.setAlbumTitle(albumName)
|
|
432
|
+
.build()
|
|
433
|
+
)
|
|
434
|
+
.build()
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private fun setPlaybackState(state: String) {
|
|
438
|
+
playbackState = state
|
|
439
|
+
emitPlaybackState()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
companion object {
|
|
443
|
+
const val NAME = NativeExpoNativeTrackPlayerSpec.NAME
|
|
444
|
+
private const val TAG = "ExpoNativeTrackPlayer"
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private fun toStoredMap(map: ReadableMap): Map<String, Any?> {
|
|
448
|
+
val result = mutableMapOf<String, Any?>()
|
|
449
|
+
val iterator = map.keySetIterator()
|
|
450
|
+
while (iterator.hasNextKey()) {
|
|
451
|
+
val key = iterator.nextKey()
|
|
452
|
+
when (map.getType(key)) {
|
|
453
|
+
ReadableType.Null -> result[key] = null
|
|
454
|
+
ReadableType.Boolean -> result[key] = map.getBoolean(key)
|
|
455
|
+
ReadableType.Number -> result[key] = map.getDouble(key)
|
|
456
|
+
ReadableType.String -> result[key] = map.getString(key)
|
|
457
|
+
ReadableType.Map -> {
|
|
458
|
+
val child = map.getMap(key)
|
|
459
|
+
result[key] = if (child == null) null else toStoredMap(child)
|
|
460
|
+
}
|
|
461
|
+
ReadableType.Array -> {
|
|
462
|
+
val child = map.getArray(key)
|
|
463
|
+
result[key] = if (child == null) null else toStoredArray(child)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return result
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private fun toStoredArray(array: ReadableArray): List<Any?> {
|
|
471
|
+
val result = mutableListOf<Any?>()
|
|
472
|
+
for (index in 0 until array.size()) {
|
|
473
|
+
when (array.getType(index)) {
|
|
474
|
+
ReadableType.Null -> result.add(null)
|
|
475
|
+
ReadableType.Boolean -> result.add(array.getBoolean(index))
|
|
476
|
+
ReadableType.Number -> result.add(array.getDouble(index))
|
|
477
|
+
ReadableType.String -> result.add(array.getString(index))
|
|
478
|
+
ReadableType.Map -> {
|
|
479
|
+
val child = array.getMap(index)
|
|
480
|
+
result.add(if (child == null) null else toStoredMap(child))
|
|
481
|
+
}
|
|
482
|
+
ReadableType.Array -> {
|
|
483
|
+
val child = array.getArray(index)
|
|
484
|
+
result.add(if (child == null) null else toStoredArray(child))
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return result
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private fun toWritableMap(map: Map<String, Any?>): WritableMap {
|
|
492
|
+
val result = Arguments.createMap()
|
|
493
|
+
map.forEach { (key, value) ->
|
|
494
|
+
when (value) {
|
|
495
|
+
null -> result.putNull(key)
|
|
496
|
+
is Boolean -> result.putBoolean(key, value)
|
|
497
|
+
is Number -> result.putDouble(key, value.toDouble())
|
|
498
|
+
is String -> result.putString(key, value)
|
|
499
|
+
is Map<*, *> -> {
|
|
500
|
+
@Suppress("UNCHECKED_CAST")
|
|
501
|
+
result.putMap(key, toWritableMap(value as Map<String, Any?>))
|
|
502
|
+
}
|
|
503
|
+
is List<*> -> result.putArray(key, toWritableArray(value))
|
|
504
|
+
else -> result.putNull(key)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return result
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private fun toWritableArray(array: List<*>): WritableArray {
|
|
511
|
+
val result = Arguments.createArray()
|
|
512
|
+
array.forEach { value ->
|
|
513
|
+
when (value) {
|
|
514
|
+
null -> result.pushNull()
|
|
515
|
+
is Boolean -> result.pushBoolean(value)
|
|
516
|
+
is Number -> result.pushDouble(value.toDouble())
|
|
517
|
+
is String -> result.pushString(value)
|
|
518
|
+
is Map<*, *> -> {
|
|
519
|
+
@Suppress("UNCHECKED_CAST")
|
|
520
|
+
result.pushMap(toWritableMap(value as Map<String, Any?>))
|
|
521
|
+
}
|
|
522
|
+
is List<*> -> result.pushArray(toWritableArray(value))
|
|
523
|
+
else -> result.pushNull()
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return result
|
|
527
|
+
}
|
|
528
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package com.exponativetrackplayer
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class ExpoNativeTrackPlayerPackage : BaseReactPackage() {
|
|
11
|
+
private var module: ExpoNativeTrackPlayerModule? = null
|
|
12
|
+
|
|
13
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
14
|
+
if (name != ExpoNativeTrackPlayerModule.NAME) return null
|
|
15
|
+
return module ?: ExpoNativeTrackPlayerModule(reactContext).also {
|
|
16
|
+
module = it
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
21
|
+
return ReactModuleInfoProvider {
|
|
22
|
+
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
|
|
23
|
+
moduleInfos[ExpoNativeTrackPlayerModule.NAME] = ReactModuleInfo(
|
|
24
|
+
ExpoNativeTrackPlayerModule.NAME,
|
|
25
|
+
ExpoNativeTrackPlayerModule.NAME,
|
|
26
|
+
false, // canOverrideExistingModule
|
|
27
|
+
false, // needsEagerInit
|
|
28
|
+
false, // isCxxModule
|
|
29
|
+
true // isTurboModule
|
|
30
|
+
)
|
|
31
|
+
moduleInfos
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
package com.exponativetrackplayer
|
|
2
|
+
|
|
3
|
+
import android.app.NotificationChannel
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import androidx.core.content.ContextCompat
|
|
8
|
+
import androidx.media3.common.Player
|
|
9
|
+
import androidx.media3.session.MediaSession
|
|
10
|
+
import androidx.media3.session.MediaSessionService
|
|
11
|
+
import androidx.media3.ui.PlayerNotificationManager
|
|
12
|
+
|
|
13
|
+
class ExpoNativeTrackPlayerService : MediaSessionService() {
|
|
14
|
+
override fun onCreate() {
|
|
15
|
+
super.onCreate()
|
|
16
|
+
serviceInstance = this
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun onDestroy() {
|
|
20
|
+
serviceInstance = null
|
|
21
|
+
super.onDestroy()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
|
25
|
+
return mediaSession
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
29
|
+
super.onTaskRemoved(rootIntent)
|
|
30
|
+
stopSelf()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
companion object {
|
|
34
|
+
private const val CHANNEL_ID = "expo_native_track_player"
|
|
35
|
+
private const val CHANNEL_NAME = "Playback"
|
|
36
|
+
private const val NOTIFICATION_ID = 4125
|
|
37
|
+
private const val SERVICE_ACTION = "com.exponativetrackplayer.PLAYBACK_SERVICE"
|
|
38
|
+
|
|
39
|
+
@Volatile
|
|
40
|
+
private var mediaSession: MediaSession? = null
|
|
41
|
+
|
|
42
|
+
@Volatile
|
|
43
|
+
private var notificationManager: PlayerNotificationManager? = null
|
|
44
|
+
|
|
45
|
+
@Volatile
|
|
46
|
+
private var serviceInstance: ExpoNativeTrackPlayerService? = null
|
|
47
|
+
|
|
48
|
+
fun start(context: Context, player: Player) {
|
|
49
|
+
ensureSession(context, player)
|
|
50
|
+
val intent = Intent(context, ExpoNativeTrackPlayerService::class.java).apply {
|
|
51
|
+
action = SERVICE_ACTION
|
|
52
|
+
}
|
|
53
|
+
ContextCompat.startForegroundService(context, intent)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fun stop(context: Context) {
|
|
57
|
+
notificationManager?.setPlayer(null)
|
|
58
|
+
notificationManager = null
|
|
59
|
+
mediaSession?.release()
|
|
60
|
+
mediaSession = null
|
|
61
|
+
context.stopService(Intent(context, ExpoNativeTrackPlayerService::class.java))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private fun ensureSession(context: Context, player: Player) {
|
|
65
|
+
if (mediaSession == null) {
|
|
66
|
+
mediaSession = MediaSession.Builder(context, player).build()
|
|
67
|
+
} else {
|
|
68
|
+
mediaSession?.player?.apply {
|
|
69
|
+
if (this != player) {
|
|
70
|
+
mediaSession?.release()
|
|
71
|
+
mediaSession = MediaSession.Builder(context, player).build()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (notificationManager == null) {
|
|
76
|
+
createNotificationChannel(context)
|
|
77
|
+
notificationManager = PlayerNotificationManager.Builder(
|
|
78
|
+
context,
|
|
79
|
+
NOTIFICATION_ID,
|
|
80
|
+
CHANNEL_ID
|
|
81
|
+
)
|
|
82
|
+
.setNotificationListener(object : PlayerNotificationManager.NotificationListener {
|
|
83
|
+
override fun onNotificationPosted(
|
|
84
|
+
notificationId: Int,
|
|
85
|
+
notification: android.app.Notification,
|
|
86
|
+
ongoing: Boolean
|
|
87
|
+
) {
|
|
88
|
+
if (ongoing) {
|
|
89
|
+
serviceInstance?.startForeground(notificationId, notification)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override fun onNotificationCancelled(
|
|
94
|
+
notificationId: Int,
|
|
95
|
+
dismissedByUser: Boolean
|
|
96
|
+
) {
|
|
97
|
+
serviceInstance?.stopForeground(STOP_FOREGROUND_REMOVE)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.build()
|
|
101
|
+
.apply { setPlayer(player) }
|
|
102
|
+
} else {
|
|
103
|
+
notificationManager?.setPlayer(player)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fun createNotificationChannel(context: Context) {
|
|
108
|
+
val notificationManager =
|
|
109
|
+
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
110
|
+
val channel = NotificationChannel(
|
|
111
|
+
CHANNEL_ID,
|
|
112
|
+
CHANNEL_NAME,
|
|
113
|
+
NotificationManager.IMPORTANCE_LOW
|
|
114
|
+
)
|
|
115
|
+
notificationManager.createNotificationChannel(channel)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|