react-native-nitro-player 0.3.0-alpha.5 → 0.3.0-alpha.7

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.
Files changed (45) hide show
  1. package/README.md +699 -1
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +4 -0
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +44 -7
  4. package/ios/HybridTrackPlayer.swift +6 -0
  5. package/ios/core/TrackPlayerCore.swift +214 -41
  6. package/lib/hooks/index.d.ts +6 -0
  7. package/lib/hooks/index.js +6 -0
  8. package/lib/hooks/useAndroidAutoConnection.d.ts +13 -0
  9. package/lib/hooks/useAndroidAutoConnection.js +26 -0
  10. package/lib/hooks/useAudioDevices.d.ts +26 -0
  11. package/lib/hooks/useAudioDevices.js +55 -0
  12. package/lib/hooks/useOnChangeTrack.d.ts +9 -0
  13. package/lib/hooks/useOnChangeTrack.js +17 -0
  14. package/lib/hooks/useOnPlaybackProgressChange.d.ts +9 -0
  15. package/lib/hooks/useOnPlaybackProgressChange.js +19 -0
  16. package/lib/hooks/useOnPlaybackStateChange.d.ts +9 -0
  17. package/lib/hooks/useOnPlaybackStateChange.js +17 -0
  18. package/lib/hooks/useOnSeek.d.ts +8 -0
  19. package/lib/hooks/useOnSeek.js +17 -0
  20. package/lib/index.d.ts +15 -0
  21. package/lib/index.js +24 -0
  22. package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +21 -0
  23. package/lib/specs/AndroidAutoMediaLibrary.nitro.js +1 -0
  24. package/lib/specs/AudioDevices.nitro.d.ts +24 -0
  25. package/lib/specs/AudioDevices.nitro.js +1 -0
  26. package/lib/specs/AudioRoutePicker.nitro.d.ts +10 -0
  27. package/lib/specs/AudioRoutePicker.nitro.js +1 -0
  28. package/lib/specs/TrackPlayer.nitro.d.ts +42 -0
  29. package/lib/specs/TrackPlayer.nitro.js +1 -0
  30. package/lib/types/AndroidAutoMediaLibrary.d.ts +44 -0
  31. package/lib/types/AndroidAutoMediaLibrary.js +1 -0
  32. package/lib/types/PlayerQueue.d.ts +32 -0
  33. package/lib/types/PlayerQueue.js +1 -0
  34. package/lib/utils/androidAutoMediaLibrary.d.ts +47 -0
  35. package/lib/utils/androidAutoMediaLibrary.js +62 -0
  36. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +5 -0
  37. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +4 -0
  39. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +8 -0
  40. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
  41. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +12 -0
  42. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
  43. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +1 -0
  44. package/package.json +10 -9
  45. package/src/specs/TrackPlayer.nitro.ts +1 -0
@@ -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)
@@ -0,0 +1,6 @@
1
+ export { useOnChangeTrack } from './useOnChangeTrack';
2
+ export { useOnPlaybackStateChange } from './useOnPlaybackStateChange';
3
+ export { useOnSeek } from './useOnSeek';
4
+ export { useOnPlaybackProgressChange } from './useOnPlaybackProgressChange';
5
+ export { useAndroidAutoConnection } from './useAndroidAutoConnection';
6
+ export { useAudioDevices } from './useAudioDevices';
@@ -0,0 +1,6 @@
1
+ export { useOnChangeTrack } from './useOnChangeTrack';
2
+ export { useOnPlaybackStateChange } from './useOnPlaybackStateChange';
3
+ export { useOnSeek } from './useOnSeek';
4
+ export { useOnPlaybackProgressChange } from './useOnPlaybackProgressChange';
5
+ export { useAndroidAutoConnection } from './useAndroidAutoConnection';
6
+ export { useAudioDevices } from './useAudioDevices';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Hook to detect Android Auto connection status using the official Android for Cars API
3
+ * Based on: https://developer.android.com/training/cars/apps#car-connection
4
+ *
5
+ * @returns Object with isConnected boolean
6
+ *
7
+ * @example
8
+ * const { isConnected } = useAndroidAutoConnection();
9
+ * console.log('Android Auto connected:', isConnected);
10
+ */
11
+ export declare function useAndroidAutoConnection(): {
12
+ isConnected: boolean;
13
+ };
@@ -0,0 +1,26 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { TrackPlayer } from '../index';
3
+ /**
4
+ * Hook to detect Android Auto connection status using the official Android for Cars API
5
+ * Based on: https://developer.android.com/training/cars/apps#car-connection
6
+ *
7
+ * @returns Object with isConnected boolean
8
+ *
9
+ * @example
10
+ * const { isConnected } = useAndroidAutoConnection();
11
+ * console.log('Android Auto connected:', isConnected);
12
+ */
13
+ export function useAndroidAutoConnection() {
14
+ const [isConnected, setIsConnected] = useState(false);
15
+ useEffect(() => {
16
+ // Set initial state
17
+ const initialState = TrackPlayer.isAndroidAutoConnected();
18
+ setIsConnected(initialState);
19
+ // Listen for connection changes
20
+ TrackPlayer.onAndroidAutoConnectionChange((connected) => {
21
+ setIsConnected(connected);
22
+ console.log('🚗 Android Auto connection changed:', connected);
23
+ });
24
+ }, []);
25
+ return { isConnected };
26
+ }
@@ -0,0 +1,26 @@
1
+ import type { TAudioDevice } from '../specs/AudioDevices.nitro';
2
+ /**
3
+ * Hook to get audio devices (Android only)
4
+ *
5
+ * Polls for device changes every 2 seconds
6
+ *
7
+ * @returns Object containing the current list of audio devices
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * function MyComponent() {
12
+ * const { devices } = useAudioDevices()
13
+ *
14
+ * return (
15
+ * <View>
16
+ * {devices.map(device => (
17
+ * <Text key={device.id}>{device.name}</Text>
18
+ * ))}
19
+ * </View>
20
+ * )
21
+ * }
22
+ * ```
23
+ */
24
+ export declare function useAudioDevices(): {
25
+ devices: TAudioDevice[];
26
+ };
@@ -0,0 +1,55 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Platform } from 'react-native';
3
+ import { NitroModules } from 'react-native-nitro-modules';
4
+ /**
5
+ * Hook to get audio devices (Android only)
6
+ *
7
+ * Polls for device changes every 2 seconds
8
+ *
9
+ * @returns Object containing the current list of audio devices
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * function MyComponent() {
14
+ * const { devices } = useAudioDevices()
15
+ *
16
+ * return (
17
+ * <View>
18
+ * {devices.map(device => (
19
+ * <Text key={device.id}>{device.name}</Text>
20
+ * ))}
21
+ * </View>
22
+ * )
23
+ * }
24
+ * ```
25
+ */
26
+ export function useAudioDevices() {
27
+ const [devices, setDevices] = useState([]);
28
+ useEffect(() => {
29
+ if (Platform.OS !== 'android') {
30
+ return undefined;
31
+ }
32
+ try {
33
+ const AudioDevices = NitroModules.createHybridObject('AudioDevices');
34
+ // Get initial devices
35
+ const updateDevices = () => {
36
+ try {
37
+ const currentDevices = AudioDevices.getAudioDevices();
38
+ setDevices(currentDevices);
39
+ }
40
+ catch (error) {
41
+ console.error('Error getting audio devices:', error);
42
+ }
43
+ };
44
+ updateDevices();
45
+ // Poll for changes every 2 seconds
46
+ const interval = setInterval(updateDevices, 2000);
47
+ return () => clearInterval(interval);
48
+ }
49
+ catch (error) {
50
+ console.error('Error setting up audio devices polling:', error);
51
+ return undefined;
52
+ }
53
+ }, []);
54
+ return { devices };
55
+ }
@@ -0,0 +1,9 @@
1
+ import type { TrackItem, Reason } from '../types/PlayerQueue';
2
+ /**
3
+ * Hook to get the current track and track change reason
4
+ * @returns Object with current track and reason, or undefined if no track is playing
5
+ */
6
+ export declare function useOnChangeTrack(): {
7
+ track: TrackItem | undefined;
8
+ reason: Reason | undefined;
9
+ };
@@ -0,0 +1,17 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { TrackPlayer } from '../index';
3
+ /**
4
+ * Hook to get the current track and track change reason
5
+ * @returns Object with current track and reason, or undefined if no track is playing
6
+ */
7
+ export function useOnChangeTrack() {
8
+ const [track, setTrack] = useState(undefined);
9
+ const [reason, setReason] = useState(undefined);
10
+ useEffect(() => {
11
+ TrackPlayer.onChangeTrack((newTrack, newReason) => {
12
+ setTrack(newTrack);
13
+ setReason(newReason);
14
+ });
15
+ }, []);
16
+ return { track, reason };
17
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Hook to get the current playback progress
3
+ * @returns Object with current position, total duration, and manual seek indicator
4
+ */
5
+ export declare function useOnPlaybackProgressChange(): {
6
+ position: number;
7
+ totalDuration: number;
8
+ isManuallySeeked: boolean | undefined;
9
+ };
@@ -0,0 +1,19 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { TrackPlayer } from '../index';
3
+ /**
4
+ * Hook to get the current playback progress
5
+ * @returns Object with current position, total duration, and manual seek indicator
6
+ */
7
+ export function useOnPlaybackProgressChange() {
8
+ const [position, setPosition] = useState(0);
9
+ const [totalDuration, setTotalDuration] = useState(0);
10
+ const [isManuallySeeked, setIsManuallySeeked] = useState(undefined);
11
+ useEffect(() => {
12
+ TrackPlayer.onPlaybackProgressChange((newPosition, newTotalDuration, newIsManuallySeeked) => {
13
+ setPosition(newPosition);
14
+ setTotalDuration(newTotalDuration);
15
+ setIsManuallySeeked(newIsManuallySeeked);
16
+ });
17
+ }, []);
18
+ return { position, totalDuration, isManuallySeeked };
19
+ }
@@ -0,0 +1,9 @@
1
+ import type { TrackPlayerState, Reason } from '../types/PlayerQueue';
2
+ /**
3
+ * Hook to get the current playback state and reason
4
+ * @returns Object with current playback state and reason
5
+ */
6
+ export declare function useOnPlaybackStateChange(): {
7
+ state: TrackPlayerState | undefined;
8
+ reason: Reason | undefined;
9
+ };
@@ -0,0 +1,17 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { TrackPlayer } from '../index';
3
+ /**
4
+ * Hook to get the current playback state and reason
5
+ * @returns Object with current playback state and reason
6
+ */
7
+ export function useOnPlaybackStateChange() {
8
+ const [state, setState] = useState(undefined);
9
+ const [reason, setReason] = useState(undefined);
10
+ useEffect(() => {
11
+ TrackPlayer.onPlaybackStateChange((newState, newReason) => {
12
+ setState(newState);
13
+ setReason(newReason);
14
+ });
15
+ }, []);
16
+ return { state, reason };
17
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Hook to get the last seek event information
3
+ * @returns Object with last seek position and total duration, or undefined if no seek has occurred
4
+ */
5
+ export declare function useOnSeek(): {
6
+ position: number | undefined;
7
+ totalDuration: number | undefined;
8
+ };