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

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 (162) hide show
  1. package/android/build.gradle +7 -1
  2. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +1 -2
  3. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +1 -2
  4. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +6 -7
  5. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +0 -1
  6. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +2 -4
  7. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
  8. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
  9. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -7
  10. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +0 -1
  11. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +2 -1
  12. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +2 -4
  13. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
  14. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
  15. package/ios/Browser/BrowserManager.swift +2 -2
  16. package/ios/Browser/SearchDrillIn.swift +1 -1
  17. package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
  18. package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
  19. package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
  20. package/ios/HybridAudioBrowser.swift +8 -7
  21. package/ios/Model/RemoteCommand.swift +18 -0
  22. package/ios/Model/TrackPlayerError.swift +2 -2
  23. package/ios/Option/SessionCategories.swift +3 -2
  24. package/ios/Player/MediaIntentCriteria.swift +4 -4
  25. package/ios/Player/PlaybackErrorHandler.swift +2 -2
  26. package/ios/Player/RetryManager.swift +2 -1
  27. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
  28. package/ios/RemoteCommand/RemoteCommandController.swift +9 -6
  29. package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
  30. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  31. package/ios/Tests/PlaybackCoordinatorTests.swift +1 -1
  32. package/ios/Tests/RemoteCommandControllerTests.swift +156 -0
  33. package/ios/Tests/RemoteCommandTests.swift +67 -0
  34. package/ios/Tests/SectionScopeTests.swift +2 -2
  35. package/ios/Tests/TabBarEntriesTests.swift +7 -7
  36. package/ios/TrackPlayer.swift +1 -1
  37. package/ios/TrackPlayerCallbacks.swift +5 -38
  38. package/lib/commonjs/AudioBrowser.js +0 -11
  39. package/lib/commonjs/AudioBrowser.js.map +1 -1
  40. package/lib/commonjs/features/carConnection.js +1 -1
  41. package/lib/commonjs/features/equalizer.js +1 -1
  42. package/lib/commonjs/features/errors.js +3 -3
  43. package/lib/commonjs/features/favorites.js +1 -1
  44. package/lib/commonjs/features/gate.js +1 -1
  45. package/lib/commonjs/features/metadata.js +3 -3
  46. package/lib/commonjs/features/network.js +1 -1
  47. package/lib/commonjs/features/nowPlaying.js +1 -1
  48. package/lib/commonjs/features/output.js +1 -1
  49. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  50. package/lib/commonjs/features/playback/playing.js +1 -1
  51. package/lib/commonjs/features/playback/progress.js +1 -1
  52. package/lib/commonjs/features/playback/state.js +1 -1
  53. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  54. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  55. package/lib/commonjs/features/playback/volume.js +1 -1
  56. package/lib/commonjs/features/player/options.js +3 -19
  57. package/lib/commonjs/features/player/options.js.map +1 -1
  58. package/lib/commonjs/features/player/setup.js +2 -2
  59. package/lib/commonjs/features/player/setup.js.map +1 -1
  60. package/lib/commonjs/features/player/validateOptions.js +24 -0
  61. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  62. package/lib/commonjs/features/queue/activeTrack.js +1 -1
  63. package/lib/commonjs/features/queue/queue.js +2 -2
  64. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  65. package/lib/commonjs/features/queue/shuffle.js +1 -1
  66. package/lib/commonjs/features/remoteControls.js +13 -92
  67. package/lib/commonjs/features/remoteControls.js.map +1 -1
  68. package/lib/commonjs/features/sleepTimer.js +1 -1
  69. package/lib/commonjs/utils/LazyNativeEmitter.js +8 -5
  70. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  71. package/lib/module/AudioBrowser.js +4 -1
  72. package/lib/module/AudioBrowser.js.map +1 -1
  73. package/lib/module/features/carConnection.js +1 -1
  74. package/lib/module/features/equalizer.js +1 -1
  75. package/lib/module/features/errors.js +3 -3
  76. package/lib/module/features/favorites.js +1 -1
  77. package/lib/module/features/gate.js +1 -1
  78. package/lib/module/features/metadata.js +3 -3
  79. package/lib/module/features/network.js +1 -1
  80. package/lib/module/features/nowPlaying.js +1 -1
  81. package/lib/module/features/output.js +1 -1
  82. package/lib/module/features/playback/playWhenReady.js +1 -1
  83. package/lib/module/features/playback/playing.js +1 -1
  84. package/lib/module/features/playback/progress.js +1 -1
  85. package/lib/module/features/playback/state.js +1 -1
  86. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  87. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  88. package/lib/module/features/playback/volume.js +1 -1
  89. package/lib/module/features/player/options.js +2 -17
  90. package/lib/module/features/player/options.js.map +1 -1
  91. package/lib/module/features/player/setup.js +1 -1
  92. package/lib/module/features/player/setup.js.map +1 -1
  93. package/lib/module/features/player/validateOptions.js +20 -0
  94. package/lib/module/features/player/validateOptions.js.map +1 -0
  95. package/lib/module/features/queue/activeTrack.js +1 -1
  96. package/lib/module/features/queue/queue.js +2 -2
  97. package/lib/module/features/queue/repeatMode.js +1 -1
  98. package/lib/module/features/queue/shuffle.js +1 -1
  99. package/lib/module/features/remoteControls.js +13 -92
  100. package/lib/module/features/remoteControls.js.map +1 -1
  101. package/lib/module/features/sleepTimer.js +1 -1
  102. package/lib/module/utils/LazyNativeEmitter.js +8 -5
  103. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  104. package/lib/typescript/src/AudioBrowser.d.ts +0 -1
  105. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  106. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  107. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  108. package/lib/typescript/src/features/errors.d.ts +3 -3
  109. package/lib/typescript/src/features/favorites.d.ts +1 -1
  110. package/lib/typescript/src/features/gate.d.ts +3 -2
  111. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  112. package/lib/typescript/src/features/metadata.d.ts +3 -3
  113. package/lib/typescript/src/features/network.d.ts +1 -1
  114. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  115. package/lib/typescript/src/features/output.d.ts +1 -1
  116. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  117. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  118. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  119. package/lib/typescript/src/features/playback/state.d.ts +1 -1
  120. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  121. package/lib/typescript/src/features/player/options.d.ts +1 -1
  122. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  123. package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
  124. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  125. package/lib/typescript/src/features/queue/activeTrack.d.ts +1 -1
  126. package/lib/typescript/src/features/queue/queue.d.ts +2 -2
  127. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  128. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  129. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  130. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  131. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  132. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +8 -4
  133. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  134. package/package.json +3 -1
  135. package/src/AudioBrowser.ts +4 -1
  136. package/src/features/carConnection.ts +1 -1
  137. package/src/features/equalizer.ts +1 -1
  138. package/src/features/errors.ts +3 -3
  139. package/src/features/favorites.ts +1 -1
  140. package/src/features/gate.ts +1 -1
  141. package/src/features/metadata.ts +3 -3
  142. package/src/features/network.ts +1 -1
  143. package/src/features/nowPlaying.ts +1 -1
  144. package/src/features/output.ts +1 -1
  145. package/src/features/playback/playWhenReady.ts +1 -1
  146. package/src/features/playback/playing.ts +1 -1
  147. package/src/features/playback/progress.ts +1 -1
  148. package/src/features/playback/state.ts +1 -1
  149. package/src/features/playback/trackPlaybackTime.test.ts +20 -6
  150. package/src/features/playback/trackPlaybackTime.ts +0 -5
  151. package/src/features/playback/volume.ts +1 -1
  152. package/src/features/player/options.ts +2 -20
  153. package/src/features/player/setup.ts +1 -1
  154. package/src/features/player/validateOptions.ts +22 -0
  155. package/src/features/queue/activeTrack.ts +1 -1
  156. package/src/features/queue/queue.ts +2 -2
  157. package/src/features/queue/repeatMode.ts +1 -1
  158. package/src/features/queue/shuffle.ts +1 -1
  159. package/src/features/remoteControls.ts +13 -92
  160. package/src/features/sleepTimer.ts +1 -1
  161. package/src/public-surface.test.ts +116 -0
  162. package/src/utils/LazyNativeEmitter.ts +9 -6
@@ -46,7 +46,7 @@ class RemoteCommandController {
46
46
  private let logger = Logger(subsystem: "com.audiobrowser", category: "RemoteCommandController")
47
47
  private var center: MPRemoteCommandCenter
48
48
 
49
- weak var callbacks: TrackPlayerCallbacks?
49
+ weak var callbacks: RemoteCommandCallbacks?
50
50
 
51
51
  var commandTargetPointers: [String: Any] = [:]
52
52
  private var enabledCommands: [RemoteCommand] = []
@@ -72,7 +72,7 @@ class RemoteCommandController {
72
72
  */
73
73
  init(
74
74
  remoteCommandCenter: MPRemoteCommandCenter = MPRemoteCommandCenter.shared(),
75
- callbacks: TrackPlayerCallbacks? = nil,
75
+ callbacks: RemoteCommandCallbacks? = nil,
76
76
  ) {
77
77
  center = remoteCommandCenter
78
78
  self.callbacks = callbacks
@@ -112,13 +112,16 @@ class RemoteCommandController {
112
112
  }
113
113
 
114
114
  func enable(commands: [RemoteCommand]) {
115
- let commandsToDisable = enabledCommands.filter { command in
116
- !commands.contains(command)
117
- }
115
+ let commandsToDisable = RemoteCommand.commandsToDisable(
116
+ enabled: enabledCommands, replacedBy: commands,
117
+ )
118
118
 
119
119
  enabledCommands = commands
120
- commands.forEach { self.enable(command: $0) }
120
+ // Disable first. The key diff already makes the two sets disjoint, so this
121
+ // is belt-and-braces: it keeps a disable from tearing down a command the
122
+ // enable pass just configured should the two ever overlap again.
121
123
  disable(commands: commandsToDisable)
124
+ commands.forEach { self.enable(command: $0) }
122
125
  }
123
126
 
124
127
  func disable(commands: [RemoteCommand]) {
@@ -246,7 +246,9 @@ struct ResetTests {
246
246
  c.capture(position: 25.0)
247
247
 
248
248
  // Let the (immediately-cancelled, itemless) seek completion drain.
249
- for _ in 0 ..< 20 { await Task.yield() }
249
+ for _ in 0 ..< 20 {
250
+ await Task.yield()
251
+ }
250
252
 
251
253
  #expect(spy.calls.isEmpty)
252
254
  #expect(c.pendingTime == 25.0)
@@ -57,12 +57,12 @@ struct MediaIntentCriteriaFromTests {
57
57
  mediaTypeMode: String? = nil,
58
58
  reference: MediaIntentCriteria.Reference = .unknown,
59
59
  hasMediaType: Bool = false,
60
- appName: String? = "Radio Garden"
60
+ appName: String? = "Radio Garden",
61
61
  ) -> MediaIntentCriteria {
62
62
  MediaIntentCriteria.from(
63
63
  mediaName: mediaName, genreNames: genreNames, artistName: artistName,
64
64
  albumName: albumName, mediaTypeMode: mediaTypeMode,
65
- reference: reference, hasMediaType: hasMediaType, appName: appName
65
+ reference: reference, hasMediaType: hasMediaType, appName: appName,
66
66
  )
67
67
  }
68
68
 
@@ -120,7 +120,7 @@ struct TransitionTests {
120
120
  @MainActor
121
121
  private func makeRetrying() -> (
122
122
  PlaybackCoordinator, MockRetryHandling, MockPlaybackCoordinatorCallbacks,
123
- MockPlaybackEffectHandler
123
+ MockPlaybackEffectHandler,
124
124
  ) {
125
125
  let retryHandler = MockRetryHandling()
126
126
  retryHandler.isRetryableResult = true
@@ -0,0 +1,156 @@
1
+ import Foundation
2
+ import MediaPlayer
3
+ import Testing
4
+
5
+ @testable import AudioBrowserTestable
6
+
7
+ /// Exercises the controller against the real `MPRemoteCommandCenter`.
8
+ ///
9
+ /// `MPRemoteCommandCenter`'s `init()` is unavailable, so `shared()` — a
10
+ /// process-wide singleton — is the only center available. The suite is
11
+ /// `.serialized` because of that shared state, and every test resets the
12
+ /// commands it touches through `reset()`.
13
+ @Suite("RemoteCommandController", .serialized)
14
+ @MainActor
15
+ struct RemoteCommandControllerTests {
16
+ final class SpyCallbacks: RemoteCommandCallbacks {
17
+ var events: [String] = []
18
+ func remotePlay() { events.append("play") }
19
+ func remotePause() { events.append("pause") }
20
+ func remoteStop() { events.append("stop") }
21
+ func remotePlayPause() { events.append("playPause") }
22
+ func remoteNext() { events.append("next") }
23
+ func remotePrevious() { events.append("previous") }
24
+ func remoteJumpForward(interval: Double) { events.append("jumpForward:\(interval)") }
25
+ func remoteJumpBackward(interval: Double) { events.append("jumpBackward:\(interval)") }
26
+ func remoteSeek(position: Double) { events.append("seek:\(position)") }
27
+ func remoteChangeRepeatMode(mode: RepeatMode) { events.append("repeat:\(mode)") }
28
+ func remoteChangeShuffleMode(enabled: Bool) { events.append("shuffle:\(enabled)") }
29
+ func remoteChangePlaybackRate(rate: Float) { events.append("rate:\(rate)") }
30
+ }
31
+
32
+ /// Clears every command on the shared center so a test starts from a known
33
+ /// state regardless of what ran before it.
34
+ private func makeController() -> (RemoteCommandController, SpyCallbacks) {
35
+ let center = MPRemoteCommandCenter.shared()
36
+ let reset = RemoteCommandController(remoteCommandCenter: center)
37
+ reset.enable(commands: RemoteCommand.all())
38
+ reset.disableAll()
39
+
40
+ let spy = SpyCallbacks()
41
+ return (RemoteCommandController(remoteCommandCenter: center, callbacks: spy), spy)
42
+ }
43
+
44
+ /// The reported bug: a changed jump interval used to disable the very command
45
+ /// it had just re-enabled, removing the skip button from the lock screen,
46
+ /// Control Center and CarPlay while `enabledCommands` still reported it on.
47
+ @Test func changedJumpIntervalKeepsCommandEnabled() {
48
+ let (controller, _) = makeController()
49
+ let center = MPRemoteCommandCenter.shared()
50
+
51
+ controller.enable(commands: [
52
+ .play, .skipForward(preferredIntervals: [15]), .skipBackward(preferredIntervals: [15]),
53
+ ])
54
+ #expect(center.skipForwardCommand.isEnabled)
55
+
56
+ controller.enable(commands: [
57
+ .play, .skipForward(preferredIntervals: [30]), .skipBackward(preferredIntervals: [30]),
58
+ ])
59
+
60
+ #expect(center.skipForwardCommand.isEnabled)
61
+ #expect(center.skipBackwardCommand.isEnabled)
62
+ #expect(controller.commandTargetPointers["skipForward"] != nil)
63
+ #expect(controller.commandTargetPointers["skipBackward"] != nil)
64
+ #expect(center.skipForwardCommand.preferredIntervals == [30])
65
+ #expect(center.skipBackwardCommand.preferredIntervals == [30])
66
+ }
67
+
68
+ @Test func changedPlaybackRatesKeepCommandEnabled() {
69
+ let (controller, _) = makeController()
70
+ let center = MPRemoteCommandCenter.shared()
71
+
72
+ controller.enable(commands: [.changePlaybackRate(supportedPlaybackRates: [1.0])])
73
+ controller.enable(commands: [.changePlaybackRate(supportedPlaybackRates: [1.0, 1.5, 2.0])])
74
+
75
+ #expect(center.changePlaybackRateCommand.isEnabled)
76
+ #expect(controller.commandTargetPointers["changePlaybackRate"] != nil)
77
+ #expect(center.changePlaybackRateCommand.supportedPlaybackRates == [1.0, 1.5, 2.0])
78
+ }
79
+
80
+ @Test func removedCommandIsDisabledAndUntargeted() {
81
+ let (controller, _) = makeController()
82
+ let center = MPRemoteCommandCenter.shared()
83
+
84
+ controller.enable(commands: [.play, .pause, .next])
85
+ #expect(center.nextTrackCommand.isEnabled)
86
+
87
+ controller.enable(commands: [.play, .pause])
88
+
89
+ #expect(!center.nextTrackCommand.isEnabled)
90
+ #expect(controller.commandTargetPointers["nextTrack"] == nil)
91
+ #expect(center.playCommand.isEnabled)
92
+ #expect(controller.commandTargetPointers["play"] != nil)
93
+ }
94
+
95
+ @Test func disableAllClearsEveryTarget() {
96
+ let (controller, _) = makeController()
97
+ let center = MPRemoteCommandCenter.shared()
98
+
99
+ controller.enable(commands: [.play, .pause, .skipForward(preferredIntervals: [15])])
100
+ controller.disableAll()
101
+
102
+ #expect(controller.commandTargetPointers.isEmpty)
103
+ #expect(!center.playCommand.isEnabled)
104
+ #expect(!center.pauseCommand.isEnabled)
105
+ #expect(!center.skipForwardCommand.isEnabled)
106
+ }
107
+
108
+ /// Re-enabling the same set must not accumulate handlers: `enableRemoteCommand`
109
+ /// removes the previous target before adding the new one.
110
+ @Test func reEnablingKeepsOneTargetPerCommand() {
111
+ let (controller, _) = makeController()
112
+
113
+ controller.enable(commands: [.play, .pause])
114
+ let first = controller.commandTargetPointers["play"]
115
+ controller.enable(commands: [.play, .pause])
116
+ let second = controller.commandTargetPointers["play"]
117
+
118
+ #expect(controller.commandTargetPointers.count == 2)
119
+ #expect(first as AnyObject? !== second as AnyObject?)
120
+ }
121
+
122
+ /// `setSkipAvailability` greys out next/previous when the queue has nowhere to
123
+ /// go, and the state must survive a later `enable(commands:)`.
124
+ @Test func skipAvailabilitySurvivesReEnable() {
125
+ let (controller, _) = makeController()
126
+ let center = MPRemoteCommandCenter.shared()
127
+
128
+ controller.enable(commands: [.next, .previous])
129
+ controller.setSkipAvailability(canNext: false, canPrevious: true)
130
+ #expect(!center.nextTrackCommand.isEnabled)
131
+ #expect(center.previousTrackCommand.isEnabled)
132
+
133
+ controller.enable(commands: [.next, .previous, .play])
134
+
135
+ #expect(!center.nextTrackCommand.isEnabled)
136
+ #expect(center.previousTrackCommand.isEnabled)
137
+ }
138
+
139
+ @Test func shuffleAndRepeatStateSurviveCommandCenterSwitch() {
140
+ let (controller, _) = makeController()
141
+ let center = MPRemoteCommandCenter.shared()
142
+
143
+ controller.enable(commands: [.changeShuffleMode, .changeRepeatMode])
144
+ controller.updateShuffleMode(true)
145
+ controller.updateRepeatMode(.queue)
146
+
147
+ // Same center — switchCommandCenter no-ops, but the restore path is the
148
+ // interesting part: state is re-applied from the stored values.
149
+ controller.switchCommandCenter(center)
150
+
151
+ #expect(center.changeShuffleModeCommand.currentShuffleType == .items)
152
+ #expect(center.changeRepeatModeCommand.currentRepeatType == .all)
153
+ #expect(center.changeShuffleModeCommand.isEnabled)
154
+ #expect(controller.commandTargetPointers["changeShuffleMode"] != nil)
155
+ }
156
+ }
@@ -0,0 +1,67 @@
1
+ import Foundation
2
+ import Testing
3
+
4
+ @testable import AudioBrowserTestable
5
+
6
+ @Suite("RemoteCommand")
7
+ struct RemoteCommandTests {
8
+ @Test func removedCommandsAreDisabled() {
9
+ let toDisable = RemoteCommand.commandsToDisable(
10
+ enabled: [.play, .pause, .next, .previous],
11
+ replacedBy: [.play, .pause],
12
+ )
13
+ #expect(toDisable.map(\.key) == ["nextTrack", "previousTrack"])
14
+ }
15
+
16
+ @Test func retainedCommandsAreNotDisabled() {
17
+ let toDisable = RemoteCommand.commandsToDisable(
18
+ enabled: [.play, .pause],
19
+ replacedBy: [.play, .pause, .stop],
20
+ )
21
+ #expect(toDisable.isEmpty)
22
+ }
23
+
24
+ /// A changed jump interval is the same command with a new associated value —
25
+ /// diffing by `==` would report it as removed and disable the button that the
26
+ /// update had just re-enabled.
27
+ @Test func changedJumpIntervalIsNotDisabled() {
28
+ let toDisable = RemoteCommand.commandsToDisable(
29
+ enabled: [.skipForward(preferredIntervals: [15]), .skipBackward(preferredIntervals: [15])],
30
+ replacedBy: [.skipForward(preferredIntervals: [30]), .skipBackward(preferredIntervals: [30])],
31
+ )
32
+ #expect(toDisable.isEmpty)
33
+ }
34
+
35
+ @Test func changedPlaybackRatesAreNotDisabled() {
36
+ let toDisable = RemoteCommand.commandsToDisable(
37
+ enabled: [.changePlaybackRate(supportedPlaybackRates: [1.0])],
38
+ replacedBy: [.changePlaybackRate(supportedPlaybackRates: [1.0, 1.5, 2.0])],
39
+ )
40
+ #expect(toDisable.isEmpty)
41
+ }
42
+
43
+ @Test func everyCommandIsDisabledWhenNoneRemain() {
44
+ let toDisable = RemoteCommand.commandsToDisable(
45
+ enabled: RemoteCommand.all(),
46
+ replacedBy: [],
47
+ )
48
+ #expect(toDisable.count == RemoteCommand.all().count)
49
+ }
50
+
51
+ /// `key` is the identity used both for the diff above and for the
52
+ /// `commandTargetPointers` slot each command's handler lives in. Two cases
53
+ /// sharing a key would silently share one slot: enabling one would remove the
54
+ /// other's handler.
55
+ @Test func keysAreUnique() {
56
+ let all = RemoteCommand.all()
57
+ #expect(Set(all.map(\.key)).count == all.count)
58
+ }
59
+
60
+ @Test func droppedJumpCommandIsStillDisabled() {
61
+ let toDisable = RemoteCommand.commandsToDisable(
62
+ enabled: [.play, .skipForward(preferredIntervals: [15])],
63
+ replacedBy: [.play],
64
+ )
65
+ #expect(toDisable.map(\.key) == ["skipForward"])
66
+ }
67
+ }
@@ -7,12 +7,12 @@ struct SectionScopeTests {
7
7
  }
8
8
 
9
9
  private func runSrcs(_ section: SectionScope.Section?) -> [String?]? {
10
- guard case .run(let tracks) = section else { return nil }
10
+ guard case let .run(tracks) = section else { return nil }
11
11
  return tracks.map(\.src)
12
12
  }
13
13
 
14
14
  private func imageRowSrcs(_ section: SectionScope.Section?) -> [String?]? {
15
- guard case .imageRow(let items) = section else { return nil }
15
+ guard case let .imageRow(items) = section else { return nil }
16
16
  return items.map(\.src)
17
17
  }
18
18
 
@@ -5,11 +5,11 @@ import Testing
5
5
  struct TabBarEntriesTests {
6
6
  private func tab(
7
7
  _ title: String, url: String, artwork: String? = nil,
8
- artworkSource: ImageSource? = nil, artist: String? = nil
8
+ artworkSource: ImageSource? = nil, artist: String? = nil,
9
9
  ) -> Track {
10
10
  Track(
11
11
  id: url, url: url, title: title, artist: artist, artwork: artwork.map { .first($0) },
12
- artworkSource: artworkSource
12
+ artworkSource: artworkSource,
13
13
  )
14
14
  }
15
15
 
@@ -22,7 +22,7 @@ struct TabBarEntriesTests {
22
22
  #expect(
23
23
  !TabBarEntries.same(
24
24
  [tab("Explore", url: "/explore")],
25
- [tab("Verkennen", url: "/explore")]
25
+ [tab("Verkennen", url: "/explore")],
26
26
  ))
27
27
  }
28
28
 
@@ -30,7 +30,7 @@ struct TabBarEntriesTests {
30
30
  #expect(
31
31
  !TabBarEntries.same(
32
32
  [tab("Explore", url: "/explore")],
33
- [tab("Explore", url: "/discover")]
33
+ [tab("Explore", url: "/discover")],
34
34
  ))
35
35
  }
36
36
 
@@ -38,7 +38,7 @@ struct TabBarEntriesTests {
38
38
  #expect(
39
39
  !TabBarEntries.same(
40
40
  [tab("Explore", url: "/explore", artwork: "sf:globe")],
41
- [tab("Explore", url: "/explore", artwork: "sf:map")]
41
+ [tab("Explore", url: "/explore", artwork: "sf:map")],
42
42
  ))
43
43
  }
44
44
 
@@ -46,7 +46,7 @@ struct TabBarEntriesTests {
46
46
  #expect(
47
47
  !TabBarEntries.same(
48
48
  [tab("Explore", url: "/explore", artworkSource: ImageSource(uri: "https://a/1.png"))],
49
- [tab("Explore", url: "/explore", artworkSource: ImageSource(uri: "https://a/2.png"))]
49
+ [tab("Explore", url: "/explore", artworkSource: ImageSource(uri: "https://a/2.png"))],
50
50
  ))
51
51
  }
52
52
 
@@ -65,7 +65,7 @@ struct TabBarEntriesTests {
65
65
  #expect(
66
66
  TabBarEntries.same(
67
67
  [tab("Explore", url: "/explore", artist: "a")],
68
- [tab("Explore", url: "/explore", artist: "b")]
68
+ [tab("Explore", url: "/explore", artist: "b")],
69
69
  ))
70
70
  }
71
71
  }
@@ -753,7 +753,7 @@ extension TrackPlayer {
753
753
  repeatMode: repeatMode.persistedString,
754
754
  shuffleEnabled: shuffleEnabled,
755
755
  playbackSpeed: rate,
756
- )
756
+ ),
757
757
  )
758
758
  }
759
759
 
@@ -9,10 +9,11 @@ import NitroModules
9
9
  /// Note: Method names are prefixed to avoid conflicts with JS callback property names.
10
10
  /// All callbacks are called from the main actor since TrackPlayer is @MainActor.
11
11
  ///
12
- /// Extends PlaybackCoordinatorCallbacks for the state machine / coordinator callbacks,
13
- /// then adds metadata, seek, remote, and configuration callbacks that are TrackPlayer-specific.
12
+ /// Extends PlaybackCoordinatorCallbacks for the state machine / coordinator callbacks
13
+ /// and RemoteCommandCallbacks for the remote-command events, then adds metadata,
14
+ /// seek, and configuration callbacks that are TrackPlayer-specific.
14
15
  @MainActor
15
- protocol TrackPlayerCallbacks: PlaybackCoordinatorCallbacks {
16
+ protocol TrackPlayerCallbacks: PlaybackCoordinatorCallbacks, RemoteCommandCallbacks {
16
17
  // MARK: - Metadata Events
17
18
 
18
19
  /// Called when common metadata is received.
@@ -34,32 +35,7 @@ protocol TrackPlayerCallbacks: PlaybackCoordinatorCallbacks {
34
35
 
35
36
  // MARK: - Remote Control Events
36
37
 
37
- /// Called when play is triggered remotely.
38
- func remotePlay()
39
-
40
- /// Called when pause is triggered remotely.
41
- func remotePause()
42
-
43
- /// Called when stop is triggered remotely.
44
- func remoteStop()
45
-
46
- /// Called when toggle play/pause is triggered remotely.
47
- func remotePlayPause()
48
-
49
- /// Called when next is triggered remotely.
50
- func remoteNext()
51
-
52
- /// Called when previous is triggered remotely.
53
- func remotePrevious()
54
-
55
- /// Called when jump forward is triggered remotely.
56
- func remoteJumpForward(interval: Double)
57
-
58
- /// Called when jump backward is triggered remotely.
59
- func remoteJumpBackward(interval: Double)
60
-
61
- /// Called when seek is triggered remotely.
62
- func remoteSeek(position: Double)
38
+ // The command-center events live in RemoteCommandCallbacks, inherited above.
63
39
 
64
40
  /// Called when change playback position is triggered remotely.
65
41
  func remoteChangePlaybackPosition(position: Double)
@@ -70,15 +46,6 @@ protocol TrackPlayerCallbacks: PlaybackCoordinatorCallbacks {
70
46
  /// Called when play from search is triggered remotely.
71
47
  func remotePlaySearch(query: String)
72
48
 
73
- /// Called when repeat mode change is triggered remotely (CarPlay/lock screen).
74
- func remoteChangeRepeatMode(mode: RepeatMode)
75
-
76
- /// Called when shuffle mode change is triggered remotely (CarPlay/lock screen).
77
- func remoteChangeShuffleMode(enabled: Bool)
78
-
79
- /// Called when playback rate change is triggered remotely (CarPlay/lock screen).
80
- func remoteChangePlaybackRate(rate: Float)
81
-
82
49
  // MARK: - Configuration Events
83
50
 
84
51
  /// Called when options are changed.
@@ -14,17 +14,6 @@ Object.keys(_features).forEach(function (key) {
14
14
  }
15
15
  });
16
16
  });
17
- var _native = require("./native");
18
- Object.keys(_native).forEach(function (key) {
19
- if (key === "default" || key === "__esModule") return;
20
- if (key in exports && exports[key] === _native[key]) return;
21
- Object.defineProperty(exports, key, {
22
- enumerable: true,
23
- get: function () {
24
- return _native[key];
25
- }
26
- });
27
- });
28
17
  var _types = require("./types");
29
18
  Object.keys(_types).forEach(function (key) {
30
19
  if (key === "default" || key === "__esModule") return;
@@ -1 +1 @@
1
- {"version":3,"names":["_features","require","Object","keys","forEach","key","exports","defineProperty","enumerable","get","_native","_types","_useDebug"],"sourceRoot":"../../src","sources":["AudioBrowser.ts"],"mappings":";;;;;AAAA,IAAAA,SAAA,GAAAC,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAH,SAAA,EAAAI,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAL,SAAA,CAAAK,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAT,SAAA,CAAAK,GAAA;IAAA;EAAA;AAAA;AACA,IAAAK,OAAA,GAAAT,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAO,OAAA,EAAAN,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAK,OAAA,CAAAL,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAC,OAAA,CAAAL,GAAA;IAAA;EAAA;AAAA;AACA,IAAAM,MAAA,GAAAV,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAQ,MAAA,EAAAP,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAM,MAAA,CAAAN,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAE,MAAA,CAAAN,GAAA;IAAA;EAAA;AAAA;AACA,IAAAO,SAAA,GAAAX,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAS,SAAA,EAAAR,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAO,SAAA,CAAAP,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAG,SAAA,CAAAP,GAAA;IAAA;EAAA;AAAA","ignoreList":[]}
1
+ {"version":3,"names":["_features","require","Object","keys","forEach","key","exports","defineProperty","enumerable","get","_types","_useDebug"],"sourceRoot":"../../src","sources":["AudioBrowser.ts"],"mappings":";;;;;AAIA,IAAAA,SAAA,GAAAC,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAH,SAAA,EAAAI,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAL,SAAA,CAAAK,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAT,SAAA,CAAAK,GAAA;IAAA;EAAA;AAAA;AACA,IAAAK,MAAA,GAAAT,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAO,MAAA,EAAAN,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAK,MAAA,CAAAL,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAC,MAAA,CAAAL,GAAA;IAAA;EAAA;AAAA;AACA,IAAAM,SAAA,GAAAV,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAQ,SAAA,EAAAP,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAM,SAAA,CAAAN,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAE,SAAA,CAAAN,GAAA;IAAA;EAAA;AAAA","ignoreList":[]}
@@ -26,7 +26,7 @@ function isCarConnected() {
26
26
  * Subscribes to car connection changes (CarPlay on iOS, Android Auto /
27
27
  * Android Automotive on Android). Never fires on web.
28
28
  * @param callback - Called with the new connection state
29
- * @returns Cleanup function to unsubscribe
29
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
30
30
  */
31
31
  const onCarConnectedChanged = exports.onCarConnectedChanged = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onCarConnectedChanged = cb);
32
32
 
@@ -53,7 +53,7 @@ function setEqualizerLevels(levels) {
53
53
  /**
54
54
  * Subscribes to equalizer settings changes (Android only).
55
55
  * @param callback - Called when equalizer settings change
56
- * @returns Cleanup function to unsubscribe
56
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
57
57
  */
58
58
  const onEqualizerChanged = exports.onEqualizerChanged = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onEqualizerChanged = cb);
59
59
 
@@ -83,7 +83,7 @@ function getPlaybackError() {
83
83
  /**
84
84
  * Subscribes to playback error events.
85
85
  * @param callback - Called when a playback error occurs
86
- * @returns Cleanup function to unsubscribe
86
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
87
87
  */
88
88
  const onPlaybackError = exports.onPlaybackError = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onPlaybackError = cb);
89
89
 
@@ -111,7 +111,7 @@ function getNavigationError() {
111
111
  /**
112
112
  * Subscribes to navigation error events.
113
113
  * @param callback - Called when a navigation error occurs
114
- * @returns Cleanup function to unsubscribe
114
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
115
115
  */
116
116
  const onNavigationError = exports.onNavigationError = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onNavigationError = cb);
117
117
 
@@ -139,7 +139,7 @@ function getFormattedNavigationError() {
139
139
  /**
140
140
  * Subscribes to formatted navigation error changes.
141
141
  * @param callback - Called when the formatted navigation error changes
142
- * @returns Cleanup function to unsubscribe
142
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
143
143
  */
144
144
  const onFormattedNavigationError = exports.onFormattedNavigationError = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onFormattedNavigationError = cb);
145
145
 
@@ -83,7 +83,7 @@ function setFavorites(favorites) {
83
83
  * The native side has already updated the track's favorite state and UI.
84
84
  *
85
85
  * @param callback - Called with the track and its new favorite state
86
- * @returns Cleanup function to unsubscribe
86
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
87
87
  *
88
88
  * @example
89
89
  * ```ts
@@ -143,7 +143,7 @@ function clearGate() {
143
143
 
144
144
  /**
145
145
  * Subscribes to gate events (fired when a request is gated).
146
- * @returns Cleanup function to unsubscribe.
146
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
147
147
  */
148
148
  const onGate = exports.onGate = _LazyNativeEmitter.LazyNativeEmitter.emitterize(cb => _native.nativeBrowser.onGate = cb);
149
149
  //# sourceMappingURL=gate.js.map
@@ -34,21 +34,21 @@ var _NativeUpdatedValue = require("../utils/NativeUpdatedValue");
34
34
  /**
35
35
  * Subscribes to chapter metadata events.
36
36
  * @param callback - Called when chapter metadata is received
37
- * @returns Cleanup function to unsubscribe
37
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
38
38
  */
39
39
  const onChapterMetadata = exports.onChapterMetadata = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onChapterMetadata = cb);
40
40
 
41
41
  /**
42
42
  * Subscribes to track metadata events.
43
43
  * @param callback - Called when static track metadata is received
44
- * @returns Cleanup function to unsubscribe
44
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
45
45
  */
46
46
  const onTrackMetadata = exports.onTrackMetadata = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onTrackMetadata = cb);
47
47
 
48
48
  /**
49
49
  * Subscribes to timed metadata events (ICY/ID3 from live streams).
50
50
  * @param callback - Called when stream metadata is received (title, artist from ICY/ID3 tags)
51
- * @returns Cleanup function to unsubscribe
51
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
52
52
  */
53
53
  const onTimedMetadata = exports.onTimedMetadata = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onTimedMetadata = cb);
54
54
  //# sourceMappingURL=metadata.js.map
@@ -24,7 +24,7 @@ function getOnline() {
24
24
  /**
25
25
  * Subscribes to network connectivity changes.
26
26
  * @param callback - Called when network state changes
27
- * @returns Cleanup function to unsubscribe
27
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
28
28
  */
29
29
  const onOnlineChanged = exports.onOnlineChanged = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onOnlineChanged = cb);
30
30
 
@@ -80,7 +80,7 @@ function clearNowPlayingFlash() {
80
80
  * Subscribes to now playing metadata change events.
81
81
  * Fires when updateNowPlaying is called or when the track changes.
82
82
  * @param callback - Called when now playing metadata changes
83
- * @returns Cleanup function to unsubscribe
83
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
84
84
  */
85
85
  const onNowPlayingChanged = exports.onNowPlayingChanged = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onNowPlayingChanged = cb);
86
86
 
@@ -52,7 +52,7 @@ function getOutput() {
52
52
  /**
53
53
  * Subscribes to current-output changes (headphones unplugged, Bluetooth speaker
54
54
  * connected, AirPlay/route selected). Fires on iOS and Android 11+; never below.
55
- * @returns Cleanup function to unsubscribe.
55
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
56
56
  */
57
57
  const onOutputChanged = exports.onOutputChanged = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onOutputChanged = cb);
58
58
 
@@ -39,7 +39,7 @@ function setPlayWhenReady(playWhenReady) {
39
39
  /**
40
40
  * Subscribes to play when ready changes.
41
41
  * @param callback - Called when playWhenReady changes
42
- * @returns Cleanup function to unsubscribe
42
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
43
43
  */
44
44
  const onPlayWhenReadyChanged = exports.onPlayWhenReadyChanged = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onPlaybackPlayWhenReadyChanged = cb);
45
45
 
@@ -25,7 +25,7 @@ function getPlayingState() {
25
25
  /**
26
26
  * Subscribes to playing state changes.
27
27
  * @param callback - Called when playing or buffering state changes
28
- * @returns Cleanup function to unsubscribe
28
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
29
29
  */
30
30
  const onPlayingState = exports.onPlayingState = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onPlaybackPlayingState = cb);
31
31
 
@@ -35,7 +35,7 @@ function getProgress() {
35
35
  /**
36
36
  * Subscribes to playback progress updates.
37
37
  * @param callback - Called periodically with playback progress updates
38
- * @returns Cleanup function to unsubscribe
38
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
39
39
  */
40
40
  const onProgressUpdated = exports.onProgressUpdated = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onPlaybackProgressUpdated = cb);
41
41
 
@@ -46,7 +46,7 @@ function getPlayback() {
46
46
  /**
47
47
  * Subscribes to playback state changes.
48
48
  * @param callback - Called when the playback state changes
49
- * @returns Cleanup function to unsubscribe
49
+ * @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
50
50
  */
51
51
  const onPlaybackChanged = exports.onPlaybackChanged = _NativeUpdatedValue.NativeUpdatedValue.emitterize(cb => _native.nativeBrowser.onPlaybackChanged = cb);
52
52