react-native-nitro-player 1.5.0-alpha.0 → 1.5.0-alpha.3

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.
@@ -20,11 +20,7 @@ extension TrackPlayerCore {
20
20
 
21
21
  func skipToIndex(index: Int) async -> Bool {
22
22
  if isCasting {
23
- return await withPlayerQueueNoThrow {
24
- guard index >= 0 && index < self.getActualQueueInternal().count else { return false }
25
- self.castManager?.jump(toQueueIndex: index)
26
- return true
27
- }
23
+ return await withPlayerQueueNoThrow { self.skipToIndexCastInternal(index: index) }
28
24
  }
29
25
  return await withPlayerQueueNoThrow { self.skipToIndexInternal(index: index) }
30
26
  }
@@ -40,6 +36,29 @@ extension TrackPlayerCore {
40
36
  // MARK: - Internal (run on playerQueue)
41
37
 
42
38
  func getStateInternal() -> PlayerState {
39
+ // While casting, report the receiver's cached state — the local AVQueuePlayer is paused/stale.
40
+ if isCasting, let cast = castManager {
41
+ let currentTrack = getCurrentTrack()
42
+ let playingType: CurrentPlayingType
43
+ if currentTrack == nil { playingType = .notPlaying }
44
+ else {
45
+ switch currentTemporaryType {
46
+ case .none: playingType = .playlist
47
+ case .playNext: playingType = .playNext
48
+ case .upNext: playingType = .upNext
49
+ }
50
+ }
51
+ return PlayerState(
52
+ currentTrack: currentTrack.map { Variant_NullType_TrackItem.second($0) },
53
+ currentPosition: cast.lastKnownRemotePosition,
54
+ totalDuration: cast.lastKnownRemoteDuration,
55
+ currentState: cast.cachedPlaybackState,
56
+ currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
57
+ currentIndex: currentTrackIndex >= 0 ? Double(currentTrackIndex) : -1.0,
58
+ currentPlayingType: playingType
59
+ )
60
+ }
61
+
43
62
  guard let player else {
44
63
  return PlayerState(
45
64
  currentTrack: nil, currentPosition: 0.0, totalDuration: 0.0,
@@ -96,7 +115,7 @@ extension TrackPlayerCore {
96
115
  if let current = getCurrentTrack() { queue.append(current) }
97
116
 
98
117
  // Add playNext stack — skip the currently playing track by ID (already added as current)
99
- let currentId = player?.currentItem?.trackId
118
+ let currentId = activeCurrentTrackId
100
119
  if currentTemporaryType == .playNext, let currentId = currentId {
101
120
  var skipped = false
102
121
  for track in playNextStack {
@@ -126,10 +145,7 @@ extension TrackPlayerCore {
126
145
  }
127
146
 
128
147
  func getCurrentTrack() -> TrackItem? {
129
- if currentTemporaryType != .none,
130
- let currentItem = player?.currentItem,
131
- let trackId = currentItem.trackId
132
- {
148
+ if currentTemporaryType != .none, let trackId = activeCurrentTrackId {
133
149
  if currentTemporaryType == .playNext { return playNextStack.first(where: { $0.id == trackId }) }
134
150
  if currentTemporaryType == .upNext { return upNextQueue.first(where: { $0.id == trackId }) }
135
151
  }
@@ -222,7 +238,7 @@ extension TrackPlayerCore {
222
238
  }
223
239
 
224
240
  func determineCurrentTemporaryType() -> TemporaryType {
225
- guard let trackId = player?.currentItem?.trackId else { return .none }
241
+ guard let trackId = activeCurrentTrackId else { return .none }
226
242
  if playNextStack.contains(where: { $0.id == trackId }) { return .playNext }
227
243
  if upNextQueue.contains(where: { $0.id == trackId }) { return .upNext }
228
244
  return .none
@@ -240,6 +240,12 @@ extension TrackPlayerCore {
240
240
  /// - Parameter changedTrackIds: When non-nil, performs a surgical update:
241
241
  /// only AVPlayerItems whose track ID is in this set are removed and re-created.
242
242
  func rebuildAVQueueFromCurrentPosition(changedTrackIds: Set<String>? = nil) {
243
+ // While casting, queue changes are applied to the receiver — the local AVQueuePlayer is dormant.
244
+ if isCasting {
245
+ syncCastQueueAfterCurrent()
246
+ return
247
+ }
248
+
243
249
  guard let player = self.player else { return }
244
250
 
245
251
  let currentItem = player.currentItem
@@ -78,8 +78,8 @@ extension TrackPlayerCore {
78
78
  // Update in PlaylistManager
79
79
  let affectedPlaylists = self.playlistManager.updateTracks(tracks: safeTracks)
80
80
 
81
- // If the current track had no URL and now has one, replace the current AVPlayerItem
82
- if let update = currentTrack, currentTrackIsEmpty, !update.url.isEmpty {
81
+ // Replace the current AVPlayerItem when its URL just resolved (local only — cast reloads below instead).
82
+ if !self.isCasting, let update = currentTrack, currentTrackIsEmpty, !update.url.isEmpty {
83
83
  NitroPlayerLogger.log("TrackPlayerCore",
84
84
  "🔄 Replacing current AVPlayerItem for track with resolved URL: \(update.id)")
85
85
  if let newItem = self.createGaplessPlayerItem(for: update, isPreload: false) {
@@ -101,6 +101,30 @@ extension TrackPlayerCore {
101
101
  "📥 Synced currentTracks from PlaylistManager (\(self.currentTracks.count) tracks)")
102
102
  }
103
103
 
104
+ if self.isCasting {
105
+ // Resolved URLs are applied by (re)loading/extending the remote queue — never by touching the silent local player.
106
+ let currentResolvedNow = currentTrackIsEmpty
107
+ && currentTrackId.map { id in updatedTrackIds.contains(id) } ?? false
108
+ let staleOnReceiver = !updatedTrackIds
109
+ .isDisjoint(with: Set(self.castManager?.loadedTrackIds ?? []))
110
+
111
+ if currentResolvedNow || !(self.castManager?.hasLoadedMedia ?? false) {
112
+ // Parked track just became castable (or nothing loaded) — atomic load from current.
113
+ self.loadCastQueue(autoplay: self.intendedToPlay, position: 0)
114
+ } else if staleOnReceiver {
115
+ // URLs changed for items already on the receiver — reload at position before stale ones fail.
116
+ self.loadCastQueue(
117
+ autoplay: self.intendedToPlay,
118
+ position: self.castManager?.lastKnownRemotePosition ?? 0
119
+ )
120
+ } else {
121
+ // Newly castable tracks extend the receiver queue without interrupting playback.
122
+ self.syncCastQueueAfterCurrent()
123
+ }
124
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Cast queue synced after track updates")
125
+ return
126
+ }
127
+
104
128
  if self.player?.currentItem == nil, let player = self.player {
105
129
  // No AVPlayerItem exists yet — lazy-load mode: URLs were empty when the queue first loaded.
106
130
  NitroPlayerLogger.log("TrackPlayerCore",
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Pressable, StyleSheet, View, } from 'react-native';
2
+ import { Pressable, View, } from 'react-native';
3
3
  import { Cast } from '../index';
4
4
  import { useCastState } from '../hooks/useCastState';
5
5
  /**
@@ -40,61 +40,86 @@ export function CastButton({ size = 24, color = '#000000', activeColor = '#1E88E
40
40
  }
41
41
  /**
42
42
  * Dependency-free rendition of the standard Cast glyph: a rounded "screen"
43
- * rectangle with a broadcast signal (dot + two arcs) in the bottom-left corner.
44
- * Drawn purely with `View`s so the library stays dependency-free.
43
+ * rectangle (with its bottom-left corner open) and a broadcast signal — a dot
44
+ * and two concentric arcs — radiating from the bottom-left. Drawn purely with
45
+ * `View`s so the library stays dependency-free.
45
46
  */
46
47
  function CastGlyph({ size, color, }) {
47
- const stroke = Math.max(1.5, Math.round(size * 0.083));
48
- const inset = stroke;
49
- const dotSize = stroke * 1.7;
50
- const r1 = size * 0.28; // inner arc radius
51
- const r2 = size * 0.46; // outer arc radius
48
+ const stroke = Math.max(1.5, Math.round(size / 13));
49
+ const inset = Math.round(size * 0.14);
50
+ const dotSize = stroke * 1.5;
51
+ // Origin of the broadcast signal — the screen's bottom-left corner.
52
+ const cx = inset + stroke / 2;
53
+ const cy = size - inset - stroke / 2;
54
+ // Leave the screen's bottom-left corner open so the signal reads clearly.
55
+ const gap = size * 0.46;
52
56
  return (React.createElement(View, { style: { width: size, height: size } },
53
- React.createElement(View, { style: [
54
- StyleSheet.absoluteFill,
55
- {
56
- borderWidth: stroke,
57
- borderColor: color,
58
- borderRadius: Math.max(2, size * 0.12),
59
- },
60
- ] }),
61
57
  React.createElement(View, { style: {
62
58
  position: 'absolute',
63
59
  left: inset,
60
+ top: inset,
61
+ right: inset,
62
+ height: stroke,
63
+ backgroundColor: color,
64
+ borderRadius: stroke,
65
+ } }),
66
+ React.createElement(View, { style: {
67
+ position: 'absolute',
68
+ top: inset,
69
+ bottom: inset,
70
+ right: inset,
71
+ width: stroke,
72
+ backgroundColor: color,
73
+ borderRadius: stroke,
74
+ } }),
75
+ React.createElement(View, { style: {
76
+ position: 'absolute',
77
+ left: inset + gap,
64
78
  bottom: inset,
79
+ right: inset,
80
+ height: stroke,
81
+ backgroundColor: color,
82
+ borderRadius: stroke,
83
+ } }),
84
+ React.createElement(View, { style: {
85
+ position: 'absolute',
86
+ left: inset,
87
+ top: inset,
88
+ height: size - 2 * inset - gap,
89
+ width: stroke,
90
+ backgroundColor: color,
91
+ borderRadius: stroke,
92
+ } }),
93
+ React.createElement(RippleArc, { cx: cx, cy: cy, radius: size * 0.2, stroke: stroke, color: color }),
94
+ React.createElement(RippleArc, { cx: cx, cy: cy, radius: size * 0.34, stroke: stroke, color: color }),
95
+ React.createElement(View, { style: {
96
+ position: 'absolute',
97
+ left: cx - dotSize / 2,
98
+ top: cy - dotSize / 2,
65
99
  width: dotSize,
66
100
  height: dotSize,
67
101
  borderRadius: dotSize / 2,
68
102
  backgroundColor: color,
69
- } }),
70
- React.createElement(Arc, { origin: inset + dotSize / 2, radius: r1, stroke: stroke, color: color }),
71
- React.createElement(Arc, { origin: inset + dotSize / 2, radius: r2, stroke: stroke, color: color })));
103
+ } })));
72
104
  }
73
105
  /**
74
- * A quarter-circle arc (the top-right quadrant) centered at the bottom-left
75
- * `origin` point. Produced by clipping a full bordered circle to one quadrant.
106
+ * A quarter-circle arc (the up-right quadrant) centered at (cx, cy). Rendered as
107
+ * a circle with only its top border colored, rotated 45° so the visible cap
108
+ * faces up-and-to-the-right — i.e. radiating from the bottom-left dot.
76
109
  */
77
- function Arc({ origin, radius, stroke, color, }) {
78
- return (React.createElement(View
79
- // Clip box reveals only the quadrant facing up-and-right from the origin.
80
- , {
81
- // Clip box reveals only the quadrant facing up-and-right from the origin.
82
- style: {
110
+ function RippleArc({ cx, cy, radius, stroke, color, }) {
111
+ return (React.createElement(View, { style: {
83
112
  position: 'absolute',
84
- left: origin,
85
- bottom: origin - radius,
86
- width: radius,
87
- height: radius,
88
- overflow: 'hidden',
89
- } },
90
- React.createElement(View, { style: {
91
- position: 'absolute',
92
- left: -radius,
93
- top: 0,
94
- width: radius * 2,
95
- height: radius * 2,
96
- borderRadius: radius,
97
- borderWidth: stroke,
98
- borderColor: color,
99
- } })));
113
+ left: cx - radius,
114
+ top: cy - radius,
115
+ width: radius * 2,
116
+ height: radius * 2,
117
+ borderRadius: radius,
118
+ borderWidth: stroke,
119
+ borderTopColor: color,
120
+ borderRightColor: 'transparent',
121
+ borderBottomColor: 'transparent',
122
+ borderLeftColor: 'transparent',
123
+ transform: [{ rotate: '45deg' }],
124
+ } }));
100
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "1.5.0-alpha.0",
3
+ "version": "1.5.0-alpha.3",
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",
@@ -1,7 +1,6 @@
1
1
  import React from 'react'
2
2
  import {
3
3
  Pressable,
4
- StyleSheet,
5
4
  View,
6
5
  type PressableProps,
7
6
  type StyleProp,
@@ -104,8 +103,9 @@ export function CastButton({
104
103
 
105
104
  /**
106
105
  * Dependency-free rendition of the standard Cast glyph: a rounded "screen"
107
- * rectangle with a broadcast signal (dot + two arcs) in the bottom-left corner.
108
- * Drawn purely with `View`s so the library stays dependency-free.
106
+ * rectangle (with its bottom-left corner open) and a broadcast signal — a dot
107
+ * and two concentric arcs — radiating from the bottom-left. Drawn purely with
108
+ * `View`s so the library stays dependency-free.
109
109
  */
110
110
  function CastGlyph({
111
111
  size,
@@ -114,83 +114,120 @@ function CastGlyph({
114
114
  size: number
115
115
  color: string
116
116
  }): React.ReactElement {
117
- const stroke = Math.max(1.5, Math.round(size * 0.083))
118
- const inset = stroke
119
- const dotSize = stroke * 1.7
120
- const r1 = size * 0.28 // inner arc radius
121
- const r2 = size * 0.46 // outer arc radius
117
+ const stroke = Math.max(1.5, Math.round(size / 13))
118
+ const inset = Math.round(size * 0.14)
119
+ const dotSize = stroke * 1.5
120
+ // Origin of the broadcast signal — the screen's bottom-left corner.
121
+ const cx = inset + stroke / 2
122
+ const cy = size - inset - stroke / 2
123
+ // Leave the screen's bottom-left corner open so the signal reads clearly.
124
+ const gap = size * 0.46
122
125
 
123
126
  return (
124
127
  <View style={{ width: size, height: size }}>
125
- {/* Screen outline */}
128
+ {/* Screen: top + right + most of bottom + most of left, leaving the
129
+ bottom-left corner open (composed from edges for a clean cast look). */}
130
+ {/* Top edge */}
126
131
  <View
127
- style={[
128
- StyleSheet.absoluteFill,
129
- {
130
- borderWidth: stroke,
131
- borderColor: color,
132
- borderRadius: Math.max(2, size * 0.12),
133
- },
134
- ]}
132
+ style={{
133
+ position: 'absolute',
134
+ left: inset,
135
+ top: inset,
136
+ right: inset,
137
+ height: stroke,
138
+ backgroundColor: color,
139
+ borderRadius: stroke,
140
+ }}
135
141
  />
136
- {/* Broadcast dot */}
142
+ {/* Right edge */}
137
143
  <View
138
144
  style={{
139
145
  position: 'absolute',
140
- left: inset,
146
+ top: inset,
147
+ bottom: inset,
148
+ right: inset,
149
+ width: stroke,
150
+ backgroundColor: color,
151
+ borderRadius: stroke,
152
+ }}
153
+ />
154
+ {/* Bottom edge (stops short of the bottom-left corner) */}
155
+ <View
156
+ style={{
157
+ position: 'absolute',
158
+ left: inset + gap,
141
159
  bottom: inset,
160
+ right: inset,
161
+ height: stroke,
162
+ backgroundColor: color,
163
+ borderRadius: stroke,
164
+ }}
165
+ />
166
+ {/* Left edge (stops short of the bottom-left corner) */}
167
+ <View
168
+ style={{
169
+ position: 'absolute',
170
+ left: inset,
171
+ top: inset,
172
+ height: size - 2 * inset - gap,
173
+ width: stroke,
174
+ backgroundColor: color,
175
+ borderRadius: stroke,
176
+ }}
177
+ />
178
+ {/* Two concentric broadcast arcs radiating from the corner dot */}
179
+ <RippleArc cx={cx} cy={cy} radius={size * 0.2} stroke={stroke} color={color} />
180
+ <RippleArc cx={cx} cy={cy} radius={size * 0.34} stroke={stroke} color={color} />
181
+ {/* Broadcast dot */}
182
+ <View
183
+ style={{
184
+ position: 'absolute',
185
+ left: cx - dotSize / 2,
186
+ top: cy - dotSize / 2,
142
187
  width: dotSize,
143
188
  height: dotSize,
144
189
  borderRadius: dotSize / 2,
145
190
  backgroundColor: color,
146
191
  }}
147
192
  />
148
- {/* Two concentric quarter arcs radiating from the dot */}
149
- <Arc origin={inset + dotSize / 2} radius={r1} stroke={stroke} color={color} />
150
- <Arc origin={inset + dotSize / 2} radius={r2} stroke={stroke} color={color} />
151
193
  </View>
152
194
  )
153
195
  }
154
196
 
155
197
  /**
156
- * A quarter-circle arc (the top-right quadrant) centered at the bottom-left
157
- * `origin` point. Produced by clipping a full bordered circle to one quadrant.
198
+ * A quarter-circle arc (the up-right quadrant) centered at (cx, cy). Rendered as
199
+ * a circle with only its top border colored, rotated 45° so the visible cap
200
+ * faces up-and-to-the-right — i.e. radiating from the bottom-left dot.
158
201
  */
159
- function Arc({
160
- origin,
202
+ function RippleArc({
203
+ cx,
204
+ cy,
161
205
  radius,
162
206
  stroke,
163
207
  color,
164
208
  }: {
165
- origin: number
209
+ cx: number
210
+ cy: number
166
211
  radius: number
167
212
  stroke: number
168
213
  color: string
169
214
  }): React.ReactElement {
170
215
  return (
171
216
  <View
172
- // Clip box reveals only the quadrant facing up-and-right from the origin.
173
217
  style={{
174
218
  position: 'absolute',
175
- left: origin,
176
- bottom: origin - radius,
177
- width: radius,
178
- height: radius,
179
- overflow: 'hidden',
219
+ left: cx - radius,
220
+ top: cy - radius,
221
+ width: radius * 2,
222
+ height: radius * 2,
223
+ borderRadius: radius,
224
+ borderWidth: stroke,
225
+ borderTopColor: color,
226
+ borderRightColor: 'transparent',
227
+ borderBottomColor: 'transparent',
228
+ borderLeftColor: 'transparent',
229
+ transform: [{ rotate: '45deg' }],
180
230
  }}
181
- >
182
- <View
183
- style={{
184
- position: 'absolute',
185
- left: -radius,
186
- top: 0,
187
- width: radius * 2,
188
- height: radius * 2,
189
- borderRadius: radius,
190
- borderWidth: stroke,
191
- borderColor: color,
192
- }}
193
- />
194
- </View>
231
+ />
195
232
  )
196
233
  }