react-native-audio-browser 0.2.0-next.7 → 0.2.0-next.9

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 (165) hide show
  1. package/android/build.gradle +7 -1
  2. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +33 -8
  3. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +46 -0
  4. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +1 -2
  5. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +1 -2
  6. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +6 -7
  7. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +0 -1
  8. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +2 -4
  9. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
  10. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
  11. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -7
  12. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +68 -0
  13. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +0 -1
  14. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +2 -1
  15. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +2 -4
  16. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
  17. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
  18. package/ios/Browser/BrowserManager.swift +2 -2
  19. package/ios/Browser/SearchDrillIn.swift +1 -1
  20. package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
  21. package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
  22. package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
  23. package/ios/HybridAudioBrowser.swift +86 -55
  24. package/ios/Model/RemoteCommand.swift +18 -0
  25. package/ios/Model/TrackPlayerError.swift +2 -2
  26. package/ios/Option/SessionCategories.swift +3 -2
  27. package/ios/Player/MediaIntentCriteria.swift +4 -4
  28. package/ios/Player/PlaybackErrorHandler.swift +2 -2
  29. package/ios/Player/RetryManager.swift +2 -1
  30. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
  31. package/ios/RemoteCommand/RemoteCommandController.swift +9 -6
  32. package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
  33. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  34. package/ios/Tests/PlaybackCoordinatorTests.swift +1 -1
  35. package/ios/Tests/RemoteCommandControllerTests.swift +156 -0
  36. package/ios/Tests/RemoteCommandTests.swift +67 -0
  37. package/ios/Tests/SectionScopeTests.swift +2 -2
  38. package/ios/Tests/TabBarEntriesTests.swift +7 -7
  39. package/ios/TrackPlayer.swift +1 -1
  40. package/ios/TrackPlayerCallbacks.swift +5 -38
  41. package/lib/commonjs/AudioBrowser.js +0 -11
  42. package/lib/commonjs/AudioBrowser.js.map +1 -1
  43. package/lib/commonjs/features/carConnection.js +1 -1
  44. package/lib/commonjs/features/equalizer.js +1 -1
  45. package/lib/commonjs/features/errors.js +3 -3
  46. package/lib/commonjs/features/favorites.js +1 -1
  47. package/lib/commonjs/features/gate.js +1 -1
  48. package/lib/commonjs/features/metadata.js +3 -3
  49. package/lib/commonjs/features/network.js +1 -1
  50. package/lib/commonjs/features/nowPlaying.js +1 -1
  51. package/lib/commonjs/features/output.js +1 -1
  52. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  53. package/lib/commonjs/features/playback/playing.js +1 -1
  54. package/lib/commonjs/features/playback/progress.js +1 -1
  55. package/lib/commonjs/features/playback/state.js +1 -1
  56. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  57. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  58. package/lib/commonjs/features/playback/volume.js +1 -1
  59. package/lib/commonjs/features/player/options.js +3 -19
  60. package/lib/commonjs/features/player/options.js.map +1 -1
  61. package/lib/commonjs/features/player/setup.js +2 -2
  62. package/lib/commonjs/features/player/setup.js.map +1 -1
  63. package/lib/commonjs/features/player/validateOptions.js +24 -0
  64. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  65. package/lib/commonjs/features/queue/activeTrack.js +1 -1
  66. package/lib/commonjs/features/queue/queue.js +2 -2
  67. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  68. package/lib/commonjs/features/queue/shuffle.js +1 -1
  69. package/lib/commonjs/features/remoteControls.js +13 -92
  70. package/lib/commonjs/features/remoteControls.js.map +1 -1
  71. package/lib/commonjs/features/sleepTimer.js +1 -1
  72. package/lib/commonjs/utils/LazyNativeEmitter.js +8 -5
  73. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  74. package/lib/module/AudioBrowser.js +4 -1
  75. package/lib/module/AudioBrowser.js.map +1 -1
  76. package/lib/module/features/carConnection.js +1 -1
  77. package/lib/module/features/equalizer.js +1 -1
  78. package/lib/module/features/errors.js +3 -3
  79. package/lib/module/features/favorites.js +1 -1
  80. package/lib/module/features/gate.js +1 -1
  81. package/lib/module/features/metadata.js +3 -3
  82. package/lib/module/features/network.js +1 -1
  83. package/lib/module/features/nowPlaying.js +1 -1
  84. package/lib/module/features/output.js +1 -1
  85. package/lib/module/features/playback/playWhenReady.js +1 -1
  86. package/lib/module/features/playback/playing.js +1 -1
  87. package/lib/module/features/playback/progress.js +1 -1
  88. package/lib/module/features/playback/state.js +1 -1
  89. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  90. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  91. package/lib/module/features/playback/volume.js +1 -1
  92. package/lib/module/features/player/options.js +2 -17
  93. package/lib/module/features/player/options.js.map +1 -1
  94. package/lib/module/features/player/setup.js +1 -1
  95. package/lib/module/features/player/setup.js.map +1 -1
  96. package/lib/module/features/player/validateOptions.js +20 -0
  97. package/lib/module/features/player/validateOptions.js.map +1 -0
  98. package/lib/module/features/queue/activeTrack.js +1 -1
  99. package/lib/module/features/queue/queue.js +2 -2
  100. package/lib/module/features/queue/repeatMode.js +1 -1
  101. package/lib/module/features/queue/shuffle.js +1 -1
  102. package/lib/module/features/remoteControls.js +13 -92
  103. package/lib/module/features/remoteControls.js.map +1 -1
  104. package/lib/module/features/sleepTimer.js +1 -1
  105. package/lib/module/utils/LazyNativeEmitter.js +8 -5
  106. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  107. package/lib/typescript/src/AudioBrowser.d.ts +0 -1
  108. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  109. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  110. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  111. package/lib/typescript/src/features/errors.d.ts +3 -3
  112. package/lib/typescript/src/features/favorites.d.ts +1 -1
  113. package/lib/typescript/src/features/gate.d.ts +3 -2
  114. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  115. package/lib/typescript/src/features/metadata.d.ts +3 -3
  116. package/lib/typescript/src/features/network.d.ts +1 -1
  117. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  118. package/lib/typescript/src/features/output.d.ts +1 -1
  119. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  120. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  121. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  122. package/lib/typescript/src/features/playback/state.d.ts +1 -1
  123. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  124. package/lib/typescript/src/features/player/options.d.ts +1 -1
  125. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  126. package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
  127. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  128. package/lib/typescript/src/features/queue/activeTrack.d.ts +1 -1
  129. package/lib/typescript/src/features/queue/queue.d.ts +2 -2
  130. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  131. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  132. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  133. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  134. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  135. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +8 -4
  136. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  137. package/package.json +3 -1
  138. package/src/AudioBrowser.ts +4 -1
  139. package/src/features/carConnection.ts +1 -1
  140. package/src/features/equalizer.ts +1 -1
  141. package/src/features/errors.ts +3 -3
  142. package/src/features/favorites.ts +1 -1
  143. package/src/features/gate.ts +1 -1
  144. package/src/features/metadata.ts +3 -3
  145. package/src/features/network.ts +1 -1
  146. package/src/features/nowPlaying.ts +1 -1
  147. package/src/features/output.ts +1 -1
  148. package/src/features/playback/playWhenReady.ts +1 -1
  149. package/src/features/playback/playing.ts +1 -1
  150. package/src/features/playback/progress.ts +1 -1
  151. package/src/features/playback/state.ts +1 -1
  152. package/src/features/playback/trackPlaybackTime.test.ts +20 -6
  153. package/src/features/playback/trackPlaybackTime.ts +0 -5
  154. package/src/features/playback/volume.ts +1 -1
  155. package/src/features/player/options.ts +2 -20
  156. package/src/features/player/setup.ts +1 -1
  157. package/src/features/player/validateOptions.ts +22 -0
  158. package/src/features/queue/activeTrack.ts +1 -1
  159. package/src/features/queue/queue.ts +2 -2
  160. package/src/features/queue/repeatMode.ts +1 -1
  161. package/src/features/queue/shuffle.ts +1 -1
  162. package/src/features/remoteControls.ts +13 -92
  163. package/src/features/sleepTimer.ts +1 -1
  164. package/src/public-surface.test.ts +123 -0
  165. package/src/utils/LazyNativeEmitter.ts +9 -6
@@ -11,7 +11,7 @@ import type {
11
11
  UpdateOptions
12
12
  } from './options'
13
13
  import { nativeBrowser } from '../../native'
14
- import { validateIOSUpdateOptions } from './options'
14
+ import { validateIOSUpdateOptions } from './validateOptions'
15
15
 
16
16
  /**
17
17
  * Parameters passed to the {@link FormatNowPlayingCallback}.
@@ -0,0 +1,22 @@
1
+ import type { IOSUpdateOptions } from './options'
2
+
3
+ const MAX_CARPLAY_NOW_PLAYING_BUTTONS = 5
4
+
5
+ /**
6
+ * Warns when more CarPlay now-playing buttons are configured than CarPlay renders.
7
+ * Shared by `updateOptions` and `setupPlayer` (both can carry `ios` options).
8
+ *
9
+ * Lives outside `options.ts` because `features/player/index.ts` re-exports that
10
+ * module wholesale: anything exported there joins the package's runtime surface,
11
+ * `@internal` or not. This file is imported directly by the two callers and by
12
+ * nothing in the barrel.
13
+ */
14
+ export function validateIOSUpdateOptions(ios?: IOSUpdateOptions): void {
15
+ const buttons = ios?.carPlayNowPlayingButtons
16
+ if (buttons && buttons.length > MAX_CARPLAY_NOW_PLAYING_BUTTONS) {
17
+ console.warn(
18
+ `[react-native-audio-browser] ${buttons.length} CarPlay now-playing ` +
19
+ `buttons configured; CarPlay shows at most ${MAX_CARPLAY_NOW_PLAYING_BUTTONS}.`
20
+ )
21
+ }
22
+ }
@@ -41,7 +41,7 @@ export function getActiveTrackIndex(): number | undefined {
41
41
  /**
42
42
  * Subscribes to active track change events.
43
43
  * @param callback - Called when the active track changes
44
- * @returns Cleanup function to unsubscribe
44
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
45
45
  */
46
46
  export const onActiveTrackChanged =
47
47
  NativeUpdatedValue.emitterize<PlaybackActiveTrackChangedEvent>(
@@ -156,7 +156,7 @@ export function getQueue(): Track[] {
156
156
  /**
157
157
  * Subscribes to playback queue ended events.
158
158
  * @param callback - Called when playback has paused due to reaching the end of the queue
159
- * @returns Cleanup function to unsubscribe
159
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
160
160
  */
161
161
  export const onQueueEnded =
162
162
  NativeUpdatedValue.emitterize<PlaybackQueueEndedEvent>(
@@ -167,7 +167,7 @@ export const onQueueEnded =
167
167
  * Subscribes to queue change events.
168
168
  * Called when tracks are added, removed, reordered, or when track metadata changes.
169
169
  * @param callback - Called with the updated queue
170
- * @returns Cleanup function to unsubscribe
170
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
171
171
  */
172
172
  export const onQueueChanged = NativeUpdatedValue.emitterize<Track[]>(
173
173
  (cb) => (nativeBrowser.onPlaybackQueueChanged = cb)
@@ -40,7 +40,7 @@ export interface RepeatModeChangedEvent {
40
40
  /**
41
41
  * Subscribes to repeat mode changes.
42
42
  * @param callback - Called when repeat mode changes
43
- * @returns Cleanup function to unsubscribe
43
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
44
44
  */
45
45
  export const onRepeatModeChanged =
46
46
  NativeUpdatedValue.emitterize<RepeatModeChangedEvent>(
@@ -32,7 +32,7 @@ export function toggleShuffle(): void {
32
32
  /**
33
33
  * Subscribes to shuffle mode changes.
34
34
  * @param callback - Called when shuffle mode changes
35
- * @returns Cleanup function to unsubscribe
35
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
36
36
  */
37
37
  export const onShuffleChanged = NativeUpdatedValue.emitterize<boolean>(
38
38
  (cb) => (nativeBrowser.onPlaybackShuffleModeChanged = cb)
@@ -53,90 +53,11 @@ export interface RemoteSkipEvent {
53
53
  index: number
54
54
  }
55
55
 
56
- // MARK: - Default Handlers
57
-
58
- // Install remote control handlers with default behavior immediately when module loads
59
- // Custom handlers can override the defaults using handleRemote* functions
60
-
61
- // Basic playback controls
62
- // AudioBrowser.onRemotePlay(() => {
63
- // const customHandler = customHandlers.get('play')
64
- // if (customHandler) {
65
- // customHandler()
66
- // } else {
67
- // play()
68
- // }
69
- // })
70
-
71
- // AudioBrowser.onRemotePause(() => {
72
- // const customHandler = customHandlers.get('pause')
73
- // if (customHandler) {
74
- // customHandler()
75
- // } else {
76
- // pause()
77
- // }
78
- // })
79
-
80
- // AudioBrowser.onRemoteNext(() => {
81
- // const customHandler = customHandlers.get('next')
82
- // if (customHandler) {
83
- // customHandler()
84
- // } else {
85
- // skipToNext()
86
- // }
87
- // })
88
-
89
- // AudioBrowser.onRemotePrevious(() => {
90
- // const customHandler = customHandlers.get('previous')
91
- // if (customHandler) {
92
- // customHandler()
93
- // } else {
94
- // skipToPrevious()
95
- // }
96
- // })
97
-
98
- // AudioBrowser.onRemoteStop(() => {
99
- // const customHandler = customHandlers.get('stop')
100
- // if (customHandler) {
101
- // customHandler()
102
- // } else {
103
- // stop()
104
- // }
105
- // })
106
-
107
- // // Seek controls
108
- // AudioBrowser.onRemoteSeek((event: any) => {
109
- // const customHandler = customHandlers.get('seek')
110
- // if (customHandler) {
111
- // customHandler(event)
112
- // } else {
113
- // seekTo(event.position)
114
- // }
115
- // })
116
-
117
- // AudioBrowser.onRemoteJumpForward((event: any) => {
118
- // const customHandler = customHandlers.get('jumpForward')
119
- // if (customHandler) {
120
- // customHandler(event)
121
- // } else {
122
- // seekBy(event.interval)
123
- // }
124
- // })
125
-
126
- // AudioBrowser.onRemoteJumpBackward((event: any) => {
127
- // const customHandler = customHandlers.get('jumpBackward')
128
- // if (customHandler) {
129
- // customHandler(event)
130
- // } else {
131
- // seekBy(-event.interval)
132
- // }
133
- // })
134
-
135
56
  // MARK: - Handler Override Functions
136
57
  //
137
58
  // Use these functions when you want to OVERRIDE the default remote control behavior.
138
59
  // These will replace the default handlers with your custom logic.
139
- // If you just want to listen to events for debugging/logging, use the onRemote* functions below.
60
+ // If you just want to listen to events for debugging/logging, use the onRemote* emitters below.
140
61
 
141
62
  /**
142
63
  * Sets a custom handler for remote play events, overriding the default behavior.
@@ -210,7 +131,7 @@ export function handleRemoteJumpBackward(
210
131
 
211
132
  // MARK: - Event Callbacks (for listening/debugging only)
212
133
  //
213
- // Use these functions when you want to LISTEN to remote control events without overriding
134
+ // Use these emitters when you want to LISTEN to remote control events without overriding
214
135
  // the default behavior. These are perfect for logging, analytics, or debugging.
215
136
  // Multiple listeners can be registered for the same event.
216
137
  // To override the default behavior, use the handleRemote* functions above.
@@ -218,7 +139,7 @@ export function handleRemoteJumpBackward(
218
139
  /**
219
140
  * Subscribes to remote jump backward events.
220
141
  * @param callback - Called when the user presses the jump backward button
221
- * @returns Cleanup function to unsubscribe
142
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
222
143
  */
223
144
  export const onRemoteJumpBackward =
224
145
  LazyNativeEmitter.emitterize<RemoteJumpBackwardEvent>(
@@ -228,7 +149,7 @@ export const onRemoteJumpBackward =
228
149
  /**
229
150
  * Subscribes to remote jump forward events.
230
151
  * @param callback - Called when the user presses the jump forward button
231
- * @returns Cleanup function to unsubscribe
152
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
232
153
  */
233
154
  export const onRemoteJumpForward =
234
155
  LazyNativeEmitter.emitterize<RemoteJumpForwardEvent>(
@@ -238,7 +159,7 @@ export const onRemoteJumpForward =
238
159
  /**
239
160
  * Subscribes to remote next events.
240
161
  * @param callback - Called when the user presses the next track button
241
- * @returns Cleanup function to unsubscribe
162
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
242
163
  */
243
164
  export const onRemoteNext = LazyNativeEmitter.emitterize<void>(
244
165
  (cb) => (nativeBrowser.onRemoteNext = cb)
@@ -247,7 +168,7 @@ export const onRemoteNext = LazyNativeEmitter.emitterize<void>(
247
168
  /**
248
169
  * Subscribes to remote pause events.
249
170
  * @param callback - Called when the user presses the pause button
250
- * @returns Cleanup function to unsubscribe
171
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
251
172
  */
252
173
  export const onRemotePause = LazyNativeEmitter.emitterize<void>(
253
174
  (cb) => (nativeBrowser.onRemotePause = cb)
@@ -256,7 +177,7 @@ export const onRemotePause = LazyNativeEmitter.emitterize<void>(
256
177
  /**
257
178
  * Subscribes to remote play events.
258
179
  * @param callback - Called when the user presses the play button
259
- * @returns Cleanup function to unsubscribe
180
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
260
181
  */
261
182
  export const onRemotePlay = LazyNativeEmitter.emitterize<void>(
262
183
  (cb) => (nativeBrowser.onRemotePlay = cb)
@@ -265,7 +186,7 @@ export const onRemotePlay = LazyNativeEmitter.emitterize<void>(
265
186
  /**
266
187
  * Subscribes to remote play ID events (Android only).
267
188
  * @param callback - Called when the user selects a track from an external device
268
- * @returns Cleanup function to unsubscribe
189
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
269
190
  */
270
191
  export const onRemotePlayId = LazyNativeEmitter.emitterize<RemotePlayIdEvent>(
271
192
  (cb) => (nativeBrowser.onRemotePlayId = cb)
@@ -274,7 +195,7 @@ export const onRemotePlayId = LazyNativeEmitter.emitterize<RemotePlayIdEvent>(
274
195
  /**
275
196
  * Subscribes to remote play search events (Android only).
276
197
  * @param callback - Called when the user searches for a track (usually voice search)
277
- * @returns Cleanup function to unsubscribe
198
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
278
199
  */
279
200
  export const onRemotePlaySearch =
280
201
  LazyNativeEmitter.emitterize<RemotePlaySearchEvent>(
@@ -284,7 +205,7 @@ export const onRemotePlaySearch =
284
205
  /**
285
206
  * Subscribes to remote previous events.
286
207
  * @param callback - Called when the user presses the previous track button
287
- * @returns Cleanup function to unsubscribe
208
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
288
209
  */
289
210
  export const onRemotePrevious = LazyNativeEmitter.emitterize<void>(
290
211
  (cb) => (nativeBrowser.onRemotePrevious = cb)
@@ -293,7 +214,7 @@ export const onRemotePrevious = LazyNativeEmitter.emitterize<void>(
293
214
  /**
294
215
  * Subscribes to remote seek events.
295
216
  * @param callback - Called when the user changes the position of the timeline
296
- * @returns Cleanup function to unsubscribe
217
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
297
218
  */
298
219
  export const onRemoteSeek = LazyNativeEmitter.emitterize<RemoteSeekEvent>(
299
220
  (cb) => (nativeBrowser.onRemoteSeek = cb)
@@ -302,7 +223,7 @@ export const onRemoteSeek = LazyNativeEmitter.emitterize<RemoteSeekEvent>(
302
223
  /**
303
224
  * Subscribes to remote skip events (Android only).
304
225
  * @param callback - Called when the user presses the skip button
305
- * @returns Cleanup function to unsubscribe
226
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
306
227
  */
307
228
  export const onRemoteSkip = LazyNativeEmitter.emitterize<RemoteSkipEvent>(
308
229
  (cb) => (nativeBrowser.onRemoteSkip = cb)
@@ -311,7 +232,7 @@ export const onRemoteSkip = LazyNativeEmitter.emitterize<RemoteSkipEvent>(
311
232
  /**
312
233
  * Subscribes to remote stop events.
313
234
  * @param callback - Called when the user presses the stop button
314
- * @returns Cleanup function to unsubscribe
235
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
315
236
  */
316
237
  export const onRemoteStop = LazyNativeEmitter.emitterize<void>(
317
238
  (cb) => (nativeBrowser.onRemoteStop = cb)
@@ -86,7 +86,7 @@ export function clearSleepTimer(): boolean {
86
86
  /**
87
87
  * Subscribes to sleep timer changes.
88
88
  * @param callback - Called when the sleep timer state changes
89
- * @returns Cleanup function to unsubscribe
89
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
90
90
  */
91
91
  export const onSleepTimerChanged = NativeUpdatedValue.emitterize<SleepTimer>(
92
92
  (cb) => (nativeBrowser.onSleepTimerChanged = cb)
@@ -0,0 +1,123 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, join, relative, resolve } from 'node:path'
3
+ import { describe, it, expect } from 'vitest'
4
+
5
+ /**
6
+ * Guard on what `import { … } from 'react-native-audio-browser'` can reach.
7
+ *
8
+ * The package root is a chain of unfiltered `export *` barrels, so every
9
+ * `export` in every feature file joins the public surface by default — there is
10
+ * no gate to forget to pass, only one to forget to add.
11
+ *
12
+ * `@internal` alone does not keep a symbol private. It is honoured by
13
+ * `stripInternal` when generating `lib/typescript`, which is what a consumer's
14
+ * TypeScript sees — but React Native resolves `"react-native": "src/index"`, so
15
+ * Metro bundles the *source*, where nothing was stripped. An `@internal` value
16
+ * is therefore invisible to the type checker and fully reachable at runtime:
17
+ * `require('react-native-audio-browser').nativeBrowser` used to hand out the raw
18
+ * Nitro object, whose `on*` properties are the single callback slots the library's
19
+ * emitters own — assigning one unsubscribes every hook in the package.
20
+ *
21
+ * So: values marked `@internal` must not be reachable from the barrel at all.
22
+ * Move them to a module the barrel doesn't re-export (see
23
+ * `features/player/validateOptions.ts`) and import them directly. Types are
24
+ * exempt — they carry no runtime representation, and `stripInternal` genuinely
25
+ * removes them from the published `.d.ts`.
26
+ */
27
+ const SRC = join(process.cwd(), 'src')
28
+ const ENTRY = join(SRC, 'index.ts')
29
+
30
+ const VALUE_KINDS = new Set([
31
+ 'const',
32
+ 'let',
33
+ 'var',
34
+ 'function',
35
+ 'class',
36
+ 'enum'
37
+ ])
38
+
39
+ function resolveModule(fromFile: string, spec: string): string | undefined {
40
+ const base = resolve(dirname(fromFile), spec.replace(/\.tsx?$/, ''))
41
+ return [`${base}.ts`, `${base}.tsx`, join(base, 'index.ts')].find(existsSync)
42
+ }
43
+
44
+ interface Export {
45
+ name: string
46
+ file: string
47
+ internal: boolean
48
+ }
49
+
50
+ /** Walks the `export *` graph from `entry`, collecting every reachable value export. */
51
+ function collectValueExports(entry: string): Export[] {
52
+ const found: Export[] = []
53
+ const seen = new Set<string>()
54
+
55
+ const visit = (file: string) => {
56
+ if (seen.has(file)) return
57
+ seen.add(file)
58
+ const source = readFileSync(file, 'utf8')
59
+
60
+ for (const [, spec] of source.matchAll(/^export \* from ['"](.+?)['"]/gm)) {
61
+ const target = resolveModule(file, spec!)
62
+ if (target) visit(target)
63
+ }
64
+
65
+ const declaration =
66
+ /^export (?:declare )?(?:async )?(?:abstract )?(const|let|var|function|class|enum) (\w+)/gm
67
+ for (const match of source.matchAll(declaration)) {
68
+ if (!VALUE_KINDS.has(match[1]!)) continue
69
+ // The JSDoc block, if any, is whatever sits directly above the export.
70
+ const preceding = source.slice(0, match.index)
71
+ const doc = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/.exec(preceding)?.[0] ?? ''
72
+ found.push({
73
+ name: match[2]!,
74
+ file: relative(SRC, file),
75
+ internal: /@internal\b/.test(doc)
76
+ })
77
+ }
78
+ }
79
+
80
+ visit(entry)
81
+ return found
82
+ }
83
+
84
+ describe('public surface', () => {
85
+ const exports = collectValueExports(ENTRY)
86
+
87
+ it('reaches the feature modules at all (the walker still works)', () => {
88
+ // Cheap canary: if the barrel structure changes shape and the walker stops
89
+ // resolving, every other assertion here would pass vacuously.
90
+ expect(exports.map((e) => e.name)).toContain('trackPlaybackTime')
91
+ expect(exports.length).toBeGreaterThan(100)
92
+ })
93
+
94
+ it('exposes no value marked @internal', () => {
95
+ const leaked = exports
96
+ .filter((e) => e.internal)
97
+ .map((e) => `${e.name} (${e.file})`)
98
+ expect(
99
+ leaked,
100
+ 'These values are tagged @internal but are reachable from the package root ' +
101
+ 'at runtime — stripInternal only hides them from the published types, and ' +
102
+ 'Metro bundles src/. Move each to a module the barrels do not re-export ' +
103
+ 'and import it directly:\n' +
104
+ leaked.join('\n')
105
+ ).toEqual([])
106
+ })
107
+
108
+ it('exposes no __-prefixed test hook', () => {
109
+ const hooks = exports
110
+ .filter((e) => e.name.startsWith('__'))
111
+ .map((e) => `${e.name} (${e.file})`)
112
+ expect(
113
+ hooks,
114
+ 'Test-only exports ship to consumers. Drive the unit under test through ' +
115
+ 'the seam it already has (a mocked native slot, say) instead:\n' +
116
+ hooks.join('\n')
117
+ ).toEqual([])
118
+ })
119
+
120
+ it('does not re-export the raw Nitro object', () => {
121
+ expect(exports.map((e) => e.name)).not.toContain('nativeBrowser')
122
+ })
123
+ })
@@ -12,9 +12,11 @@ type Callback<T> = (arg: T) => void
12
12
  *
13
13
  * @example
14
14
  * ```typescript
15
- * export const onRemotePlay = LazyEmitter.emitterize<void>(
15
+ * export const onRemotePlay = LazyNativeEmitter.emitterize<void>(
16
16
  * (cb) => (nativeBrowser.onRemotePlay = cb)
17
17
  * )
18
+ *
19
+ * const unsubscribe = onRemotePlay.addListener(() => console.log('play'))
18
20
  * ```
19
21
  */
20
22
  export class LazyNativeEmitter<T> {
@@ -43,14 +45,15 @@ export class LazyNativeEmitter<T> {
43
45
  }
44
46
 
45
47
  /**
46
- * Creates a lazy emitter subscription function for a native callback.
48
+ * Creates a lazy emitter for a native callback.
49
+ *
47
50
  * @param setter - Function that sets the native callback property
48
- * @returns A subscription function that returns an unsubscribe function
51
+ * @returns The emitter instance — subscribe with `addListener`, which returns
52
+ * an unsubscribe function
49
53
  */
50
54
  static emitterize<T>(
51
55
  setter: (callback: (data: T) => void) => void
52
- ): (callback: Callback<T>) => () => void {
53
- const emitter = new LazyNativeEmitter(setter)
54
- return (callback: Callback<T>) => emitter.addListener(callback)
56
+ ): LazyNativeEmitter<T> {
57
+ return new LazyNativeEmitter(setter)
55
58
  }
56
59
  }