react-native-nitro-player 0.3.0-alpha.14 → 0.3.0-alpha.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.
Files changed (76) hide show
  1. package/README.md +69 -0
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +105 -0
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +200 -179
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +486 -0
  5. package/ios/HybridDownloadManager.swift +147 -145
  6. package/ios/HybridEqualizer.swift +111 -0
  7. package/ios/core/TrackPlayerCore.swift +15 -7
  8. package/ios/download/DownloadDatabase.swift +390 -380
  9. package/ios/download/DownloadFileManager.swift +183 -167
  10. package/ios/download/DownloadManagerCore.swift +786 -749
  11. package/ios/equalizer/EqualizerCore.swift +685 -0
  12. package/lib/hooks/equalizerCallbackManager.d.ts +37 -0
  13. package/lib/hooks/equalizerCallbackManager.js +109 -0
  14. package/lib/hooks/index.d.ts +4 -0
  15. package/lib/hooks/index.js +3 -0
  16. package/lib/hooks/useEqualizer.d.ts +25 -0
  17. package/lib/hooks/useEqualizer.js +124 -0
  18. package/lib/hooks/useEqualizerPresets.d.ts +22 -0
  19. package/lib/hooks/useEqualizerPresets.js +96 -0
  20. package/lib/index.d.ts +3 -0
  21. package/lib/index.js +3 -0
  22. package/lib/specs/Equalizer.nitro.d.ts +43 -0
  23. package/lib/specs/Equalizer.nitro.js +1 -0
  24. package/lib/types/EqualizerTypes.d.ts +52 -0
  25. package/lib/types/EqualizerTypes.js +1 -0
  26. package/nitro.json +4 -0
  27. package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +2 -0
  28. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +16 -2
  29. package/nitrogen/generated/android/c++/JEqualizerBand.hpp +69 -0
  30. package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +78 -0
  31. package/nitrogen/generated/android/c++/JEqualizerState.hpp +91 -0
  32. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +81 -0
  33. package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +97 -0
  34. package/nitrogen/generated/android/c++/JGainRange.hpp +61 -0
  35. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +204 -0
  36. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +82 -0
  37. package/nitrogen/generated/android/c++/JPresetType.hpp +59 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerBand.kt +47 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerPreset.kt +44 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerState.kt +44 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__optional_std__variant_nitro__NullType__std__string__.kt +80 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_EqualizerBand_.kt +80 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/GainRange.kt +41 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +141 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PresetType.kt +21 -0
  46. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +41 -8
  47. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +167 -22
  48. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +20 -0
  49. package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +8 -0
  50. package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +15 -0
  51. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.cpp +11 -0
  52. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +223 -0
  53. package/nitrogen/generated/ios/swift/EqualizerBand.swift +69 -0
  54. package/nitrogen/generated/ios/swift/EqualizerPreset.swift +70 -0
  55. package/nitrogen/generated/ios/swift/EqualizerState.swift +115 -0
  56. package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_nitro__NullType__std__string__.swift +66 -0
  57. package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +47 -0
  58. package/nitrogen/generated/ios/swift/GainRange.swift +47 -0
  59. package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +73 -0
  60. package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +396 -0
  61. package/nitrogen/generated/ios/swift/PresetType.swift +40 -0
  62. package/nitrogen/generated/shared/c++/EqualizerBand.hpp +87 -0
  63. package/nitrogen/generated/shared/c++/EqualizerPreset.hpp +86 -0
  64. package/nitrogen/generated/shared/c++/EqualizerState.hpp +89 -0
  65. package/nitrogen/generated/shared/c++/GainRange.hpp +79 -0
  66. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.cpp +38 -0
  67. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +95 -0
  68. package/nitrogen/generated/shared/c++/PresetType.hpp +76 -0
  69. package/package.json +1 -1
  70. package/src/hooks/equalizerCallbackManager.ts +138 -0
  71. package/src/hooks/index.ts +6 -0
  72. package/src/hooks/useEqualizer.ts +173 -0
  73. package/src/hooks/useEqualizerPresets.ts +140 -0
  74. package/src/index.ts +6 -0
  75. package/src/specs/Equalizer.nitro.ts +69 -0
  76. package/src/types/EqualizerTypes.ts +72 -0
package/README.md CHANGED
@@ -20,6 +20,7 @@ npm install react-native-nitro-modules
20
20
 
21
21
  ## API Reference
22
22
 
23
+
23
24
  ### React Hooks
24
25
 
25
26
  | Name | Platform | Description |
@@ -31,6 +32,7 @@ npm install react-native-nitro-modules
31
32
  | `useNowPlaying` | Both | Returns complete player state (track, state, duration, playlist) in one object. |
32
33
  | `useActualQueue` | Both | Returns the efficient playback queue including temporary tracks. |
33
34
  | `usePlaylist` | Both | Manages playlist state, providing access to all playlists and tracks. |
35
+ | `useEqualizer` | Both | Controls the 5-band equalizer, including presets and individual band gains. |
34
36
  | `useAndroidAutoConnection` | Both | Monitors Android Auto connection status. |
35
37
  | `useAudioDevices` | Android | Returns list of available audio output devices. |
36
38
  | `useDownloadProgress` | Both | Tracks download progress for tracks. Returns progress map and overall status. |
@@ -584,6 +586,73 @@ if (AudioRoutePicker) {
584
586
  }
585
587
  ```
586
588
 
589
+
590
+ ## Equalizer
591
+
592
+ The player includes a powerful 5-band equalizer that works on both iOS and Android.
593
+
594
+ ### `useEqualizer()`
595
+
596
+ Returns the current equalizer state and control methods.
597
+
598
+ **Returns:**
599
+
600
+ - `isEnabled: boolean` - Whether the equalizer is currently active
601
+ - `bands: EqualizerBand[]` - Current gain settings for all 5 bands
602
+ - `currentPreset: string | null` - Name of the currently applied preset
603
+ - `setEnabled(enabled: boolean): boolean` - Toggle the equalizer on/off
604
+ - `setBandGain(index: number, gainDb: number): boolean` - Set gain for a specific band (range: -12dB to +12dB)
605
+ - `setAllBandGains(gains: number[]): boolean` - Set all band gains at once
606
+ - `reset(): void` - Reset to flat response
607
+
608
+ **Bands:**
609
+
610
+ The equalizer features 5 bands at the following center frequencies:
611
+ 1. **60 Hz** - Sub-bass/Bass
612
+ 2. **230 Hz** - Bass/Low-mids
613
+ 3. **910 Hz** - Mids
614
+ 4. **3.6 kHz** - Upper-mids/Treble
615
+ 5. **14 kHz** - High treble/Air
616
+
617
+ **Example:**
618
+
619
+ ```typescript
620
+ import { useEqualizer } from 'react-native-nitro-player'
621
+
622
+ function EqualizerControl() {
623
+ const {
624
+ isEnabled,
625
+ setEnabled,
626
+ bands,
627
+ setBandGain,
628
+ reset
629
+ } = useEqualizer()
630
+
631
+ return (
632
+ <View>
633
+ <Switch
634
+ value={isEnabled}
635
+ onValueChange={setEnabled}
636
+ />
637
+
638
+ {bands.map((band) => (
639
+ <View key={band.index}>
640
+ <Text>{band.frequencyLabel}</Text>
641
+ <Slider
642
+ minimumValue={-12}
643
+ maximumValue={12}
644
+ value={band.gainDb}
645
+ onSlidingComplete={(value) => setBandGain(band.index, value)}
646
+ />
647
+ </View>
648
+ ))}
649
+
650
+ <Button title="Reset" onPress={reset} />
651
+ </View>
652
+ )
653
+ }
654
+ ```
655
+
587
656
  ## Repeat Mode
588
657
 
589
658
  Control how tracks repeat during playback.
@@ -0,0 +1,105 @@
1
+ package com.margelo.nitro.nitroplayer
2
+
3
+ import androidx.annotation.Keep
4
+ import com.facebook.proguard.annotations.DoNotStrip
5
+ import com.margelo.nitro.NitroModules
6
+ import com.margelo.nitro.core.NullType
7
+ import com.margelo.nitro.nitroplayer.equalizer.EqualizerCore
8
+
9
+ @DoNotStrip
10
+ @Keep
11
+ class HybridEqualizer : HybridEqualizerSpec() {
12
+ private val core: EqualizerCore
13
+
14
+ init {
15
+ val context =
16
+ NitroModules.applicationContext ?: throw IllegalStateException("React Context is not initialized")
17
+
18
+ // Get the equalizer core - it will initialize lazily with audio session 0
19
+ // and be re-initialized with the proper session when onAudioSessionIdChanged fires
20
+ core = EqualizerCore.getInstance(context)
21
+ core.ensureInitialized()
22
+ }
23
+
24
+ @DoNotStrip
25
+ @Keep
26
+ override fun setEnabled(enabled: Boolean): Boolean = core.setEnabled(enabled)
27
+
28
+ @DoNotStrip
29
+ @Keep
30
+ override fun isEnabled(): Boolean = core.isEnabled()
31
+
32
+ @DoNotStrip
33
+ @Keep
34
+ override fun getBands(): Array<EqualizerBand> = core.getBands()
35
+
36
+ @DoNotStrip
37
+ @Keep
38
+ override fun setBandGain(
39
+ bandIndex: Double,
40
+ gainDb: Double,
41
+ ): Boolean = core.setBandGain(bandIndex.toInt(), gainDb)
42
+
43
+ @DoNotStrip
44
+ @Keep
45
+ override fun setAllBandGains(gains: DoubleArray): Boolean = core.setAllBandGains(gains)
46
+
47
+ @DoNotStrip
48
+ @Keep
49
+ override fun getBandRange(): GainRange = core.getBandRange()
50
+
51
+ @DoNotStrip
52
+ @Keep
53
+ override fun getPresets(): Array<EqualizerPreset> = core.getPresets()
54
+
55
+ @DoNotStrip
56
+ @Keep
57
+ override fun getBuiltInPresets(): Array<EqualizerPreset> = core.getBuiltInPresets()
58
+
59
+ @DoNotStrip
60
+ @Keep
61
+ override fun getCustomPresets(): Array<EqualizerPreset> = core.getCustomPresets()
62
+
63
+ @DoNotStrip
64
+ @Keep
65
+ override fun applyPreset(presetName: String): Boolean = core.applyPreset(presetName)
66
+
67
+ @DoNotStrip
68
+ @Keep
69
+ override fun getCurrentPresetName(): Variant_NullType_String {
70
+ val name = core.getCurrentPresetName()
71
+ return if (name != null) {
72
+ Variant_NullType_String.create(name)
73
+ } else {
74
+ Variant_NullType_String.create(NullType.NULL)
75
+ }
76
+ }
77
+
78
+ @DoNotStrip
79
+ @Keep
80
+ override fun saveCustomPreset(name: String): Boolean = core.saveCustomPreset(name)
81
+
82
+ @DoNotStrip
83
+ @Keep
84
+ override fun deleteCustomPreset(name: String): Boolean = core.deleteCustomPreset(name)
85
+
86
+ @DoNotStrip
87
+ @Keep
88
+ override fun getState(): EqualizerState = core.getState()
89
+
90
+ @DoNotStrip
91
+ @Keep
92
+ override fun reset() = core.reset()
93
+
94
+ override fun onEnabledChange(callback: (enabled: Boolean) -> Unit) {
95
+ core.addOnEnabledChangeListener(callback)
96
+ }
97
+
98
+ override fun onBandChange(callback: (bands: Array<EqualizerBand>) -> Unit) {
99
+ core.addOnBandChangeListener(callback)
100
+ }
101
+
102
+ override fun onPresetChange(callback: (presetName: Variant_NullType_String?) -> Unit) {
103
+ core.addOnPresetChangeListener(callback)
104
+ }
105
+ }
@@ -23,6 +23,7 @@ import com.margelo.nitro.nitroplayer.Variant_NullType_String
23
23
  import com.margelo.nitro.nitroplayer.Variant_NullType_TrackItem
24
24
  import com.margelo.nitro.nitroplayer.connection.AndroidAutoConnectionDetector
25
25
  import com.margelo.nitro.nitroplayer.download.DownloadManagerCore
26
+ import com.margelo.nitro.nitroplayer.equalizer.EqualizerCore
26
27
  import com.margelo.nitro.nitroplayer.media.MediaLibrary
27
28
  import com.margelo.nitro.nitroplayer.media.MediaLibraryManager
28
29
  import com.margelo.nitro.nitroplayer.media.MediaLibraryParser
@@ -49,6 +50,7 @@ class TrackPlayerCore private constructor(
49
50
  private var androidAutoConnectionDetector: AndroidAutoConnectionDetector? = null
50
51
  var onAndroidAutoConnectionChange: ((Boolean) -> Unit)? = null
51
52
  private var previousMediaItem: MediaItem? = null
53
+
52
54
  private val progressUpdateRunnable =
53
55
  object : Runnable {
54
56
  override fun run() {
@@ -106,219 +108,238 @@ class TrackPlayerCore private constructor(
106
108
  }
107
109
 
108
110
  init {
109
- handler.post {
110
- // ============================================================
111
- // GAPLESS PLAYBACK CONFIGURATION
112
- // ============================================================
113
- // Configure LoadControl for maximum gapless playback
114
- // Large buffers ensure next track is fully ready before current ends
115
- val loadControl =
116
- DefaultLoadControl
117
- .Builder()
118
- .setBufferDurationsMs(
119
- 30_000, // MIN_BUFFER_MS: 30 seconds minimum buffer
120
- 120_000, // MAX_BUFFER_MS: 2 minutes maximum buffer (enables preloading next tracks)
121
- 2_500, // BUFFER_FOR_PLAYBACK_MS: 2.5s before playback starts
122
- 5_000, // BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS: 5s after rebuffer
123
- ).setBackBuffer(30_000, true) // Keep 30s back buffer for seamless seek-back
124
- .setTargetBufferBytes(C.LENGTH_UNSET) // No size limit - prioritize time
125
- .setPrioritizeTimeOverSizeThresholds(true) // Prioritize time-based buffering
126
- .build()
127
-
128
- // Configure audio attributes for optimal music playback
129
- // This enables gapless audio processing in the audio pipeline
130
- val audioAttributes =
131
- AudioAttributes
132
- .Builder()
133
- .setUsage(C.USAGE_MEDIA)
134
- .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
135
- .build()
136
-
137
- player =
138
- ExoPlayer
139
- .Builder(context)
140
- .setLoadControl(loadControl)
141
- .setAudioAttributes(audioAttributes, true) // handleAudioFocus = true for gapless
142
- .setHandleAudioBecomingNoisy(true) // Pause when headphones disconnected
143
- .setPauseAtEndOfMediaItems(false) // Don't pause between items - key for gapless!
144
- .build()
145
-
146
- println("🎵 TrackPlayerCore: Gapless playback configured - 120s buffer, audio focus handling enabled")
147
- mediaSessionManager =
148
- MediaSessionManager(context, player, playlistManager).apply {
149
- setTrackPlayerCore(this@TrackPlayerCore)
150
- }
111
+ // Run synchronously on main thread to avoid deadlock
112
+ // when awaitInitialization is called from main thread
113
+ val initRunnable =
114
+ Runnable {
115
+ // ============================================================
116
+ // GAPLESS PLAYBACK CONFIGURATION
117
+ // ============================================================
118
+ // Configure LoadControl for maximum gapless playback
119
+ // Large buffers ensure next track is fully ready before current ends
120
+ val loadControl =
121
+ DefaultLoadControl
122
+ .Builder()
123
+ .setBufferDurationsMs(
124
+ 30_000, // MIN_BUFFER_MS: 30 seconds minimum buffer
125
+ 120_000, // MAX_BUFFER_MS: 2 minutes maximum buffer (enables preloading next tracks)
126
+ 2_500, // BUFFER_FOR_PLAYBACK_MS: 2.5s before playback starts
127
+ 5_000, // BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS: 5s after rebuffer
128
+ ).setBackBuffer(30_000, true) // Keep 30s back buffer for seamless seek-back
129
+ .setTargetBufferBytes(C.LENGTH_UNSET) // No size limit - prioritize time
130
+ .setPrioritizeTimeOverSizeThresholds(true) // Prioritize time-based buffering
131
+ .build()
132
+
133
+ // Configure audio attributes for optimal music playback
134
+ // This enables gapless audio processing in the audio pipeline
135
+ val audioAttributes =
136
+ AudioAttributes
137
+ .Builder()
138
+ .setUsage(C.USAGE_MEDIA)
139
+ .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
140
+ .build()
141
+
142
+ player =
143
+ ExoPlayer
144
+ .Builder(context)
145
+ .setLoadControl(loadControl)
146
+ .setAudioAttributes(audioAttributes, true) // handleAudioFocus = true for gapless
147
+ .setHandleAudioBecomingNoisy(true) // Pause when headphones disconnected
148
+ .setPauseAtEndOfMediaItems(false) // Don't pause between items - key for gapless!
149
+ .build()
150
+
151
+ mediaSessionManager =
152
+ MediaSessionManager(context, player, playlistManager).apply {
153
+ setTrackPlayerCore(this@TrackPlayerCore)
154
+ }
151
155
 
152
- // Set references for MediaBrowserService
153
- NitroPlayerMediaBrowserService.trackPlayerCore = this
154
- NitroPlayerMediaBrowserService.mediaSessionManager = mediaSessionManager
156
+ // Set references for MediaBrowserService
157
+ NitroPlayerMediaBrowserService.trackPlayerCore = this
158
+ NitroPlayerMediaBrowserService.mediaSessionManager = mediaSessionManager
155
159
 
156
- // Initialize Android Auto connection detector
157
- androidAutoConnectionDetector =
158
- AndroidAutoConnectionDetector(context).apply {
159
- onConnectionChanged = { connected, connectionType ->
160
- handler.post {
161
- isAndroidAutoConnected = connected
162
- NitroPlayerMediaBrowserService.isAndroidAutoConnected = connected
160
+ // Initialize Android Auto connection detector
161
+ androidAutoConnectionDetector =
162
+ AndroidAutoConnectionDetector(context).apply {
163
+ onConnectionChanged = { connected, connectionType ->
164
+ handler.post {
165
+ isAndroidAutoConnected = connected
166
+ NitroPlayerMediaBrowserService.isAndroidAutoConnected = connected
163
167
 
164
- // Notify JavaScript
165
- onAndroidAutoConnectionChange?.invoke(connected)
168
+ // Notify JavaScript
169
+ onAndroidAutoConnectionChange?.invoke(connected)
166
170
 
167
- println("🚗 Android Auto connection changed: connected=$connected, type=$connectionType")
171
+ println("🚗 Android Auto connection changed: connected=$connected, type=$connectionType")
172
+ }
168
173
  }
174
+ registerCarConnectionReceiver()
169
175
  }
170
- registerCarConnectionReceiver()
171
- }
172
176
 
173
- player.addListener(
174
- object : Player.Listener {
175
- override fun onMediaItemTransition(
176
- mediaItem: MediaItem?,
177
- reason: Int,
178
- ) {
179
- println("\n🔄 onMediaItemTransition called")
180
- println(
181
- " reason: ${when (reason) {
182
- Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -> "AUTO (track ended)"
183
- Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -> "SEEK"
184
- Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> "PLAYLIST_CHANGED"
185
- Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT -> "REPEAT"
186
- else -> "UNKNOWN($reason)"
187
- }}",
188
- )
189
- println(" previousMediaItem: ${previousMediaItem?.mediaId}")
190
- println(" new mediaItem: ${mediaItem?.mediaId}")
191
- println(" playNextStack: ${playNextStack.map { it.id }}")
192
- println(" upNextQueue: ${upNextQueue.map { it.id }}")
193
-
194
- // Remove finished track from temporary lists
195
- // Handle AUTO (natural end) and SEEK (skip next) transitions
196
- if ((
197
- reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
198
- reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
199
- ) &&
200
- previousMediaItem != null
177
+ player.addListener(
178
+ object : Player.Listener {
179
+ override fun onMediaItemTransition(
180
+ mediaItem: MediaItem?,
181
+ reason: Int,
201
182
  ) {
202
- previousMediaItem?.mediaId?.let { mediaId ->
203
- val trackId = extractTrackId(mediaId)
204
- println("🏁 Track finished/skipped, checking for removal: $trackId")
205
-
206
- // Find and remove from playNext stack (like iOS does)
207
- val playNextIndex = playNextStack.indexOfFirst { it.id == trackId }
208
- if (playNextIndex >= 0) {
209
- val track = playNextStack.removeAt(playNextIndex)
210
- println(" ✅ Removed from playNext stack: ${track.title}")
211
- } else {
212
- // Find and remove from upNext queue
213
- val upNextIndex = upNextQueue.indexOfFirst { it.id == trackId }
214
- if (upNextIndex >= 0) {
215
- val track = upNextQueue.removeAt(upNextIndex)
216
- println(" ✅ Removed from upNext queue: ${track.title}")
183
+ println("\n🔄 onMediaItemTransition called")
184
+ println(
185
+ " reason: ${when (reason) {
186
+ Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -> "AUTO (track ended)"
187
+ Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -> "SEEK"
188
+ Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> "PLAYLIST_CHANGED"
189
+ Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT -> "REPEAT"
190
+ else -> "UNKNOWN($reason)"
191
+ }}",
192
+ )
193
+ println(" previousMediaItem: ${previousMediaItem?.mediaId}")
194
+ println(" new mediaItem: ${mediaItem?.mediaId}")
195
+ println(" playNextStack: ${playNextStack.map { it.id }}")
196
+ println(" upNextQueue: ${upNextQueue.map { it.id }}")
197
+
198
+ // Remove finished track from temporary lists
199
+ // Handle AUTO (natural end) and SEEK (skip next) transitions
200
+ if ((
201
+ reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
202
+ reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
203
+ ) &&
204
+ previousMediaItem != null
205
+ ) {
206
+ previousMediaItem?.mediaId?.let { mediaId ->
207
+ val trackId = extractTrackId(mediaId)
208
+ println("🏁 Track finished/skipped, checking for removal: $trackId")
209
+
210
+ // Find and remove from playNext stack (like iOS does)
211
+ val playNextIndex = playNextStack.indexOfFirst { it.id == trackId }
212
+ if (playNextIndex >= 0) {
213
+ val track = playNextStack.removeAt(playNextIndex)
214
+ println(" ✅ Removed from playNext stack: ${track.title}")
217
215
  } else {
218
- println(" ℹ️ Was an original playlist track")
216
+ // Find and remove from upNext queue
217
+ val upNextIndex = upNextQueue.indexOfFirst { it.id == trackId }
218
+ if (upNextIndex >= 0) {
219
+ val track = upNextQueue.removeAt(upNextIndex)
220
+ println(" ✅ Removed from upNext queue: ${track.title}")
221
+ } else {
222
+ println(" ℹ️ Was an original playlist track")
223
+ }
219
224
  }
220
225
  }
226
+ } else {
227
+ println(" ⏭️ Skipping removal (reason=$reason, prev=${previousMediaItem != null})")
221
228
  }
222
- } else {
223
- println(" ⏭️ Skipping removal (reason=$reason, prev=${previousMediaItem != null})")
224
- }
225
229
 
226
- // Store current item as previous for next transition
227
- previousMediaItem = mediaItem
230
+ // Store current item as previous for next transition
231
+ previousMediaItem = mediaItem
228
232
 
229
- // Update temporary type for current track
230
- currentTemporaryType = determineCurrentTemporaryType()
231
- println(" Updated currentTemporaryType: $currentTemporaryType")
233
+ // Update temporary type for current track
234
+ currentTemporaryType = determineCurrentTemporaryType()
235
+ println(" Updated currentTemporaryType: $currentTemporaryType")
232
236
 
233
- // Update currentTrackIndex when we land on an original playlist track
234
- if (currentTemporaryType == TemporaryType.NONE && mediaItem != null) {
235
- val trackId = extractTrackId(mediaItem.mediaId)
236
- val newIndex = currentTracks.indexOfFirst { it.id == trackId }
237
- if (newIndex >= 0 && newIndex != currentTrackIndex) {
238
- println(" 📍 Updating currentTrackIndex from $currentTrackIndex to $newIndex")
239
- currentTrackIndex = newIndex
237
+ // Update currentTrackIndex when we land on an original playlist track
238
+ if (currentTemporaryType == TemporaryType.NONE && mediaItem != null) {
239
+ val trackId = extractTrackId(mediaItem.mediaId)
240
+ val newIndex = currentTracks.indexOfFirst { it.id == trackId }
241
+ if (newIndex >= 0 && newIndex != currentTrackIndex) {
242
+ println(" 📍 Updating currentTrackIndex from $currentTrackIndex to $newIndex")
243
+ currentTrackIndex = newIndex
244
+ }
240
245
  }
241
- }
242
246
 
243
- // Handle playlist switching if needed
244
- mediaItem?.mediaId?.let { mediaId ->
245
- if (mediaId.contains(':')) {
246
- val colonIndex = mediaId.indexOf(':')
247
- val playlistId = mediaId.substring(0, colonIndex)
248
- if (playlistId != currentPlaylistId) {
249
- // Track from different playlist - ensure playlist is loaded
250
- val playlist = playlistManager.getPlaylist(playlistId)
251
- if (playlist != null && currentPlaylistId != playlistId) {
252
- // This shouldn't happen if playlists are loaded correctly,
253
- // but handle it as a safety measure
254
- println(
255
- "⚠️ TrackPlayerCore: Detected track from different playlist, updating...",
256
- )
247
+ // Handle playlist switching if needed
248
+ mediaItem?.mediaId?.let { mediaId ->
249
+ if (mediaId.contains(':')) {
250
+ val colonIndex = mediaId.indexOf(':')
251
+ val playlistId = mediaId.substring(0, colonIndex)
252
+ if (playlistId != currentPlaylistId) {
253
+ // Track from different playlist - ensure playlist is loaded
254
+ val playlist = playlistManager.getPlaylist(playlistId)
255
+ if (playlist != null && currentPlaylistId != playlistId) {
256
+ // This shouldn't happen if playlists are loaded correctly,
257
+ // but handle it as a safety measure
258
+ println(
259
+ "⚠️ TrackPlayerCore: Detected track from different playlist, updating...",
260
+ )
261
+ }
257
262
  }
258
263
  }
259
264
  }
265
+
266
+ // Use getCurrentTrack() which handles temporary tracks properly
267
+ val track = getCurrentTrack()
268
+ if (track != null) {
269
+ val r =
270
+ when (reason) {
271
+ Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -> Reason.END
272
+ Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -> Reason.USER_ACTION
273
+ Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> Reason.USER_ACTION
274
+ else -> null
275
+ }
276
+ notifyTrackChange(track, r)
277
+ mediaSessionManager?.onTrackChanged()
278
+ }
260
279
  }
261
280
 
262
- // Use getCurrentTrack() which handles temporary tracks properly
263
- val track = getCurrentTrack()
264
- if (track != null) {
281
+ override fun onTimelineChanged(
282
+ timeline: androidx.media3.common.Timeline,
283
+ reason: Int,
284
+ ) {
285
+ if (reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
286
+ // Playlist changed - update MediaBrowserService
287
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
288
+ }
289
+ }
290
+
291
+ override fun onPlayWhenReadyChanged(
292
+ playWhenReady: Boolean,
293
+ reason: Int,
294
+ ) {
265
295
  val r =
266
296
  when (reason) {
267
- Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -> Reason.END
268
- Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -> Reason.USER_ACTION
269
- Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> Reason.USER_ACTION
297
+ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST -> Reason.USER_ACTION
270
298
  else -> null
271
299
  }
272
- notifyTrackChange(track, r)
273
- mediaSessionManager?.onTrackChanged()
300
+ emitStateChange(r)
274
301
  }
275
- }
276
302
 
277
- override fun onTimelineChanged(
278
- timeline: androidx.media3.common.Timeline,
279
- reason: Int,
280
- ) {
281
- if (reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
282
- // Playlist changed - update MediaBrowserService
283
- NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
303
+ override fun onPlaybackStateChanged(playbackState: Int) {
304
+ emitStateChange()
284
305
  }
285
- }
286
-
287
- override fun onPlayWhenReadyChanged(
288
- playWhenReady: Boolean,
289
- reason: Int,
290
- ) {
291
- val r =
292
- when (reason) {
293
- Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST -> Reason.USER_ACTION
294
- else -> null
295
- }
296
- emitStateChange(r)
297
- }
298
306
 
299
- override fun onPlaybackStateChanged(playbackState: Int) {
300
- emitStateChange()
301
- }
307
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
308
+ emitStateChange()
309
+ }
302
310
 
303
- override fun onIsPlayingChanged(isPlaying: Boolean) {
304
- emitStateChange()
305
- }
311
+ override fun onPositionDiscontinuity(
312
+ oldPosition: Player.PositionInfo,
313
+ newPosition: Player.PositionInfo,
314
+ reason: Int,
315
+ ) {
316
+ if (reason == Player.DISCONTINUITY_REASON_SEEK) {
317
+ isManuallySeeked = true
318
+ notifySeek(newPosition.positionMs / 1000.0, player.duration / 1000.0)
319
+ }
320
+ }
306
321
 
307
- override fun onPositionDiscontinuity(
308
- oldPosition: Player.PositionInfo,
309
- newPosition: Player.PositionInfo,
310
- reason: Int,
311
- ) {
312
- if (reason == Player.DISCONTINUITY_REASON_SEEK) {
313
- isManuallySeeked = true
314
- notifySeek(newPosition.positionMs / 1000.0, player.duration / 1000.0)
322
+ override fun onAudioSessionIdChanged(audioSessionId: Int) {
323
+ if (audioSessionId != 0) {
324
+ try {
325
+ EqualizerCore.getInstance(context).initialize(audioSessionId)
326
+ } catch (e: Exception) {
327
+ // Equalizer initialization failed - non-critical
328
+ }
329
+ }
315
330
  }
316
- }
317
- },
318
- )
331
+ },
332
+ )
333
+
334
+ // Start progress updates
335
+ handler.post(progressUpdateRunnable)
336
+ }
319
337
 
320
- // Start progress updates
321
- handler.post(progressUpdateRunnable)
338
+ // Execute on main thread: if already on main thread, run synchronously to avoid deadlock
339
+ if (android.os.Looper.myLooper() == android.os.Looper.getMainLooper()) {
340
+ initRunnable.run()
341
+ } else {
342
+ handler.post(initRunnable)
322
343
  }
323
344
  }
324
345