react-native-nitro-player 1.5.1-alpha.0 → 1.6.0-alpha.1
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 +32 -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 +89 -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 +15 -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 +74 -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 +50 -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 +114 -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 +100 -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 +16 -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,14 @@
|
|
|
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
|
|
10
|
+
import androidx.media3.extractor.metadata.icy.IcyInfo
|
|
7
11
|
import com.margelo.nitro.nitroplayer.Reason
|
|
8
12
|
import com.margelo.nitro.nitroplayer.RepeatMode
|
|
13
|
+
import com.margelo.nitro.nitroplayer.TimedMetadata
|
|
9
14
|
import com.margelo.nitro.nitroplayer.equalizer.EqualizerCore
|
|
10
15
|
import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
11
16
|
|
|
@@ -82,6 +87,33 @@ internal class TrackPlayerEventListener(
|
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
/** In-stream metadata (ICY StreamTitle, ID3 frames) arriving mid-playback. */
|
|
91
|
+
@UnstableApi
|
|
92
|
+
override fun onMetadata(metadata: Metadata) {
|
|
93
|
+
val builder = MediaMetadata.Builder()
|
|
94
|
+
var url: String? = null
|
|
95
|
+
for (i in 0 until metadata.length()) {
|
|
96
|
+
val entry = metadata.get(i)
|
|
97
|
+
entry.populateMediaMetadata(builder)
|
|
98
|
+
// populateMediaMetadata drops IcyInfo.url — radio providers put the
|
|
99
|
+
// current track's artwork URL there, so read it off the entry directly.
|
|
100
|
+
if (entry is IcyInfo && !entry.url.isNullOrEmpty()) url = entry.url
|
|
101
|
+
}
|
|
102
|
+
val merged = builder.build()
|
|
103
|
+
var artist = merged.artist?.toString()
|
|
104
|
+
var title = merged.title?.toString()
|
|
105
|
+
// ICY streams pack both fields into one "Artist - Title" string
|
|
106
|
+
val separator = if (artist == null) title?.indexOf(" - ") ?: -1 else -1
|
|
107
|
+
if (separator > 0 && title != null && separator + 3 < title.length) {
|
|
108
|
+
artist = title.substring(0, separator)
|
|
109
|
+
title = title.substring(separator + 3)
|
|
110
|
+
}
|
|
111
|
+
val album = merged.albumTitle?.toString()
|
|
112
|
+
val artworkUrl = merged.artworkUri?.toString()
|
|
113
|
+
if (title == null && artist == null && album == null && url == null && artworkUrl == null) return
|
|
114
|
+
core.notifyTimedMetadata(TimedMetadata(title, artist, album, url, artworkUrl))
|
|
115
|
+
}
|
|
116
|
+
|
|
85
117
|
override fun onTimelineChanged(
|
|
86
118
|
timeline: androidx.media3.common.Timeline,
|
|
87
119
|
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,56 @@ 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
|
+
var url: String?
|
|
514
|
+
var artworkUrl: String?
|
|
515
|
+
|
|
516
|
+
for group in groups {
|
|
517
|
+
for item in group.items {
|
|
518
|
+
guard let value = item.stringValue, !value.isEmpty else { continue }
|
|
519
|
+
switch item.commonKey {
|
|
520
|
+
case .some(.commonKeyTitle): title = value
|
|
521
|
+
case .some(.commonKeyArtist): artist = value
|
|
522
|
+
case .some(.commonKeyAlbumName): album = value
|
|
523
|
+
case .some(.commonKeyArtwork): artworkUrl = value
|
|
524
|
+
default:
|
|
525
|
+
// ICY/Shoutcast streams carry no common keys — match on the identifier.
|
|
526
|
+
// StreamUrl is where radio providers put the current track's artwork.
|
|
527
|
+
switch item.identifier {
|
|
528
|
+
case .some(.icyMetadataStreamTitle): title = value
|
|
529
|
+
case .some(.icyMetadataStreamURL): url = value
|
|
530
|
+
case .some(.id3MetadataOfficialAudioSourceWebpage),
|
|
531
|
+
.some(.id3MetadataOfficialAudioFileWebpage):
|
|
532
|
+
if url == nil { url = value }
|
|
533
|
+
default: break
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
(artist, title) = splitStreamTitle(artist: artist, title: title)
|
|
540
|
+
|
|
541
|
+
guard title != nil || artist != nil || album != nil || url != nil || artworkUrl != nil else { return }
|
|
542
|
+
NitroPlayerLogger.log("TrackPlayerCore", "🏷️ Timed metadata - \(title ?? "-") / \(artist ?? "-")")
|
|
543
|
+
notifyTimedMetadata(
|
|
544
|
+
TimedMetadata(title: title, artist: artist, album: album, url: url, artworkUrl: artworkUrl))
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/// ICY streams pack both fields into one `"Artist - Title"` string. Split it when
|
|
548
|
+
/// the stream gave no separate artist, leaving an explicit artist untouched.
|
|
549
|
+
func splitStreamTitle(artist: String?, title: String?) -> (String?, String?) {
|
|
550
|
+
guard artist == nil, let title, let separator = title.range(of: " - ") else {
|
|
551
|
+
return (artist, title)
|
|
552
|
+
}
|
|
553
|
+
let left = String(title[title.startIndex..<separator.lowerBound])
|
|
554
|
+
let right = String(title[separator.upperBound...])
|
|
555
|
+
guard !left.isEmpty, !right.isEmpty else { return (artist, title) }
|
|
556
|
+
return (left, right)
|
|
557
|
+
}
|
|
558
|
+
|
|
484
559
|
func emitStateChange(reason: Reason? = nil) {
|
|
485
560
|
guard let player else { return }
|
|
486
561
|
let state: TrackPlayerState
|
|
@@ -501,3 +576,17 @@ extension TrackPlayerCore {
|
|
|
501
576
|
notifyPlaybackStateChange(state, reason)
|
|
502
577
|
}
|
|
503
578
|
}
|
|
579
|
+
|
|
580
|
+
extension TrackPlayerCore: AVPlayerItemMetadataOutputPushDelegate {
|
|
581
|
+
// Delivered on playerQueue (set in attachMetadataOutput)
|
|
582
|
+
func metadataOutput(
|
|
583
|
+
_ output: AVPlayerItemMetadataOutput,
|
|
584
|
+
didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup],
|
|
585
|
+
from track: AVPlayerItemTrack?
|
|
586
|
+
) {
|
|
587
|
+
// A detached output can still flush metadata it had already buffered — ignore it,
|
|
588
|
+
// it belongs to an item that is no longer current.
|
|
589
|
+
guard output === metadataOutput else { return }
|
|
590
|
+
handleTimedMetadataGroups(groups)
|
|
591
|
+
}
|
|
592
|
+
}
|
|
@@ -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,21 @@ 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
|
+
/** Split out of `title` when the stream sends only `"Artist - Title"` */
|
|
36
|
+
artist?: string;
|
|
37
|
+
album?: string;
|
|
38
|
+
/**
|
|
39
|
+
* ICY `StreamUrl` / ID3 webpage frame. Radio providers commonly put the
|
|
40
|
+
* current track's artwork URL here (Live365, StreamGuys), but some send a
|
|
41
|
+
* station or artist page instead — check the value before treating it as art.
|
|
42
|
+
*/
|
|
43
|
+
url?: string;
|
|
44
|
+
/** Artwork URL the stream tagged as artwork, when it sends one explicitly */
|
|
45
|
+
artworkUrl?: string;
|
|
46
|
+
}
|
|
32
47
|
export interface PlayerConfig {
|
|
33
48
|
androidAutoEnabled?: boolean;
|
|
34
49
|
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;
|