react-native-nitro-player 0.7.1-alpha.2 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +8 -48
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +78 -18
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +15 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +25 -7
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +94 -23
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/ExoPlayerBuilder.kt +49 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionCallbackFactory.kt +167 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +10 -301
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroPlayerNotificationProvider.kt +200 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +72 -11
- package/ios/core/TrackPlayerListener.swift +20 -15
- package/ios/core/TrackPlayerPlayback.swift +10 -2
- package/ios/core/TrackPlayerQueue.swift +2 -1
- package/ios/core/TrackPlayerQueueBuild.swift +16 -2
- package/package.json +1 -1
package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionCallbackFactory.kt
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
@file:Suppress("ktlint:standard:max-line-length")
|
|
2
|
+
|
|
3
|
+
package com.margelo.nitro.nitroplayer.media
|
|
4
|
+
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import androidx.media3.common.MediaItem
|
|
7
|
+
import androidx.media3.common.MediaMetadata
|
|
8
|
+
import androidx.media3.exoplayer.ExoPlayer
|
|
9
|
+
import androidx.media3.session.MediaSession
|
|
10
|
+
import androidx.media3.session.SessionCommand
|
|
11
|
+
import androidx.media3.session.SessionResult
|
|
12
|
+
import com.google.common.util.concurrent.Futures
|
|
13
|
+
import com.google.common.util.concurrent.ListenableFuture
|
|
14
|
+
import com.margelo.nitro.nitroplayer.TrackItem
|
|
15
|
+
import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
|
|
16
|
+
import com.margelo.nitro.nitroplayer.core.loadPlaylist
|
|
17
|
+
import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
|
|
18
|
+
import kotlinx.coroutines.CoroutineScope
|
|
19
|
+
import kotlinx.coroutines.Dispatchers
|
|
20
|
+
import kotlinx.coroutines.SupervisorJob
|
|
21
|
+
import kotlinx.coroutines.launch
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates the [MediaSession.Callback] used by both notification controllers and
|
|
25
|
+
* Android Auto. Extracted from the old MediaSessionManager so the service can
|
|
26
|
+
* own session creation while keeping callback logic isolated.
|
|
27
|
+
*/
|
|
28
|
+
object MediaSessionCallbackFactory {
|
|
29
|
+
|
|
30
|
+
fun create(
|
|
31
|
+
service: NitroPlayerPlaybackService,
|
|
32
|
+
playlistManager: PlaylistManager,
|
|
33
|
+
): MediaSession.Callback {
|
|
34
|
+
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
35
|
+
|
|
36
|
+
return object : MediaSession.Callback {
|
|
37
|
+
override fun onConnect(
|
|
38
|
+
session: MediaSession,
|
|
39
|
+
controller: MediaSession.ControllerInfo,
|
|
40
|
+
): MediaSession.ConnectionResult =
|
|
41
|
+
MediaSession.ConnectionResult
|
|
42
|
+
.AcceptedResultBuilder(session)
|
|
43
|
+
.setAvailableSessionCommands(
|
|
44
|
+
MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS,
|
|
45
|
+
).setAvailablePlayerCommands(
|
|
46
|
+
MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS,
|
|
47
|
+
).build()
|
|
48
|
+
|
|
49
|
+
override fun onAddMediaItems(
|
|
50
|
+
mediaSession: MediaSession,
|
|
51
|
+
controller: MediaSession.ControllerInfo,
|
|
52
|
+
mediaItems: MutableList<MediaItem>,
|
|
53
|
+
): ListenableFuture<MutableList<MediaItem>> {
|
|
54
|
+
NitroPlayerLogger.log("MediaSessionCallback") { "onAddMediaItems called with ${mediaItems.size} items" }
|
|
55
|
+
if (mediaItems.isEmpty()) return Futures.immediateFuture(mutableListOf())
|
|
56
|
+
|
|
57
|
+
val updated = mutableListOf<MediaItem>()
|
|
58
|
+
for (requested in mediaItems) {
|
|
59
|
+
val mediaId =
|
|
60
|
+
requested.requestMetadata.mediaUri?.toString()
|
|
61
|
+
?: requested.mediaId
|
|
62
|
+
try {
|
|
63
|
+
if (mediaId.contains(':')) {
|
|
64
|
+
val colonIdx = mediaId.indexOf(':')
|
|
65
|
+
val playlistId = mediaId.substring(0, colonIdx)
|
|
66
|
+
val trackId = mediaId.substring(colonIdx + 1)
|
|
67
|
+
val playlist = playlistManager.getPlaylist(playlistId)
|
|
68
|
+
val track = playlist?.tracks?.find { it.id == trackId }
|
|
69
|
+
if (track != null) {
|
|
70
|
+
updated.add(createMediaItem(track, mediaId))
|
|
71
|
+
} else {
|
|
72
|
+
updated.add(requested)
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
updated.add(requested)
|
|
76
|
+
}
|
|
77
|
+
} catch (e: Exception) {
|
|
78
|
+
NitroPlayerLogger.log("MediaSessionCallback") { "Error processing mediaId: ${e.message}" }
|
|
79
|
+
updated.add(requested)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return Futures.immediateFuture(updated)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
override fun onSetMediaItems(
|
|
86
|
+
mediaSession: MediaSession,
|
|
87
|
+
controller: MediaSession.ControllerInfo,
|
|
88
|
+
mediaItems: MutableList<MediaItem>,
|
|
89
|
+
startIndex: Int,
|
|
90
|
+
startPositionMs: Long,
|
|
91
|
+
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
|
|
92
|
+
NitroPlayerLogger.log("MediaSessionCallback") { "onSetMediaItems called with ${mediaItems.size} items, startIndex: $startIndex" }
|
|
93
|
+
if (mediaItems.isEmpty()) {
|
|
94
|
+
return Futures.immediateFuture(
|
|
95
|
+
MediaSession.MediaItemsWithStartPosition(mutableListOf(), 0, 0),
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
val firstMediaId = mediaItems[0].mediaId
|
|
100
|
+
if (firstMediaId.contains(':')) {
|
|
101
|
+
val colonIdx = firstMediaId.indexOf(':')
|
|
102
|
+
val playlistId = firstMediaId.substring(0, colonIdx)
|
|
103
|
+
val trackId = firstMediaId.substring(colonIdx + 1)
|
|
104
|
+
val playlist = playlistManager.getPlaylist(playlistId)
|
|
105
|
+
if (playlist != null) {
|
|
106
|
+
val trackIndex = playlist.tracks.indexOfFirst { it.id == trackId }
|
|
107
|
+
if (trackIndex >= 0) {
|
|
108
|
+
service.trackPlayerCore?.let { core ->
|
|
109
|
+
scope.launch { core.loadPlaylist(playlistId) }
|
|
110
|
+
}
|
|
111
|
+
val playlistMediaItems =
|
|
112
|
+
playlist.tracks
|
|
113
|
+
.map { track -> createMediaItem(track, "$playlistId:${track.id}") }
|
|
114
|
+
.toMutableList()
|
|
115
|
+
return Futures.immediateFuture(
|
|
116
|
+
MediaSession.MediaItemsWithStartPosition(
|
|
117
|
+
playlistMediaItems,
|
|
118
|
+
trackIndex,
|
|
119
|
+
startPositionMs,
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (e: Exception) {
|
|
126
|
+
NitroPlayerLogger.log("MediaSessionCallback") { "Error in onSetMediaItems: ${e.message}" }
|
|
127
|
+
}
|
|
128
|
+
return Futures.immediateFuture(
|
|
129
|
+
MediaSession.MediaItemsWithStartPosition(mediaItems, startIndex, startPositionMs),
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
override fun onCustomCommand(
|
|
134
|
+
session: MediaSession,
|
|
135
|
+
controller: MediaSession.ControllerInfo,
|
|
136
|
+
customCommand: SessionCommand,
|
|
137
|
+
args: android.os.Bundle,
|
|
138
|
+
): ListenableFuture<SessionResult> =
|
|
139
|
+
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private fun createMediaItem(
|
|
144
|
+
track: TrackItem,
|
|
145
|
+
mediaId: String,
|
|
146
|
+
): MediaItem {
|
|
147
|
+
val metadataBuilder =
|
|
148
|
+
MediaMetadata
|
|
149
|
+
.Builder()
|
|
150
|
+
.setTitle(track.title)
|
|
151
|
+
.setArtist(track.artist)
|
|
152
|
+
.setAlbumTitle(track.album)
|
|
153
|
+
|
|
154
|
+
track.artwork?.asSecondOrNull()?.let { artworkUrl ->
|
|
155
|
+
try {
|
|
156
|
+
metadataBuilder.setArtworkUri(Uri.parse(artworkUrl))
|
|
157
|
+
} catch (_: Exception) {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return MediaItem
|
|
161
|
+
.Builder()
|
|
162
|
+
.setMediaId(mediaId)
|
|
163
|
+
.setUri(track.url)
|
|
164
|
+
.setMediaMetadata(metadataBuilder.build())
|
|
165
|
+
.build()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -1,33 +1,20 @@
|
|
|
1
1
|
package com.margelo.nitro.nitroplayer.media
|
|
2
2
|
|
|
3
|
-
import android.app.Notification
|
|
4
|
-
import android.app.NotificationChannel
|
|
5
|
-
import android.app.NotificationManager
|
|
6
3
|
import android.content.Context
|
|
7
|
-
import android.content.Intent
|
|
8
|
-
import android.net.Uri
|
|
9
|
-
import android.os.Build
|
|
10
|
-
import androidx.media3.common.MediaItem
|
|
11
|
-
import androidx.media3.common.MediaMetadata
|
|
12
|
-
import androidx.media3.exoplayer.ExoPlayer
|
|
13
4
|
import androidx.media3.session.MediaSession
|
|
14
|
-
import androidx.media3.session.SessionCommand
|
|
15
|
-
import androidx.media3.session.SessionResult
|
|
16
|
-
import com.google.common.util.concurrent.Futures
|
|
17
|
-
import com.google.common.util.concurrent.ListenableFuture
|
|
18
5
|
import com.margelo.nitro.nitroplayer.TrackItem
|
|
19
|
-
import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
|
|
20
6
|
import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
|
|
21
|
-
import com.margelo.nitro.nitroplayer.core.loadPlaylist
|
|
22
7
|
import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
|
|
23
|
-
import kotlinx.coroutines.CoroutineScope
|
|
24
|
-
import kotlinx.coroutines.Dispatchers
|
|
25
|
-
import kotlinx.coroutines.SupervisorJob
|
|
26
|
-
import kotlinx.coroutines.launch
|
|
27
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Thin wrapper around a [MediaSession] owned by the playback service.
|
|
11
|
+
*
|
|
12
|
+
* No longer creates the session, notification channel, or manages service
|
|
13
|
+
* start/stop — the service handles all of that automatically per Media3 docs.
|
|
14
|
+
*/
|
|
28
15
|
class MediaSessionManager(
|
|
29
16
|
private val context: Context,
|
|
30
|
-
|
|
17
|
+
session: MediaSession,
|
|
31
18
|
private val playlistManager: PlaylistManager,
|
|
32
19
|
) {
|
|
33
20
|
private var trackPlayerCore: TrackPlayerCore? = null
|
|
@@ -36,28 +23,16 @@ class MediaSessionManager(
|
|
|
36
23
|
trackPlayerCore = core
|
|
37
24
|
}
|
|
38
25
|
|
|
39
|
-
var mediaSession: MediaSession? =
|
|
26
|
+
var mediaSession: MediaSession? = session
|
|
40
27
|
private set
|
|
41
|
-
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
42
28
|
|
|
43
29
|
@Volatile private var currentTrack: TrackItem? = null
|
|
44
|
-
|
|
45
30
|
@Volatile private var isPlaying: Boolean = false
|
|
46
31
|
|
|
47
32
|
private var androidAutoEnabled: Boolean = false
|
|
48
33
|
private var carPlayEnabled: Boolean = false
|
|
49
34
|
private var showInNotification: Boolean = true
|
|
50
35
|
|
|
51
|
-
companion object {
|
|
52
|
-
private const val CHANNEL_ID = "nitro_player_channel"
|
|
53
|
-
private const val CHANNEL_NAME = "Music Player"
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
init {
|
|
57
|
-
setupMediaSession()
|
|
58
|
-
createNotificationChannel()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
36
|
fun configure(
|
|
62
37
|
androidAutoEnabled: Boolean?,
|
|
63
38
|
carPlayEnabled: Boolean?,
|
|
@@ -65,239 +40,7 @@ class MediaSessionManager(
|
|
|
65
40
|
) {
|
|
66
41
|
androidAutoEnabled?.let { this.androidAutoEnabled = it }
|
|
67
42
|
carPlayEnabled?.let { this.carPlayEnabled = it }
|
|
68
|
-
showInNotification?.let {
|
|
69
|
-
this.showInNotification = it
|
|
70
|
-
if (!it) {
|
|
71
|
-
stopPlaybackService()
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private fun setupMediaSession() {
|
|
77
|
-
try {
|
|
78
|
-
mediaSession =
|
|
79
|
-
MediaSession
|
|
80
|
-
.Builder(context, player)
|
|
81
|
-
.setCallback(
|
|
82
|
-
object : MediaSession.Callback {
|
|
83
|
-
override fun onConnect(
|
|
84
|
-
session: MediaSession,
|
|
85
|
-
controller: MediaSession.ControllerInfo,
|
|
86
|
-
): MediaSession.ConnectionResult {
|
|
87
|
-
// Accept all connections with default commands
|
|
88
|
-
// Media3 automatically handles play, pause, skip, etc. through the player
|
|
89
|
-
return MediaSession.ConnectionResult
|
|
90
|
-
.AcceptedResultBuilder(session)
|
|
91
|
-
.setAvailableSessionCommands(
|
|
92
|
-
MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS,
|
|
93
|
-
).setAvailablePlayerCommands(
|
|
94
|
-
MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS,
|
|
95
|
-
).build()
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
override fun onAddMediaItems(
|
|
99
|
-
mediaSession: MediaSession,
|
|
100
|
-
controller: MediaSession.ControllerInfo,
|
|
101
|
-
mediaItems: MutableList<MediaItem>,
|
|
102
|
-
): ListenableFuture<MutableList<MediaItem>> {
|
|
103
|
-
// This is called when Android Auto requests to play a track
|
|
104
|
-
NitroPlayerLogger.log("MediaSessionManager") { "🎵 MediaSessionManager: onAddMediaItems called with ${mediaItems.size} items" }
|
|
105
|
-
|
|
106
|
-
if (mediaItems.isEmpty()) {
|
|
107
|
-
return Futures.immediateFuture(mutableListOf())
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
val updatedMediaItems = mutableListOf<MediaItem>()
|
|
111
|
-
|
|
112
|
-
for (requestedMediaItem in mediaItems) {
|
|
113
|
-
// Get the mediaId from requestMetadata or mediaId
|
|
114
|
-
val mediaId =
|
|
115
|
-
requestedMediaItem.requestMetadata.mediaUri?.toString()
|
|
116
|
-
?: requestedMediaItem.mediaId
|
|
117
|
-
|
|
118
|
-
NitroPlayerLogger.log("MediaSessionManager") { "🎵 MediaSessionManager: Processing mediaId: $mediaId" }
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
// Parse mediaId format: "playlistId:trackId"
|
|
122
|
-
if (mediaId.contains(':')) {
|
|
123
|
-
val colonIndex = mediaId.indexOf(':')
|
|
124
|
-
val playlistId = mediaId.substring(0, colonIndex)
|
|
125
|
-
val trackId = mediaId.substring(colonIndex + 1)
|
|
126
|
-
|
|
127
|
-
NitroPlayerLogger.log("MediaSessionManager") { "🎵 MediaSessionManager: Parsed playlistId: $playlistId, trackId: $trackId" }
|
|
128
|
-
|
|
129
|
-
// Get the playlist and track
|
|
130
|
-
val playlist = playlistManager.getPlaylist(playlistId)
|
|
131
|
-
if (playlist != null) {
|
|
132
|
-
val track = playlist.tracks.find { it.id == trackId }
|
|
133
|
-
if (track != null) {
|
|
134
|
-
// Create a proper MediaItem with all metadata
|
|
135
|
-
val resolvedMediaItem = createMediaItem(track, mediaId)
|
|
136
|
-
updatedMediaItems.add(resolvedMediaItem)
|
|
137
|
-
NitroPlayerLogger.log("MediaSessionManager") { "✅ MediaSessionManager: Resolved track: ${track.title}" }
|
|
138
|
-
} else {
|
|
139
|
-
NitroPlayerLogger.log("MediaSessionManager") { "⚠️ MediaSessionManager: Track $trackId not found in playlist" }
|
|
140
|
-
updatedMediaItems.add(requestedMediaItem)
|
|
141
|
-
}
|
|
142
|
-
} else {
|
|
143
|
-
NitroPlayerLogger.log("MediaSessionManager") { "⚠️ MediaSessionManager: Playlist $playlistId not found" }
|
|
144
|
-
updatedMediaItems.add(requestedMediaItem)
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
NitroPlayerLogger.log("MediaSessionManager") { "⚠️ MediaSessionManager: Invalid mediaId format: $mediaId" }
|
|
148
|
-
updatedMediaItems.add(requestedMediaItem)
|
|
149
|
-
}
|
|
150
|
-
} catch (e: Exception) {
|
|
151
|
-
NitroPlayerLogger.log("MediaSessionManager") { "❌ MediaSessionManager: Error processing mediaId - ${e.message}" }
|
|
152
|
-
e.printStackTrace()
|
|
153
|
-
updatedMediaItems.add(requestedMediaItem)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
NitroPlayerLogger.log("MediaSessionManager") { "🎵 MediaSessionManager: Returning ${updatedMediaItems.size} resolved media items" }
|
|
158
|
-
return Futures.immediateFuture(updatedMediaItems)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
override fun onSetMediaItems(
|
|
162
|
-
mediaSession: MediaSession,
|
|
163
|
-
controller: MediaSession.ControllerInfo,
|
|
164
|
-
mediaItems: MutableList<MediaItem>,
|
|
165
|
-
startIndex: Int,
|
|
166
|
-
startPositionMs: Long,
|
|
167
|
-
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
|
|
168
|
-
// This is called when Android Auto wants to set and play media items
|
|
169
|
-
NitroPlayerLogger.log("MediaSessionManager") { "🎵 MediaSessionManager: onSetMediaItems called with ${mediaItems.size} items, startIndex: $startIndex" }
|
|
170
|
-
|
|
171
|
-
if (mediaItems.isEmpty()) {
|
|
172
|
-
return Futures.immediateFuture(
|
|
173
|
-
MediaSession.MediaItemsWithStartPosition(
|
|
174
|
-
mutableListOf(),
|
|
175
|
-
0,
|
|
176
|
-
0,
|
|
177
|
-
),
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
// Get the first item's mediaId to determine the playlist
|
|
183
|
-
val firstMediaId = mediaItems[0].mediaId
|
|
184
|
-
NitroPlayerLogger.log("MediaSessionManager") { "🎵 MediaSessionManager: First mediaId: $firstMediaId" }
|
|
185
|
-
|
|
186
|
-
// Parse mediaId format: "playlistId:trackId"
|
|
187
|
-
if (firstMediaId.contains(':')) {
|
|
188
|
-
val colonIndex = firstMediaId.indexOf(':')
|
|
189
|
-
val playlistId = firstMediaId.substring(0, colonIndex)
|
|
190
|
-
val trackId = firstMediaId.substring(colonIndex + 1)
|
|
191
|
-
|
|
192
|
-
NitroPlayerLogger.log("MediaSessionManager") { "🎵 MediaSessionManager: Loading full playlist: $playlistId, starting at track: $trackId" }
|
|
193
|
-
|
|
194
|
-
// Get the full playlist
|
|
195
|
-
val playlist = playlistManager.getPlaylist(playlistId)
|
|
196
|
-
if (playlist != null) {
|
|
197
|
-
// Find the track index in the full playlist
|
|
198
|
-
val trackIndex = playlist.tracks.indexOfFirst { it.id == trackId }
|
|
199
|
-
|
|
200
|
-
if (trackIndex >= 0) {
|
|
201
|
-
// Load the entire playlist into TrackPlayerCore
|
|
202
|
-
trackPlayerCore?.let { core -> scope.launch { core.loadPlaylist(playlistId) } }
|
|
203
|
-
|
|
204
|
-
// Create MediaItems for the entire playlist
|
|
205
|
-
val playlistMediaItems =
|
|
206
|
-
playlist.tracks
|
|
207
|
-
.map { track ->
|
|
208
|
-
val trackMediaId = "$playlistId:${track.id}"
|
|
209
|
-
createMediaItem(track, trackMediaId)
|
|
210
|
-
}.toMutableList()
|
|
211
|
-
|
|
212
|
-
NitroPlayerLogger.log("MediaSessionManager") { "✅ MediaSessionManager: Loaded ${playlistMediaItems.size} tracks, starting at index $trackIndex" }
|
|
213
|
-
|
|
214
|
-
// Return the full playlist with the correct start index
|
|
215
|
-
return Futures.immediateFuture(
|
|
216
|
-
MediaSession.MediaItemsWithStartPosition(
|
|
217
|
-
playlistMediaItems,
|
|
218
|
-
trackIndex,
|
|
219
|
-
startPositionMs,
|
|
220
|
-
),
|
|
221
|
-
)
|
|
222
|
-
} else {
|
|
223
|
-
NitroPlayerLogger.log("MediaSessionManager", "⚠️ MediaSessionManager: Track not found in playlist")
|
|
224
|
-
}
|
|
225
|
-
} else {
|
|
226
|
-
NitroPlayerLogger.log("MediaSessionManager", "⚠️ MediaSessionManager: Playlist not found")
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
} catch (e: Exception) {
|
|
230
|
-
NitroPlayerLogger.log("MediaSessionManager") { "❌ MediaSessionManager: Error in onSetMediaItems - ${e.message}" }
|
|
231
|
-
e.printStackTrace()
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Fallback: use the provided media items
|
|
235
|
-
NitroPlayerLogger.log("MediaSessionManager", "🎵 MediaSessionManager: Using fallback - provided media items")
|
|
236
|
-
return Futures.immediateFuture(
|
|
237
|
-
MediaSession.MediaItemsWithStartPosition(
|
|
238
|
-
mediaItems,
|
|
239
|
-
startIndex,
|
|
240
|
-
startPositionMs,
|
|
241
|
-
),
|
|
242
|
-
)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
override fun onCustomCommand(
|
|
246
|
-
session: MediaSession,
|
|
247
|
-
controller: MediaSession.ControllerInfo,
|
|
248
|
-
customCommand: SessionCommand,
|
|
249
|
-
args: android.os.Bundle,
|
|
250
|
-
): ListenableFuture<SessionResult> {
|
|
251
|
-
// Handle custom commands if needed
|
|
252
|
-
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
).build()
|
|
256
|
-
|
|
257
|
-
// Wire the session into the PlaybackService so it can be promoted to foreground
|
|
258
|
-
NitroPlayerPlaybackService.mediaSession = mediaSession
|
|
259
|
-
} catch (e: Exception) {
|
|
260
|
-
e.printStackTrace()
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
private fun createNotificationChannel() {
|
|
265
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
266
|
-
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
267
|
-
val channel =
|
|
268
|
-
NotificationChannel(
|
|
269
|
-
CHANNEL_ID,
|
|
270
|
-
CHANNEL_NAME,
|
|
271
|
-
NotificationManager.IMPORTANCE_LOW,
|
|
272
|
-
).apply {
|
|
273
|
-
description = "Media playback controls"
|
|
274
|
-
setShowBadge(false)
|
|
275
|
-
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
276
|
-
}
|
|
277
|
-
manager.createNotificationChannel(channel)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private fun startPlaybackService() {
|
|
282
|
-
try {
|
|
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)
|
|
288
|
-
}
|
|
289
|
-
} catch (e: Exception) {
|
|
290
|
-
NitroPlayerLogger.log("MediaSessionManager") { "Failed to start PlaybackService: ${e.message}" }
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
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
|
-
}
|
|
43
|
+
showInNotification?.let { this.showInNotification = it }
|
|
301
44
|
}
|
|
302
45
|
|
|
303
46
|
fun onTrackChanged(track: TrackItem?) {
|
|
@@ -306,44 +49,10 @@ class MediaSessionManager(
|
|
|
306
49
|
|
|
307
50
|
fun onPlaybackStateChanged(playing: Boolean) {
|
|
308
51
|
isPlaying = playing
|
|
309
|
-
if (playing && showInNotification) {
|
|
310
|
-
startPlaybackService()
|
|
311
|
-
} else if (!playing) {
|
|
312
|
-
stopPlaybackService()
|
|
313
|
-
}
|
|
314
52
|
}
|
|
315
53
|
|
|
316
54
|
fun release() {
|
|
317
|
-
|
|
318
|
-
NitroPlayerPlaybackService.mediaSession = null
|
|
319
|
-
mediaSession?.release()
|
|
55
|
+
// Service owns the session — just null out our reference
|
|
320
56
|
mediaSession = null
|
|
321
57
|
}
|
|
322
|
-
|
|
323
|
-
private fun createMediaItem(
|
|
324
|
-
track: TrackItem,
|
|
325
|
-
mediaId: String,
|
|
326
|
-
): MediaItem {
|
|
327
|
-
val metadataBuilder =
|
|
328
|
-
MediaMetadata
|
|
329
|
-
.Builder()
|
|
330
|
-
.setTitle(track.title)
|
|
331
|
-
.setArtist(track.artist)
|
|
332
|
-
.setAlbumTitle(track.album)
|
|
333
|
-
|
|
334
|
-
track.artwork?.asSecondOrNull()?.let { artworkUrl ->
|
|
335
|
-
try {
|
|
336
|
-
metadataBuilder.setArtworkUri(Uri.parse(artworkUrl))
|
|
337
|
-
} catch (e: Exception) {
|
|
338
|
-
NitroPlayerLogger.log("MediaSessionManager") { "⚠️ MediaSessionManager: Invalid artwork URI: $artworkUrl" }
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return MediaItem
|
|
343
|
-
.Builder()
|
|
344
|
-
.setMediaId(mediaId)
|
|
345
|
-
.setUri(track.url)
|
|
346
|
-
.setMediaMetadata(metadataBuilder.build())
|
|
347
|
-
.build()
|
|
348
|
-
}
|
|
349
58
|
}
|