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.
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +17 -5
- package/ios/core/TrackPlayerListener.swift +30 -5
- package/lib/types/PlayerQueue.d.ts +8 -0
- package/nitrogen/generated/android/c++/JTimedMetadata.hpp +5 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TimedMetadata.kt +5 -2
- package/nitrogen/generated/ios/swift/TimedMetadata.swift +19 -1
- package/nitrogen/generated/shared/c++/TimedMetadata.hpp +5 -1
- package/package.json +1 -1
- package/src/types/PlayerQueue.ts +8 -0
|
@@ -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)
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
531
|
-
|
|
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.
|
|
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",
|
package/src/types/PlayerQueue.ts
CHANGED
|
@@ -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
|
|