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.
Files changed (53) hide show
  1. package/ExpoNativeTrackPlayer.podspec +21 -0
  2. package/LICENSE +20 -0
  3. package/README.md +267 -0
  4. package/android/build.gradle +71 -0
  5. package/android/src/main/AndroidManifest.xml +12 -0
  6. package/android/src/main/java/com/exponativetrackplayer/ExpoNativeTrackPlayerModule.kt +528 -0
  7. package/android/src/main/java/com/exponativetrackplayer/ExpoNativeTrackPlayerPackage.kt +34 -0
  8. package/android/src/main/java/com/exponativetrackplayer/ExpoNativeTrackPlayerService.kt +118 -0
  9. package/ios/ExpoNativeTrackPlayer.h +6 -0
  10. package/ios/ExpoNativeTrackPlayer.mm +475 -0
  11. package/lib/module/NativeExpoNativeTrackPlayer.js +5 -0
  12. package/lib/module/NativeExpoNativeTrackPlayer.js.map +1 -0
  13. package/lib/module/constants.js +31 -0
  14. package/lib/module/constants.js.map +1 -0
  15. package/lib/module/events.js +56 -0
  16. package/lib/module/events.js.map +1 -0
  17. package/lib/module/hooks.js +105 -0
  18. package/lib/module/hooks.js.map +1 -0
  19. package/lib/module/index.js +150 -0
  20. package/lib/module/index.js.map +1 -0
  21. package/lib/module/package.json +1 -0
  22. package/lib/module/types/ResourceObject.js +2 -0
  23. package/lib/module/types/ResourceObject.js.map +1 -0
  24. package/lib/module/types/Track.js +4 -0
  25. package/lib/module/types/Track.js.map +1 -0
  26. package/lib/module/types/TrackMetadataBase.js +2 -0
  27. package/lib/module/types/TrackMetadataBase.js.map +1 -0
  28. package/lib/typescript/package.json +1 -0
  29. package/lib/typescript/src/NativeExpoNativeTrackPlayer.d.ts +57 -0
  30. package/lib/typescript/src/NativeExpoNativeTrackPlayer.d.ts.map +1 -0
  31. package/lib/typescript/src/constants.d.ts +23 -0
  32. package/lib/typescript/src/constants.d.ts.map +1 -0
  33. package/lib/typescript/src/events.d.ts +163 -0
  34. package/lib/typescript/src/events.d.ts.map +1 -0
  35. package/lib/typescript/src/hooks.d.ts +12 -0
  36. package/lib/typescript/src/hooks.d.ts.map +1 -0
  37. package/lib/typescript/src/index.d.ts +65 -0
  38. package/lib/typescript/src/index.d.ts.map +1 -0
  39. package/lib/typescript/src/types/ResourceObject.d.ts +5 -0
  40. package/lib/typescript/src/types/ResourceObject.d.ts.map +1 -0
  41. package/lib/typescript/src/types/Track.d.ts +22 -0
  42. package/lib/typescript/src/types/Track.d.ts.map +1 -0
  43. package/lib/typescript/src/types/TrackMetadataBase.d.ts +18 -0
  44. package/lib/typescript/src/types/TrackMetadataBase.d.ts.map +1 -0
  45. package/package.json +172 -0
  46. package/src/NativeExpoNativeTrackPlayer.ts +70 -0
  47. package/src/constants.ts +40 -0
  48. package/src/events.ts +136 -0
  49. package/src/hooks.ts +149 -0
  50. package/src/index.tsx +250 -0
  51. package/src/types/ResourceObject.ts +4 -0
  52. package/src/types/Track.ts +23 -0
  53. 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
+ }
@@ -0,0 +1,6 @@
1
+ #import <ExpoNativeTrackPlayerSpec/ExpoNativeTrackPlayerSpec.h>
2
+ #import <React/RCTBridgeModule.h>
3
+
4
+ @interface ExpoNativeTrackPlayer : NSObject <NativeExpoNativeTrackPlayerSpec, RCTBridgeModule>
5
+
6
+ @end