react-native-nitro-player 1.5.0 → 1.6.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/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +18 -7
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +77 -56
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +89 -9
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +30 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +5 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +84 -70
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +4 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +43 -11
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +6 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +67 -53
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +42 -11
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +26 -4
- package/ios/HybridTrackPlayer.swift +58 -29
- package/ios/core/TrackPlayerCore.swift +22 -1
- package/ios/core/TrackPlayerListener.swift +84 -5
- package/ios/core/TrackPlayerNotify.swift +13 -7
- package/ios/core/TrackPlayerPlayback.swift +75 -39
- package/ios/core/TrackPlayerQueue.swift +28 -4
- package/ios/core/TrackPlayerQueueBuild.swift +100 -40
- package/ios/core/TrackPlayerRecovery.swift +1 -1
- package/ios/core/TrackPlayerTempQueue.swift +129 -122
- package/ios/core/TrackPlayerUrlLoader.swift +34 -10
- package/ios/download/DownloadDatabase.swift +41 -13
- package/ios/media/MediaSessionManager.swift +19 -12
- package/ios/queue/HybridPlayerQueue.swift +10 -5
- package/lib/hooks/callbackManager.d.ts +27 -1
- package/lib/hooks/callbackManager.js +100 -1
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/useActualQueue.js +18 -12
- package/lib/hooks/useNowPlaying.js +10 -2
- package/lib/hooks/useTimedMetadata.d.ts +16 -0
- package/lib/hooks/useTimedMetadata.js +21 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +6 -1
- package/lib/types/PlayerQueue.d.ts +7 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_TimedMetadata.hpp +79 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +9 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
- package/nitrogen/generated/android/c++/JTimedMetadata.hpp +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TimedMetadata.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/TimedMetadata.kt +47 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +25 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +9 -0
- package/nitrogen/generated/ios/swift/Func_void_TimedMetadata.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/TimedMetadata.swift +96 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +4 -0
- package/nitrogen/generated/shared/c++/TimedMetadata.hpp +96 -0
- package/package.json +1 -1
- package/src/components/CastButton.tsx +14 -2
- package/src/hooks/callbackManager.ts +134 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useActualQueue.ts +18 -13
- package/src/hooks/useNowPlaying.ts +12 -2
- package/src/hooks/useTimedMetadata.ts +25 -0
- package/src/specs/TrackPlayer.nitro.ts +6 -0
- package/src/types/PlayerQueue.ts +8 -0
|
@@ -8,7 +8,7 @@ import com.margelo.nitro.NitroModules
|
|
|
8
8
|
import com.margelo.nitro.core.NullType
|
|
9
9
|
import com.margelo.nitro.core.Promise
|
|
10
10
|
import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
|
|
11
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
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
14
|
import java.util.UUID
|
|
@@ -109,13 +109,24 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
109
109
|
|
|
110
110
|
// ── Playback control ──────────────────────────────────────────────────────
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Enqueued synchronously on the player looper so a `loadPlaylist()` followed by
|
|
114
|
+
* `play()` from JS cannot be reordered — see HybridTrackPlayer.enqueue.
|
|
115
|
+
*/
|
|
116
|
+
override fun loadPlaylist(
|
|
117
|
+
playlistId: String,
|
|
118
|
+
index: Double?,
|
|
119
|
+
): Promise<Unit> {
|
|
120
|
+
val startIndex = index?.toInt()
|
|
121
|
+
val promise = Promise<Unit>()
|
|
122
|
+
core.enqueue {
|
|
123
|
+
if (playlistManager.loadPlaylist(playlistId, startIndex)) {
|
|
124
|
+
core.loadPlaylistOnQueue(playlistId, startIndex)
|
|
125
|
+
}
|
|
126
|
+
promise.resolve(Unit)
|
|
118
127
|
}
|
|
128
|
+
return promise
|
|
129
|
+
}
|
|
119
130
|
|
|
120
131
|
override fun getCurrentPlaylistId(): Variant_NullType_String {
|
|
121
132
|
val id = core.getCurrentPlaylistId()
|
|
@@ -7,35 +7,30 @@ import com.facebook.proguard.annotations.DoNotStrip
|
|
|
7
7
|
import com.margelo.nitro.NitroModules
|
|
8
8
|
import com.margelo.nitro.core.Promise
|
|
9
9
|
import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
|
|
10
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
11
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
12
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
13
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
14
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
15
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
16
|
-
import com.margelo.nitro.nitroplayer.core.getNextTracks
|
|
17
|
-
import com.margelo.nitro.nitroplayer.core.getPlayBackSpeed
|
|
18
|
-
import com.margelo.nitro.nitroplayer.core.getPlayNextQueue
|
|
10
|
+
import com.margelo.nitro.nitroplayer.core.addToUpNextInternal
|
|
11
|
+
import com.margelo.nitro.nitroplayer.core.clearPlayNextOnQueue
|
|
12
|
+
import com.margelo.nitro.nitroplayer.core.clearUpNextOnQueue
|
|
13
|
+
import com.margelo.nitro.nitroplayer.core.configureOnQueue
|
|
14
|
+
import com.margelo.nitro.nitroplayer.core.getActualQueueInternal
|
|
15
|
+
import com.margelo.nitro.nitroplayer.core.getNextTracksInternal
|
|
19
16
|
import com.margelo.nitro.nitroplayer.core.getRepeatMode
|
|
20
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
21
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
22
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
23
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
24
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
25
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
26
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
27
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
28
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
29
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
30
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
31
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
32
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
33
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
34
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
35
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
36
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
37
|
-
import com.margelo.nitro.nitroplayer.core.skipToPrevious
|
|
38
|
-
import com.margelo.nitro.nitroplayer.core.updateTracks
|
|
17
|
+
import com.margelo.nitro.nitroplayer.core.getStateInternal
|
|
18
|
+
import com.margelo.nitro.nitroplayer.core.getTracksNeedingUrlsInternal
|
|
19
|
+
import com.margelo.nitro.nitroplayer.core.pauseOnQueue
|
|
20
|
+
import com.margelo.nitro.nitroplayer.core.playNextInternal
|
|
21
|
+
import com.margelo.nitro.nitroplayer.core.playOnQueue
|
|
22
|
+
import com.margelo.nitro.nitroplayer.core.playSongInternal
|
|
23
|
+
import com.margelo.nitro.nitroplayer.core.removeFromPlayNextOnQueue
|
|
24
|
+
import com.margelo.nitro.nitroplayer.core.removeFromUpNextOnQueue
|
|
25
|
+
import com.margelo.nitro.nitroplayer.core.reorderTemporaryTrackOnQueue
|
|
26
|
+
import com.margelo.nitro.nitroplayer.core.seekOnQueue
|
|
27
|
+
import com.margelo.nitro.nitroplayer.core.setPlayBackSpeedOnQueue
|
|
28
|
+
import com.margelo.nitro.nitroplayer.core.setRepeatModeOnQueue
|
|
29
|
+
import com.margelo.nitro.nitroplayer.core.setVolumeOnQueue
|
|
30
|
+
import com.margelo.nitro.nitroplayer.core.skipToIndexInternal
|
|
31
|
+
import com.margelo.nitro.nitroplayer.core.skipToNextOnQueue
|
|
32
|
+
import com.margelo.nitro.nitroplayer.core.skipToPreviousOnQueue
|
|
33
|
+
import com.margelo.nitro.nitroplayer.core.updateTracksOnQueue
|
|
39
34
|
|
|
40
35
|
@DoNotStrip
|
|
41
36
|
@Keep
|
|
@@ -52,79 +47,99 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
|
|
|
52
47
|
core = TrackPlayerCore.getInstance(context)
|
|
53
48
|
}
|
|
54
49
|
|
|
50
|
+
// ── Ordered dispatch ──────────────────────────────────────────────────────
|
|
51
|
+
//
|
|
52
|
+
// `Promise.async { … }` launches an unordered coroutine, so the hop onto the player
|
|
53
|
+
// looper happened at an arbitrary later point: two calls made in order from JS could
|
|
54
|
+
// reach the player in reverse order (play-then-pause landing as pause-then-play).
|
|
55
|
+
// `core.enqueue` appends synchronously, on the JS thread at call time, so execution
|
|
56
|
+
// order equals JS call order.
|
|
57
|
+
|
|
58
|
+
private fun <T> enqueue(block: () -> T): Promise<T> {
|
|
59
|
+
val promise = Promise<T>()
|
|
60
|
+
core.enqueue {
|
|
61
|
+
try {
|
|
62
|
+
promise.resolve(block())
|
|
63
|
+
} catch (e: Throwable) {
|
|
64
|
+
promise.reject(e)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return promise
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
// ── Playback ─────────────────────────────────────────────────────────────
|
|
56
71
|
|
|
57
|
-
override fun play(): Promise<Unit> =
|
|
72
|
+
override fun play(): Promise<Unit> = enqueue { core.playOnQueue() }
|
|
58
73
|
|
|
59
|
-
override fun pause(): Promise<Unit> =
|
|
74
|
+
override fun pause(): Promise<Unit> = enqueue { core.pauseOnQueue() }
|
|
60
75
|
|
|
61
|
-
override fun seek(position: Double): Promise<Unit> =
|
|
76
|
+
override fun seek(position: Double): Promise<Unit> = enqueue { core.seekOnQueue(position) }
|
|
62
77
|
|
|
63
|
-
override fun skipToNext(): Promise<Unit> =
|
|
78
|
+
override fun skipToNext(): Promise<Unit> = enqueue { core.skipToNextOnQueue() }
|
|
64
79
|
|
|
65
|
-
override fun skipToPrevious(): Promise<Unit> =
|
|
80
|
+
override fun skipToPrevious(): Promise<Unit> = enqueue { core.skipToPreviousOnQueue() }
|
|
66
81
|
|
|
67
82
|
override fun playSong(
|
|
68
83
|
songId: String,
|
|
69
84
|
fromPlaylist: String?,
|
|
70
|
-
): Promise<Unit> =
|
|
85
|
+
): Promise<Unit> = enqueue { core.playSongInternal(songId, fromPlaylist) }
|
|
71
86
|
|
|
72
|
-
override fun skipToIndex(index: Double): Promise<Boolean> =
|
|
87
|
+
override fun skipToIndex(index: Double): Promise<Boolean> = enqueue { core.skipToIndexInternal(index.toInt()) }
|
|
73
88
|
|
|
74
|
-
override fun setRepeatMode(mode: RepeatMode): Promise<Unit> =
|
|
89
|
+
override fun setRepeatMode(mode: RepeatMode): Promise<Unit> = enqueue { core.setRepeatModeOnQueue(mode) }
|
|
75
90
|
|
|
76
91
|
override fun getRepeatMode(): RepeatMode = core.getRepeatMode()
|
|
77
92
|
|
|
78
|
-
override fun setVolume(volume: Double): Promise<Unit> =
|
|
93
|
+
override fun setVolume(volume: Double): Promise<Unit> = enqueue { core.setVolumeOnQueue(volume) }
|
|
79
94
|
|
|
80
|
-
override fun configure(config: PlayerConfig): Promise<Unit> =
|
|
95
|
+
override fun configure(config: PlayerConfig): Promise<Unit> = enqueue { core.configureOnQueue(config) }
|
|
81
96
|
|
|
82
97
|
// ── Queue / state reads ───────────────────────────────────────────────────
|
|
83
98
|
|
|
84
|
-
override fun getActualQueue(): Promise<Array<TrackItem>> =
|
|
99
|
+
override fun getActualQueue(): Promise<Array<TrackItem>> = enqueue { core.getActualQueueInternal().toTypedArray() }
|
|
85
100
|
|
|
86
|
-
override fun getState(): Promise<PlayerState> =
|
|
101
|
+
override fun getState(): Promise<PlayerState> = enqueue { core.getStateInternal() }
|
|
87
102
|
|
|
88
|
-
override fun getTracksById(trackIds: Array<String>): Promise<Array<TrackItem>> =
|
|
103
|
+
override fun getTracksById(trackIds: Array<String>): Promise<Array<TrackItem>> = enqueue { core.getPlaylistManager().getTracksById(trackIds.toList()).toTypedArray() }
|
|
89
104
|
|
|
90
|
-
override fun getTracksNeedingUrls(): Promise<Array<TrackItem>> =
|
|
105
|
+
override fun getTracksNeedingUrls(): Promise<Array<TrackItem>> = enqueue { core.getTracksNeedingUrlsInternal().toTypedArray() }
|
|
91
106
|
|
|
92
|
-
override fun getNextTracks(count: Double): Promise<Array<TrackItem>> =
|
|
107
|
+
override fun getNextTracks(count: Double): Promise<Array<TrackItem>> = enqueue { core.getNextTracksInternal(count.toInt()).toTypedArray() }
|
|
93
108
|
|
|
94
|
-
override fun getCurrentTrackIndex(): Promise<Double> =
|
|
109
|
+
override fun getCurrentTrackIndex(): Promise<Double> = enqueue { core.currentTrackIndex.toDouble() }
|
|
95
110
|
|
|
96
111
|
// ── URL updates ───────────────────────────────────────────────────────────
|
|
97
112
|
|
|
98
|
-
override fun updateTracks(tracks: Array<TrackItem>): Promise<Unit> =
|
|
113
|
+
override fun updateTracks(tracks: Array<TrackItem>): Promise<Unit> = enqueue { core.updateTracksOnQueue(tracks.toList()) }
|
|
99
114
|
|
|
100
115
|
// ── Temporary queue ───────────────────────────────────────────────────────
|
|
101
116
|
|
|
102
|
-
override fun addToUpNext(trackId: String): Promise<Unit> =
|
|
117
|
+
override fun addToUpNext(trackId: String): Promise<Unit> = enqueue { core.addToUpNextInternal(trackId) }
|
|
103
118
|
|
|
104
|
-
override fun playNext(trackId: String): Promise<Unit> =
|
|
119
|
+
override fun playNext(trackId: String): Promise<Unit> = enqueue { core.playNextInternal(trackId) }
|
|
105
120
|
|
|
106
|
-
override fun removeFromPlayNext(trackId: String): Promise<Boolean> =
|
|
121
|
+
override fun removeFromPlayNext(trackId: String): Promise<Boolean> = enqueue { core.removeFromPlayNextOnQueue(trackId) }
|
|
107
122
|
|
|
108
|
-
override fun removeFromUpNext(trackId: String): Promise<Boolean> =
|
|
123
|
+
override fun removeFromUpNext(trackId: String): Promise<Boolean> = enqueue { core.removeFromUpNextOnQueue(trackId) }
|
|
109
124
|
|
|
110
|
-
override fun clearPlayNext(): Promise<Unit> =
|
|
125
|
+
override fun clearPlayNext(): Promise<Unit> = enqueue { core.clearPlayNextOnQueue() }
|
|
111
126
|
|
|
112
|
-
override fun clearUpNext(): Promise<Unit> =
|
|
127
|
+
override fun clearUpNext(): Promise<Unit> = enqueue { core.clearUpNextOnQueue() }
|
|
113
128
|
|
|
114
129
|
override fun reorderTemporaryTrack(
|
|
115
130
|
trackId: String,
|
|
116
131
|
newIndex: Double,
|
|
117
|
-
): Promise<Boolean> =
|
|
132
|
+
): Promise<Boolean> = enqueue { core.reorderTemporaryTrackOnQueue(trackId, newIndex.toInt()) }
|
|
118
133
|
|
|
119
|
-
override fun getPlayNextQueue(): Promise<Array<TrackItem>> =
|
|
134
|
+
override fun getPlayNextQueue(): Promise<Array<TrackItem>> = enqueue { core.playNextStack.toTypedArray() }
|
|
120
135
|
|
|
121
|
-
override fun getUpNextQueue(): Promise<Array<TrackItem>> =
|
|
136
|
+
override fun getUpNextQueue(): Promise<Array<TrackItem>> = enqueue { core.upNextQueue.toTypedArray() }
|
|
122
137
|
|
|
123
138
|
// ── Playback speed ────────────────────────────────────────────────────────
|
|
124
139
|
|
|
125
|
-
override fun setPlaybackSpeed(speed: Double): Promise<Unit> =
|
|
140
|
+
override fun setPlaybackSpeed(speed: Double): Promise<Unit> = enqueue { core.setPlayBackSpeedOnQueue(speed) }
|
|
126
141
|
|
|
127
|
-
override fun getPlaybackSpeed(): Promise<Double> =
|
|
142
|
+
override fun getPlaybackSpeed(): Promise<Double> = enqueue { if (core.isExoInitialized) core.exo.getPlaybackSpeed().toDouble() else 1.0 }
|
|
128
143
|
|
|
129
144
|
// ── Android Auto ──────────────────────────────────────────────────────────
|
|
130
145
|
|
|
@@ -171,6 +186,11 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
|
|
|
171
186
|
listenerIds += "onTracksNeedUpdate" to id
|
|
172
187
|
}
|
|
173
188
|
|
|
189
|
+
override fun onTimedMetadata(callback: (metadata: TimedMetadata) -> Unit) {
|
|
190
|
+
val id = core.addOnTimedMetadataListener(callback)
|
|
191
|
+
listenerIds += "onTimedMetadata" to id
|
|
192
|
+
}
|
|
193
|
+
|
|
174
194
|
override fun onTemporaryQueueChange(callback: (playNextQueue: Array<TrackItem>, upNextQueue: Array<TrackItem>) -> Unit) {
|
|
175
195
|
val id =
|
|
176
196
|
core.addOnTemporaryQueueChangeListener { pn, un ->
|
|
@@ -192,6 +212,7 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
|
|
|
192
212
|
"onAndroidAutoConnectionChange" -> core.removeOnAndroidAutoConnectionListener(id)
|
|
193
213
|
"onTracksNeedUpdate" -> core.removeOnTracksNeedUpdateListener(id)
|
|
194
214
|
"onTemporaryQueueChange" -> core.removeOnTemporaryQueueChangeListener(id)
|
|
215
|
+
"onTimedMetadata" -> core.removeOnTimedMetadataListener(id)
|
|
195
216
|
}
|
|
196
217
|
}
|
|
197
218
|
listenerIds.clear()
|
|
@@ -11,6 +11,7 @@ import android.os.IBinder
|
|
|
11
11
|
import android.os.Looper
|
|
12
12
|
import com.margelo.nitro.nitroplayer.Reason
|
|
13
13
|
import com.margelo.nitro.nitroplayer.RepeatMode
|
|
14
|
+
import com.margelo.nitro.nitroplayer.TimedMetadata
|
|
14
15
|
import com.margelo.nitro.nitroplayer.TrackItem
|
|
15
16
|
import com.margelo.nitro.nitroplayer.TrackPlayerState
|
|
16
17
|
import com.margelo.nitro.nitroplayer.connection.AndroidAutoConnectionDetector
|
|
@@ -90,10 +91,13 @@ class TrackPlayerCore private constructor(
|
|
|
90
91
|
ListenerRegistry<(List<TrackItem>, List<TrackItem>) -> Unit>()
|
|
91
92
|
internal val onAndroidAutoConnectionListeners =
|
|
92
93
|
ListenerRegistry<(Boolean) -> Unit>()
|
|
94
|
+
internal val onTimedMetadataListeners =
|
|
95
|
+
ListenerRegistry<(TimedMetadata) -> Unit>()
|
|
93
96
|
internal val onCastStateChangeListeners =
|
|
94
97
|
ListenerRegistry<(com.margelo.nitro.nitroplayer.CastState, String?) -> Unit>()
|
|
95
98
|
|
|
96
99
|
// ── Google Cast ──────────────────────────────────────────────────────────
|
|
100
|
+
|
|
97
101
|
/** Set once the playback service has a usable CastContext; null when Cast is unavailable. */
|
|
98
102
|
internal var castSessionController: com.margelo.nitro.nitroplayer.media.CastSessionController? = null
|
|
99
103
|
|
|
@@ -104,18 +108,23 @@ class TrackPlayerCore private constructor(
|
|
|
104
108
|
internal var lastCastWaitTrackId: String? = null
|
|
105
109
|
|
|
106
110
|
// ── Progress & playlist-update runnables ───────────────────────────────
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Emits progress once per second while actually playing (matching iOS), instead of
|
|
114
|
+
* four times a second regardless of state. A paused player has nothing to report,
|
|
115
|
+
* and every tick is a JS callback that re-renders every `useNowPlaying` consumer.
|
|
116
|
+
* A seek still reports immediately via `onPositionDiscontinuity`.
|
|
117
|
+
*/
|
|
107
118
|
internal val progressUpdateRunnable =
|
|
108
119
|
object : Runnable {
|
|
109
120
|
override fun run() {
|
|
110
|
-
if (::exo.isInitialized &&
|
|
111
|
-
exo.playbackState != androidx.media3.common.Player.STATE_IDLE
|
|
112
|
-
) {
|
|
121
|
+
if (::exo.isInitialized && exo.isPlaying) {
|
|
113
122
|
val pos = exo.currentPosition / 1000.0
|
|
114
123
|
val dur = if (exo.duration > 0) exo.duration / 1000.0 else 0.0
|
|
115
124
|
notifyPlaybackProgress(pos, dur, if (isManuallySeeked) true else null)
|
|
116
125
|
isManuallySeeked = false
|
|
117
126
|
}
|
|
118
|
-
playerHandler.postDelayed(this,
|
|
127
|
+
playerHandler.postDelayed(this, PROGRESS_INTERVAL_MS)
|
|
119
128
|
}
|
|
120
129
|
}
|
|
121
130
|
|
|
@@ -155,7 +164,8 @@ class TrackPlayerCore private constructor(
|
|
|
155
164
|
}
|
|
156
165
|
try {
|
|
157
166
|
context.unbindService(this)
|
|
158
|
-
} catch (_: Exception) {
|
|
167
|
+
} catch (_: Exception) {
|
|
168
|
+
}
|
|
159
169
|
serviceBound = false
|
|
160
170
|
if (rebindAttempts < 3) {
|
|
161
171
|
rebindAttempts++
|
|
@@ -192,6 +202,8 @@ class TrackPlayerCore private constructor(
|
|
|
192
202
|
|
|
193
203
|
// ── Singleton ──────────────────────────────────────────────────────────
|
|
194
204
|
companion object {
|
|
205
|
+
internal const val PROGRESS_INTERVAL_MS = 1000L
|
|
206
|
+
|
|
195
207
|
@Volatile
|
|
196
208
|
@Suppress("ktlint:standard:property-naming")
|
|
197
209
|
private var INSTANCE: TrackPlayerCore? = null
|
|
@@ -206,8 +218,18 @@ class TrackPlayerCore private constructor(
|
|
|
206
218
|
// Defer service start/bind to the main thread so it doesn't run
|
|
207
219
|
// synchronously on the JNI thread during HybridObject creation.
|
|
208
220
|
handler.post {
|
|
209
|
-
|
|
210
|
-
|
|
221
|
+
// startService throws BackgroundServiceStartNotAllowedException on Android 12+
|
|
222
|
+
// when the process is in the background — which is exactly what happens when JS
|
|
223
|
+
// loads the module from a headless/backgrounded start. It is only here to keep
|
|
224
|
+
// the service alive across unbind; BIND_AUTO_CREATE below already creates it,
|
|
225
|
+
// and the service promotes itself to foreground once playback starts.
|
|
226
|
+
try {
|
|
227
|
+
context.startService(Intent(context, NitroPlayerPlaybackService::class.java))
|
|
228
|
+
} catch (e: Exception) {
|
|
229
|
+
NitroPlayerLogger.log("TrackPlayerCore") {
|
|
230
|
+
"startService skipped (app in background): ${e.message}"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
211
233
|
|
|
212
234
|
val bindIntent =
|
|
213
235
|
Intent(context, NitroPlayerPlaybackService::class.java).apply {
|
|
@@ -219,6 +241,58 @@ class TrackPlayerCore private constructor(
|
|
|
219
241
|
|
|
220
242
|
// ── Coroutine bridge to player looper (main thread) ────────────────────
|
|
221
243
|
|
|
244
|
+
// ── Ordered command dispatch ───────────────────────────────────────────
|
|
245
|
+
//
|
|
246
|
+
// `Promise.async { … }` launches an unordered coroutine, so the hop onto the
|
|
247
|
+
// player looper used to happen at an arbitrary later point: two calls made in
|
|
248
|
+
// order from JS could reach the player in reverse order (play-then-pause landing
|
|
249
|
+
// as pause-then-play). `enqueue` appends to a queue synchronously, on the JS
|
|
250
|
+
// thread at call time, so execution order equals JS call order — including for
|
|
251
|
+
// commands issued before the playback service has bound.
|
|
252
|
+
|
|
253
|
+
private val pendingCommands = ArrayDeque<() -> Unit>()
|
|
254
|
+
private var commandsDraining = false
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Runs [block] on the player looper, preserving the order in which callers
|
|
258
|
+
* enqueued. Safe to call before the service binds — commands buffer until then.
|
|
259
|
+
*/
|
|
260
|
+
internal fun enqueue(block: () -> Unit) {
|
|
261
|
+
synchronized(pendingCommands) {
|
|
262
|
+
pendingCommands.addLast(block)
|
|
263
|
+
if (commandsDraining) return
|
|
264
|
+
if (!::playerHandler.isInitialized) return
|
|
265
|
+
commandsDraining = true
|
|
266
|
+
}
|
|
267
|
+
playerHandler.post { drainCommands() }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private fun drainCommands() {
|
|
271
|
+
while (true) {
|
|
272
|
+
val next =
|
|
273
|
+
synchronized(pendingCommands) {
|
|
274
|
+
val head = pendingCommands.removeFirstOrNull()
|
|
275
|
+
if (head == null) commandsDraining = false
|
|
276
|
+
head
|
|
277
|
+
} ?: return
|
|
278
|
+
next()
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Called once the player looper exists — flushes anything queued before binding. */
|
|
283
|
+
internal fun startCommandDraining() {
|
|
284
|
+
val shouldPost =
|
|
285
|
+
synchronized(pendingCommands) {
|
|
286
|
+
if (commandsDraining || pendingCommands.isEmpty()) {
|
|
287
|
+
false
|
|
288
|
+
} else {
|
|
289
|
+
commandsDraining = true
|
|
290
|
+
true
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (shouldPost) playerHandler.post { drainCommands() }
|
|
294
|
+
}
|
|
295
|
+
|
|
222
296
|
internal suspend fun <T> withPlayerContext(block: () -> T): T {
|
|
223
297
|
// Wait until the service is bound and player is initialized
|
|
224
298
|
serviceReady.await()
|
|
@@ -256,13 +330,15 @@ class TrackPlayerCore private constructor(
|
|
|
256
330
|
}
|
|
257
331
|
}
|
|
258
332
|
scope.cancel()
|
|
259
|
-
com.margelo.nitro.nitroplayer.media.AuthAwareHttpDataSourceFactory
|
|
333
|
+
com.margelo.nitro.nitroplayer.media.AuthAwareHttpDataSourceFactory
|
|
334
|
+
.clear()
|
|
260
335
|
// Do NOT stop the service — it owns the player.
|
|
261
336
|
// Unbind so Android can clean up if needed.
|
|
262
337
|
if (serviceBound) {
|
|
263
338
|
try {
|
|
264
339
|
context.unbindService(serviceConnection)
|
|
265
|
-
} catch (_: Exception) {
|
|
340
|
+
} catch (_: Exception) {
|
|
341
|
+
}
|
|
266
342
|
serviceBound = false
|
|
267
343
|
}
|
|
268
344
|
}
|
|
@@ -303,6 +379,10 @@ class TrackPlayerCore private constructor(
|
|
|
303
379
|
|
|
304
380
|
fun removeOnTemporaryQueueChangeListener(id: Long): Boolean = onTemporaryQueueChangeListeners.remove(id)
|
|
305
381
|
|
|
382
|
+
fun addOnTimedMetadataListener(cb: (TimedMetadata) -> Unit): Long = onTimedMetadataListeners.add(cb)
|
|
383
|
+
|
|
384
|
+
fun removeOnTimedMetadataListener(id: Long): Boolean = onTimedMetadataListeners.remove(id)
|
|
385
|
+
|
|
306
386
|
fun addOnAndroidAutoConnectionListener(cb: (Boolean) -> Unit): Long = onAndroidAutoConnectionListeners.add(cb)
|
|
307
387
|
|
|
308
388
|
fun removeOnAndroidAutoConnectionListener(id: Long): Boolean = onAndroidAutoConnectionListeners.remove(id)
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
package com.margelo.nitro.nitroplayer.core
|
|
4
4
|
|
|
5
5
|
import androidx.media3.common.MediaItem
|
|
6
|
+
import androidx.media3.common.MediaMetadata
|
|
7
|
+
import androidx.media3.common.Metadata
|
|
6
8
|
import androidx.media3.common.Player
|
|
9
|
+
import androidx.media3.common.util.UnstableApi
|
|
7
10
|
import com.margelo.nitro.nitroplayer.Reason
|
|
8
11
|
import com.margelo.nitro.nitroplayer.RepeatMode
|
|
12
|
+
import com.margelo.nitro.nitroplayer.TimedMetadata
|
|
9
13
|
import com.margelo.nitro.nitroplayer.equalizer.EqualizerCore
|
|
10
14
|
import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
11
15
|
|
|
@@ -57,6 +61,16 @@ internal class TrackPlayerEventListener(
|
|
|
57
61
|
if (newIdx >= 0 && newIdx != currentTrackIndex) currentTrackIndex = newIdx
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
// Top up the windowed timeline — the item we just left freed a slot.
|
|
65
|
+
// Only when it is actually short: rebuildQueueFromCurrentPosition can fall
|
|
66
|
+
// into the substitute-jump path (setMediaItems), which re-enters this very
|
|
67
|
+
// callback. The length check makes any such re-entry a no-op and terminate.
|
|
68
|
+
if (mediaItem != null &&
|
|
69
|
+
exo.mediaItemCount - exo.currentMediaItemIndex - 1 < QUEUE_WINDOW_SIZE
|
|
70
|
+
) {
|
|
71
|
+
rebuildQueueFromCurrentPosition()
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
val track = getCurrentTrack() ?: return
|
|
61
75
|
val r =
|
|
62
76
|
when (reason) {
|
|
@@ -72,6 +86,22 @@ internal class TrackPlayerEventListener(
|
|
|
72
86
|
}
|
|
73
87
|
}
|
|
74
88
|
|
|
89
|
+
/** In-stream metadata (ICY StreamTitle, ID3 frames) arriving mid-playback. */
|
|
90
|
+
@UnstableApi
|
|
91
|
+
override fun onMetadata(metadata: Metadata) {
|
|
92
|
+
val builder = MediaMetadata.Builder()
|
|
93
|
+
for (i in 0 until metadata.length()) {
|
|
94
|
+
metadata.get(i).populateMediaMetadata(builder)
|
|
95
|
+
}
|
|
96
|
+
val merged = builder.build()
|
|
97
|
+
val title = merged.title?.toString()
|
|
98
|
+
val artist = merged.artist?.toString()
|
|
99
|
+
val album = merged.albumTitle?.toString()
|
|
100
|
+
val artworkUrl = merged.artworkUri?.toString()
|
|
101
|
+
if (title == null && artist == null && album == null && artworkUrl == null) return
|
|
102
|
+
core.notifyTimedMetadata(TimedMetadata(title, artist, album, artworkUrl))
|
|
103
|
+
}
|
|
104
|
+
|
|
75
105
|
override fun onTimelineChanged(
|
|
76
106
|
timeline: androidx.media3.common.Timeline,
|
|
77
107
|
reason: Int,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.margelo.nitro.nitroplayer.core
|
|
2
2
|
|
|
3
3
|
import com.margelo.nitro.nitroplayer.Reason
|
|
4
|
+
import com.margelo.nitro.nitroplayer.TimedMetadata
|
|
4
5
|
import com.margelo.nitro.nitroplayer.TrackItem
|
|
5
6
|
import com.margelo.nitro.nitroplayer.TrackPlayerState
|
|
6
7
|
|
|
@@ -45,6 +46,10 @@ internal fun TrackPlayerCore.notifyTracksNeedUpdate(
|
|
|
45
46
|
onTracksNeedUpdateListeners.forEach { it(tracks, lookahead) }
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
internal fun TrackPlayerCore.notifyTimedMetadata(metadata: TimedMetadata) {
|
|
50
|
+
onTimedMetadataListeners.forEach { it(metadata) }
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
internal fun TrackPlayerCore.notifyTemporaryQueueChange() {
|
|
49
54
|
val pn = playNextStack.toList()
|
|
50
55
|
val un = upNextQueue.toList()
|