react-native-mp3-player 1.0.3 → 1.0.5

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/README.md CHANGED
@@ -34,6 +34,7 @@ For audio to continue when the app is backgrounded or the screen is locked (and
34
34
  1. **Enable Background Modes → Audio** (or “Audio, AirPlay, and Picture in Picture”) in your app’s Xcode project: select your target → **Signing & Capabilities** → **+ Capability** → **Background Modes** → check **Audio**.
35
35
  2. The package configures **AVAudioSession** (category `.playback` with options for Bluetooth, AirPlay, ducking) and handles **interruptions** and **background transitions** so that playback can continue when the app is backgrounded.
36
36
  3. **Lock screen and Control Center** controls (play, pause, seek, 15-second skip) are handled **natively**, so they work even when the JavaScript thread is suspended (e.g. screen locked). When the app returns to the foreground, events are emitted so your UI stays in sync.
37
+ 4. **Now Playing widget:** The package sets and updates **MPNowPlayingInfoCenter** as soon as a track is loaded (title, artist, duration, elapsed, rate, artwork) and keeps it updated every second during playback. When you pause, the widget shows the track as paused (rate 0), not "Not Playing". Now playing info is only cleared when there is no current track (e.g. after `reset()`).
37
38
 
38
39
  ### Android background playback
39
40
 
@@ -79,8 +80,8 @@ TrackPlayer.registerPlaybackService(() => PlaybackService);
79
80
  - **Lifecycle:** `setupPlayer(options?, background?)`, `registerPlaybackService(factory)`, `reset()`
80
81
  - **Queue:** `add()`, `load()`, `remove()`, `skip()`, `skipToNext()`, `skipToPrevious()`, `setQueue()`, `getQueue()`, **`getActiveTrack()`** (current track), `getActiveTrackIndex()`
81
82
  - **Playback:** `play()`, `pause()`, `stop()`, `seekTo()`, `seekBy()`, `setVolume()`, `setRate()`, `setRepeatMode()`
82
- - **State:** `getPlaybackState()`, `getProgress()`, `getVolume()`, `getRate()`
83
- - **Events:** `addEventListener(event, listener)` – see `Event` enum.
83
+ - **State & progress:** **`getPlaybackState()`** (returns `{ state }`; use this, not `getState`), **`getProgress()`** (returns `{ position, duration, buffered }` in seconds), **`getPosition()`** and **`getDuration()`** (convenience wrappers around `getProgress()`), `getVolume()`, `getRate()`
84
+ - **Events:** `addEventListener(event, listener)` – see `Event` enum. Listen for `Event.PlaybackState` so the UI stays in sync when the user taps play/pause.
84
85
  - **Hooks:** **`useProgress(updateInterval?, background?)`** (interval in **milliseconds**; e.g. `useProgress(250)` = every 250 ms), `usePlaybackState()`, `useActiveTrack()`, `useIsPlaying()`, `useTrackPlayerEvents()`, etc.
85
86
 
86
87
  **Setup options** (e.g. in `setupPlayer` / `updateOptions`): `iosCategory` (e.g. `'playback'`), `iosCategoryOptions` (e.g. `['allowAirPlay','allowBluetooth','duckOthers']`), `autoHandleInterruptions`, `autoUpdateMetadata`, `waitForBuffer`, `minBuffer` / buffer-related options, `forwardJumpInterval` / `backwardJumpInterval` (seconds, e.g. 15), `progressUpdateEventInterval` (seconds). Types and options are in the package TypeScript definitions.
@@ -27,6 +27,9 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
27
27
  private var forwardJumpInterval: NSNumber? = nil;
28
28
  private var backwardJumpInterval: NSNumber? = nil;
29
29
  private var sessionCategory: AVAudioSession.Category = .playback
30
+ /// Timer work item for updating Now Playing elapsed/duration every second so the lock screen widget stays in sync.
31
+ private var nowPlayingUpdateWorkItem: DispatchWorkItem? = nil
32
+ private let nowPlayingUpdateQueue = DispatchQueue.main
30
33
  private var sessionCategoryMode: AVAudioSession.Mode = .default
31
34
  private var sessionCategoryPolicy: AVAudioSession.RouteSharingPolicy = .longFormAudio
32
35
  private var sessionCategoryOptions: AVAudioSession.CategoryOptions = []
@@ -199,6 +202,10 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
199
202
  player.remoteCommandController.handlePauseCommand = { [weak self] _ in
200
203
  guard let self = self else { return MPRemoteCommandHandlerStatus.commandFailed }
201
204
  self.player.pause()
205
+ if self.player.currentItem != nil && self.player.automaticallyUpdateNowPlayingInfo {
206
+ self.player.updateNowPlayingPlaybackValues()
207
+ }
208
+ self.emit(event: EventType.PlaybackState, body: self.getPlaybackStateBodyKeyValues(state: .paused))
202
209
  self.emit(event: EventType.RemotePause)
203
210
  return MPRemoteCommandHandlerStatus.success
204
211
  }
@@ -206,6 +213,10 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
206
213
  player.remoteCommandController.handlePlayCommand = { [weak self] _ in
207
214
  guard let self = self else { return MPRemoteCommandHandlerStatus.commandFailed }
208
215
  self.player.play()
216
+ if self.player.currentItem != nil && self.player.automaticallyUpdateNowPlayingInfo {
217
+ self.player.updateNowPlayingPlaybackValues()
218
+ }
219
+ self.emit(event: EventType.PlaybackState, body: self.getPlaybackStateBodyKeyValues(state: .playing))
209
220
  self.emit(event: EventType.RemotePlay)
210
221
  return MPRemoteCommandHandlerStatus.success
211
222
  }
@@ -244,6 +255,10 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
244
255
  player.remoteCommandController.handleStopCommand = { [weak self] _ in
245
256
  guard let self = self else { return MPRemoteCommandHandlerStatus.commandFailed }
246
257
  self.player.stop()
258
+ if self.player.currentItem != nil && self.player.automaticallyUpdateNowPlayingInfo {
259
+ self.player.updateNowPlayingPlaybackValues()
260
+ }
261
+ self.emit(event: EventType.PlaybackState, body: self.getPlaybackStateBodyKeyValues(state: .stopped))
247
262
  self.emit(event: EventType.RemoteStop)
248
263
  return MPRemoteCommandHandlerStatus.success
249
264
  }
@@ -252,9 +267,17 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
252
267
  guard let self = self else { return MPRemoteCommandHandlerStatus.commandFailed }
253
268
  if self.player.playerState == .paused {
254
269
  self.player.play()
270
+ if self.player.currentItem != nil && self.player.automaticallyUpdateNowPlayingInfo {
271
+ self.player.updateNowPlayingPlaybackValues()
272
+ }
273
+ self.emit(event: EventType.PlaybackState, body: self.getPlaybackStateBodyKeyValues(state: .playing))
255
274
  self.emit(event: EventType.RemotePlay)
256
275
  } else {
257
276
  self.player.pause()
277
+ if self.player.currentItem != nil && self.player.automaticallyUpdateNowPlayingInfo {
278
+ self.player.updateNowPlayingPlaybackValues()
279
+ }
280
+ self.emit(event: EventType.PlaybackState, body: self.getPlaybackStateBodyKeyValues(state: .paused))
258
281
  self.emit(event: EventType.RemotePause)
259
282
  }
260
283
  return MPRemoteCommandHandlerStatus.success
@@ -531,6 +554,7 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
531
554
  public func reset(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
532
555
  if (rejectWhenNotInitialized(reject: reject)) { return }
533
556
 
557
+ stopNowPlayingUpdateTimer()
534
558
  player.stop()
535
559
  player.clear()
536
560
  resolve(NSNull())
@@ -540,7 +564,12 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
540
564
  public func play(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
541
565
  if (rejectWhenNotInitialized(reject: reject)) { return }
542
566
  player.play()
567
+ // Update Now Playing widget immediately (rate 1, current elapsed) so it never shows "Not Playing".
568
+ if player.currentItem != nil && player.automaticallyUpdateNowPlayingInfo {
569
+ player.updateNowPlayingPlaybackValues()
570
+ }
543
571
  resolve(NSNull())
572
+ emit(event: EventType.PlaybackState, body: getPlaybackStateBodyKeyValues(state: .playing))
544
573
  }
545
574
 
546
575
  @objc(pause:rejecter:)
@@ -548,7 +577,12 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
548
577
  if (rejectWhenNotInitialized(reject: reject)) { return }
549
578
 
550
579
  player.pause()
580
+ // Update Now Playing widget immediately (rate 0, current elapsed) so it shows "Paused" not "Not Playing".
581
+ if player.currentItem != nil && player.automaticallyUpdateNowPlayingInfo {
582
+ player.updateNowPlayingPlaybackValues()
583
+ }
551
584
  resolve(NSNull())
585
+ emit(event: EventType.PlaybackState, body: getPlaybackStateBodyKeyValues(state: .paused))
552
586
  }
553
587
 
554
588
  @objc(setPlayWhenReady:resolver:rejecter:)
@@ -823,6 +857,33 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
823
857
  "position": player.currentTime,
824
858
  ] as [String : Any])
825
859
  }
860
+ // Keep Now Playing widget elapsed/duration in sync: update every second when we have a current item and are ready/playing/paused.
861
+ switch state {
862
+ case .ready, .playing, .paused:
863
+ if player.currentItem != nil && player.automaticallyUpdateNowPlayingInfo {
864
+ scheduleNextNowPlayingUpdate()
865
+ } else {
866
+ stopNowPlayingUpdateTimer()
867
+ }
868
+ default:
869
+ stopNowPlayingUpdateTimer()
870
+ }
871
+ }
872
+
873
+ private func scheduleNextNowPlayingUpdate() {
874
+ stopNowPlayingUpdateTimer()
875
+ let workItem = DispatchWorkItem { [weak self] in
876
+ guard let self = self, self.player.currentItem != nil, self.player.automaticallyUpdateNowPlayingInfo else { return }
877
+ self.player.updateNowPlayingPlaybackValues()
878
+ self.scheduleNextNowPlayingUpdate()
879
+ }
880
+ nowPlayingUpdateWorkItem = workItem
881
+ nowPlayingUpdateQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
882
+ }
883
+
884
+ private func stopNowPlayingUpdateTimer() {
885
+ nowPlayingUpdateWorkItem?.cancel()
886
+ nowPlayingUpdateWorkItem = nil
826
887
  }
827
888
 
828
889
  func handleAudioPlayerCommonMetadataReceived(metadata: [AVMetadataItem]) {
@@ -211,12 +211,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
211
211
  currentItem = item
212
212
 
213
213
  if (automaticallyUpdateNowPlayingInfo) {
214
- // Reset playback values without updating, because that will happen in
215
- // the loadNowPlayingMetaValues call straight after:
216
- nowPlayingInfoController.setWithoutUpdate(keyValues: [
217
- MediaItemProperty.duration(nil),
218
- NowPlayingInfoProperty.playbackRate(nil),
219
- NowPlayingInfoProperty.elapsedPlaybackTime(nil)
214
+ // Set initial playback values so the Now Playing widget never shows "Not Playing"
215
+ // (duration/elapsed/rate must be set; use 0 until the player reports real values).
216
+ nowPlayingInfoController.set(keyValues: [
217
+ MediaItemProperty.duration(0),
218
+ NowPlayingInfoProperty.playbackRate(playWhenReady ? 1.0 : 0.0),
219
+ NowPlayingInfoProperty.elapsedPlaybackTime(0)
220
220
  ])
221
221
  loadNowPlayingMetaValues()
222
222
  }
@@ -269,6 +269,14 @@ export declare function getActiveTrack(): Promise<Track | undefined>;
269
269
  * duration in seconds.
270
270
  */
271
271
  export declare function getProgress(): Promise<Progress>;
272
+ /**
273
+ * Gets the current playback position in seconds. Convenience wrapper around getProgress().
274
+ */
275
+ export declare function getPosition(): Promise<number>;
276
+ /**
277
+ * Gets the duration of the current track in seconds. Convenience wrapper around getProgress().
278
+ */
279
+ export declare function getDuration(): Promise<number>;
272
280
  /**
273
281
  * Gets the playback state of the player.
274
282
  *
@@ -474,6 +474,20 @@ export async function getProgress() {
474
474
  // @ts-expect-error codegen issues
475
475
  return TrackPlayer.getProgress();
476
476
  }
477
+ /**
478
+ * Gets the current playback position in seconds. Convenience wrapper around getProgress().
479
+ */
480
+ export async function getPosition() {
481
+ const { position } = await getProgress();
482
+ return position;
483
+ }
484
+ /**
485
+ * Gets the duration of the current track in seconds. Convenience wrapper around getProgress().
486
+ */
487
+ export async function getDuration() {
488
+ const { duration } = await getProgress();
489
+ return duration;
490
+ }
477
491
  /**
478
492
  * Gets the playback state of the player.
479
493
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-mp3-player",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "React Native audio player with reliable iOS background playback. Media controls, queue, hooks. Built for stability and long-running playback.",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",
@@ -633,6 +633,22 @@ export async function getProgress(): Promise<Progress> {
633
633
  return TrackPlayer.getProgress();
634
634
  }
635
635
 
636
+ /**
637
+ * Gets the current playback position in seconds. Convenience wrapper around getProgress().
638
+ */
639
+ export async function getPosition(): Promise<number> {
640
+ const { position } = await getProgress();
641
+ return position;
642
+ }
643
+
644
+ /**
645
+ * Gets the duration of the current track in seconds. Convenience wrapper around getProgress().
646
+ */
647
+ export async function getDuration(): Promise<number> {
648
+ const { duration } = await getProgress();
649
+ return duration;
650
+ }
651
+
636
652
  /**
637
653
  * Gets the playback state of the player.
638
654
  *