react-native-nitro-player 0.3.0-alpha.6 → 0.3.0-alpha.8

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.
@@ -94,4 +94,8 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
94
94
 
95
95
  @Keep
96
96
  override fun isAndroidAutoConnected(): Boolean = core.isAndroidAutoConnected()
97
+
98
+ @DoNotStrip
99
+ @Keep
100
+ override fun setVolume(volume: Double): Boolean = core.setVolume(volume)
97
101
  }
@@ -4,6 +4,8 @@ package com.margelo.nitro.nitroplayer.core
4
4
 
5
5
  import android.content.Context
6
6
  import android.net.Uri
7
+ import androidx.media3.common.AudioAttributes
8
+ import androidx.media3.common.C
7
9
  import androidx.media3.common.MediaItem
8
10
  import androidx.media3.common.MediaMetadata
9
11
  import androidx.media3.common.Player
@@ -72,25 +74,43 @@ class TrackPlayerCore private constructor(
72
74
 
73
75
  init {
74
76
  handler.post {
75
- // Configure LoadControl for gapless playback
76
- // This enables pre-buffering of the next track for seamless transitions
77
+ // ============================================================
78
+ // GAPLESS PLAYBACK CONFIGURATION
79
+ // ============================================================
80
+ // Configure LoadControl for maximum gapless playback
81
+ // Large buffers ensure next track is fully ready before current ends
77
82
  val loadControl =
78
83
  DefaultLoadControl
79
84
  .Builder()
80
85
  .setBufferDurationsMs(
81
- DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, // Minimum buffer: 1.5s
82
- DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, // Maximum buffer: 5s
83
- DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, // Buffer for playback: 2.5s
84
- DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, // Buffer after rebuffer: 5s
85
- ).setBackBuffer(DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS, true) // Keep back buffer for seamless transitions
86
+ 30_000, // MIN_BUFFER_MS: 30 seconds minimum buffer
87
+ 120_000, // MAX_BUFFER_MS: 2 minutes maximum buffer (enables preloading next tracks)
88
+ 2_500, // BUFFER_FOR_PLAYBACK_MS: 2.5s before playback starts
89
+ 5_000, // BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS: 5s after rebuffer
90
+ ).setBackBuffer(30_000, true) // Keep 30s back buffer for seamless seek-back
91
+ .setTargetBufferBytes(C.LENGTH_UNSET) // No size limit - prioritize time
86
92
  .setPrioritizeTimeOverSizeThresholds(true) // Prioritize time-based buffering
87
93
  .build()
88
94
 
95
+ // Configure audio attributes for optimal music playback
96
+ // This enables gapless audio processing in the audio pipeline
97
+ val audioAttributes =
98
+ AudioAttributes
99
+ .Builder()
100
+ .setUsage(C.USAGE_MEDIA)
101
+ .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
102
+ .build()
103
+
89
104
  player =
90
105
  ExoPlayer
91
106
  .Builder(context)
92
107
  .setLoadControl(loadControl)
108
+ .setAudioAttributes(audioAttributes, true) // handleAudioFocus = true for gapless
109
+ .setHandleAudioBecomingNoisy(true) // Pause when headphones disconnected
110
+ .setPauseAtEndOfMediaItems(false) // Don't pause between items - key for gapless!
93
111
  .build()
112
+
113
+ println("🎵 TrackPlayerCore: Gapless playback configured - 120s buffer, audio focus handling enabled")
94
114
  mediaSessionManager =
95
115
  MediaSessionManager(context, player, playlistManager).apply {
96
116
  setTrackPlayerCore(this@TrackPlayerCore)
@@ -652,4 +672,21 @@ class TrackPlayerCore private constructor(
652
672
  NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
653
673
  }
654
674
  }
675
+
676
+ // Set volume (0-100 range, converted to 0.0-1.0 for ExoPlayer)
677
+ fun setVolume(volume: Double): Boolean =
678
+ if (::player.isInitialized) {
679
+ handler.post {
680
+ // Clamp volume to 0-100 range
681
+ val clampedVolume = volume.coerceIn(0.0, 100.0)
682
+ // Convert to 0.0-1.0 range for ExoPlayer
683
+ val normalizedVolume = (clampedVolume / 100.0).toFloat()
684
+ player.volume = normalizedVolume
685
+ println("🔊 TrackPlayerCore: Volume set to $clampedVolume% (normalized: $normalizedVolume)")
686
+ }
687
+ true
688
+ } else {
689
+ println("⚠️ TrackPlayerCore: Cannot set volume - player not initialized")
690
+ false
691
+ }
655
692
  }
@@ -101,4 +101,10 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
101
101
  func isAndroidAutoConnected() throws -> Bool {
102
102
  return false
103
103
  }
104
+
105
+ // MARK: - Volume Control
106
+
107
+ func setVolume(volume: Double) throws -> Bool {
108
+ return core.setVolume(volume: volume)
109
+ }
104
110
  }
@@ -31,6 +31,13 @@ class TrackPlayerCore: NSObject {
31
31
  // UI/Display constants
32
32
  static let separatorLineLength: Int = 80
33
33
  static let playlistSeparatorLength: Int = 40
34
+
35
+ // Gapless playback configuration
36
+ static let preferredForwardBufferDuration: Double = 30.0 // Buffer 30 seconds ahead
37
+ static let preloadAssetKeys: [String] = [
38
+ "playable", "duration", "tracks", "preferredTransform",
39
+ ]
40
+ static let gaplessPreloadCount: Int = 3 // Number of tracks to preload ahead
34
41
  }
35
42
 
36
43
  // MARK: - Properties
@@ -46,6 +53,10 @@ class TrackPlayerCore: NSObject {
46
53
  private var boundaryTimeObserver: Any?
47
54
  private var currentItemObservers: [NSKeyValueObservation] = []
48
55
 
56
+ // Gapless playback: Cache for preloaded assets
57
+ private var preloadedAssets: [String: AVURLAsset] = [:]
58
+ private let preloadQueue = DispatchQueue(label: "com.nitroplayer.preload", qos: .utility)
59
+
49
60
  var onChangeTrack: ((TrackItem, Reason?) -> Void)?
50
61
  var onPlaybackStateChange: ((TrackPlayerState, Reason?) -> Void)?
51
62
  var onSeek: ((Double, Double) -> Void)?
@@ -77,6 +88,24 @@ class TrackPlayerCore: NSObject {
77
88
 
78
89
  private func setupPlayer() {
79
90
  player = AVQueuePlayer()
91
+
92
+ // MARK: - Gapless Playback Configuration
93
+
94
+ // Disable automatic waiting to minimize stalling - this allows smoother transitions
95
+ // between tracks as AVPlayer won't pause to buffer excessively
96
+ player?.automaticallyWaitsToMinimizeStalling = false
97
+
98
+ // Set playback rate to 1.0 immediately when ready (reduces gap between tracks)
99
+ player?.actionAtItemEnd = .advance
100
+
101
+ // Configure for high-quality audio playback with minimal latency
102
+ if #available(iOS 15.0, *) {
103
+ player?.audiovisualBackgroundPlaybackPolicy = .continuesIfPossible
104
+ }
105
+
106
+ print(
107
+ "🎵 TrackPlayerCore: Gapless playback configured - automaticallyWaitsToMinimizeStalling=false")
108
+
80
109
  setupPlayerObservers()
81
110
  }
82
111
 
@@ -446,6 +475,11 @@ class TrackPlayerCore: NSObject {
446
475
  if currentItem.status == .readyToPlay {
447
476
  setupBoundaryTimeObserver()
448
477
  }
478
+
479
+ // MARK: - Gapless Playback: Preload upcoming tracks when track changes
480
+ // This ensures the next tracks are ready for seamless transitions
481
+ preloadUpcomingTracks(from: currentTrackIndex + 1)
482
+ cleanupPreloadedAssets(keepingFrom: currentTrackIndex)
449
483
  }
450
484
 
451
485
  private func setupCurrentItemObservers(item: AVPlayerItem) {
@@ -554,6 +588,135 @@ class TrackPlayerCore: NSObject {
554
588
  mediaSessionManager?.onPlaybackStateChanged()
555
589
  }
556
590
 
591
+ // MARK: - Gapless Playback Helpers
592
+
593
+ /// Creates a gapless-optimized AVPlayerItem with proper buffering configuration
594
+ private func createGaplessPlayerItem(for track: TrackItem, isPreload: Bool = false)
595
+ -> AVPlayerItem?
596
+ {
597
+ guard let url = URL(string: track.url) else {
598
+ print("❌ TrackPlayerCore: Invalid URL for track: \(track.title) - \(track.url)")
599
+ return nil
600
+ }
601
+
602
+ // Check if we have a preloaded asset for this track
603
+ let asset: AVURLAsset
604
+ if let preloadedAsset = preloadedAssets[track.id] {
605
+ asset = preloadedAsset
606
+ print("🚀 TrackPlayerCore: Using preloaded asset for \(track.title)")
607
+ } else {
608
+ // Create asset with options optimized for gapless playback
609
+ asset = AVURLAsset(
610
+ url: url,
611
+ options: [
612
+ AVURLAssetPreferPreciseDurationAndTimingKey: true // Ensures accurate duration for gapless transitions
613
+ ])
614
+ }
615
+
616
+ let item = AVPlayerItem(asset: asset)
617
+
618
+ // Configure buffer duration for gapless playback
619
+ // This tells AVPlayer how much content to buffer ahead
620
+ item.preferredForwardBufferDuration = Constants.preferredForwardBufferDuration
621
+
622
+ // Enable automatic loading of item properties for faster starts
623
+ item.canUseNetworkResourcesForLiveStreamingWhilePaused = true
624
+
625
+ // Store track ID for later reference
626
+ item.trackId = track.id
627
+
628
+ // If this is a preload request, start loading asset keys asynchronously
629
+ if isPreload {
630
+ asset.loadValuesAsynchronously(forKeys: Constants.preloadAssetKeys) {
631
+ // Asset keys are now loaded, which speeds up playback start
632
+ var allKeysLoaded = true
633
+ for key in Constants.preloadAssetKeys {
634
+ var error: NSError?
635
+ let status = asset.statusOfValue(forKey: key, error: &error)
636
+ if status == .failed {
637
+ print(
638
+ "⚠️ TrackPlayerCore: Failed to load key '\(key)' for \(track.title): \(error?.localizedDescription ?? "unknown")"
639
+ )
640
+ allKeysLoaded = false
641
+ }
642
+ }
643
+ if allKeysLoaded {
644
+ print("✅ TrackPlayerCore: All asset keys preloaded for \(track.title)")
645
+ }
646
+ }
647
+ }
648
+
649
+ return item
650
+ }
651
+
652
+ /// Preloads assets for upcoming tracks to enable gapless playback
653
+ private func preloadUpcomingTracks(from startIndex: Int) {
654
+ preloadQueue.async { [weak self] in
655
+ guard let self = self else { return }
656
+
657
+ let endIndex = min(startIndex + Constants.gaplessPreloadCount, self.currentTracks.count)
658
+
659
+ for i in startIndex..<endIndex {
660
+ let track = self.currentTracks[i]
661
+
662
+ // Skip if already preloaded
663
+ if self.preloadedAssets[track.id] != nil {
664
+ continue
665
+ }
666
+
667
+ guard let url = URL(string: track.url) else { continue }
668
+
669
+ let asset = AVURLAsset(
670
+ url: url,
671
+ options: [
672
+ AVURLAssetPreferPreciseDurationAndTimingKey: true
673
+ ])
674
+
675
+ // Preload essential keys for gapless playback
676
+ asset.loadValuesAsynchronously(forKeys: Constants.preloadAssetKeys) { [weak self] in
677
+ var allKeysLoaded = true
678
+ for key in Constants.preloadAssetKeys {
679
+ var error: NSError?
680
+ let status = asset.statusOfValue(forKey: key, error: &error)
681
+ if status != .loaded {
682
+ allKeysLoaded = false
683
+ break
684
+ }
685
+ }
686
+
687
+ if allKeysLoaded {
688
+ DispatchQueue.main.async {
689
+ self?.preloadedAssets[track.id] = asset
690
+ print("🎯 TrackPlayerCore: Preloaded asset for upcoming track: \(track.title)")
691
+ }
692
+ }
693
+ }
694
+ }
695
+ }
696
+ }
697
+
698
+ /// Clears preloaded assets that are no longer needed
699
+ private func cleanupPreloadedAssets(keepingFrom currentIndex: Int) {
700
+ preloadQueue.async { [weak self] in
701
+ guard let self = self else { return }
702
+
703
+ // Keep assets for current track and upcoming tracks within preload range
704
+ let keepRange =
705
+ currentIndex..<min(
706
+ currentIndex + Constants.gaplessPreloadCount + 1, self.currentTracks.count)
707
+ let keepIds = Set(keepRange.compactMap { self.currentTracks[safe: $0]?.id })
708
+
709
+ let assetsToRemove = self.preloadedAssets.keys.filter { !keepIds.contains($0) }
710
+ for id in assetsToRemove {
711
+ self.preloadedAssets.removeValue(forKey: id)
712
+ }
713
+
714
+ if !assetsToRemove.isEmpty {
715
+ print("🧹 TrackPlayerCore: Cleaned up \(assetsToRemove.count) preloaded assets")
716
+ }
717
+ }
718
+ }
719
+
557
720
  // MARK: - Queue Management
558
721
 
559
722
  private func updatePlayerQueue(tracks: [TrackItem]) {
@@ -578,40 +741,18 @@ class TrackPlayerCore: NSObject {
578
741
  boundaryTimeObserver = nil
579
742
  }
580
743
 
581
- // Create AVPlayerItems from tracks
582
- let items = tracks.compactMap { track -> AVPlayerItem? in
583
- guard let url = URL(string: track.url) else {
584
- print("❌ TrackPlayerCore: Invalid URL for track: \(track.title) - \(track.url)")
585
- return nil
586
- }
587
-
588
- let item = AVPlayerItem(url: url)
589
-
590
- // Set metadata using AVMutableMetadataItem
591
- let metadata = AVMutableMetadataItem()
592
- metadata.identifier = .commonIdentifierTitle
593
- metadata.value = track.title as NSString
594
- metadata.locale = Locale.current
595
-
596
- let artistMetadata = AVMutableMetadataItem()
597
- artistMetadata.identifier = .commonIdentifierArtist
598
- artistMetadata.value = track.artist as NSString
599
- artistMetadata.locale = Locale.current
600
-
601
- let albumMetadata = AVMutableMetadataItem()
602
- albumMetadata.identifier = .commonIdentifierAlbumName
603
- albumMetadata.value = track.album as NSString
604
- albumMetadata.locale = Locale.current
605
-
606
- // Note: AVPlayerItem doesn't have externalMetadata property
607
- // Metadata will be set via MPNowPlayingInfoCenter in MediaSessionManager
744
+ // Clear old preloaded assets when loading new queue
745
+ preloadedAssets.removeAll()
608
746
 
609
- // Store track ID in item for later reference
610
- item.trackId = track.id
611
-
612
- return item
747
+ // Create gapless-optimized AVPlayerItems from tracks
748
+ let items = tracks.enumerated().compactMap { (index, track) -> AVPlayerItem? in
749
+ // First few items get preload treatment for faster initial playback
750
+ let isPreload = index < Constants.gaplessPreloadCount
751
+ return createGaplessPlayerItem(for: track, isPreload: isPreload)
613
752
  }
614
753
 
754
+ print("🎵 TrackPlayerCore: Created \(items.count) gapless-optimized player items")
755
+
615
756
  guard !items.isEmpty else {
616
757
  print("❌ TrackPlayerCore: No valid items to play")
617
758
  return
@@ -672,7 +813,10 @@ class TrackPlayerCore: NSObject {
672
813
  mediaSessionManager?.onTrackChanged()
673
814
  }
674
815
 
675
- print("✅ TrackPlayerCore: Queue updated with \(items.count) tracks")
816
+ // Start preloading upcoming tracks for gapless playback
817
+ preloadUpcomingTracks(from: 1)
818
+
819
+ print("✅ TrackPlayerCore: Queue updated with \(items.count) gapless-optimized tracks")
676
820
  }
677
821
 
678
822
  func getCurrentTrack() -> TrackItem? {
@@ -966,6 +1110,28 @@ class TrackPlayerCore: NSObject {
966
1110
  return playlistManager.getAllPlaylists().map { $0.toGeneratedPlaylist() }
967
1111
  }
968
1112
 
1113
+ // MARK: - Volume Control
1114
+
1115
+ func setVolume(volume: Double) -> Bool {
1116
+ guard let player = player else {
1117
+ print("⚠️ TrackPlayerCore: Cannot set volume - no player available")
1118
+ return false
1119
+ }
1120
+ DispatchQueue.main.async { [weak self] in
1121
+ guard let self = self, let currentPlayer = self.player else {
1122
+ return
1123
+ }
1124
+ // Clamp volume to 0-100 range
1125
+ let clampedVolume = max(0.0, min(100.0, volume))
1126
+ // Convert to 0.0-1.0 range for AVQueuePlayer
1127
+ let normalizedVolume = Float(clampedVolume / 100.0)
1128
+ currentPlayer.volume = normalizedVolume
1129
+ print(
1130
+ "🔊 TrackPlayerCore: Volume set to \(Int(clampedVolume))% (normalized: \(normalizedVolume))")
1131
+ }
1132
+ return true
1133
+ }
1134
+
969
1135
  func playFromIndex(index: Int) {
970
1136
  DispatchQueue.main.async { [weak self] in
971
1137
  guard let self = self,
@@ -988,14 +1154,15 @@ class TrackPlayerCore: NSObject {
988
1154
  // Recreate the queue starting from the target index
989
1155
  // This ensures all remaining tracks are in the queue
990
1156
  let tracksToPlay = Array(fullPlaylist[index...])
991
- print(" 🔄 Creating queue with \(tracksToPlay.count) tracks starting from index \(index)")
992
-
993
- // Update the queue (but keep the full currentTracks for reference)
994
- let items = tracksToPlay.compactMap { track -> AVPlayerItem? in
995
- guard let url = URL(string: track.url) else { return nil }
996
- let item = AVPlayerItem(url: url)
997
- item.trackId = track.id
998
- return item
1157
+ print(
1158
+ " 🔄 Creating gapless queue with \(tracksToPlay.count) tracks starting from index \(index)"
1159
+ )
1160
+
1161
+ // Create gapless-optimized player items
1162
+ let items = tracksToPlay.enumerated().compactMap { (offset, track) -> AVPlayerItem? in
1163
+ // First few items get preload treatment for faster playback
1164
+ let isPreload = offset < Constants.gaplessPreloadCount
1165
+ return self.createGaplessPlayerItem(for: track, isPreload: isPreload)
999
1166
  }
1000
1167
 
1001
1168
  guard let player = self.player, !items.isEmpty else {
@@ -1020,13 +1187,16 @@ class TrackPlayerCore: NSObject {
1020
1187
  // Restore the full playlist reference (don't slice it!)
1021
1188
  self.currentTracks = fullPlaylist
1022
1189
 
1023
- print(" ✅ Queue recreated. Now at index: \(self.currentTrackIndex)")
1190
+ print(" ✅ Gapless queue recreated. Now at index: \(self.currentTrackIndex)")
1024
1191
  if let track = self.getCurrentTrack() {
1025
1192
  print(" 🎵 Playing: \(track.title)")
1026
1193
  self.onChangeTrack?(track, .skip)
1027
1194
  self.mediaSessionManager?.onTrackChanged()
1028
1195
  }
1029
1196
 
1197
+ // Start preloading upcoming tracks for gapless playback
1198
+ self.preloadUpcomingTracks(from: index + 1)
1199
+
1030
1200
  player.play()
1031
1201
  }
1032
1202
  }
@@ -1036,6 +1206,9 @@ class TrackPlayerCore: NSObject {
1036
1206
  deinit {
1037
1207
  print("🧹 TrackPlayerCore: Cleaning up...")
1038
1208
 
1209
+ // Clear preloaded assets for gapless playback
1210
+ preloadedAssets.removeAll()
1211
+
1039
1212
  // Remove boundary time observer
1040
1213
  if let boundaryObserver = boundaryTimeObserver, let currentPlayer = player {
1041
1214
  currentPlayer.removeTimeObserver(boundaryObserver)
@@ -4,3 +4,4 @@ export { useOnSeek } from './useOnSeek';
4
4
  export { useOnPlaybackProgressChange } from './useOnPlaybackProgressChange';
5
5
  export { useAndroidAutoConnection } from './useAndroidAutoConnection';
6
6
  export { useAudioDevices } from './useAudioDevices';
7
+ export { useNowPlaying } from './useNowPlaying';
@@ -4,3 +4,4 @@ export { useOnSeek } from './useOnSeek';
4
4
  export { useOnPlaybackProgressChange } from './useOnPlaybackProgressChange';
5
5
  export { useAndroidAutoConnection } from './useAndroidAutoConnection';
6
6
  export { useAudioDevices } from './useAudioDevices';
7
+ export { useNowPlaying } from './useNowPlaying';
@@ -0,0 +1,36 @@
1
+ import type { PlayerState } from '../types/PlayerQueue';
2
+ /**
3
+ * Hook to get the current player state (same as TrackPlayer.getState())
4
+ *
5
+ * This hook provides all player state information including:
6
+ * - Current track
7
+ * - Current position and duration
8
+ * - Playback state (playing, paused, stopped)
9
+ * - Current playlist ID
10
+ * - Current track index
11
+ *
12
+ * The hook polls getState() periodically and also listens to events
13
+ * for immediate updates when state changes.
14
+ *
15
+ * @returns PlayerState object with all current player information
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * function PlayerComponent() {
20
+ * const state = useNowPlaying()
21
+ *
22
+ * return (
23
+ * <View>
24
+ * {state.currentTrack && (
25
+ * <Text>Now Playing: {state.currentTrack.title}</Text>
26
+ * )}
27
+ * <Text>Position: {state.currentPosition} / {state.totalDuration}</Text>
28
+ * <Text>State: {state.currentState}</Text>
29
+ * <Text>Playlist: {state.currentPlaylistId || 'None'}</Text>
30
+ * <Text>Index: {state.currentIndex}</Text>
31
+ * </View>
32
+ * )
33
+ * }
34
+ * ```
35
+ */
36
+ export declare function useNowPlaying(): PlayerState;
@@ -0,0 +1,79 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { TrackPlayer } from '../index';
3
+ /**
4
+ * Hook to get the current player state (same as TrackPlayer.getState())
5
+ *
6
+ * This hook provides all player state information including:
7
+ * - Current track
8
+ * - Current position and duration
9
+ * - Playback state (playing, paused, stopped)
10
+ * - Current playlist ID
11
+ * - Current track index
12
+ *
13
+ * The hook polls getState() periodically and also listens to events
14
+ * for immediate updates when state changes.
15
+ *
16
+ * @returns PlayerState object with all current player information
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * function PlayerComponent() {
21
+ * const state = useNowPlaying()
22
+ *
23
+ * return (
24
+ * <View>
25
+ * {state.currentTrack && (
26
+ * <Text>Now Playing: {state.currentTrack.title}</Text>
27
+ * )}
28
+ * <Text>Position: {state.currentPosition} / {state.totalDuration}</Text>
29
+ * <Text>State: {state.currentState}</Text>
30
+ * <Text>Playlist: {state.currentPlaylistId || 'None'}</Text>
31
+ * <Text>Index: {state.currentIndex}</Text>
32
+ * </View>
33
+ * )
34
+ * }
35
+ * ```
36
+ */
37
+ export function useNowPlaying() {
38
+ const [state, setState] = useState(() => {
39
+ // Get initial state
40
+ try {
41
+ return TrackPlayer.getState();
42
+ }
43
+ catch (error) {
44
+ console.error('Error getting initial player state:', error);
45
+ // Return default state
46
+ return {
47
+ currentTrack: null,
48
+ currentPosition: 0,
49
+ totalDuration: 0,
50
+ currentState: 'stopped',
51
+ currentPlaylistId: null,
52
+ currentIndex: -1,
53
+ };
54
+ }
55
+ });
56
+ useEffect(() => {
57
+ // Update state function
58
+ const updateState = () => {
59
+ try {
60
+ const newState = TrackPlayer.getState();
61
+ setState(newState);
62
+ }
63
+ catch (error) {
64
+ console.error('Error updating player state:', error);
65
+ }
66
+ };
67
+ // Get initial state
68
+ updateState();
69
+ // Listen to track changes
70
+ TrackPlayer.onChangeTrack(() => {
71
+ updateState();
72
+ });
73
+ // Listen to playback state changes
74
+ TrackPlayer.onPlaybackStateChange(() => {
75
+ updateState();
76
+ });
77
+ }, []);
78
+ return state;
79
+ }
@@ -38,4 +38,5 @@ export interface TrackPlayer extends HybridObject<{
38
38
  onPlaybackProgressChange(callback: (position: number, totalDuration: number, isManuallySeeked?: boolean) => void): void;
39
39
  onAndroidAutoConnectionChange(callback: (connected: boolean) => void): void;
40
40
  isAndroidAutoConnected(): boolean;
41
+ setVolume(volume: number): boolean;
41
42
  }
@@ -142,5 +142,10 @@ namespace margelo::nitro::nitroplayer {
142
142
  auto __result = method(_javaPart);
143
143
  return static_cast<bool>(__result);
144
144
  }
145
+ bool JHybridTrackPlayerSpec::setVolume(double volume) {
146
+ static const auto method = javaClassStatic()->getMethod<jboolean(double /* volume */)>("setVolume");
147
+ auto __result = method(_javaPart, volume);
148
+ return static_cast<bool>(__result);
149
+ }
145
150
 
146
151
  } // namespace margelo::nitro::nitroplayer
@@ -69,6 +69,7 @@ namespace margelo::nitro::nitroplayer {
69
69
  void onPlaybackProgressChange(const std::function<void(double /* position */, double /* totalDuration */, std::optional<bool> /* isManuallySeeked */)>& callback) override;
70
70
  void onAndroidAutoConnectionChange(const std::function<void(bool /* connected */)>& callback) override;
71
71
  bool isAndroidAutoConnected() override;
72
+ bool setVolume(double volume) override;
72
73
 
73
74
  private:
74
75
  friend HybridBase;
@@ -129,6 +129,10 @@ abstract class HybridTrackPlayerSpec: HybridObject() {
129
129
  @DoNotStrip
130
130
  @Keep
131
131
  abstract fun isAndroidAutoConnected(): Boolean
132
+
133
+ @DoNotStrip
134
+ @Keep
135
+ abstract fun setVolume(volume: Double): Boolean
132
136
 
133
137
  private external fun initHybrid(): HybridData
134
138
 
@@ -177,6 +177,14 @@ namespace margelo::nitro::nitroplayer {
177
177
  auto __value = std::move(__result.value());
178
178
  return __value;
179
179
  }
180
+ inline bool setVolume(double volume) override {
181
+ auto __result = _swiftPart.setVolume(std::forward<decltype(volume)>(volume));
182
+ if (__result.hasError()) [[unlikely]] {
183
+ std::rethrow_exception(__result.error());
184
+ }
185
+ auto __value = std::move(__result.value());
186
+ return __value;
187
+ }
180
188
 
181
189
  private:
182
190
  NitroPlayer::HybridTrackPlayerSpec_cxx _swiftPart;
@@ -29,6 +29,7 @@ public protocol HybridTrackPlayerSpec_protocol: HybridObject {
29
29
  func onPlaybackProgressChange(callback: @escaping (_ position: Double, _ totalDuration: Double, _ isManuallySeeked: Bool?) -> Void) throws -> Void
30
30
  func onAndroidAutoConnectionChange(callback: @escaping (_ connected: Bool) -> Void) throws -> Void
31
31
  func isAndroidAutoConnected() throws -> Bool
32
+ func setVolume(volume: Double) throws -> Bool
32
33
  }
33
34
 
34
35
  public extension HybridTrackPlayerSpec_protocol {
@@ -334,4 +334,16 @@ open class HybridTrackPlayerSpec_cxx {
334
334
  return bridge.create_Result_bool_(__exceptionPtr)
335
335
  }
336
336
  }
337
+
338
+ @inline(__always)
339
+ public final func setVolume(volume: Double) -> bridge.Result_bool_ {
340
+ do {
341
+ let __result = try self.__implementation.setVolume(volume: volume)
342
+ let __resultCpp = __result
343
+ return bridge.create_Result_bool_(__resultCpp)
344
+ } catch (let __error) {
345
+ let __exceptionPtr = __error.toCpp()
346
+ return bridge.create_Result_bool_(__exceptionPtr)
347
+ }
348
+ }
337
349
  }
@@ -29,6 +29,7 @@ namespace margelo::nitro::nitroplayer {
29
29
  prototype.registerHybridMethod("onPlaybackProgressChange", &HybridTrackPlayerSpec::onPlaybackProgressChange);
30
30
  prototype.registerHybridMethod("onAndroidAutoConnectionChange", &HybridTrackPlayerSpec::onAndroidAutoConnectionChange);
31
31
  prototype.registerHybridMethod("isAndroidAutoConnected", &HybridTrackPlayerSpec::isAndroidAutoConnected);
32
+ prototype.registerHybridMethod("setVolume", &HybridTrackPlayerSpec::setVolume);
32
33
  });
33
34
  }
34
35
 
@@ -82,6 +82,7 @@ namespace margelo::nitro::nitroplayer {
82
82
  virtual void onPlaybackProgressChange(const std::function<void(double /* position */, double /* totalDuration */, std::optional<bool> /* isManuallySeeked */)>& callback) = 0;
83
83
  virtual void onAndroidAutoConnectionChange(const std::function<void(bool /* connected */)>& callback) = 0;
84
84
  virtual bool isAndroidAutoConnected() = 0;
85
+ virtual bool setVolume(double volume) = 0;
85
86
 
86
87
  protected:
87
88
  // Hybrid Setup