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.
Files changed (41) hide show
  1. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +6 -0
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +7 -0
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +20 -0
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +5 -0
  5. package/ios/HybridTrackPlayer.swift +6 -0
  6. package/ios/core/TrackPlayerCore.swift +13 -0
  7. package/ios/core/TrackPlayerListener.swift +64 -0
  8. package/ios/core/TrackPlayerNotify.swift +4 -0
  9. package/lib/hooks/callbackManager.d.ts +10 -1
  10. package/lib/hooks/callbackManager.js +33 -0
  11. package/lib/hooks/index.d.ts +1 -0
  12. package/lib/hooks/index.js +1 -0
  13. package/lib/hooks/useTimedMetadata.d.ts +16 -0
  14. package/lib/hooks/useTimedMetadata.js +21 -0
  15. package/lib/specs/TrackPlayer.nitro.d.ts +6 -1
  16. package/lib/types/PlayerQueue.d.ts +7 -0
  17. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
  18. package/nitrogen/generated/android/c++/JFunc_void_TimedMetadata.hpp +79 -0
  19. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +9 -0
  20. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
  21. package/nitrogen/generated/android/c++/JTimedMetadata.hpp +70 -0
  22. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TimedMetadata.kt +80 -0
  23. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +9 -0
  24. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TimedMetadata.kt +47 -0
  25. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +8 -0
  26. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +25 -0
  27. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +3 -0
  28. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +9 -0
  29. package/nitrogen/generated/ios/swift/Func_void_TimedMetadata.swift +46 -0
  30. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
  31. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +16 -0
  32. package/nitrogen/generated/ios/swift/TimedMetadata.swift +96 -0
  33. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
  34. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +4 -0
  35. package/nitrogen/generated/shared/c++/TimedMetadata.hpp +96 -0
  36. package/package.json +1 -1
  37. package/src/hooks/callbackManager.ts +47 -1
  38. package/src/hooks/index.ts +1 -0
  39. package/src/hooks/useTimedMetadata.ts +25 -0
  40. package/src/specs/TrackPlayer.nitro.ts +6 -0
  41. 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();
@@ -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';
@@ -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