react-native-nitro-player 1.5.1-alpha.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/HybridTrackPlayer.kt +6 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +7 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +20 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +5 -0
- package/ios/HybridTrackPlayer.swift +6 -0
- package/ios/core/TrackPlayerCore.swift +13 -0
- package/ios/core/TrackPlayerListener.swift +64 -0
- package/ios/core/TrackPlayerNotify.swift +4 -0
- package/lib/hooks/callbackManager.d.ts +10 -1
- package/lib/hooks/callbackManager.js +33 -0
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- 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/hooks/callbackManager.ts +47 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useTimedMetadata.ts +25 -0
- package/src/specs/TrackPlayer.nitro.ts +6 -0
- package/src/types/PlayerQueue.ts +8 -0
|
@@ -186,6 +186,11 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
|
|
|
186
186
|
listenerIds += "onTracksNeedUpdate" to id
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
override fun onTimedMetadata(callback: (metadata: TimedMetadata) -> Unit) {
|
|
190
|
+
val id = core.addOnTimedMetadataListener(callback)
|
|
191
|
+
listenerIds += "onTimedMetadata" to id
|
|
192
|
+
}
|
|
193
|
+
|
|
189
194
|
override fun onTemporaryQueueChange(callback: (playNextQueue: Array<TrackItem>, upNextQueue: Array<TrackItem>) -> Unit) {
|
|
190
195
|
val id =
|
|
191
196
|
core.addOnTemporaryQueueChangeListener { pn, un ->
|
|
@@ -207,6 +212,7 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
|
|
|
207
212
|
"onAndroidAutoConnectionChange" -> core.removeOnAndroidAutoConnectionListener(id)
|
|
208
213
|
"onTracksNeedUpdate" -> core.removeOnTracksNeedUpdateListener(id)
|
|
209
214
|
"onTemporaryQueueChange" -> core.removeOnTemporaryQueueChangeListener(id)
|
|
215
|
+
"onTimedMetadata" -> core.removeOnTimedMetadataListener(id)
|
|
210
216
|
}
|
|
211
217
|
}
|
|
212
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,6 +91,8 @@ 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
|
|
|
@@ -376,6 +379,10 @@ class TrackPlayerCore private constructor(
|
|
|
376
379
|
|
|
377
380
|
fun removeOnTemporaryQueueChangeListener(id: Long): Boolean = onTemporaryQueueChangeListeners.remove(id)
|
|
378
381
|
|
|
382
|
+
fun addOnTimedMetadataListener(cb: (TimedMetadata) -> Unit): Long = onTimedMetadataListeners.add(cb)
|
|
383
|
+
|
|
384
|
+
fun removeOnTimedMetadataListener(id: Long): Boolean = onTimedMetadataListeners.remove(id)
|
|
385
|
+
|
|
379
386
|
fun addOnAndroidAutoConnectionListener(cb: (Boolean) -> Unit): Long = onAndroidAutoConnectionListeners.add(cb)
|
|
380
387
|
|
|
381
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
|
|
|
@@ -82,6 +86,22 @@ internal class TrackPlayerEventListener(
|
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
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
|
+
|
|
85
105
|
override fun onTimelineChanged(
|
|
86
106
|
timeline: androidx.media3.common.Timeline,
|
|
87
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()
|
|
@@ -219,6 +219,11 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
|
|
|
219
219
|
listenerIds.append(("onTracksNeedUpdate", id))
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
func onTimedMetadata(callback: @escaping (_ metadata: TimedMetadata) -> Void) throws {
|
|
223
|
+
let id = core.addOnTimedMetadataListener(callback)
|
|
224
|
+
listenerIds.append(("onTimedMetadata", id))
|
|
225
|
+
}
|
|
226
|
+
|
|
222
227
|
func onTemporaryQueueChange(callback: @escaping (_ playNextQueue: [TrackItem], _ upNextQueue: [TrackItem]) -> Void) throws {
|
|
223
228
|
let id = core.addOnTemporaryQueueChangeListener(callback)
|
|
224
229
|
listenerIds.append(("onTemporaryQueueChange", id))
|
|
@@ -235,6 +240,7 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
|
|
|
235
240
|
case "onPlaybackProgressChange":_ = core.removeOnProgressListener(id: id)
|
|
236
241
|
case "onTracksNeedUpdate": _ = core.removeOnTracksNeedUpdateListener(id: id)
|
|
237
242
|
case "onTemporaryQueueChange": _ = core.removeOnTemporaryQueueChangeListener(id: id)
|
|
243
|
+
case "onTimedMetadata": _ = core.removeOnTimedMetadataListener(id: id)
|
|
238
244
|
default: break
|
|
239
245
|
}
|
|
240
246
|
}
|
|
@@ -59,6 +59,10 @@ class TrackPlayerCore: NSObject {
|
|
|
59
59
|
internal var boundaryTimeObserver: Any?
|
|
60
60
|
internal var currentItemObservers: [NSKeyValueObservation] = []
|
|
61
61
|
|
|
62
|
+
// In-stream (timed) metadata output, attached to the current item only.
|
|
63
|
+
internal var metadataOutput: AVPlayerItemMetadataOutput?
|
|
64
|
+
internal weak var metadataOutputItem: AVPlayerItem?
|
|
65
|
+
|
|
62
66
|
// Gapless playback
|
|
63
67
|
internal var preloadedAssets: [String: AVURLAsset] = [:]
|
|
64
68
|
internal let preloadQueue = DispatchQueue(label: "com.nitroplayer.preload", qos: .utility)
|
|
@@ -110,6 +114,7 @@ class TrackPlayerCore: NSObject {
|
|
|
110
114
|
internal let onProgressListeners = ListenerRegistry<(Double, Double, Bool?) -> Void>()
|
|
111
115
|
internal let onTracksNeedUpdateListeners = ListenerRegistry<([TrackItem], Int) -> Void>()
|
|
112
116
|
internal let onTemporaryQueueChangeListeners = ListenerRegistry<([TrackItem], [TrackItem]) -> Void>()
|
|
117
|
+
internal let onTimedMetadataListeners = ListenerRegistry<(TimedMetadata) -> Void>()
|
|
113
118
|
internal let onCastStateChangeListeners = ListenerRegistry<(CastState, String?) -> Void>()
|
|
114
119
|
|
|
115
120
|
// MARK: - Google Cast
|
|
@@ -216,6 +221,13 @@ class TrackPlayerCore: NSObject {
|
|
|
216
221
|
onTemporaryQueueChangeListeners.remove(id: id)
|
|
217
222
|
}
|
|
218
223
|
|
|
224
|
+
@discardableResult func addOnTimedMetadataListener(_ cb: @escaping (TimedMetadata) -> Void) -> Int64 {
|
|
225
|
+
onTimedMetadataListeners.add(cb)
|
|
226
|
+
}
|
|
227
|
+
@discardableResult func removeOnTimedMetadataListener(id: Int64) -> Bool {
|
|
228
|
+
onTimedMetadataListeners.remove(id: id)
|
|
229
|
+
}
|
|
230
|
+
|
|
219
231
|
@discardableResult func addOnCastStateChangeListener(_ cb: @escaping (CastState, String?) -> Void) -> Int64 {
|
|
220
232
|
onCastStateChangeListeners.add(cb)
|
|
221
233
|
}
|
|
@@ -237,6 +249,7 @@ class TrackPlayerCore: NSObject {
|
|
|
237
249
|
p.removeTimeObserver(obs)
|
|
238
250
|
}
|
|
239
251
|
self.currentItemObservers.removeAll()
|
|
252
|
+
self.detachMetadataOutput()
|
|
240
253
|
if let p = self.player {
|
|
241
254
|
p.removeObserver(self, forKeyPath: "status")
|
|
242
255
|
p.removeObserver(self, forKeyPath: "rate")
|
|
@@ -285,6 +285,7 @@ extension TrackPlayerCore {
|
|
|
285
285
|
|
|
286
286
|
guard let player, let currentItem = player.currentItem else {
|
|
287
287
|
NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Current item changed to nil")
|
|
288
|
+
detachMetadataOutput()
|
|
288
289
|
// Queue exhausted — handle PLAYLIST repeat
|
|
289
290
|
if currentRepeatMode == .playlist && !currentTracks.isEmpty, let player = self.player {
|
|
290
291
|
NitroPlayerLogger.log("TrackPlayerCore", "🔁 PLAYLIST repeat — rebuilding original queue and restarting")
|
|
@@ -414,9 +415,33 @@ extension TrackPlayerCore {
|
|
|
414
415
|
cleanupPreloadedAssets(keepingFrom: currentTrackIndex)
|
|
415
416
|
}
|
|
416
417
|
|
|
418
|
+
/// Attaches an `AVPlayerItemMetadataOutput` to the current item so ICY/ID3
|
|
419
|
+
/// metadata emitted mid-stream reaches JS. Detaches from the previous item first —
|
|
420
|
+
/// an output may only be attached to one item at a time.
|
|
421
|
+
func attachMetadataOutput(to item: AVPlayerItem) {
|
|
422
|
+
detachMetadataOutput()
|
|
423
|
+
let output = AVPlayerItemMetadataOutput(identifiers: nil)
|
|
424
|
+
output.setDelegate(self, queue: playerQueue)
|
|
425
|
+
item.add(output)
|
|
426
|
+
metadataOutput = output
|
|
427
|
+
metadataOutputItem = item
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/// Clears the metadata output so a detached one can no longer pass the identity
|
|
431
|
+
/// check in the delegate and emit metadata for an item that is no longer playing.
|
|
432
|
+
func detachMetadataOutput() {
|
|
433
|
+
if let output = metadataOutput, let item = metadataOutputItem {
|
|
434
|
+
item.remove(output)
|
|
435
|
+
}
|
|
436
|
+
metadataOutput = nil
|
|
437
|
+
metadataOutputItem = nil
|
|
438
|
+
}
|
|
439
|
+
|
|
417
440
|
func setupCurrentItemObservers(item: AVPlayerItem) {
|
|
418
441
|
NitroPlayerLogger.log("TrackPlayerCore", "📱 Setting up item observers")
|
|
419
442
|
|
|
443
|
+
attachMetadataOutput(to: item)
|
|
444
|
+
|
|
420
445
|
let statusObserver = item.observe(\.status, options: [.new]) { [weak self] item, _ in
|
|
421
446
|
self?.playerQueue.async {
|
|
422
447
|
if item.status == .readyToPlay {
|
|
@@ -481,6 +506,31 @@ extension TrackPlayerCore {
|
|
|
481
506
|
currentItemObservers.append(bufferKeepUpObserver)
|
|
482
507
|
}
|
|
483
508
|
|
|
509
|
+
func handleTimedMetadataGroups(_ groups: [AVTimedMetadataGroup]) {
|
|
510
|
+
var title: String?
|
|
511
|
+
var artist: String?
|
|
512
|
+
var album: String?
|
|
513
|
+
|
|
514
|
+
for group in groups {
|
|
515
|
+
for item in group.items {
|
|
516
|
+
guard let value = item.stringValue, !value.isEmpty else { continue }
|
|
517
|
+
switch item.commonKey {
|
|
518
|
+
case .some(.commonKeyTitle): title = value
|
|
519
|
+
case .some(.commonKeyArtist): artist = value
|
|
520
|
+
case .some(.commonKeyAlbumName): album = value
|
|
521
|
+
default:
|
|
522
|
+
// ICY/Shoutcast streams carry no common keys — match on the identifier
|
|
523
|
+
if item.identifier == .icyMetadataStreamTitle { title = value }
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
guard title != nil || artist != nil || album != nil else { return }
|
|
529
|
+
NitroPlayerLogger.log("TrackPlayerCore", "🏷️ Timed metadata - \(title ?? "-") / \(artist ?? "-")")
|
|
530
|
+
// artworkUrl is Android-only: iOS delivers embedded art as raw data, not a URL
|
|
531
|
+
notifyTimedMetadata(TimedMetadata(title: title, artist: artist, album: album, artworkUrl: nil))
|
|
532
|
+
}
|
|
533
|
+
|
|
484
534
|
func emitStateChange(reason: Reason? = nil) {
|
|
485
535
|
guard let player else { return }
|
|
486
536
|
let state: TrackPlayerState
|
|
@@ -501,3 +551,17 @@ extension TrackPlayerCore {
|
|
|
501
551
|
notifyPlaybackStateChange(state, reason)
|
|
502
552
|
}
|
|
503
553
|
}
|
|
554
|
+
|
|
555
|
+
extension TrackPlayerCore: AVPlayerItemMetadataOutputPushDelegate {
|
|
556
|
+
// Delivered on playerQueue (set in attachMetadataOutput)
|
|
557
|
+
func metadataOutput(
|
|
558
|
+
_ output: AVPlayerItemMetadataOutput,
|
|
559
|
+
didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup],
|
|
560
|
+
from track: AVPlayerItemTrack?
|
|
561
|
+
) {
|
|
562
|
+
// A detached output can still flush metadata it had already buffered — ignore it,
|
|
563
|
+
// it belongs to an item that is no longer current.
|
|
564
|
+
guard output === metadataOutput else { return }
|
|
565
|
+
handleTimedMetadataGroups(groups)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
@@ -46,6 +46,10 @@ extension TrackPlayerCore {
|
|
|
46
46
|
onTracksNeedUpdateListeners.forEach { $0(tracks, lookahead) }
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
func notifyTimedMetadata(_ metadata: TimedMetadata) {
|
|
50
|
+
onTimedMetadataListeners.forEach { $0(metadata) }
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
func notifyTemporaryQueueChange() {
|
|
50
54
|
let pn = playNextStack
|
|
51
55
|
let un = upNextQueue
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { TrackItem, TrackPlayerState, Reason } from '../types/PlayerQueue';
|
|
1
|
+
import type { TrackItem, TrackPlayerState, Reason, TimedMetadata } from '../types/PlayerQueue';
|
|
2
2
|
type PlaybackStateCallback = (state: TrackPlayerState, reason?: Reason) => void;
|
|
3
3
|
type TrackChangeCallback = (track: TrackItem, reason?: Reason) => void;
|
|
4
4
|
type PlaybackProgressCallback = (position: number, totalDuration: number, isManuallySeeked?: boolean) => void;
|
|
5
5
|
type SeekCallback = (position: number, totalDuration: number) => void;
|
|
6
|
+
type TimedMetadataCallback = (metadata: TimedMetadata) => void;
|
|
6
7
|
type TemporaryQueueCallback = (playNextQueue: TrackItem[], upNextQueue: TrackItem[]) => void;
|
|
7
8
|
/**
|
|
8
9
|
* Internal subscription manager that allows multiple hooks to subscribe
|
|
@@ -14,6 +15,7 @@ declare class CallbackSubscriptionManager {
|
|
|
14
15
|
private trackChangeSubscribers;
|
|
15
16
|
private playbackProgressSubscribers;
|
|
16
17
|
private seekSubscribers;
|
|
18
|
+
private timedMetadataSubscribers;
|
|
17
19
|
private temporaryQueueSubscribers;
|
|
18
20
|
private playlistsChangedSubscribers;
|
|
19
21
|
private isPlaylistsChangedRegistered;
|
|
@@ -21,6 +23,7 @@ declare class CallbackSubscriptionManager {
|
|
|
21
23
|
private isTrackChangeRegistered;
|
|
22
24
|
private isPlaybackProgressRegistered;
|
|
23
25
|
private isSeekRegistered;
|
|
26
|
+
private isTimedMetadataRegistered;
|
|
24
27
|
private isTemporaryQueueRegistered;
|
|
25
28
|
/**
|
|
26
29
|
* Subscribe to playback state changes
|
|
@@ -58,6 +61,12 @@ declare class CallbackSubscriptionManager {
|
|
|
58
61
|
private ensurePlaylistsChangedRegistered;
|
|
59
62
|
private ensureTemporaryQueueRegistered;
|
|
60
63
|
private ensureSeekRegistered;
|
|
64
|
+
/**
|
|
65
|
+
* Subscribe to in-stream (timed) metadata events
|
|
66
|
+
* @returns Unsubscribe function
|
|
67
|
+
*/
|
|
68
|
+
subscribeToTimedMetadata(callback: TimedMetadataCallback): () => void;
|
|
69
|
+
private ensureTimedMetadataRegistered;
|
|
61
70
|
}
|
|
62
71
|
export declare const callbackManager: CallbackSubscriptionManager;
|
|
63
72
|
export {};
|
|
@@ -9,6 +9,7 @@ class CallbackSubscriptionManager {
|
|
|
9
9
|
trackChangeSubscribers = new Set();
|
|
10
10
|
playbackProgressSubscribers = new Set();
|
|
11
11
|
seekSubscribers = new Set();
|
|
12
|
+
timedMetadataSubscribers = new Set();
|
|
12
13
|
temporaryQueueSubscribers = new Set();
|
|
13
14
|
playlistsChangedSubscribers = new Set();
|
|
14
15
|
isPlaylistsChangedRegistered = false;
|
|
@@ -16,6 +17,7 @@ class CallbackSubscriptionManager {
|
|
|
16
17
|
isTrackChangeRegistered = false;
|
|
17
18
|
isPlaybackProgressRegistered = false;
|
|
18
19
|
isSeekRegistered = false;
|
|
20
|
+
isTimedMetadataRegistered = false;
|
|
19
21
|
isTemporaryQueueRegistered = false;
|
|
20
22
|
/**
|
|
21
23
|
* Subscribe to playback state changes
|
|
@@ -203,6 +205,37 @@ class CallbackSubscriptionManager {
|
|
|
203
205
|
console.error('[CallbackManager] Failed to register seek callback:', error);
|
|
204
206
|
}
|
|
205
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Subscribe to in-stream (timed) metadata events
|
|
210
|
+
* @returns Unsubscribe function
|
|
211
|
+
*/
|
|
212
|
+
subscribeToTimedMetadata(callback) {
|
|
213
|
+
this.timedMetadataSubscribers.add(callback);
|
|
214
|
+
this.ensureTimedMetadataRegistered();
|
|
215
|
+
return () => {
|
|
216
|
+
this.timedMetadataSubscribers.delete(callback);
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
ensureTimedMetadataRegistered() {
|
|
220
|
+
if (this.isTimedMetadataRegistered)
|
|
221
|
+
return;
|
|
222
|
+
try {
|
|
223
|
+
TrackPlayer.onTimedMetadata((metadata) => {
|
|
224
|
+
this.timedMetadataSubscribers.forEach((subscriber) => {
|
|
225
|
+
try {
|
|
226
|
+
subscriber(metadata);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
console.error('[CallbackManager] Error in timed metadata subscriber:', error);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
this.isTimedMetadataRegistered = true;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
console.error('[CallbackManager] Failed to register timed metadata callback:', error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
206
239
|
}
|
|
207
240
|
// Export singleton instance
|
|
208
241
|
export const callbackManager = new CallbackSubscriptionManager();
|
package/lib/hooks/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { useOnPlaybackStateChange } from './useOnPlaybackStateChange';
|
|
|
4
4
|
export type { PlaybackStateResult } from './useOnPlaybackStateChange';
|
|
5
5
|
export { useOnSeek } from './useOnSeek';
|
|
6
6
|
export { useOnPlaybackProgressChange } from './useOnPlaybackProgressChange';
|
|
7
|
+
export { useTimedMetadata } from './useTimedMetadata';
|
|
7
8
|
export { useAndroidAutoConnection } from './useAndroidAutoConnection';
|
|
8
9
|
export { useAudioDevices } from './useAudioDevices';
|
|
9
10
|
export { useNowPlaying } from './useNowPlaying';
|
package/lib/hooks/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { useOnChangeTrack } from './useOnChangeTrack';
|
|
|
2
2
|
export { useOnPlaybackStateChange } from './useOnPlaybackStateChange';
|
|
3
3
|
export { useOnSeek } from './useOnSeek';
|
|
4
4
|
export { useOnPlaybackProgressChange } from './useOnPlaybackProgressChange';
|
|
5
|
+
export { useTimedMetadata } from './useTimedMetadata';
|
|
5
6
|
export { useAndroidAutoConnection } from './useAndroidAutoConnection';
|
|
6
7
|
export { useAudioDevices } from './useAudioDevices';
|
|
7
8
|
export { useNowPlaying } from './useNowPlaying';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TimedMetadata } from '../types/PlayerQueue';
|
|
2
|
+
/**
|
|
3
|
+
* Hook to subscribe to in-stream (timed) metadata — ICY/Shoutcast StreamTitle
|
|
4
|
+
* updates and ID3 frames emitted while a live stream plays.
|
|
5
|
+
*
|
|
6
|
+
* @returns The most recent metadata, or undefined until the first event arrives
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* function NowPlayingFromStream() {
|
|
11
|
+
* const metadata = useTimedMetadata()
|
|
12
|
+
* return <Text>{metadata?.title ?? 'Live'}</Text>
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function useTimedMetadata(): TimedMetadata | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { callbackManager } from './callbackManager';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to subscribe to in-stream (timed) metadata — ICY/Shoutcast StreamTitle
|
|
5
|
+
* updates and ID3 frames emitted while a live stream plays.
|
|
6
|
+
*
|
|
7
|
+
* @returns The most recent metadata, or undefined until the first event arrives
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* function NowPlayingFromStream() {
|
|
12
|
+
* const metadata = useTimedMetadata()
|
|
13
|
+
* return <Text>{metadata?.title ?? 'Live'}</Text>
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function useTimedMetadata() {
|
|
18
|
+
const [metadata, setMetadata] = useState(undefined);
|
|
19
|
+
useEffect(() => callbackManager.subscribeToTimedMetadata(setMetadata), []);
|
|
20
|
+
return metadata;
|
|
21
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
-
import type { QueueOperation, Reason, TrackItem, TrackPlayerState, PlayerState, PlayerConfig, Playlist } from '../types/PlayerQueue';
|
|
2
|
+
import type { QueueOperation, Reason, TrackItem, TrackPlayerState, PlayerState, PlayerConfig, Playlist, TimedMetadata } from '../types/PlayerQueue';
|
|
3
3
|
export interface PlayerQueue extends HybridObject<{
|
|
4
4
|
android: 'kotlin';
|
|
5
5
|
ios: 'swift';
|
|
@@ -41,6 +41,11 @@ export interface TrackPlayer extends HybridObject<{
|
|
|
41
41
|
onPlaybackStateChange(callback: (state: TrackPlayerState, reason?: Reason) => void): void;
|
|
42
42
|
onSeek(callback: (position: number, totalDuration: number) => void): void;
|
|
43
43
|
onPlaybackProgressChange(callback: (position: number, totalDuration: number, isManuallySeeked?: boolean) => void): void;
|
|
44
|
+
/**
|
|
45
|
+
* Register callback for in-stream (timed) metadata — ICY/Shoutcast StreamTitle
|
|
46
|
+
* updates and ID3 frames emitted while a live stream plays.
|
|
47
|
+
*/
|
|
48
|
+
onTimedMetadata(callback: (metadata: TimedMetadata) => void): void;
|
|
44
49
|
onAndroidAutoConnectionChange(callback: (connected: boolean) => void): void;
|
|
45
50
|
isAndroidAutoConnected(): boolean;
|
|
46
51
|
setVolume(volume: number): Promise<void>;
|
|
@@ -29,6 +29,13 @@ export interface PlayerState {
|
|
|
29
29
|
currentIndex: number;
|
|
30
30
|
currentPlayingType: CurrentPlayingType;
|
|
31
31
|
}
|
|
32
|
+
/** In-stream metadata emitted while a stream plays (ICY/Shoutcast StreamTitle, ID3 frames) */
|
|
33
|
+
export interface TimedMetadata {
|
|
34
|
+
title?: string;
|
|
35
|
+
artist?: string;
|
|
36
|
+
album?: string;
|
|
37
|
+
artworkUrl?: string;
|
|
38
|
+
}
|
|
32
39
|
export interface PlayerConfig {
|
|
33
40
|
androidAutoEnabled?: boolean;
|
|
34
41
|
carPlayEnabled?: boolean;
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
#include "JFunc_void_TrackPlayerState_std__optional_Reason_.hpp"
|
|
36
36
|
#include "JFunc_void_double_double.hpp"
|
|
37
37
|
#include "JFunc_void_double_double_std__optional_bool_.hpp"
|
|
38
|
+
#include "JFunc_void_TimedMetadata.hpp"
|
|
38
39
|
#include "JFunc_void_std__vector_TrackItem__double.hpp"
|
|
39
40
|
#include "JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp"
|
|
40
41
|
#include <NitroModules/DefaultConstructableObject.hpp>
|
|
@@ -129,6 +130,7 @@ void registerAllNatives() {
|
|
|
129
130
|
margelo::nitro::nitroplayer::JFunc_void_TrackPlayerState_std__optional_Reason__cxx::registerNatives();
|
|
130
131
|
margelo::nitro::nitroplayer::JFunc_void_double_double_cxx::registerNatives();
|
|
131
132
|
margelo::nitro::nitroplayer::JFunc_void_double_double_std__optional_bool__cxx::registerNatives();
|
|
133
|
+
margelo::nitro::nitroplayer::JFunc_void_TimedMetadata_cxx::registerNatives();
|
|
132
134
|
margelo::nitro::nitroplayer::JFunc_void_std__vector_TrackItem__double_cxx::registerNatives();
|
|
133
135
|
margelo::nitro::nitroplayer::JFunc_void_std__vector_TrackItem__std__vector_TrackItem__cxx::registerNatives();
|
|
134
136
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// JFunc_void_TimedMetadata.hpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <fbjni/fbjni.h>
|
|
11
|
+
#include <functional>
|
|
12
|
+
|
|
13
|
+
#include "TimedMetadata.hpp"
|
|
14
|
+
#include <functional>
|
|
15
|
+
#include <NitroModules/JNICallable.hpp>
|
|
16
|
+
#include "JTimedMetadata.hpp"
|
|
17
|
+
#include <string>
|
|
18
|
+
#include <optional>
|
|
19
|
+
|
|
20
|
+
namespace margelo::nitro::nitroplayer {
|
|
21
|
+
|
|
22
|
+
using namespace facebook;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Represents the Java/Kotlin callback `(metadata: TimedMetadata) -> Unit`.
|
|
26
|
+
* This can be passed around between C++ and Java/Kotlin.
|
|
27
|
+
*/
|
|
28
|
+
struct JFunc_void_TimedMetadata: public jni::JavaClass<JFunc_void_TimedMetadata> {
|
|
29
|
+
public:
|
|
30
|
+
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/nitroplayer/Func_void_TimedMetadata;";
|
|
31
|
+
|
|
32
|
+
public:
|
|
33
|
+
/**
|
|
34
|
+
* Invokes the function this `JFunc_void_TimedMetadata` instance holds through JNI.
|
|
35
|
+
*/
|
|
36
|
+
void invoke(const TimedMetadata& metadata) const {
|
|
37
|
+
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<JTimedMetadata> /* metadata */)>("invoke");
|
|
38
|
+
method(self(), JTimedMetadata::fromCpp(metadata));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* An implementation of Func_void_TimedMetadata that is backed by a C++ implementation (using `std::function<...>`)
|
|
44
|
+
*/
|
|
45
|
+
class JFunc_void_TimedMetadata_cxx final: public jni::HybridClass<JFunc_void_TimedMetadata_cxx, JFunc_void_TimedMetadata> {
|
|
46
|
+
public:
|
|
47
|
+
static jni::local_ref<JFunc_void_TimedMetadata::javaobject> fromCpp(const std::function<void(const TimedMetadata& /* metadata */)>& func) {
|
|
48
|
+
return JFunc_void_TimedMetadata_cxx::newObjectCxxArgs(func);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public:
|
|
52
|
+
/**
|
|
53
|
+
* Invokes the C++ `std::function<...>` this `JFunc_void_TimedMetadata_cxx` instance holds.
|
|
54
|
+
*/
|
|
55
|
+
void invoke_cxx(jni::alias_ref<JTimedMetadata> metadata) {
|
|
56
|
+
_func(metadata->toCpp());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public:
|
|
60
|
+
[[nodiscard]]
|
|
61
|
+
inline const std::function<void(const TimedMetadata& /* metadata */)>& getFunction() const {
|
|
62
|
+
return _func;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public:
|
|
66
|
+
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/nitroplayer/Func_void_TimedMetadata_cxx;";
|
|
67
|
+
static void registerNatives() {
|
|
68
|
+
registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_TimedMetadata_cxx::invoke_cxx)});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private:
|
|
72
|
+
explicit JFunc_void_TimedMetadata_cxx(const std::function<void(const TimedMetadata& /* metadata */)>& func): _func(func) { }
|
|
73
|
+
|
|
74
|
+
private:
|
|
75
|
+
friend HybridBase;
|
|
76
|
+
std::function<void(const TimedMetadata& /* metadata */)> _func;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
} // namespace margelo::nitro::nitroplayer
|
|
@@ -21,6 +21,8 @@ namespace margelo::nitro::nitroplayer { enum class RepeatMode; }
|
|
|
21
21
|
namespace margelo::nitro::nitroplayer { struct PlayerConfig; }
|
|
22
22
|
// Forward declaration of `Reason` to properly resolve imports.
|
|
23
23
|
namespace margelo::nitro::nitroplayer { enum class Reason; }
|
|
24
|
+
// Forward declaration of `TimedMetadata` to properly resolve imports.
|
|
25
|
+
namespace margelo::nitro::nitroplayer { struct TimedMetadata; }
|
|
24
26
|
|
|
25
27
|
#include <NitroModules/Promise.hpp>
|
|
26
28
|
#include <NitroModules/JPromise.hpp>
|
|
@@ -55,6 +57,9 @@ namespace margelo::nitro::nitroplayer { enum class Reason; }
|
|
|
55
57
|
#include "JFunc_void_TrackPlayerState_std__optional_Reason_.hpp"
|
|
56
58
|
#include "JFunc_void_double_double.hpp"
|
|
57
59
|
#include "JFunc_void_double_double_std__optional_bool_.hpp"
|
|
60
|
+
#include "TimedMetadata.hpp"
|
|
61
|
+
#include "JFunc_void_TimedMetadata.hpp"
|
|
62
|
+
#include "JTimedMetadata.hpp"
|
|
58
63
|
#include "JFunc_void_bool.hpp"
|
|
59
64
|
#include "JFunc_void_std__vector_TrackItem__double.hpp"
|
|
60
65
|
#include "JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp"
|
|
@@ -320,6 +325,10 @@ namespace margelo::nitro::nitroplayer {
|
|
|
320
325
|
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_double_double_std__optional_bool_::javaobject> /* callback */)>("onPlaybackProgressChange_cxx");
|
|
321
326
|
method(_javaPart, JFunc_void_double_double_std__optional_bool__cxx::fromCpp(callback));
|
|
322
327
|
}
|
|
328
|
+
void JHybridTrackPlayerSpec::onTimedMetadata(const std::function<void(const TimedMetadata& /* metadata */)>& callback) {
|
|
329
|
+
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_TimedMetadata::javaobject> /* callback */)>("onTimedMetadata_cxx");
|
|
330
|
+
method(_javaPart, JFunc_void_TimedMetadata_cxx::fromCpp(callback));
|
|
331
|
+
}
|
|
323
332
|
void JHybridTrackPlayerSpec::onAndroidAutoConnectionChange(const std::function<void(bool /* connected */)>& callback) {
|
|
324
333
|
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_bool::javaobject> /* callback */)>("onAndroidAutoConnectionChange_cxx");
|
|
325
334
|
method(_javaPart, JFunc_void_bool_cxx::fromCpp(callback));
|
|
@@ -72,6 +72,7 @@ namespace margelo::nitro::nitroplayer {
|
|
|
72
72
|
void onPlaybackStateChange(const std::function<void(TrackPlayerState /* state */, std::optional<Reason> /* reason */)>& callback) override;
|
|
73
73
|
void onSeek(const std::function<void(double /* position */, double /* totalDuration */)>& callback) override;
|
|
74
74
|
void onPlaybackProgressChange(const std::function<void(double /* position */, double /* totalDuration */, std::optional<bool> /* isManuallySeeked */)>& callback) override;
|
|
75
|
+
void onTimedMetadata(const std::function<void(const TimedMetadata& /* metadata */)>& callback) override;
|
|
75
76
|
void onAndroidAutoConnectionChange(const std::function<void(bool /* connected */)>& callback) override;
|
|
76
77
|
bool isAndroidAutoConnected() override;
|
|
77
78
|
std::shared_ptr<Promise<void>> setVolume(double volume) override;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// JTimedMetadata.hpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <fbjni/fbjni.h>
|
|
11
|
+
#include "TimedMetadata.hpp"
|
|
12
|
+
|
|
13
|
+
#include <optional>
|
|
14
|
+
#include <string>
|
|
15
|
+
|
|
16
|
+
namespace margelo::nitro::nitroplayer {
|
|
17
|
+
|
|
18
|
+
using namespace facebook;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The C++ JNI bridge between the C++ struct "TimedMetadata" and the the Kotlin data class "TimedMetadata".
|
|
22
|
+
*/
|
|
23
|
+
struct JTimedMetadata final: public jni::JavaClass<JTimedMetadata> {
|
|
24
|
+
public:
|
|
25
|
+
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/nitroplayer/TimedMetadata;";
|
|
26
|
+
|
|
27
|
+
public:
|
|
28
|
+
/**
|
|
29
|
+
* Convert this Java/Kotlin-based struct to the C++ struct TimedMetadata by copying all values to C++.
|
|
30
|
+
*/
|
|
31
|
+
[[maybe_unused]]
|
|
32
|
+
[[nodiscard]]
|
|
33
|
+
TimedMetadata toCpp() const {
|
|
34
|
+
static const auto clazz = javaClassStatic();
|
|
35
|
+
static const auto fieldTitle = clazz->getField<jni::JString>("title");
|
|
36
|
+
jni::local_ref<jni::JString> title = this->getFieldValue(fieldTitle);
|
|
37
|
+
static const auto fieldArtist = clazz->getField<jni::JString>("artist");
|
|
38
|
+
jni::local_ref<jni::JString> artist = this->getFieldValue(fieldArtist);
|
|
39
|
+
static const auto fieldAlbum = clazz->getField<jni::JString>("album");
|
|
40
|
+
jni::local_ref<jni::JString> album = this->getFieldValue(fieldAlbum);
|
|
41
|
+
static const auto fieldArtworkUrl = clazz->getField<jni::JString>("artworkUrl");
|
|
42
|
+
jni::local_ref<jni::JString> artworkUrl = this->getFieldValue(fieldArtworkUrl);
|
|
43
|
+
return TimedMetadata(
|
|
44
|
+
title != nullptr ? std::make_optional(title->toStdString()) : std::nullopt,
|
|
45
|
+
artist != nullptr ? std::make_optional(artist->toStdString()) : std::nullopt,
|
|
46
|
+
album != nullptr ? std::make_optional(album->toStdString()) : std::nullopt,
|
|
47
|
+
artworkUrl != nullptr ? std::make_optional(artworkUrl->toStdString()) : std::nullopt
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public:
|
|
52
|
+
/**
|
|
53
|
+
* Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java.
|
|
54
|
+
*/
|
|
55
|
+
[[maybe_unused]]
|
|
56
|
+
static jni::local_ref<JTimedMetadata::javaobject> fromCpp(const TimedMetadata& value) {
|
|
57
|
+
using JSignature = JTimedMetadata(jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>);
|
|
58
|
+
static const auto clazz = javaClassStatic();
|
|
59
|
+
static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
|
|
60
|
+
return create(
|
|
61
|
+
clazz,
|
|
62
|
+
value.title.has_value() ? jni::make_jstring(value.title.value()) : nullptr,
|
|
63
|
+
value.artist.has_value() ? jni::make_jstring(value.artist.value()) : nullptr,
|
|
64
|
+
value.album.has_value() ? jni::make_jstring(value.album.value()) : nullptr,
|
|
65
|
+
value.artworkUrl.has_value() ? jni::make_jstring(value.artworkUrl.value()) : nullptr
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
} // namespace margelo::nitro::nitroplayer
|