expo-gliph-player 1.3.16 → 1.3.18

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.
@@ -2,47 +2,37 @@ import AVFoundation
2
2
  import MediaPlayer
3
3
  import UIKit
4
4
 
5
- // MARK: - Type aliases
6
-
7
5
  public typealias EventEmitter = (String, [String: Any]?) -> Void
8
6
 
9
- // React Native's promise block types
10
7
  public typealias RCTPromiseResolveBlock = (Any?) -> Void
11
8
  public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
12
9
 
13
- // MARK: - GliphAudioPlayer
14
-
15
10
  @objc public class GliphAudioPlayer: NSObject {
16
11
 
17
- // ── Properties ──────────────────────────────────────────────────────────────
18
-
19
12
  private var player: AVQueuePlayer?
20
13
  private var playerItems: [AVPlayerItem] = []
21
14
  private var queue: [[String: Any]] = []
22
15
  private var currentIndex: Int = -1
23
- private var repeatMode: Int = 0 // 0=off, 1=track, 2=queue
16
+ private var repeatMode: Int = 0
24
17
  private var isSetup = false
25
18
  private var progressTimer: Timer?
26
19
  private var progressInterval: TimeInterval = 1.0
27
20
  private var options: [String: Any] = [:]
28
21
 
29
- // ✅ AVPlayerLooper для повтора трека
30
22
  private var playerLooper: AVPlayerLooper?
31
23
  private var templateItem: AVPlayerItem?
24
+ private var lastLooperTime: Double = 0
25
+ private var looperTimeObserver: Any?
32
26
 
33
27
  private let eventEmitter: EventEmitter
34
28
  private var timeObserver: Any?
35
29
  private var playerObservations: [NSKeyValueObservation] = []
36
30
 
37
- // ── Init ────────────────────────────────────────────────────────────────────
38
-
39
31
  @objc public init(eventEmitter: @escaping EventEmitter) {
40
32
  self.eventEmitter = eventEmitter
41
33
  super.init()
42
34
  }
43
35
 
44
- // ── Thread helper ───────────────────────────────────────────────────────────
45
-
46
36
  private func runOnMain(_ block: () -> Void) {
47
37
  if Thread.isMainThread {
48
38
  block()
@@ -51,8 +41,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
51
41
  }
52
42
  }
53
43
 
54
- // ── Setup ───────────────────────────────────────────────────────────────────
55
-
56
44
  @objc public func setupPlayer(
57
45
  options: [String: Any],
58
46
  resolve: @escaping RCTPromiseResolveBlock,
@@ -99,6 +87,16 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
99
87
  @objc public func destroy() {
100
88
  stopProgressTimer()
101
89
  removeObservers()
90
+
91
+ if let obs = looperTimeObserver {
92
+ player?.removeTimeObserver(obs)
93
+ looperTimeObserver = nil
94
+ }
95
+ playerLooper?.disableLooping()
96
+ playerLooper = nil
97
+ templateItem = nil
98
+ lastLooperTime = 0
99
+
102
100
  player?.pause()
103
101
  player?.removeAllItems()
104
102
  player = nil
@@ -114,8 +112,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
114
112
 
115
113
  @objc public func isReady() -> Bool { return isSetup }
116
114
 
117
- // ── Queue management ────────────────────────────────────────────────────────
118
-
119
115
  @objc public func addTracks(
120
116
  _ tracks: [[String: Any]],
121
117
  insertBeforeIndex: Int,
@@ -233,8 +229,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
233
229
  resolve(nil)
234
230
  }
235
231
 
236
- // ── Playback control ────────────────────────────────────────────────────────
237
-
238
232
  @objc public func play(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
239
233
  player?.play()
240
234
  startProgressTimer()
@@ -265,10 +259,14 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
265
259
  @objc public func reset(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
266
260
  player?.pause()
267
261
 
268
- // ✅ Уничтожаем looper
262
+ if let obs = looperTimeObserver {
263
+ player?.removeTimeObserver(obs)
264
+ looperTimeObserver = nil
265
+ }
269
266
  playerLooper?.disableLooping()
270
267
  playerLooper = nil
271
268
  templateItem = nil
269
+ lastLooperTime = 0
272
270
 
273
271
  player?.removeAllItems()
274
272
  queue.removeAll()
@@ -314,21 +312,29 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
314
312
 
315
313
  @objc public func getRate() -> Float { return player?.rate ?? 1.0 }
316
314
 
317
- // ✅ ИСПРАВЛЕНО: пересоздаём очередь при смене режима
318
315
  @objc public func setRepeatMode(_ mode: Int, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
319
316
  let oldMode = repeatMode
320
317
  repeatMode = mode
321
318
 
322
- // ✅ Пересоздаём очередь с учётом нового режима
323
- if oldMode != mode {
319
+ let oldNeedsLooper = oldMode == 1
320
+ let newNeedsLooper = mode == 1
321
+
322
+ if oldNeedsLooper != newNeedsLooper {
324
323
  let wasPlaying = player?.timeControlStatus == .playing
324
+ let savedPosition = CMTimeGetSeconds(player?.currentTime() ?? .zero)
325
+ let savedIndex = currentIndex
326
+
325
327
  rebuildPlayerQueue()
328
+
329
+ currentIndex = savedIndex
330
+ if savedPosition > 0 {
331
+ player?.seek(to: CMTime(seconds: savedPosition, preferredTimescale: 1000))
332
+ }
326
333
  if wasPlaying {
327
334
  player?.play()
328
335
  }
329
336
  }
330
337
 
331
- // ✅ Отправляем событие
332
338
  eventEmitter("playback-repeat-mode-changed", ["mode": mode])
333
339
 
334
340
  resolve(nil)
@@ -336,8 +342,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
336
342
 
337
343
  @objc public func getRepeatMode() -> Int { return repeatMode }
338
344
 
339
- // ── Queue getters ───────────────────────────────────────────────────────────
340
-
341
345
  @objc public func getQueue() -> [[String: Any]] { return queue }
342
346
 
343
347
  @objc public func getActiveTrackIndex() -> NSNumber? {
@@ -357,8 +361,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
357
361
 
358
362
  @objc public func getQueueSize() -> Int { return queue.count }
359
363
 
360
- // ── State / progress ────────────────────────────────────────────────────────
361
-
362
364
  @objc public func getPlaybackState() -> [String: Any] {
363
365
  return ["state": currentStateString()]
364
366
  }
@@ -380,8 +382,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
380
382
  ]
381
383
  }
382
384
 
383
- // ── Metadata ────────────────────────────────────────────────────────────────
384
-
385
385
  @objc public func updateMetadataForTrack(
386
386
  _ index: Int,
387
387
  metadata: [String: Any],
@@ -431,8 +431,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
431
431
  resolve(nil)
432
432
  }
433
433
 
434
- // ── Private helpers ─────────────────────────────────────────────────────────
435
-
436
434
  private func emitActiveTrackChanged(lastIndex: Int = -1) {
437
435
  let lastPos = CMTimeGetSeconds(player?.currentTime() ?? .zero)
438
436
  let track = (currentIndex >= 0 && currentIndex < queue.count) ? queue[currentIndex] : nil
@@ -445,40 +443,55 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
445
443
  ])
446
444
  }
447
445
 
448
- // ✅ ИСПРАВЛЕНО: добавлен AVPlayerLooper для повторения трека
449
446
  private func rebuildPlayerQueue() {
450
447
  guard let player = player else { return }
451
448
 
452
- // ✅ Уничтожаем старый looper
449
+ if let obs = looperTimeObserver {
450
+ player.removeTimeObserver(obs)
451
+ looperTimeObserver = nil
452
+ }
453
453
  playerLooper?.disableLooping()
454
454
  playerLooper = nil
455
455
  templateItem = nil
456
+ lastLooperTime = 0
456
457
 
457
458
  player.removeAllItems()
458
459
  playerItems.removeAll()
459
460
 
460
461
  let startIndex = max(0, currentIndex)
461
462
 
462
- // ✅ РЕЖИМ ПОВТОРА ТРЕКА (RepeatMode.Track = 1)
463
463
  if repeatMode == 1 && startIndex < queue.count {
464
464
  let track = queue[startIndex]
465
465
  guard let urlString = track["url"] as? String,
466
466
  let url = URL(string: urlString) else { return }
467
467
 
468
- // ✅ Создаём template item
469
468
  let item = AVPlayerItem(url: url)
470
469
  templateItem = item
471
470
  playerItems.append(item)
472
471
 
473
- // ✅ Создаём looper для бесконечного повтора
474
472
  playerLooper = AVPlayerLooper(player: player, templateItem: item)
475
473
 
474
+ let interval = CMTime(seconds: 0.1, preferredTimescale: 1000)
475
+ looperTimeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
476
+ guard let self = self else { return }
477
+ let timeSeconds = CMTimeGetSeconds(time)
478
+ guard !timeSeconds.isNaN else { return }
479
+
480
+ if self.lastLooperTime > 1.0 && timeSeconds < 0.5 {
481
+ self.eventEmitter("playback-track-ended", [
482
+ "index": self.currentIndex,
483
+ "position": self.lastLooperTime,
484
+ ])
485
+ }
486
+
487
+ self.lastLooperTime = timeSeconds
488
+ }
489
+
476
490
  currentIndex = startIndex
477
491
  updateNowPlayingInfo()
478
492
  return
479
493
  }
480
494
 
481
- // ✅ ОБЫЧНАЯ ОЧЕРЕДЬ (RepeatMode.Off / RepeatMode.Queue)
482
495
  for i in startIndex..<queue.count {
483
496
  let track = queue[i]
484
497
  guard let urlString = track["url"] as? String,
@@ -547,8 +560,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
547
560
  }
548
561
  }
549
562
 
550
- // ── Remote commands ─────────────────────────────────────────────────────────
551
-
552
563
  private func setupRemoteCommands() {
553
564
  let center = MPRemoteCommandCenter.shared()
554
565
  UIApplication.shared.beginReceivingRemoteControlEvents()
@@ -638,8 +649,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
638
649
  }
639
650
  }
640
651
 
641
- // ── Observers ───────────────────────────────────────────────────────────────
642
-
643
652
  private func setupNotificationObservers() {
644
653
  NotificationCenter.default.addObserver(
645
654
  self,
@@ -685,12 +694,10 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
685
694
  }
686
695
  }
687
696
 
688
- // ✅ ИСПРАВЛЕНО: AVPlayerLooper сам обрабатывает повтор трека
689
697
  @objc private func playerItemDidFinish(_ notification: Notification) {
690
698
  guard let finishedItem = notification.object as? AVPlayerItem,
691
699
  finishedItem === player?.currentItem else { return }
692
700
 
693
- // ✅ Если режим повтора трека - AVPlayerLooper сам обрабатывает, выходим
694
701
  if repeatMode == 1 {
695
702
  return
696
703
  }
@@ -713,7 +720,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
713
720
  "lastPosition": 0.0,
714
721
  ])
715
722
  } else if repeatMode == 2 {
716
- // ✅ ПОВТОР ВСЕЙ ОЧЕРЕДИ
717
723
  currentIndex = 0
718
724
  rebuildPlayerQueue()
719
725
  player?.play()
@@ -762,8 +768,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
762
768
  if reason == .oldDeviceUnavailable { player?.pause() }
763
769
  }
764
770
 
765
- // ── Progress timer ──────────────────────────────────────────────────────────
766
-
767
771
  private func startProgressTimer() {
768
772
  stopProgressTimer()
769
773
  progressTimer = Timer.scheduledTimer(withTimeInterval: progressInterval, repeats: true) { [weak self] _ in
@@ -781,8 +785,6 @@ public typealias RCTPromiseRejectBlock = (String?, String?, Error?) -> Void
781
785
  progressTimer = nil
782
786
  }
783
787
 
784
- // ── Audio session mapping ───────────────────────────────────────────────────
785
-
786
788
  private func mapIOSCategory(_ string: String?) -> AVAudioSession.Category {
787
789
  switch string {
788
790
  case "playAndRecord": return .playAndRecord
@@ -246,8 +246,6 @@ RCT_EXPORT_METHOD(updateOptions:(NSDictionary *)options
246
246
  [_player updateOptions:options resolve:resolve reject:reject];
247
247
  }
248
248
 
249
- // ── New Architecture (TurboModule) ────────────────────────────────────────────
250
-
251
249
  #ifdef RCT_NEW_ARCH_ENABLED
252
250
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
253
251
  (const facebook::react::ObjCTurboModule::InitParams &)params {
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.16",
7
+ "version": "1.3.18",
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",