react-native-nitro-player 1.6.0-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.
@@ -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(
@@ -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) {
@@ -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 {
@@ -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
  }
@@ -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
  }
@@ -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
@@ -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-alpha.1",
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",
@@ -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