react-native-nitro-player 0.5.6 → 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/TrackPlayerCore.kt +340 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +60 -0
- package/ios/HybridTrackPlayer.swift +54 -1
- package/ios/core/TrackPlayerCore.swift +254 -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
|
}
|
|
@@ -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
|
|
|
@@ -811,11 +824,16 @@ class TrackPlayerCore private constructor(
|
|
|
811
824
|
androidAutoEnabled: Boolean?,
|
|
812
825
|
carPlayEnabled: Boolean?,
|
|
813
826
|
showInNotification: Boolean?,
|
|
827
|
+
lookaheadCount: Int? = null,
|
|
814
828
|
) {
|
|
815
829
|
handler.post {
|
|
816
830
|
androidAutoEnabled?.let {
|
|
817
831
|
NitroPlayerMediaBrowserService.isAndroidAutoEnabled = it
|
|
818
832
|
}
|
|
833
|
+
lookaheadCount?.let {
|
|
834
|
+
this.lookaheadCount = it
|
|
835
|
+
NitroPlayerLogger.log("TrackPlayerCore", "🔄 Lookahead count set to: $it")
|
|
836
|
+
}
|
|
819
837
|
mediaSessionManager?.configure(
|
|
820
838
|
androidAutoEnabled,
|
|
821
839
|
carPlayEnabled,
|
|
@@ -1015,9 +1033,16 @@ class TrackPlayerCore private constructor(
|
|
|
1015
1033
|
currentTemporaryType = TemporaryType.NONE
|
|
1016
1034
|
|
|
1017
1035
|
rebuildQueueAndPlayFromIndex(originalIndex)
|
|
1036
|
+
|
|
1037
|
+
// Check if upcoming tracks need URLs
|
|
1038
|
+
checkUpcomingTracksForUrls(lookahead = lookaheadCount)
|
|
1039
|
+
|
|
1018
1040
|
return true
|
|
1019
1041
|
}
|
|
1020
1042
|
|
|
1043
|
+
// Check if upcoming tracks need URLs after any successful skip
|
|
1044
|
+
checkUpcomingTracksForUrls(lookahead = lookaheadCount)
|
|
1045
|
+
|
|
1021
1046
|
return false
|
|
1022
1047
|
}
|
|
1023
1048
|
|
|
@@ -1468,4 +1493,319 @@ class TrackPlayerCore private constructor(
|
|
|
1468
1493
|
|
|
1469
1494
|
return queue
|
|
1470
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
|
+
}
|
|
1471
1811
|
}
|
|
@@ -302,6 +302,66 @@ class PlaylistManager private constructor(
|
|
|
302
302
|
return true
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Update entire track objects across all playlists
|
|
307
|
+
* Matches by track.id and replaces the entire track object
|
|
308
|
+
* @param tracks List of full TrackItem objects to update
|
|
309
|
+
* @return Map of playlistId -> count of tracks updated
|
|
310
|
+
*/
|
|
311
|
+
fun updateTracks(tracks: List<TrackItem>): Map<String, Int> {
|
|
312
|
+
val tracksMap = tracks.associateBy { it.id }
|
|
313
|
+
val affectedPlaylists = mutableMapOf<String, Int>()
|
|
314
|
+
|
|
315
|
+
synchronized(playlists) {
|
|
316
|
+
playlists.forEach { (playlistId, playlist) ->
|
|
317
|
+
var updateCount = 0
|
|
318
|
+
val newTracks =
|
|
319
|
+
playlist.tracks
|
|
320
|
+
.map { track ->
|
|
321
|
+
tracksMap[track.id]?.also { updateCount++ } ?: track
|
|
322
|
+
}.toMutableList()
|
|
323
|
+
|
|
324
|
+
if (updateCount > 0) {
|
|
325
|
+
affectedPlaylists[playlistId] = updateCount
|
|
326
|
+
playlists[playlistId] = playlist.copy(tracks = newTracks)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (affectedPlaylists.isNotEmpty()) {
|
|
332
|
+
scheduleSave()
|
|
333
|
+
affectedPlaylists.keys.forEach { playlistId ->
|
|
334
|
+
notifyPlaylistChanged(playlistId, QueueOperation.UPDATE)
|
|
335
|
+
}
|
|
336
|
+
notifyPlaylistsChanged(QueueOperation.UPDATE)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return affectedPlaylists
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get tracks by IDs from all playlists
|
|
344
|
+
* @param trackIds List of track IDs to fetch
|
|
345
|
+
* @return List of matching TrackItem objects
|
|
346
|
+
*/
|
|
347
|
+
fun getTracksById(trackIds: List<String>): List<TrackItem> {
|
|
348
|
+
val trackIdSet = trackIds.toSet()
|
|
349
|
+
val foundTracks = mutableMapOf<String, TrackItem>()
|
|
350
|
+
|
|
351
|
+
synchronized(playlists) {
|
|
352
|
+
playlists.values.forEach { playlist ->
|
|
353
|
+
playlist.tracks.forEach { track ->
|
|
354
|
+
if (trackIdSet.contains(track.id) && !foundTracks.containsKey(track.id)) {
|
|
355
|
+
foundTracks[track.id] = track
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Return in same order as requested
|
|
362
|
+
return trackIds.mapNotNull { foundTracks[it] }
|
|
363
|
+
}
|
|
364
|
+
|
|
305
365
|
/**
|
|
306
366
|
* Load a playlist for playback (sets it as current)
|
|
307
367
|
*/
|
|
@@ -88,7 +88,8 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
|
|
|
88
88
|
core.configure(
|
|
89
89
|
androidAutoEnabled: config.androidAutoEnabled,
|
|
90
90
|
carPlayEnabled: config.carPlayEnabled,
|
|
91
|
-
showInNotification: config.showInNotification
|
|
91
|
+
showInNotification: config.showInNotification,
|
|
92
|
+
lookaheadCount: config.lookaheadCount.map { Int($0) }
|
|
92
93
|
)
|
|
93
94
|
}
|
|
94
95
|
|
|
@@ -139,4 +140,56 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
|
|
|
139
140
|
func setVolume(volume: Double) throws -> Bool {
|
|
140
141
|
return core.setVolume(volume: volume)
|
|
141
142
|
}
|
|
143
|
+
|
|
144
|
+
// MARK: - Lazy URL Loading
|
|
145
|
+
|
|
146
|
+
func updateTracks(tracks: [TrackItem]) throws -> Promise<Void> {
|
|
147
|
+
return Promise.async {
|
|
148
|
+
self.core.updateTracks(tracks: tracks)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func getTracksById(trackIds: [String]) throws -> Promise<[TrackItem]> {
|
|
153
|
+
return Promise.async {
|
|
154
|
+
return self.core.getTracksById(trackIds: trackIds)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func getTracksNeedingUrls() throws -> Promise<[TrackItem]> {
|
|
159
|
+
return Promise.async {
|
|
160
|
+
return self.core.getTracksNeedingUrls()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func getNextTracks(count: Double) throws -> Promise<[TrackItem]> {
|
|
165
|
+
return Promise.async {
|
|
166
|
+
return self.core.getNextTracks(count: Int(count))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func getCurrentTrackIndex() throws -> Promise<Double> {
|
|
171
|
+
return Promise.async {
|
|
172
|
+
return Double(self.core.getCurrentTrackIndex())
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func onTracksNeedUpdate(callback: @escaping ([TrackItem], Double) -> Void) throws {
|
|
177
|
+
core.addOnTracksNeedUpdateListener { tracks, lookahead in
|
|
178
|
+
callback(tracks, Double(lookahead))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
func setPlaybackSpeed(speed: Double) throws -> Promise<Void> {
|
|
183
|
+
Promise.async{
|
|
184
|
+
self.core.setPlaybackSpeed(speed)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func getPlaybackSpeed() throws -> Promise<Double> {
|
|
190
|
+
return Promise.async{
|
|
191
|
+
return self.core.getPlaybackSpeed()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
}
|
|
142
195
|
}
|