react-native-audio-browser 0.2.0-next.1 → 0.2.0-next.11

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 (290) hide show
  1. package/README.md +22 -18
  2. package/android/build.gradle +13 -10
  3. package/android/gradle.properties +2 -2
  4. package/android/src/main/AndroidManifest.xml +3 -1
  5. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +76 -21
  6. package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
  7. package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
  8. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
  9. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +1 -2
  10. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +1 -2
  11. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +6 -7
  12. package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
  13. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +11 -11
  14. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
  15. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +9 -5
  16. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
  17. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +0 -1
  18. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +2 -4
  19. package/android/src/main/java/com/audiobrowser/player/Player.kt +30 -8
  20. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
  21. package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +2 -1
  22. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
  23. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
  24. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  25. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
  26. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
  27. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
  28. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -7
  29. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
  30. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +52 -1
  31. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
  32. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +167 -8
  33. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +2 -1
  34. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +2 -4
  35. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +100 -56
  36. package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
  37. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
  38. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
  39. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
  40. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
  41. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  42. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
  43. package/ios/Browser/BrowserManager.swift +2 -27
  44. package/ios/Browser/SearchDrillIn.swift +1 -1
  45. package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
  46. package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
  47. package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
  48. package/ios/HybridAudioBrowser.swift +87 -56
  49. package/ios/Model/NitroTypeStubs.swift +18 -2
  50. package/ios/Model/RemoteCommand.swift +18 -0
  51. package/ios/Model/TrackPlayerError.swift +2 -2
  52. package/ios/Option/SessionCategories.swift +3 -2
  53. package/ios/Player/MediaIntentCriteria.swift +4 -4
  54. package/ios/Player/MediaLoader.swift +3 -1
  55. package/ios/Player/PlaybackCoordinator.swift +4 -1
  56. package/ios/Player/PlaybackErrorHandler.swift +4 -4
  57. package/ios/Player/QueueManager.swift +10 -3
  58. package/ios/Player/RetryManager.swift +79 -18
  59. package/ios/Player/ShuffleOrder.swift +10 -0
  60. package/ios/Player/SleepTimerManager.swift +43 -15
  61. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
  62. package/ios/RemoteCommand/RemoteCommandController.swift +9 -6
  63. package/ios/Tests/LRUCacheTests.swift +111 -0
  64. package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
  65. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  66. package/ios/Tests/MediaLoaderTests.swift +10 -13
  67. package/ios/Tests/PlaybackCoordinatorTests.swift +10 -7
  68. package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
  69. package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
  70. package/ios/Tests/QueueManagerTests.swift +50 -4
  71. package/ios/Tests/RemoteCommandControllerTests.swift +156 -0
  72. package/ios/Tests/RemoteCommandTests.swift +67 -0
  73. package/ios/Tests/RetryManagerTests.swift +89 -0
  74. package/ios/Tests/SectionScopeTests.swift +2 -2
  75. package/ios/Tests/SleepTimerManagerTests.swift +22 -19
  76. package/ios/Tests/Support/ManualSleepTimerScheduler.swift +47 -0
  77. package/ios/Tests/Support/TestTypes.swift +1 -1
  78. package/ios/Tests/TabBarEntriesTests.swift +7 -7
  79. package/ios/TrackPlayer.swift +6 -1
  80. package/ios/TrackPlayerCallbacks.swift +5 -38
  81. package/ios/Util/LRUCache.swift +4 -1
  82. package/lib/commonjs/AudioBrowser.js +0 -11
  83. package/lib/commonjs/AudioBrowser.js.map +1 -1
  84. package/lib/commonjs/CLAUDE.md +52 -0
  85. package/lib/commonjs/features/browser.js +1 -1
  86. package/lib/commonjs/features/browser.js.map +1 -1
  87. package/lib/commonjs/features/carConnection.js +1 -1
  88. package/lib/commonjs/features/equalizer.js +1 -1
  89. package/lib/commonjs/features/errors.js +3 -3
  90. package/lib/commonjs/features/favorites.js +1 -1
  91. package/lib/commonjs/features/gate.js +16 -1
  92. package/lib/commonjs/features/gate.js.map +1 -1
  93. package/lib/commonjs/features/metadata.js +20 -3
  94. package/lib/commonjs/features/metadata.js.map +1 -1
  95. package/lib/commonjs/features/network.js +1 -1
  96. package/lib/commonjs/features/nowPlaying.js +1 -1
  97. package/lib/commonjs/features/output.js +1 -1
  98. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  99. package/lib/commonjs/features/playback/playing.js +1 -1
  100. package/lib/commonjs/features/playback/progress.js +1 -1
  101. package/lib/commonjs/features/playback/state.js +1 -1
  102. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  103. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  104. package/lib/commonjs/features/playback/volume.js +1 -1
  105. package/lib/commonjs/features/player/options.js +48 -34
  106. package/lib/commonjs/features/player/options.js.map +1 -1
  107. package/lib/commonjs/features/player/setup.js +15 -8
  108. package/lib/commonjs/features/player/setup.js.map +1 -1
  109. package/lib/commonjs/features/player/validateOptions.js +21 -0
  110. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  111. package/lib/commonjs/features/queue/activeTrack.js +1 -1
  112. package/lib/commonjs/features/queue/queue.js +2 -2
  113. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  114. package/lib/commonjs/features/queue/shuffle.js +1 -1
  115. package/lib/commonjs/features/remoteControls.js +13 -92
  116. package/lib/commonjs/features/remoteControls.js.map +1 -1
  117. package/lib/commonjs/features/sleepTimer.js +13 -1
  118. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  119. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  120. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  121. package/lib/commonjs/utils/LazyNativeEmitter.js +8 -5
  122. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  123. package/lib/commonjs/web/NativeAudioBrowser.js +1 -1
  124. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  125. package/lib/commonjs/web/browser/BrowserManager.js +1 -1
  126. package/lib/module/AudioBrowser.js +1 -1
  127. package/lib/module/AudioBrowser.js.map +1 -1
  128. package/lib/module/CLAUDE.md +52 -0
  129. package/lib/module/features/browser.js +1 -1
  130. package/lib/module/features/browser.js.map +1 -1
  131. package/lib/module/features/carConnection.js +1 -1
  132. package/lib/module/features/equalizer.js +1 -1
  133. package/lib/module/features/errors.js +3 -3
  134. package/lib/module/features/favorites.js +1 -1
  135. package/lib/module/features/gate.js +16 -1
  136. package/lib/module/features/gate.js.map +1 -1
  137. package/lib/module/features/metadata.js +20 -3
  138. package/lib/module/features/metadata.js.map +1 -1
  139. package/lib/module/features/network.js +1 -1
  140. package/lib/module/features/nowPlaying.js +1 -1
  141. package/lib/module/features/output.js +1 -1
  142. package/lib/module/features/playback/playWhenReady.js +1 -1
  143. package/lib/module/features/playback/playing.js +1 -1
  144. package/lib/module/features/playback/progress.js +1 -1
  145. package/lib/module/features/playback/state.js +1 -1
  146. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  147. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  148. package/lib/module/features/playback/volume.js +1 -1
  149. package/lib/module/features/player/options.js +47 -32
  150. package/lib/module/features/player/options.js.map +1 -1
  151. package/lib/module/features/player/setup.js +14 -7
  152. package/lib/module/features/player/setup.js.map +1 -1
  153. package/lib/module/features/player/validateOptions.js +17 -0
  154. package/lib/module/features/player/validateOptions.js.map +1 -0
  155. package/lib/module/features/queue/activeTrack.js +1 -1
  156. package/lib/module/features/queue/queue.js +2 -2
  157. package/lib/module/features/queue/repeatMode.js +1 -1
  158. package/lib/module/features/queue/shuffle.js +1 -1
  159. package/lib/module/features/remoteControls.js +13 -92
  160. package/lib/module/features/remoteControls.js.map +1 -1
  161. package/lib/module/features/sleepTimer.js +13 -1
  162. package/lib/module/features/sleepTimer.js.map +1 -1
  163. package/lib/module/test-utils/reactNativeStub.js +58 -0
  164. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  165. package/lib/module/utils/LazyNativeEmitter.js +8 -5
  166. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  167. package/lib/module/web/NativeAudioBrowser.js +1 -1
  168. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  169. package/lib/module/web/browser/BrowserManager.js +1 -1
  170. package/lib/typescript/src/AudioBrowser.d.ts +0 -1
  171. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  172. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  173. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  174. package/lib/typescript/src/features/errors.d.ts +3 -3
  175. package/lib/typescript/src/features/favorites.d.ts +1 -1
  176. package/lib/typescript/src/features/gate.d.ts +18 -2
  177. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  178. package/lib/typescript/src/features/metadata.d.ts +20 -3
  179. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  180. package/lib/typescript/src/features/network.d.ts +1 -1
  181. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  182. package/lib/typescript/src/features/output.d.ts +1 -1
  183. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  184. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  185. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  186. package/lib/typescript/src/features/playback/state.d.ts +1 -1
  187. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  188. package/lib/typescript/src/features/player/options.d.ts +87 -37
  189. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  190. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  191. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  192. package/lib/typescript/src/features/player/validateOptions.d.ts +9 -0
  193. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  194. package/lib/typescript/src/features/queue/activeTrack.d.ts +1 -1
  195. package/lib/typescript/src/features/queue/queue.d.ts +2 -2
  196. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  197. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  198. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  199. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  200. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  201. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  202. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  203. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  204. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +8 -4
  205. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  206. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -1
  207. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
  208. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +5 -5
  209. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +11 -11
  210. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
  211. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +5 -5
  212. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +5 -5
  213. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
  214. package/nitrogen/generated/android/c++/JOptions.hpp +5 -5
  215. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  216. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  217. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  218. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  219. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
  220. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
  221. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
  222. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  223. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
  224. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
  225. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +36 -51
  226. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +6 -6
  227. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +8 -8
  228. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  229. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  230. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  231. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  232. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  233. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  234. package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
  235. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  236. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  237. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  238. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  239. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  240. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  241. package/package.json +12 -4
  242. package/src/AudioBrowser.ts +1 -1
  243. package/src/CLAUDE.md +52 -0
  244. package/src/features/battery.test.ts +79 -0
  245. package/src/features/browser.ts +1 -1
  246. package/src/features/carConnection.ts +1 -1
  247. package/src/features/equalizer.ts +1 -1
  248. package/src/features/errors.ts +3 -3
  249. package/src/features/favorites.ts +1 -1
  250. package/src/features/gate.ts +16 -1
  251. package/src/features/hook-wiring.test.ts +254 -0
  252. package/src/features/metadata.ts +20 -3
  253. package/src/features/network.ts +1 -1
  254. package/src/features/nowPlaying.ts +1 -1
  255. package/src/features/output.ts +1 -1
  256. package/src/features/playback/playWhenReady.ts +1 -1
  257. package/src/features/playback/playing.ts +1 -1
  258. package/src/features/playback/progress.test.ts +169 -0
  259. package/src/features/playback/progress.ts +1 -1
  260. package/src/features/playback/state.ts +1 -1
  261. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  262. package/src/features/playback/trackPlaybackTime.ts +0 -5
  263. package/src/features/playback/volume.ts +1 -1
  264. package/src/features/player/options.ts +99 -57
  265. package/src/features/player/setup.test.ts +12 -4
  266. package/src/features/player/setup.ts +19 -9
  267. package/src/features/player/validateOptions.ts +19 -0
  268. package/src/features/queue/activeTrack.ts +1 -1
  269. package/src/features/queue/queue.ts +2 -2
  270. package/src/features/queue/repeatMode.ts +1 -1
  271. package/src/features/queue/shuffle.ts +1 -1
  272. package/src/features/remoteControls.ts +13 -92
  273. package/src/features/sleepTimer.test.ts +172 -0
  274. package/src/features/sleepTimer.ts +9 -1
  275. package/src/nitro-stub-drift.test.ts +237 -0
  276. package/src/public-surface.test.ts +107 -0
  277. package/src/specs/audio-browser.nitro.ts +1 -1
  278. package/src/test-utils/reactNativeStub.ts +60 -0
  279. package/src/utils/LazyNativeEmitter.ts +9 -6
  280. package/src/utils/useDebug.test.ts +182 -0
  281. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  282. package/src/web/NativeAudioBrowser.ts +1 -1
  283. package/src/web/browser/BrowserManager.ts +1 -1
  284. package/ios/Model/MediaURL.swift +0 -67
  285. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  286. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  287. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  289. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  290. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
@@ -0,0 +1,254 @@
1
+ /** @vitest-environment happy-dom */
2
+
3
+ import { renderHook, act, cleanup } from '@testing-library/react'
4
+ import { describe, it, expect, afterEach, vi } from 'vitest'
5
+
6
+ /**
7
+ * The 22 hooks that are a single delegation to `useNativeUpdatedValue`. Their
8
+ * behaviour is covered once in `utils/useNativeUpdatedValue.test.ts`; what is
9
+ * per-hook and untestable there is the wiring — which native callback slot the
10
+ * emitter claims, and which `eventKey` is pulled out of the payload.
11
+ *
12
+ * Both are easy to get wrong and silent when wrong: the hook simply never
13
+ * updates, or yields the whole event where a field was meant. Five slots
14
+ * already differ in name from the emitter that claims them
15
+ * (`onPlaybackPlayWhenReadyChanged` behind `onPlayWhenReadyChanged`, and
16
+ * friends), so the names cannot be derived — they have to be pinned.
17
+ */
18
+ const slots = vi.hoisted(() => new Map<string, (event: unknown) => void>())
19
+
20
+ vi.mock('../native', () => ({
21
+ nativeBrowser: new Proxy({} as Record<string, unknown>, {
22
+ get(target, prop: string) {
23
+ // Getters are the pre-native fallback; undefined stands in for "native
24
+ // has not answered yet", which is what these tests start from.
25
+ target[prop] ??= vi.fn(() => undefined)
26
+ return target[prop]
27
+ },
28
+ set(target, prop: string, value) {
29
+ if (typeof value === 'function') slots.set(prop, value)
30
+ target[prop] = value
31
+ return true
32
+ }
33
+ })
34
+ }))
35
+
36
+ const { useBatteryOptimizationStatus, useBatteryWarningPending } =
37
+ await import('./battery')
38
+ const { useContent, usePath, useTabs } = await import('./browser')
39
+ const { useCarConnected } = await import('./carConnection')
40
+ const { useEqualizerSettings } = await import('./equalizer')
41
+ const { useFormattedNavigationError, useNavigationError, usePlaybackError } =
42
+ await import('./errors')
43
+ const { useOnline } = await import('./network')
44
+ const { useNowPlaying } = await import('./nowPlaying')
45
+ const { useOutput } = await import('./output')
46
+ const { useOptions } = await import('./player/options')
47
+ const { usePlayingState } = await import('./playback/playing')
48
+ const { usePlayWhenReady } = await import('./playback/playWhenReady')
49
+ const { useProgress } = await import('./playback/progress')
50
+ const { usePlayback } = await import('./playback/state')
51
+ const { useSystemVolume } = await import('./playback/volume')
52
+ const { useActiveTrack } = await import('./queue/activeTrack')
53
+ const { useQueue } = await import('./queue/queue')
54
+ const { useRepeatMode } = await import('./queue/repeatMode')
55
+ const { useShuffle } = await import('./queue/shuffle')
56
+
57
+ afterEach(cleanup)
58
+
59
+ type Wiring = {
60
+ name: string
61
+ hook: () => unknown
62
+ /** The property on the Nitro object the emitter assigns its callback to. */
63
+ slot: string
64
+ event: unknown
65
+ /** What the hook returns — differs from `event` exactly when an eventKey applies. */
66
+ expected: unknown
67
+ }
68
+
69
+ const wirings: Wiring[] = [
70
+ // Payload passed through whole (no eventKey).
71
+ {
72
+ name: 'useOutput',
73
+ hook: useOutput,
74
+ slot: 'onOutputChanged',
75
+ event: { type: 'bluetooth' },
76
+ expected: { type: 'bluetooth' }
77
+ },
78
+ {
79
+ name: 'useNowPlaying',
80
+ hook: useNowPlaying,
81
+ slot: 'onNowPlayingChanged',
82
+ event: { title: 'Song' },
83
+ expected: { title: 'Song' }
84
+ },
85
+ {
86
+ name: 'useOnline',
87
+ hook: useOnline,
88
+ slot: 'onOnlineChanged',
89
+ event: true,
90
+ expected: true
91
+ },
92
+ {
93
+ name: 'useCarConnected',
94
+ hook: useCarConnected,
95
+ slot: 'onCarConnectedChanged',
96
+ event: true,
97
+ expected: true
98
+ },
99
+ {
100
+ name: 'useEqualizerSettings',
101
+ hook: useEqualizerSettings,
102
+ slot: 'onEqualizerChanged',
103
+ event: { enabled: true },
104
+ expected: { enabled: true }
105
+ },
106
+ {
107
+ name: 'usePath',
108
+ hook: usePath,
109
+ slot: 'onPathChanged',
110
+ event: '/artists/1',
111
+ expected: '/artists/1'
112
+ },
113
+ {
114
+ name: 'useContent',
115
+ hook: useContent,
116
+ slot: 'onContentChanged',
117
+ event: { src: 'a' },
118
+ expected: { src: 'a' }
119
+ },
120
+ {
121
+ name: 'useTabs',
122
+ hook: useTabs,
123
+ slot: 'onTabsChanged',
124
+ event: [{ src: 'a' }],
125
+ expected: [{ src: 'a' }]
126
+ },
127
+ {
128
+ name: 'usePlayback',
129
+ hook: usePlayback,
130
+ slot: 'onPlaybackChanged',
131
+ event: { state: 'playing' },
132
+ expected: { state: 'playing' }
133
+ },
134
+ {
135
+ name: 'useSystemVolume',
136
+ hook: useSystemVolume,
137
+ slot: 'onSystemVolumeChanged',
138
+ event: 0.4,
139
+ expected: 0.4
140
+ },
141
+ {
142
+ name: 'useProgress',
143
+ hook: useProgress,
144
+ slot: 'onPlaybackProgressUpdated',
145
+ event: { position: 1, duration: 2, buffered: 3 },
146
+ expected: { position: 1, duration: 2, buffered: 3 }
147
+ },
148
+ {
149
+ name: 'usePlayingState',
150
+ hook: usePlayingState,
151
+ slot: 'onPlaybackPlayingState',
152
+ event: { state: 'ready' },
153
+ expected: { state: 'ready' }
154
+ },
155
+ {
156
+ name: 'useShuffle',
157
+ hook: useShuffle,
158
+ slot: 'onPlaybackShuffleModeChanged',
159
+ event: true,
160
+ expected: true
161
+ },
162
+ {
163
+ name: 'useQueue',
164
+ hook: useQueue,
165
+ slot: 'onPlaybackQueueChanged',
166
+ event: [{ src: 'a' }],
167
+ expected: [{ src: 'a' }]
168
+ },
169
+ {
170
+ name: 'useFormattedNavigationError',
171
+ hook: useFormattedNavigationError,
172
+ slot: 'onFormattedNavigationError',
173
+ event: { title: 'Offline' },
174
+ expected: { title: 'Offline' }
175
+ },
176
+ {
177
+ name: 'useOptions',
178
+ hook: useOptions,
179
+ slot: 'onOptionsChanged',
180
+ event: { playbackSpeed: 1.5 },
181
+ expected: { playbackSpeed: 1.5 }
182
+ },
183
+
184
+ // A field lifted out of the payload — the eventKey cases.
185
+ {
186
+ name: 'usePlaybackError',
187
+ hook: usePlaybackError,
188
+ slot: 'onPlaybackError',
189
+ event: { error: { code: 'network' } },
190
+ expected: { code: 'network' }
191
+ },
192
+ {
193
+ name: 'useNavigationError',
194
+ hook: useNavigationError,
195
+ slot: 'onNavigationError',
196
+ event: { error: { code: 'notFound' } },
197
+ expected: { code: 'notFound' }
198
+ },
199
+ {
200
+ name: 'usePlayWhenReady',
201
+ hook: usePlayWhenReady,
202
+ slot: 'onPlaybackPlayWhenReadyChanged',
203
+ event: { playWhenReady: true },
204
+ expected: true
205
+ },
206
+ {
207
+ name: 'useRepeatMode',
208
+ hook: useRepeatMode,
209
+ slot: 'onPlaybackRepeatModeChanged',
210
+ event: { repeatMode: 'queue' },
211
+ expected: 'queue'
212
+ },
213
+ {
214
+ name: 'useActiveTrack',
215
+ hook: useActiveTrack,
216
+ slot: 'onPlaybackActiveTrackChanged',
217
+ event: { track: { src: 'a' } },
218
+ expected: { src: 'a' }
219
+ },
220
+ {
221
+ name: 'useBatteryWarningPending',
222
+ hook: useBatteryWarningPending,
223
+ slot: 'onBatteryWarningPendingChanged',
224
+ event: { pending: true },
225
+ expected: true
226
+ },
227
+ {
228
+ name: 'useBatteryOptimizationStatus',
229
+ hook: useBatteryOptimizationStatus,
230
+ slot: 'onBatteryOptimizationStatusChanged',
231
+ event: { status: 'unrestricted' },
232
+ expected: 'unrestricted'
233
+ }
234
+ ]
235
+
236
+ describe.each(wirings)('$name', ({ hook, slot, event, expected }) => {
237
+ it(`reads $slot`, () => {
238
+ // A slot nothing claimed means the emitter assigns a different property
239
+ // than the one named here — the hook would never update in production.
240
+ expect(slots.has(slot)).toBe(true)
241
+
242
+ const { result } = renderHook(() => hook())
243
+ act(() => slots.get(slot)!(event))
244
+ expect(result.current).toEqual(expected)
245
+ })
246
+ })
247
+
248
+ describe('hook wiring table', () => {
249
+ it('covers every delegating hook exactly once', () => {
250
+ const names = wirings.map((w) => w.name)
251
+ expect(new Set(names).size).toBe(names.length)
252
+ expect(new Set(wirings.map((w) => w.slot)).size).toBe(wirings.length)
253
+ })
254
+ })
@@ -98,8 +98,13 @@ export interface ChapterMetadata {
98
98
 
99
99
  /**
100
100
  * Subscribes to chapter metadata events.
101
+ *
102
+ * @platform ios android
103
+ * Never fires on web — the web player does not read chapter markers. See
104
+ * {@link https://github.com/radio-garden/react-native-audio-browser/issues/118 | #118}.
105
+ *
101
106
  * @param callback - Called when chapter metadata is received
102
- * @returns Cleanup function to unsubscribe
107
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
103
108
  */
104
109
  export const onChapterMetadata = NativeUpdatedValue.emitterize<
105
110
  ChapterMetadata[]
@@ -107,8 +112,13 @@ export const onChapterMetadata = NativeUpdatedValue.emitterize<
107
112
 
108
113
  /**
109
114
  * Subscribes to track metadata events.
115
+ *
116
+ * @platform ios android
117
+ * Never fires on web — the web player does not read embedded file tags. See
118
+ * {@link https://github.com/radio-garden/react-native-audio-browser/issues/118 | #118}.
119
+ *
110
120
  * @param callback - Called when static track metadata is received
111
- * @returns Cleanup function to unsubscribe
121
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
112
122
  */
113
123
  export const onTrackMetadata = NativeUpdatedValue.emitterize<TrackMetadata>(
114
124
  (cb) => (nativeBrowser.onTrackMetadata = cb)
@@ -116,8 +126,15 @@ export const onTrackMetadata = NativeUpdatedValue.emitterize<TrackMetadata>(
116
126
 
117
127
  /**
118
128
  * Subscribes to timed metadata events (ICY/ID3 from live streams).
129
+ *
130
+ * @platform ios android
131
+ * Never fires on web. ICY metadata is stripped by the browser before the player
132
+ * can see it, and no browser API exposes it; in-band ID3 from HLS is reachable
133
+ * but not yet wired. See
134
+ * {@link https://github.com/radio-garden/react-native-audio-browser/issues/118 | #118}.
135
+ *
119
136
  * @param callback - Called when stream metadata is received (title, artist from ICY/ID3 tags)
120
- * @returns Cleanup function to unsubscribe
137
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
121
138
  */
122
139
  export const onTimedMetadata = NativeUpdatedValue.emitterize<TimedMetadata>(
123
140
  (cb) => (nativeBrowser.onTimedMetadata = cb)
@@ -17,7 +17,7 @@ export function getOnline(): boolean {
17
17
  /**
18
18
  * Subscribes to network connectivity changes.
19
19
  * @param callback - Called when network state changes
20
- * @returns Cleanup function to unsubscribe
20
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
21
21
  */
22
22
  export const onOnlineChanged = NativeUpdatedValue.emitterize<boolean>(
23
23
  (cb) => (nativeBrowser.onOnlineChanged = cb)
@@ -74,7 +74,7 @@ export function clearNowPlayingFlash(): void {
74
74
  * Subscribes to now playing metadata change events.
75
75
  * Fires when updateNowPlaying is called or when the track changes.
76
76
  * @param callback - Called when now playing metadata changes
77
- * @returns Cleanup function to unsubscribe
77
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
78
78
  */
79
79
  export const onNowPlayingChanged =
80
80
  NativeUpdatedValue.emitterize<NowPlayingMetadata>(
@@ -46,7 +46,7 @@ export function getOutput(): Output | undefined {
46
46
  /**
47
47
  * Subscribes to current-output changes (headphones unplugged, Bluetooth speaker
48
48
  * connected, AirPlay/route selected). Fires on iOS and Android 11+; never below.
49
- * @returns Cleanup function to unsubscribe.
49
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
50
50
  */
51
51
  export const onOutputChanged = NativeUpdatedValue.emitterize<Output>(
52
52
  (cb) => (nativeBrowser.onOutputChanged = cb)
@@ -35,7 +35,7 @@ export function setPlayWhenReady(playWhenReady: boolean): void {
35
35
  /**
36
36
  * Subscribes to play when ready changes.
37
37
  * @param callback - Called when playWhenReady changes
38
- * @returns Cleanup function to unsubscribe
38
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
39
39
  */
40
40
  export const onPlayWhenReadyChanged =
41
41
  NativeUpdatedValue.emitterize<PlaybackPlayWhenReadyChangedEvent>(
@@ -23,7 +23,7 @@ export function getPlayingState(): PlayingState {
23
23
  /**
24
24
  * Subscribes to playing state changes.
25
25
  * @param callback - Called when playing or buffering state changes
26
- * @returns Cleanup function to unsubscribe
26
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
27
27
  */
28
28
  export const onPlayingState = NativeUpdatedValue.emitterize<PlayingState>(
29
29
  (cb) => (nativeBrowser.onPlaybackPlayingState = cb)
@@ -0,0 +1,169 @@
1
+ /** @vitest-environment happy-dom */
2
+
3
+ import { renderHook, act, cleanup } from '@testing-library/react'
4
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
5
+ import type { Progress } from './progress'
6
+ import {
7
+ appStateListenerCount,
8
+ resetAppState,
9
+ setAppState
10
+ } from '../../test-utils/reactNativeStub'
11
+
12
+ const native = vi.hoisted(() => ({
13
+ getProgress: vi.fn<() => Progress>(() => ({
14
+ position: 0,
15
+ duration: 0,
16
+ buffered: 0
17
+ })),
18
+ onPlaybackChanged: undefined as ((event: unknown) => void) | undefined,
19
+ onPlaybackProgressUpdated: undefined as ((event: unknown) => void) | undefined
20
+ }))
21
+
22
+ vi.mock('../../native', () => ({ nativeBrowser: native }))
23
+
24
+ const { usePolledProgress } = await import('./progress')
25
+
26
+ const at = (position: number): Progress => ({
27
+ position,
28
+ duration: 300,
29
+ buffered: position + 10
30
+ })
31
+
32
+ beforeEach(() => {
33
+ vi.useFakeTimers()
34
+ resetAppState()
35
+ native.getProgress.mockReturnValue(at(0))
36
+ })
37
+
38
+ afterEach(() => {
39
+ cleanup()
40
+ vi.useRealTimers()
41
+ })
42
+
43
+ describe('usePolledProgress', () => {
44
+ it('starts at zero and polls the first value in', () => {
45
+ native.getProgress.mockReturnValue(at(12))
46
+ const { result } = renderHook(() => usePolledProgress())
47
+ // The initial poll runs inside the effect, so the zeroed useState default is
48
+ // never observable to a caller.
49
+ expect(result.current).toEqual(at(12))
50
+ })
51
+
52
+ it('polls on the given interval', () => {
53
+ const { result } = renderHook(() => usePolledProgress(500))
54
+ native.getProgress.mockReturnValue(at(1))
55
+ act(() => {
56
+ vi.advanceTimersByTime(500)
57
+ })
58
+ expect(result.current).toEqual(at(1))
59
+
60
+ native.getProgress.mockReturnValue(at(2))
61
+ act(() => {
62
+ vi.advanceTimersByTime(500)
63
+ })
64
+ expect(result.current).toEqual(at(2))
65
+ })
66
+
67
+ it('does not poll before the interval elapses', () => {
68
+ renderHook(() => usePolledProgress(1000))
69
+ native.getProgress.mockClear()
70
+ act(() => {
71
+ vi.advanceTimersByTime(999)
72
+ })
73
+ expect(native.getProgress).not.toHaveBeenCalled()
74
+ })
75
+
76
+ it('keeps the same object when nothing changed', () => {
77
+ const { result } = renderHook(() => usePolledProgress(500))
78
+ const first = result.current
79
+ act(() => {
80
+ vi.advanceTimersByTime(500)
81
+ })
82
+ // Identity, not equality: the hook returns the previous state object so a
83
+ // consumer's useEffect/memo on progress does not re-fire every tick.
84
+ expect(result.current).toBe(first)
85
+ })
86
+
87
+ it('updates immediately when playback changes, without waiting for a tick', () => {
88
+ const { result } = renderHook(() => usePolledProgress(10_000))
89
+ native.getProgress.mockReturnValue(at(7))
90
+ act(() => {
91
+ native.onPlaybackChanged?.({ state: 'playing' })
92
+ })
93
+ expect(result.current).toEqual(at(7))
94
+ })
95
+
96
+ it('survives a getter that throws before setup', () => {
97
+ native.getProgress.mockImplementation(() => {
98
+ throw new Error('not set up')
99
+ })
100
+ const { result } = renderHook(() => usePolledProgress(500))
101
+ expect(result.current).toEqual({ position: 0, duration: 0, buffered: 0 })
102
+ expect(() => act(() => vi.advanceTimersByTime(500))).not.toThrow()
103
+ })
104
+
105
+ it('stops polling in the background and resumes on return', () => {
106
+ const { result } = renderHook(() => usePolledProgress(500))
107
+
108
+ act(() => {
109
+ setAppState('background')
110
+ })
111
+ native.getProgress.mockReturnValue(at(99))
112
+ act(() => {
113
+ vi.advanceTimersByTime(5_000)
114
+ })
115
+ expect(result.current).toEqual(at(0))
116
+
117
+ // Returning to the foreground updates once immediately rather than leaving
118
+ // a stale position on screen until the next interval.
119
+ act(() => {
120
+ setAppState('active')
121
+ })
122
+ expect(result.current).toEqual(at(99))
123
+ })
124
+
125
+ it('ignores playback events while backgrounded', () => {
126
+ const { result } = renderHook(() => usePolledProgress(500))
127
+ act(() => {
128
+ setAppState('background')
129
+ })
130
+ native.getProgress.mockReturnValue(at(42))
131
+ act(() => {
132
+ native.onPlaybackChanged?.({ state: 'playing' })
133
+ })
134
+ expect(result.current).toEqual(at(0))
135
+ })
136
+
137
+ it('does not stack pollers when backgrounded twice', () => {
138
+ renderHook(() => usePolledProgress(500))
139
+ act(() => {
140
+ setAppState('background')
141
+ })
142
+ act(() => {
143
+ setAppState('active')
144
+ })
145
+ act(() => {
146
+ setAppState('active')
147
+ })
148
+
149
+ native.getProgress.mockClear()
150
+ act(() => {
151
+ vi.advanceTimersByTime(500)
152
+ })
153
+ expect(native.getProgress).toHaveBeenCalledTimes(1)
154
+ })
155
+
156
+ it('tears down its timer and subscriptions on unmount', () => {
157
+ const { unmount } = renderHook(() => usePolledProgress(500))
158
+ expect(appStateListenerCount()).toBe(1)
159
+
160
+ unmount()
161
+ expect(appStateListenerCount()).toBe(0)
162
+
163
+ native.getProgress.mockClear()
164
+ act(() => {
165
+ vi.advanceTimersByTime(5_000)
166
+ })
167
+ expect(native.getProgress).not.toHaveBeenCalled()
168
+ })
169
+ })
@@ -46,7 +46,7 @@ export function getProgress(): Progress {
46
46
  /**
47
47
  * Subscribes to playback progress updates.
48
48
  * @param callback - Called periodically with playback progress updates
49
- * @returns Cleanup function to unsubscribe
49
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
50
50
  */
51
51
  export const onProgressUpdated =
52
52
  NativeUpdatedValue.emitterize<PlaybackProgressUpdatedEvent>(
@@ -55,7 +55,7 @@ export function getPlayback(): Playback {
55
55
  /**
56
56
  * Subscribes to playback state changes.
57
57
  * @param callback - Called when the playback state changes
58
- * @returns Cleanup function to unsubscribe
58
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
59
59
  */
60
60
  export const onPlaybackChanged = NativeUpdatedValue.emitterize<Playback>(
61
61
  (cb) => (nativeBrowser.onPlaybackChanged = cb)
@@ -13,12 +13,22 @@ vi.mock('../queue/activeTrack', () => ({
13
13
  onActiveTrackChanged: { addListener: vi.fn(() => () => {}) }
14
14
  }))
15
15
 
16
- import { trackPlaybackTime, __emitTickForTests } from './trackPlaybackTime'
16
+ import { trackPlaybackTime } from './trackPlaybackTime'
17
17
 
18
18
  beforeEach(() => {
19
19
  nativeMock.setPlaybackIntervalEnabled.mockClear()
20
20
  })
21
21
 
22
+ /** Ticks through the slot `install()` assigns, so this covers that wiring too. */
23
+ function tick(times = 1) {
24
+ for (let i = 0; i < times; i++) {
25
+ if (!nativeMock.onPlaybackInterval) {
26
+ throw new Error('trackPlaybackTime never installed onPlaybackInterval')
27
+ }
28
+ nativeMock.onPlaybackInterval()
29
+ }
30
+ }
31
+
22
32
  describe('trackPlaybackTime', () => {
23
33
  it('enables native tick on first subscriber, disables on last', () => {
24
34
  const a = trackPlaybackTime(() => {}, 5)
@@ -38,17 +48,17 @@ describe('trackPlaybackTime', () => {
38
48
  (e) => calls.push({ total: e.total, sinceLast: e.sinceLast }),
39
49
  3
40
50
  )
41
- __emitTickForTests(2)
51
+ tick(2)
42
52
  expect(calls).toEqual([])
43
- __emitTickForTests(1) // total now 3 → fires
53
+ tick(1) // total now 3 → fires
44
54
  expect(calls).toEqual([{ total: 3, sinceLast: 3 }])
45
- __emitTickForTests(3) // total now 6 → fires again
55
+ tick(3) // total now 6 → fires again
46
56
  expect(calls).toEqual([
47
57
  { total: 3, sinceLast: 3 },
48
58
  { total: 6, sinceLast: 3 }
49
59
  ])
50
60
  cancel()
51
- __emitTickForTests(3)
61
+ tick(3)
52
62
  expect(calls).toHaveLength(2) // cancelled
53
63
  })
54
64
 
@@ -57,7 +67,7 @@ describe('trackPlaybackTime', () => {
57
67
  const slow: number[] = []
58
68
  const cancelFast = trackPlaybackTime((e) => fast.push(e.total), 1)
59
69
  const cancelSlow = trackPlaybackTime((e) => slow.push(e.total), 2)
60
- __emitTickForTests(2)
70
+ tick(2)
61
71
  expect(fast).toEqual([1, 2])
62
72
  expect(slow).toEqual([2])
63
73
  cancelFast()
@@ -99,8 +99,3 @@ export function trackPlaybackTime(
99
99
  if (handles.size === 0) nativeBrowser.setPlaybackIntervalEnabled(false)
100
100
  }
101
101
  }
102
-
103
- /** @internal test hook — drives the fan-out without a native tick. */
104
- export function __emitTickForTests(ticks = 1) {
105
- for (let i = 0; i < ticks; i++) onTick()
106
- }
@@ -42,7 +42,7 @@ export function setSystemVolume(level: number): void {
42
42
  /**
43
43
  * Subscribes to system volume changes.
44
44
  * @param callback - Called when the system volume changes
45
- * @returns Cleanup function to unsubscribe
45
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
46
46
  */
47
47
  export const onSystemVolumeChanged = NativeUpdatedValue.emitterize<number>(
48
48
  (cb) => (nativeBrowser.onSystemVolumeChanged = cb)