expo-gliph-player 1.3.13 → 1.3.15

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.
@@ -6,29 +6,12 @@ import UIKit
6
6
 
7
7
  public typealias EventEmitter = (String, [String: Any]?) -> Void
8
8
 
9
- // React Native's promise block types, redeclared locally so this file has
10
- // no dependency on a bridging header (`import <React/RCTBridgeModule.h>`).
11
- // Bridging headers are unsupported on framework targets in Xcode, and this
12
- // pod builds as a static_framework — so this file must never `import React`
13
- // or rely on one being injected. These signatures are structurally
14
- // identical to the real `RCTPromiseResolveBlock` / `RCTPromiseRejectBlock`
15
- // (id -> Any?, NSString* -> String?, NSError* -> Error?), so the resulting
16
- // @objc blocks are ABI-compatible with what GliphPlayerModule.mm expects.
9
+ // React Native's promise block types
17
10
  public typealias RCTPromiseResolveBlock = (Any?) -> Void
18
11
  public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
19
12
 
20
13
  // MARK: - GliphAudioPlayer
21
14
 
22
- /**
23
- * GliphAudioPlayer
24
- *
25
- * Core Swift class that manages:
26
- * - AVQueuePlayer for audio playback
27
- * - MPRemoteCommandCenter for lock screen / headphone controls
28
- * - MPNowPlayingInfoCenter for Now Playing metadata
29
- * - Audio session configuration
30
- * - Queue management
31
- */
32
15
  @objc public class GliphAudioPlayer: NSObject {
33
16
 
34
17
  // ── Properties ──────────────────────────────────────────────────────────────
@@ -37,7 +20,7 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
37
20
  private var playerItems: [AVPlayerItem] = []
38
21
  private var queue: [[String: Any]] = []
39
22
  private var currentIndex: Int = -1
40
- private var repeatMode: Int = 0 // 0=off, 1=track, 2=queue
23
+ private var repeatMode: Int = 0
41
24
  private var isSetup = false
42
25
  private var progressTimer: Timer?
43
26
  private var progressInterval: TimeInterval = 1.0
@@ -56,12 +39,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
56
39
 
57
40
  // ── Thread helper ───────────────────────────────────────────────────────────
58
41
 
59
- /// Executes `block` synchronously on the main thread.
60
- /// Safe to call from any thread — avoids deadlock if already on main.
61
- /// Several AVAudioSession / UIApplication / MPRemoteCommandCenter APIs are
62
- /// documented as main-thread-only; the RN bridge does not guarantee that
63
- /// native module methods run on main, so every touch of those APIs goes
64
- /// through this helper.
65
42
  private func runOnMain(_ block: () -> Void) {
66
43
  if Thread.isMainThread {
67
44
  block()
@@ -145,11 +122,19 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
145
122
  ? queue.count
146
123
  : insertBeforeIndex
147
124
 
125
+ let wasEmpty = queue.isEmpty
126
+
148
127
  for (i, track) in tracks.enumerated() {
149
128
  queue.insert(track, at: insertAt + i)
150
129
  }
151
130
 
152
131
  rebuildPlayerQueue()
132
+
133
+ if wasEmpty && !queue.isEmpty {
134
+ currentIndex = 0
135
+ emitActiveTrackChanged()
136
+ }
137
+
153
138
  resolve(insertAt)
154
139
  }
155
140
 
@@ -177,23 +162,34 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
177
162
  @objc public func skipToIndex(
178
163
  _ index: Int,
179
164
  initialPosition: Double,
165
+ autoPlay: Bool = true,
180
166
  resolve: @escaping RCTPromiseResolveBlock,
181
167
  reject: @escaping RCTPromiseRejectBlock
182
168
  ) {
183
169
  guard index >= 0 && index < queue.count else {
184
170
  reject("skip_error", "Index out of bounds", nil); return
185
171
  }
172
+
173
+ let lastIndex = currentIndex
186
174
  currentIndex = index
175
+
187
176
  rebuildPlayerQueue()
188
177
  if initialPosition >= 0 {
189
178
  player?.seek(to: CMTime(seconds: initialPosition, preferredTimescale: 1000))
190
179
  }
191
- player?.play()
180
+
181
+ if autoPlay {
182
+ player?.play()
183
+ }
184
+
185
+ emitActiveTrackChanged(lastIndex: lastIndex)
186
+
192
187
  resolve(nil)
193
188
  }
194
189
 
195
190
  @objc public func skipToNext(
196
191
  initialPosition: Double,
192
+ autoPlay: Bool = true,
197
193
  resolve: @escaping RCTPromiseResolveBlock,
198
194
  reject: @escaping RCTPromiseRejectBlock
199
195
  ) {
@@ -201,11 +197,12 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
201
197
  guard next < queue.count else {
202
198
  reject("skip_error", "No next track", nil); return
203
199
  }
204
- skipToIndex(next, initialPosition: initialPosition, resolve: resolve, reject: reject)
200
+ skipToIndex(next, initialPosition: initialPosition, autoPlay: autoPlay, resolve: resolve, reject: reject)
205
201
  }
206
202
 
207
203
  @objc public func skipToPrevious(
208
204
  initialPosition: Double,
205
+ autoPlay: Bool = true,
209
206
  resolve: @escaping RCTPromiseResolveBlock,
210
207
  reject: @escaping RCTPromiseRejectBlock
211
208
  ) {
@@ -213,7 +210,7 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
213
210
  guard prev >= 0 else {
214
211
  reject("skip_error", "No previous track", nil); return
215
212
  }
216
- skipToIndex(prev, initialPosition: initialPosition, resolve: resolve, reject: reject)
213
+ skipToIndex(prev, initialPosition: initialPosition, autoPlay: autoPlay, resolve: resolve, reject: reject)
217
214
  }
218
215
 
219
216
  @objc public func moveFrom(
@@ -237,11 +234,19 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
237
234
  @objc public func play(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
238
235
  player?.play()
239
236
  startProgressTimer()
237
+
238
+ eventEmitter("playback-state", ["state": "playing"])
239
+
240
+ if currentIndex >= 0 && currentIndex < queue.count {
241
+ emitActiveTrackChanged()
242
+ }
243
+
240
244
  resolve(nil)
241
245
  }
242
246
 
243
247
  @objc public func pause(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
244
248
  player?.pause()
249
+ eventEmitter("playback-state", ["state": "paused"])
245
250
  resolve(nil)
246
251
  }
247
252
 
@@ -249,6 +254,7 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
249
254
  player?.pause()
250
255
  player?.seek(to: .zero)
251
256
  stopProgressTimer()
257
+ eventEmitter("playback-state", ["state": "stopped"])
252
258
  resolve(nil)
253
259
  }
254
260
 
@@ -257,9 +263,19 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
257
263
  player?.removeAllItems()
258
264
  queue.removeAll()
259
265
  playerItems.removeAll()
266
+ let lastIndex = currentIndex
260
267
  currentIndex = -1
261
268
  stopProgressTimer()
262
269
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
270
+
271
+ eventEmitter("playback-active-track-changed", [
272
+ "index": -1,
273
+ "track": NSNull(),
274
+ "lastIndex": lastIndex,
275
+ "lastPosition": 0.0,
276
+ ] as [String : Any])
277
+ eventEmitter("playback-state", ["state": "none"])
278
+
263
279
  resolve(nil)
264
280
  }
265
281
 
@@ -392,6 +408,18 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
392
408
 
393
409
  // ── Private helpers ─────────────────────────────────────────────────────────
394
410
 
411
+ private func emitActiveTrackChanged(lastIndex: Int = -1) {
412
+ let lastPos = CMTimeGetSeconds(player?.currentTime() ?? .zero)
413
+ let track = (currentIndex >= 0 && currentIndex < queue.count) ? queue[currentIndex] : nil
414
+
415
+ eventEmitter("playback-active-track-changed", [
416
+ "index": currentIndex,
417
+ "track": track as Any,
418
+ "lastIndex": lastIndex >= 0 ? lastIndex : currentIndex,
419
+ "lastPosition": lastPos,
420
+ ])
421
+ }
422
+
395
423
  private func rebuildPlayerQueue() {
396
424
  guard let player = player else { return }
397
425
  player.removeAllItems()
@@ -407,7 +435,9 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
407
435
  player.insert(item, after: player.items().last)
408
436
  }
409
437
 
410
- if currentIndex < 0 && !queue.isEmpty { currentIndex = 0 }
438
+ if currentIndex < 0 && !queue.isEmpty {
439
+ currentIndex = 0
440
+ }
411
441
  updateNowPlayingInfo()
412
442
  }
413
443
 
@@ -430,7 +460,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
430
460
  info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = CMTimeGetSeconds(player?.currentTime() ?? .zero)
431
461
  info[MPNowPlayingInfoPropertyPlaybackRate] = player?.rate ?? 0.0
432
462
 
433
- // Artwork
434
463
  if let artworkURL = track["artwork"] as? String, !artworkURL.isEmpty {
435
464
  loadArtwork(from: artworkURL) { image in
436
465
  if let image = image {
@@ -467,10 +496,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
467
496
 
468
497
  // ── Remote commands ─────────────────────────────────────────────────────────
469
498
 
470
- /// Must always be called on the main thread (see `runOnMain` call sites:
471
- /// `setupPlayer` and `updateOptions`). `MPRemoteCommandCenter` and
472
- /// `UIApplication.beginReceivingRemoteControlEvents()` are main-thread-only
473
- /// APIs.
474
499
  private func setupRemoteCommands() {
475
500
  let center = MPRemoteCommandCenter.shared()
476
501
  UIApplication.shared.beginReceivingRemoteControlEvents()
@@ -509,7 +534,7 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
509
534
  guard let self = self else { return .commandFailed }
510
535
  let next = self.currentIndex + 1
511
536
  if next < self.queue.count {
512
- self.skipToIndex(next, initialPosition: -1, resolve: { _ in }, reject: { _, _, _ in })
537
+ self.skipToIndex(next, initialPosition: -1, autoPlay: true, resolve: { _ in }, reject: { _, _, _ in })
513
538
  }
514
539
  self.eventEmitter("remote-next", nil)
515
540
  return .success
@@ -521,7 +546,7 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
521
546
  guard let self = self else { return .commandFailed }
522
547
  let prev = self.currentIndex - 1
523
548
  if prev >= 0 {
524
- self.skipToIndex(prev, initialPosition: -1, resolve: { _ in }, reject: { _, _, _ in })
549
+ self.skipToIndex(prev, initialPosition: -1, autoPlay: true, resolve: { _ in }, reject: { _, _, _ in })
525
550
  }
526
551
  self.eventEmitter("remote-previous", nil)
527
552
  return .success
@@ -588,7 +613,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
588
613
  object: nil
589
614
  )
590
615
 
591
- // Observe player status
592
616
  if let player = player {
593
617
  let obs = player.observe(\.timeControlStatus, options: [.new]) { [weak self] p, _ in
594
618
  self?.eventEmitter("playback-state", ["state": self?.currentStateString() ?? "none"])
@@ -609,16 +633,18 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
609
633
  }
610
634
 
611
635
  @objc private func playerItemDidFinish(_ notification: Notification) {
612
- // Only react to notifications for our own player's current item —
613
- // `object: nil` on the observer means this fires for *any* AVPlayerItem
614
- // in the whole app (e.g. a video player elsewhere), so we must filter here.
615
636
  guard let finishedItem = notification.object as? AVPlayerItem,
616
637
  finishedItem === player?.currentItem else { return }
617
638
 
618
639
  let nextIndex = currentIndex + 1
640
+ let currentPosition = CMTimeGetSeconds(player?.currentTime() ?? .zero)
641
+
642
+ eventEmitter("playback-track-ended", [
643
+ "index": currentIndex,
644
+ "position": currentPosition
645
+ ])
619
646
 
620
647
  if repeatMode == 1 {
621
- // Repeat current track
622
648
  player?.seek(to: .zero)
623
649
  player?.play()
624
650
  return
@@ -634,14 +660,16 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
634
660
  "lastPosition": 0.0,
635
661
  ])
636
662
  } else if repeatMode == 2 {
637
- // Repeat queue
638
663
  currentIndex = 0
639
664
  rebuildPlayerQueue()
640
665
  player?.play()
666
+ if !queue.isEmpty {
667
+ emitActiveTrackChanged()
668
+ }
641
669
  } else {
642
670
  eventEmitter("playback-queue-ended", [
643
671
  "index": currentIndex,
644
- "position": CMTimeGetSeconds(player?.currentTime() ?? .zero),
672
+ "position": currentPosition,
645
673
  ])
646
674
  }
647
675
  }
@@ -677,7 +705,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
677
705
  @objc private func audioRouteChanged(_ notification: Notification) {
678
706
  guard let reasonValue = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt,
679
707
  let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return }
680
- // Pause on headphone unplug (standard iOS behavior)
681
708
  if reason == .oldDeviceUnavailable { player?.pause() }
682
709
  }
683
710
 
@@ -37,6 +37,7 @@ RCT_EXPORT_MODULE(RNGliphPlayer)
37
37
  @"playback-state",
38
38
  @"playback-error",
39
39
  @"playback-active-track-changed",
40
+ @"playback-track-ended",
40
41
  @"playback-queue-ended",
41
42
  @"playback-progress-updated",
42
43
  @"playback-metadata-received",
@@ -95,21 +96,34 @@ RCT_EXPORT_METHOD(removeUpcomingTracks:(RCTPromiseResolveBlock)resolve
95
96
 
96
97
  RCT_EXPORT_METHOD(skip:(double)index
97
98
  initialPosition:(double)initialPosition
99
+ autoPlay:(BOOL)autoPlay
98
100
  resolve:(RCTPromiseResolveBlock)resolve
99
101
  reject:(RCTPromiseRejectBlock)reject) {
100
- [_player skipToIndex:(NSInteger)index initialPosition:initialPosition resolve:resolve reject:reject];
102
+ [_player skipToIndex:(NSInteger)index
103
+ initialPosition:initialPosition
104
+ autoPlay:autoPlay
105
+ resolve:resolve
106
+ reject:reject];
101
107
  }
102
108
 
103
109
  RCT_EXPORT_METHOD(skipToNext:(double)initialPosition
110
+ autoPlay:(BOOL)autoPlay
104
111
  resolve:(RCTPromiseResolveBlock)resolve
105
112
  reject:(RCTPromiseRejectBlock)reject) {
106
- [_player skipToNextWithInitialPosition:initialPosition resolve:resolve reject:reject];
113
+ [_player skipToNextWithInitialPosition:initialPosition
114
+ autoPlay:autoPlay
115
+ resolve:resolve
116
+ reject:reject];
107
117
  }
108
118
 
109
119
  RCT_EXPORT_METHOD(skipToPrevious:(double)initialPosition
120
+ autoPlay:(BOOL)autoPlay
110
121
  resolve:(RCTPromiseResolveBlock)resolve
111
122
  reject:(RCTPromiseRejectBlock)reject) {
112
- [_player skipToPreviousWithInitialPosition:initialPosition resolve:resolve reject:reject];
123
+ [_player skipToPreviousWithInitialPosition:initialPosition
124
+ autoPlay:autoPlay
125
+ resolve:resolve
126
+ reject:reject];
113
127
  }
114
128
 
115
129
  RCT_EXPORT_METHOD(move:(double)fromIndex
@@ -240,4 +254,4 @@ RCT_EXPORT_METHOD(updateOptions:(NSDictionary *)options
240
254
  }
241
255
  #endif
242
256
 
243
- @end
257
+ @end
@@ -6,20 +6,24 @@
6
6
  */
7
7
  import type { Track, PlayerOptions, UpdateOptions, Progress, PlaybackState } from './types';
8
8
  import { RepeatMode, State } from './types';
9
+
9
10
  declare const GliphPlayer: {
10
11
  /**
11
12
  * Initialize the player. Must be called once before any other method.
12
13
  * Safe to call multiple times — subsequent calls are no-ops.
13
14
  */
14
15
  setupPlayer(options?: PlayerOptions): Promise<void>;
16
+
15
17
  /**
16
18
  * Destroy the player and release all resources.
17
19
  */
18
20
  destroy(): void;
21
+
19
22
  /**
20
23
  * Returns true if the background service is running (Android).
21
24
  */
22
25
  isServiceRunning(): Promise<boolean>;
26
+
23
27
  /**
24
28
  * Add one or more tracks to the queue.
25
29
  * @param tracks Single track or array of tracks.
@@ -27,71 +31,96 @@ declare const GliphPlayer: {
27
31
  * @returns The index of the first inserted track.
28
32
  */
29
33
  add(tracks: Track | Track[], insertBeforeIndex?: number): Promise<number | void>;
34
+
30
35
  /**
31
36
  * Remove tracks by their IDs.
32
37
  */
33
38
  remove(trackIds: string | string[]): Promise<void>;
39
+
34
40
  /**
35
41
  * Remove all tracks after the current one.
36
42
  */
37
43
  removeUpcomingTracks(): Promise<void>;
44
+
38
45
  /**
39
46
  * Skip to a specific queue index.
47
+ * @param index - Index of the track to skip to
48
+ * @param initialPosition - Position in seconds to start from (default: -1 = start)
49
+ * @param autoPlay - Whether to automatically start playback (default: true)
40
50
  */
41
- skip(index: number, initialPosition?: number): Promise<void>;
51
+ skip(index: number, initialPosition?: number, autoPlay?: boolean): Promise<void>;
52
+
42
53
  /**
43
54
  * Skip to the next track.
55
+ * @param initialPosition - Position in seconds to start from (default: -1 = start)
56
+ * @param autoPlay - Whether to automatically start playback (default: true)
44
57
  */
45
- skipToNext(initialPosition?: number): Promise<void>;
58
+ skipToNext(initialPosition?: number, autoPlay?: boolean): Promise<void>;
59
+
46
60
  /**
47
61
  * Skip to the previous track.
62
+ * @param initialPosition - Position in seconds to start from (default: -1 = start)
63
+ * @param autoPlay - Whether to automatically start playback (default: true)
48
64
  */
49
- skipToPrevious(initialPosition?: number): Promise<void>;
65
+ skipToPrevious(initialPosition?: number, autoPlay?: boolean): Promise<void>;
66
+
50
67
  /**
51
68
  * Move a track from one index to another.
52
69
  */
53
70
  move(fromIndex: number, toIndex: number): Promise<void>;
71
+
54
72
  play(): Promise<void>;
55
73
  pause(): Promise<void>;
56
74
  stop(): Promise<void>;
75
+
57
76
  /**
58
77
  * Stop playback and clear the queue.
59
78
  */
60
79
  reset(): Promise<void>;
80
+
61
81
  /**
62
82
  * Seek to an absolute position in seconds.
63
83
  */
64
84
  seekTo(position: number): Promise<void>;
85
+
65
86
  /**
66
87
  * Seek by a relative offset in seconds (positive = forward, negative = back).
67
88
  */
68
89
  seekBy(offset: number): Promise<void>;
90
+
69
91
  /**
70
92
  * Set volume (0.0 – 1.0).
71
93
  */
72
94
  setVolume(volume: number): Promise<void>;
73
95
  getVolume(): Promise<number>;
96
+
74
97
  /**
75
98
  * Set playback rate (1.0 = normal speed).
76
99
  */
77
100
  setRate(rate: number): Promise<void>;
78
101
  getRate(): Promise<number>;
102
+
79
103
  setRepeatMode(mode: RepeatMode): Promise<void>;
80
104
  getRepeatMode(): Promise<RepeatMode>;
105
+
81
106
  getQueue(): Promise<Track[]>;
82
107
  getActiveTrackIndex(): Promise<number | null>;
83
108
  getActiveTrack(): Promise<Track | null>;
84
109
  getTrack(index: number): Promise<Track | null>;
85
110
  getQueueSize(): Promise<number>;
111
+
86
112
  getPlaybackState(): Promise<PlaybackState>;
87
113
  getProgress(): Promise<Progress>;
114
+
88
115
  updateMetadataForTrack(index: number, metadata: Partial<Track>): Promise<void>;
89
116
  clearNowPlayingMetadata(): Promise<void>;
90
117
  updateNowPlayingMetadata(metadata: Partial<Track>): Promise<void>;
118
+
91
119
  /**
92
120
  * Configure notification capabilities, jump intervals, etc.
93
121
  */
94
122
  updateOptions(options: UpdateOptions): Promise<void>;
123
+
95
124
  /**
96
125
  * Subscribe to a player event.
97
126
  * Returns an unsubscribe function — call it to remove the listener.
@@ -105,9 +134,10 @@ declare const GliphPlayer: {
105
134
  */
106
135
  addEventListener<E extends string>(event: E, listener: (data: any) => void): () => void;
107
136
  };
137
+
108
138
  export default GliphPlayer;
139
+
109
140
  export declare function isPlaying(state: State): boolean;
110
141
  export declare function isPaused(state: State): boolean;
111
142
  export declare function isBuffering(state: State): boolean;
112
- export declare function isStopped(state: State): boolean;
113
- //# sourceMappingURL=GliphPlayer.d.ts.map
143
+ export declare function isStopped(state: State): boolean;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "name": "Aleksey Ostrikov",
5
5
  "email": "alex303606@gmail.com"
6
6
  },
7
- "version": "1.3.13",
7
+ "version": "1.3.15",
8
8
  "description": "Fixed version of react-native-gliph-player with Expo compatibility",
9
9
  "main": "app.plugin.js",
10
10
  "module": "lib/module/index.js",