react-native-nitro-player 0.3.0-alpha.6 → 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.
package/README.md CHANGED
@@ -1 +1,699 @@
1
- Refer Root readme
1
+ # React Native Nitro Player
2
+
3
+ A powerful audio player library for React Native with playlist management, playback controls, and support for Android Auto and CarPlay.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install react-native-nitro-player
9
+ # or
10
+ yarn add react-native-nitro-player
11
+ ```
12
+
13
+ ### Peer Dependencies
14
+
15
+ Make sure you have these installed:
16
+
17
+ ```bash
18
+ npm install react-native-nitro-modules
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### 1. Configure the Player
24
+
25
+ Configure the player before using it in your app:
26
+
27
+ ```typescript
28
+ import { TrackPlayer } from 'react-native-nitro-player'
29
+
30
+ TrackPlayer.configure({
31
+ androidAutoEnabled: true,
32
+ carPlayEnabled: false,
33
+ showInNotification: true,
34
+ })
35
+ ```
36
+
37
+ ### 2. Create Playlists
38
+
39
+ ```typescript
40
+ import { PlayerQueue } from 'react-native-nitro-player'
41
+ import type { TrackItem } from 'react-native-nitro-player'
42
+
43
+ const tracks: TrackItem[] = [
44
+ {
45
+ id: '1',
46
+ title: 'Song Title',
47
+ artist: 'Artist Name',
48
+ album: 'Album Name',
49
+ duration: 180.0, // in seconds
50
+ url: 'https://example.com/song.mp3',
51
+ artwork: 'https://example.com/artwork.jpg',
52
+ },
53
+ ]
54
+
55
+ // Create a playlist
56
+ const playlistId = PlayerQueue.createPlaylist(
57
+ 'My Playlist',
58
+ 'Playlist description',
59
+ 'https://example.com/playlist-artwork.jpg'
60
+ )
61
+
62
+ // Add tracks to the playlist
63
+ PlayerQueue.addTracksToPlaylist(playlistId, tracks)
64
+ ```
65
+
66
+ ### 3. Play Music
67
+
68
+ ```typescript
69
+ import { TrackPlayer, PlayerQueue } from 'react-native-nitro-player'
70
+
71
+ // Load and play a playlist
72
+ PlayerQueue.loadPlaylist(playlistId)
73
+
74
+ // Or play a specific song
75
+ TrackPlayer.playSong('song-id', playlistId)
76
+
77
+ // Basic controls
78
+ TrackPlayer.play()
79
+ TrackPlayer.pause()
80
+ TrackPlayer.skipToNext()
81
+ TrackPlayer.skipToPrevious()
82
+ TrackPlayer.seek(30) // Seek to 30 seconds
83
+
84
+ // Set repeat mode
85
+ TrackPlayer.setRepeatMode('off') // No repeat
86
+ TrackPlayer.setRepeatMode('Playlist') // Repeat entire playlist
87
+ TrackPlayer.setRepeatMode('track') // Repeat current track
88
+
89
+ // Set volume (0-100)
90
+ TrackPlayer.setVolume(50) // Set volume to 50%
91
+ TrackPlayer.setVolume(0) // Mute
92
+ TrackPlayer.setVolume(100) // Maximum volume
93
+ ```
94
+
95
+ ## Core Concepts
96
+
97
+ ### PlayerQueue
98
+
99
+ Manages playlists and tracks. Use it to:
100
+
101
+ - Create, update, and delete playlists
102
+ - Add or remove tracks from playlists
103
+ - Load playlists for playback
104
+ - Listen to playlist changes
105
+
106
+ ### TrackPlayer
107
+
108
+ Controls playback. Use it to:
109
+
110
+ - Play, pause, and seek
111
+ - Skip tracks
112
+ - Control repeat mode
113
+ - Control volume
114
+ - Get current player state
115
+ - Listen to playback events
116
+
117
+ ## React Hooks
118
+
119
+ The library provides React hooks for reactive state management. These hooks automatically update your components when player state changes.
120
+
121
+ ### `useOnChangeTrack()`
122
+
123
+ Returns the current track and the reason why it changed.
124
+
125
+ **Returns:**
126
+
127
+ - `track: TrackItem | undefined` - The current track, or `undefined` if no track is playing
128
+ - `reason: Reason | undefined` - The reason for the track change (`'user_action'`, `'skip'`, `'end'`, or `'error'`)
129
+
130
+ ### `useOnPlaybackStateChange()`
131
+
132
+ Returns the current playback state and the reason for the state change.
133
+
134
+ **Returns:**
135
+
136
+ - `state: TrackPlayerState | undefined` - Current playback state (`'playing'`, `'paused'`, or `'stopped'`)
137
+ - `reason: Reason | undefined` - The reason for the state change
138
+
139
+ ### `useOnPlaybackProgressChange()`
140
+
141
+ Returns real-time playback progress updates.
142
+
143
+ **Returns:**
144
+
145
+ - `position: number` - Current playback position in seconds
146
+ - `totalDuration: number` - Total duration of the current track in seconds
147
+ - `isManuallySeeked: boolean | undefined` - `true` if the user manually seeked, `undefined` otherwise
148
+
149
+ ### `useOnSeek()`
150
+
151
+ Returns information about the last seek event.
152
+
153
+ **Returns:**
154
+
155
+ - `position: number | undefined` - The position where the user seeked to, or `undefined` if no seek has occurred
156
+ - `totalDuration: number | undefined` - The total duration at the time of seek, or `undefined` if no seek has occurred
157
+
158
+ ### `useAndroidAutoConnection()`
159
+
160
+ Monitors Android Auto connection status.
161
+
162
+ **Returns:**
163
+
164
+ - `isConnected: boolean` - `true` if connected to Android Auto, `false` otherwise
165
+
166
+ ### `useAudioDevices()` (Android only)
167
+
168
+ Automatically polls for audio device changes every 2 seconds.
169
+
170
+ **Returns:**
171
+
172
+ - `devices: TAudioDevice[]` - Array of available audio devices
173
+
174
+ ## Audio Device APIs
175
+
176
+ ### `AudioDevices` (Android only)
177
+
178
+ Android-specific API for managing audio output devices.
179
+
180
+ #### `getAudioDevices(): TAudioDevice[]`
181
+
182
+ Returns the list of available audio output devices.
183
+
184
+ **Returns:** Array of `TAudioDevice` objects with:
185
+
186
+ - `id: number` - Unique device ID
187
+ - `name: string` - Device name (e.g., "Built-in Speaker", "Bluetooth")
188
+ - `type: number` - Device type constant
189
+ - `isActive: boolean` - Whether this device is currently active
190
+
191
+ **Example:**
192
+
193
+ ```typescript
194
+ import { AudioDevices } from 'react-native-nitro-player'
195
+
196
+ if (AudioDevices) {
197
+ const devices = AudioDevices.getAudioDevices()
198
+ devices.forEach(device => {
199
+ console.log(`${device.name} - Active: ${device.isActive}`)
200
+ })
201
+ }
202
+ ```
203
+
204
+ #### `setAudioDevice(deviceId: number): boolean`
205
+
206
+ Sets the active audio output device.
207
+
208
+ **Parameters:**
209
+
210
+ - `deviceId: number` - The ID of the device to activate
211
+
212
+ **Returns:** `true` if successful, `false` otherwise
213
+
214
+ **Example:**
215
+
216
+ ```typescript
217
+ import { AudioDevices } from 'react-native-nitro-player'
218
+
219
+ if (AudioDevices) {
220
+ const success = AudioDevices.setAudioDevice(deviceId)
221
+ console.log(`Device switch: ${success ? 'success' : 'failed'}`)
222
+ }
223
+ ```
224
+
225
+ ### `AudioRoutePicker` (iOS only)
226
+
227
+ iOS-specific API for displaying the native audio route picker (AirPlay menu).
228
+
229
+ #### `showRoutePicker(): void`
230
+
231
+ Shows the native AVRoutePickerView for selecting audio output routes like AirPlay, Bluetooth, etc.
232
+
233
+ **Example:**
234
+
235
+ ```typescript
236
+ import { AudioRoutePicker } from 'react-native-nitro-player'
237
+
238
+ if (AudioRoutePicker) {
239
+ AudioRoutePicker.showRoutePicker()
240
+ }
241
+ ```
242
+
243
+ ## Repeat Mode
244
+
245
+ Control how tracks repeat during playback.
246
+
247
+ ### `setRepeatMode(mode: RepeatMode): boolean`
248
+
249
+ Sets the repeat mode for the player.
250
+
251
+ **Parameters:**
252
+
253
+ - `mode: 'off' | 'Playlist' | 'track'` - The repeat mode to set
254
+ - `'off'` - No repeat, playlist stops at the end
255
+ - `'Playlist'` - Repeat the entire playlist
256
+ - `'track'` - Repeat the current track only
257
+
258
+ **Returns:** `true` if successful, `false` otherwise
259
+
260
+ **Example:**
261
+
262
+ ```typescript
263
+ import { TrackPlayer } from 'react-native-nitro-player'
264
+
265
+ // Turn off repeat
266
+ TrackPlayer.setRepeatMode('off')
267
+
268
+ // Repeat entire playlist
269
+ TrackPlayer.setRepeatMode('Playlist')
270
+
271
+ // Repeat current track
272
+ TrackPlayer.setRepeatMode('track')
273
+ ```
274
+
275
+ ## Volume Control
276
+
277
+ Control the playback volume level.
278
+
279
+ ### `setVolume(volume: number): boolean`
280
+
281
+ Sets the playback volume level.
282
+
283
+ **Parameters:**
284
+
285
+ - `volume: number` - Volume level between 0 and 100
286
+ - `0` - Mute (no sound)
287
+ - `50` - Half volume
288
+ - `100` - Maximum volume
289
+
290
+ **Returns:** `true` if successful, `false` otherwise (e.g., if player is not initialized)
291
+
292
+ **Example:**
293
+
294
+ ```typescript
295
+ import { TrackPlayer } from 'react-native-nitro-player'
296
+
297
+ // Set volume to 50%
298
+ const success = TrackPlayer.setVolume(50)
299
+ if (success) {
300
+ console.log('Volume set successfully')
301
+ } else {
302
+ console.warn('Failed to set volume')
303
+ }
304
+
305
+ // Mute the player
306
+ TrackPlayer.setVolume(0)
307
+
308
+ // Set to maximum volume
309
+ TrackPlayer.setVolume(100)
310
+
311
+ // Incremental volume control
312
+ const currentVolume = 50
313
+ TrackPlayer.setVolume(currentVolume + 10) // Increase by 10%
314
+ TrackPlayer.setVolume(currentVolume - 10) // Decrease by 10%
315
+ ```
316
+
317
+ **Note:** The volume value is automatically clamped to the 0-100 range. Values outside this range will be clamped to the nearest valid value.
318
+
319
+ ## Usage Examples
320
+
321
+ ### Using React Hooks
322
+
323
+ The library provides convenient React hooks for reactive state management:
324
+
325
+ ```typescript
326
+ import {
327
+ useOnChangeTrack,
328
+ useOnPlaybackStateChange,
329
+ useOnPlaybackProgressChange,
330
+ useOnSeek,
331
+ useAndroidAutoConnection,
332
+ } from 'react-native-nitro-player'
333
+
334
+ function PlayerComponent() {
335
+ // Get current track
336
+ const { track, reason } = useOnChangeTrack()
337
+
338
+ // Get playback state (playing, paused, stopped)
339
+ const { state, reason: stateReason } = useOnPlaybackStateChange()
340
+
341
+ // Get playback progress
342
+ const { position, totalDuration, isManuallySeeked } = useOnPlaybackProgressChange()
343
+
344
+ // Get seek events
345
+ const { position: seekPosition, totalDuration: seekDuration } = useOnSeek()
346
+
347
+ // Check Android Auto connection
348
+ const { isConnected } = useAndroidAutoConnection()
349
+
350
+ return (
351
+ <View>
352
+ {track && (
353
+ <Text>Now Playing: {track.title} by {track.artist}</Text>
354
+ )}
355
+ <Text>State: {state}</Text>
356
+ <Text>Progress: {position} / {totalDuration}</Text>
357
+ </View>
358
+ )
359
+ }
360
+ ```
361
+
362
+ ### Managing Playlists
363
+
364
+ ```typescript
365
+ import { PlayerQueue } from 'react-native-nitro-player'
366
+ import type { TrackItem, Playlist } from 'react-native-nitro-player'
367
+
368
+ // Get all playlists
369
+ const playlists = PlayerQueue.getAllPlaylists()
370
+
371
+ // Get a specific playlist
372
+ const playlist = PlayerQueue.getPlaylist(playlistId)
373
+
374
+ // Get current playing playlist
375
+ const currentPlaylistId = PlayerQueue.getCurrentPlaylistId()
376
+
377
+ // Update playlist metadata
378
+ PlayerQueue.updatePlaylist(playlistId, {
379
+ name: 'Updated Name',
380
+ description: 'New description',
381
+ artwork: 'https://example.com/new-artwork.jpg',
382
+ })
383
+
384
+ // Add a single track
385
+ PlayerQueue.addTrackToPlaylist(playlistId, newTrack)
386
+
387
+ // Add multiple tracks
388
+ PlayerQueue.addTracksToPlaylist(playlistId, [track1, track2, track3])
389
+
390
+ // Remove a track
391
+ PlayerQueue.removeTrackFromPlaylist(playlistId, trackId)
392
+
393
+ // Reorder tracks
394
+ PlayerQueue.reorderTrackInPlaylist(playlistId, trackId, newIndex)
395
+
396
+ // Delete a playlist
397
+ PlayerQueue.deletePlaylist(playlistId)
398
+ ```
399
+
400
+ ### Listening to Events
401
+
402
+ ```typescript
403
+ import { PlayerQueue, TrackPlayer } from 'react-native-nitro-player'
404
+
405
+ // Listen to playlist changes
406
+ PlayerQueue.onPlaylistsChanged((playlists, operation) => {
407
+ console.log('Playlists updated:', operation)
408
+ // operation can be: 'add', 'remove', 'clear', 'update'
409
+ })
410
+
411
+ // Listen to specific playlist changes
412
+ PlayerQueue.onPlaylistChanged((playlistId, playlist, operation) => {
413
+ console.log('Playlist changed:', playlistId, operation)
414
+ })
415
+
416
+ // Listen to track changes
417
+ TrackPlayer.onChangeTrack((track, reason) => {
418
+ console.log('Track changed:', track.title, reason)
419
+ // reason can be: 'user_action', 'skip', 'end', 'error'
420
+ })
421
+
422
+ // Listen to playback state changes
423
+ TrackPlayer.onPlaybackStateChange((state, reason) => {
424
+ console.log('State changed:', state, reason)
425
+ })
426
+
427
+ // Listen to seek events
428
+ TrackPlayer.onSeek((position, totalDuration) => {
429
+ console.log('Seeked to:', position)
430
+ })
431
+
432
+ // Listen to playback progress
433
+ TrackPlayer.onPlaybackProgressChange(
434
+ (position, totalDuration, isManuallySeeked) => {
435
+ console.log('Progress:', position, '/', totalDuration)
436
+ }
437
+ )
438
+
439
+ // Listen to Android Auto connection changes
440
+ TrackPlayer.onAndroidAutoConnectionChange(connected => {
441
+ console.log('Android Auto:', connected ? 'Connected' : 'Disconnected')
442
+ })
443
+ ```
444
+
445
+ ### Getting Player State
446
+
447
+ ```typescript
448
+ import { TrackPlayer } from 'react-native-nitro-player'
449
+
450
+ const state = TrackPlayer.getState()
451
+
452
+ console.log(state.currentState) // 'playing' | 'paused' | 'stopped'
453
+ console.log(state.currentPosition) // current position in seconds
454
+ console.log(state.totalDuration) // total duration in seconds
455
+ console.log(state.currentTrack) // current TrackItem or null
456
+ console.log(state.currentPlaylistId) // current playlist ID or null
457
+ console.log(state.currentIndex) // current track index in playlist
458
+ ```
459
+
460
+ ## Track Item Structure
461
+
462
+ Each track must follow this structure:
463
+
464
+ ```typescript
465
+ interface TrackItem {
466
+ id: string // Unique identifier
467
+ title: string // Track title
468
+ artist: string // Artist name
469
+ album: string // Album name
470
+ duration: number // Duration in seconds
471
+ url: string // Audio file URL
472
+ artwork?: string | null // Optional artwork URL
473
+ }
474
+ ```
475
+
476
+ ## Playlist Structure
477
+
478
+ ```typescript
479
+ interface Playlist {
480
+ id: string // Unique identifier
481
+ name: string // Playlist name
482
+ description?: string | null // Optional description
483
+ artwork?: string | null // Optional artwork URL
484
+ tracks: TrackItem[] // Array of tracks
485
+ }
486
+ ```
487
+
488
+ ## Android Auto Customization
489
+
490
+ Customize how your music library appears in Android Auto with a custom folder structure.
491
+
492
+ ### Basic Setup
493
+
494
+ By default, all playlists are shown in Android Auto. You can create a custom structure:
495
+
496
+ ```typescript
497
+ import { AndroidAutoMediaLibraryHelper } from 'react-native-nitro-player'
498
+ import type { MediaLibrary } from 'react-native-nitro-player'
499
+
500
+ // Check if available (Android only)
501
+ if (AndroidAutoMediaLibraryHelper.isAvailable()) {
502
+ const mediaLibrary: MediaLibrary = {
503
+ layoutType: 'grid', // 'grid' or 'list'
504
+ rootItems: [
505
+ {
506
+ id: 'my_music',
507
+ title: '🎵 My Music',
508
+ subtitle: 'Your music collection',
509
+ mediaType: 'folder',
510
+ isPlayable: false,
511
+ layoutType: 'grid',
512
+ children: [
513
+ {
514
+ id: 'favorites',
515
+ title: 'Favorites',
516
+ subtitle: '10 tracks',
517
+ mediaType: 'playlist',
518
+ playlistId: 'my-playlist-id', // References a playlist created with PlayerQueue
519
+ isPlayable: false,
520
+ },
521
+ ],
522
+ },
523
+ {
524
+ id: 'recent',
525
+ title: '🕐 Recently Played',
526
+ mediaType: 'folder',
527
+ isPlayable: false,
528
+ children: [
529
+ // More playlist references...
530
+ ],
531
+ },
532
+ ],
533
+ }
534
+
535
+ AndroidAutoMediaLibraryHelper.set(mediaLibrary)
536
+ }
537
+
538
+ // Reset to default (show all playlists)
539
+ AndroidAutoMediaLibraryHelper.clear()
540
+ ```
541
+
542
+ ### MediaLibrary Structure
543
+
544
+ ```typescript
545
+ interface MediaLibrary {
546
+ layoutType: 'grid' | 'list' // Default layout for items
547
+ rootItems: MediaItem[] // Top-level items
548
+ appName?: string // Optional app name
549
+ appIconUrl?: string // Optional app icon
550
+ }
551
+
552
+ interface MediaItem {
553
+ id: string // Unique identifier
554
+ title: string // Display title
555
+ subtitle?: string // Optional subtitle
556
+ iconUrl?: string // Optional icon/artwork URL
557
+ isPlayable: boolean // Whether item can be played
558
+ mediaType: 'folder' | 'audio' | 'playlist' // Type of item
559
+ playlistId?: string // Reference to playlist (for playlist items)
560
+ children?: MediaItem[] // Child items (for folders)
561
+ layoutType?: 'grid' | 'list' // Override default layout
562
+ }
563
+ ```
564
+
565
+ ### Example: Organizing Playlists by Genre
566
+
567
+ ```typescript
568
+ import {
569
+ PlayerQueue,
570
+ AndroidAutoMediaLibraryHelper,
571
+ } from 'react-native-nitro-player'
572
+
573
+ // Create playlists first
574
+ const rockPlaylistId = PlayerQueue.createPlaylist('Rock Classics')
575
+ const jazzPlaylistId = PlayerQueue.createPlaylist('Jazz Essentials')
576
+ const popPlaylistId = PlayerQueue.createPlaylist('Pop Hits')
577
+
578
+ // Add tracks to playlists...
579
+ PlayerQueue.addTracksToPlaylist(rockPlaylistId, rockTracks)
580
+ PlayerQueue.addTracksToPlaylist(jazzPlaylistId, jazzTracks)
581
+ PlayerQueue.addTracksToPlaylist(popPlaylistId, popTracks)
582
+
583
+ // Create custom Android Auto structure
584
+ AndroidAutoMediaLibraryHelper.set({
585
+ layoutType: 'list',
586
+ rootItems: [
587
+ {
588
+ id: 'genres',
589
+ title: '🎸 By Genre',
590
+ mediaType: 'folder',
591
+ isPlayable: false,
592
+ layoutType: 'grid',
593
+ children: [
594
+ {
595
+ id: 'rock',
596
+ title: 'Rock',
597
+ mediaType: 'playlist',
598
+ playlistId: rockPlaylistId,
599
+ isPlayable: false,
600
+ },
601
+ {
602
+ id: 'jazz',
603
+ title: 'Jazz',
604
+ mediaType: 'playlist',
605
+ playlistId: jazzPlaylistId,
606
+ isPlayable: false,
607
+ },
608
+ {
609
+ id: 'pop',
610
+ title: 'Pop',
611
+ mediaType: 'playlist',
612
+ playlistId: popPlaylistId,
613
+ isPlayable: false,
614
+ },
615
+ ],
616
+ },
617
+ {
618
+ id: 'all_music',
619
+ title: '📀 All Music',
620
+ mediaType: 'folder',
621
+ isPlayable: false,
622
+ children: [
623
+ {
624
+ id: 'all_rock',
625
+ title: 'Rock Classics',
626
+ mediaType: 'playlist',
627
+ playlistId: rockPlaylistId,
628
+ isPlayable: false,
629
+ },
630
+ {
631
+ id: 'all_jazz',
632
+ title: 'Jazz Essentials',
633
+ mediaType: 'playlist',
634
+ playlistId: jazzPlaylistId,
635
+ isPlayable: false,
636
+ },
637
+ {
638
+ id: 'all_pop',
639
+ title: 'Pop Hits',
640
+ mediaType: 'playlist',
641
+ playlistId: popPlaylistId,
642
+ isPlayable: false,
643
+ },
644
+ ],
645
+ },
646
+ ],
647
+ })
648
+ ```
649
+
650
+ ### Notes
651
+
652
+ - The `playlistId` field must reference a playlist created with `PlayerQueue.createPlaylist()`
653
+ - Changes are immediately reflected in Android Auto
654
+ - Use folders to organize playlists hierarchically
655
+ - Grid layout is best for album/playlist browsing
656
+ - List layout is best for song lists
657
+ - Only available on Android (use `isAvailable()` to check)
658
+
659
+ ## Features
660
+
661
+ - ✅ **Playlist Management**: Create, update, and manage multiple playlists
662
+ - ✅ **Playback Controls**: Play, pause, seek, skip tracks
663
+ - ✅ **Volume Control**: Adjust playback volume (0-100)
664
+ - ✅ **React Hooks**: Built-in hooks for reactive state management
665
+ - ✅ **Event Listeners**: Listen to track changes, state changes, and more
666
+ - ✅ **Android Auto Support**: Control playback from Android Auto with customizable UI
667
+ - ✅ **CarPlay Support**: Control playback from CarPlay (iOS)
668
+ - ✅ **Notification Controls**: Show playback controls in notifications
669
+ - ✅ **Progress Tracking**: Real-time playback progress updates
670
+
671
+ ## TypeScript Support
672
+
673
+ The library is written in TypeScript and includes full type definitions. All types are exported for your convenience:
674
+
675
+ ```typescript
676
+ import type {
677
+ TrackItem,
678
+ Playlist,
679
+ PlayerState,
680
+ TrackPlayerState,
681
+ QueueOperation,
682
+ Reason,
683
+ PlayerConfig,
684
+ MediaLibrary,
685
+ MediaItem,
686
+ LayoutType,
687
+ MediaType,
688
+ } from 'react-native-nitro-player'
689
+ ```
690
+
691
+ ## Platform Support
692
+
693
+ - ✅ **iOS**: Full support with CarPlay integration
694
+ - ✅ **Android**: Full support with Android Auto integration
695
+ - 🎯 **Android Auto Media Library**: Android-only feature for customizing the Android Auto UI
696
+
697
+ ## License
698
+
699
+ MIT
@@ -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)
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "0.3.0-alpha.6",
3
+ "version": "0.3.0-alpha.7",
4
4
  "description": "react-native-nitro-player",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",
@@ -30,13 +30,14 @@
30
30
  "scripts": {
31
31
  "postinstall": "tsc || exit 0;",
32
32
  "typecheck": "tsc --noEmit",
33
- "clean": "rm -rf android/build node_modules/**/android/build lib nitrogen",
33
+ "clean": "rm -rf android/build node_modules/**/android/build lib nitrogen tsconfig.tsbuildinfo",
34
34
  "lint": "eslint \"**/*.{js,ts,tsx}\" --fix",
35
35
  "lint-ci": "eslint \"**/*.{js,ts,tsx}\" -f @jamesacarr/github-actions",
36
36
  "typescript": "tsc",
37
- "specs": "tsc --noEmit false && nitrogen --logLevel=\"debug\"",
37
+ "specs": "tsc && nitrogen --logLevel=\"debug\"",
38
+ "copy-readme": "cp ../README.md README.md",
38
39
  "release": "release-it",
39
- "build": "bun run clean && bun run specs && bun run typescript"
40
+ "build": "bun run clean && bun run specs && bun run typescript && bun run copy-readme"
40
41
  },
41
42
  "keywords": [
42
43
  "react-native",
@@ -123,6 +124,9 @@
123
124
  "github": {
124
125
  "release": true
125
126
  },
127
+ "hooks": {
128
+ "before:release": "bun run clean && bun run specs && bun run typescript && bun run copy-readme"
129
+ },
126
130
  "plugins": {
127
131
  "@release-it/bumper": {
128
132
  "in": "package.json",
@@ -81,4 +81,5 @@ export interface TrackPlayer
81
81
  ): void
82
82
  onAndroidAutoConnectionChange(callback: (connected: boolean) => void): void
83
83
  isAndroidAutoConnected(): boolean
84
+ setVolume(volume: number): boolean
84
85
  }