react-native-nitro-player 0.7.0 → 0.7.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +9 -13
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +45 -90
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +48 -182
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +21 -77
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +55 -104
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +113 -123
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +82 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +48 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +62 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +153 -1887
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +122 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +44 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +162 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +165 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +161 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +121 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +98 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +27 -18
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +11 -58
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +13 -30
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +102 -162
- package/ios/HybridDownloadManager.swift +32 -26
- package/ios/HybridEqualizer.swift +48 -35
- package/ios/HybridTrackPlayer.swift +127 -102
- package/ios/core/ListenerRegistry.swift +60 -0
- package/ios/core/TrackPlayerCore.swift +130 -2356
- package/ios/core/TrackPlayerListener.swift +395 -0
- package/ios/core/TrackPlayerNotify.swift +52 -0
- package/ios/core/TrackPlayerPlayback.swift +274 -0
- package/ios/core/TrackPlayerQueue.swift +212 -0
- package/ios/core/TrackPlayerQueueBuild.swift +482 -0
- package/ios/core/TrackPlayerTempQueue.swift +167 -0
- package/ios/core/TrackPlayerUrlLoader.swift +169 -0
- package/ios/equalizer/EqualizerCore.swift +24 -89
- package/ios/media/MediaSessionManager.swift +32 -49
- package/ios/playlist/PlaylistManager.swift +2 -9
- package/ios/queue/HybridPlayerQueue.swift +69 -66
- package/lib/hooks/useDownloadedTracks.js +16 -13
- package/lib/hooks/useEqualizer.d.ts +4 -4
- package/lib/hooks/useEqualizer.js +12 -12
- package/lib/hooks/useEqualizerPresets.d.ts +3 -3
- package/lib/hooks/useEqualizerPresets.js +12 -18
- package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +2 -2
- package/lib/specs/AudioDevices.nitro.d.ts +2 -2
- package/lib/specs/DownloadManager.nitro.d.ts +10 -10
- package/lib/specs/Equalizer.nitro.d.ts +9 -9
- package/lib/specs/TrackPlayer.nitro.d.ts +38 -16
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp +122 -0
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +31 -6
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +2 -2
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +16 -3
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +154 -44
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +10 -10
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +130 -34
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +9 -9
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +115 -24
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +8 -8
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +243 -24
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +16 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__std__vector_TrackItem_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +3 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +2 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +10 -10
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +10 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +9 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +45 -8
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +74 -18
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +380 -151
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +10 -10
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +12 -9
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +23 -8
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +82 -8
- package/nitrogen/generated/ios/swift/Func_void_EqualizerState.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedPlaylist_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedTrack_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedPlaylist_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedTrack_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__std__vector_TrackItem_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +10 -10
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +141 -71
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +9 -9
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +105 -41
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +8 -8
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +95 -32
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +16 -8
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +267 -32
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +3 -2
- package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +2 -1
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +10 -10
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +10 -9
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +9 -8
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +16 -8
- package/package.json +1 -1
- package/src/hooks/useDownloadedTracks.ts +17 -13
- package/src/hooks/useEqualizer.ts +16 -16
- package/src/hooks/useEqualizerPresets.ts +15 -21
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +2 -2
- package/src/specs/AudioDevices.nitro.ts +2 -2
- package/src/specs/DownloadManager.nitro.ts +10 -10
- package/src/specs/Equalizer.nitro.ts +9 -9
- package/src/specs/TrackPlayer.nitro.ts +52 -16
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
@file:Suppress("ktlint:standard:max-line-length")
|
|
2
|
+
|
|
3
|
+
package com.margelo.nitro.nitroplayer.core
|
|
4
|
+
|
|
5
|
+
import com.margelo.nitro.nitroplayer.TrackItem
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Temporary queue management (playNext stack + upNext queue) and playlist loading.
|
|
9
|
+
* All public functions are suspend and execute on the player thread.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── Playlist loading ──────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
suspend fun TrackPlayerCore.loadPlaylist(playlistId: String) = withPlayerContext {
|
|
15
|
+
playNextStack.clear()
|
|
16
|
+
upNextQueue.clear()
|
|
17
|
+
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
18
|
+
val playlist = playlistManager.getPlaylist(playlistId) ?: return@withPlayerContext
|
|
19
|
+
currentPlaylistId = playlistId
|
|
20
|
+
updatePlayerQueue(playlist.tracks)
|
|
21
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
22
|
+
notifyTemporaryQueueChange()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Debounced update — coalesces rapid back-to-back mutations into one player rebuild.
|
|
27
|
+
* Called by HybridPlayerQueue when playlist data changes.
|
|
28
|
+
*/
|
|
29
|
+
fun TrackPlayerCore.updatePlaylist(playlistId: String) {
|
|
30
|
+
if (currentPlaylistId != playlistId) return
|
|
31
|
+
playerHandler.removeCallbacks(updateCurrentPlaylistRunnable)
|
|
32
|
+
playerHandler.post(updateCurrentPlaylistRunnable)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── playNext (LIFO) ────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
suspend fun TrackPlayerCore.playNext(trackId: String) = withPlayerContext { playNextInternal(trackId) }
|
|
38
|
+
|
|
39
|
+
internal fun TrackPlayerCore.playNextInternal(trackId: String) {
|
|
40
|
+
val track = findTrackById(trackId)
|
|
41
|
+
?: throw IllegalArgumentException("Track $trackId not found")
|
|
42
|
+
playNextStack.add(0, track)
|
|
43
|
+
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
44
|
+
notifyTemporaryQueueChange()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── addToUpNext (FIFO) ────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
suspend fun TrackPlayerCore.addToUpNext(trackId: String) = withPlayerContext { addToUpNextInternal(trackId) }
|
|
50
|
+
|
|
51
|
+
internal fun TrackPlayerCore.addToUpNextInternal(trackId: String) {
|
|
52
|
+
val track = findTrackById(trackId)
|
|
53
|
+
?: throw IllegalArgumentException("Track $trackId not found")
|
|
54
|
+
upNextQueue.add(track)
|
|
55
|
+
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
56
|
+
notifyTemporaryQueueChange()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Remove / clear ────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
suspend fun TrackPlayerCore.removeFromPlayNext(trackId: String): Boolean = withPlayerContext {
|
|
62
|
+
val idx = playNextStack.indexOfFirst { it.id == trackId }
|
|
63
|
+
if (idx < 0) return@withPlayerContext false
|
|
64
|
+
playNextStack.removeAt(idx)
|
|
65
|
+
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
66
|
+
notifyTemporaryQueueChange()
|
|
67
|
+
true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
suspend fun TrackPlayerCore.removeFromUpNext(trackId: String): Boolean = withPlayerContext {
|
|
71
|
+
val idx = upNextQueue.indexOfFirst { it.id == trackId }
|
|
72
|
+
if (idx < 0) return@withPlayerContext false
|
|
73
|
+
upNextQueue.removeAt(idx)
|
|
74
|
+
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
75
|
+
notifyTemporaryQueueChange()
|
|
76
|
+
true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
suspend fun TrackPlayerCore.clearPlayNext() = withPlayerContext {
|
|
80
|
+
playNextStack.clear()
|
|
81
|
+
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
82
|
+
notifyTemporaryQueueChange()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
suspend fun TrackPlayerCore.clearUpNext() = withPlayerContext {
|
|
86
|
+
upNextQueue.clear()
|
|
87
|
+
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
88
|
+
notifyTemporaryQueueChange()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Reorder ───────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Reorder within the combined virtual list [playNextStack + upNextQueue].
|
|
95
|
+
* newIndex is 0-based within that combined list.
|
|
96
|
+
*/
|
|
97
|
+
suspend fun TrackPlayerCore.reorderTemporaryTrack(trackId: String, newIndex: Int): Boolean = withPlayerContext {
|
|
98
|
+
val combined = (playNextStack + upNextQueue).toMutableList()
|
|
99
|
+
val fromIdx = combined.indexOfFirst { it.id == trackId }
|
|
100
|
+
if (fromIdx < 0) return@withPlayerContext false
|
|
101
|
+
val track = combined.removeAt(fromIdx)
|
|
102
|
+
val clampedIndex = newIndex.coerceIn(0, combined.size)
|
|
103
|
+
combined.add(clampedIndex, track)
|
|
104
|
+
|
|
105
|
+
// Split back at original playNextStack.size boundary (reduced if an item was moved out)
|
|
106
|
+
val pnSize = playNextStack.size
|
|
107
|
+
playNextStack.clear()
|
|
108
|
+
upNextQueue.clear()
|
|
109
|
+
playNextStack.addAll(combined.take(pnSize))
|
|
110
|
+
upNextQueue.addAll(combined.drop(pnSize))
|
|
111
|
+
|
|
112
|
+
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
113
|
+
notifyTemporaryQueueChange()
|
|
114
|
+
true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Read-only accessors ────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
suspend fun TrackPlayerCore.getPlayNextQueue(): List<TrackItem> = withPlayerContext { playNextStack.toList() }
|
|
120
|
+
|
|
121
|
+
suspend fun TrackPlayerCore.getUpNextQueue(): List<TrackItem> = withPlayerContext { upNextQueue.toList() }
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
@file:Suppress("ktlint:standard:max-line-length")
|
|
2
|
+
|
|
3
|
+
package com.margelo.nitro.nitroplayer.core
|
|
4
|
+
|
|
5
|
+
import androidx.media3.common.Player
|
|
6
|
+
import com.margelo.nitro.nitroplayer.TrackItem
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Lazy URL loading support, track queries, and playback speed.
|
|
10
|
+
* All public functions are suspend and execute on the player thread.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ── Track updates (URL resolution) ────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
suspend fun TrackPlayerCore.updateTracks(tracks: List<TrackItem>) = withPlayerContext {
|
|
16
|
+
val currentTrack = getCurrentTrack()
|
|
17
|
+
val currentTrackId = currentTrack?.id
|
|
18
|
+
val currentTrackIsEmpty = currentTrack?.url.isNullOrEmpty()
|
|
19
|
+
val currentTrackUpdate = if (currentTrackId != null) tracks.find { it.id == currentTrackId } else null
|
|
20
|
+
|
|
21
|
+
val safeTracks = tracks.filter { track ->
|
|
22
|
+
when {
|
|
23
|
+
track.id == currentTrackId && !currentTrackIsEmpty -> false // preserve gapless
|
|
24
|
+
track.id == currentTrackId && currentTrackIsEmpty -> track.url.isNotEmpty()
|
|
25
|
+
track.url.isEmpty() -> false
|
|
26
|
+
else -> true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (safeTracks.isEmpty()) return@withPlayerContext
|
|
30
|
+
|
|
31
|
+
val affectedPlaylists: Map<String, Int> = playlistManager.updateTracks(safeTracks)
|
|
32
|
+
|
|
33
|
+
// Replace current track's MediaItem if it was empty-URL and now has a URL
|
|
34
|
+
if (currentTrackUpdate != null && currentTrackIsEmpty && currentTrackUpdate.url.isNotEmpty()) {
|
|
35
|
+
val exoIndex = exo.currentMediaItemIndex
|
|
36
|
+
if (exoIndex >= 0) {
|
|
37
|
+
val playlistId = currentPlaylistId ?: ""
|
|
38
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${currentTrackUpdate.id}" else currentTrackUpdate.id
|
|
39
|
+
exo.replaceMediaItem(exoIndex, makeMediaItem(currentTrackUpdate, mediaId))
|
|
40
|
+
if (exo.playbackState == Player.STATE_IDLE) exo.prepare()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (currentPlaylistId != null && affectedPlaylists.containsKey(currentPlaylistId)) {
|
|
45
|
+
val refreshedPlaylist = playlistManager.getPlaylist(currentPlaylistId!!)
|
|
46
|
+
if (refreshedPlaylist != null) {
|
|
47
|
+
currentTracks = refreshedPlaylist.tracks
|
|
48
|
+
val updatedById = currentTracks.associateBy { it.id }
|
|
49
|
+
playNextStack.forEachIndexed { i, t -> updatedById[t.id]?.let { if (it !== t) playNextStack[i] = it } }
|
|
50
|
+
upNextQueue.forEachIndexed { i, t -> updatedById[t.id]?.let { if (it !== t) upNextQueue[i] = it } }
|
|
51
|
+
}
|
|
52
|
+
rebuildQueueFromCurrentPosition()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Track queries ─────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
suspend fun TrackPlayerCore.getTracksById(trackIds: List<String>): List<TrackItem> =
|
|
59
|
+
withPlayerContext { playlistManager.getTracksById(trackIds) as List<TrackItem> }
|
|
60
|
+
|
|
61
|
+
suspend fun TrackPlayerCore.getTracksNeedingUrls(): List<TrackItem> =
|
|
62
|
+
withPlayerContext { getTracksNeedingUrlsInternal() }
|
|
63
|
+
|
|
64
|
+
internal fun TrackPlayerCore.getTracksNeedingUrlsInternal(): List<TrackItem> {
|
|
65
|
+
val pid = currentPlaylistId ?: return emptyList()
|
|
66
|
+
return playlistManager.getPlaylist(pid)?.tracks?.filter { it.url.isEmpty() } ?: emptyList()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
suspend fun TrackPlayerCore.getNextTracks(count: Int): List<TrackItem> =
|
|
70
|
+
withPlayerContext { getNextTracksInternal(count) }
|
|
71
|
+
|
|
72
|
+
internal fun TrackPlayerCore.getNextTracksInternal(count: Int): List<TrackItem> {
|
|
73
|
+
val actualQueue = getActualQueueInternal()
|
|
74
|
+
if (actualQueue.isEmpty()) return emptyList()
|
|
75
|
+
val currentIdx = actualQueue.indexOfFirst { it.id == getCurrentTrack()?.id }
|
|
76
|
+
if (currentIdx == -1) return emptyList()
|
|
77
|
+
val start = currentIdx + 1
|
|
78
|
+
val end = minOf(start + count, actualQueue.size)
|
|
79
|
+
return if (start < actualQueue.size) actualQueue.subList(start, end) else emptyList()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
suspend fun TrackPlayerCore.getCurrentTrackIndex(): Int = withPlayerContext { currentTrackIndex }
|
|
83
|
+
|
|
84
|
+
// ── URL lookahead ─────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
internal fun TrackPlayerCore.checkUpcomingTracksForUrls(lookahead: Int = 5) {
|
|
87
|
+
val upcomingTracks = if (currentTrackIndex < 0) {
|
|
88
|
+
currentTracks.take(lookahead)
|
|
89
|
+
} else {
|
|
90
|
+
getNextTracksInternal(lookahead)
|
|
91
|
+
}
|
|
92
|
+
val currentTrack = getCurrentTrack()
|
|
93
|
+
val currentNeedsUrl = currentTrack != null && currentTrack.url.isEmpty()
|
|
94
|
+
val candidates = if (currentNeedsUrl) listOf(currentTrack!!) + upcomingTracks else upcomingTracks
|
|
95
|
+
val needUrls = candidates.filter { it.url.isEmpty() }
|
|
96
|
+
if (needUrls.isNotEmpty()) notifyTracksNeedUpdate(needUrls, lookahead)
|
|
97
|
+
}
|
|
98
|
+
|
|
@@ -10,6 +10,10 @@ import com.margelo.nitro.nitroplayer.storage.NitroPlayerStorage
|
|
|
10
10
|
import org.json.JSONArray
|
|
11
11
|
import org.json.JSONObject
|
|
12
12
|
import java.io.File
|
|
13
|
+
import kotlinx.coroutines.CoroutineScope
|
|
14
|
+
import kotlinx.coroutines.Dispatchers
|
|
15
|
+
import kotlinx.coroutines.SupervisorJob
|
|
16
|
+
import kotlinx.coroutines.launch
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Manages persistence of downloaded track metadata using file storage
|
|
@@ -37,6 +41,7 @@ class DownloadDatabase private constructor(
|
|
|
37
41
|
private val downloadedTracks = mutableMapOf<String, DownloadedTrackRecord>()
|
|
38
42
|
private val playlistTracks = mutableMapOf<String, MutableSet<String>>()
|
|
39
43
|
private val fileManager = DownloadFileManager.getInstance(context)
|
|
44
|
+
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
40
45
|
|
|
41
46
|
init {
|
|
42
47
|
loadFromDisk()
|
|
@@ -295,27 +300,31 @@ class DownloadDatabase private constructor(
|
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
302
|
|
|
298
|
-
// Persistence
|
|
303
|
+
// Persistence — called while holding synchronized(this); snapshots data then writes on IO.
|
|
299
304
|
private fun saveToDisk() {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
playlistJson.put(playlistId, JSONArray(trackIds.toList()))
|
|
309
|
-
}
|
|
305
|
+
val trackSnapshot = downloadedTracks.toMap()
|
|
306
|
+
val playlistSnapshot = playlistTracks.mapValues { it.value.toSet() }
|
|
307
|
+
ioScope.launch {
|
|
308
|
+
try {
|
|
309
|
+
val tracksJson = JSONObject()
|
|
310
|
+
for ((trackId, record) in trackSnapshot) {
|
|
311
|
+
tracksJson.put(trackId, record.toJson())
|
|
312
|
+
}
|
|
310
313
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
put(
|
|
314
|
-
put("playlistTracks", playlistJson)
|
|
314
|
+
val playlistJson = JSONObject()
|
|
315
|
+
for ((playlistId, trackIds) in playlistSnapshot) {
|
|
316
|
+
playlistJson.put(playlistId, JSONArray(trackIds.toList()))
|
|
315
317
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
318
|
+
|
|
319
|
+
val wrapper =
|
|
320
|
+
JSONObject().apply {
|
|
321
|
+
put("downloadedTracks", tracksJson)
|
|
322
|
+
put("playlistTracks", playlistJson)
|
|
323
|
+
}
|
|
324
|
+
NitroPlayerStorage.write(context, "downloads.json", wrapper.toString())
|
|
325
|
+
} catch (e: Exception) {
|
|
326
|
+
e.printStackTrace()
|
|
327
|
+
}
|
|
319
328
|
}
|
|
320
329
|
}
|
|
321
330
|
|
|
@@ -14,8 +14,7 @@ import com.margelo.nitro.nitroplayer.Variant_NullType_String
|
|
|
14
14
|
import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
|
|
15
15
|
import org.json.JSONArray
|
|
16
16
|
import org.json.JSONObject
|
|
17
|
-
import
|
|
18
|
-
import java.util.Collections
|
|
17
|
+
import com.margelo.nitro.nitroplayer.core.ListenerRegistry
|
|
19
18
|
|
|
20
19
|
class EqualizerCore private constructor(
|
|
21
20
|
private val context: Context,
|
|
@@ -35,21 +34,10 @@ class EqualizerCore private constructor(
|
|
|
35
34
|
private val prefs: SharedPreferences =
|
|
36
35
|
context.getSharedPreferences("equalizer_settings", Context.MODE_PRIVATE)
|
|
37
36
|
|
|
38
|
-
// Weak callback wrapper for auto-cleanup
|
|
39
|
-
private data class WeakCallbackBox<T>(
|
|
40
|
-
private val ownerRef: WeakReference<Any>,
|
|
41
|
-
val callback: T,
|
|
42
|
-
) {
|
|
43
|
-
val isAlive: Boolean get() = ownerRef.get() != null
|
|
44
|
-
}
|
|
45
|
-
|
|
46
37
|
// Event listeners
|
|
47
|
-
private val onEnabledChangeListeners =
|
|
48
|
-
|
|
49
|
-
private val
|
|
50
|
-
Collections.synchronizedList(mutableListOf<WeakCallbackBox<(Array<EqualizerBand>) -> Unit>>())
|
|
51
|
-
private val onPresetChangeListeners =
|
|
52
|
-
Collections.synchronizedList(mutableListOf<WeakCallbackBox<(Variant_NullType_String?) -> Unit>>())
|
|
38
|
+
private val onEnabledChangeListeners = ListenerRegistry<(Boolean) -> Unit>()
|
|
39
|
+
private val onBandChangeListeners = ListenerRegistry<(Array<EqualizerBand>) -> Unit>()
|
|
40
|
+
private val onPresetChangeListeners = ListenerRegistry<(Variant_NullType_String?) -> Unit>()
|
|
53
41
|
|
|
54
42
|
companion object {
|
|
55
43
|
private const val TAG = "EqualizerCore"
|
|
@@ -429,63 +417,28 @@ class EqualizerCore private constructor(
|
|
|
429
417
|
// === Callback management ===
|
|
430
418
|
|
|
431
419
|
fun addOnEnabledChangeListener(callback: (Boolean) -> Unit) {
|
|
432
|
-
|
|
433
|
-
onEnabledChangeListeners.add(box)
|
|
420
|
+
onEnabledChangeListeners.add(callback)
|
|
434
421
|
}
|
|
435
422
|
|
|
436
423
|
fun addOnBandChangeListener(callback: (Array<EqualizerBand>) -> Unit) {
|
|
437
|
-
|
|
438
|
-
synchronized(onBandChangeListeners) {
|
|
439
|
-
@Suppress("UNCHECKED_CAST")
|
|
440
|
-
(onBandChangeListeners as MutableList<WeakCallbackBox<(Array<EqualizerBand>) -> Unit>>).add(box)
|
|
441
|
-
}
|
|
424
|
+
onBandChangeListeners.add(callback)
|
|
442
425
|
}
|
|
443
426
|
|
|
444
427
|
fun addOnPresetChangeListener(callback: (Variant_NullType_String?) -> Unit) {
|
|
445
|
-
|
|
446
|
-
onPresetChangeListeners.add(box)
|
|
428
|
+
onPresetChangeListeners.add(callback)
|
|
447
429
|
}
|
|
448
430
|
|
|
449
431
|
private fun notifyEnabledChange(enabled: Boolean) {
|
|
450
|
-
|
|
451
|
-
onEnabledChangeListeners.removeAll { !it.isAlive }
|
|
452
|
-
onEnabledChangeListeners.forEach { box ->
|
|
453
|
-
try {
|
|
454
|
-
box.callback(enabled)
|
|
455
|
-
} catch (e: Exception) {
|
|
456
|
-
// Ignore callback errors
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
432
|
+
onEnabledChangeListeners.forEach { it(enabled) }
|
|
460
433
|
}
|
|
461
434
|
|
|
462
435
|
private fun notifyBandChange(bands: Array<EqualizerBand>) {
|
|
463
|
-
|
|
464
|
-
@Suppress("UNCHECKED_CAST")
|
|
465
|
-
val listeners = onBandChangeListeners as MutableList<WeakCallbackBox<(Array<EqualizerBand>) -> Unit>>
|
|
466
|
-
listeners.removeAll { !it.isAlive }
|
|
467
|
-
listeners.forEach { box ->
|
|
468
|
-
try {
|
|
469
|
-
box.callback(bands)
|
|
470
|
-
} catch (e: Exception) {
|
|
471
|
-
// Ignore callback errors
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
436
|
+
onBandChangeListeners.forEach { it(bands) }
|
|
475
437
|
}
|
|
476
438
|
|
|
477
439
|
private fun notifyPresetChange(presetName: String?) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
onPresetChangeListeners.forEach { box ->
|
|
481
|
-
try {
|
|
482
|
-
val variant = presetName?.let { Variant_NullType_String.create(it) }
|
|
483
|
-
box.callback(variant)
|
|
484
|
-
} catch (e: Exception) {
|
|
485
|
-
// Ignore callback errors
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
440
|
+
val variant = presetName?.let { Variant_NullType_String.create(it) }
|
|
441
|
+
onPresetChangeListeners.forEach { it(variant) }
|
|
489
442
|
}
|
|
490
443
|
|
|
491
444
|
fun release() {
|
|
@@ -26,6 +26,7 @@ import com.google.common.util.concurrent.ListenableFuture
|
|
|
26
26
|
import com.margelo.nitro.nitroplayer.TrackItem
|
|
27
27
|
import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
|
|
28
28
|
import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
|
|
29
|
+
import com.margelo.nitro.nitroplayer.core.loadPlaylist
|
|
29
30
|
import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
30
31
|
import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
|
|
31
32
|
import kotlinx.coroutines.CoroutineScope
|
|
@@ -50,6 +51,8 @@ class MediaSessionManager(
|
|
|
50
51
|
private set
|
|
51
52
|
private var notificationManager: NotificationManager? = null
|
|
52
53
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
54
|
+
@Volatile private var currentTrack: TrackItem? = null
|
|
55
|
+
@Volatile private var isPlaying: Boolean = false
|
|
53
56
|
private val artworkCache = object : LruCache<String, Bitmap>(20) {
|
|
54
57
|
override fun sizeOf(key: String, value: Bitmap): Int = 1
|
|
55
58
|
}
|
|
@@ -216,7 +219,7 @@ class MediaSessionManager(
|
|
|
216
219
|
|
|
217
220
|
if (trackIndex >= 0) {
|
|
218
221
|
// Load the entire playlist into TrackPlayerCore
|
|
219
|
-
trackPlayerCore?.loadPlaylist(playlistId)
|
|
222
|
+
trackPlayerCore?.let { core -> scope.launch { core.loadPlaylist(playlistId) } }
|
|
220
223
|
|
|
221
224
|
// Create MediaItems for the entire playlist
|
|
222
225
|
val playlistMediaItems =
|
|
@@ -324,28 +327,7 @@ class MediaSessionManager(
|
|
|
324
327
|
}
|
|
325
328
|
}
|
|
326
329
|
|
|
327
|
-
private fun getCurrentTrack(): TrackItem?
|
|
328
|
-
val currentMediaItem = player.currentMediaItem ?: return null
|
|
329
|
-
val mediaId = currentMediaItem.mediaId
|
|
330
|
-
|
|
331
|
-
// Parse mediaId format: "playlistId:trackId" or just "trackId"
|
|
332
|
-
val trackId =
|
|
333
|
-
if (mediaId.contains(':')) {
|
|
334
|
-
mediaId.substring(mediaId.indexOf(':') + 1)
|
|
335
|
-
} else {
|
|
336
|
-
mediaId
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Find track in current playlist or all playlists
|
|
340
|
-
return trackPlayerCore?.getCurrentPlaylistId()?.let { playlistId ->
|
|
341
|
-
playlistManager.getPlaylist(playlistId)?.tracks?.find { it.id == trackId }
|
|
342
|
-
} ?: run {
|
|
343
|
-
for (playlist in playlistManager.getAllPlaylists()) {
|
|
344
|
-
playlist.tracks.find { it.id == trackId }?.let { return it }
|
|
345
|
-
}
|
|
346
|
-
null
|
|
347
|
-
}
|
|
348
|
-
}
|
|
330
|
+
private fun getCurrentTrack(): TrackItem? = currentTrack
|
|
349
331
|
|
|
350
332
|
private fun updateNotification() {
|
|
351
333
|
if (!showInNotification) return
|
|
@@ -376,7 +358,7 @@ class MediaSessionManager(
|
|
|
376
358
|
.setSmallIcon(android.R.drawable.ic_media_play)
|
|
377
359
|
.setContentIntent(contentIntent)
|
|
378
360
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
379
|
-
.setOngoing(
|
|
361
|
+
.setOngoing(isPlaying)
|
|
380
362
|
.setShowWhen(false)
|
|
381
363
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
382
364
|
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
|
@@ -401,7 +383,7 @@ class MediaSessionManager(
|
|
|
401
383
|
createMediaAction(ACTION_PREVIOUS),
|
|
402
384
|
)
|
|
403
385
|
|
|
404
|
-
if (
|
|
386
|
+
if (isPlaying) {
|
|
405
387
|
builder.addAction(
|
|
406
388
|
android.R.drawable.ic_media_pause,
|
|
407
389
|
"Pause",
|
|
@@ -459,12 +441,12 @@ class MediaSessionManager(
|
|
|
459
441
|
notificationManager?.cancel(NOTIFICATION_ID)
|
|
460
442
|
}
|
|
461
443
|
|
|
462
|
-
fun onTrackChanged() {
|
|
444
|
+
fun onTrackChanged(track: TrackItem?) {
|
|
445
|
+
currentTrack = track
|
|
463
446
|
// Preload artwork for better notification display
|
|
464
|
-
|
|
465
|
-
if (currentTrack != null) {
|
|
447
|
+
if (track != null) {
|
|
466
448
|
scope.launch {
|
|
467
|
-
|
|
449
|
+
track.artwork?.asSecondOrNull()?.let { artworkUrl ->
|
|
468
450
|
loadArtworkBitmap(artworkUrl)
|
|
469
451
|
}
|
|
470
452
|
updateNotification()
|
|
@@ -474,7 +456,8 @@ class MediaSessionManager(
|
|
|
474
456
|
}
|
|
475
457
|
}
|
|
476
458
|
|
|
477
|
-
fun onPlaybackStateChanged() {
|
|
459
|
+
fun onPlaybackStateChanged(playing: Boolean) {
|
|
460
|
+
isPlaying = playing
|
|
478
461
|
updateNotification()
|
|
479
462
|
}
|
|
480
463
|
|