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.
- package/README.md +69 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +105 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +200 -179
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +486 -0
- package/ios/HybridDownloadManager.swift +147 -145
- package/ios/HybridEqualizer.swift +111 -0
- package/ios/core/TrackPlayerCore.swift +15 -7
- package/ios/download/DownloadDatabase.swift +390 -380
- package/ios/download/DownloadFileManager.swift +183 -167
- package/ios/download/DownloadManagerCore.swift +786 -749
- package/ios/equalizer/EqualizerCore.swift +685 -0
- package/lib/hooks/equalizerCallbackManager.d.ts +37 -0
- package/lib/hooks/equalizerCallbackManager.js +109 -0
- package/lib/hooks/index.d.ts +4 -0
- package/lib/hooks/index.js +3 -0
- package/lib/hooks/useEqualizer.d.ts +25 -0
- package/lib/hooks/useEqualizer.js +124 -0
- package/lib/hooks/useEqualizerPresets.d.ts +22 -0
- package/lib/hooks/useEqualizerPresets.js +96 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/specs/Equalizer.nitro.d.ts +43 -0
- package/lib/specs/Equalizer.nitro.js +1 -0
- package/lib/types/EqualizerTypes.d.ts +52 -0
- package/lib/types/EqualizerTypes.js +1 -0
- package/nitro.json +4 -0
- package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +2 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +16 -2
- package/nitrogen/generated/android/c++/JEqualizerBand.hpp +69 -0
- package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +78 -0
- package/nitrogen/generated/android/c++/JEqualizerState.hpp +91 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +81 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +97 -0
- package/nitrogen/generated/android/c++/JGainRange.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +204 -0
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +82 -0
- package/nitrogen/generated/android/c++/JPresetType.hpp +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerBand.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerPreset.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerState.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__optional_std__variant_nitro__NullType__std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_EqualizerBand_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/GainRange.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +141 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PresetType.kt +21 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +41 -8
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +167 -22
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +20 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +8 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +15 -0
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +223 -0
- package/nitrogen/generated/ios/swift/EqualizerBand.swift +69 -0
- package/nitrogen/generated/ios/swift/EqualizerPreset.swift +70 -0
- package/nitrogen/generated/ios/swift/EqualizerState.swift +115 -0
- package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_nitro__NullType__std__string__.swift +66 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +47 -0
- package/nitrogen/generated/ios/swift/GainRange.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +73 -0
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +396 -0
- package/nitrogen/generated/ios/swift/PresetType.swift +40 -0
- package/nitrogen/generated/shared/c++/EqualizerBand.hpp +87 -0
- package/nitrogen/generated/shared/c++/EqualizerPreset.hpp +86 -0
- package/nitrogen/generated/shared/c++/EqualizerState.hpp +89 -0
- package/nitrogen/generated/shared/c++/GainRange.hpp +79 -0
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.cpp +38 -0
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +95 -0
- package/nitrogen/generated/shared/c++/PresetType.hpp +76 -0
- package/package.json +1 -1
- package/src/hooks/equalizerCallbackManager.ts +138 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useEqualizer.ts +173 -0
- package/src/hooks/useEqualizerPresets.ts +140 -0
- package/src/index.ts +6 -0
- package/src/specs/Equalizer.nitro.ts +69 -0
- 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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
// Set references for MediaBrowserService
|
|
157
|
+
NitroPlayerMediaBrowserService.trackPlayerCore = this
|
|
158
|
+
NitroPlayerMediaBrowserService.mediaSessionManager = mediaSessionManager
|
|
155
159
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
168
|
+
// Notify JavaScript
|
|
169
|
+
onAndroidAutoConnectionChange?.invoke(connected)
|
|
166
170
|
|
|
167
|
-
|
|
171
|
+
println("🚗 Android Auto connection changed: connected=$connected, type=$connectionType")
|
|
172
|
+
}
|
|
168
173
|
}
|
|
174
|
+
registerCarConnectionReceiver()
|
|
169
175
|
}
|
|
170
|
-
registerCarConnectionReceiver()
|
|
171
|
-
}
|
|
172
176
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
230
|
+
// Store current item as previous for next transition
|
|
231
|
+
previousMediaItem = mediaItem
|
|
228
232
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
233
|
+
// Update temporary type for current track
|
|
234
|
+
currentTemporaryType = determineCurrentTemporaryType()
|
|
235
|
+
println(" Updated currentTemporaryType: $currentTemporaryType")
|
|
232
236
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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.
|
|
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
|
-
|
|
273
|
-
mediaSessionManager?.onTrackChanged()
|
|
300
|
+
emitStateChange(r)
|
|
274
301
|
}
|
|
275
|
-
}
|
|
276
302
|
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
307
|
+
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
308
|
+
emitStateChange()
|
|
309
|
+
}
|
|
302
310
|
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
|