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
|
@@ -50,6 +50,7 @@ class TrackPlayerCore: NSObject {
|
|
|
50
50
|
private var currentTracks: [TrackItem] = []
|
|
51
51
|
private var isManuallySeeked = false
|
|
52
52
|
private var currentRepeatMode: RepeatMode = .off
|
|
53
|
+
private var lookaheadCount: Int = 5 // Number of tracks to preload ahead
|
|
53
54
|
private var boundaryTimeObserver: Any?
|
|
54
55
|
private var currentItemObservers: [NSKeyValueObservation] = []
|
|
55
56
|
|
|
@@ -310,6 +311,9 @@ class TrackPlayerCore: NSObject {
|
|
|
310
311
|
if let player = player {
|
|
311
312
|
NitroPlayerLogger.log("TrackPlayerCore", "📋 Remaining items in queue: \(player.items().count)")
|
|
312
313
|
}
|
|
314
|
+
|
|
315
|
+
// Check if upcoming tracks need URLs
|
|
316
|
+
checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
313
317
|
}
|
|
314
318
|
|
|
315
319
|
@objc private func playerItemFailedToPlayToEndTime(notification: Notification) {
|
|
@@ -571,6 +575,14 @@ class TrackPlayerCore: NSObject {
|
|
|
571
575
|
}
|
|
572
576
|
}
|
|
573
577
|
}
|
|
578
|
+
|
|
579
|
+
func setPlaybackSpeed(_ speed: Double) {
|
|
580
|
+
player?.rate = Float(speed)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
func getPlaybackSpeed() -> Double {
|
|
584
|
+
return Double(player?.rate ?? 1.0)
|
|
585
|
+
}
|
|
574
586
|
|
|
575
587
|
private func loadPlaylistInternal(playlistId: String) {
|
|
576
588
|
NitroPlayerLogger.log("TrackPlayerCore", "\n" + String(repeating: "🎼", count: Constants.playlistSeparatorLength))
|
|
@@ -596,6 +608,9 @@ class TrackPlayerCore: NSObject {
|
|
|
596
608
|
self.updatePlayerQueue(tracks: playlist.tracks)
|
|
597
609
|
// Emit initial state (paused/stopped before play)
|
|
598
610
|
self.emitStateChange()
|
|
611
|
+
|
|
612
|
+
// Check if upcoming tracks need URLs
|
|
613
|
+
self.checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
599
614
|
} else {
|
|
600
615
|
NitroPlayerLogger.log("TrackPlayerCore", " ❌ Playlist NOT FOUND")
|
|
601
616
|
NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
|
|
@@ -1301,6 +1316,9 @@ class TrackPlayerCore: NSObject {
|
|
|
1301
1316
|
queuePlayer.pause()
|
|
1302
1317
|
self.notifyPlaybackStateChange(.stopped, .end)
|
|
1303
1318
|
}
|
|
1319
|
+
|
|
1320
|
+
// Check if upcoming tracks need URLs
|
|
1321
|
+
checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
1304
1322
|
}
|
|
1305
1323
|
|
|
1306
1324
|
func skipToPrevious() {
|
|
@@ -1343,6 +1361,9 @@ class TrackPlayerCore: NSObject {
|
|
|
1343
1361
|
// Already at first track, restart it
|
|
1344
1362
|
queuePlayer.seek(to: .zero)
|
|
1345
1363
|
}
|
|
1364
|
+
|
|
1365
|
+
// Check if upcoming tracks need URLs
|
|
1366
|
+
checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
1346
1367
|
}
|
|
1347
1368
|
|
|
1348
1369
|
func seek(position: Double) {
|
|
@@ -1469,9 +1490,14 @@ class TrackPlayerCore: NSObject {
|
|
|
1469
1490
|
func configure(
|
|
1470
1491
|
androidAutoEnabled: Bool?,
|
|
1471
1492
|
carPlayEnabled: Bool?,
|
|
1472
|
-
showInNotification: Bool
|
|
1493
|
+
showInNotification: Bool?,
|
|
1494
|
+
lookaheadCount: Int? = nil
|
|
1473
1495
|
) {
|
|
1474
1496
|
DispatchQueue.main.async { [weak self] in
|
|
1497
|
+
if let lookahead = lookaheadCount {
|
|
1498
|
+
self?.lookaheadCount = lookahead
|
|
1499
|
+
NitroPlayerLogger.log("TrackPlayerCore", "🔄 Lookahead count set to: \(lookahead)")
|
|
1500
|
+
}
|
|
1475
1501
|
self?.mediaSessionManager?.configure(
|
|
1476
1502
|
androidAutoEnabled: androidAutoEnabled,
|
|
1477
1503
|
carPlayEnabled: carPlayEnabled,
|
|
@@ -1619,9 +1645,17 @@ class TrackPlayerCore: NSObject {
|
|
|
1619
1645
|
upNextQueue.removeAll()
|
|
1620
1646
|
currentTemporaryType = .none
|
|
1621
1647
|
|
|
1622
|
-
|
|
1648
|
+
let result = playFromIndexInternalWithResult(index: originalIndex)
|
|
1649
|
+
|
|
1650
|
+
// Check if upcoming tracks need URLs
|
|
1651
|
+
checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
1652
|
+
|
|
1653
|
+
return result
|
|
1623
1654
|
}
|
|
1624
1655
|
|
|
1656
|
+
// Check if upcoming tracks need URLs after any successful skip
|
|
1657
|
+
checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
1658
|
+
|
|
1625
1659
|
return false
|
|
1626
1660
|
}
|
|
1627
1661
|
|
|
@@ -1857,6 +1891,224 @@ class TrackPlayerCore: NSObject {
|
|
|
1857
1891
|
return .none
|
|
1858
1892
|
}
|
|
1859
1893
|
|
|
1894
|
+
// MARK: - Lazy URL Loading Support
|
|
1895
|
+
|
|
1896
|
+
/**
|
|
1897
|
+
* Update entire track objects and rebuild queue if needed
|
|
1898
|
+
* Skips currently playing track to preserve gapless playback
|
|
1899
|
+
* CRITICAL: Invalidates preloaded assets and re-preloads for gapless
|
|
1900
|
+
*/
|
|
1901
|
+
func updateTracks(tracks: [TrackItem]) {
|
|
1902
|
+
DispatchQueue.main.async { [weak self] in
|
|
1903
|
+
guard let self = self else { return }
|
|
1904
|
+
|
|
1905
|
+
NitroPlayerLogger.log("TrackPlayerCore", "🔄 updateTracks: \(tracks.count) updates")
|
|
1906
|
+
|
|
1907
|
+
// Get current track ID to avoid updating it (preserves gapless playback)
|
|
1908
|
+
let currentTrackId = self.getCurrentTrack()?.id
|
|
1909
|
+
|
|
1910
|
+
// Filter out current track and validate
|
|
1911
|
+
let safeTracks = tracks.filter { track in
|
|
1912
|
+
switch true {
|
|
1913
|
+
case track.id == currentTrackId:
|
|
1914
|
+
NitroPlayerLogger.log(
|
|
1915
|
+
"TrackPlayerCore",
|
|
1916
|
+
"⚠️ Skipping update for currently playing track: \(track.id) (preserves gapless)")
|
|
1917
|
+
return false
|
|
1918
|
+
case track.url.isEmpty:
|
|
1919
|
+
NitroPlayerLogger.log(
|
|
1920
|
+
"TrackPlayerCore", "⚠️ Skipping track with empty URL: \(track.id)")
|
|
1921
|
+
return false
|
|
1922
|
+
default:
|
|
1923
|
+
return true
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
guard !safeTracks.isEmpty else {
|
|
1928
|
+
NitroPlayerLogger.log("TrackPlayerCore", "✅ No valid updates to apply")
|
|
1929
|
+
return
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// Invalidate preloaded assets for tracks with updated data
|
|
1933
|
+
// This is CRITICAL for gapless playback - old assets might use old URLs
|
|
1934
|
+
let updatedTrackIds = Set(safeTracks.map { $0.id })
|
|
1935
|
+
for trackId in updatedTrackIds {
|
|
1936
|
+
if self.preloadedAssets[trackId] != nil {
|
|
1937
|
+
NitroPlayerLogger.log(
|
|
1938
|
+
"TrackPlayerCore", "🗑️ Invalidating preloaded asset for track: \(trackId)")
|
|
1939
|
+
self.preloadedAssets.removeValue(forKey: trackId)
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// Update in PlaylistManager
|
|
1944
|
+
let affectedPlaylists = self.playlistManager.updateTracks(tracks: safeTracks)
|
|
1945
|
+
|
|
1946
|
+
// Rebuild queue if current playlist was affected
|
|
1947
|
+
if let currentId = self.currentPlaylistId,
|
|
1948
|
+
let updateCount = affectedPlaylists[currentId]
|
|
1949
|
+
{
|
|
1950
|
+
NitroPlayerLogger.log(
|
|
1951
|
+
"TrackPlayerCore",
|
|
1952
|
+
"🔄 Rebuilding queue - \(updateCount) tracks updated in current playlist")
|
|
1953
|
+
|
|
1954
|
+
// This method preserves current item
|
|
1955
|
+
self.rebuildAVQueueFromCurrentPosition()
|
|
1956
|
+
|
|
1957
|
+
// Re-preload upcoming tracks for gapless playback
|
|
1958
|
+
// CRITICAL: This restores gapless buffering after queue rebuild
|
|
1959
|
+
self.preloadUpcomingTracks(from: self.currentTrackIndex + 1)
|
|
1960
|
+
|
|
1961
|
+
NitroPlayerLogger.log("TrackPlayerCore", "✅ Queue rebuilt, gapless playback preserved")
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
NitroPlayerLogger.log(
|
|
1965
|
+
"TrackPlayerCore",
|
|
1966
|
+
"✅ Track updates complete - \(affectedPlaylists.count) playlists affected")
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/**
|
|
1971
|
+
* Get tracks by IDs from all playlists
|
|
1972
|
+
*/
|
|
1973
|
+
func getTracksById(trackIds: [String]) -> [TrackItem] {
|
|
1974
|
+
if Thread.isMainThread {
|
|
1975
|
+
return playlistManager.getTracksById(trackIds: trackIds)
|
|
1976
|
+
} else {
|
|
1977
|
+
var tracks: [TrackItem] = []
|
|
1978
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1979
|
+
tracks = self?.playlistManager.getTracksById(trackIds: trackIds) ?? []
|
|
1980
|
+
}
|
|
1981
|
+
return tracks
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
/**
|
|
1986
|
+
* Get tracks needing URLs from current playlist
|
|
1987
|
+
*/
|
|
1988
|
+
func getTracksNeedingUrls() -> [TrackItem] {
|
|
1989
|
+
if Thread.isMainThread {
|
|
1990
|
+
return getTracksNeedingUrlsInternal()
|
|
1991
|
+
} else {
|
|
1992
|
+
var tracks: [TrackItem] = []
|
|
1993
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1994
|
+
tracks = self?.getTracksNeedingUrlsInternal() ?? []
|
|
1995
|
+
}
|
|
1996
|
+
return tracks
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
private func getTracksNeedingUrlsInternal() -> [TrackItem] {
|
|
2001
|
+
guard let currentId = currentPlaylistId,
|
|
2002
|
+
let playlist = playlistManager.getPlaylist(playlistId: currentId)
|
|
2003
|
+
else {
|
|
2004
|
+
return []
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
return playlist.tracks.filter { $0.url.isEmpty }
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
/**
|
|
2011
|
+
* Get next N tracks from current position
|
|
2012
|
+
*/
|
|
2013
|
+
func getNextTracks(count: Int) -> [TrackItem] {
|
|
2014
|
+
if Thread.isMainThread {
|
|
2015
|
+
return getNextTracksInternal(count: count)
|
|
2016
|
+
} else {
|
|
2017
|
+
var tracks: [TrackItem] = []
|
|
2018
|
+
DispatchQueue.main.sync { [weak self] in
|
|
2019
|
+
tracks = self?.getNextTracksInternal(count: count) ?? []
|
|
2020
|
+
}
|
|
2021
|
+
return tracks
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
private func getNextTracksInternal(count: Int) -> [TrackItem] {
|
|
2026
|
+
let actualQueue = getActualQueueInternal()
|
|
2027
|
+
guard !actualQueue.isEmpty else { return [] }
|
|
2028
|
+
|
|
2029
|
+
guard let currentTrack = getCurrentTrack(),
|
|
2030
|
+
let currentIndex = actualQueue.firstIndex(where: { $0.id == currentTrack.id })
|
|
2031
|
+
else {
|
|
2032
|
+
return []
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
let startIndex = currentIndex + 1
|
|
2036
|
+
let endIndex = min(startIndex + count, actualQueue.count)
|
|
2037
|
+
|
|
2038
|
+
return startIndex < actualQueue.count ? Array(actualQueue[startIndex..<endIndex]) : []
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* Get current track index in playlist
|
|
2043
|
+
*/
|
|
2044
|
+
func getCurrentTrackIndex() -> Int {
|
|
2045
|
+
if Thread.isMainThread {
|
|
2046
|
+
return currentTrackIndex
|
|
2047
|
+
} else {
|
|
2048
|
+
var index = -1
|
|
2049
|
+
DispatchQueue.main.sync { [weak self] in
|
|
2050
|
+
index = self?.currentTrackIndex ?? -1
|
|
2051
|
+
}
|
|
2052
|
+
return index
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
/**
|
|
2057
|
+
* Callback for tracks needing update
|
|
2058
|
+
*/
|
|
2059
|
+
typealias OnTracksNeedUpdateCallback = ([TrackItem], Int) -> Void
|
|
2060
|
+
|
|
2061
|
+
// Add to class properties
|
|
2062
|
+
private var onTracksNeedUpdateListeners: [(callback: OnTracksNeedUpdateCallback, isAlive: Bool)] =
|
|
2063
|
+
[]
|
|
2064
|
+
private let tracksNeedUpdateQueue = DispatchQueue(
|
|
2065
|
+
label: "com.nitroplayer.tracksneedupdate", attributes: .concurrent)
|
|
2066
|
+
|
|
2067
|
+
/**
|
|
2068
|
+
* Register listener for when tracks need update
|
|
2069
|
+
*/
|
|
2070
|
+
func addOnTracksNeedUpdateListener(callback: @escaping OnTracksNeedUpdateCallback) {
|
|
2071
|
+
tracksNeedUpdateQueue.async(flags: .barrier) { [weak self] in
|
|
2072
|
+
self?.onTracksNeedUpdateListeners.append((callback: callback, isAlive: true))
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* Notify listeners that tracks need updating
|
|
2078
|
+
*/
|
|
2079
|
+
private func notifyTracksNeedUpdate(tracks: [TrackItem], lookahead: Int) {
|
|
2080
|
+
tracksNeedUpdateQueue.async(flags: .barrier) { [weak self] in
|
|
2081
|
+
guard let self = self else { return }
|
|
2082
|
+
|
|
2083
|
+
// Clean up dead listeners
|
|
2084
|
+
self.onTracksNeedUpdateListeners.removeAll { !$0.isAlive }
|
|
2085
|
+
let liveCallbacks = self.onTracksNeedUpdateListeners.map { $0.callback }
|
|
2086
|
+
|
|
2087
|
+
if !liveCallbacks.isEmpty {
|
|
2088
|
+
DispatchQueue.main.async {
|
|
2089
|
+
for callback in liveCallbacks {
|
|
2090
|
+
callback(tracks, lookahead)
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* Check if upcoming tracks need URLs and notify listeners
|
|
2099
|
+
* Call this in playerItemDidPlayToEndTime or after skip operations
|
|
2100
|
+
*/
|
|
2101
|
+
private func checkUpcomingTracksForUrls(lookahead: Int = 5) {
|
|
2102
|
+
let nextTracks = getNextTracksInternal(count: lookahead)
|
|
2103
|
+
let tracksNeedingUrls = nextTracks.filter { $0.url.isEmpty }
|
|
2104
|
+
|
|
2105
|
+
if !tracksNeedingUrls.isEmpty {
|
|
2106
|
+
NitroPlayerLogger.log(
|
|
2107
|
+
"TrackPlayerCore", "⚠️ \(tracksNeedingUrls.count) upcoming tracks need URLs")
|
|
2108
|
+
notifyTracksNeedUpdate(tracks: tracksNeedingUrls, lookahead: lookahead)
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
1860
2112
|
// MARK: - Cleanup
|
|
1861
2113
|
|
|
1862
2114
|
deinit {
|
|
@@ -274,6 +274,74 @@ class PlaylistManager {
|
|
|
274
274
|
return true
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Update entire track objects across all playlists
|
|
279
|
+
* Matches by track.id and replaces the entire track object
|
|
280
|
+
* @param tracks Array of full TrackItem objects to update
|
|
281
|
+
* @return Dictionary of playlistId -> count of tracks updated
|
|
282
|
+
*/
|
|
283
|
+
func updateTracks(tracks: [TrackItem]) -> [String: Int] {
|
|
284
|
+
let tracksMap = Dictionary(uniqueKeysWithValues: tracks.map { ($0.id, $0) })
|
|
285
|
+
var affectedPlaylists: [String: Int] = [:]
|
|
286
|
+
|
|
287
|
+
queue.sync {
|
|
288
|
+
for (playlistId, playlist) in playlists {
|
|
289
|
+
var updateCount = 0
|
|
290
|
+
let newTracks = playlist.tracks.map { track -> TrackItem in
|
|
291
|
+
if let updatedTrack = tracksMap[track.id] {
|
|
292
|
+
updateCount += 1
|
|
293
|
+
return updatedTrack
|
|
294
|
+
}
|
|
295
|
+
return track
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if updateCount > 0 {
|
|
299
|
+
affectedPlaylists[playlistId] = updateCount
|
|
300
|
+
playlists[playlistId] = PlaylistModel(
|
|
301
|
+
id: playlist.id,
|
|
302
|
+
name: playlist.name,
|
|
303
|
+
description: playlist.description,
|
|
304
|
+
artwork: playlist.artwork,
|
|
305
|
+
tracks: newTracks
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if !affectedPlaylists.isEmpty {
|
|
312
|
+
scheduleSave()
|
|
313
|
+
affectedPlaylists.keys.forEach { playlistId in
|
|
314
|
+
notifyPlaylistChanged(playlistId, .update)
|
|
315
|
+
}
|
|
316
|
+
notifyPlaylistsChanged(.update)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return affectedPlaylists
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get tracks by IDs from all playlists
|
|
324
|
+
* @param trackIds Array of track IDs to fetch
|
|
325
|
+
* @return Array of matching TrackItem objects
|
|
326
|
+
*/
|
|
327
|
+
func getTracksById(trackIds: [String]) -> [TrackItem] {
|
|
328
|
+
let trackIdSet = Set(trackIds)
|
|
329
|
+
var foundTracks: [String: TrackItem] = [:]
|
|
330
|
+
|
|
331
|
+
queue.sync {
|
|
332
|
+
for playlist in playlists.values {
|
|
333
|
+
for track in playlist.tracks {
|
|
334
|
+
if trackIdSet.contains(track.id) && foundTracks[track.id] == nil {
|
|
335
|
+
foundTracks[track.id] = track
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Return in same order as requested
|
|
342
|
+
return trackIds.compactMap { foundTracks[$0] }
|
|
343
|
+
}
|
|
344
|
+
|
|
277
345
|
/**
|
|
278
346
|
* Get the current playlist ID
|
|
279
347
|
*/
|
|
@@ -44,4 +44,51 @@ export interface TrackPlayer extends HybridObject<{
|
|
|
44
44
|
onAndroidAutoConnectionChange(callback: (connected: boolean) => void): void;
|
|
45
45
|
isAndroidAutoConnected(): boolean;
|
|
46
46
|
setVolume(volume: number): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Update entire track objects across all playlists
|
|
49
|
+
* Matches by track.id and updates all properties (url, artwork, title, etc.)
|
|
50
|
+
* Note: Empty string "" is valid for TrackItem.url to support lazy loading
|
|
51
|
+
* @param tracks Array of full TrackItem objects to update
|
|
52
|
+
* @returns Promise that resolves when updates complete
|
|
53
|
+
*/
|
|
54
|
+
updateTracks(tracks: TrackItem[]): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Get tracks by IDs from all playlists
|
|
57
|
+
* @param trackIds Array of track IDs to fetch
|
|
58
|
+
* @returns Promise resolving to array of matching tracks
|
|
59
|
+
*/
|
|
60
|
+
getTracksById(trackIds: string[]): Promise<TrackItem[]>;
|
|
61
|
+
/**
|
|
62
|
+
* Get tracks with missing/empty URLs from current playlist
|
|
63
|
+
* @returns Promise resolving to array of tracks needing URLs
|
|
64
|
+
*/
|
|
65
|
+
getTracksNeedingUrls(): Promise<TrackItem[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Get next N tracks from current position in playlist
|
|
68
|
+
* Useful for preloading URLs before they're needed
|
|
69
|
+
* @param count Number of upcoming tracks to return
|
|
70
|
+
* @returns Promise resolving to array of next tracks
|
|
71
|
+
*/
|
|
72
|
+
getNextTracks(count: number): Promise<TrackItem[]>;
|
|
73
|
+
/**
|
|
74
|
+
* Get current track index in the active playlist
|
|
75
|
+
* @returns Promise resolving to 0-based index, or -1 if no track playing
|
|
76
|
+
*/
|
|
77
|
+
getCurrentTrackIndex(): Promise<number>;
|
|
78
|
+
/**
|
|
79
|
+
* Register callback that fires when tracks will be needed soon
|
|
80
|
+
* Useful for proactive URL resolution in Android Auto/CarPlay
|
|
81
|
+
* @param callback Function called with tracks needing URLs and lookahead count
|
|
82
|
+
*/
|
|
83
|
+
onTracksNeedUpdate(callback: (tracks: TrackItem[], lookahead: number) => void): void;
|
|
84
|
+
/**
|
|
85
|
+
* Get the current track index in the active playlist
|
|
86
|
+
* @returns Promise resolving to 0-based index, or -1 if no track playing
|
|
87
|
+
*/
|
|
88
|
+
setPlaybackSpeed(speed: number): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Get the current playback speed
|
|
91
|
+
* @returns Promise resolving to playback speed
|
|
92
|
+
*/
|
|
93
|
+
getPlaybackSpeed(): Promise<number>;
|
|
47
94
|
}
|
|
@@ -33,4 +33,9 @@ export interface PlayerConfig {
|
|
|
33
33
|
androidAutoEnabled?: boolean;
|
|
34
34
|
carPlayEnabled?: boolean;
|
|
35
35
|
showInNotification?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Number of upcoming tracks to preload URLs for (default: 5)
|
|
38
|
+
* Higher values = more proactive loading, but more network requests
|
|
39
|
+
*/
|
|
40
|
+
lookaheadCount?: number;
|
|
36
41
|
}
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
#include "JFunc_void_TrackPlayerState_std__optional_Reason_.hpp"
|
|
34
34
|
#include "JFunc_void_double_double.hpp"
|
|
35
35
|
#include "JFunc_void_double_double_std__optional_bool_.hpp"
|
|
36
|
+
#include "JFunc_void_std__vector_TrackItem__double.hpp"
|
|
36
37
|
#include <NitroModules/DefaultConstructableObject.hpp>
|
|
37
38
|
|
|
38
39
|
namespace margelo::nitro::nitroplayer {
|
|
@@ -62,6 +63,7 @@ int initialize(JavaVM* vm) {
|
|
|
62
63
|
margelo::nitro::nitroplayer::JFunc_void_TrackPlayerState_std__optional_Reason__cxx::registerNatives();
|
|
63
64
|
margelo::nitro::nitroplayer::JFunc_void_double_double_cxx::registerNatives();
|
|
64
65
|
margelo::nitro::nitroplayer::JFunc_void_double_double_std__optional_bool__cxx::registerNatives();
|
|
66
|
+
margelo::nitro::nitroplayer::JFunc_void_std__vector_TrackItem__double_cxx::registerNatives();
|
|
65
67
|
|
|
66
68
|
// Register Nitro Hybrid Objects
|
|
67
69
|
HybridObjectRegistry::registerHybridObjectConstructor(
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// JFunc_void_std__vector_TrackItem__double.hpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © 2026 Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <fbjni/fbjni.h>
|
|
11
|
+
#include <functional>
|
|
12
|
+
|
|
13
|
+
#include "TrackItem.hpp"
|
|
14
|
+
#include <vector>
|
|
15
|
+
#include <functional>
|
|
16
|
+
#include <NitroModules/JNICallable.hpp>
|
|
17
|
+
#include "JTrackItem.hpp"
|
|
18
|
+
#include <string>
|
|
19
|
+
#include <NitroModules/Null.hpp>
|
|
20
|
+
#include <variant>
|
|
21
|
+
#include <optional>
|
|
22
|
+
#include "JVariant_NullType_String.hpp"
|
|
23
|
+
#include <NitroModules/JNull.hpp>
|
|
24
|
+
#include <NitroModules/AnyMap.hpp>
|
|
25
|
+
#include <NitroModules/JAnyMap.hpp>
|
|
26
|
+
|
|
27
|
+
namespace margelo::nitro::nitroplayer {
|
|
28
|
+
|
|
29
|
+
using namespace facebook;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Represents the Java/Kotlin callback `(tracks: Array<TrackItem>, lookahead: Double) -> Unit`.
|
|
33
|
+
* This can be passed around between C++ and Java/Kotlin.
|
|
34
|
+
*/
|
|
35
|
+
struct JFunc_void_std__vector_TrackItem__double: public jni::JavaClass<JFunc_void_std__vector_TrackItem__double> {
|
|
36
|
+
public:
|
|
37
|
+
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__double;";
|
|
38
|
+
|
|
39
|
+
public:
|
|
40
|
+
/**
|
|
41
|
+
* Invokes the function this `JFunc_void_std__vector_TrackItem__double` instance holds through JNI.
|
|
42
|
+
*/
|
|
43
|
+
void invoke(const std::vector<TrackItem>& tracks, double lookahead) const {
|
|
44
|
+
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JArrayClass<JTrackItem>> /* tracks */, double /* lookahead */)>("invoke");
|
|
45
|
+
method(self(), [&]() {
|
|
46
|
+
size_t __size = tracks.size();
|
|
47
|
+
jni::local_ref<jni::JArrayClass<JTrackItem>> __array = jni::JArrayClass<JTrackItem>::newArray(__size);
|
|
48
|
+
for (size_t __i = 0; __i < __size; __i++) {
|
|
49
|
+
const auto& __element = tracks[__i];
|
|
50
|
+
auto __elementJni = JTrackItem::fromCpp(__element);
|
|
51
|
+
__array->setElement(__i, *__elementJni);
|
|
52
|
+
}
|
|
53
|
+
return __array;
|
|
54
|
+
}(), lookahead);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* An implementation of Func_void_std__vector_TrackItem__double that is backed by a C++ implementation (using `std::function<...>`)
|
|
60
|
+
*/
|
|
61
|
+
class JFunc_void_std__vector_TrackItem__double_cxx final: public jni::HybridClass<JFunc_void_std__vector_TrackItem__double_cxx, JFunc_void_std__vector_TrackItem__double> {
|
|
62
|
+
public:
|
|
63
|
+
static jni::local_ref<JFunc_void_std__vector_TrackItem__double::javaobject> fromCpp(const std::function<void(const std::vector<TrackItem>& /* tracks */, double /* lookahead */)>& func) {
|
|
64
|
+
return JFunc_void_std__vector_TrackItem__double_cxx::newObjectCxxArgs(func);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public:
|
|
68
|
+
/**
|
|
69
|
+
* Invokes the C++ `std::function<...>` this `JFunc_void_std__vector_TrackItem__double_cxx` instance holds.
|
|
70
|
+
*/
|
|
71
|
+
void invoke_cxx(jni::alias_ref<jni::JArrayClass<JTrackItem>> tracks, double lookahead) {
|
|
72
|
+
_func([&]() {
|
|
73
|
+
size_t __size = tracks->size();
|
|
74
|
+
std::vector<TrackItem> __vector;
|
|
75
|
+
__vector.reserve(__size);
|
|
76
|
+
for (size_t __i = 0; __i < __size; __i++) {
|
|
77
|
+
auto __element = tracks->getElement(__i);
|
|
78
|
+
__vector.push_back(__element->toCpp());
|
|
79
|
+
}
|
|
80
|
+
return __vector;
|
|
81
|
+
}(), lookahead);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public:
|
|
85
|
+
[[nodiscard]]
|
|
86
|
+
inline const std::function<void(const std::vector<TrackItem>& /* tracks */, double /* lookahead */)>& getFunction() const {
|
|
87
|
+
return _func;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public:
|
|
91
|
+
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__double_cxx;";
|
|
92
|
+
static void registerNatives() {
|
|
93
|
+
registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_std__vector_TrackItem__double_cxx::invoke_cxx)});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private:
|
|
97
|
+
explicit JFunc_void_std__vector_TrackItem__double_cxx(const std::function<void(const std::vector<TrackItem>& /* tracks */, double /* lookahead */)>& func): _func(func) { }
|
|
98
|
+
|
|
99
|
+
private:
|
|
100
|
+
friend HybridBase;
|
|
101
|
+
std::function<void(const std::vector<TrackItem>& /* tracks */, double /* lookahead */)> _func;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
} // namespace margelo::nitro::nitroplayer
|