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,352 @@
|
|
|
1
|
+
@file:Suppress("ktlint:standard:filename")
|
|
2
|
+
|
|
3
|
+
package com.margelo.nitro.nitroplayer.media
|
|
4
|
+
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.os.Bundle
|
|
8
|
+
import android.support.v4.media.MediaBrowserCompat
|
|
9
|
+
import android.support.v4.media.MediaDescriptionCompat
|
|
10
|
+
import androidx.media.MediaBrowserServiceCompat
|
|
11
|
+
import androidx.media.utils.MediaConstants
|
|
12
|
+
import com.margelo.nitro.nitroplayer.TrackItem
|
|
13
|
+
import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
|
|
14
|
+
import com.margelo.nitro.nitroplayer.playlist.Playlist
|
|
15
|
+
import kotlinx.coroutines.CoroutineScope
|
|
16
|
+
import kotlinx.coroutines.Dispatchers
|
|
17
|
+
import kotlinx.coroutines.SupervisorJob
|
|
18
|
+
import kotlinx.coroutines.cancel
|
|
19
|
+
import kotlinx.coroutines.launch
|
|
20
|
+
|
|
21
|
+
class NitroPlayerMediaBrowserService : MediaBrowserServiceCompat() {
|
|
22
|
+
companion object {
|
|
23
|
+
private const val ROOT_ID = "root"
|
|
24
|
+
private const val EMPTY_ROOT_ID = "empty_root"
|
|
25
|
+
private const val PLAYLIST_PREFIX = "playlist_"
|
|
26
|
+
|
|
27
|
+
var trackPlayerCore: TrackPlayerCore? = null
|
|
28
|
+
var mediaSessionManager: MediaSessionManager? = null
|
|
29
|
+
var isAndroidAutoEnabled: Boolean = false
|
|
30
|
+
var isAndroidAutoConnected: Boolean = false
|
|
31
|
+
|
|
32
|
+
@Volatile
|
|
33
|
+
private var instance: NitroPlayerMediaBrowserService? = null
|
|
34
|
+
|
|
35
|
+
fun getInstance(): NitroPlayerMediaBrowserService? = instance
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
39
|
+
private lateinit var mediaLibraryManager: MediaLibraryManager
|
|
40
|
+
|
|
41
|
+
override fun onCreate() {
|
|
42
|
+
super.onCreate()
|
|
43
|
+
|
|
44
|
+
instance = this
|
|
45
|
+
mediaLibraryManager = MediaLibraryManager.getInstance(applicationContext)
|
|
46
|
+
|
|
47
|
+
// Use the existing MediaSession from MediaSessionManager
|
|
48
|
+
// This ensures the session is already connected to the ExoPlayer
|
|
49
|
+
try {
|
|
50
|
+
val session = mediaSessionManager?.mediaSession
|
|
51
|
+
if (session != null) {
|
|
52
|
+
// Convert Media3 MediaSession to MediaSessionCompat for MediaBrowserService
|
|
53
|
+
sessionToken = session.sessionCompatToken
|
|
54
|
+
println("🎵 NitroPlayerMediaBrowserService: MediaSession token set successfully")
|
|
55
|
+
} else {
|
|
56
|
+
println("⚠️ NitroPlayerMediaBrowserService: MediaSession not available yet")
|
|
57
|
+
}
|
|
58
|
+
} catch (e: Exception) {
|
|
59
|
+
println("❌ NitroPlayerMediaBrowserService: Error setting session token - ${e.message}")
|
|
60
|
+
e.printStackTrace()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
println("🚀 NitroPlayerMediaBrowserService: Service created")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override fun onDestroy() {
|
|
67
|
+
super.onDestroy()
|
|
68
|
+
instance = null
|
|
69
|
+
serviceScope.cancel()
|
|
70
|
+
println("🛑 NitroPlayerMediaBrowserService: Service destroyed")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override fun onGetRoot(
|
|
74
|
+
clientPackageName: String,
|
|
75
|
+
clientUid: Int,
|
|
76
|
+
rootHints: Bundle?,
|
|
77
|
+
): BrowserRoot? {
|
|
78
|
+
println("📂 NitroPlayerMediaBrowserService: onGetRoot called from $clientPackageName")
|
|
79
|
+
|
|
80
|
+
// Check if Android Auto is enabled
|
|
81
|
+
if (!isAndroidAutoEnabled) {
|
|
82
|
+
println("⚠️ NitroPlayerMediaBrowserService: Android Auto not enabled")
|
|
83
|
+
return BrowserRoot(EMPTY_ROOT_ID, null)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Allow Android Auto and other media browsers to connect
|
|
87
|
+
// Enable grid layout for playlists at root level
|
|
88
|
+
val extras =
|
|
89
|
+
Bundle().apply {
|
|
90
|
+
putInt(
|
|
91
|
+
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
|
|
92
|
+
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
println("✅ NitroPlayerMediaBrowserService: Allowing connection from $clientPackageName with grid layout")
|
|
96
|
+
return BrowserRoot(ROOT_ID, extras)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
override fun onLoadChildren(
|
|
100
|
+
parentId: String,
|
|
101
|
+
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
|
|
102
|
+
) {
|
|
103
|
+
println("📂 NitroPlayerMediaBrowserService: onLoadChildren called for parentId: $parentId")
|
|
104
|
+
|
|
105
|
+
if (!isAndroidAutoEnabled) {
|
|
106
|
+
println("⚠️ NitroPlayerMediaBrowserService: Android Auto not enabled, returning empty")
|
|
107
|
+
result.sendResult(mutableListOf())
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
when {
|
|
112
|
+
parentId == ROOT_ID -> {
|
|
113
|
+
// Return root items from media library
|
|
114
|
+
result.detach()
|
|
115
|
+
|
|
116
|
+
serviceScope.launch {
|
|
117
|
+
try {
|
|
118
|
+
val library = mediaLibraryManager.getMediaLibrary()
|
|
119
|
+
|
|
120
|
+
if (library == null) {
|
|
121
|
+
// Fallback: show playlists if no media library is set
|
|
122
|
+
println("⚠️ NitroPlayerMediaBrowserService: No media library set, using fallback playlists")
|
|
123
|
+
val mediaItems = loadFallbackPlaylists()
|
|
124
|
+
result.sendResult(mediaItems)
|
|
125
|
+
return@launch
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
val mediaItems = mutableListOf<MediaBrowserCompat.MediaItem>()
|
|
129
|
+
|
|
130
|
+
library.rootItems.forEach { item ->
|
|
131
|
+
mediaItems.add(convertToMediaBrowserItem(item, library.layoutType))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
println("✅ NitroPlayerMediaBrowserService: Returning ${mediaItems.size} root items")
|
|
135
|
+
result.sendResult(mediaItems)
|
|
136
|
+
} catch (e: Exception) {
|
|
137
|
+
println("❌ NitroPlayerMediaBrowserService: Error loading root items - ${e.message}")
|
|
138
|
+
e.printStackTrace()
|
|
139
|
+
result.sendResult(mutableListOf())
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
parentId.startsWith(PLAYLIST_PREFIX) -> {
|
|
145
|
+
// Return tracks in a specific playlist
|
|
146
|
+
result.detach()
|
|
147
|
+
|
|
148
|
+
val playlistId = parentId.removePrefix(PLAYLIST_PREFIX)
|
|
149
|
+
|
|
150
|
+
serviceScope.launch {
|
|
151
|
+
try {
|
|
152
|
+
val playlist = trackPlayerCore?.getPlaylistManager()?.getPlaylist(playlistId)
|
|
153
|
+
|
|
154
|
+
if (playlist == null) {
|
|
155
|
+
println("⚠️ NitroPlayerMediaBrowserService: Playlist '$playlistId' not found")
|
|
156
|
+
result.sendResult(mutableListOf())
|
|
157
|
+
return@launch
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
val mediaItems = mutableListOf<MediaBrowserCompat.MediaItem>()
|
|
161
|
+
|
|
162
|
+
playlist.tracks.forEachIndexed { index, track ->
|
|
163
|
+
val extras =
|
|
164
|
+
Bundle().apply {
|
|
165
|
+
putString("playlistId", playlistId)
|
|
166
|
+
putInt("trackIndex", index)
|
|
167
|
+
putString("trackId", track.id)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
val description =
|
|
171
|
+
MediaDescriptionCompat
|
|
172
|
+
.Builder()
|
|
173
|
+
.setMediaId("$playlistId:${track.id}")
|
|
174
|
+
.setTitle(track.title)
|
|
175
|
+
.setSubtitle(track.artist)
|
|
176
|
+
.setDescription(track.album)
|
|
177
|
+
.setIconUri(track.artwork?.asSecondOrNull()?.let { Uri.parse(it) })
|
|
178
|
+
.setExtras(extras)
|
|
179
|
+
.build()
|
|
180
|
+
|
|
181
|
+
mediaItems.add(
|
|
182
|
+
MediaBrowserCompat.MediaItem(
|
|
183
|
+
description,
|
|
184
|
+
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE,
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
println("✅ NitroPlayerMediaBrowserService: Returning ${mediaItems.size} tracks from playlist '$playlistId'")
|
|
190
|
+
result.sendResult(mediaItems)
|
|
191
|
+
} catch (e: Exception) {
|
|
192
|
+
println("❌ NitroPlayerMediaBrowserService: Error loading playlist tracks - ${e.message}")
|
|
193
|
+
e.printStackTrace()
|
|
194
|
+
result.sendResult(mutableListOf())
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
parentId == EMPTY_ROOT_ID -> {
|
|
200
|
+
result.sendResult(mutableListOf())
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
else -> {
|
|
204
|
+
// Handle custom folder IDs from media library
|
|
205
|
+
result.detach()
|
|
206
|
+
|
|
207
|
+
serviceScope.launch {
|
|
208
|
+
try {
|
|
209
|
+
val children = mediaLibraryManager.getChildrenById(parentId)
|
|
210
|
+
|
|
211
|
+
if (children == null) {
|
|
212
|
+
println("⚠️ NitroPlayerMediaBrowserService: No children found for parentId: $parentId")
|
|
213
|
+
result.sendResult(mutableListOf())
|
|
214
|
+
return@launch
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
val library = mediaLibraryManager.getMediaLibrary()
|
|
218
|
+
val defaultLayout = library?.layoutType ?: LayoutType.LIST
|
|
219
|
+
val mediaItems =
|
|
220
|
+
children
|
|
221
|
+
.map { item ->
|
|
222
|
+
convertToMediaBrowserItem(item, defaultLayout)
|
|
223
|
+
}.toMutableList()
|
|
224
|
+
|
|
225
|
+
println("✅ NitroPlayerMediaBrowserService: Returning ${mediaItems.size} items for parentId: $parentId")
|
|
226
|
+
result.sendResult(mediaItems)
|
|
227
|
+
} catch (e: Exception) {
|
|
228
|
+
println("❌ NitroPlayerMediaBrowserService: Error loading children - ${e.message}")
|
|
229
|
+
e.printStackTrace()
|
|
230
|
+
result.sendResult(mutableListOf())
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fun onPlaylistsUpdated() {
|
|
238
|
+
try {
|
|
239
|
+
notifyChildrenChanged(ROOT_ID)
|
|
240
|
+
println("📢 NitroPlayerMediaBrowserService: Notified Android Auto of playlist update")
|
|
241
|
+
} catch (e: Exception) {
|
|
242
|
+
println("⚠️ NitroPlayerMediaBrowserService: Error notifying children changed: ${e.message}")
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fun onPlaylistUpdated(playlistId: String) {
|
|
247
|
+
try {
|
|
248
|
+
notifyChildrenChanged("$PLAYLIST_PREFIX$playlistId")
|
|
249
|
+
println("📢 NitroPlayerMediaBrowserService: Notified Android Auto of playlist '$playlistId' update")
|
|
250
|
+
} catch (e: Exception) {
|
|
251
|
+
println("⚠️ NitroPlayerMediaBrowserService: Error notifying playlist changed: ${e.message}")
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Convert MediaLibrary MediaItem to Android Auto MediaBrowserCompat.MediaItem
|
|
257
|
+
*/
|
|
258
|
+
private fun convertToMediaBrowserItem(
|
|
259
|
+
item: MediaItem,
|
|
260
|
+
defaultLayout: LayoutType,
|
|
261
|
+
): MediaBrowserCompat.MediaItem {
|
|
262
|
+
val layoutType = item.layoutType ?: defaultLayout
|
|
263
|
+
val contentStyle =
|
|
264
|
+
when (layoutType) {
|
|
265
|
+
LayoutType.GRID -> MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
|
|
266
|
+
LayoutType.LIST -> MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
val extras =
|
|
270
|
+
Bundle().apply {
|
|
271
|
+
putInt(
|
|
272
|
+
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
|
|
273
|
+
contentStyle,
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Determine the media ID based on item type
|
|
278
|
+
val mediaId =
|
|
279
|
+
when (item.mediaType) {
|
|
280
|
+
MediaType.PLAYLIST -> {
|
|
281
|
+
// For playlist items, use the playlist reference
|
|
282
|
+
if (item.playlistId != null) {
|
|
283
|
+
"$PLAYLIST_PREFIX${item.playlistId}"
|
|
284
|
+
} else {
|
|
285
|
+
item.id
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
else -> {
|
|
290
|
+
item.id
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
val description =
|
|
295
|
+
MediaDescriptionCompat
|
|
296
|
+
.Builder()
|
|
297
|
+
.setMediaId(mediaId)
|
|
298
|
+
.setTitle(item.title)
|
|
299
|
+
.setSubtitle(item.subtitle)
|
|
300
|
+
.setIconUri(item.iconUrl?.let { Uri.parse(it) })
|
|
301
|
+
.setExtras(extras)
|
|
302
|
+
.build()
|
|
303
|
+
|
|
304
|
+
// Determine if browsable or playable
|
|
305
|
+
val flag =
|
|
306
|
+
if (item.isPlayable && item.mediaType == MediaType.AUDIO) {
|
|
307
|
+
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
|
308
|
+
} else {
|
|
309
|
+
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return MediaBrowserCompat.MediaItem(description, flag)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Fallback: Load playlists when no media library is set
|
|
317
|
+
*/
|
|
318
|
+
private suspend fun loadFallbackPlaylists(): MutableList<MediaBrowserCompat.MediaItem> {
|
|
319
|
+
val playlists = trackPlayerCore?.getAllPlaylists() ?: emptyList()
|
|
320
|
+
val mediaItems = mutableListOf<MediaBrowserCompat.MediaItem>()
|
|
321
|
+
|
|
322
|
+
playlists.forEach { playlist ->
|
|
323
|
+
val extras =
|
|
324
|
+
Bundle().apply {
|
|
325
|
+
putInt(
|
|
326
|
+
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
|
|
327
|
+
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM,
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
val description =
|
|
332
|
+
MediaDescriptionCompat
|
|
333
|
+
.Builder()
|
|
334
|
+
.setMediaId("$PLAYLIST_PREFIX${playlist.id}")
|
|
335
|
+
.setTitle(playlist.name)
|
|
336
|
+
.setSubtitle(playlist.description ?: "${playlist.tracks.size} tracks")
|
|
337
|
+
.setIconUri(playlist.artwork?.let { Uri.parse(it) })
|
|
338
|
+
.setExtras(extras)
|
|
339
|
+
.build()
|
|
340
|
+
|
|
341
|
+
mediaItems.add(
|
|
342
|
+
MediaBrowserCompat.MediaItem(
|
|
343
|
+
description,
|
|
344
|
+
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE,
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
println("✅ NitroPlayerMediaBrowserService: Loaded ${mediaItems.size} playlists as fallback")
|
|
350
|
+
return mediaItems
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroplayer.media
|
|
2
|
+
|
|
3
|
+
import com.margelo.nitro.nitroplayer.Variant_NullType_String
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Layout type for Android Auto media browser
|
|
7
|
+
*/
|
|
8
|
+
enum class LayoutType {
|
|
9
|
+
GRID,
|
|
10
|
+
LIST,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Media type for different kinds of content
|
|
15
|
+
*/
|
|
16
|
+
enum class MediaType {
|
|
17
|
+
FOLDER,
|
|
18
|
+
AUDIO,
|
|
19
|
+
PLAYLIST,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Media item that can be displayed in Android Auto
|
|
24
|
+
*/
|
|
25
|
+
data class MediaItem(
|
|
26
|
+
/** Unique identifier for the media item */
|
|
27
|
+
val id: String,
|
|
28
|
+
/** Display title */
|
|
29
|
+
val title: String,
|
|
30
|
+
/** Optional subtitle/description */
|
|
31
|
+
val subtitle: String? = null,
|
|
32
|
+
/** Optional icon/artwork URL */
|
|
33
|
+
val iconUrl: String? = null,
|
|
34
|
+
/** Whether this item can be played directly */
|
|
35
|
+
val isPlayable: Boolean = false,
|
|
36
|
+
/** Media type */
|
|
37
|
+
val mediaType: MediaType = MediaType.FOLDER,
|
|
38
|
+
/** Reference to playlist ID (for playlist items) */
|
|
39
|
+
val playlistId: String? = null,
|
|
40
|
+
/** Child items for browsable folders */
|
|
41
|
+
val children: List<MediaItem>? = null,
|
|
42
|
+
/** Layout type for folder items (overrides library default) */
|
|
43
|
+
val layoutType: LayoutType? = null,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Media library structure for Android Auto
|
|
48
|
+
*/
|
|
49
|
+
data class MediaLibrary(
|
|
50
|
+
/** Layout type for the media browser (applies to all folders by default) */
|
|
51
|
+
val layoutType: LayoutType = LayoutType.LIST,
|
|
52
|
+
/** Root level media items */
|
|
53
|
+
val rootItems: List<MediaItem> = emptyList(),
|
|
54
|
+
/** Optional app name to display */
|
|
55
|
+
val appName: String? = null,
|
|
56
|
+
/** Optional app icon URL */
|
|
57
|
+
val appIconUrl: String? = null,
|
|
58
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroplayer.media
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Manages the Android Auto media library structure
|
|
7
|
+
*/
|
|
8
|
+
class MediaLibraryManager private constructor(
|
|
9
|
+
context: Context,
|
|
10
|
+
) {
|
|
11
|
+
@Volatile
|
|
12
|
+
private var mediaLibrary: MediaLibrary? = null
|
|
13
|
+
|
|
14
|
+
companion object {
|
|
15
|
+
@Volatile
|
|
16
|
+
@Suppress("ktlint:standard:property-naming")
|
|
17
|
+
private var INSTANCE: MediaLibraryManager? = null
|
|
18
|
+
|
|
19
|
+
fun getInstance(context: Context): MediaLibraryManager =
|
|
20
|
+
INSTANCE ?: synchronized(this) {
|
|
21
|
+
INSTANCE ?: MediaLibraryManager(context).also { INSTANCE = it }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set the media library structure
|
|
27
|
+
*/
|
|
28
|
+
fun setMediaLibrary(library: MediaLibrary) {
|
|
29
|
+
mediaLibrary = library
|
|
30
|
+
println("📚 MediaLibraryManager: Media library set with ${library.rootItems.size} root items")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the current media library
|
|
35
|
+
*/
|
|
36
|
+
fun getMediaLibrary(): MediaLibrary? = mediaLibrary
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get media item by ID (searches recursively)
|
|
40
|
+
*/
|
|
41
|
+
fun getMediaItemById(itemId: String): MediaItem? {
|
|
42
|
+
val library = mediaLibrary ?: return null
|
|
43
|
+
return findMediaItemRecursive(library.rootItems, itemId)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private fun findMediaItemRecursive(
|
|
47
|
+
items: List<MediaItem>,
|
|
48
|
+
targetId: String,
|
|
49
|
+
): MediaItem? {
|
|
50
|
+
for (item in items) {
|
|
51
|
+
if (item.id == targetId) {
|
|
52
|
+
return item
|
|
53
|
+
}
|
|
54
|
+
item.children?.let { children ->
|
|
55
|
+
val found = findMediaItemRecursive(children, targetId)
|
|
56
|
+
if (found != null) return found
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get children of a media item by ID
|
|
64
|
+
*/
|
|
65
|
+
fun getChildrenById(parentId: String): List<MediaItem>? {
|
|
66
|
+
val item = getMediaItemById(parentId)
|
|
67
|
+
return item?.children
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Clear the media library
|
|
72
|
+
*/
|
|
73
|
+
fun clear() {
|
|
74
|
+
mediaLibrary = null
|
|
75
|
+
println("📚 MediaLibraryManager: Media library cleared")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroplayer.media
|
|
2
|
+
|
|
3
|
+
import org.json.JSONArray
|
|
4
|
+
import org.json.JSONObject
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parser for MediaLibrary JSON structure
|
|
8
|
+
*/
|
|
9
|
+
object MediaLibraryParser {
|
|
10
|
+
fun fromJson(json: String): MediaLibrary {
|
|
11
|
+
val jsonObject = JSONObject(json)
|
|
12
|
+
|
|
13
|
+
val layoutType =
|
|
14
|
+
when (jsonObject.optString("layoutType", "list").lowercase()) {
|
|
15
|
+
"grid" -> LayoutType.GRID
|
|
16
|
+
else -> LayoutType.LIST
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
val rootItems = parseMediaItems(jsonObject.optJSONArray("rootItems"))
|
|
20
|
+
|
|
21
|
+
return MediaLibrary(
|
|
22
|
+
layoutType = layoutType,
|
|
23
|
+
rootItems = rootItems,
|
|
24
|
+
appName = jsonObject.optString("appName").takeIf { it.isNotEmpty() },
|
|
25
|
+
appIconUrl = jsonObject.optString("appIconUrl").takeIf { it.isNotEmpty() },
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private fun parseMediaItems(jsonArray: JSONArray?): List<MediaItem> {
|
|
30
|
+
if (jsonArray == null) return emptyList()
|
|
31
|
+
|
|
32
|
+
val items = mutableListOf<MediaItem>()
|
|
33
|
+
|
|
34
|
+
for (i in 0 until jsonArray.length()) {
|
|
35
|
+
val itemJson = jsonArray.optJSONObject(i) ?: continue
|
|
36
|
+
items.add(parseMediaItem(itemJson))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return items
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private fun parseMediaItem(jsonObject: JSONObject): MediaItem {
|
|
43
|
+
val mediaTypeStr = jsonObject.optString("mediaType", "folder").lowercase()
|
|
44
|
+
val mediaType =
|
|
45
|
+
when (mediaTypeStr) {
|
|
46
|
+
"audio" -> MediaType.AUDIO
|
|
47
|
+
"playlist" -> MediaType.PLAYLIST
|
|
48
|
+
else -> MediaType.FOLDER
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
val layoutTypeStr = jsonObject.optString("layoutType", "").lowercase()
|
|
52
|
+
val layoutType =
|
|
53
|
+
when (layoutTypeStr) {
|
|
54
|
+
"grid" -> LayoutType.GRID
|
|
55
|
+
"list" -> LayoutType.LIST
|
|
56
|
+
else -> null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
val children = parseMediaItems(jsonObject.optJSONArray("children"))
|
|
60
|
+
|
|
61
|
+
return MediaItem(
|
|
62
|
+
id = jsonObject.getString("id"),
|
|
63
|
+
title = jsonObject.getString("title"),
|
|
64
|
+
subtitle = jsonObject.optString("subtitle").takeIf { it.isNotEmpty() },
|
|
65
|
+
iconUrl = jsonObject.optString("iconUrl").takeIf { it.isNotEmpty() },
|
|
66
|
+
isPlayable = jsonObject.optBoolean("isPlayable", false),
|
|
67
|
+
mediaType = mediaType,
|
|
68
|
+
playlistId = jsonObject.optString("playlistId").takeIf { it.isNotEmpty() },
|
|
69
|
+
children = children.takeIf { it.isNotEmpty() },
|
|
70
|
+
layoutType = layoutType,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|