react-native-mp3 0.1.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 (64) hide show
  1. package/android/build.gradle +111 -0
  2. package/android/src/main/AndroidManifest.xml +44 -0
  3. package/android/src/main/java/com/reactnativemp3/Mp3Package.kt +23 -0
  4. package/android/src/main/java/com/reactnativemp3/Mp3TurboModule.kt +43 -0
  5. package/android/src/main/java/com/reactnativemp3/database/MusicDatabase.kt +48 -0
  6. package/android/src/main/java/com/reactnativemp3/database/dao/SongDao.kt +72 -0
  7. package/android/src/main/java/com/reactnativemp3/database/entities/PlaylistEntity.kt +58 -0
  8. package/android/src/main/java/com/reactnativemp3/database/entities/SongEntity.kt +104 -0
  9. package/android/src/main/java/com/reactnativemp3/database/entities/ThumbnailCacheEntity.kt +43 -0
  10. package/android/src/main/java/com/reactnativemp3/managers/CacheManager.kt +0 -0
  11. package/android/src/main/java/com/reactnativemp3/managers/EqualizerManager.kt +0 -0
  12. package/android/src/main/java/com/reactnativemp3/modules/MetadataModule.kt +330 -0
  13. package/android/src/main/java/com/reactnativemp3/modules/NotificationModule.kt +236 -0
  14. package/android/src/main/java/com/reactnativemp3/modules/PlayerModule.kt +710 -0
  15. package/android/src/main/java/com/reactnativemp3/modules/ScannerModule.kt +640 -0
  16. package/android/src/main/java/com/reactnativemp3/services/AudioFocusService.kt +0 -0
  17. package/android/src/main/java/com/reactnativemp3/services/FileObserverService.kt +0 -0
  18. package/android/src/main/java/com/reactnativemp3/services/MusicService.kt +309 -0
  19. package/android/src/main/java/com/reactnativemp3/utils/MediaStoreUtils.kt +0 -0
  20. package/android/src/main/java/com/reactnativemp3/utils/PermissionUtils.kt +0 -0
  21. package/android/src/main/jni/Mp3TurboModule.cpp +29 -0
  22. package/android/src/main/res/drawable/ic_music_note.xml +11 -0
  23. package/android/src/main/res/drawable/ic_pause.xml +11 -0
  24. package/android/src/main/res/drawable/ic_play.xml +11 -0
  25. package/android/src/main/res/drawable/ic_skip_next.xml +11 -0
  26. package/android/src/main/res/drawable/ic_skip_previous.xml +11 -0
  27. package/android/src/main/res/drawable/ic_stop.xml +11 -0
  28. package/lib/components/MusicList.d.ts +0 -0
  29. package/lib/components/MusicList.js +1 -0
  30. package/lib/components/MusicPlayerUI.d.ts +0 -0
  31. package/lib/components/MusicPlayerUI.js +1 -0
  32. package/lib/hooks/useMusicPlayer.d.ts +38 -0
  33. package/lib/hooks/useMusicPlayer.js +242 -0
  34. package/lib/hooks/useMusicScanner.d.ts +27 -0
  35. package/lib/hooks/useMusicScanner.js +217 -0
  36. package/lib/hooks/usePermissions.d.ts +9 -0
  37. package/lib/hooks/usePermissions.js +55 -0
  38. package/lib/index.d.ts +144 -0
  39. package/lib/index.js +148 -0
  40. package/lib/types/common.types.d.ts +79 -0
  41. package/lib/types/common.types.js +2 -0
  42. package/lib/types/index.d.ts +3 -0
  43. package/lib/types/index.js +2 -0
  44. package/lib/types/player.types.d.ts +35 -0
  45. package/lib/types/player.types.js +2 -0
  46. package/lib/types/scanner.types.d.ts +29 -0
  47. package/lib/types/scanner.types.js +2 -0
  48. package/lib/utils/constants.d.ts +31 -0
  49. package/lib/utils/constants.js +55 -0
  50. package/lib/utils/events.d.ts +0 -0
  51. package/lib/utils/events.js +1 -0
  52. package/package.json +62 -0
  53. package/src/components/MusicList.tsx +0 -0
  54. package/src/components/MusicPlayerUI.tsx +0 -0
  55. package/src/hooks/useMusicPlayer.ts +358 -0
  56. package/src/hooks/useMusicScanner.ts +286 -0
  57. package/src/hooks/usePermissions.ts +64 -0
  58. package/src/index.ts +214 -0
  59. package/src/types/common.types.ts +86 -0
  60. package/src/types/index.ts +4 -0
  61. package/src/types/player.types.ts +37 -0
  62. package/src/types/scanner.types.ts +31 -0
  63. package/src/utils/constants.ts +56 -0
  64. package/src/utils/events.ts +0 -0
@@ -0,0 +1,710 @@
1
+ package com.reactnativemp3.modules
2
+
3
+ import android.content.Context
4
+ import android.media.AudioAttributes
5
+ import android.media.AudioFocusRequest
6
+ import android.media.AudioManager
7
+ import android.media.MediaPlayer
8
+ import android.media.audiofx.Equalizer
9
+ import android.net.Uri
10
+ import android.os.Build
11
+ import android.os.Handler
12
+ import android.os.Looper
13
+ import androidx.annotation.RequiresApi
14
+ import com.facebook.react.bridge.*
15
+ import com.facebook.react.modules.core.DeviceEventManagerModule
16
+ import com.reactnativemp3.Mp3TurboModule
17
+ import com.reactnativemp3.database.MusicDatabase
18
+ import com.reactnativemp3.database.entities.SongEntity
19
+ import com.reactnativemp3.services.MusicService
20
+ import kotlinx.coroutines.CoroutineScope
21
+ import kotlinx.coroutines.Dispatchers
22
+ import kotlinx.coroutines.SupervisorJob
23
+ import kotlinx.coroutines.launch
24
+ import java.util.*
25
+
26
+ @ReactModule(name = PlayerModule.NAME)
27
+ class PlayerModule(reactContext: ReactApplicationContext) :
28
+ Mp3TurboModule(reactContext, NAME) {
29
+
30
+ companion object {
31
+ const val NAME = "PlayerModule"
32
+
33
+ private const val EVENT_PLAYBACK_STATE_CHANGED = "PLAYBACK_STATE_CHANGED"
34
+ private const val EVENT_PLAYBACK_QUEUE_CHANGED = "PLAYBACK_QUEUE_CHANGED"
35
+ private const val EVENT_PLAYBACK_ERROR = "PLAYBACK_ERROR"
36
+ private const val EVENT_PLAYBACK_COMPLETE = "PLAYBACK_COMPLETE"
37
+ private const val EVENT_PLAYBACK_NEXT = "PLAYBACK_NEXT"
38
+ private const val EVENT_PLAYBACK_PREVIOUS = "PLAYBACK_PREVIOUS"
39
+ private const val EVENT_EQUALIZER_CHANGED = "EQUALIZER_CHANGED"
40
+
41
+ private const val UPDATE_INTERVAL_MS = 250L
42
+ }
43
+
44
+ private lateinit var database: MusicDatabase
45
+ private lateinit var audioManager: AudioManager
46
+ private var mediaPlayer: MediaPlayer? = null
47
+ private var equalizer: Equalizer? = null
48
+
49
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
50
+ private val handler = Handler(Looper.getMainLooper())
51
+ private val updateRunnable = Runnable { updatePlaybackState() }
52
+
53
+ private var currentSong: SongEntity? = null
54
+ private var queue: MutableList<SongEntity> = mutableListOf()
55
+ private var queuePosition: Int = -1
56
+
57
+ private var isPlaying: Boolean = false
58
+ private var repeatMode: String = "none" // none, one, all
59
+ private var shuffleMode: Boolean = false
60
+ private var volume: Float = 1.0f
61
+ private var playbackRate: Float = 1.0f
62
+
63
+ private var audioFocusRequest: Any? = null // AudioFocusRequest for API 26+
64
+ private var audioFocusGranted: Boolean = false
65
+
66
+ override fun initialize() {
67
+ super.initialize()
68
+ database = MusicDatabase.getInstance(reactApplicationContext)
69
+ audioManager = reactApplicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
70
+ }
71
+
72
+ @ReactMethod
73
+ fun play(uri: String, promise: Promise) {
74
+ scope.launch {
75
+ try {
76
+ val context = reactApplicationContext ?: throw Exception("Context not available")
77
+
78
+ // Find song in database
79
+ val song = database.songDao().getAllSongs().first()
80
+ .find { it.uri == uri || it.path == uri }
81
+
82
+ if (song == null) {
83
+ promise.reject("SONG_NOT_FOUND", "Song not found in database")
84
+ return@launch
85
+ }
86
+
87
+ currentSong = song
88
+ queuePosition = queue.indexOfFirst { it.id == song.id }
89
+ if (queuePosition == -1) {
90
+ queue = mutableListOf(song)
91
+ queuePosition = 0
92
+ emitQueueChanged()
93
+ }
94
+
95
+ prepareAndPlay(song, promise)
96
+ } catch (e: Exception) {
97
+ promise.reject("PLAY_ERROR", "Failed to play song", e)
98
+ }
99
+ }
100
+ }
101
+
102
+ @ReactMethod
103
+ fun playFromQueue(index: Int) {
104
+ if (index < 0 || index >= queue.size) return
105
+
106
+ scope.launch {
107
+ try {
108
+ val song = queue[index]
109
+ currentSong = song
110
+ queuePosition = index
111
+ prepareAndPlay(song, null)
112
+ } catch (e: Exception) {
113
+ emitPlaybackError("Failed to play from queue: ${e.message}")
114
+ }
115
+ }
116
+ }
117
+
118
+ @ReactMethod
119
+ fun pause() {
120
+ mediaPlayer?.pause()
121
+ isPlaying = false
122
+ stopProgressUpdates()
123
+ emitPlaybackStateChanged()
124
+ }
125
+
126
+ @ReactMethod
127
+ fun stop() {
128
+ mediaPlayer?.stop()
129
+ mediaPlayer?.release()
130
+ mediaPlayer = null
131
+ equalizer?.release()
132
+ equalizer = null
133
+
134
+ isPlaying = false
135
+ stopProgressUpdates()
136
+ releaseAudioFocus()
137
+ emitPlaybackStateChanged()
138
+ }
139
+
140
+ @ReactMethod
141
+ fun seekTo(position: Double) {
142
+ mediaPlayer?.seekTo(position.toInt())
143
+ emitPlaybackStateChanged()
144
+ }
145
+
146
+ @ReactMethod
147
+ fun skipToNext() {
148
+ if (queue.isEmpty() || queuePosition == -1) return
149
+
150
+ var nextPosition = queuePosition + 1
151
+
152
+ when {
153
+ shuffleMode -> {
154
+ nextPosition = Random().nextInt(queue.size)
155
+ }
156
+ repeatMode == "one" -> {
157
+ // Play same song again
158
+ currentSong?.let { prepareAndPlay(it, null) }
159
+ return
160
+ }
161
+ nextPosition >= queue.size -> {
162
+ if (repeatMode == "all") {
163
+ nextPosition = 0
164
+ } else {
165
+ stop()
166
+ return
167
+ }
168
+ }
169
+ }
170
+
171
+ playFromQueue(nextPosition)
172
+ emitEvent(EVENT_PLAYBACK_NEXT, null)
173
+ }
174
+
175
+ @ReactMethod
176
+ fun skipToPrevious() {
177
+ if (queue.isEmpty() || queuePosition == -1) return
178
+
179
+ val currentTime = mediaPlayer?.currentPosition ?: 0
180
+ if (currentTime > 3000) {
181
+ // If more than 3 seconds into the song, restart it
182
+ seekTo(0.0)
183
+ return
184
+ }
185
+
186
+ var prevPosition = queuePosition - 1
187
+
188
+ when {
189
+ shuffleMode -> {
190
+ prevPosition = Random().nextInt(queue.size)
191
+ }
192
+ prevPosition < 0 -> {
193
+ if (repeatMode == "all") {
194
+ prevPosition = queue.size - 1
195
+ } else {
196
+ // Already at first song, just restart it
197
+ seekTo(0.0)
198
+ return
199
+ }
200
+ }
201
+ }
202
+
203
+ playFromQueue(prevPosition)
204
+ emitEvent(EVENT_PLAYBACK_PREVIOUS, null)
205
+ }
206
+
207
+ @ReactMethod
208
+ fun setVolume(volume: Double) {
209
+ this.volume = volume.toFloat()
210
+ mediaPlayer?.setVolume(volume.toFloat(), volume.toFloat())
211
+ }
212
+
213
+ @ReactMethod
214
+ fun setPlaybackRate(rate: Double) {
215
+ this.playbackRate = rate.toFloat()
216
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
217
+ mediaPlayer?.playbackParams = mediaPlayer?.playbackParams?.setSpeed(rate.toFloat())
218
+ }
219
+ }
220
+
221
+ @ReactMethod
222
+ fun setQueue(songs: ReadableArray) {
223
+ scope.launch {
224
+ try {
225
+ queue.clear()
226
+ for (i in 0 until songs.size()) {
227
+ val songMap = songs.getMap(i)
228
+ val songId = songMap?.getString("id") ?: continue
229
+
230
+ val song = database.songDao().getById(songId)
231
+ if (song != null) {
232
+ queue.add(song)
233
+ }
234
+ }
235
+
236
+ queuePosition = -1
237
+ currentSong = null
238
+ emitQueueChanged()
239
+ } catch (e: Exception) {
240
+ emitPlaybackError("Failed to set queue: ${e.message}")
241
+ }
242
+ }
243
+ }
244
+
245
+ @ReactMethod
246
+ fun addToQueue(songs: ReadableArray) {
247
+ scope.launch {
248
+ try {
249
+ for (i in 0 until songs.size()) {
250
+ val songMap = songs.getMap(i)
251
+ val songId = songMap?.getString("id") ?: continue
252
+
253
+ val song = database.songDao().getById(songId)
254
+ if (song != null) {
255
+ queue.add(song)
256
+ }
257
+ }
258
+
259
+ emitQueueChanged()
260
+ } catch (e: Exception) {
261
+ emitPlaybackError("Failed to add to queue: ${e.message}")
262
+ }
263
+ }
264
+ }
265
+
266
+ @ReactMethod
267
+ fun clearQueue() {
268
+ queue.clear()
269
+ queuePosition = -1
270
+ currentSong = null
271
+ emitQueueChanged()
272
+ }
273
+
274
+ @ReactMethod
275
+ fun getQueue(promise: Promise) {
276
+ val result = Arguments.createArray()
277
+ queue.forEach { song ->
278
+ result.pushMap(convertSongToMap(song))
279
+ }
280
+ promise.resolve(result)
281
+ }
282
+
283
+ @ReactMethod
284
+ fun getCurrentTrack(promise: Promise) {
285
+ currentSong?.let {
286
+ promise.resolve(convertSongToMap(it))
287
+ } ?: promise.resolve(null)
288
+ }
289
+
290
+ @ReactMethod
291
+ fun getPlaybackState(promise: Promise) {
292
+ val state = Arguments.createMap()
293
+ state.putBoolean("isPlaying", isPlaying)
294
+ state.putDouble("currentTime", (mediaPlayer?.currentPosition ?: 0).toDouble())
295
+ state.putDouble("duration", (mediaPlayer?.duration ?: 0).toDouble())
296
+ state.putDouble("volume", volume.toDouble())
297
+ state.putDouble("playbackRate", playbackRate.toDouble())
298
+ state.putString("repeatMode", repeatMode)
299
+ state.putBoolean("shuffleMode", shuffleMode)
300
+
301
+ currentSong?.let {
302
+ state.putMap("currentSong", convertSongToMap(it))
303
+ }
304
+
305
+ val queueArray = Arguments.createArray()
306
+ queue.forEach { song ->
307
+ queueArray.pushMap(convertSongToMap(song))
308
+ }
309
+ state.putArray("queue", queueArray)
310
+ state.putInt("queuePosition", queuePosition)
311
+
312
+ promise.resolve(state)
313
+ }
314
+
315
+ @ReactMethod
316
+ fun getCurrentTime(promise: Promise) {
317
+ promise.resolve((mediaPlayer?.currentPosition ?: 0).toDouble())
318
+ }
319
+
320
+ @ReactMethod
321
+ fun getDuration(promise: Promise) {
322
+ promise.resolve((mediaPlayer?.duration ?: 0).toDouble())
323
+ }
324
+
325
+ @ReactMethod
326
+ fun isPlaying(promise: Promise) {
327
+ promise.resolve(isPlaying)
328
+ }
329
+
330
+ @ReactMethod
331
+ fun setRepeatMode(mode: String) {
332
+ repeatMode = when (mode) {
333
+ "none", "one", "all" -> mode
334
+ else -> "none"
335
+ }
336
+ emitPlaybackStateChanged()
337
+ }
338
+
339
+ @ReactMethod
340
+ fun setShuffleMode(enabled: Boolean) {
341
+ shuffleMode = enabled
342
+ emitPlaybackStateChanged()
343
+ }
344
+
345
+ @ReactMethod
346
+ fun setEqualizerPreset(preset: String) {
347
+ equalizer?.let { eq ->
348
+ val presetId = when (preset.toLowerCase(Locale.US)) {
349
+ "normal" -> 0
350
+ "rock" -> 1
351
+ "pop" -> 2
352
+ "jazz" -> 3
353
+ "classic" -> 4
354
+ "dance" -> 5
355
+ else -> -1
356
+ }
357
+
358
+ if (presetId >= 0) {
359
+ try {
360
+ eq.usePreset(presetId.toShort())
361
+ emitEqualizerChanged(preset, null)
362
+ } catch (e: Exception) {
363
+ // Preset not available
364
+ }
365
+ }
366
+ }
367
+ }
368
+
369
+ @ReactMethod
370
+ fun setEqualizerBands(bands: ReadableArray) {
371
+ equalizer?.let { eq ->
372
+ for (i in 0 until bands.size()) {
373
+ val bandValue = bands.getInt(i)
374
+ try {
375
+ eq.setBandLevel(i.toShort(), bandValue.toShort())
376
+ } catch (e: Exception) {
377
+ // Band index out of range
378
+ }
379
+ }
380
+ emitEqualizerChanged(null, bands)
381
+ }
382
+ }
383
+
384
+ @ReactMethod
385
+ fun getEqualizerPresets(promise: Promise) {
386
+ val presets = Arguments.createArray()
387
+
388
+ equalizer?.let { eq ->
389
+ val numberOfPresets = eq.numberOfPresets
390
+ for (i in 0 until numberOfPresets) {
391
+ val presetName = eq.getPresetName(i.toShort())
392
+ presets.pushString(presetName)
393
+ }
394
+ }
395
+
396
+ promise.resolve(presets)
397
+ }
398
+
399
+ @ReactMethod
400
+ fun enableEqualizer(enabled: Boolean) {
401
+ if (enabled) {
402
+ if (equalizer == null && mediaPlayer != null) {
403
+ try {
404
+ equalizer = Equalizer(0, mediaPlayer!!.audioSessionId)
405
+ equalizer?.enabled = true
406
+ } catch (e: Exception) {
407
+ emitPlaybackError("Failed to enable equalizer: ${e.message}")
408
+ }
409
+ }
410
+ } else {
411
+ equalizer?.enabled = false
412
+ }
413
+
414
+ val params = Arguments.createMap()
415
+ params.putBoolean("enabled", enabled)
416
+ emitEvent(EVENT_EQUALIZER_CHANGED, params)
417
+ }
418
+
419
+ @ReactMethod
420
+ fun setSleepTimer(minutes: Int) {
421
+ // Implement sleep timer logic
422
+ handler.postDelayed({
423
+ pause()
424
+ }, (minutes * 60 * 1000).toLong())
425
+ }
426
+
427
+ @ReactMethod
428
+ fun cancelSleepTimer() {
429
+ handler.removeCallbacksAndMessages(null)
430
+ }
431
+
432
+ @ReactMethod
433
+ fun setupBackgroundPlayback(config: ReadableMap) {
434
+ val context = reactApplicationContext ?: return
435
+
436
+ // Start foreground service for background playback
437
+ val serviceIntent = MusicService.createIntent(context)
438
+
439
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
440
+ context.startForegroundService(serviceIntent)
441
+ } else {
442
+ context.startService(serviceIntent)
443
+ }
444
+ }
445
+
446
+ @ReactMethod
447
+ fun stopBackgroundPlayback() {
448
+ val context = reactApplicationContext ?: return
449
+ val serviceIntent = MusicService.createIntent(context)
450
+ context.stopService(serviceIntent)
451
+ }
452
+
453
+ override fun onCatalystInstanceDestroy() {
454
+ super.onCatalystInstanceDestroy()
455
+ stop()
456
+ handler.removeCallbacksAndMessages(null)
457
+ scope.cancel()
458
+ }
459
+
460
+ // Private helper methods
461
+
462
+ private fun prepareAndPlay(song: SongEntity, promise: Promise?) {
463
+ try {
464
+ // Release previous player
465
+ mediaPlayer?.release()
466
+ equalizer?.release()
467
+
468
+ // Create new media player
469
+ val player = MediaPlayer().apply {
470
+ setAudioAttributes(
471
+ AudioAttributes.Builder()
472
+ .setUsage(AudioAttributes.USAGE_MEDIA)
473
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
474
+ .build()
475
+ )
476
+
477
+ setDataSource(reactApplicationContext, Uri.parse(song.uri))
478
+ setVolume(volume, volume)
479
+
480
+ setOnPreparedListener {
481
+ requestAudioFocus()
482
+ it.start()
483
+ isPlaying = true
484
+ startProgressUpdates()
485
+
486
+ // Update play count in database
487
+ scope.launch {
488
+ database.songDao().incrementPlayCount(
489
+ song.id,
490
+ System.currentTimeMillis()
491
+ )
492
+ }
493
+
494
+ promise?.resolve(null)
495
+ emitPlaybackStateChanged()
496
+ }
497
+
498
+ setOnCompletionListener {
499
+ onPlaybackComplete()
500
+ }
501
+
502
+ setOnErrorListener { _, what, extra ->
503
+ emitPlaybackError("MediaPlayer error: what=$what, extra=$extra")
504
+ false
505
+ }
506
+
507
+ prepareAsync()
508
+ }
509
+
510
+ mediaPlayer = player
511
+
512
+ // Initialize equalizer
513
+ try {
514
+ equalizer = Equalizer(0, player.audioSessionId)
515
+ equalizer?.enabled = true
516
+ } catch (e: Exception) {
517
+ // Equalizer not supported on this device
518
+ }
519
+
520
+ } catch (e: Exception) {
521
+ promise?.reject("PLAYBACK_ERROR", "Failed to prepare media player", e)
522
+ emitPlaybackError("Failed to prepare media player: ${e.message}")
523
+ }
524
+ }
525
+
526
+ private fun requestAudioFocus() {
527
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
528
+ requestAudioFocusV26()
529
+ } else {
530
+ requestAudioFocusLegacy()
531
+ }
532
+ }
533
+
534
+ @Suppress("DEPRECATION")
535
+ private fun requestAudioFocusLegacy() {
536
+ val result = audioManager.requestAudioFocus(
537
+ { focusChange ->
538
+ when (focusChange) {
539
+ AudioManager.AUDIOFOCUS_LOSS -> {
540
+ // Lost focus for an indefinite period
541
+ pause()
542
+ releaseAudioFocus()
543
+ }
544
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
545
+ // Lost focus for a short time
546
+ pause()
547
+ }
548
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
549
+ // Lost focus but can play at lower volume
550
+ mediaPlayer?.setVolume(volume * 0.2f, volume * 0.2f)
551
+ }
552
+ AudioManager.AUDIOFOCUS_GAIN -> {
553
+ // Gained focus
554
+ mediaPlayer?.setVolume(volume, volume)
555
+ if (isPlaying) {
556
+ mediaPlayer?.start()
557
+ }
558
+ }
559
+ }
560
+ },
561
+ AudioManager.STREAM_MUSIC,
562
+ AudioManager.AUDIOFOCUS_GAIN
563
+ )
564
+
565
+ audioFocusGranted = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
566
+ }
567
+
568
+ @RequiresApi(Build.VERSION_CODES.O)
569
+ private fun requestAudioFocusV26() {
570
+ val audioAttributes = AudioAttributes.Builder()
571
+ .setUsage(AudioAttributes.USAGE_MEDIA)
572
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
573
+ .build()
574
+
575
+ val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
576
+ .setAudioAttributes(audioAttributes)
577
+ .setAcceptsDelayedFocusGain(true)
578
+ .setOnAudioFocusChangeListener { focusChange ->
579
+ when (focusChange) {
580
+ AudioManager.AUDIOFOCUS_LOSS -> {
581
+ pause()
582
+ releaseAudioFocus()
583
+ }
584
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
585
+ pause()
586
+ }
587
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
588
+ mediaPlayer?.setVolume(volume * 0.2f, volume * 0.2f)
589
+ }
590
+ AudioManager.AUDIOFOCUS_GAIN -> {
591
+ mediaPlayer?.setVolume(volume, volume)
592
+ if (isPlaying) {
593
+ mediaPlayer?.start()
594
+ }
595
+ }
596
+ }
597
+ }
598
+ .build()
599
+
600
+ val result = audioManager.requestAudioFocus(focusRequest)
601
+ audioFocusGranted = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
602
+ audioFocusRequest = focusRequest
603
+ }
604
+
605
+ private fun releaseAudioFocus() {
606
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
607
+ (audioFocusRequest as? AudioFocusRequest)?.let {
608
+ audioManager.abandonAudioFocusRequest(it)
609
+ }
610
+ } else {
611
+ audioManager.abandonAudioFocus(null)
612
+ }
613
+ audioFocusGranted = false
614
+ }
615
+
616
+ private fun onPlaybackComplete() {
617
+ emitEvent(EVENT_PLAYBACK_COMPLETE, null)
618
+ skipToNext()
619
+ }
620
+
621
+ private fun startProgressUpdates() {
622
+ handler.postDelayed(updateRunnable, UPDATE_INTERVAL_MS)
623
+ }
624
+
625
+ private fun stopProgressUpdates() {
626
+ handler.removeCallbacks(updateRunnable)
627
+ }
628
+
629
+ private fun updatePlaybackState() {
630
+ if (isPlaying) {
631
+ emitPlaybackStateChanged()
632
+ handler.postDelayed(updateRunnable, UPDATE_INTERVAL_MS)
633
+ }
634
+ }
635
+
636
+ private fun emitPlaybackStateChanged() {
637
+ val params = Arguments.createMap()
638
+ params.putBoolean("isPlaying", isPlaying)
639
+ params.putDouble("currentTime", (mediaPlayer?.currentPosition ?: 0).toDouble())
640
+ params.putDouble("duration", (mediaPlayer?.duration ?: 0).toDouble())
641
+ params.putDouble("volume", volume.toDouble())
642
+ params.putDouble("playbackRate", playbackRate.toDouble())
643
+ emitEvent(EVENT_PLAYBACK_STATE_CHANGED, params)
644
+ }
645
+
646
+ private fun emitQueueChanged() {
647
+ val params = Arguments.createMap()
648
+ val queueArray = Arguments.createArray()
649
+ queue.forEach { song ->
650
+ queueArray.pushMap(convertSongToMap(song))
651
+ }
652
+ params.putArray("queue", queueArray)
653
+ params.putInt("position", queuePosition)
654
+ emitEvent(EVENT_PLAYBACK_QUEUE_CHANGED, params)
655
+ }
656
+
657
+ private fun emitPlaybackError(message: String?) {
658
+ val params = Arguments.createMap()
659
+ params.putString("error", message ?: "Unknown error")
660
+ currentSong?.let {
661
+ params.putString("songId", it.id)
662
+ }
663
+ emitEvent(EVENT_PLAYBACK_ERROR, params)
664
+ }
665
+
666
+ private fun emitEqualizerChanged(preset: String?, bands: ReadableArray?) {
667
+ val params = Arguments.createMap()
668
+ params.putBoolean("enabled", equalizer?.enabled ?: false)
669
+ preset?.let { params.putString("preset", it) }
670
+ bands?.let { params.putArray("bands", it) }
671
+ emitEvent(EVENT_EQUALIZER_CHANGED, params)
672
+ }
673
+
674
+ private fun convertSongToMap(song: SongEntity): WritableMap {
675
+ val map = Arguments.createMap()
676
+ map.putString("id", song.id)
677
+ map.putString("title", song.title)
678
+ map.putString("artist", song.artist)
679
+ map.putString("album", song.album)
680
+ map.putDouble("duration", song.duration.toDouble())
681
+ map.putString("path", song.path)
682
+ map.putString("uri", song.uri)
683
+ map.putString("albumArtUri", song.albumArtUri)
684
+ map.putDouble("size", song.size.toDouble())
685
+ map.putString("mimeType", song.mimeType)
686
+ map.putInt("trackNumber", song.trackNumber)
687
+ map.putInt("year", song.year ?: 0)
688
+ map.putString("genre", song.genre)
689
+ map.putDouble("dateAdded", song.dateAdded.toDouble())
690
+ map.putDouble("lastModified", song.lastModified.toDouble())
691
+ map.putBoolean("isFavorite", song.isFavorite)
692
+ map.putInt("playCount", song.playCount)
693
+ map.putDouble("lastPlayed", song.lastPlayed?.toDouble() ?: 0.0)
694
+ map.putInt("bitrate", song.bitrate ?: 0)
695
+ map.putInt("sampleRate", song.sampleRate ?: 0)
696
+ map.putInt("channels", song.channels ?: 2)
697
+ map.putString("composer", song.composer)
698
+ map.putDouble("bookmark", song.bookmark.toDouble())
699
+ map.putBoolean("isPodcast", song.isPodcast)
700
+ map.putInt("rating", song.rating ?: 0)
701
+
702
+ return map
703
+ }
704
+
705
+ private fun emitEvent(eventName: String, params: WritableMap?) {
706
+ reactApplicationContext
707
+ ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
708
+ ?.emit(eventName, params)
709
+ }
710
+ }