react-native-nitro-player 1.6.0-alpha.1 → 1.7.0-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/README.md +6 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +9 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +23 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +23 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +34 -17
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +6 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +22 -22
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +78 -7
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +19 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +33 -19
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +0 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +9 -5
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +96 -56
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +62 -22
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/TrackItemJson.kt +60 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +9 -9
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/AuthAwareHttpDataSourceFactory.kt +12 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/ExoPlayerBuilder.kt +4 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +99 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +22 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +85 -70
- package/android/src/main/res/values/strings.xml +7 -0
- package/ios/HybridDownloadManager.swift +13 -3
- package/ios/HybridTrackPlayer.swift +8 -2
- package/ios/core/TrackPlayerCast.swift +32 -1
- package/ios/core/TrackPlayerCore.swift +19 -3
- package/ios/core/TrackPlayerListener.swift +15 -16
- package/ios/core/TrackPlayerNotify.swift +11 -11
- package/ios/core/TrackPlayerPlayback.swift +54 -16
- package/ios/core/TrackPlayerQueueBuild.swift +14 -2
- package/ios/core/TrackPlayerTempQueue.swift +1 -0
- package/ios/core/TrackPlayerUrlLoader.swift +11 -3
- package/ios/download/DownloadDatabase.swift +16 -5
- package/ios/download/DownloadFileManager.swift +12 -15
- package/ios/download/DownloadManagerCore.swift +61 -16
- package/ios/equalizer/EqualizerCore.swift +37 -9
- package/ios/media/MediaSessionManager.swift +89 -4
- package/ios/playlist/PlaylistManager.swift +47 -46
- package/ios/queue/HybridPlayerQueue.swift +28 -7
- package/lib/hooks/callbackManager.d.ts +9 -0
- package/lib/hooks/callbackManager.js +33 -0
- package/lib/hooks/useActualQueue.js +18 -6
- package/lib/hooks/useAndroidAutoConnection.js +2 -4
- package/lib/hooks/useAudioDevices.js +21 -24
- package/lib/hooks/useDownloadActions.js +33 -11
- package/lib/hooks/useDownloadProgress.js +20 -14
- package/lib/hooks/useDownloadStorage.js +1 -1
- package/lib/hooks/useNowPlaying.js +32 -6
- package/lib/hooks/useOnSeek.js +2 -2
- package/lib/hooks/usePlaylist.js +6 -15
- package/lib/specs/TrackPlayer.nitro.d.ts +6 -2
- package/lib/types/PlayerQueue.d.ts +8 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_TrackItem.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +5 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +9 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TrackItem.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +9 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +8 -2
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +22 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -0
- package/nitrogen/generated/ios/swift/Func_void_TrackItem.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +16 -0
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +37 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +1 -0
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +9 -1
- package/package.json +1 -1
- package/src/hooks/callbackManager.ts +44 -0
- package/src/hooks/useActualQueue.ts +18 -6
- package/src/hooks/useAndroidAutoConnection.ts +6 -6
- package/src/hooks/useAudioDevices.ts +23 -24
- package/src/hooks/useDownloadActions.ts +35 -11
- package/src/hooks/useDownloadProgress.ts +22 -19
- package/src/hooks/useDownloadStorage.ts +4 -3
- package/src/hooks/useNowPlaying.ts +34 -6
- package/src/hooks/useOnSeek.ts +2 -2
- package/src/hooks/usePlaylist.ts +6 -14
- package/src/specs/TrackPlayer.nitro.ts +6 -2
- package/src/types/PlayerQueue.ts +8 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroPlayerNotificationProvider.kt +0 -200
- package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/Queue.kt +0 -94
- package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/QueueManager.kt +0 -143
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Command-style methods return **`Promise<void>`** (or another **`Promise`**) and
|
|
|
52
52
|
| `skipToNext()` | Both | **Async**. Skips to the next track in the queue. |
|
|
53
53
|
| `skipToPrevious()` | Both | **Async**. Skips to the previous track. |
|
|
54
54
|
| `seek(position)` | Both | **Async**. Seeks to a specific time position in seconds. |
|
|
55
|
-
| `setPlaybackSpeed(speed)` | Both | **Async**. Sets playback speed
|
|
55
|
+
| `setPlaybackSpeed(speed)` | Both | **Async**. Sets playback speed; rejects non-finite and non-positive values. |
|
|
56
56
|
| `getPlaybackSpeed()` | Both | **Async**. Gets the current playback speed. |
|
|
57
57
|
| `setVolume(0-100)` | Both | **Async**. Sets playback volume (0-100). |
|
|
58
58
|
| `setRepeatMode(mode)` | Both | **Async**. Sets repeat mode (`off`, `track`, `Playlist`). |
|
|
@@ -131,9 +131,13 @@ await TrackPlayer.configure({
|
|
|
131
131
|
androidAutoEnabled: true,
|
|
132
132
|
carPlayEnabled: false,
|
|
133
133
|
showInNotification: true,
|
|
134
|
+
remoteSkipForwardInterval: 15,
|
|
135
|
+
remoteSkipBackwardInterval: 15,
|
|
134
136
|
})
|
|
135
137
|
```
|
|
136
138
|
|
|
139
|
+
Remote skip intervals are in seconds. iOS uses them for AirPods/Control Center fixed skip buttons and exposes remote playback rates `0.5, 1.0, 1.25, 1.5, 1.75, 2.0`; Android uses them for the Media3 seek back/forward notification buttons and does not expose a playback-rate media button. Press-and-hold continuous seek is not implemented on either platform. Custom intervals apply to local playback; during a Google Cast session the receiver skips by a fixed 15 seconds.
|
|
140
|
+
|
|
137
141
|
### 2. Create Playlists
|
|
138
142
|
|
|
139
143
|
```typescript
|
|
@@ -1227,6 +1231,7 @@ AndroidAutoMediaLibraryHelper.set({
|
|
|
1227
1231
|
|
|
1228
1232
|
- ✅ **Playlist Management**: Create, update, and manage multiple playlists
|
|
1229
1233
|
- ✅ **Playback Controls**: Play, pause, seek, skip tracks
|
|
1234
|
+
- ✅ **Remote Controls**: AirPods, lock screen, and notification skip controls
|
|
1230
1235
|
- ✅ **Volume Control**: Adjust playback volume (0-100)
|
|
1231
1236
|
- ✅ **React Hooks**: Built-in hooks for reactive state management
|
|
1232
1237
|
- ✅ **Event Listeners**: Listen to track changes, state changes, and more
|
|
@@ -67,11 +67,19 @@ class HybridAudioDevices : HybridAudioDevicesSpec() {
|
|
|
67
67
|
audioManager.setCommunicationDevice(device)
|
|
68
68
|
} else {
|
|
69
69
|
when (device.type) {
|
|
70
|
-
AudioDeviceInfo.TYPE_BLUETOOTH_SCO
|
|
70
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> {
|
|
71
71
|
audioManager.startBluetoothSco()
|
|
72
72
|
audioManager.isBluetoothScoOn = true
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// A2DP is the default media route — routing it through SCO
|
|
76
|
+
// would drop music to narrowband call quality
|
|
77
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> {
|
|
78
|
+
audioManager.stopBluetoothSco()
|
|
79
|
+
audioManager.isBluetoothScoOn = false
|
|
80
|
+
audioManager.isSpeakerphoneOn = false
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> {
|
|
76
84
|
audioManager.isSpeakerphoneOn = true
|
|
77
85
|
}
|
|
@@ -129,9 +129,29 @@ class HybridDownloadManager : HybridDownloadManagerSpec() {
|
|
|
129
129
|
override fun getPlaybackSourcePreference(): PlaybackSource = core.getPlaybackSourcePreference()
|
|
130
130
|
|
|
131
131
|
// ── Events ────────────────────────────────────────────────────────────────
|
|
132
|
-
|
|
132
|
+
private val listenerIds = java.util.concurrent.CopyOnWriteArrayList<Pair<String, Long>>()
|
|
133
133
|
|
|
134
|
-
override fun
|
|
134
|
+
override fun onDownloadProgress(callback: (progress: DownloadProgress) -> Unit) {
|
|
135
|
+
listenerIds.add("progress" to core.addProgressCallback(callback))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
override fun onDownloadStateChange(callback: (downloadId: String, trackId: String, state: DownloadState, error: DownloadError?) -> Unit) {
|
|
139
|
+
listenerIds.add("state" to core.addStateChangeCallback(callback))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
override fun onDownloadComplete(callback: (downloadedTrack: DownloadedTrack) -> Unit) {
|
|
143
|
+
listenerIds.add("complete" to core.addCompleteCallback(callback))
|
|
144
|
+
}
|
|
135
145
|
|
|
136
|
-
override fun
|
|
146
|
+
override fun dispose() {
|
|
147
|
+
super.dispose()
|
|
148
|
+
listenerIds.forEach { (type, id) ->
|
|
149
|
+
when (type) {
|
|
150
|
+
"progress" -> core.removeProgressCallback(id)
|
|
151
|
+
"state" -> core.removeStateChangeCallback(id)
|
|
152
|
+
"complete" -> core.removeCompleteCallback(id)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
listenerIds.clear()
|
|
156
|
+
}
|
|
137
157
|
}
|
|
@@ -63,9 +63,29 @@ class HybridEqualizer : HybridEqualizerSpec() {
|
|
|
63
63
|
override fun reset(): Promise<Unit> = Promise.async { core.reset() }
|
|
64
64
|
|
|
65
65
|
// ── Events ────────────────────────────────────────────────────────────────
|
|
66
|
-
|
|
66
|
+
private val listenerIds = java.util.concurrent.CopyOnWriteArrayList<Pair<String, Long>>()
|
|
67
67
|
|
|
68
|
-
override fun
|
|
68
|
+
override fun onEnabledChange(callback: (enabled: Boolean) -> Unit) {
|
|
69
|
+
listenerIds.add("enabled" to core.addOnEnabledChangeListener(callback))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override fun onBandChange(callback: (bands: Array<EqualizerBand>) -> Unit) {
|
|
73
|
+
listenerIds.add("band" to core.addOnBandChangeListener(callback))
|
|
74
|
+
}
|
|
69
75
|
|
|
70
|
-
override fun onPresetChange(callback: (presetName: Variant_NullType_String?) -> Unit)
|
|
76
|
+
override fun onPresetChange(callback: (presetName: Variant_NullType_String?) -> Unit) {
|
|
77
|
+
listenerIds.add("preset" to core.addOnPresetChangeListener(callback))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
override fun dispose() {
|
|
81
|
+
super.dispose()
|
|
82
|
+
listenerIds.forEach { (type, id) ->
|
|
83
|
+
when (type) {
|
|
84
|
+
"enabled" -> core.removeOnEnabledChangeListener(id)
|
|
85
|
+
"band" -> core.removeOnBandChangeListener(id)
|
|
86
|
+
"preset" -> core.removeOnPresetChangeListener(id)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
listenerIds.clear()
|
|
90
|
+
}
|
|
71
91
|
}
|
|
@@ -11,7 +11,6 @@ import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
|
|
|
11
11
|
import com.margelo.nitro.nitroplayer.core.loadPlaylistOnQueue
|
|
12
12
|
import com.margelo.nitro.nitroplayer.core.updatePlaylist
|
|
13
13
|
import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
|
|
14
|
-
import java.util.UUID
|
|
15
14
|
import com.margelo.nitro.nitroplayer.playlist.Playlist as InternalPlaylist
|
|
16
15
|
|
|
17
16
|
@DoNotStrip
|
|
@@ -29,7 +28,6 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
private val playlistsChangeListeners = java.util.concurrent.CopyOnWriteArrayList<() -> Unit>()
|
|
32
|
-
private val playlistChangeListeners = java.util.concurrent.ConcurrentHashMap<String, () -> Unit>()
|
|
33
31
|
|
|
34
32
|
// ── Playlist CRUD ─────────────────────────────────────────────────────────
|
|
35
33
|
|
|
@@ -41,7 +39,9 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
41
39
|
|
|
42
40
|
override fun deletePlaylist(playlistId: String): Promise<Unit> =
|
|
43
41
|
Promise.async {
|
|
44
|
-
playlistManager.deletePlaylist(playlistId)
|
|
42
|
+
if (!playlistManager.deletePlaylist(playlistId)) {
|
|
43
|
+
throw IllegalArgumentException("Playlist not found: $playlistId")
|
|
44
|
+
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
override fun updatePlaylist(
|
|
@@ -51,7 +51,9 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
51
51
|
artwork: String?,
|
|
52
52
|
): Promise<Unit> =
|
|
53
53
|
Promise.async {
|
|
54
|
-
playlistManager.updatePlaylist(playlistId, name, description, artwork)
|
|
54
|
+
if (!playlistManager.updatePlaylist(playlistId, name, description, artwork)) {
|
|
55
|
+
throw IllegalArgumentException("Playlist not found: $playlistId")
|
|
56
|
+
}
|
|
55
57
|
core.updatePlaylist(playlistId)
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -74,7 +76,9 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
74
76
|
index: Double?,
|
|
75
77
|
): Promise<Unit> =
|
|
76
78
|
Promise.async {
|
|
77
|
-
playlistManager.addTrackToPlaylist(playlistId, track, index?.toInt())
|
|
79
|
+
if (!playlistManager.addTrackToPlaylist(playlistId, track, index?.toInt())) {
|
|
80
|
+
throw IllegalArgumentException("Playlist not found: $playlistId")
|
|
81
|
+
}
|
|
78
82
|
core.updatePlaylist(playlistId)
|
|
79
83
|
}
|
|
80
84
|
|
|
@@ -84,7 +88,9 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
84
88
|
index: Double?,
|
|
85
89
|
): Promise<Unit> =
|
|
86
90
|
Promise.async {
|
|
87
|
-
playlistManager.addTracksToPlaylist(playlistId, tracks.toList(), index?.toInt())
|
|
91
|
+
if (!playlistManager.addTracksToPlaylist(playlistId, tracks.toList(), index?.toInt())) {
|
|
92
|
+
throw IllegalArgumentException("Playlist not found: $playlistId")
|
|
93
|
+
}
|
|
88
94
|
core.updatePlaylist(playlistId)
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -93,7 +99,9 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
93
99
|
trackId: String,
|
|
94
100
|
): Promise<Unit> =
|
|
95
101
|
Promise.async {
|
|
96
|
-
playlistManager.removeTrackFromPlaylist(playlistId, trackId)
|
|
102
|
+
if (!playlistManager.removeTrackFromPlaylist(playlistId, trackId)) {
|
|
103
|
+
throw IllegalArgumentException("Track $trackId not found in playlist $playlistId")
|
|
104
|
+
}
|
|
97
105
|
core.updatePlaylist(playlistId)
|
|
98
106
|
}
|
|
99
107
|
|
|
@@ -103,7 +111,9 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
103
111
|
newIndex: Double,
|
|
104
112
|
): Promise<Unit> =
|
|
105
113
|
Promise.async {
|
|
106
|
-
playlistManager.reorderTrackInPlaylist(playlistId, trackId, newIndex.toInt())
|
|
114
|
+
if (!playlistManager.reorderTrackInPlaylist(playlistId, trackId, newIndex.toInt())) {
|
|
115
|
+
throw IllegalArgumentException("Invalid reorder for track $trackId in playlist $playlistId")
|
|
116
|
+
}
|
|
107
117
|
core.updatePlaylist(playlistId)
|
|
108
118
|
}
|
|
109
119
|
|
|
@@ -122,8 +132,10 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
122
132
|
core.enqueue {
|
|
123
133
|
if (playlistManager.loadPlaylist(playlistId, startIndex)) {
|
|
124
134
|
core.loadPlaylistOnQueue(playlistId, startIndex)
|
|
135
|
+
promise.resolve(Unit)
|
|
136
|
+
} else {
|
|
137
|
+
promise.reject(IllegalArgumentException("Invalid playlist or index: $playlistId"))
|
|
125
138
|
}
|
|
126
|
-
promise.resolve(Unit)
|
|
127
139
|
}
|
|
128
140
|
return promise
|
|
129
141
|
}
|
|
@@ -148,14 +160,19 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
148
160
|
}
|
|
149
161
|
|
|
150
162
|
override fun onPlaylistChanged(callback: (playlistId: String, playlist: Playlist, operation: QueueOperation?) -> Unit) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
163
|
+
// Manager-level listener: covers playlists created after registration,
|
|
164
|
+
// and one remover per callback instead of one per playlist
|
|
165
|
+
val removeListener =
|
|
166
|
+
playlistManager.addAnyPlaylistChangeListener { playlist, operation ->
|
|
167
|
+
callback(playlist.id, playlist.toPlaylist(), operation)
|
|
168
|
+
}
|
|
169
|
+
playlistsChangeListeners.add(removeListener)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
override fun dispose() {
|
|
173
|
+
super.dispose()
|
|
174
|
+
playlistsChangeListeners.forEach { it() }
|
|
175
|
+
playlistsChangeListeners.clear()
|
|
159
176
|
}
|
|
160
177
|
|
|
161
178
|
// ── Helper ────────────────────────────────────────────────────────────────
|
|
@@ -178,6 +178,11 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
|
|
|
178
178
|
listenerIds += "onAndroidAutoConnectionChange" to id
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
override fun appStartedWithNotification(callback: (track: TrackItem) -> Unit) {
|
|
182
|
+
val id = core.addOnNotificationLaunchListener(callback)
|
|
183
|
+
listenerIds += "appStartedWithNotification" to id
|
|
184
|
+
}
|
|
185
|
+
|
|
181
186
|
override fun onTracksNeedUpdate(callback: (tracks: Array<TrackItem>, lookahead: Double) -> Unit) {
|
|
182
187
|
val id =
|
|
183
188
|
core.addOnTracksNeedUpdateListener { tracks, lookahead ->
|
|
@@ -213,6 +218,7 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
|
|
|
213
218
|
"onTracksNeedUpdate" -> core.removeOnTracksNeedUpdateListener(id)
|
|
214
219
|
"onTemporaryQueueChange" -> core.removeOnTemporaryQueueChangeListener(id)
|
|
215
220
|
"onTimedMetadata" -> core.removeOnTimedMetadataListener(id)
|
|
221
|
+
"appStartedWithNotification" -> core.removeOnNotificationLaunchListener(id)
|
|
216
222
|
}
|
|
217
223
|
}
|
|
218
224
|
listenerIds.clear()
|
|
@@ -144,29 +144,29 @@ class AndroidAutoConnectionDetector(
|
|
|
144
144
|
return
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
response.use { cursor ->
|
|
148
|
+
val carConnectionTypeColumn = cursor.getColumnIndex(CAR_CONNECTION_STATE)
|
|
149
|
+
if (carConnectionTypeColumn < 0) {
|
|
150
|
+
NitroPlayerLogger.log("AndroidAutoConnection", "⚠️ Connection type column missing, treating as disconnected")
|
|
151
|
+
notifyCarDisconnected()
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!cursor.moveToNext()) {
|
|
156
|
+
NitroPlayerLogger.log("AndroidAutoConnection", "⚠️ Empty response, treating as disconnected")
|
|
157
|
+
notifyCarDisconnected()
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
val connectionState = cursor.getInt(carConnectionTypeColumn)
|
|
162
|
+
NitroPlayerLogger.log("AndroidAutoConnection", "📊 Connection state queried: $connectionState")
|
|
163
|
+
|
|
164
|
+
if (connectionState == CONNECTION_TYPE_NOT_CONNECTED) {
|
|
165
|
+
notifyCarDisconnected()
|
|
166
|
+
} else {
|
|
167
|
+
notifyCarConnected(connectionState)
|
|
168
|
+
}
|
|
158
169
|
}
|
|
159
|
-
|
|
160
|
-
val connectionState = response.getInt(carConnectionTypeColumn)
|
|
161
|
-
NitroPlayerLogger.log("AndroidAutoConnection", "📊 Connection state queried: $connectionState")
|
|
162
|
-
|
|
163
|
-
if (connectionState == CONNECTION_TYPE_NOT_CONNECTED) {
|
|
164
|
-
notifyCarDisconnected()
|
|
165
|
-
} else {
|
|
166
|
-
notifyCarConnected(connectionState)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
response.close()
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
}
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
package com.margelo.nitro.nitroplayer.core
|
|
4
4
|
|
|
5
|
+
import android.app.Activity
|
|
6
|
+
import android.app.Application
|
|
5
7
|
import android.content.ComponentName
|
|
6
8
|
import android.content.Context
|
|
7
9
|
import android.content.Intent
|
|
8
10
|
import android.content.ServiceConnection
|
|
11
|
+
import android.os.Bundle
|
|
9
12
|
import android.os.Handler
|
|
10
13
|
import android.os.IBinder
|
|
11
14
|
import android.os.Looper
|
|
15
|
+
import com.margelo.nitro.NitroModules
|
|
12
16
|
import com.margelo.nitro.nitroplayer.Reason
|
|
13
17
|
import com.margelo.nitro.nitroplayer.RepeatMode
|
|
14
18
|
import com.margelo.nitro.nitroplayer.TimedMetadata
|
|
@@ -16,6 +20,7 @@ import com.margelo.nitro.nitroplayer.TrackItem
|
|
|
16
20
|
import com.margelo.nitro.nitroplayer.TrackPlayerState
|
|
17
21
|
import com.margelo.nitro.nitroplayer.connection.AndroidAutoConnectionDetector
|
|
18
22
|
import com.margelo.nitro.nitroplayer.download.DownloadManagerCore
|
|
23
|
+
import com.margelo.nitro.nitroplayer.media.ExoPlayerBuilder
|
|
19
24
|
import com.margelo.nitro.nitroplayer.media.MediaLibraryManager
|
|
20
25
|
import com.margelo.nitro.nitroplayer.media.MediaSessionManager
|
|
21
26
|
import com.margelo.nitro.nitroplayer.media.NitroPlayerPlaybackService
|
|
@@ -65,6 +70,9 @@ class TrackPlayerCore private constructor(
|
|
|
65
70
|
|
|
66
71
|
@Volatile internal var currentRepeatMode: RepeatMode = RepeatMode.OFF
|
|
67
72
|
internal var lookaheadCount: Int = 5
|
|
73
|
+
|
|
74
|
+
internal var remoteSkipForwardIntervalMs: Long = ExoPlayerBuilder.DEFAULT_REMOTE_SKIP_INTERVAL_MS
|
|
75
|
+
internal var remoteSkipBackwardIntervalMs: Long = ExoPlayerBuilder.DEFAULT_REMOTE_SKIP_INTERVAL_MS
|
|
68
76
|
internal var playerListener: androidx.media3.common.Player.Listener? = null
|
|
69
77
|
|
|
70
78
|
// ── Temporary queue ────────────────────────────────────────────────────
|
|
@@ -95,6 +103,8 @@ class TrackPlayerCore private constructor(
|
|
|
95
103
|
ListenerRegistry<(TimedMetadata) -> Unit>()
|
|
96
104
|
internal val onCastStateChangeListeners =
|
|
97
105
|
ListenerRegistry<(com.margelo.nitro.nitroplayer.CastState, String?) -> Unit>()
|
|
106
|
+
internal val onNotificationLaunchListeners =
|
|
107
|
+
ListenerRegistry<(TrackItem) -> Unit>()
|
|
98
108
|
|
|
99
109
|
// ── Google Cast ──────────────────────────────────────────────────────────
|
|
100
110
|
|
|
@@ -118,16 +128,22 @@ class TrackPlayerCore private constructor(
|
|
|
118
128
|
internal val progressUpdateRunnable =
|
|
119
129
|
object : Runnable {
|
|
120
130
|
override fun run() {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
// Stop ticking while paused/idle — onIsPlayingChanged restarts it,
|
|
132
|
+
// so the main looper isn't woken every second for the process lifetime
|
|
133
|
+
if (!::exo.isInitialized || !exo.isPlaying) return
|
|
134
|
+
val pos = exo.currentPosition / 1000.0
|
|
135
|
+
val dur = if (exo.duration > 0) exo.duration / 1000.0 else 0.0
|
|
136
|
+
notifyPlaybackProgress(pos, dur, if (isManuallySeeked) true else null)
|
|
137
|
+
isManuallySeeked = false
|
|
127
138
|
playerHandler.postDelayed(this, PROGRESS_INTERVAL_MS)
|
|
128
139
|
}
|
|
129
140
|
}
|
|
130
141
|
|
|
142
|
+
internal fun startProgressTicks() {
|
|
143
|
+
playerHandler.removeCallbacks(progressUpdateRunnable)
|
|
144
|
+
playerHandler.postDelayed(progressUpdateRunnable, PROGRESS_INTERVAL_MS)
|
|
145
|
+
}
|
|
146
|
+
|
|
131
147
|
internal val updateCurrentPlaylistRunnable =
|
|
132
148
|
Runnable {
|
|
133
149
|
val id = currentPlaylistId ?: return@Runnable
|
|
@@ -208,9 +224,11 @@ class TrackPlayerCore private constructor(
|
|
|
208
224
|
@Suppress("ktlint:standard:property-naming")
|
|
209
225
|
private var INSTANCE: TrackPlayerCore? = null
|
|
210
226
|
|
|
227
|
+
// applicationContext: the process-lifetime singleton must not pin the
|
|
228
|
+
// ReactApplicationContext (and its whole JS runtime) across reloads
|
|
211
229
|
fun getInstance(context: Context): TrackPlayerCore =
|
|
212
230
|
INSTANCE ?: synchronized(this) {
|
|
213
|
-
INSTANCE ?: TrackPlayerCore(context).also { INSTANCE = it }
|
|
231
|
+
INSTANCE ?: TrackPlayerCore(context.applicationContext).also { INSTANCE = it }
|
|
214
232
|
}
|
|
215
233
|
}
|
|
216
234
|
|
|
@@ -390,4 +408,57 @@ class TrackPlayerCore private constructor(
|
|
|
390
408
|
fun addOnCastStateChangeListener(cb: (com.margelo.nitro.nitroplayer.CastState, String?) -> Unit): Long = onCastStateChangeListeners.add(cb)
|
|
391
409
|
|
|
392
410
|
fun removeOnCastStateChangeListener(id: Long): Boolean = onCastStateChangeListeners.remove(id)
|
|
411
|
+
|
|
412
|
+
// ── Notification-launch detection ─────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
private var notificationLaunchDetectionRegistered = false
|
|
415
|
+
|
|
416
|
+
fun addOnNotificationLaunchListener(cb: (TrackItem) -> Unit): Long {
|
|
417
|
+
val id = onNotificationLaunchListeners.add(cb)
|
|
418
|
+
handler.post {
|
|
419
|
+
registerNotificationLaunchDetection()
|
|
420
|
+
checkNotificationLaunch(NitroModules.applicationContext?.currentActivity)
|
|
421
|
+
}
|
|
422
|
+
return id
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
fun removeOnNotificationLaunchListener(id: Long): Boolean = onNotificationLaunchListeners.remove(id)
|
|
426
|
+
|
|
427
|
+
private fun registerNotificationLaunchDetection() {
|
|
428
|
+
if (notificationLaunchDetectionRegistered) return
|
|
429
|
+
notificationLaunchDetectionRegistered = true
|
|
430
|
+
val app = context.applicationContext as? Application ?: return
|
|
431
|
+
app.registerActivityLifecycleCallbacks(
|
|
432
|
+
object : Application.ActivityLifecycleCallbacks {
|
|
433
|
+
override fun onActivityResumed(activity: Activity) = checkNotificationLaunch(activity)
|
|
434
|
+
|
|
435
|
+
override fun onActivityCreated(
|
|
436
|
+
activity: Activity,
|
|
437
|
+
savedInstanceState: Bundle?,
|
|
438
|
+
) {}
|
|
439
|
+
|
|
440
|
+
override fun onActivityStarted(activity: Activity) {}
|
|
441
|
+
|
|
442
|
+
override fun onActivityPaused(activity: Activity) {}
|
|
443
|
+
|
|
444
|
+
override fun onActivityStopped(activity: Activity) {}
|
|
445
|
+
|
|
446
|
+
override fun onActivitySaveInstanceState(
|
|
447
|
+
activity: Activity,
|
|
448
|
+
outState: Bundle,
|
|
449
|
+
) {}
|
|
450
|
+
|
|
451
|
+
override fun onActivityDestroyed(activity: Activity) {}
|
|
452
|
+
},
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Must run on the main looper — reads the player and mutates the activity intent
|
|
457
|
+
internal fun checkNotificationLaunch(activity: Activity?) {
|
|
458
|
+
val intent = activity?.intent ?: return
|
|
459
|
+
if (!intent.getBooleanExtra(NitroPlayerPlaybackService.EXTRA_STARTED_FROM_NOTIFICATION, false)) return
|
|
460
|
+
val track = getCurrentTrack() ?: return
|
|
461
|
+
intent.removeExtra(NitroPlayerPlaybackService.EXTRA_STARTED_FROM_NOTIFICATION)
|
|
462
|
+
onNotificationLaunchListeners.forEach { it(track) }
|
|
463
|
+
}
|
|
393
464
|
}
|
|
@@ -5,12 +5,14 @@ package com.margelo.nitro.nitroplayer.core
|
|
|
5
5
|
import androidx.media3.common.MediaItem
|
|
6
6
|
import androidx.media3.common.MediaMetadata
|
|
7
7
|
import androidx.media3.common.Metadata
|
|
8
|
+
import androidx.media3.common.PlaybackException
|
|
8
9
|
import androidx.media3.common.Player
|
|
9
10
|
import androidx.media3.common.util.UnstableApi
|
|
10
11
|
import androidx.media3.extractor.metadata.icy.IcyInfo
|
|
11
12
|
import com.margelo.nitro.nitroplayer.Reason
|
|
12
13
|
import com.margelo.nitro.nitroplayer.RepeatMode
|
|
13
14
|
import com.margelo.nitro.nitroplayer.TimedMetadata
|
|
15
|
+
import com.margelo.nitro.nitroplayer.TrackPlayerState
|
|
14
16
|
import com.margelo.nitro.nitroplayer.equalizer.EqualizerCore
|
|
15
17
|
import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
16
18
|
|
|
@@ -147,6 +149,7 @@ internal class TrackPlayerEventListener(
|
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
152
|
+
if (isPlaying) core.startProgressTicks()
|
|
150
153
|
core.emitStateChange()
|
|
151
154
|
}
|
|
152
155
|
|
|
@@ -163,6 +166,22 @@ internal class TrackPlayerEventListener(
|
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
168
|
|
|
169
|
+
override fun onPlayerError(error: PlaybackException) {
|
|
170
|
+
with(core) {
|
|
171
|
+
NitroPlayerLogger.log(
|
|
172
|
+
"TrackPlayer",
|
|
173
|
+
"Player error: ${error.errorCodeName} - ${error.message}",
|
|
174
|
+
)
|
|
175
|
+
notifyPlaybackStateChange(TrackPlayerState.STOPPED, Reason.ERROR)
|
|
176
|
+
// A source error leaves ExoPlayer in IDLE; skip the dead item and
|
|
177
|
+
// re-prepare so the rest of the queue keeps playing.
|
|
178
|
+
if (exo.hasNextMediaItem()) {
|
|
179
|
+
exo.seekToNextMediaItem()
|
|
180
|
+
exo.prepare()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
166
185
|
override fun onAudioSessionIdChanged(audioSessionId: Int) {
|
|
167
186
|
if (audioSessionId != 0) {
|
|
168
187
|
try {
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
package com.margelo.nitro.nitroplayer.core
|
|
4
4
|
|
|
5
|
+
import androidx.annotation.OptIn
|
|
5
6
|
import androidx.media3.common.Player
|
|
7
|
+
import androidx.media3.common.util.UnstableApi
|
|
8
|
+
import androidx.media3.exoplayer.ExoPlayer
|
|
6
9
|
import com.margelo.nitro.nitroplayer.PlayerConfig
|
|
7
10
|
import com.margelo.nitro.nitroplayer.Reason
|
|
8
11
|
import com.margelo.nitro.nitroplayer.RepeatMode
|
|
@@ -52,23 +55,8 @@ internal fun TrackPlayerCore.skipToPreviousOnQueue() {
|
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
currentTemporaryType != TrackPlayerCore.TemporaryType.NONE -> {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
when (currentTemporaryType) {
|
|
58
|
-
TrackPlayerCore.TemporaryType.PLAY_NEXT -> {
|
|
59
|
-
val idx = playNextStack.indexOfFirst { it.id == trackId }
|
|
60
|
-
if (idx >= 0) playNextStack.removeAt(idx)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
TrackPlayerCore.TemporaryType.UP_NEXT -> {
|
|
64
|
-
val idx = upNextQueue.indexOfFirst { it.id == trackId }
|
|
65
|
-
if (idx >= 0) upNextQueue.removeAt(idx)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
else -> {}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
58
|
+
// playFromIndexInternal clears both temp lists (matching iOS behavior),
|
|
59
|
+
// so no per-track removal is needed here
|
|
72
60
|
playFromIndexInternal(currentTrackIndex)
|
|
73
61
|
}
|
|
74
62
|
|
|
@@ -110,7 +98,31 @@ internal fun TrackPlayerCore.configureOnQueue(config: PlayerConfig) {
|
|
|
110
98
|
config.androidAutoEnabled?.let { NitroPlayerMediaBrowserService.isAndroidAutoEnabled = it }
|
|
111
99
|
config.lookaheadCount?.let { lookaheadCount = it.toInt() }
|
|
112
100
|
config.androidNotificationIcon?.let { NitroPlayerPlaybackService.notificationSmallIconResName = it }
|
|
113
|
-
|
|
101
|
+
remoteSkipIntervalMs(config.remoteSkipForwardInterval)?.let { remoteSkipForwardIntervalMs = it }
|
|
102
|
+
remoteSkipIntervalMs(config.remoteSkipBackwardInterval)?.let { remoteSkipBackwardIntervalMs = it }
|
|
103
|
+
applyRemoteSkipIntervals()
|
|
104
|
+
mediaSessionManager?.configure(
|
|
105
|
+
config.androidAutoEnabled,
|
|
106
|
+
config.carPlayEnabled,
|
|
107
|
+
config.showInNotification,
|
|
108
|
+
remoteSkipForwardIntervalMs,
|
|
109
|
+
remoteSkipBackwardIntervalMs,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Resolved from the cast controller: mid-cast `exo` is the CastPlayer, whose increments are fixed
|
|
114
|
+
@OptIn(UnstableApi::class)
|
|
115
|
+
internal fun TrackPlayerCore.applyRemoteSkipIntervals() {
|
|
116
|
+
val local = castSessionController?.localPlayer ?: if (isExoInitialized) exo.player else null
|
|
117
|
+
(local as? ExoPlayer)?.let {
|
|
118
|
+
it.setSeekBackIncrementMs(remoteSkipBackwardIntervalMs)
|
|
119
|
+
it.setSeekForwardIncrementMs(remoteSkipForwardIntervalMs)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private fun remoteSkipIntervalMs(intervalSeconds: Double?): Long? {
|
|
124
|
+
if (intervalSeconds == null || !intervalSeconds.isFinite() || intervalSeconds <= 0.0) return null
|
|
125
|
+
return (intervalSeconds * 1000.0).toLong().coerceAtLeast(1L)
|
|
114
126
|
}
|
|
115
127
|
|
|
116
128
|
suspend fun TrackPlayerCore.playSong(
|
|
@@ -197,7 +209,9 @@ internal fun TrackPlayerCore.emitStateChange(reason: Reason? = null) {
|
|
|
197
209
|
suspend fun TrackPlayerCore.setPlayBackSpeed(speed: Double) = withPlayerContext { setPlayBackSpeedOnQueue(speed) }
|
|
198
210
|
|
|
199
211
|
internal fun TrackPlayerCore.setPlayBackSpeedOnQueue(speed: Double) {
|
|
200
|
-
if (speed <= 0.0)
|
|
212
|
+
if (!speed.isFinite() || speed <= 0.0) {
|
|
213
|
+
throw IllegalArgumentException("Speed must be finite and greater than 0")
|
|
214
|
+
}
|
|
201
215
|
if (isExoInitialized) exo.setPlaybackSpeed(speed.toFloat())
|
|
202
216
|
}
|
|
203
217
|
|
|
@@ -8,6 +8,7 @@ import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
|
|
|
8
8
|
import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
|
|
9
9
|
import com.margelo.nitro.nitroplayer.storage.NitroPlayerStorage
|
|
10
10
|
import kotlinx.coroutines.CoroutineScope
|
|
11
|
+
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
11
12
|
import kotlinx.coroutines.Dispatchers
|
|
12
13
|
import kotlinx.coroutines.SupervisorJob
|
|
13
14
|
import kotlinx.coroutines.launch
|
|
@@ -50,7 +51,9 @@ class DownloadDatabase private constructor(
|
|
|
50
51
|
private val existenceCache = mutableMapOf<String, Boolean>()
|
|
51
52
|
private val playlistTracks = mutableMapOf<String, MutableSet<String>>()
|
|
52
53
|
private val fileManager = DownloadFileManager.getInstance(context)
|
|
53
|
-
|
|
54
|
+
// Single-threaded so rapid saves cannot land out of order and persist a stale snapshot
|
|
55
|
+
@OptIn(ExperimentalCoroutinesApi::class)
|
|
56
|
+
private val ioScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1) + SupervisorJob())
|
|
54
57
|
|
|
55
58
|
init {
|
|
56
59
|
loadFromDisk()
|
|
@@ -101,10 +104,11 @@ class DownloadDatabase private constructor(
|
|
|
101
104
|
path: String,
|
|
102
105
|
): Boolean = existenceCache.getOrPut(trackId) { File(path).exists() }
|
|
103
106
|
|
|
104
|
-
/**
|
|
105
|
-
fun
|
|
106
|
-
synchronized(this) {
|
|
107
|
-
|
|
107
|
+
/** Count via the memoized existence cache — no per-record stat() on the caller's thread. */
|
|
108
|
+
fun countDownloadedTracks(): Int =
|
|
109
|
+
synchronized(this) {
|
|
110
|
+
downloadedTracks.count { (trackId, record) -> cachedFileExists(trackId, record.localPath) }
|
|
111
|
+
}
|
|
108
112
|
|
|
109
113
|
fun isPlaylistDownloaded(playlistId: String): Boolean {
|
|
110
114
|
synchronized(this) {
|