react-native-nitro-player 0.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/NitroPlayer.podspec +31 -0
- package/README.md +610 -0
- package/android/CMakeLists.txt +29 -0
- package/android/build.gradle +147 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +7 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +29 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +116 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +167 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +93 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/NitroPlayerPackage.kt +21 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +171 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +639 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +352 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibrary.kt +58 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryManager.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryParser.kt +73 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +506 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/Playlist.kt +21 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +454 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/Queue.kt +94 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/QueueManager.kt +143 -0
- package/ios/HybridAudioRoutePicker.swift +53 -0
- package/ios/HybridTrackPlayer.swift +100 -0
- package/ios/core/TrackPlayerCore.swift +1040 -0
- package/ios/media/MediaSessionManager.swift +230 -0
- package/ios/playlist/PlaylistManager.swift +446 -0
- package/ios/playlist/PlaylistModel.swift +49 -0
- package/ios/queue/HybridPlayerQueue.swift +95 -0
- package/ios/queue/Queue.swift +126 -0
- package/ios/queue/QueueManager.swift +157 -0
- package/lib/hooks/index.d.ts +6 -0
- package/lib/hooks/index.js +6 -0
- package/lib/hooks/useAndroidAutoConnection.d.ts +13 -0
- package/lib/hooks/useAndroidAutoConnection.js +26 -0
- package/lib/hooks/useAudioDevices.d.ts +26 -0
- package/lib/hooks/useAudioDevices.js +55 -0
- package/lib/hooks/useOnChangeTrack.d.ts +9 -0
- package/lib/hooks/useOnChangeTrack.js +17 -0
- package/lib/hooks/useOnPlaybackProgressChange.d.ts +9 -0
- package/lib/hooks/useOnPlaybackProgressChange.js +19 -0
- package/lib/hooks/useOnPlaybackStateChange.d.ts +9 -0
- package/lib/hooks/useOnPlaybackStateChange.js +17 -0
- package/lib/hooks/useOnSeek.d.ts +8 -0
- package/lib/hooks/useOnSeek.js +17 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.js +24 -0
- package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +21 -0
- package/lib/specs/AndroidAutoMediaLibrary.nitro.js +1 -0
- package/lib/specs/AudioDevices.nitro.d.ts +24 -0
- package/lib/specs/AudioDevices.nitro.js +1 -0
- package/lib/specs/AudioRoutePicker.nitro.d.ts +10 -0
- package/lib/specs/AudioRoutePicker.nitro.js +1 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +39 -0
- package/lib/specs/TrackPlayer.nitro.js +1 -0
- package/lib/types/AndroidAutoMediaLibrary.d.ts +44 -0
- package/lib/types/AndroidAutoMediaLibrary.js +1 -0
- package/lib/types/PlayerQueue.d.ts +32 -0
- package/lib/types/PlayerQueue.js +1 -0
- package/lib/utils/androidAutoMediaLibrary.d.ts +47 -0
- package/lib/utils/androidAutoMediaLibrary.js +62 -0
- package/nitro.json +31 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +91 -0
- package/nitrogen/generated/android/NitroPlayer+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +88 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.hpp +25 -0
- package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +85 -0
- package/nitrogen/generated/android/c++/JFunc_void_TrackPlayerState_std__optional_Reason_.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double_std__optional_bool_.hpp +76 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +88 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +106 -0
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +55 -0
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +70 -0
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +143 -0
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +137 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +78 -0
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +65 -0
- package/nitrogen/generated/android/c++/JPlayerState.hpp +87 -0
- package/nitrogen/generated/android/c++/JPlaylist.hpp +99 -0
- package/nitrogen/generated/android/c++/JQueueOperation.hpp +65 -0
- package/nitrogen/generated/android/c++/JReason.hpp +65 -0
- package/nitrogen/generated/android/c++/JTAudioDevice.hpp +69 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +86 -0
- package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +62 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +77 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_String.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +70 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +74 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TrackItem_std__optional_Reason_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TrackPlayerState_std__optional_Reason_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_bool.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_double_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_double_double_std__optional_bool_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_Playlist_std__optional_QueueOperation_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_Playlist__std__optional_QueueOperation_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +116 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +134 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/NitroPlayerOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Playlist.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/QueueOperation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Reason.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TAudioDevice.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Playlist.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_String.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_TrackItem.kt +59 -0
- package/nitrogen/generated/ios/NitroPlayer+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +123 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +531 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +80 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +49 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +55 -0
- package/nitrogen/generated/ios/c++/HybridAudioRoutePickerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridAudioRoutePickerSpecSwift.hpp +74 -0
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +167 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +174 -0
- package/nitrogen/generated/ios/swift/Func_void_TrackItem_std__optional_Reason_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_TrackPlayerState_std__optional_Reason_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double_std__optional_bool_.swift +54 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_Playlist_std__optional_QueueOperation_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_Playlist__std__optional_QueueOperation_.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridAudioRoutePickerSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridAudioRoutePickerSpec_cxx.swift +130 -0
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +68 -0
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +349 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +69 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +325 -0
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +115 -0
- package/nitrogen/generated/ios/swift/PlayerState.swift +181 -0
- package/nitrogen/generated/ios/swift/Playlist.swift +182 -0
- package/nitrogen/generated/ios/swift/QueueOperation.swift +48 -0
- package/nitrogen/generated/ios/swift/Reason.swift +48 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +147 -0
- package/nitrogen/generated/ios/swift/TrackPlayerState.swift +44 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_Playlist.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_String.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_TrackItem.swift +18 -0
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +63 -0
- package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +65 -0
- package/nitrogen/generated/shared/c++/HybridAudioRoutePickerSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridAudioRoutePickerSpec.hpp +62 -0
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.cpp +33 -0
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +87 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +34 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +91 -0
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +83 -0
- package/nitrogen/generated/shared/c++/PlayerState.hpp +103 -0
- package/nitrogen/generated/shared/c++/Playlist.hpp +97 -0
- package/nitrogen/generated/shared/c++/QueueOperation.hpp +84 -0
- package/nitrogen/generated/shared/c++/Reason.hpp +84 -0
- package/nitrogen/generated/shared/c++/TAudioDevice.hpp +87 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +102 -0
- package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +80 -0
- package/package.json +172 -0
- package/react-native.config.js +16 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useAndroidAutoConnection.ts +30 -0
- package/src/hooks/useAudioDevices.ts +64 -0
- package/src/hooks/useOnChangeTrack.ts +24 -0
- package/src/hooks/useOnPlaybackProgressChange.ts +30 -0
- package/src/hooks/useOnPlaybackStateChange.ts +24 -0
- package/src/hooks/useOnSeek.ts +25 -0
- package/src/index.ts +47 -0
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +22 -0
- package/src/specs/AudioDevices.nitro.ts +25 -0
- package/src/specs/AudioRoutePicker.nitro.ts +9 -0
- package/src/specs/TrackPlayer.nitro.ts +81 -0
- package/src/types/AndroidAutoMediaLibrary.ts +58 -0
- package/src/types/PlayerQueue.ts +38 -0
- package/src/utils/androidAutoMediaLibrary.ts +66 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroplayer.media
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.PendingIntent
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.graphics.Bitmap
|
|
10
|
+
import android.graphics.BitmapFactory
|
|
11
|
+
import android.net.Uri
|
|
12
|
+
import android.os.Build
|
|
13
|
+
import androidx.core.app.NotificationCompat
|
|
14
|
+
import androidx.media3.common.MediaItem
|
|
15
|
+
import androidx.media3.common.MediaMetadata
|
|
16
|
+
import androidx.media3.common.Player
|
|
17
|
+
import androidx.media3.exoplayer.ExoPlayer
|
|
18
|
+
import androidx.media3.session.MediaSession
|
|
19
|
+
import androidx.media3.session.MediaSessionService
|
|
20
|
+
import androidx.media3.session.SessionCommand
|
|
21
|
+
import androidx.media3.session.SessionResult
|
|
22
|
+
import com.google.common.util.concurrent.Futures
|
|
23
|
+
import com.google.common.util.concurrent.ListenableFuture
|
|
24
|
+
import com.margelo.nitro.nitroplayer.TrackItem
|
|
25
|
+
import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
|
|
26
|
+
import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
27
|
+
import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
|
|
28
|
+
import kotlinx.coroutines.CoroutineScope
|
|
29
|
+
import kotlinx.coroutines.Dispatchers
|
|
30
|
+
import kotlinx.coroutines.SupervisorJob
|
|
31
|
+
import kotlinx.coroutines.launch
|
|
32
|
+
import kotlinx.coroutines.withContext
|
|
33
|
+
import java.net.URL
|
|
34
|
+
|
|
35
|
+
class MediaSessionManager(
|
|
36
|
+
private val context: Context,
|
|
37
|
+
private val player: ExoPlayer,
|
|
38
|
+
private val playlistManager: PlaylistManager,
|
|
39
|
+
) {
|
|
40
|
+
private var trackPlayerCore: TrackPlayerCore? = null
|
|
41
|
+
|
|
42
|
+
fun setTrackPlayerCore(core: TrackPlayerCore) {
|
|
43
|
+
trackPlayerCore = core
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var mediaSession: MediaSession? = null // Make public so MediaBrowserService can access it
|
|
47
|
+
private set
|
|
48
|
+
private var notificationManager: NotificationManager? = null
|
|
49
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
50
|
+
private val artworkCache = mutableMapOf<String, Bitmap>()
|
|
51
|
+
|
|
52
|
+
private var androidAutoEnabled: Boolean = false
|
|
53
|
+
private var carPlayEnabled: Boolean = false
|
|
54
|
+
private var showInNotification: Boolean = true
|
|
55
|
+
|
|
56
|
+
companion object {
|
|
57
|
+
private const val NOTIFICATION_ID = 1001
|
|
58
|
+
private const val CHANNEL_ID = "nitro_player_channel"
|
|
59
|
+
private const val CHANNEL_NAME = "Music Player"
|
|
60
|
+
const val ACTION_PLAY = "com.margelo.nitro.nitroplayer.PLAY"
|
|
61
|
+
const val ACTION_PAUSE = "com.margelo.nitro.nitroplayer.PAUSE"
|
|
62
|
+
const val ACTION_NEXT = "com.margelo.nitro.nitroplayer.NEXT"
|
|
63
|
+
const val ACTION_PREVIOUS = "com.margelo.nitro.nitroplayer.PREVIOUS"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
init {
|
|
67
|
+
setupMediaSession()
|
|
68
|
+
createNotificationChannel()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fun configure(
|
|
72
|
+
androidAutoEnabled: Boolean?,
|
|
73
|
+
carPlayEnabled: Boolean?,
|
|
74
|
+
showInNotification: Boolean?,
|
|
75
|
+
) {
|
|
76
|
+
androidAutoEnabled?.let { this.androidAutoEnabled = it }
|
|
77
|
+
carPlayEnabled?.let { this.carPlayEnabled = it }
|
|
78
|
+
showInNotification?.let {
|
|
79
|
+
this.showInNotification = it
|
|
80
|
+
if (it) {
|
|
81
|
+
updateNotification()
|
|
82
|
+
} else {
|
|
83
|
+
hideNotification()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private fun setupMediaSession() {
|
|
89
|
+
try {
|
|
90
|
+
mediaSession =
|
|
91
|
+
MediaSession
|
|
92
|
+
.Builder(context, player)
|
|
93
|
+
.setCallback(
|
|
94
|
+
object : MediaSession.Callback {
|
|
95
|
+
override fun onConnect(
|
|
96
|
+
session: MediaSession,
|
|
97
|
+
controller: MediaSession.ControllerInfo,
|
|
98
|
+
): MediaSession.ConnectionResult {
|
|
99
|
+
// Accept all connections with default commands
|
|
100
|
+
// Media3 automatically handles play, pause, skip, etc. through the player
|
|
101
|
+
return MediaSession.ConnectionResult
|
|
102
|
+
.AcceptedResultBuilder(session)
|
|
103
|
+
.setAvailableSessionCommands(
|
|
104
|
+
MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS,
|
|
105
|
+
).setAvailablePlayerCommands(
|
|
106
|
+
MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS,
|
|
107
|
+
).build()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
override fun onAddMediaItems(
|
|
111
|
+
mediaSession: MediaSession,
|
|
112
|
+
controller: MediaSession.ControllerInfo,
|
|
113
|
+
mediaItems: MutableList<MediaItem>,
|
|
114
|
+
): ListenableFuture<MutableList<MediaItem>> {
|
|
115
|
+
// This is called when Android Auto requests to play a track
|
|
116
|
+
println("🎵 MediaSessionManager: onAddMediaItems called with ${mediaItems.size} items")
|
|
117
|
+
|
|
118
|
+
if (mediaItems.isEmpty()) {
|
|
119
|
+
return Futures.immediateFuture(mutableListOf())
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
val updatedMediaItems = mutableListOf<MediaItem>()
|
|
123
|
+
|
|
124
|
+
for (requestedMediaItem in mediaItems) {
|
|
125
|
+
// Get the mediaId from requestMetadata or mediaId
|
|
126
|
+
val mediaId =
|
|
127
|
+
requestedMediaItem.requestMetadata.mediaUri?.toString()
|
|
128
|
+
?: requestedMediaItem.mediaId
|
|
129
|
+
|
|
130
|
+
println("🎵 MediaSessionManager: Processing mediaId: $mediaId")
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
// Parse mediaId format: "playlistId:trackId"
|
|
134
|
+
if (mediaId.contains(':')) {
|
|
135
|
+
val colonIndex = mediaId.indexOf(':')
|
|
136
|
+
val playlistId = mediaId.substring(0, colonIndex)
|
|
137
|
+
val trackId = mediaId.substring(colonIndex + 1)
|
|
138
|
+
|
|
139
|
+
println("🎵 MediaSessionManager: Parsed playlistId: $playlistId, trackId: $trackId")
|
|
140
|
+
|
|
141
|
+
// Get the playlist and track
|
|
142
|
+
val playlist = playlistManager.getPlaylist(playlistId)
|
|
143
|
+
if (playlist != null) {
|
|
144
|
+
val track = playlist.tracks.find { it.id == trackId }
|
|
145
|
+
if (track != null) {
|
|
146
|
+
// Create a proper MediaItem with all metadata
|
|
147
|
+
val resolvedMediaItem = createMediaItem(track, mediaId)
|
|
148
|
+
updatedMediaItems.add(resolvedMediaItem)
|
|
149
|
+
println("✅ MediaSessionManager: Resolved track: ${track.title}")
|
|
150
|
+
} else {
|
|
151
|
+
println("⚠️ MediaSessionManager: Track $trackId not found in playlist")
|
|
152
|
+
updatedMediaItems.add(requestedMediaItem)
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
println("⚠️ MediaSessionManager: Playlist $playlistId not found")
|
|
156
|
+
updatedMediaItems.add(requestedMediaItem)
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
println("⚠️ MediaSessionManager: Invalid mediaId format: $mediaId")
|
|
160
|
+
updatedMediaItems.add(requestedMediaItem)
|
|
161
|
+
}
|
|
162
|
+
} catch (e: Exception) {
|
|
163
|
+
println("❌ MediaSessionManager: Error processing mediaId - ${e.message}")
|
|
164
|
+
e.printStackTrace()
|
|
165
|
+
updatedMediaItems.add(requestedMediaItem)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
println("🎵 MediaSessionManager: Returning ${updatedMediaItems.size} resolved media items")
|
|
170
|
+
return Futures.immediateFuture(updatedMediaItems)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
override fun onSetMediaItems(
|
|
174
|
+
mediaSession: MediaSession,
|
|
175
|
+
controller: MediaSession.ControllerInfo,
|
|
176
|
+
mediaItems: MutableList<MediaItem>,
|
|
177
|
+
startIndex: Int,
|
|
178
|
+
startPositionMs: Long,
|
|
179
|
+
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
|
|
180
|
+
// This is called when Android Auto wants to set and play media items
|
|
181
|
+
println("🎵 MediaSessionManager: onSetMediaItems called with ${mediaItems.size} items, startIndex: $startIndex")
|
|
182
|
+
|
|
183
|
+
if (mediaItems.isEmpty()) {
|
|
184
|
+
return Futures.immediateFuture(
|
|
185
|
+
MediaSession.MediaItemsWithStartPosition(
|
|
186
|
+
mutableListOf(),
|
|
187
|
+
0,
|
|
188
|
+
0,
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Get the first item's mediaId to determine the playlist
|
|
195
|
+
val firstMediaId = mediaItems[0].mediaId
|
|
196
|
+
println("🎵 MediaSessionManager: First mediaId: $firstMediaId")
|
|
197
|
+
|
|
198
|
+
// Parse mediaId format: "playlistId:trackId"
|
|
199
|
+
if (firstMediaId.contains(':')) {
|
|
200
|
+
val colonIndex = firstMediaId.indexOf(':')
|
|
201
|
+
val playlistId = firstMediaId.substring(0, colonIndex)
|
|
202
|
+
val trackId = firstMediaId.substring(colonIndex + 1)
|
|
203
|
+
|
|
204
|
+
println("🎵 MediaSessionManager: Loading full playlist: $playlistId, starting at track: $trackId")
|
|
205
|
+
|
|
206
|
+
// Get the full playlist
|
|
207
|
+
val playlist = playlistManager.getPlaylist(playlistId)
|
|
208
|
+
if (playlist != null) {
|
|
209
|
+
// Find the track index in the full playlist
|
|
210
|
+
val trackIndex = playlist.tracks.indexOfFirst { it.id == trackId }
|
|
211
|
+
|
|
212
|
+
if (trackIndex >= 0) {
|
|
213
|
+
// Load the entire playlist into TrackPlayerCore
|
|
214
|
+
trackPlayerCore?.loadPlaylist(playlistId)
|
|
215
|
+
|
|
216
|
+
// Create MediaItems for the entire playlist
|
|
217
|
+
val playlistMediaItems =
|
|
218
|
+
playlist.tracks
|
|
219
|
+
.map { track ->
|
|
220
|
+
val trackMediaId = "$playlistId:${track.id}"
|
|
221
|
+
createMediaItem(track, trackMediaId)
|
|
222
|
+
}.toMutableList()
|
|
223
|
+
|
|
224
|
+
println("✅ MediaSessionManager: Loaded ${playlistMediaItems.size} tracks, starting at index $trackIndex")
|
|
225
|
+
|
|
226
|
+
// Return the full playlist with the correct start index
|
|
227
|
+
return Futures.immediateFuture(
|
|
228
|
+
MediaSession.MediaItemsWithStartPosition(
|
|
229
|
+
playlistMediaItems,
|
|
230
|
+
trackIndex,
|
|
231
|
+
startPositionMs,
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
} else {
|
|
235
|
+
println("⚠️ MediaSessionManager: Track not found in playlist")
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
println("⚠️ MediaSessionManager: Playlist not found")
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch (e: Exception) {
|
|
242
|
+
println("❌ MediaSessionManager: Error in onSetMediaItems - ${e.message}")
|
|
243
|
+
e.printStackTrace()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Fallback: use the provided media items
|
|
247
|
+
println("🎵 MediaSessionManager: Using fallback - provided media items")
|
|
248
|
+
return Futures.immediateFuture(
|
|
249
|
+
MediaSession.MediaItemsWithStartPosition(
|
|
250
|
+
mediaItems,
|
|
251
|
+
startIndex,
|
|
252
|
+
startPositionMs,
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
override fun onCustomCommand(
|
|
258
|
+
session: MediaSession,
|
|
259
|
+
controller: MediaSession.ControllerInfo,
|
|
260
|
+
customCommand: SessionCommand,
|
|
261
|
+
args: android.os.Bundle,
|
|
262
|
+
): ListenableFuture<SessionResult> {
|
|
263
|
+
// Handle custom commands if needed
|
|
264
|
+
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
).build()
|
|
268
|
+
// MediaSession is active by default in Media3
|
|
269
|
+
updateMediaSessionMetadata()
|
|
270
|
+
} catch (e: Exception) {
|
|
271
|
+
e.printStackTrace()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private fun updateMediaSessionMetadata() {
|
|
276
|
+
// MediaSession will automatically use the metadata from player's current MediaItem
|
|
277
|
+
// No need to manually update here as TrackPlayerCore already sets metadata
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private suspend fun loadArtworkBitmap(artworkUrl: String?): Bitmap? {
|
|
281
|
+
if (artworkUrl.isNullOrEmpty()) return null
|
|
282
|
+
|
|
283
|
+
// Check cache first
|
|
284
|
+
artworkCache[artworkUrl]?.let { return it }
|
|
285
|
+
|
|
286
|
+
return try {
|
|
287
|
+
val bitmap =
|
|
288
|
+
withContext(Dispatchers.IO) {
|
|
289
|
+
val url = URL(artworkUrl)
|
|
290
|
+
BitmapFactory.decodeStream(url.openConnection().getInputStream())
|
|
291
|
+
}
|
|
292
|
+
// Cache the bitmap
|
|
293
|
+
if (bitmap != null) {
|
|
294
|
+
artworkCache[artworkUrl] = bitmap
|
|
295
|
+
}
|
|
296
|
+
bitmap
|
|
297
|
+
} catch (e: Exception) {
|
|
298
|
+
e.printStackTrace()
|
|
299
|
+
null
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private fun bitmapToByteArray(bitmap: Bitmap): ByteArray {
|
|
304
|
+
val stream = java.io.ByteArrayOutputStream()
|
|
305
|
+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
|
306
|
+
return stream.toByteArray()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private fun createNotificationChannel() {
|
|
310
|
+
notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
311
|
+
|
|
312
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
313
|
+
val channel =
|
|
314
|
+
NotificationChannel(
|
|
315
|
+
CHANNEL_ID,
|
|
316
|
+
CHANNEL_NAME,
|
|
317
|
+
NotificationManager.IMPORTANCE_LOW,
|
|
318
|
+
).apply {
|
|
319
|
+
description = "Media playback controls"
|
|
320
|
+
setShowBadge(false)
|
|
321
|
+
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
notificationManager?.createNotificationChannel(channel)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private fun getCurrentTrack(): TrackItem? {
|
|
329
|
+
val currentMediaItem = player.currentMediaItem ?: return null
|
|
330
|
+
val mediaId = currentMediaItem.mediaId
|
|
331
|
+
|
|
332
|
+
// Parse mediaId format: "playlistId:trackId" or just "trackId"
|
|
333
|
+
val trackId =
|
|
334
|
+
if (mediaId.contains(':')) {
|
|
335
|
+
mediaId.substring(mediaId.indexOf(':') + 1)
|
|
336
|
+
} else {
|
|
337
|
+
mediaId
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Find track in current playlist or all playlists
|
|
341
|
+
return trackPlayerCore?.getCurrentPlaylistId()?.let { playlistId ->
|
|
342
|
+
playlistManager.getPlaylist(playlistId)?.tracks?.find { it.id == trackId }
|
|
343
|
+
} ?: playlistManager
|
|
344
|
+
.getAllPlaylists()
|
|
345
|
+
.flatMap { it.tracks }
|
|
346
|
+
.find { it.id == trackId }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private fun updateNotification() {
|
|
350
|
+
if (!showInNotification) return
|
|
351
|
+
|
|
352
|
+
val currentTrack = getCurrentTrack()
|
|
353
|
+
val notification = buildNotification(currentTrack)
|
|
354
|
+
notificationManager?.notify(NOTIFICATION_ID, notification)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private fun buildNotification(track: TrackItem?): Notification {
|
|
358
|
+
val mediaSession = this.mediaSession ?: return createEmptyNotification()
|
|
359
|
+
|
|
360
|
+
// Launch intent
|
|
361
|
+
val contentIntent =
|
|
362
|
+
PendingIntent.getActivity(
|
|
363
|
+
context,
|
|
364
|
+
0,
|
|
365
|
+
context.packageManager.getLaunchIntentForPackage(context.packageName),
|
|
366
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
val builder =
|
|
370
|
+
NotificationCompat
|
|
371
|
+
.Builder(context, CHANNEL_ID)
|
|
372
|
+
.setContentTitle(track?.title ?: "Unknown Title")
|
|
373
|
+
.setContentText(track?.artist ?: "Unknown Artist")
|
|
374
|
+
.setSubText(track?.album ?: "")
|
|
375
|
+
.setSmallIcon(android.R.drawable.ic_media_play)
|
|
376
|
+
.setContentIntent(contentIntent)
|
|
377
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
378
|
+
.setOngoing(player.isPlaying)
|
|
379
|
+
.setShowWhen(false)
|
|
380
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
381
|
+
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
|
382
|
+
.setStyle(
|
|
383
|
+
androidx.media.app.NotificationCompat
|
|
384
|
+
.MediaStyle()
|
|
385
|
+
.setMediaSession(mediaSession.sessionCompatToken)
|
|
386
|
+
.setShowActionsInCompactView(0, 1, 2),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
// Add action buttons
|
|
390
|
+
builder.addAction(
|
|
391
|
+
android.R.drawable.ic_media_previous,
|
|
392
|
+
"Previous",
|
|
393
|
+
createMediaAction(ACTION_PREVIOUS),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
if (player.isPlaying) {
|
|
397
|
+
builder.addAction(
|
|
398
|
+
android.R.drawable.ic_media_pause,
|
|
399
|
+
"Pause",
|
|
400
|
+
createMediaAction(ACTION_PAUSE),
|
|
401
|
+
)
|
|
402
|
+
} else {
|
|
403
|
+
builder.addAction(
|
|
404
|
+
android.R.drawable.ic_media_play,
|
|
405
|
+
"Play",
|
|
406
|
+
createMediaAction(ACTION_PLAY),
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
builder.addAction(
|
|
411
|
+
android.R.drawable.ic_media_next,
|
|
412
|
+
"Next",
|
|
413
|
+
createMediaAction(ACTION_NEXT),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
// Load artwork asynchronously and update notification
|
|
417
|
+
track?.artwork?.asSecondOrNull()?.let { artworkUrl ->
|
|
418
|
+
scope.launch {
|
|
419
|
+
val bitmap = loadArtworkBitmap(artworkUrl)
|
|
420
|
+
if (bitmap != null) {
|
|
421
|
+
builder.setLargeIcon(bitmap)
|
|
422
|
+
notificationManager?.notify(NOTIFICATION_ID, builder.build())
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return builder.build()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private fun createEmptyNotification(): Notification =
|
|
431
|
+
NotificationCompat
|
|
432
|
+
.Builder(context, CHANNEL_ID)
|
|
433
|
+
.setContentTitle("Music Player")
|
|
434
|
+
.setSmallIcon(android.R.drawable.ic_media_play)
|
|
435
|
+
.build()
|
|
436
|
+
|
|
437
|
+
private fun createMediaAction(action: String): PendingIntent {
|
|
438
|
+
val intent =
|
|
439
|
+
Intent(action).apply {
|
|
440
|
+
setPackage(context.packageName)
|
|
441
|
+
}
|
|
442
|
+
return PendingIntent.getBroadcast(
|
|
443
|
+
context,
|
|
444
|
+
0,
|
|
445
|
+
intent,
|
|
446
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private fun hideNotification() {
|
|
451
|
+
notificationManager?.cancel(NOTIFICATION_ID)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
fun onTrackChanged() {
|
|
455
|
+
// Preload artwork for better notification display
|
|
456
|
+
val currentTrack = getCurrentTrack()
|
|
457
|
+
if (currentTrack != null) {
|
|
458
|
+
scope.launch {
|
|
459
|
+
currentTrack.artwork?.asSecondOrNull()?.let { artworkUrl ->
|
|
460
|
+
loadArtworkBitmap(artworkUrl)
|
|
461
|
+
}
|
|
462
|
+
updateNotification()
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
updateNotification()
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
fun onPlaybackStateChanged() {
|
|
470
|
+
updateNotification()
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
fun release() {
|
|
474
|
+
hideNotification()
|
|
475
|
+
mediaSession?.release()
|
|
476
|
+
mediaSession = null
|
|
477
|
+
artworkCache.clear()
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private fun createMediaItem(
|
|
481
|
+
track: TrackItem,
|
|
482
|
+
mediaId: String,
|
|
483
|
+
): MediaItem {
|
|
484
|
+
val metadataBuilder =
|
|
485
|
+
MediaMetadata
|
|
486
|
+
.Builder()
|
|
487
|
+
.setTitle(track.title)
|
|
488
|
+
.setArtist(track.artist)
|
|
489
|
+
.setAlbumTitle(track.album)
|
|
490
|
+
|
|
491
|
+
track.artwork?.asSecondOrNull()?.let { artworkUrl ->
|
|
492
|
+
try {
|
|
493
|
+
metadataBuilder.setArtworkUri(Uri.parse(artworkUrl))
|
|
494
|
+
} catch (e: Exception) {
|
|
495
|
+
println("⚠️ MediaSessionManager: Invalid artwork URI: $artworkUrl")
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return MediaItem
|
|
500
|
+
.Builder()
|
|
501
|
+
.setMediaId(mediaId)
|
|
502
|
+
.setUri(track.url)
|
|
503
|
+
.setMediaMetadata(metadataBuilder.build())
|
|
504
|
+
.build()
|
|
505
|
+
}
|
|
506
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroplayer.playlist
|
|
2
|
+
|
|
3
|
+
import com.margelo.nitro.nitroplayer.TrackItem
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents a playlist containing multiple tracks
|
|
7
|
+
* Uses ExoPlayer's native playlist functionality
|
|
8
|
+
*/
|
|
9
|
+
data class Playlist(
|
|
10
|
+
val id: String,
|
|
11
|
+
val name: String,
|
|
12
|
+
val description: String? = null,
|
|
13
|
+
val artwork: String? = null,
|
|
14
|
+
val tracks: MutableList<TrackItem> = mutableListOf(),
|
|
15
|
+
) {
|
|
16
|
+
fun toTrackItemArray(): Array<TrackItem> = tracks.toTypedArray()
|
|
17
|
+
|
|
18
|
+
fun getTrackCount(): Int = tracks.size
|
|
19
|
+
|
|
20
|
+
fun isEmpty(): Boolean = tracks.isEmpty()
|
|
21
|
+
}
|