react-native-nitro-player 0.5.5 → 0.5.7
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/README.md +2 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +43 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/NitroPlayerLogger.kt +8 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +345 -4
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +43 -10
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +76 -7
- package/android/src/main/java/com/margelo/nitro/nitroplayer/storage/NitroPlayerStorage.kt +9 -2
- package/ios/HybridTrackPlayer.swift +54 -1
- package/ios/core/TrackPlayerCore.swift +254 -2
- package/ios/download/DownloadDatabase.swift +79 -2
- package/ios/download/DownloadManagerCore.swift +81 -2
- package/ios/playlist/PlaylistManager.swift +68 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +47 -0
- package/lib/types/PlayerQueue.d.ts +5 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double.hpp +104 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +160 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +8 -0
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +7 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +37 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +6 -3
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +16 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +65 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +62 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +8 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +173 -0
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +24 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +8 -0
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +6 -2
- package/package.json +1 -1
- package/src/specs/TrackPlayer.nitro.ts +57 -0
- package/src/types/PlayerQueue.ts +5 -0
package/README.md
CHANGED
|
@@ -50,6 +50,8 @@ npm install react-native-nitro-modules
|
|
|
50
50
|
| `skipToNext()` | Both | Skips to the next track in the queue. |
|
|
51
51
|
| `skipToPrevious()` | Both | Skips to the previous track. |
|
|
52
52
|
| `seek(position)` | Both | Seeks to a specific time position in seconds. |
|
|
53
|
+
| `setPlaybackSpeed(speed)` | Both | **Async**. Sets playback speed (e.g. 0.5x, 1x, 1.5x, 2x). |
|
|
54
|
+
| `getPlaybackSpeed()` | Both | **Async**. Gets the current playback speed. |
|
|
53
55
|
| `setVolume(0-100)` | Both | Sets playback volume (0-100). |
|
|
54
56
|
| `setRepeatMode(mode)` | Both | Sets repeat mode (`off`, `track`, `Playlist`). |
|
|
55
57
|
| `addToUpNext(id)` | Both | **Async**. Adds a track to the "up next" queue (FIFO). |
|
|
@@ -106,6 +106,7 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
|
|
|
106
106
|
androidAutoEnabled = config.androidAutoEnabled,
|
|
107
107
|
carPlayEnabled = config.carPlayEnabled,
|
|
108
108
|
showInNotification = config.showInNotification,
|
|
109
|
+
lookaheadCount = config.lookaheadCount?.toInt(),
|
|
109
110
|
)
|
|
110
111
|
}
|
|
111
112
|
|
|
@@ -127,4 +128,46 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
|
|
|
127
128
|
Promise.async {
|
|
128
129
|
core.skipToIndex(index.toInt())
|
|
129
130
|
}
|
|
131
|
+
|
|
132
|
+
override fun updateTracks(tracks: Array<TrackItem>): Promise<Unit> =
|
|
133
|
+
Promise.async {
|
|
134
|
+
core.updateTracks(tracks.toList())
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
override fun getTracksById(trackIds: Array<String>): Promise<Array<TrackItem>> =
|
|
138
|
+
Promise.async {
|
|
139
|
+
core.getTracksById(trackIds.toList()).toTypedArray()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
override fun getTracksNeedingUrls(): Promise<Array<TrackItem>> =
|
|
143
|
+
Promise.async {
|
|
144
|
+
core.getTracksNeedingUrls().toTypedArray()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override fun getNextTracks(count: Double): Promise<Array<TrackItem>> =
|
|
148
|
+
Promise.async {
|
|
149
|
+
core.getNextTracks(count.toInt()).toTypedArray()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
override fun getCurrentTrackIndex(): Promise<Double> =
|
|
153
|
+
Promise.async {
|
|
154
|
+
core.getCurrentTrackIndex().toDouble()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
override fun setPlaybackSpeed(speed: Double): Promise<Unit> =
|
|
158
|
+
Promise.async {
|
|
159
|
+
core.setPlayBackSpeed(speed)
|
|
160
|
+
Unit
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
override fun getPlaybackSpeed(): Promise<Double> =
|
|
164
|
+
Promise.async {
|
|
165
|
+
core.getPlayBackSpeed()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
override fun onTracksNeedUpdate(callback: (tracks: Array<TrackItem>, lookahead: Double) -> Unit) {
|
|
169
|
+
core.addOnTracksNeedUpdateListener { tracks, lookahead ->
|
|
170
|
+
callback(tracks.toTypedArray(), lookahead.toDouble())
|
|
171
|
+
}
|
|
172
|
+
}
|
|
130
173
|
}
|
|
@@ -12,7 +12,10 @@ object NitroPlayerLogger {
|
|
|
12
12
|
* Use trailing lambda syntax: NitroPlayerLogger.log("Tag") { "msg $value" }
|
|
13
13
|
* The lambda is inlined (no heap allocation) and skipped entirely when disabled.
|
|
14
14
|
*/
|
|
15
|
-
inline fun log(
|
|
15
|
+
inline fun log(
|
|
16
|
+
header: String = "NitroPlayer",
|
|
17
|
+
message: () -> String,
|
|
18
|
+
) {
|
|
16
19
|
if (isEnabled) {
|
|
17
20
|
Log.d(header, message())
|
|
18
21
|
}
|
|
@@ -23,7 +26,10 @@ object NitroPlayerLogger {
|
|
|
23
26
|
* Note: the String is evaluated at the call site before this function runs.
|
|
24
27
|
* Migrate to the lambda overload for hot paths.
|
|
25
28
|
*/
|
|
26
|
-
fun log(
|
|
29
|
+
fun log(
|
|
30
|
+
header: String = "NitroPlayer",
|
|
31
|
+
message: String,
|
|
32
|
+
) {
|
|
27
33
|
if (isEnabled) {
|
|
28
34
|
Log.d(header, message)
|
|
29
35
|
}
|
|
@@ -83,6 +83,7 @@ class TrackPlayerCore private constructor(
|
|
|
83
83
|
Collections.synchronizedList(mutableListOf<WeakCallbackBox<(Double, Double, Boolean?) -> Unit>>())
|
|
84
84
|
|
|
85
85
|
private var currentRepeatMode: RepeatMode = RepeatMode.OFF
|
|
86
|
+
private var lookaheadCount: Int = 5 // Number of tracks to preload ahead
|
|
86
87
|
|
|
87
88
|
// Temporary tracks for addToUpNext and playNext
|
|
88
89
|
private var playNextStack: MutableList<TrackItem> = mutableListOf() // LIFO - last added plays first
|
|
@@ -283,6 +284,9 @@ class TrackPlayerCore private constructor(
|
|
|
283
284
|
}
|
|
284
285
|
notifyTrackChange(track, r)
|
|
285
286
|
mediaSessionManager?.onTrackChanged()
|
|
287
|
+
|
|
288
|
+
// Check if upcoming tracks need URLs
|
|
289
|
+
checkUpcomingTracksForUrls(lookahead = lookaheadCount)
|
|
286
290
|
}
|
|
287
291
|
}
|
|
288
292
|
|
|
@@ -380,6 +384,9 @@ class TrackPlayerCore private constructor(
|
|
|
380
384
|
if (playlist != null) {
|
|
381
385
|
currentPlaylistId = playlistId
|
|
382
386
|
updatePlayerQueue(playlist.tracks)
|
|
387
|
+
|
|
388
|
+
// Check if upcoming tracks need URLs
|
|
389
|
+
checkUpcomingTracksForUrls(lookahead = lookaheadCount)
|
|
383
390
|
}
|
|
384
391
|
}
|
|
385
392
|
}
|
|
@@ -641,6 +648,9 @@ class TrackPlayerCore private constructor(
|
|
|
641
648
|
handler.post {
|
|
642
649
|
if (player.hasNextMediaItem()) {
|
|
643
650
|
player.seekToNextMediaItem()
|
|
651
|
+
|
|
652
|
+
// Check if upcoming tracks need URLs
|
|
653
|
+
checkUpcomingTracksForUrls(lookahead = lookaheadCount)
|
|
644
654
|
}
|
|
645
655
|
}
|
|
646
656
|
}
|
|
@@ -684,6 +694,9 @@ class TrackPlayerCore private constructor(
|
|
|
684
694
|
NitroPlayerLogger.log("TrackPlayerCore", "🔄 TrackPlayerCore: Already at first track, seeking to beginning")
|
|
685
695
|
player.seekTo(0)
|
|
686
696
|
}
|
|
697
|
+
|
|
698
|
+
// Check if upcoming tracks need URLs
|
|
699
|
+
checkUpcomingTracksForUrls(lookahead = lookaheadCount)
|
|
687
700
|
}
|
|
688
701
|
}
|
|
689
702
|
|
|
@@ -698,10 +711,11 @@ class TrackPlayerCore private constructor(
|
|
|
698
711
|
currentRepeatMode = mode
|
|
699
712
|
if (::player.isInitialized) {
|
|
700
713
|
handler.post {
|
|
701
|
-
player.repeatMode =
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
714
|
+
player.repeatMode =
|
|
715
|
+
when (mode) {
|
|
716
|
+
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
|
|
717
|
+
else -> Player.REPEAT_MODE_OFF
|
|
718
|
+
}
|
|
705
719
|
}
|
|
706
720
|
}
|
|
707
721
|
NitroPlayerLogger.log("TrackPlayerCore", "🔁 setRepeatMode: $mode")
|
|
@@ -810,11 +824,16 @@ class TrackPlayerCore private constructor(
|
|
|
810
824
|
androidAutoEnabled: Boolean?,
|
|
811
825
|
carPlayEnabled: Boolean?,
|
|
812
826
|
showInNotification: Boolean?,
|
|
827
|
+
lookaheadCount: Int? = null,
|
|
813
828
|
) {
|
|
814
829
|
handler.post {
|
|
815
830
|
androidAutoEnabled?.let {
|
|
816
831
|
NitroPlayerMediaBrowserService.isAndroidAutoEnabled = it
|
|
817
832
|
}
|
|
833
|
+
lookaheadCount?.let {
|
|
834
|
+
this.lookaheadCount = it
|
|
835
|
+
NitroPlayerLogger.log("TrackPlayerCore", "🔄 Lookahead count set to: $it")
|
|
836
|
+
}
|
|
818
837
|
mediaSessionManager?.configure(
|
|
819
838
|
androidAutoEnabled,
|
|
820
839
|
carPlayEnabled,
|
|
@@ -1014,9 +1033,16 @@ class TrackPlayerCore private constructor(
|
|
|
1014
1033
|
currentTemporaryType = TemporaryType.NONE
|
|
1015
1034
|
|
|
1016
1035
|
rebuildQueueAndPlayFromIndex(originalIndex)
|
|
1036
|
+
|
|
1037
|
+
// Check if upcoming tracks need URLs
|
|
1038
|
+
checkUpcomingTracksForUrls(lookahead = lookaheadCount)
|
|
1039
|
+
|
|
1017
1040
|
return true
|
|
1018
1041
|
}
|
|
1019
1042
|
|
|
1043
|
+
// Check if upcoming tracks need URLs after any successful skip
|
|
1044
|
+
checkUpcomingTracksForUrls(lookahead = lookaheadCount)
|
|
1045
|
+
|
|
1020
1046
|
return false
|
|
1021
1047
|
}
|
|
1022
1048
|
|
|
@@ -1467,4 +1493,319 @@ class TrackPlayerCore private constructor(
|
|
|
1467
1493
|
|
|
1468
1494
|
return queue
|
|
1469
1495
|
}
|
|
1496
|
+
|
|
1497
|
+
// MARK: - Lazy URL Loading Support
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* Update entire track objects and rebuild queue if needed
|
|
1501
|
+
* Skips currently playing track to preserve gapless playback
|
|
1502
|
+
*/
|
|
1503
|
+
fun updateTracks(tracks: List<TrackItem>) {
|
|
1504
|
+
handler.post {
|
|
1505
|
+
NitroPlayerLogger.log("TrackPlayerCore", "🔄 updateTracks: ${tracks.size} updates")
|
|
1506
|
+
|
|
1507
|
+
// Get current track ID to avoid updating it (preserves gapless playback)
|
|
1508
|
+
val currentTrackId = getCurrentTrack()?.id
|
|
1509
|
+
|
|
1510
|
+
// Filter out current track and validate
|
|
1511
|
+
val safeTracks =
|
|
1512
|
+
tracks.filter { track ->
|
|
1513
|
+
when {
|
|
1514
|
+
track.id == currentTrackId -> {
|
|
1515
|
+
NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Skipping update for currently playing track: ${track.id} (preserves gapless)")
|
|
1516
|
+
false
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
track.url.isEmpty() -> {
|
|
1520
|
+
NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Skipping track with empty URL: ${track.id}")
|
|
1521
|
+
false
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
else -> {
|
|
1525
|
+
true
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
if (safeTracks.isEmpty()) {
|
|
1531
|
+
NitroPlayerLogger.log("TrackPlayerCore", "✅ No valid updates to apply")
|
|
1532
|
+
return@post
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Update in PlaylistManager
|
|
1536
|
+
val affectedPlaylists = playlistManager.updateTracks(safeTracks)
|
|
1537
|
+
|
|
1538
|
+
// Rebuild queue if current playlist was affected
|
|
1539
|
+
if (currentPlaylistId != null && affectedPlaylists.containsKey(currentPlaylistId)) {
|
|
1540
|
+
NitroPlayerLogger.log("TrackPlayerCore", "🔄 Rebuilding queue - ${affectedPlaylists[currentPlaylistId]} tracks updated in current playlist")
|
|
1541
|
+
|
|
1542
|
+
// This method preserves current item and gapless buffering
|
|
1543
|
+
rebuildQueueFromCurrentPosition()
|
|
1544
|
+
|
|
1545
|
+
NitroPlayerLogger.log("TrackPlayerCore", "✅ Queue rebuilt, gapless playback preserved")
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
NitroPlayerLogger.log("TrackPlayerCore", "✅ Track updates complete - ${affectedPlaylists.size} playlists affected")
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/**
|
|
1553
|
+
* Get tracks by IDs from all playlists
|
|
1554
|
+
*/
|
|
1555
|
+
fun getTracksById(trackIds: List<String>): List<TrackItem> {
|
|
1556
|
+
if (android.os.Looper.myLooper() == handler.looper) {
|
|
1557
|
+
return playlistManager.getTracksById(trackIds)
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
val latch = CountDownLatch(1)
|
|
1561
|
+
var result: List<TrackItem>? = null
|
|
1562
|
+
|
|
1563
|
+
handler.post {
|
|
1564
|
+
try {
|
|
1565
|
+
result = playlistManager.getTracksById(trackIds)
|
|
1566
|
+
} finally {
|
|
1567
|
+
latch.countDown()
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
try {
|
|
1572
|
+
latch.await(5, TimeUnit.SECONDS)
|
|
1573
|
+
} catch (e: InterruptedException) {
|
|
1574
|
+
Thread.currentThread().interrupt()
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
return result ?: emptyList()
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* Get tracks needing URLs from current playlist
|
|
1582
|
+
*/
|
|
1583
|
+
fun getTracksNeedingUrls(): List<TrackItem> {
|
|
1584
|
+
if (android.os.Looper.myLooper() == handler.looper) {
|
|
1585
|
+
return getTracksNeedingUrlsInternal()
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
val latch = CountDownLatch(1)
|
|
1589
|
+
var result: List<TrackItem>? = null
|
|
1590
|
+
|
|
1591
|
+
handler.post {
|
|
1592
|
+
try {
|
|
1593
|
+
result = getTracksNeedingUrlsInternal()
|
|
1594
|
+
} finally {
|
|
1595
|
+
latch.countDown()
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
try {
|
|
1600
|
+
latch.await(5, TimeUnit.SECONDS)
|
|
1601
|
+
} catch (e: InterruptedException) {
|
|
1602
|
+
Thread.currentThread().interrupt()
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
return result ?: emptyList()
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
private fun getTracksNeedingUrlsInternal(): List<TrackItem> {
|
|
1609
|
+
if (currentPlaylistId == null) return emptyList()
|
|
1610
|
+
|
|
1611
|
+
val playlist = playlistManager.getPlaylist(currentPlaylistId!!)
|
|
1612
|
+
return playlist?.tracks?.filter { it.url.isEmpty() } ?: emptyList()
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* Get next N tracks from current position
|
|
1617
|
+
*/
|
|
1618
|
+
fun getNextTracks(count: Int): List<TrackItem> {
|
|
1619
|
+
if (android.os.Looper.myLooper() == handler.looper) {
|
|
1620
|
+
return getNextTracksInternal(count)
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
val latch = CountDownLatch(1)
|
|
1624
|
+
var result: List<TrackItem>? = null
|
|
1625
|
+
|
|
1626
|
+
handler.post {
|
|
1627
|
+
try {
|
|
1628
|
+
result = getNextTracksInternal(count)
|
|
1629
|
+
} finally {
|
|
1630
|
+
latch.countDown()
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
try {
|
|
1635
|
+
latch.await(5, TimeUnit.SECONDS)
|
|
1636
|
+
} catch (e: InterruptedException) {
|
|
1637
|
+
Thread.currentThread().interrupt()
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
return result ?: emptyList()
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
private fun getNextTracksInternal(count: Int): List<TrackItem> {
|
|
1644
|
+
val actualQueue = getActualQueueInternal()
|
|
1645
|
+
if (actualQueue.isEmpty()) return emptyList()
|
|
1646
|
+
|
|
1647
|
+
val currentIndex = actualQueue.indexOfFirst { it.id == getCurrentTrack()?.id }
|
|
1648
|
+
if (currentIndex == -1) return emptyList()
|
|
1649
|
+
|
|
1650
|
+
val startIndex = currentIndex + 1
|
|
1651
|
+
val endIndex = minOf(startIndex + count, actualQueue.size)
|
|
1652
|
+
|
|
1653
|
+
return if (startIndex < actualQueue.size) {
|
|
1654
|
+
actualQueue.subList(startIndex, endIndex)
|
|
1655
|
+
} else {
|
|
1656
|
+
emptyList()
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* Get current track index in playlist
|
|
1662
|
+
*/
|
|
1663
|
+
fun getCurrentTrackIndex(): Int {
|
|
1664
|
+
if (android.os.Looper.myLooper() == handler.looper) {
|
|
1665
|
+
return currentTrackIndex
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
val latch = CountDownLatch(1)
|
|
1669
|
+
var result = -1
|
|
1670
|
+
|
|
1671
|
+
handler.post {
|
|
1672
|
+
try {
|
|
1673
|
+
result = currentTrackIndex
|
|
1674
|
+
} finally {
|
|
1675
|
+
latch.countDown()
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
try {
|
|
1680
|
+
latch.await(5, TimeUnit.SECONDS)
|
|
1681
|
+
} catch (e: InterruptedException) {
|
|
1682
|
+
Thread.currentThread().interrupt()
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
return result
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* Callback interface for tracks needing update
|
|
1690
|
+
*/
|
|
1691
|
+
fun interface OnTracksNeedUpdateListener {
|
|
1692
|
+
fun onTracksNeedUpdate(
|
|
1693
|
+
tracks: List<TrackItem>,
|
|
1694
|
+
lookahead: Int,
|
|
1695
|
+
)
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// Add to class properties
|
|
1699
|
+
private val onTracksNeedUpdateListeners = mutableListOf<WeakReference<OnTracksNeedUpdateListener>>()
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
* Register listener for when tracks need update
|
|
1703
|
+
*/
|
|
1704
|
+
fun addOnTracksNeedUpdateListener(listener: OnTracksNeedUpdateListener) {
|
|
1705
|
+
handler.post {
|
|
1706
|
+
onTracksNeedUpdateListeners.add(WeakReference(listener))
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Remove listener
|
|
1712
|
+
*/
|
|
1713
|
+
fun removeOnTracksNeedUpdateListener(listener: OnTracksNeedUpdateListener) {
|
|
1714
|
+
handler.post {
|
|
1715
|
+
onTracksNeedUpdateListeners.removeAll { it.get() == listener || it.get() == null }
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Notify listeners that tracks need updating
|
|
1721
|
+
* Called internally when moving to next track and upcoming tracks have empty URLs
|
|
1722
|
+
*/
|
|
1723
|
+
private fun notifyTracksNeedUpdate(
|
|
1724
|
+
tracks: List<TrackItem>,
|
|
1725
|
+
lookahead: Int,
|
|
1726
|
+
) {
|
|
1727
|
+
val liveCallbacks =
|
|
1728
|
+
synchronized(onTracksNeedUpdateListeners) {
|
|
1729
|
+
onTracksNeedUpdateListeners.removeAll { it.get() == null }
|
|
1730
|
+
onTracksNeedUpdateListeners.mapNotNull { it.get() }
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
handler.post {
|
|
1734
|
+
for (callback in liveCallbacks) {
|
|
1735
|
+
try {
|
|
1736
|
+
callback.onTracksNeedUpdate(tracks, lookahead)
|
|
1737
|
+
} catch (e: Exception) {
|
|
1738
|
+
NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Error in onTracksNeedUpdate listener: ${e.message}")
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* Check if upcoming tracks need URLs and notify listeners
|
|
1746
|
+
* Call this in onMediaItemTransition or after skipTo operations
|
|
1747
|
+
*/
|
|
1748
|
+
private fun checkUpcomingTracksForUrls(lookahead: Int = 5) {
|
|
1749
|
+
val nextTracks = getNextTracksInternal(lookahead)
|
|
1750
|
+
val tracksNeedingUrls = nextTracks.filter { it.url.isEmpty() }
|
|
1751
|
+
|
|
1752
|
+
if (tracksNeedingUrls.isNotEmpty()) {
|
|
1753
|
+
NitroPlayerLogger.log("TrackPlayerCore", "⚠️ ${tracksNeedingUrls.size} upcoming tracks need URLs")
|
|
1754
|
+
notifyTracksNeedUpdate(tracksNeedingUrls, lookahead)
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
fun setPlayBackSpeed(speed: Double) {
|
|
1759
|
+
if (android.os.Looper.myLooper() == handler.looper) {
|
|
1760
|
+
setPlayBackSpeedInternal(speed)
|
|
1761
|
+
return
|
|
1762
|
+
}
|
|
1763
|
+
val latch = CountDownLatch(1)
|
|
1764
|
+
handler.post {
|
|
1765
|
+
try {
|
|
1766
|
+
setPlayBackSpeedInternal(speed)
|
|
1767
|
+
} finally {
|
|
1768
|
+
latch.countDown()
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
try {
|
|
1772
|
+
latch.await(5, TimeUnit.SECONDS)
|
|
1773
|
+
} catch (e: InterruptedException) {
|
|
1774
|
+
Thread.currentThread().interrupt()
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
private fun setPlayBackSpeedInternal(speed: Double) {
|
|
1779
|
+
if (::player.isInitialized) {
|
|
1780
|
+
player.setPlaybackSpeed(speed.toFloat())
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
fun getPlayBackSpeed(): Double {
|
|
1785
|
+
if (android.os.Looper.myLooper() == handler.looper) {
|
|
1786
|
+
return getPlayBackSpeedInternal()
|
|
1787
|
+
}
|
|
1788
|
+
val latch = CountDownLatch(1)
|
|
1789
|
+
var result = 1.0
|
|
1790
|
+
handler.post {
|
|
1791
|
+
try {
|
|
1792
|
+
result = getPlayBackSpeedInternal()
|
|
1793
|
+
} finally {
|
|
1794
|
+
latch.countDown()
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
try {
|
|
1798
|
+
latch.await(5, TimeUnit.SECONDS)
|
|
1799
|
+
} catch (e: InterruptedException) {
|
|
1800
|
+
Thread.currentThread().interrupt()
|
|
1801
|
+
}
|
|
1802
|
+
return result
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
private fun getPlayBackSpeedInternal(): Double =
|
|
1806
|
+
if (::player.isInitialized) {
|
|
1807
|
+
player.playbackParameters.speed.toDouble()
|
|
1808
|
+
} else {
|
|
1809
|
+
1.0
|
|
1810
|
+
}
|
|
1470
1811
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.margelo.nitro.nitroplayer.download
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import com.margelo.nitro.core.AnyMap
|
|
4
5
|
import com.margelo.nitro.core.NullType
|
|
5
6
|
import com.margelo.nitro.nitroplayer.*
|
|
6
7
|
import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
|
|
@@ -18,6 +19,7 @@ class DownloadDatabase private constructor(
|
|
|
18
19
|
) {
|
|
19
20
|
companion object {
|
|
20
21
|
private const val TAG = "DownloadDatabase"
|
|
22
|
+
|
|
21
23
|
// Legacy SharedPreferences keys (migration only)
|
|
22
24
|
private const val LEGACY_PREFS_NAME = "NitroPlayerDownloads"
|
|
23
25
|
private const val LEGACY_KEY_DOWNLOADED_TRACKS = "downloaded_tracks"
|
|
@@ -304,10 +306,11 @@ class DownloadDatabase private constructor(
|
|
|
304
306
|
playlistJson.put(playlistId, JSONArray(trackIds.toList()))
|
|
305
307
|
}
|
|
306
308
|
|
|
307
|
-
val wrapper =
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
val wrapper =
|
|
310
|
+
JSONObject().apply {
|
|
311
|
+
put("downloadedTracks", tracksJson)
|
|
312
|
+
put("playlistTracks", playlistJson)
|
|
313
|
+
}
|
|
311
314
|
NitroPlayerStorage.write(context, "downloads.json", wrapper.toString())
|
|
312
315
|
} catch (e: Exception) {
|
|
313
316
|
e.printStackTrace()
|
|
@@ -390,8 +393,14 @@ class DownloadDatabase private constructor(
|
|
|
390
393
|
}
|
|
391
394
|
|
|
392
395
|
// Conversion Helpers
|
|
393
|
-
private fun trackItemToRecord(track: TrackItem): TrackItemRecord
|
|
394
|
-
|
|
396
|
+
private fun trackItemToRecord(track: TrackItem): TrackItemRecord {
|
|
397
|
+
val extraPayloadJson =
|
|
398
|
+
track.extraPayload?.let { payload ->
|
|
399
|
+
val extraPayloadMap = payload.toHashMap()
|
|
400
|
+
JSONObject(extraPayloadMap)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return TrackItemRecord(
|
|
395
404
|
id = track.id,
|
|
396
405
|
title = track.title,
|
|
397
406
|
artist = track.artist,
|
|
@@ -399,7 +408,9 @@ class DownloadDatabase private constructor(
|
|
|
399
408
|
duration = track.duration,
|
|
400
409
|
url = track.url,
|
|
401
410
|
artwork = track.artwork?.asSecondOrNull(),
|
|
411
|
+
extraPayload = extraPayloadJson,
|
|
402
412
|
)
|
|
413
|
+
}
|
|
403
414
|
|
|
404
415
|
private fun recordToTrackItem(record: TrackItemRecord): TrackItem {
|
|
405
416
|
val artworkVariant =
|
|
@@ -409,6 +420,21 @@ class DownloadDatabase private constructor(
|
|
|
409
420
|
null
|
|
410
421
|
}
|
|
411
422
|
|
|
423
|
+
val extraPayload: AnyMap? =
|
|
424
|
+
record.extraPayload?.let { extraPayloadJson ->
|
|
425
|
+
val map = AnyMap()
|
|
426
|
+
val keyIterator = extraPayloadJson.keys()
|
|
427
|
+
while (keyIterator.hasNext()) {
|
|
428
|
+
val key = keyIterator.next()
|
|
429
|
+
when (val value = extraPayloadJson.get(key)) {
|
|
430
|
+
is String -> map.setString(key, value)
|
|
431
|
+
is Number -> map.setDouble(key, value.toDouble())
|
|
432
|
+
is Boolean -> map.setBoolean(key, value)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
map
|
|
436
|
+
}
|
|
437
|
+
|
|
412
438
|
return TrackItem(
|
|
413
439
|
id = record.id,
|
|
414
440
|
title = record.title,
|
|
@@ -417,7 +443,7 @@ class DownloadDatabase private constructor(
|
|
|
417
443
|
duration = record.duration,
|
|
418
444
|
url = record.url,
|
|
419
445
|
artwork = artworkVariant,
|
|
420
|
-
extraPayload =
|
|
446
|
+
extraPayload = extraPayload,
|
|
421
447
|
)
|
|
422
448
|
}
|
|
423
449
|
|
|
@@ -440,15 +466,14 @@ class DownloadDatabase private constructor(
|
|
|
440
466
|
)
|
|
441
467
|
}
|
|
442
468
|
|
|
443
|
-
private fun convertPlaylistManagerToNitro(playlist: com.margelo.nitro.nitroplayer.playlist.Playlist): Playlist
|
|
444
|
-
|
|
469
|
+
private fun convertPlaylistManagerToNitro(playlist: com.margelo.nitro.nitroplayer.playlist.Playlist): Playlist =
|
|
470
|
+
Playlist(
|
|
445
471
|
id = playlist.id,
|
|
446
472
|
name = playlist.name,
|
|
447
473
|
description = null,
|
|
448
474
|
artwork = null,
|
|
449
475
|
tracks = playlist.tracks.toTypedArray(),
|
|
450
476
|
)
|
|
451
|
-
}
|
|
452
477
|
}
|
|
453
478
|
|
|
454
479
|
// Internal record classes
|
|
@@ -494,6 +519,7 @@ internal data class TrackItemRecord(
|
|
|
494
519
|
val duration: Double,
|
|
495
520
|
val url: String,
|
|
496
521
|
val artwork: String?,
|
|
522
|
+
val extraPayload: JSONObject?,
|
|
497
523
|
) {
|
|
498
524
|
fun toJson(): JSONObject =
|
|
499
525
|
JSONObject().apply {
|
|
@@ -504,6 +530,7 @@ internal data class TrackItemRecord(
|
|
|
504
530
|
put("duration", duration)
|
|
505
531
|
put("url", url)
|
|
506
532
|
put("artwork", artwork)
|
|
533
|
+
put("extraPayload", extraPayload)
|
|
507
534
|
}
|
|
508
535
|
|
|
509
536
|
companion object {
|
|
@@ -516,6 +543,12 @@ internal data class TrackItemRecord(
|
|
|
516
543
|
duration = json.getDouble("duration"),
|
|
517
544
|
url = json.getString("url"),
|
|
518
545
|
artwork = if (json.isNull("artwork")) null else json.getString("artwork"),
|
|
546
|
+
extraPayload =
|
|
547
|
+
if (json.has("extraPayload") && !json.isNull("extraPayload")) {
|
|
548
|
+
json.getJSONObject("extraPayload")
|
|
549
|
+
} else {
|
|
550
|
+
null
|
|
551
|
+
},
|
|
519
552
|
)
|
|
520
553
|
}
|
|
521
554
|
}
|