react-native-nitro-player 1.6.0-alpha.0 → 1.6.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 (29) 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 +59 -0
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +17 -5
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +15 -0
  5. package/ios/HybridTrackPlayer.swift +4 -0
  6. package/ios/core/TrackPlayerListener.swift +30 -5
  7. package/lib/specs/TrackPlayer.nitro.d.ts +5 -0
  8. package/lib/types/PlayerQueue.d.ts +8 -0
  9. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
  10. package/nitrogen/generated/android/c++/JFunc_void_TrackItem.hpp +85 -0
  11. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +5 -0
  12. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
  13. package/nitrogen/generated/android/c++/JTimedMetadata.hpp +5 -1
  14. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TrackItem.kt +80 -0
  15. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +9 -0
  16. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TimedMetadata.kt +5 -2
  17. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +8 -0
  18. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +22 -0
  19. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -0
  20. package/nitrogen/generated/ios/swift/Func_void_TrackItem.swift +46 -0
  21. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
  22. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +16 -0
  23. package/nitrogen/generated/ios/swift/TimedMetadata.swift +19 -1
  24. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
  25. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +1 -0
  26. package/nitrogen/generated/shared/c++/TimedMetadata.hpp +5 -1
  27. package/package.json +1 -1
  28. package/src/specs/TrackPlayer.nitro.ts +5 -0
  29. package/src/types/PlayerQueue.ts +8 -0
@@ -178,6 +178,11 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
178
178
  listenerIds += "onAndroidAutoConnectionChange" to id
179
179
  }
180
180
 
181
+ override fun appStartedWithNotification(callback: (track: TrackItem) -> Unit) {
182
+ val id = core.addOnNotificationLaunchListener(callback)
183
+ listenerIds += "appStartedWithNotification" to id
184
+ }
185
+
181
186
  override fun onTracksNeedUpdate(callback: (tracks: Array<TrackItem>, lookahead: Double) -> Unit) {
182
187
  val id =
183
188
  core.addOnTracksNeedUpdateListener { tracks, lookahead ->
@@ -213,6 +218,7 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
213
218
  "onTracksNeedUpdate" -> core.removeOnTracksNeedUpdateListener(id)
214
219
  "onTemporaryQueueChange" -> core.removeOnTemporaryQueueChangeListener(id)
215
220
  "onTimedMetadata" -> core.removeOnTimedMetadataListener(id)
221
+ "appStartedWithNotification" -> core.removeOnNotificationLaunchListener(id)
216
222
  }
217
223
  }
218
224
  listenerIds.clear()
@@ -2,13 +2,17 @@
2
2
 
3
3
  package com.margelo.nitro.nitroplayer.core
4
4
 
5
+ import android.app.Activity
6
+ import android.app.Application
5
7
  import android.content.ComponentName
6
8
  import android.content.Context
7
9
  import android.content.Intent
8
10
  import android.content.ServiceConnection
11
+ import android.os.Bundle
9
12
  import android.os.Handler
10
13
  import android.os.IBinder
11
14
  import android.os.Looper
15
+ import com.facebook.react.bridge.ReactApplicationContext
12
16
  import com.margelo.nitro.nitroplayer.Reason
13
17
  import com.margelo.nitro.nitroplayer.RepeatMode
14
18
  import com.margelo.nitro.nitroplayer.TimedMetadata
@@ -95,6 +99,8 @@ class TrackPlayerCore private constructor(
95
99
  ListenerRegistry<(TimedMetadata) -> Unit>()
96
100
  internal val onCastStateChangeListeners =
97
101
  ListenerRegistry<(com.margelo.nitro.nitroplayer.CastState, String?) -> Unit>()
102
+ internal val onNotificationLaunchListeners =
103
+ ListenerRegistry<(TrackItem) -> Unit>()
98
104
 
99
105
  // ── Google Cast ──────────────────────────────────────────────────────────
100
106
 
@@ -390,4 +396,57 @@ class TrackPlayerCore private constructor(
390
396
  fun addOnCastStateChangeListener(cb: (com.margelo.nitro.nitroplayer.CastState, String?) -> Unit): Long = onCastStateChangeListeners.add(cb)
391
397
 
392
398
  fun removeOnCastStateChangeListener(id: Long): Boolean = onCastStateChangeListeners.remove(id)
399
+
400
+ // ── Notification-launch detection ─────────────────────────────────────
401
+
402
+ private var notificationLaunchDetectionRegistered = false
403
+
404
+ fun addOnNotificationLaunchListener(cb: (TrackItem) -> Unit): Long {
405
+ val id = onNotificationLaunchListeners.add(cb)
406
+ handler.post {
407
+ registerNotificationLaunchDetection()
408
+ checkNotificationLaunch((context as? ReactApplicationContext)?.currentActivity)
409
+ }
410
+ return id
411
+ }
412
+
413
+ fun removeOnNotificationLaunchListener(id: Long): Boolean = onNotificationLaunchListeners.remove(id)
414
+
415
+ private fun registerNotificationLaunchDetection() {
416
+ if (notificationLaunchDetectionRegistered) return
417
+ notificationLaunchDetectionRegistered = true
418
+ val app = context.applicationContext as? Application ?: return
419
+ app.registerActivityLifecycleCallbacks(
420
+ object : Application.ActivityLifecycleCallbacks {
421
+ override fun onActivityResumed(activity: Activity) = checkNotificationLaunch(activity)
422
+
423
+ override fun onActivityCreated(
424
+ activity: Activity,
425
+ savedInstanceState: Bundle?,
426
+ ) {}
427
+
428
+ override fun onActivityStarted(activity: Activity) {}
429
+
430
+ override fun onActivityPaused(activity: Activity) {}
431
+
432
+ override fun onActivityStopped(activity: Activity) {}
433
+
434
+ override fun onActivitySaveInstanceState(
435
+ activity: Activity,
436
+ outState: Bundle,
437
+ ) {}
438
+
439
+ override fun onActivityDestroyed(activity: Activity) {}
440
+ },
441
+ )
442
+ }
443
+
444
+ // Must run on the main looper — reads the player and mutates the activity intent
445
+ internal fun checkNotificationLaunch(activity: Activity?) {
446
+ val intent = activity?.intent ?: return
447
+ if (!intent.getBooleanExtra(NitroPlayerPlaybackService.EXTRA_STARTED_FROM_NOTIFICATION, false)) return
448
+ val track = getCurrentTrack() ?: return
449
+ intent.removeExtra(NitroPlayerPlaybackService.EXTRA_STARTED_FROM_NOTIFICATION)
450
+ onNotificationLaunchListeners.forEach { it(track) }
451
+ }
393
452
  }
@@ -7,6 +7,7 @@ import androidx.media3.common.MediaMetadata
7
7
  import androidx.media3.common.Metadata
8
8
  import androidx.media3.common.Player
9
9
  import androidx.media3.common.util.UnstableApi
10
+ import androidx.media3.extractor.metadata.icy.IcyInfo
10
11
  import com.margelo.nitro.nitroplayer.Reason
11
12
  import com.margelo.nitro.nitroplayer.RepeatMode
12
13
  import com.margelo.nitro.nitroplayer.TimedMetadata
@@ -90,16 +91,27 @@ internal class TrackPlayerEventListener(
90
91
  @UnstableApi
91
92
  override fun onMetadata(metadata: Metadata) {
92
93
  val builder = MediaMetadata.Builder()
94
+ var url: String? = null
93
95
  for (i in 0 until metadata.length()) {
94
- metadata.get(i).populateMediaMetadata(builder)
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
95
101
  }
96
102
  val merged = builder.build()
97
- val title = merged.title?.toString()
98
- val artist = merged.artist?.toString()
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
+ }
99
111
  val album = merged.albumTitle?.toString()
100
112
  val artworkUrl = merged.artworkUri?.toString()
101
- if (title == null && artist == null && album == null && artworkUrl == null) return
102
- core.notifyTimedMetadata(TimedMetadata(title, artist, album, artworkUrl))
113
+ if (title == null && artist == null && album == null && url == null && artworkUrl == null) return
114
+ core.notifyTimedMetadata(TimedMetadata(title, artist, album, url, artworkUrl))
103
115
  }
104
116
 
105
117
  override fun onTimelineChanged(
@@ -1,5 +1,6 @@
1
1
  package com.margelo.nitro.nitroplayer.media
2
2
 
3
+ import android.app.PendingIntent
3
4
  import android.content.Intent
4
5
  import android.os.Binder
5
6
  import android.os.Handler
@@ -32,6 +33,7 @@ class NitroPlayerPlaybackService : MediaSessionService() {
32
33
 
33
34
  companion object {
34
35
  const val ACTION_LOCAL_BIND = "com.margelo.nitro.nitroplayer.LOCAL_BIND"
36
+ const val EXTRA_STARTED_FROM_NOTIFICATION = "com.margelo.nitro.nitroplayer.STARTED_FROM_NOTIFICATION"
35
37
 
36
38
  @Volatile
37
39
  var notificationSmallIconResName: String? = null
@@ -84,6 +86,7 @@ class NitroPlayerPlaybackService : MediaSessionService() {
84
86
  mediaSession = MediaSession
85
87
  .Builder(this, player)
86
88
  .setCallback(MediaSessionCallbackFactory.create(this, playlistManager))
89
+ .apply { buildSessionActivity()?.let { setSessionActivity(it) } }
87
90
  .build()
88
91
 
89
92
  val provider = DefaultMediaNotificationProvider.Builder(this).build()
@@ -119,6 +122,18 @@ class NitroPlayerPlaybackService : MediaSessionService() {
119
122
  }
120
123
  }
121
124
 
125
+ // Content intent for the notification tap — newer Android has no fallback without it
126
+ private fun buildSessionActivity(): PendingIntent? {
127
+ val launch = packageManager.getLaunchIntentForPackage(packageName) ?: return null
128
+ launch.putExtra(EXTRA_STARTED_FROM_NOTIFICATION, true)
129
+ return PendingIntent.getActivity(
130
+ this,
131
+ 0,
132
+ launch,
133
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
134
+ )
135
+ }
136
+
122
137
  private fun applyNotificationSmallIcon() {
123
138
  val name = notificationSmallIconResName ?: return
124
139
  val provider = notificationProvider ?: return
@@ -188,6 +188,10 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
188
188
  // No-op on iOS
189
189
  }
190
190
 
191
+ func appStartedWithNotification(callback: @escaping (_ track: TrackItem) -> Void) throws {
192
+ // No-op on iOS — Now Playing controls never launch the app
193
+ }
194
+
191
195
  func isAndroidAutoConnected() throws -> Bool { false }
192
196
 
193
197
  // MARK: - Event listeners (v2 — store IDs for cleanup)
@@ -510,6 +510,8 @@ extension TrackPlayerCore {
510
510
  var title: String?
511
511
  var artist: String?
512
512
  var album: String?
513
+ var url: String?
514
+ var artworkUrl: String?
513
515
 
514
516
  for group in groups {
515
517
  for item in group.items {
@@ -518,17 +520,40 @@ extension TrackPlayerCore {
518
520
  case .some(.commonKeyTitle): title = value
519
521
  case .some(.commonKeyArtist): artist = value
520
522
  case .some(.commonKeyAlbumName): album = value
523
+ case .some(.commonKeyArtwork): artworkUrl = value
521
524
  default:
522
- // ICY/Shoutcast streams carry no common keys — match on the identifier
523
- if item.identifier == .icyMetadataStreamTitle { title = value }
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
+ }
524
535
  }
525
536
  }
526
537
  }
527
538
 
528
- guard title != nil || artist != nil || album != nil else { return }
539
+ (artist, title) = splitStreamTitle(artist: artist, title: title)
540
+
541
+ guard title != nil || artist != nil || album != nil || url != nil || artworkUrl != nil else { return }
529
542
  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))
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)
532
557
  }
533
558
 
534
559
  func emitStateChange(reason: Reason? = nil) {
@@ -46,6 +46,11 @@ export interface TrackPlayer extends HybridObject<{
46
46
  * updates and ID3 frames emitted while a live stream plays.
47
47
  */
48
48
  onTimedMetadata(callback: (metadata: TimedMetadata) => void): void;
49
+ /**
50
+ * Fires when the app was opened by tapping the playback notification,
51
+ * with the track that was playing at tap time. Android only — no-op on iOS.
52
+ */
53
+ appStartedWithNotification(callback: (track: TrackItem) => void): void;
49
54
  onAndroidAutoConnectionChange(callback: (connected: boolean) => void): void;
50
55
  isAndroidAutoConnected(): boolean;
51
56
  setVolume(volume: number): Promise<void>;
@@ -32,8 +32,16 @@ export interface PlayerState {
32
32
  /** In-stream metadata emitted while a stream plays (ICY/Shoutcast StreamTitle, ID3 frames) */
33
33
  export interface TimedMetadata {
34
34
  title?: string;
35
+ /** Split out of `title` when the stream sends only `"Artist - Title"` */
35
36
  artist?: string;
36
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 */
37
45
  artworkUrl?: string;
38
46
  }
39
47
  export interface PlayerConfig {
@@ -36,6 +36,7 @@
36
36
  #include "JFunc_void_double_double.hpp"
37
37
  #include "JFunc_void_double_double_std__optional_bool_.hpp"
38
38
  #include "JFunc_void_TimedMetadata.hpp"
39
+ #include "JFunc_void_TrackItem.hpp"
39
40
  #include "JFunc_void_std__vector_TrackItem__double.hpp"
40
41
  #include "JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp"
41
42
  #include <NitroModules/DefaultConstructableObject.hpp>
@@ -131,6 +132,7 @@ void registerAllNatives() {
131
132
  margelo::nitro::nitroplayer::JFunc_void_double_double_cxx::registerNatives();
132
133
  margelo::nitro::nitroplayer::JFunc_void_double_double_std__optional_bool__cxx::registerNatives();
133
134
  margelo::nitro::nitroplayer::JFunc_void_TimedMetadata_cxx::registerNatives();
135
+ margelo::nitro::nitroplayer::JFunc_void_TrackItem_cxx::registerNatives();
134
136
  margelo::nitro::nitroplayer::JFunc_void_std__vector_TrackItem__double_cxx::registerNatives();
135
137
  margelo::nitro::nitroplayer::JFunc_void_std__vector_TrackItem__std__vector_TrackItem__cxx::registerNatives();
136
138
 
@@ -0,0 +1,85 @@
1
+ ///
2
+ /// JFunc_void_TrackItem.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 "TrackItem.hpp"
14
+ #include <functional>
15
+ #include <NitroModules/JNICallable.hpp>
16
+ #include "JTrackItem.hpp"
17
+ #include <string>
18
+ #include <NitroModules/Null.hpp>
19
+ #include <variant>
20
+ #include <optional>
21
+ #include "JVariant_NullType_String.hpp"
22
+ #include <NitroModules/JNull.hpp>
23
+ #include <NitroModules/AnyMap.hpp>
24
+ #include <NitroModules/JAnyMap.hpp>
25
+
26
+ namespace margelo::nitro::nitroplayer {
27
+
28
+ using namespace facebook;
29
+
30
+ /**
31
+ * Represents the Java/Kotlin callback `(track: TrackItem) -> Unit`.
32
+ * This can be passed around between C++ and Java/Kotlin.
33
+ */
34
+ struct JFunc_void_TrackItem: public jni::JavaClass<JFunc_void_TrackItem> {
35
+ public:
36
+ static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/nitroplayer/Func_void_TrackItem;";
37
+
38
+ public:
39
+ /**
40
+ * Invokes the function this `JFunc_void_TrackItem` instance holds through JNI.
41
+ */
42
+ void invoke(const TrackItem& track) const {
43
+ static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<JTrackItem> /* track */)>("invoke");
44
+ method(self(), JTrackItem::fromCpp(track));
45
+ }
46
+ };
47
+
48
+ /**
49
+ * An implementation of Func_void_TrackItem that is backed by a C++ implementation (using `std::function<...>`)
50
+ */
51
+ class JFunc_void_TrackItem_cxx final: public jni::HybridClass<JFunc_void_TrackItem_cxx, JFunc_void_TrackItem> {
52
+ public:
53
+ static jni::local_ref<JFunc_void_TrackItem::javaobject> fromCpp(const std::function<void(const TrackItem& /* track */)>& func) {
54
+ return JFunc_void_TrackItem_cxx::newObjectCxxArgs(func);
55
+ }
56
+
57
+ public:
58
+ /**
59
+ * Invokes the C++ `std::function<...>` this `JFunc_void_TrackItem_cxx` instance holds.
60
+ */
61
+ void invoke_cxx(jni::alias_ref<JTrackItem> track) {
62
+ _func(track->toCpp());
63
+ }
64
+
65
+ public:
66
+ [[nodiscard]]
67
+ inline const std::function<void(const TrackItem& /* track */)>& getFunction() const {
68
+ return _func;
69
+ }
70
+
71
+ public:
72
+ static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/nitroplayer/Func_void_TrackItem_cxx;";
73
+ static void registerNatives() {
74
+ registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_TrackItem_cxx::invoke_cxx)});
75
+ }
76
+
77
+ private:
78
+ explicit JFunc_void_TrackItem_cxx(const std::function<void(const TrackItem& /* track */)>& func): _func(func) { }
79
+
80
+ private:
81
+ friend HybridBase;
82
+ std::function<void(const TrackItem& /* track */)> _func;
83
+ };
84
+
85
+ } // namespace margelo::nitro::nitroplayer
@@ -60,6 +60,7 @@ namespace margelo::nitro::nitroplayer { struct TimedMetadata; }
60
60
  #include "TimedMetadata.hpp"
61
61
  #include "JFunc_void_TimedMetadata.hpp"
62
62
  #include "JTimedMetadata.hpp"
63
+ #include "JFunc_void_TrackItem.hpp"
63
64
  #include "JFunc_void_bool.hpp"
64
65
  #include "JFunc_void_std__vector_TrackItem__double.hpp"
65
66
  #include "JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp"
@@ -329,6 +330,10 @@ namespace margelo::nitro::nitroplayer {
329
330
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_TimedMetadata::javaobject> /* callback */)>("onTimedMetadata_cxx");
330
331
  method(_javaPart, JFunc_void_TimedMetadata_cxx::fromCpp(callback));
331
332
  }
333
+ void JHybridTrackPlayerSpec::appStartedWithNotification(const std::function<void(const TrackItem& /* track */)>& callback) {
334
+ static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_TrackItem::javaobject> /* callback */)>("appStartedWithNotification_cxx");
335
+ method(_javaPart, JFunc_void_TrackItem_cxx::fromCpp(callback));
336
+ }
332
337
  void JHybridTrackPlayerSpec::onAndroidAutoConnectionChange(const std::function<void(bool /* connected */)>& callback) {
333
338
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_bool::javaobject> /* callback */)>("onAndroidAutoConnectionChange_cxx");
334
339
  method(_javaPart, JFunc_void_bool_cxx::fromCpp(callback));
@@ -73,6 +73,7 @@ namespace margelo::nitro::nitroplayer {
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
75
  void onTimedMetadata(const std::function<void(const TimedMetadata& /* metadata */)>& callback) override;
76
+ void appStartedWithNotification(const std::function<void(const TrackItem& /* track */)>& callback) override;
76
77
  void onAndroidAutoConnectionChange(const std::function<void(bool /* connected */)>& callback) override;
77
78
  bool isAndroidAutoConnected() override;
78
79
  std::shared_ptr<Promise<void>> setVolume(double volume) override;
@@ -38,12 +38,15 @@ namespace margelo::nitro::nitroplayer {
38
38
  jni::local_ref<jni::JString> artist = this->getFieldValue(fieldArtist);
39
39
  static const auto fieldAlbum = clazz->getField<jni::JString>("album");
40
40
  jni::local_ref<jni::JString> album = this->getFieldValue(fieldAlbum);
41
+ static const auto fieldUrl = clazz->getField<jni::JString>("url");
42
+ jni::local_ref<jni::JString> url = this->getFieldValue(fieldUrl);
41
43
  static const auto fieldArtworkUrl = clazz->getField<jni::JString>("artworkUrl");
42
44
  jni::local_ref<jni::JString> artworkUrl = this->getFieldValue(fieldArtworkUrl);
43
45
  return TimedMetadata(
44
46
  title != nullptr ? std::make_optional(title->toStdString()) : std::nullopt,
45
47
  artist != nullptr ? std::make_optional(artist->toStdString()) : std::nullopt,
46
48
  album != nullptr ? std::make_optional(album->toStdString()) : std::nullopt,
49
+ url != nullptr ? std::make_optional(url->toStdString()) : std::nullopt,
47
50
  artworkUrl != nullptr ? std::make_optional(artworkUrl->toStdString()) : std::nullopt
48
51
  );
49
52
  }
@@ -54,7 +57,7 @@ namespace margelo::nitro::nitroplayer {
54
57
  */
55
58
  [[maybe_unused]]
56
59
  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>);
60
+ using JSignature = JTimedMetadata(jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>);
58
61
  static const auto clazz = javaClassStatic();
59
62
  static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
60
63
  return create(
@@ -62,6 +65,7 @@ namespace margelo::nitro::nitroplayer {
62
65
  value.title.has_value() ? jni::make_jstring(value.title.value()) : nullptr,
63
66
  value.artist.has_value() ? jni::make_jstring(value.artist.value()) : nullptr,
64
67
  value.album.has_value() ? jni::make_jstring(value.album.value()) : nullptr,
68
+ value.url.has_value() ? jni::make_jstring(value.url.value()) : nullptr,
65
69
  value.artworkUrl.has_value() ? jni::make_jstring(value.artworkUrl.value()) : nullptr
66
70
  );
67
71
  }
@@ -0,0 +1,80 @@
1
+ ///
2
+ /// Func_void_TrackItem.kt
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
+ package com.margelo.nitro.nitroplayer
9
+
10
+ import androidx.annotation.Keep
11
+ import com.facebook.jni.HybridData
12
+ import com.facebook.proguard.annotations.DoNotStrip
13
+ import dalvik.annotation.optimization.FastNative
14
+
15
+
16
+ /**
17
+ * Represents the JavaScript callback `(track: struct) => void`.
18
+ * This can be either implemented in C++ (in which case it might be a callback coming from JS),
19
+ * or in Kotlin/Java (in which case it is a native callback).
20
+ */
21
+ @DoNotStrip
22
+ @Keep
23
+ @Suppress("ClassName", "RedundantUnitReturnType")
24
+ fun interface Func_void_TrackItem: (TrackItem) -> Unit {
25
+ /**
26
+ * Call the given JS callback.
27
+ * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted.
28
+ */
29
+ @DoNotStrip
30
+ @Keep
31
+ override fun invoke(track: TrackItem): Unit
32
+ }
33
+
34
+ /**
35
+ * Represents the JavaScript callback `(track: struct) => void`.
36
+ * This is implemented in C++, via a `std::function<...>`.
37
+ * The callback might be coming from JS.
38
+ */
39
+ @DoNotStrip
40
+ @Keep
41
+ @Suppress(
42
+ "KotlinJniMissingFunction", "unused",
43
+ "RedundantSuppression", "RedundantUnitReturnType", "FunctionName",
44
+ "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName",
45
+ )
46
+ class Func_void_TrackItem_cxx: Func_void_TrackItem {
47
+ @DoNotStrip
48
+ @Keep
49
+ private val mHybridData: HybridData
50
+
51
+ @DoNotStrip
52
+ @Keep
53
+ private constructor(hybridData: HybridData) {
54
+ mHybridData = hybridData
55
+ }
56
+
57
+ @DoNotStrip
58
+ @Keep
59
+ override fun invoke(track: TrackItem): Unit
60
+ = invoke_cxx(track)
61
+
62
+ @FastNative
63
+ private external fun invoke_cxx(track: TrackItem): Unit
64
+ }
65
+
66
+ /**
67
+ * Represents the JavaScript callback `(track: struct) => void`.
68
+ * This is implemented in Java/Kotlin, via a `(TrackItem) -> Unit`.
69
+ * The callback is always coming from native.
70
+ */
71
+ @DoNotStrip
72
+ @Keep
73
+ @Suppress("ClassName", "RedundantUnitReturnType", "unused")
74
+ class Func_void_TrackItem_java(private val function: (TrackItem) -> Unit): Func_void_TrackItem {
75
+ @DoNotStrip
76
+ @Keep
77
+ override fun invoke(track: TrackItem): Unit {
78
+ return this.function(track)
79
+ }
80
+ }
@@ -130,6 +130,15 @@ abstract class HybridTrackPlayerSpec: HybridObject() {
130
130
  return __result
131
131
  }
132
132
 
133
+ abstract fun appStartedWithNotification(callback: (track: TrackItem) -> Unit): Unit
134
+
135
+ @DoNotStrip
136
+ @Keep
137
+ private fun appStartedWithNotification_cxx(callback: Func_void_TrackItem): Unit {
138
+ val __result = appStartedWithNotification(callback)
139
+ return __result
140
+ }
141
+
133
142
  abstract fun onAndroidAutoConnectionChange(callback: (connected: Boolean) -> Unit): Unit
134
143
 
135
144
  @DoNotStrip
@@ -28,6 +28,9 @@ data class TimedMetadata(
28
28
  val album: String?,
29
29
  @DoNotStrip
30
30
  @Keep
31
+ val url: String?,
32
+ @DoNotStrip
33
+ @Keep
31
34
  val artworkUrl: String?
32
35
  ) {
33
36
  /* primary constructor */
@@ -40,8 +43,8 @@ data class TimedMetadata(
40
43
  @Keep
41
44
  @Suppress("unused")
42
45
  @JvmStatic
43
- private fun fromCpp(title: String?, artist: String?, album: String?, artworkUrl: String?): TimedMetadata {
44
- return TimedMetadata(title, artist, album, artworkUrl)
46
+ private fun fromCpp(title: String?, artist: String?, album: String?, url: String?, artworkUrl: String?): TimedMetadata {
47
+ return TimedMetadata(title, artist, album, url, artworkUrl)
45
48
  }
46
49
  }
47
50
  }
@@ -323,6 +323,14 @@ namespace margelo::nitro::nitroplayer::bridge::swift {
323
323
  };
324
324
  }
325
325
 
326
+ // pragma MARK: std::function<void(const TrackItem& /* track */)>
327
+ Func_void_TrackItem create_Func_void_TrackItem(void* NON_NULL swiftClosureWrapper) noexcept {
328
+ auto swiftClosure = NitroPlayer::Func_void_TrackItem::fromUnsafe(swiftClosureWrapper);
329
+ return [swiftClosure = std::move(swiftClosure)](const TrackItem& track) mutable -> void {
330
+ swiftClosure.call(track);
331
+ };
332
+ }
333
+
326
334
  // pragma MARK: std::function<void(const std::vector<TrackItem>& /* tracks */, double /* lookahead */)>
327
335
  Func_void_std__vector_TrackItem__double create_Func_void_std__vector_TrackItem__double(void* NON_NULL swiftClosureWrapper) noexcept {
328
336
  auto swiftClosure = NitroPlayer::Func_void_std__vector_TrackItem__double::fromUnsafe(swiftClosureWrapper);
@@ -1752,6 +1752,28 @@ namespace margelo::nitro::nitroplayer::bridge::swift {
1752
1752
  return Func_void_TimedMetadata_Wrapper(std::move(value));
1753
1753
  }
1754
1754
 
1755
+ // pragma MARK: std::function<void(const TrackItem& /* track */)>
1756
+ /**
1757
+ * Specialized version of `std::function<void(const TrackItem&)>`.
1758
+ */
1759
+ using Func_void_TrackItem = std::function<void(const TrackItem& /* track */)>;
1760
+ /**
1761
+ * Wrapper class for a `std::function<void(const TrackItem& / * track * /)>`, this can be used from Swift.
1762
+ */
1763
+ class Func_void_TrackItem_Wrapper final {
1764
+ public:
1765
+ explicit Func_void_TrackItem_Wrapper(std::function<void(const TrackItem& /* track */)>&& func): _function(std::make_unique<std::function<void(const TrackItem& /* track */)>>(std::move(func))) {}
1766
+ inline void call(TrackItem track) const noexcept {
1767
+ _function->operator()(track);
1768
+ }
1769
+ private:
1770
+ std::unique_ptr<std::function<void(const TrackItem& /* track */)>> _function;
1771
+ } SWIFT_NONCOPYABLE;
1772
+ Func_void_TrackItem create_Func_void_TrackItem(void* NON_NULL swiftClosureWrapper) noexcept;
1773
+ inline Func_void_TrackItem_Wrapper wrap_Func_void_TrackItem(Func_void_TrackItem value) noexcept {
1774
+ return Func_void_TrackItem_Wrapper(std::move(value));
1775
+ }
1776
+
1755
1777
  // pragma MARK: std::function<void(const std::vector<TrackItem>& /* tracks */, double /* lookahead */)>
1756
1778
  /**
1757
1779
  * Specialized version of `std::function<void(const std::vector<TrackItem>&, double)>`.
@@ -238,6 +238,12 @@ namespace margelo::nitro::nitroplayer {
238
238
  std::rethrow_exception(__result.error());
239
239
  }
240
240
  }
241
+ inline void appStartedWithNotification(const std::function<void(const TrackItem& /* track */)>& callback) override {
242
+ auto __result = _swiftPart.appStartedWithNotification(callback);
243
+ if (__result.hasError()) [[unlikely]] {
244
+ std::rethrow_exception(__result.error());
245
+ }
246
+ }
241
247
  inline void onAndroidAutoConnectionChange(const std::function<void(bool /* connected */)>& callback) override {
242
248
  auto __result = _swiftPart.onAndroidAutoConnectionChange(callback);
243
249
  if (__result.hasError()) [[unlikely]] {
@@ -0,0 +1,46 @@
1
+ ///
2
+ /// Func_void_TrackItem.swift
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
+ import NitroModules
9
+
10
+ /**
11
+ * Wraps a Swift `(_ track: TrackItem) -> Void` as a class.
12
+ * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`.
13
+ */
14
+ public final class Func_void_TrackItem {
15
+ public typealias bridge = margelo.nitro.nitroplayer.bridge.swift
16
+
17
+ private let closure: (_ track: TrackItem) -> Void
18
+
19
+ public init(_ closure: @escaping (_ track: TrackItem) -> Void) {
20
+ self.closure = closure
21
+ }
22
+
23
+ @inline(__always)
24
+ public func call(track: TrackItem) -> Void {
25
+ self.closure(track)
26
+ }
27
+
28
+ /**
29
+ * Casts this instance to a retained unsafe raw pointer.
30
+ * This acquires one additional strong reference on the object!
31
+ */
32
+ @inline(__always)
33
+ public func toUnsafe() -> UnsafeMutableRawPointer {
34
+ return Unmanaged.passRetained(self).toOpaque()
35
+ }
36
+
37
+ /**
38
+ * Casts an unsafe pointer to a `Func_void_TrackItem`.
39
+ * The pointer has to be a retained opaque `Unmanaged<Func_void_TrackItem>`.
40
+ * This removes one strong reference from the object!
41
+ */
42
+ @inline(__always)
43
+ public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_TrackItem {
44
+ return Unmanaged<Func_void_TrackItem>.fromOpaque(pointer).takeRetainedValue()
45
+ }
46
+ }
@@ -32,6 +32,7 @@ public protocol HybridTrackPlayerSpec_protocol: HybridObject {
32
32
  func onSeek(callback: @escaping (_ position: Double, _ totalDuration: Double) -> Void) throws -> Void
33
33
  func onPlaybackProgressChange(callback: @escaping (_ position: Double, _ totalDuration: Double, _ isManuallySeeked: Bool?) -> Void) throws -> Void
34
34
  func onTimedMetadata(callback: @escaping (_ metadata: TimedMetadata) -> Void) throws -> Void
35
+ func appStartedWithNotification(callback: @escaping (_ track: TrackItem) -> Void) throws -> Void
35
36
  func onAndroidAutoConnectionChange(callback: @escaping (_ connected: Bool) -> Void) throws -> Void
36
37
  func isAndroidAutoConnected() throws -> Bool
37
38
  func setVolume(volume: Double) throws -> Promise<Void>
@@ -494,6 +494,22 @@ open class HybridTrackPlayerSpec_cxx {
494
494
  }
495
495
  }
496
496
 
497
+ @inline(__always)
498
+ public final func appStartedWithNotification(callback: bridge.Func_void_TrackItem) -> bridge.Result_void_ {
499
+ do {
500
+ try self.__implementation.appStartedWithNotification(callback: { () -> (TrackItem) -> Void in
501
+ let __wrappedFunction = bridge.wrap_Func_void_TrackItem(callback)
502
+ return { (__track: TrackItem) -> Void in
503
+ __wrappedFunction.call(__track)
504
+ }
505
+ }())
506
+ return bridge.create_Result_void_()
507
+ } catch (let __error) {
508
+ let __exceptionPtr = __error.toCpp()
509
+ return bridge.create_Result_void_(__exceptionPtr)
510
+ }
511
+ }
512
+
497
513
  @inline(__always)
498
514
  public final func onAndroidAutoConnectionChange(callback: bridge.Func_void_bool) -> bridge.Result_void_ {
499
515
  do {
@@ -18,7 +18,7 @@ public extension TimedMetadata {
18
18
  /**
19
19
  * Create a new instance of `TimedMetadata`.
20
20
  */
21
- init(title: String?, artist: String?, album: String?, artworkUrl: String?) {
21
+ init(title: String?, artist: String?, album: String?, url: String?, artworkUrl: String?) {
22
22
  self.init({ () -> bridge.std__optional_std__string_ in
23
23
  if let __unwrappedValue = title {
24
24
  return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
@@ -37,6 +37,12 @@ public extension TimedMetadata {
37
37
  } else {
38
38
  return .init()
39
39
  }
40
+ }(), { () -> bridge.std__optional_std__string_ in
41
+ if let __unwrappedValue = url {
42
+ return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
43
+ } else {
44
+ return .init()
45
+ }
40
46
  }(), { () -> bridge.std__optional_std__string_ in
41
47
  if let __unwrappedValue = artworkUrl {
42
48
  return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
@@ -82,6 +88,18 @@ public extension TimedMetadata {
82
88
  }()
83
89
  }
84
90
 
91
+ @inline(__always)
92
+ var url: String? {
93
+ return { () -> String? in
94
+ if bridge.has_value_std__optional_std__string_(self.__url) {
95
+ let __unwrapped = bridge.get_std__optional_std__string_(self.__url)
96
+ return String(__unwrapped)
97
+ } else {
98
+ return nil
99
+ }
100
+ }()
101
+ }
102
+
85
103
  @inline(__always)
86
104
  var artworkUrl: String? {
87
105
  return { () -> String? in
@@ -33,6 +33,7 @@ namespace margelo::nitro::nitroplayer {
33
33
  prototype.registerHybridMethod("onSeek", &HybridTrackPlayerSpec::onSeek);
34
34
  prototype.registerHybridMethod("onPlaybackProgressChange", &HybridTrackPlayerSpec::onPlaybackProgressChange);
35
35
  prototype.registerHybridMethod("onTimedMetadata", &HybridTrackPlayerSpec::onTimedMetadata);
36
+ prototype.registerHybridMethod("appStartedWithNotification", &HybridTrackPlayerSpec::appStartedWithNotification);
36
37
  prototype.registerHybridMethod("onAndroidAutoConnectionChange", &HybridTrackPlayerSpec::onAndroidAutoConnectionChange);
37
38
  prototype.registerHybridMethod("isAndroidAutoConnected", &HybridTrackPlayerSpec::isAndroidAutoConnected);
38
39
  prototype.registerHybridMethod("setVolume", &HybridTrackPlayerSpec::setVolume);
@@ -91,6 +91,7 @@ namespace margelo::nitro::nitroplayer {
91
91
  virtual void onSeek(const std::function<void(double /* position */, double /* totalDuration */)>& callback) = 0;
92
92
  virtual void onPlaybackProgressChange(const std::function<void(double /* position */, double /* totalDuration */, std::optional<bool> /* isManuallySeeked */)>& callback) = 0;
93
93
  virtual void onTimedMetadata(const std::function<void(const TimedMetadata& /* metadata */)>& callback) = 0;
94
+ virtual void appStartedWithNotification(const std::function<void(const TrackItem& /* track */)>& callback) = 0;
94
95
  virtual void onAndroidAutoConnectionChange(const std::function<void(bool /* connected */)>& callback) = 0;
95
96
  virtual bool isAndroidAutoConnected() = 0;
96
97
  virtual std::shared_ptr<Promise<void>> setVolume(double volume) = 0;
@@ -43,11 +43,12 @@ namespace margelo::nitro::nitroplayer {
43
43
  std::optional<std::string> title SWIFT_PRIVATE;
44
44
  std::optional<std::string> artist SWIFT_PRIVATE;
45
45
  std::optional<std::string> album SWIFT_PRIVATE;
46
+ std::optional<std::string> url SWIFT_PRIVATE;
46
47
  std::optional<std::string> artworkUrl SWIFT_PRIVATE;
47
48
 
48
49
  public:
49
50
  TimedMetadata() = default;
50
- explicit TimedMetadata(std::optional<std::string> title, std::optional<std::string> artist, std::optional<std::string> album, std::optional<std::string> artworkUrl): title(title), artist(artist), album(album), artworkUrl(artworkUrl) {}
51
+ explicit TimedMetadata(std::optional<std::string> title, std::optional<std::string> artist, std::optional<std::string> album, std::optional<std::string> url, std::optional<std::string> artworkUrl): title(title), artist(artist), album(album), url(url), artworkUrl(artworkUrl) {}
51
52
 
52
53
  public:
53
54
  friend bool operator==(const TimedMetadata& lhs, const TimedMetadata& rhs) = default;
@@ -66,6 +67,7 @@ namespace margelo::nitro {
66
67
  JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "title"))),
67
68
  JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artist"))),
68
69
  JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "album"))),
70
+ JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "url"))),
69
71
  JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artworkUrl")))
70
72
  );
71
73
  }
@@ -74,6 +76,7 @@ namespace margelo::nitro {
74
76
  obj.setProperty(runtime, PropNameIDCache::get(runtime, "title"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.title));
75
77
  obj.setProperty(runtime, PropNameIDCache::get(runtime, "artist"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.artist));
76
78
  obj.setProperty(runtime, PropNameIDCache::get(runtime, "album"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.album));
79
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "url"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.url));
77
80
  obj.setProperty(runtime, PropNameIDCache::get(runtime, "artworkUrl"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.artworkUrl));
78
81
  return obj;
79
82
  }
@@ -88,6 +91,7 @@ namespace margelo::nitro {
88
91
  if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "title")))) return false;
89
92
  if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artist")))) return false;
90
93
  if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "album")))) return false;
94
+ if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "url")))) return false;
91
95
  if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artworkUrl")))) return false;
92
96
  return true;
93
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "1.6.0-alpha.0",
3
+ "version": "1.6.0",
4
4
  "description": "A powerful audio player library for React Native with playlist management, playback controls, and support for Android Auto and CarPlay",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",
@@ -102,6 +102,11 @@ export interface TrackPlayer extends HybridObject<{
102
102
  * updates and ID3 frames emitted while a live stream plays.
103
103
  */
104
104
  onTimedMetadata(callback: (metadata: TimedMetadata) => void): void
105
+ /**
106
+ * Fires when the app was opened by tapping the playback notification,
107
+ * with the track that was playing at tap time. Android only — no-op on iOS.
108
+ */
109
+ appStartedWithNotification(callback: (track: TrackItem) => void): void
105
110
  onAndroidAutoConnectionChange(callback: (connected: boolean) => void): void
106
111
  isAndroidAutoConnected(): boolean
107
112
  setVolume(volume: number): Promise<void>
@@ -43,8 +43,16 @@ export interface PlayerState {
43
43
  /** In-stream metadata emitted while a stream plays (ICY/Shoutcast StreamTitle, ID3 frames) */
44
44
  export interface TimedMetadata {
45
45
  title?: string
46
+ /** Split out of `title` when the stream sends only `"Artist - Title"` */
46
47
  artist?: string
47
48
  album?: string
49
+ /**
50
+ * ICY `StreamUrl` / ID3 webpage frame. Radio providers commonly put the
51
+ * current track's artwork URL here (Live365, StreamGuys), but some send a
52
+ * station or artist page instead — check the value before treating it as art.
53
+ */
54
+ url?: string
55
+ /** Artwork URL the stream tagged as artwork, when it sends one explicitly */
48
56
  artworkUrl?: string
49
57
  }
50
58