react-native-nitro-player 0.7.1-alpha.1 → 0.7.1-alpha.2
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/src/main/AndroidManifest.xml +14 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +68 -49
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +67 -21
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +27 -5
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +88 -49
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +40 -10
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +70 -29
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +4 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +38 -32
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +70 -65
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +23 -12
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +16 -4
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +101 -72
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +42 -22
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +40 -23
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +73 -62
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +51 -48
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +12 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +43 -34
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +30 -178
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +40 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +87 -85
- package/package.json +1 -1
- package/src/hooks/useEqualizer.ts +15 -12
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +3 -2
- package/src/specs/DownloadManager.nitro.ts +4 -2
- package/src/specs/Equalizer.nitro.ts +4 -2
- package/src/specs/TrackPlayer.nitro.ts +18 -6
|
@@ -12,62 +12,65 @@ import com.margelo.nitro.nitroplayer.TrackItem
|
|
|
12
12
|
|
|
13
13
|
// ── Track updates (URL resolution) ────────────────────────────────────────
|
|
14
14
|
|
|
15
|
-
suspend fun TrackPlayerCore.updateTracks(tracks: List<TrackItem>) =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
15
|
+
suspend fun TrackPlayerCore.updateTracks(tracks: List<TrackItem>) =
|
|
16
|
+
withPlayerContext {
|
|
17
|
+
val currentTrack = getCurrentTrack()
|
|
18
|
+
val currentTrackId = currentTrack?.id
|
|
19
|
+
val currentTrackIsEmpty = currentTrack?.url.isNullOrEmpty()
|
|
20
|
+
val currentTrackUpdate = if (currentTrackId != null) tracks.find { it.id == currentTrackId } else null
|
|
21
|
+
|
|
22
|
+
val safeTracks =
|
|
23
|
+
tracks.filter { track ->
|
|
24
|
+
when {
|
|
25
|
+
track.id == currentTrackId && !currentTrackIsEmpty -> false
|
|
26
|
+
|
|
27
|
+
// preserve gapless
|
|
28
|
+
track.id == currentTrackId && currentTrackIsEmpty -> track.url.isNotEmpty()
|
|
29
|
+
|
|
30
|
+
track.url.isEmpty() -> false
|
|
31
|
+
|
|
32
|
+
else -> true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (safeTracks.isEmpty()) return@withPlayerContext
|
|
36
|
+
|
|
37
|
+
val affectedPlaylists: Map<String, Int> = playlistManager.updateTracks(safeTracks)
|
|
38
|
+
|
|
39
|
+
// Replace current track's MediaItem if it was empty-URL and now has a URL
|
|
40
|
+
if (currentTrackUpdate != null && currentTrackIsEmpty && currentTrackUpdate.url.isNotEmpty()) {
|
|
41
|
+
val exoIndex = exo.currentMediaItemIndex
|
|
42
|
+
if (exoIndex >= 0) {
|
|
43
|
+
val playlistId = currentPlaylistId ?: ""
|
|
44
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${currentTrackUpdate.id}" else currentTrackUpdate.id
|
|
45
|
+
exo.replaceMediaItem(exoIndex, makeMediaItem(currentTrackUpdate, mediaId))
|
|
46
|
+
if (exo.playbackState == Player.STATE_IDLE) exo.prepare()
|
|
47
|
+
}
|
|
41
48
|
}
|
|
42
|
-
}
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
if (currentPlaylistId != null && affectedPlaylists.containsKey(currentPlaylistId)) {
|
|
51
|
+
val refreshedPlaylist = playlistManager.getPlaylist(currentPlaylistId!!)
|
|
52
|
+
if (refreshedPlaylist != null) {
|
|
53
|
+
currentTracks = refreshedPlaylist.tracks
|
|
54
|
+
val updatedById = currentTracks.associateBy { it.id }
|
|
55
|
+
playNextStack.forEachIndexed { i, t -> updatedById[t.id]?.let { if (it !== t) playNextStack[i] = it } }
|
|
56
|
+
upNextQueue.forEachIndexed { i, t -> updatedById[t.id]?.let { if (it !== t) upNextQueue[i] = it } }
|
|
57
|
+
}
|
|
58
|
+
rebuildQueueFromCurrentPosition()
|
|
51
59
|
}
|
|
52
|
-
rebuildQueueFromCurrentPosition()
|
|
53
60
|
}
|
|
54
|
-
}
|
|
55
61
|
|
|
56
62
|
// ── Track queries ─────────────────────────────────────────────────────────
|
|
57
63
|
|
|
58
|
-
suspend fun TrackPlayerCore.getTracksById(trackIds: List<String>): List<TrackItem> =
|
|
59
|
-
withPlayerContext { playlistManager.getTracksById(trackIds) as List<TrackItem> }
|
|
64
|
+
suspend fun TrackPlayerCore.getTracksById(trackIds: List<String>): List<TrackItem> = withPlayerContext { playlistManager.getTracksById(trackIds) as List<TrackItem> }
|
|
60
65
|
|
|
61
|
-
suspend fun TrackPlayerCore.getTracksNeedingUrls(): List<TrackItem> =
|
|
62
|
-
withPlayerContext { getTracksNeedingUrlsInternal() }
|
|
66
|
+
suspend fun TrackPlayerCore.getTracksNeedingUrls(): List<TrackItem> = withPlayerContext { getTracksNeedingUrlsInternal() }
|
|
63
67
|
|
|
64
68
|
internal fun TrackPlayerCore.getTracksNeedingUrlsInternal(): List<TrackItem> {
|
|
65
69
|
val pid = currentPlaylistId ?: return emptyList()
|
|
66
70
|
return playlistManager.getPlaylist(pid)?.tracks?.filter { it.url.isEmpty() } ?: emptyList()
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
suspend fun TrackPlayerCore.getNextTracks(count: Int): List<TrackItem> =
|
|
70
|
-
withPlayerContext { getNextTracksInternal(count) }
|
|
73
|
+
suspend fun TrackPlayerCore.getNextTracks(count: Int): List<TrackItem> = withPlayerContext { getNextTracksInternal(count) }
|
|
71
74
|
|
|
72
75
|
internal fun TrackPlayerCore.getNextTracksInternal(count: Int): List<TrackItem> {
|
|
73
76
|
val actualQueue = getActualQueueInternal()
|
|
@@ -84,15 +87,15 @@ suspend fun TrackPlayerCore.getCurrentTrackIndex(): Int = withPlayerContext { cu
|
|
|
84
87
|
// ── URL lookahead ─────────────────────────────────────────────────────────
|
|
85
88
|
|
|
86
89
|
internal fun TrackPlayerCore.checkUpcomingTracksForUrls(lookahead: Int = 5) {
|
|
87
|
-
val upcomingTracks =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
val upcomingTracks =
|
|
91
|
+
if (currentTrackIndex < 0) {
|
|
92
|
+
currentTracks.take(lookahead)
|
|
93
|
+
} else {
|
|
94
|
+
getNextTracksInternal(lookahead)
|
|
95
|
+
}
|
|
92
96
|
val currentTrack = getCurrentTrack()
|
|
93
97
|
val currentNeedsUrl = currentTrack != null && currentTrack.url.isEmpty()
|
|
94
98
|
val candidates = if (currentNeedsUrl) listOf(currentTrack!!) + upcomingTracks else upcomingTracks
|
|
95
99
|
val needUrls = candidates.filter { it.url.isEmpty() }
|
|
96
100
|
if (needUrls.isNotEmpty()) notifyTracksNeedUpdate(needUrls, lookahead)
|
|
97
101
|
}
|
|
98
|
-
|
|
@@ -7,13 +7,13 @@ import com.margelo.nitro.nitroplayer.*
|
|
|
7
7
|
import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
|
|
8
8
|
import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
|
|
9
9
|
import com.margelo.nitro.nitroplayer.storage.NitroPlayerStorage
|
|
10
|
-
import org.json.JSONArray
|
|
11
|
-
import org.json.JSONObject
|
|
12
|
-
import java.io.File
|
|
13
10
|
import kotlinx.coroutines.CoroutineScope
|
|
14
11
|
import kotlinx.coroutines.Dispatchers
|
|
15
12
|
import kotlinx.coroutines.SupervisorJob
|
|
16
13
|
import kotlinx.coroutines.launch
|
|
14
|
+
import org.json.JSONArray
|
|
15
|
+
import org.json.JSONObject
|
|
16
|
+
import java.io.File
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Manages persistence of downloaded track metadata using file storage
|
|
@@ -236,9 +236,18 @@ class DownloadManagerCore private constructor(
|
|
|
236
236
|
|
|
237
237
|
for (m in activeTasks.values) {
|
|
238
238
|
when (m.state) {
|
|
239
|
-
DownloadState.PENDING ->
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
DownloadState.PENDING -> {
|
|
240
|
+
pendingCount++
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
DownloadState.DOWNLOADING -> {
|
|
244
|
+
activeCount++
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
DownloadState.FAILED -> {
|
|
248
|
+
failedCount++
|
|
249
|
+
}
|
|
250
|
+
|
|
242
251
|
else -> {}
|
|
243
252
|
}
|
|
244
253
|
totalBytes += m.totalBytes ?: 0.0
|
|
@@ -13,10 +13,10 @@ import com.margelo.nitro.nitroplayer.EqualizerState
|
|
|
13
13
|
import com.margelo.nitro.nitroplayer.GainRange
|
|
14
14
|
import com.margelo.nitro.nitroplayer.PresetType
|
|
15
15
|
import com.margelo.nitro.nitroplayer.Variant_NullType_String
|
|
16
|
+
import com.margelo.nitro.nitroplayer.core.ListenerRegistry
|
|
16
17
|
import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
|
|
17
18
|
import org.json.JSONArray
|
|
18
19
|
import org.json.JSONObject
|
|
19
|
-
import com.margelo.nitro.nitroplayer.core.ListenerRegistry
|
|
20
20
|
|
|
21
21
|
class EqualizerCore private constructor(
|
|
22
22
|
private val context: Context,
|
|
@@ -95,9 +95,10 @@ class EqualizerCore private constructor(
|
|
|
95
95
|
initDynamicsProcessing(audioSessionId)
|
|
96
96
|
usingDynamicsProcessing = true
|
|
97
97
|
} else {
|
|
98
|
-
equalizer =
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
equalizer =
|
|
99
|
+
Equalizer(0, audioSessionId).apply {
|
|
100
|
+
enabled = false
|
|
101
|
+
}
|
|
101
102
|
usingDynamicsProcessing = false
|
|
102
103
|
setupBandMapping()
|
|
103
104
|
}
|
|
@@ -109,14 +110,19 @@ class EqualizerCore private constructor(
|
|
|
109
110
|
|
|
110
111
|
@RequiresApi(Build.VERSION_CODES.P)
|
|
111
112
|
private fun initDynamicsProcessing(sessionId: Int) {
|
|
112
|
-
val config =
|
|
113
|
-
DynamicsProcessing.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
val config =
|
|
114
|
+
DynamicsProcessing.Config
|
|
115
|
+
.Builder(
|
|
116
|
+
DynamicsProcessing.VARIANT_FAVOR_FREQUENCY_RESOLUTION,
|
|
117
|
+
1, // channelCount (stereo handled internally)
|
|
118
|
+
true,
|
|
119
|
+
10, // Pre-EQ enabled, 10 bands
|
|
120
|
+
false,
|
|
121
|
+
0, // MBC disabled
|
|
122
|
+
false,
|
|
123
|
+
0, // Post-EQ disabled
|
|
124
|
+
false, // Limiter disabled
|
|
125
|
+
).build()
|
|
120
126
|
dynamicsProcessing = DynamicsProcessing(0, sessionId, config).apply { enabled = false }
|
|
121
127
|
for (i in 0 until 10) {
|
|
122
128
|
val band = DynamicsProcessing.EqBand(true, frequencies[i].toFloat(), 0f)
|
|
@@ -155,8 +161,8 @@ class EqualizerCore private constructor(
|
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
163
|
|
|
158
|
-
fun setEnabled(enabled: Boolean): Boolean
|
|
159
|
-
|
|
164
|
+
fun setEnabled(enabled: Boolean): Boolean =
|
|
165
|
+
try {
|
|
160
166
|
if (usingDynamicsProcessing && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
161
167
|
dynamicsProcessing?.enabled = enabled
|
|
162
168
|
} else {
|
|
@@ -170,12 +176,11 @@ class EqualizerCore private constructor(
|
|
|
170
176
|
NitroPlayerLogger.log("EqualizerCore", "Failed to set enabled: ${e.message}")
|
|
171
177
|
false
|
|
172
178
|
}
|
|
173
|
-
}
|
|
174
179
|
|
|
175
180
|
fun isEnabled(): Boolean = isEqualizerEnabled
|
|
176
181
|
|
|
177
|
-
fun getBands(): Array<EqualizerBand>
|
|
178
|
-
|
|
182
|
+
fun getBands(): Array<EqualizerBand> =
|
|
183
|
+
(0 until 10)
|
|
179
184
|
.map { i ->
|
|
180
185
|
val gainDb = getCurrentBandGain(i)
|
|
181
186
|
EqualizerBand(
|
|
@@ -185,7 +190,6 @@ class EqualizerCore private constructor(
|
|
|
185
190
|
frequencyLabel = frequencyLabels[i],
|
|
186
191
|
)
|
|
187
192
|
}.toTypedArray()
|
|
188
|
-
}
|
|
189
193
|
|
|
190
194
|
private fun getCurrentBandGain(bandIndex: Int): Double = currentGainsArray[bandIndex]
|
|
191
195
|
|
|
@@ -217,7 +221,10 @@ class EqualizerCore private constructor(
|
|
|
217
221
|
}
|
|
218
222
|
|
|
219
223
|
@RequiresApi(Build.VERSION_CODES.P)
|
|
220
|
-
private fun setDPBandGain(
|
|
224
|
+
private fun setDPBandGain(
|
|
225
|
+
bandIndex: Int,
|
|
226
|
+
gainDb: Float,
|
|
227
|
+
) {
|
|
221
228
|
val band = DynamicsProcessing.EqBand(true, frequencies[bandIndex].toFloat(), gainDb)
|
|
222
229
|
dynamicsProcessing?.setPreEqBandAllChannelsTo(bandIndex, band)
|
|
223
230
|
}
|
|
@@ -249,12 +256,10 @@ class EqualizerCore private constructor(
|
|
|
249
256
|
}
|
|
250
257
|
}
|
|
251
258
|
|
|
252
|
-
private fun getAllGains(): List<Double> {
|
|
253
|
-
return (0 until 10).map { i -> getCurrentBandGain(i) }
|
|
254
|
-
}
|
|
259
|
+
private fun getAllGains(): List<Double> = (0 until 10).map { i -> getCurrentBandGain(i) }
|
|
255
260
|
|
|
256
|
-
fun getBandRange(): GainRange
|
|
257
|
-
|
|
261
|
+
fun getBandRange(): GainRange =
|
|
262
|
+
if (usingDynamicsProcessing) {
|
|
258
263
|
GainRange(min = -12.0, max = 12.0)
|
|
259
264
|
} else {
|
|
260
265
|
val eq = equalizer
|
|
@@ -268,7 +273,6 @@ class EqualizerCore private constructor(
|
|
|
268
273
|
GainRange(min = -12.0, max = 12.0)
|
|
269
274
|
}
|
|
270
275
|
}
|
|
271
|
-
}
|
|
272
276
|
|
|
273
277
|
fun getPresets(): Array<EqualizerPreset> {
|
|
274
278
|
val builtIn = getBuiltInPresets()
|
|
@@ -418,17 +422,22 @@ class EqualizerCore private constructor(
|
|
|
418
422
|
}
|
|
419
423
|
}
|
|
420
424
|
|
|
421
|
-
private fun saveBandGainsAndPreset(
|
|
425
|
+
private fun saveBandGainsAndPreset(
|
|
426
|
+
gains: List<Double>,
|
|
427
|
+
presetName: String?,
|
|
428
|
+
) {
|
|
422
429
|
val json = JSONArray()
|
|
423
430
|
gains.forEach { json.put(it) }
|
|
424
|
-
prefs
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
putString("
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
431
|
+
prefs
|
|
432
|
+
.edit()
|
|
433
|
+
.apply {
|
|
434
|
+
putString("eq_band_gains", json.toString())
|
|
435
|
+
if (presetName != null) {
|
|
436
|
+
putString("eq_current_preset", presetName)
|
|
437
|
+
} else {
|
|
438
|
+
remove("eq_current_preset")
|
|
439
|
+
}
|
|
440
|
+
}.apply()
|
|
432
441
|
}
|
|
433
442
|
|
|
434
443
|
private fun restoreSettings() {
|
|
@@ -3,22 +3,14 @@ package com.margelo.nitro.nitroplayer.media
|
|
|
3
3
|
import android.app.Notification
|
|
4
4
|
import android.app.NotificationChannel
|
|
5
5
|
import android.app.NotificationManager
|
|
6
|
-
import android.app.PendingIntent
|
|
7
6
|
import android.content.Context
|
|
8
7
|
import android.content.Intent
|
|
9
|
-
import android.graphics.Bitmap
|
|
10
|
-
import android.graphics.BitmapFactory
|
|
11
8
|
import android.net.Uri
|
|
12
9
|
import android.os.Build
|
|
13
|
-
import android.util.Log
|
|
14
|
-
import android.util.LruCache
|
|
15
|
-
import androidx.core.app.NotificationCompat
|
|
16
10
|
import androidx.media3.common.MediaItem
|
|
17
11
|
import androidx.media3.common.MediaMetadata
|
|
18
|
-
import androidx.media3.common.Player
|
|
19
12
|
import androidx.media3.exoplayer.ExoPlayer
|
|
20
13
|
import androidx.media3.session.MediaSession
|
|
21
|
-
import androidx.media3.session.MediaSessionService
|
|
22
14
|
import androidx.media3.session.SessionCommand
|
|
23
15
|
import androidx.media3.session.SessionResult
|
|
24
16
|
import com.google.common.util.concurrent.Futures
|
|
@@ -27,14 +19,11 @@ import com.margelo.nitro.nitroplayer.TrackItem
|
|
|
27
19
|
import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
|
|
28
20
|
import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
|
|
29
21
|
import com.margelo.nitro.nitroplayer.core.loadPlaylist
|
|
30
|
-
import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
31
22
|
import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
|
|
32
23
|
import kotlinx.coroutines.CoroutineScope
|
|
33
24
|
import kotlinx.coroutines.Dispatchers
|
|
34
25
|
import kotlinx.coroutines.SupervisorJob
|
|
35
26
|
import kotlinx.coroutines.launch
|
|
36
|
-
import kotlinx.coroutines.withContext
|
|
37
|
-
import java.net.URL
|
|
38
27
|
|
|
39
28
|
class MediaSessionManager(
|
|
40
29
|
private val context: Context,
|
|
@@ -49,26 +38,19 @@ class MediaSessionManager(
|
|
|
49
38
|
|
|
50
39
|
var mediaSession: MediaSession? = null // Make public so MediaBrowserService can access it
|
|
51
40
|
private set
|
|
52
|
-
private var notificationManager: NotificationManager? = null
|
|
53
41
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
42
|
+
|
|
54
43
|
@Volatile private var currentTrack: TrackItem? = null
|
|
44
|
+
|
|
55
45
|
@Volatile private var isPlaying: Boolean = false
|
|
56
|
-
private val artworkCache = object : LruCache<String, Bitmap>(20) {
|
|
57
|
-
override fun sizeOf(key: String, value: Bitmap): Int = 1
|
|
58
|
-
}
|
|
59
46
|
|
|
60
47
|
private var androidAutoEnabled: Boolean = false
|
|
61
48
|
private var carPlayEnabled: Boolean = false
|
|
62
49
|
private var showInNotification: Boolean = true
|
|
63
50
|
|
|
64
51
|
companion object {
|
|
65
|
-
private const val NOTIFICATION_ID = 1001
|
|
66
52
|
private const val CHANNEL_ID = "nitro_player_channel"
|
|
67
53
|
private const val CHANNEL_NAME = "Music Player"
|
|
68
|
-
const val ACTION_PLAY = "com.margelo.nitro.nitroplayer.PLAY"
|
|
69
|
-
const val ACTION_PAUSE = "com.margelo.nitro.nitroplayer.PAUSE"
|
|
70
|
-
const val ACTION_NEXT = "com.margelo.nitro.nitroplayer.NEXT"
|
|
71
|
-
const val ACTION_PREVIOUS = "com.margelo.nitro.nitroplayer.PREVIOUS"
|
|
72
54
|
}
|
|
73
55
|
|
|
74
56
|
init {
|
|
@@ -85,10 +67,8 @@ class MediaSessionManager(
|
|
|
85
67
|
carPlayEnabled?.let { this.carPlayEnabled = it }
|
|
86
68
|
showInNotification?.let {
|
|
87
69
|
this.showInNotification = it
|
|
88
|
-
if (it) {
|
|
89
|
-
|
|
90
|
-
} else {
|
|
91
|
-
hideNotification()
|
|
70
|
+
if (!it) {
|
|
71
|
+
stopPlaybackService()
|
|
92
72
|
}
|
|
93
73
|
}
|
|
94
74
|
}
|
|
@@ -273,45 +253,17 @@ class MediaSessionManager(
|
|
|
273
253
|
}
|
|
274
254
|
},
|
|
275
255
|
).build()
|
|
276
|
-
// MediaSession is active by default in Media3
|
|
277
|
-
updateMediaSessionMetadata()
|
|
278
|
-
} catch (e: Exception) {
|
|
279
|
-
e.printStackTrace()
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private fun updateMediaSessionMetadata() {
|
|
284
|
-
// MediaSession will automatically use the metadata from player's current MediaItem
|
|
285
|
-
// No need to manually update here as TrackPlayerCore already sets metadata
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
private suspend fun loadArtworkBitmap(artworkUrl: String?): Bitmap? {
|
|
289
|
-
if (artworkUrl.isNullOrEmpty()) return null
|
|
290
256
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return try {
|
|
295
|
-
val bitmap =
|
|
296
|
-
withContext(Dispatchers.IO) {
|
|
297
|
-
val url = URL(artworkUrl)
|
|
298
|
-
BitmapFactory.decodeStream(url.openConnection().getInputStream())
|
|
299
|
-
}
|
|
300
|
-
// Cache the bitmap
|
|
301
|
-
if (bitmap != null) {
|
|
302
|
-
artworkCache.put(artworkUrl, bitmap)
|
|
303
|
-
}
|
|
304
|
-
bitmap
|
|
257
|
+
// Wire the session into the PlaybackService so it can be promoted to foreground
|
|
258
|
+
NitroPlayerPlaybackService.mediaSession = mediaSession
|
|
305
259
|
} catch (e: Exception) {
|
|
306
260
|
e.printStackTrace()
|
|
307
|
-
null
|
|
308
261
|
}
|
|
309
262
|
}
|
|
310
263
|
|
|
311
264
|
private fun createNotificationChannel() {
|
|
312
|
-
notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
313
|
-
|
|
314
265
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
266
|
+
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
315
267
|
val channel =
|
|
316
268
|
NotificationChannel(
|
|
317
269
|
CHANNEL_ID,
|
|
@@ -322,150 +274,50 @@ class MediaSessionManager(
|
|
|
322
274
|
setShowBadge(false)
|
|
323
275
|
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
324
276
|
}
|
|
325
|
-
|
|
326
|
-
notificationManager?.createNotificationChannel(channel)
|
|
277
|
+
manager.createNotificationChannel(channel)
|
|
327
278
|
}
|
|
328
279
|
}
|
|
329
280
|
|
|
330
|
-
private fun
|
|
331
|
-
|
|
332
|
-
private fun updateNotification() {
|
|
333
|
-
if (!showInNotification) return
|
|
334
|
-
|
|
335
|
-
val currentTrack = getCurrentTrack()
|
|
336
|
-
val notification = buildNotification(currentTrack)
|
|
337
|
-
notificationManager?.notify(NOTIFICATION_ID, notification)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private fun buildNotification(track: TrackItem?): Notification {
|
|
341
|
-
val mediaSession = this.mediaSession ?: return createEmptyNotification()
|
|
342
|
-
|
|
343
|
-
// Launch intent
|
|
344
|
-
val contentIntent =
|
|
345
|
-
PendingIntent.getActivity(
|
|
346
|
-
context,
|
|
347
|
-
0,
|
|
348
|
-
context.packageManager.getLaunchIntentForPackage(context.packageName),
|
|
349
|
-
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
val builder =
|
|
353
|
-
NotificationCompat
|
|
354
|
-
.Builder(context, CHANNEL_ID)
|
|
355
|
-
.setContentTitle(track?.title ?: "Unknown Title")
|
|
356
|
-
.setContentText(track?.artist ?: "Unknown Artist")
|
|
357
|
-
.setSubText(track?.album ?: "")
|
|
358
|
-
.setSmallIcon(android.R.drawable.ic_media_play)
|
|
359
|
-
.setContentIntent(contentIntent)
|
|
360
|
-
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
361
|
-
.setOngoing(isPlaying)
|
|
362
|
-
.setShowWhen(false)
|
|
363
|
-
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
364
|
-
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
|
281
|
+
private fun startPlaybackService() {
|
|
365
282
|
try {
|
|
366
|
-
val
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
.MediaStyle()
|
|
372
|
-
.setMediaSession(compatToken)
|
|
373
|
-
.setShowActionsInCompactView(0, 1, 2),
|
|
374
|
-
)
|
|
375
|
-
} catch (e: Exception) {
|
|
376
|
-
NitroPlayerLogger.log("MediaSessionManager") { "Failed to set media session token: ${e.message}" }
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Add action buttons
|
|
380
|
-
builder.addAction(
|
|
381
|
-
android.R.drawable.ic_media_previous,
|
|
382
|
-
"Previous",
|
|
383
|
-
createMediaAction(ACTION_PREVIOUS),
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
if (isPlaying) {
|
|
387
|
-
builder.addAction(
|
|
388
|
-
android.R.drawable.ic_media_pause,
|
|
389
|
-
"Pause",
|
|
390
|
-
createMediaAction(ACTION_PAUSE),
|
|
391
|
-
)
|
|
392
|
-
} else {
|
|
393
|
-
builder.addAction(
|
|
394
|
-
android.R.drawable.ic_media_play,
|
|
395
|
-
"Play",
|
|
396
|
-
createMediaAction(ACTION_PLAY),
|
|
397
|
-
)
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
builder.addAction(
|
|
401
|
-
android.R.drawable.ic_media_next,
|
|
402
|
-
"Next",
|
|
403
|
-
createMediaAction(ACTION_NEXT),
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
// Load artwork asynchronously and update notification
|
|
407
|
-
track?.artwork?.asSecondOrNull()?.let { artworkUrl ->
|
|
408
|
-
scope.launch {
|
|
409
|
-
val bitmap = loadArtworkBitmap(artworkUrl)
|
|
410
|
-
if (bitmap != null) {
|
|
411
|
-
builder.setLargeIcon(bitmap)
|
|
412
|
-
notificationManager?.notify(NOTIFICATION_ID, builder.build())
|
|
413
|
-
}
|
|
283
|
+
val intent = Intent(context, NitroPlayerPlaybackService::class.java)
|
|
284
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
285
|
+
context.startForegroundService(intent)
|
|
286
|
+
} else {
|
|
287
|
+
context.startService(intent)
|
|
414
288
|
}
|
|
289
|
+
} catch (e: Exception) {
|
|
290
|
+
NitroPlayerLogger.log("MediaSessionManager") { "Failed to start PlaybackService: ${e.message}" }
|
|
415
291
|
}
|
|
416
|
-
|
|
417
|
-
return builder.build()
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
private fun createEmptyNotification(): Notification =
|
|
421
|
-
NotificationCompat
|
|
422
|
-
.Builder(context, CHANNEL_ID)
|
|
423
|
-
.setContentTitle("Music Player")
|
|
424
|
-
.setSmallIcon(android.R.drawable.ic_media_play)
|
|
425
|
-
.build()
|
|
426
|
-
|
|
427
|
-
private fun createMediaAction(action: String): PendingIntent {
|
|
428
|
-
val intent =
|
|
429
|
-
Intent(action).apply {
|
|
430
|
-
setPackage(context.packageName)
|
|
431
|
-
}
|
|
432
|
-
return PendingIntent.getBroadcast(
|
|
433
|
-
context,
|
|
434
|
-
0,
|
|
435
|
-
intent,
|
|
436
|
-
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
437
|
-
)
|
|
438
292
|
}
|
|
439
293
|
|
|
440
|
-
private fun
|
|
441
|
-
|
|
294
|
+
private fun stopPlaybackService() {
|
|
295
|
+
try {
|
|
296
|
+
val intent = Intent(context, NitroPlayerPlaybackService::class.java)
|
|
297
|
+
context.stopService(intent)
|
|
298
|
+
} catch (e: Exception) {
|
|
299
|
+
NitroPlayerLogger.log("MediaSessionManager") { "Failed to stop PlaybackService: ${e.message}" }
|
|
300
|
+
}
|
|
442
301
|
}
|
|
443
302
|
|
|
444
303
|
fun onTrackChanged(track: TrackItem?) {
|
|
445
304
|
currentTrack = track
|
|
446
|
-
// Preload artwork for better notification display
|
|
447
|
-
if (track != null) {
|
|
448
|
-
scope.launch {
|
|
449
|
-
track.artwork?.asSecondOrNull()?.let { artworkUrl ->
|
|
450
|
-
loadArtworkBitmap(artworkUrl)
|
|
451
|
-
}
|
|
452
|
-
updateNotification()
|
|
453
|
-
}
|
|
454
|
-
} else {
|
|
455
|
-
updateNotification()
|
|
456
|
-
}
|
|
457
305
|
}
|
|
458
306
|
|
|
459
307
|
fun onPlaybackStateChanged(playing: Boolean) {
|
|
460
308
|
isPlaying = playing
|
|
461
|
-
|
|
309
|
+
if (playing && showInNotification) {
|
|
310
|
+
startPlaybackService()
|
|
311
|
+
} else if (!playing) {
|
|
312
|
+
stopPlaybackService()
|
|
313
|
+
}
|
|
462
314
|
}
|
|
463
315
|
|
|
464
316
|
fun release() {
|
|
465
|
-
|
|
317
|
+
stopPlaybackService()
|
|
318
|
+
NitroPlayerPlaybackService.mediaSession = null
|
|
466
319
|
mediaSession?.release()
|
|
467
320
|
mediaSession = null
|
|
468
|
-
artworkCache.evictAll()
|
|
469
321
|
}
|
|
470
322
|
|
|
471
323
|
private fun createMediaItem(
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroplayer.media
|
|
2
|
+
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import androidx.media3.session.MediaSession
|
|
5
|
+
import androidx.media3.session.MediaSessionService
|
|
6
|
+
import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Foreground service that keeps playback alive when the app is backgrounded or the screen is locked.
|
|
10
|
+
*
|
|
11
|
+
* Media3's [MediaSessionService] automatically:
|
|
12
|
+
* - Promotes to a foreground service (with notification) when playback starts
|
|
13
|
+
* - Demotes / stops when playback stops
|
|
14
|
+
* - Posts a media-style notification with transport controls
|
|
15
|
+
* - Handles media-button intents
|
|
16
|
+
*/
|
|
17
|
+
class NitroPlayerPlaybackService : MediaSessionService() {
|
|
18
|
+
|
|
19
|
+
companion object {
|
|
20
|
+
@Volatile
|
|
21
|
+
var mediaSession: MediaSession? = null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
|
25
|
+
return mediaSession
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
29
|
+
val session = mediaSession
|
|
30
|
+
if (session == null || !session.player.playWhenReady) {
|
|
31
|
+
stopSelf()
|
|
32
|
+
}
|
|
33
|
+
super.onTaskRemoved(rootIntent)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override fun onDestroy() {
|
|
37
|
+
NitroPlayerLogger.log("PlaybackService") { "Service destroyed" }
|
|
38
|
+
super.onDestroy()
|
|
39
|
+
}
|
|
40
|
+
}
|