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.
- package/android/build.gradle +111 -0
- package/android/src/main/AndroidManifest.xml +44 -0
- package/android/src/main/java/com/reactnativemp3/Mp3Package.kt +23 -0
- package/android/src/main/java/com/reactnativemp3/Mp3TurboModule.kt +43 -0
- package/android/src/main/java/com/reactnativemp3/database/MusicDatabase.kt +48 -0
- package/android/src/main/java/com/reactnativemp3/database/dao/SongDao.kt +72 -0
- package/android/src/main/java/com/reactnativemp3/database/entities/PlaylistEntity.kt +58 -0
- package/android/src/main/java/com/reactnativemp3/database/entities/SongEntity.kt +104 -0
- package/android/src/main/java/com/reactnativemp3/database/entities/ThumbnailCacheEntity.kt +43 -0
- package/android/src/main/java/com/reactnativemp3/managers/CacheManager.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/managers/EqualizerManager.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/modules/MetadataModule.kt +330 -0
- package/android/src/main/java/com/reactnativemp3/modules/NotificationModule.kt +236 -0
- package/android/src/main/java/com/reactnativemp3/modules/PlayerModule.kt +710 -0
- package/android/src/main/java/com/reactnativemp3/modules/ScannerModule.kt +640 -0
- package/android/src/main/java/com/reactnativemp3/services/AudioFocusService.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/services/FileObserverService.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/services/MusicService.kt +309 -0
- package/android/src/main/java/com/reactnativemp3/utils/MediaStoreUtils.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/utils/PermissionUtils.kt +0 -0
- package/android/src/main/jni/Mp3TurboModule.cpp +29 -0
- package/android/src/main/res/drawable/ic_music_note.xml +11 -0
- package/android/src/main/res/drawable/ic_pause.xml +11 -0
- package/android/src/main/res/drawable/ic_play.xml +11 -0
- package/android/src/main/res/drawable/ic_skip_next.xml +11 -0
- package/android/src/main/res/drawable/ic_skip_previous.xml +11 -0
- package/android/src/main/res/drawable/ic_stop.xml +11 -0
- package/lib/components/MusicList.d.ts +0 -0
- package/lib/components/MusicList.js +1 -0
- package/lib/components/MusicPlayerUI.d.ts +0 -0
- package/lib/components/MusicPlayerUI.js +1 -0
- package/lib/hooks/useMusicPlayer.d.ts +38 -0
- package/lib/hooks/useMusicPlayer.js +242 -0
- package/lib/hooks/useMusicScanner.d.ts +27 -0
- package/lib/hooks/useMusicScanner.js +217 -0
- package/lib/hooks/usePermissions.d.ts +9 -0
- package/lib/hooks/usePermissions.js +55 -0
- package/lib/index.d.ts +144 -0
- package/lib/index.js +148 -0
- package/lib/types/common.types.d.ts +79 -0
- package/lib/types/common.types.js +2 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/index.js +2 -0
- package/lib/types/player.types.d.ts +35 -0
- package/lib/types/player.types.js +2 -0
- package/lib/types/scanner.types.d.ts +29 -0
- package/lib/types/scanner.types.js +2 -0
- package/lib/utils/constants.d.ts +31 -0
- package/lib/utils/constants.js +55 -0
- package/lib/utils/events.d.ts +0 -0
- package/lib/utils/events.js +1 -0
- package/package.json +62 -0
- package/src/components/MusicList.tsx +0 -0
- package/src/components/MusicPlayerUI.tsx +0 -0
- package/src/hooks/useMusicPlayer.ts +358 -0
- package/src/hooks/useMusicScanner.ts +286 -0
- package/src/hooks/usePermissions.ts +64 -0
- package/src/index.ts +214 -0
- package/src/types/common.types.ts +86 -0
- package/src/types/index.ts +4 -0
- package/src/types/player.types.ts +37 -0
- package/src/types/scanner.types.ts +31 -0
- package/src/utils/constants.ts +56 -0
- 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
|
+
}
|