react-native-audio-browser 0.2.0-next.28 → 0.2.0-next.29

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 (137) hide show
  1. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +3 -4
  2. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +2 -2
  3. package/ios/AVPlayerEngine.swift +334 -0
  4. package/ios/AVPlayerEngineCallbacks.swift +15 -0
  5. package/ios/Browser/BrowseGate.swift +58 -0
  6. package/ios/Browser/BrowserConfig.swift +31 -89
  7. package/ios/Browser/BrowserManager.swift +68 -487
  8. package/ios/Browser/NavigationErrorState.swift +41 -0
  9. package/ios/Browser/PageResolver.swift +227 -0
  10. package/ios/Browser/RequestLayer.swift +90 -0
  11. package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +175 -107
  12. package/ios/CarPlay/CPInterfaceController+Safe.swift +11 -24
  13. package/ios/CarPlay/CarPlayArtworkResolver.swift +35 -83
  14. package/ios/CarPlay/CarPlayController.swift +42 -1442
  15. package/ios/CarPlay/CarPlayImageLoader.swift +42 -83
  16. package/ios/CarPlay/CarPlayListItemFactory.swift +9 -23
  17. package/ios/CarPlay/CarPlayNowPlayingManager.swift +119 -129
  18. package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
  19. package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
  20. package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
  21. package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
  22. package/ios/CarPlay/CarPlaySession.swift +209 -0
  23. package/ios/CarPlay/CarPlayTemplates.swift +63 -0
  24. package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
  25. package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
  26. package/ios/CarPlay/RNABMediaAddHandler.swift +2 -18
  27. package/ios/CarPlay/RNABMediaAffinityHandler.swift +5 -21
  28. package/ios/CarPlay/RNABMediaIntentHandler.swift +2 -3
  29. package/ios/Extension/NavigationError+Conversion.swift +7 -4
  30. package/ios/Extension/UIImage+Adaptive.swift +12 -0
  31. package/ios/Extension/URLError+Connectivity.swift +15 -0
  32. package/ios/Http/HttpClient.swift +63 -145
  33. package/ios/HybridAudioBrowser+Browser.swift +153 -0
  34. package/ios/HybridAudioBrowser+Options.swift +144 -0
  35. package/ios/HybridAudioBrowser+Player.swift +397 -0
  36. package/ios/HybridAudioBrowser+PlayerCallbacks.swift +190 -0
  37. package/ios/HybridAudioBrowser+Stubs.swift +46 -0
  38. package/ios/HybridAudioBrowser.swift +74 -1687
  39. package/ios/Model/NitroTypeStubs.swift +18 -1
  40. package/ios/Model/PlayerUpdateOptions.swift +1 -1
  41. package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
  42. package/ios/Observer/ItemObserver.swift +90 -0
  43. package/ios/Observer/PlayerObserver.swift +59 -0
  44. package/ios/Option/SessionCategories.swift +41 -92
  45. package/ios/Player/AudioSessionController.swift +177 -0
  46. package/ios/Player/DeferredSeek.swift +70 -0
  47. package/ios/Player/ErrorStatus.swift +27 -0
  48. package/ios/Player/MediaLoader.swift +24 -11
  49. package/ios/Player/NowPlayingUpdater.swift +157 -58
  50. package/ios/Player/PlaybackFailure.swift +125 -0
  51. package/ios/Player/PlaybackPersistence.swift +73 -0
  52. package/ios/Player/Player.swift +1083 -0
  53. package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
  54. package/ios/Player/PlayerEngine.swift +89 -0
  55. package/ios/Player/PlayerStatusTypes.swift +2 -6
  56. package/ios/Player/QueueManager.swift +52 -59
  57. package/ios/Player/RetryLoop.swift +261 -0
  58. package/ios/Player/SleepTimerManager.swift +1 -1
  59. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +2 -3
  60. package/ios/RemoteCommand/RemoteCommandController.swift +85 -367
  61. package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -356
  62. package/ios/Tests/DeferredSeekTests.swift +70 -0
  63. package/ios/Tests/EmitterTests.swift +29 -0
  64. package/ios/Tests/GenerationMemoTests.swift +121 -0
  65. package/ios/Tests/LRUCacheTests.swift +4 -5
  66. package/ios/Tests/MediaLoaderTests.swift +12 -33
  67. package/ios/Tests/ObserverTests.swift +88 -0
  68. package/ios/Tests/PlaybackErrorTests.swift +76 -0
  69. package/ios/Tests/PlaybackFailureTests.swift +189 -0
  70. package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
  71. package/ios/Tests/PlayerPolicyTests.swift +405 -0
  72. package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +298 -248
  73. package/ios/Tests/PlayingStateTests.swift +42 -0
  74. package/ios/Tests/QueueManagerTests.swift +76 -107
  75. package/ios/Tests/ReadyValueTests.swift +73 -0
  76. package/ios/Tests/RemoteCommandControllerTests.swift +10 -30
  77. package/ios/Tests/RemoteCommandTests.swift +2 -4
  78. package/ios/Tests/RequestLayerTests.swift +112 -0
  79. package/ios/Tests/RetryLoopTests.swift +270 -0
  80. package/ios/Tests/Support/TestTypes.swift +111 -112
  81. package/ios/Tests/TrackIdTemplateTests.swift +1 -1
  82. package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
  83. package/ios/TrackSelector.swift +1 -1
  84. package/ios/Util/ArtworkImageFetcher.swift +42 -0
  85. package/ios/Util/Emitter.swift +42 -0
  86. package/ios/Util/GenerationMemo.swift +36 -0
  87. package/ios/Util/LRUCache.swift +23 -162
  88. package/ios/Util/NetworkMonitor.swift +1 -1
  89. package/ios/Util/ReadyValue.swift +56 -0
  90. package/ios/Util/SFSymbolRenderer.swift +35 -29
  91. package/ios/Util/SVGProcessor.swift +0 -10
  92. package/lib/commonjs/web/NativeAudioBrowser.js +1 -1
  93. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +1 -1
  94. package/lib/module/web/NativeAudioBrowser.js +1 -1
  95. package/lib/module/web/TrackPlayer/PlayingStateFactory.js +1 -1
  96. package/lib/typescript/src/types/browser.d.ts +3 -3
  97. package/package.json +1 -1
  98. package/src/types/browser.ts +3 -3
  99. package/src/web/NativeAudioBrowser.ts +1 -1
  100. package/src/web/TrackPlayer/PlayingStateFactory.test.ts +1 -1
  101. package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
  102. package/ios/Bridge.h +0 -8
  103. package/ios/Browser/MediaResolveComposer.swift +0 -114
  104. package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
  105. package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
  106. package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
  107. package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
  108. package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
  109. package/ios/Observer/PlayerStateObserver.swift +0 -50
  110. package/ios/Observer/PlayerTimeObserver.swift +0 -55
  111. package/ios/Option/PitchAlgorithms.swift +0 -17
  112. package/ios/Option/TimeEventFrequency.swift +0 -18
  113. package/ios/Player/LoadSeekCoordinator.swift +0 -99
  114. package/ios/Player/MediaLoaderDelegate.swift +0 -11
  115. package/ios/Player/PlaybackCoordinator.swift +0 -761
  116. package/ios/Player/PlaybackEffectHandler.swift +0 -54
  117. package/ios/Player/PlaybackErrorHandler.swift +0 -175
  118. package/ios/Player/PlaybackStateStore.swift +0 -27
  119. package/ios/Player/PlayingStateManager.swift +0 -33
  120. package/ios/Player/RetryManager.swift +0 -384
  121. package/ios/Player/SeekCompletionHandler.swift +0 -5
  122. package/ios/Player/SleepTimerHandling.swift +0 -28
  123. package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
  124. package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -327
  125. package/ios/Tests/MediaResolveTests.swift +0 -126
  126. package/ios/Tests/OnceValueTests.swift +0 -79
  127. package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -549
  128. package/ios/Tests/PlaybackStateStoreTests.swift +0 -49
  129. package/ios/Tests/PlayerTimeObserverTests.swift +0 -37
  130. package/ios/Tests/PlayingStateManagerTests.swift +0 -57
  131. package/ios/Tests/RetryManagerTests.swift +0 -178
  132. package/ios/TrackPlayer.swift +0 -1042
  133. package/ios/TrackPlayerCallbacks.swift +0 -53
  134. package/ios/Util/MetadataAdapter.swift +0 -131
  135. package/ios/Util/OnceValue.swift +0 -104
  136. /package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +0 -0
  137. /package/ios/{Browser → CarPlay}/SectionPresentation.swift +0 -0
@@ -2,8 +2,6 @@ import Foundation
2
2
  import MediaPlayer
3
3
  import os.log
4
4
 
5
- typealias RemoteCommandHandler = @MainActor (MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus
6
-
7
5
  extension RepeatMode {
8
6
  /// Convert to MPRepeatType for MPRemoteCommandCenter sync
9
7
  var mpRepeatType: MPRepeatType {
@@ -32,44 +30,25 @@ extension Bool {
32
30
  }
33
31
  }
34
32
 
35
- /**
36
- Manages MPRemoteCommandCenter integration for media control (lock screen, control center, CarPlay, etc.).
37
-
38
- This controller enables/disables remote commands and routes them to handlers. It provides default handlers
39
- that invoke TrackPlayerCallbacks, but allows customization by setting the lazy handler properties.
40
-
41
- Note: This class is @MainActor because it's owned by TrackPlayer (which is @MainActor) and
42
- MPRemoteCommandCenter handlers are always called on the main thread.
43
- */
33
+ /// Enables the remote commands (lock screen, Control Center, CarPlay, headset) and routes
34
+ /// each event to `RemoteCommandCallbacks`.
44
35
  @MainActor
45
- class RemoteCommandController {
36
+ final class RemoteCommandController {
46
37
  private let logger = Logger(subsystem: "com.audiobrowser", category: "RemoteCommandController")
47
38
  private var center: MPRemoteCommandCenter
48
39
 
49
40
  weak var callbacks: RemoteCommandCallbacks?
50
41
 
51
- var commandTargetPointers: [String: Any] = [:]
52
42
  private var enabledCommands: [RemoteCommand] = []
53
43
 
54
- /// Whether there's a next/previous track to skip to. Stored so it survives
55
- /// command (re)enable and iOS-16 command-center switches; the next/previous
56
- /// commands are enabled in this state rather than unconditionally.
57
- private var canNext = true
58
- private var canPrevious = true
44
+ /// Whether the queue has a next / previous track; read whenever those commands are enabled.
45
+ var skipAvailability: @MainActor () -> (canNext: Bool, canPrevious: Bool) = { (true, true) }
59
46
 
60
- /// Shuffle/repeat display state, stored so it survives a command-center switch
61
- /// (iOS-16 MPNowPlayingSession re-creation). A new center defaults these to
62
- /// off; without re-applying, the CarPlay/lock-screen shuffle & repeat buttons
63
- /// flash off on every switch.
47
+ /// The shuffle/repeat display state, kept so it survives a switch to a new command center,
48
+ /// which starts with both off.
64
49
  private var shuffleEnabled = false
65
50
  private var repeatMode: RepeatMode = .off
66
51
 
67
- /**
68
- Create a new RemoteCommandController.
69
-
70
- - parameter remoteCommandCenter: The MPRemoteCommandCenter used. Default is `MPRemoteCommandCenter.shared()`
71
- - parameter callbacks: The callbacks to invoke for remote command events
72
- */
73
52
  init(
74
53
  remoteCommandCenter: MPRemoteCommandCenter = MPRemoteCommandCenter.shared(),
75
54
  callbacks: RemoteCommandCallbacks? = nil,
@@ -78,383 +57,122 @@ class RemoteCommandController {
78
57
  self.callbacks = callbacks
79
58
  }
80
59
 
81
- /**
82
- Switches to a new remote command center.
83
- This is used when MPNowPlayingSession is created/destroyed on iOS 16+,
84
- as the session has its own command center that must be used instead of the shared one.
85
-
86
- - parameter newCenter: The new MPRemoteCommandCenter to use
87
- */
60
+ /// Moves the enabled commands and their state to `newCenter`: the linked
61
+ /// `MPNowPlayingSession` has its own command center, which replaces the shared one.
88
62
  func switchCommandCenter(_ newCenter: MPRemoteCommandCenter) {
89
- guard newCenter !== center else {
90
- logger.debug("switchCommandCenter: same command center, skipping")
91
- return
92
- }
93
-
94
- logger.info("Switching remote command center")
95
-
96
- // Disable all commands on the old center
97
- let commandsToReEnable = enabledCommands
98
- disable(commands: commandsToReEnable)
99
-
100
- // Switch to new center
63
+ guard newCenter !== center else { return }
64
+ let commands = enabledCommands
65
+ disableAll()
101
66
  center = newCenter
102
-
103
- // Re-enable commands on the new center
104
- enable(commands: commandsToReEnable)
105
-
106
- // Restore shuffle/repeat display state — the new center defaults them to off,
107
- // so without this the shuffle & repeat buttons flash off on every switch.
67
+ enable(commands: commands)
108
68
  center.changeShuffleModeCommand.currentShuffleType = shuffleEnabled.mpShuffleType
109
69
  center.changeRepeatModeCommand.currentRepeatType = repeatMode.mpRepeatType
110
-
111
- logger.info("Switched command center, re-enabled \(commandsToReEnable.count) commands")
70
+ logger.info("Switched command center, re-enabled \(commands.count) commands")
112
71
  }
113
72
 
73
+ /// Enables exactly `commands`, disabling any enabled command they no longer include.
114
74
  func enable(commands: [RemoteCommand]) {
115
- let commandsToDisable = RemoteCommand.commandsToDisable(
116
- enabled: enabledCommands, replacedBy: commands,
117
- )
118
-
75
+ RemoteCommand.commandsToDisable(enabled: enabledCommands, replacedBy: commands).forEach(disable)
119
76
  enabledCommands = commands
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.
123
- disable(commands: commandsToDisable)
124
- commands.forEach { self.enable(command: $0) }
125
- }
126
-
127
- func disable(commands: [RemoteCommand]) {
128
- commands.forEach { self.disable(command: $0) }
77
+ commands.forEach(enable)
129
78
  }
130
79
 
131
- /// Disables all currently enabled remote commands and removes their targets.
132
80
  func disableAll() {
133
- disable(commands: enabledCommands)
134
- enabledCommands.removeAll()
135
- }
136
-
137
- /**
138
- Enables a remote command by setting it up with the provided handler.
139
- Removes any existing target before adding the new one to prevent duplicate handlers.
140
- */
141
- private func enableRemoteCommand(
142
- _ command: MPRemoteCommand,
143
- key: String,
144
- handler: @escaping RemoteCommandHandler,
145
- ) {
146
- command.isEnabled = true
147
- command.removeTarget(commandTargetPointers[key])
148
- commandTargetPointers[key] = command.addTarget(handler: handler)
149
- }
150
-
151
- /**
152
- Disables a remote command and cleans up its target from the command center.
153
- */
154
- private func disableRemoteCommand(_ command: MPRemoteCommand, key: String) {
155
- command.isEnabled = false
156
- command.removeTarget(commandTargetPointers[key])
157
- commandTargetPointers.removeValue(forKey: key)
158
- }
159
-
160
- private func enable(command: RemoteCommand) {
161
- switch command {
162
- case .play:
163
- enableRemoteCommand(center.playCommand, key: command.key, handler: handlePlayCommand)
164
- case .pause:
165
- enableRemoteCommand(center.pauseCommand, key: command.key, handler: handlePauseCommand)
166
- case .stop:
167
- enableRemoteCommand(center.stopCommand, key: command.key, handler: handleStopCommand)
168
- case .togglePlayPause:
169
- enableRemoteCommand(
170
- center.togglePlayPauseCommand,
171
- key: command.key,
172
- handler: handleTogglePlayPauseCommand,
173
- )
174
- case .next:
175
- enableRemoteCommand(
176
- center.nextTrackCommand,
177
- key: command.key,
178
- handler: handleNextTrackCommand,
179
- )
180
- center.nextTrackCommand.isEnabled = canNext
181
- case .previous:
182
- enableRemoteCommand(
183
- center.previousTrackCommand,
184
- key: command.key,
185
- handler: handlePreviousTrackCommand,
186
- )
187
- center.previousTrackCommand.isEnabled = canPrevious
188
- case .changePlaybackPosition:
189
- enableRemoteCommand(
190
- center.changePlaybackPositionCommand,
191
- key: command.key,
192
- handler: handleChangePlaybackPositionCommand,
193
- )
194
- case let .skipForward(preferredIntervals):
195
- center.skipForwardCommand.preferredIntervals = preferredIntervals
196
- enableRemoteCommand(
197
- center.skipForwardCommand,
198
- key: command.key,
199
- handler: handleSkipForwardCommand,
200
- )
201
- case let .skipBackward(preferredIntervals):
202
- center.skipBackwardCommand.preferredIntervals = preferredIntervals
203
- enableRemoteCommand(
204
- center.skipBackwardCommand,
205
- key: command.key,
206
- handler: handleSkipBackwardCommand,
207
- )
208
- case .changeRepeatMode:
209
- enableRemoteCommand(
210
- center.changeRepeatModeCommand,
211
- key: command.key,
212
- handler: handleChangeRepeatModeCommand,
213
- )
214
- case .changeShuffleMode:
215
- enableRemoteCommand(
216
- center.changeShuffleModeCommand,
217
- key: command.key,
218
- handler: handleChangeShuffleModeCommand,
219
- )
220
- case let .changePlaybackRate(supportedPlaybackRates):
221
- center.changePlaybackRateCommand.supportedPlaybackRates = supportedPlaybackRates
222
- enableRemoteCommand(
223
- center.changePlaybackRateCommand,
224
- key: command.key,
225
- handler: handleChangePlaybackRateCommand,
226
- )
227
- }
228
- }
229
-
230
- private func disable(command: RemoteCommand) {
231
- switch command {
232
- case .play:
233
- disableRemoteCommand(center.playCommand, key: command.key)
234
- case .pause:
235
- disableRemoteCommand(center.pauseCommand, key: command.key)
236
- case .stop:
237
- disableRemoteCommand(center.stopCommand, key: command.key)
238
- case .togglePlayPause:
239
- disableRemoteCommand(center.togglePlayPauseCommand, key: command.key)
240
- case .next:
241
- disableRemoteCommand(center.nextTrackCommand, key: command.key)
242
- case .previous:
243
- disableRemoteCommand(center.previousTrackCommand, key: command.key)
244
- case .changePlaybackPosition:
245
- disableRemoteCommand(center.changePlaybackPositionCommand, key: command.key)
246
- case .skipForward:
247
- disableRemoteCommand(center.skipForwardCommand, key: command.key)
248
- case .skipBackward:
249
- disableRemoteCommand(center.skipBackwardCommand, key: command.key)
250
- case .changeRepeatMode:
251
- disableRemoteCommand(center.changeRepeatModeCommand, key: command.key)
252
- case .changeShuffleMode:
253
- disableRemoteCommand(center.changeShuffleModeCommand, key: command.key)
254
- case .changePlaybackRate:
255
- disableRemoteCommand(center.changePlaybackRateCommand, key: command.key)
256
- }
81
+ enabledCommands.forEach(disable)
82
+ enabledCommands = []
257
83
  }
258
84
 
259
- // MARK: - Repeat/Shuffle State Sync
260
-
261
- /// Updates the repeat mode state shown on CarPlay and lock screen
262
85
  func updateRepeatMode(_ mode: RepeatMode) {
263
86
  repeatMode = mode
264
87
  center.changeRepeatModeCommand.currentRepeatType = mode.mpRepeatType
265
88
  }
266
89
 
267
- /// Updates the shuffle mode state shown on CarPlay and lock screen
268
90
  func updateShuffleMode(_ enabled: Bool) {
269
91
  shuffleEnabled = enabled
270
92
  center.changeShuffleModeCommand.currentShuffleType = enabled.mpShuffleType
271
93
  }
272
94
 
273
- /// Greys out next/previous (lock screen / Control Center / CarPlay) when the
274
- /// queue has nowhere to skip. Applied live when those commands are configured.
275
- func setSkipAvailability(canNext: Bool, canPrevious: Bool) {
276
- self.canNext = canNext
277
- self.canPrevious = canPrevious
95
+ /// Greys out next/previous when `skipAvailability` says the queue has nowhere to skip.
96
+ func refreshSkipAvailability() {
97
+ let (canNext, canPrevious) = skipAvailability()
278
98
  if enabledCommands.contains(.next) { center.nextTrackCommand.isEnabled = canNext }
279
99
  if enabledCommands.contains(.previous) { center.previousTrackCommand.isEnabled = canPrevious }
280
100
  }
281
101
 
282
- // MARK: - Handlers
283
-
284
- // Each default is wrapped in a `[weak self]` closure rather than assigned as a
285
- // bare method reference (`= handlePlayCommandDefault`): an unapplied instance
286
- // method curries to `{ [self] in self.method($0) }`, which strongly captures
287
- // self. Stored back onto self, that is a permanent cycle — `enable(command:)`
288
- // forces every lazy init, so the controller would never deallocate, leaking one
289
- // per player instance across JS reloads and re-setups.
290
- lazy var handlePlayCommand: RemoteCommandHandler = { [weak self] event in
291
- self?.handlePlayCommandDefault(event: event) ?? .commandFailed
292
- }
293
-
294
- lazy var handlePauseCommand: RemoteCommandHandler = { [weak self] event in
295
- self?.handlePauseCommandDefault(event: event) ?? .commandFailed
296
- }
297
-
298
- lazy var handleStopCommand: RemoteCommandHandler = { [weak self] event in
299
- self?.handleStopCommandDefault(event: event) ?? .commandFailed
300
- }
301
-
302
- lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = { [weak self] event in
303
- self?.handleTogglePlayPauseCommandDefault(event: event) ?? .commandFailed
304
- }
305
-
306
- lazy var handleSkipForwardCommand: RemoteCommandHandler = { [weak self] event in
307
- self?.handleSkipForwardCommandDefault(event: event) ?? .commandFailed
308
- }
309
-
310
- lazy var handleSkipBackwardCommand: RemoteCommandHandler = { [weak self] event in
311
- self?.handleSkipBackwardDefault(event: event) ?? .commandFailed
312
- }
313
-
314
- lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = { [weak self] event in
315
- self?.handleChangePlaybackPositionCommandDefault(event: event) ?? .commandFailed
316
- }
317
-
318
- lazy var handleNextTrackCommand: RemoteCommandHandler = { [weak self] event in
319
- self?.handleNextTrackCommandDefault(event: event) ?? .commandFailed
320
- }
321
-
322
- lazy var handlePreviousTrackCommand: RemoteCommandHandler = { [weak self] event in
323
- self?.handlePreviousTrackCommandDefault(event: event) ?? .commandFailed
324
- }
325
-
326
- lazy var handleChangeRepeatModeCommand: RemoteCommandHandler = { [weak self] event in
327
- self?.handleChangeRepeatModeCommandDefault(event: event) ?? .commandFailed
328
- }
329
-
330
- lazy var handleChangeShuffleModeCommand: RemoteCommandHandler = { [weak self] event in
331
- self?.handleChangeShuffleModeCommandDefault(event: event) ?? .commandFailed
332
- }
333
-
334
- lazy var handleChangePlaybackRateCommand: RemoteCommandHandler = { [weak self] event in
335
- self?.handleChangePlaybackRateCommandDefault(event: event) ?? .commandFailed
336
- }
337
-
338
- private func handlePlayCommandDefault(event _: MPRemoteCommandEvent)
339
- -> MPRemoteCommandHandlerStatus
340
- {
341
- callbacks?.remotePlay()
342
- return MPRemoteCommandHandlerStatus.success
343
- }
344
-
345
- private func handlePauseCommandDefault(event _: MPRemoteCommandEvent)
346
- -> MPRemoteCommandHandlerStatus
347
- {
348
- callbacks?.remotePause()
349
- return MPRemoteCommandHandlerStatus.success
350
- }
351
-
352
- private func handleStopCommandDefault(event _: MPRemoteCommandEvent)
353
- -> MPRemoteCommandHandlerStatus
354
- {
355
- callbacks?.remoteStop()
356
- return MPRemoteCommandHandlerStatus.success
357
- }
358
-
359
- private func handleTogglePlayPauseCommandDefault(event _: MPRemoteCommandEvent)
360
- -> MPRemoteCommandHandlerStatus
361
- {
362
- callbacks?.remotePlayPause()
363
- return MPRemoteCommandHandlerStatus.success
364
- }
102
+ // MARK: - Private
365
103
 
366
- private func handleSkipForwardCommandDefault(event: MPRemoteCommandEvent)
367
- -> MPRemoteCommandHandlerStatus
368
- {
369
- if let command = event.command as? MPSkipIntervalCommand,
370
- let interval = command.preferredIntervals.first
371
- {
372
- callbacks?.remoteJumpForward(interval: Double(truncating: interval))
373
- return MPRemoteCommandHandlerStatus.success
104
+ private func mpCommand(for command: RemoteCommand) -> MPRemoteCommand {
105
+ switch command {
106
+ case .play: center.playCommand
107
+ case .pause: center.pauseCommand
108
+ case .stop: center.stopCommand
109
+ case .togglePlayPause: center.togglePlayPauseCommand
110
+ case .next: center.nextTrackCommand
111
+ case .previous: center.previousTrackCommand
112
+ case .changePlaybackPosition: center.changePlaybackPositionCommand
113
+ case .skipForward: center.skipForwardCommand
114
+ case .skipBackward: center.skipBackwardCommand
115
+ case .changeRepeatMode: center.changeRepeatModeCommand
116
+ case .changeShuffleMode: center.changeShuffleModeCommand
117
+ case .changePlaybackRate: center.changePlaybackRateCommand
374
118
  }
375
- return MPRemoteCommandHandlerStatus.commandFailed
376
119
  }
377
120
 
378
- private func handleSkipBackwardDefault(event: MPRemoteCommandEvent)
379
- -> MPRemoteCommandHandlerStatus
380
- {
381
- if let command = event.command as? MPSkipIntervalCommand,
382
- let interval = command.preferredIntervals.first
383
- {
384
- callbacks?.remoteJumpBackward(interval: Double(truncating: interval))
385
- return MPRemoteCommandHandlerStatus.success
121
+ private func enable(_ command: RemoteCommand) {
122
+ switch command {
123
+ case let .skipForward(intervals): center.skipForwardCommand.preferredIntervals = intervals
124
+ case let .skipBackward(intervals): center.skipBackwardCommand.preferredIntervals = intervals
125
+ case let .changePlaybackRate(rates): center.changePlaybackRateCommand.supportedPlaybackRates = rates
126
+ default: break
386
127
  }
387
- return MPRemoteCommandHandlerStatus.commandFailed
388
- }
389
-
390
- private func handleChangePlaybackPositionCommandDefault(event: MPRemoteCommandEvent)
391
- -> MPRemoteCommandHandlerStatus
392
- {
393
- if let event = event as? MPChangePlaybackPositionCommandEvent {
394
- logger.debug("changePlaybackPosition: \(event.positionTime)")
395
- callbacks?.remoteSeek(position: event.positionTime)
396
- return MPRemoteCommandHandlerStatus.success
128
+ let mpCommand = mpCommand(for: command)
129
+ mpCommand.removeTarget(nil)
130
+ mpCommand.addTarget { [weak self] event in
131
+ self?.handle(event, for: command) ?? .commandFailed
397
132
  }
398
- logger.debug("changePlaybackPosition: failed to cast event")
399
- return MPRemoteCommandHandlerStatus.commandFailed
400
- }
401
-
402
- private func handleNextTrackCommandDefault(event _: MPRemoteCommandEvent)
403
- -> MPRemoteCommandHandlerStatus
404
- {
405
- callbacks?.remoteNext()
406
- return MPRemoteCommandHandlerStatus.success
407
- }
408
-
409
- private func handlePreviousTrackCommandDefault(event _: MPRemoteCommandEvent)
410
- -> MPRemoteCommandHandlerStatus
411
- {
412
- callbacks?.remotePrevious()
413
- return MPRemoteCommandHandlerStatus.success
414
- }
415
-
416
- private func handleChangeRepeatModeCommandDefault(event: MPRemoteCommandEvent)
417
- -> MPRemoteCommandHandlerStatus
418
- {
419
- if let event = event as? MPChangeRepeatModeCommandEvent {
420
- let mode = RepeatMode(from: event.repeatType)
421
- callbacks?.remoteChangeRepeatMode(mode: mode)
422
- // Update the command center state to reflect the new mode
423
- center.changeRepeatModeCommand.currentRepeatType = event.repeatType
424
- return MPRemoteCommandHandlerStatus.success
133
+ mpCommand.isEnabled = switch command {
134
+ case .next: skipAvailability().canNext
135
+ case .previous: skipAvailability().canPrevious
136
+ default: true
425
137
  }
426
- return MPRemoteCommandHandlerStatus.commandFailed
427
138
  }
428
139
 
429
- private func handleChangeShuffleModeCommandDefault(event: MPRemoteCommandEvent)
430
- -> MPRemoteCommandHandlerStatus
431
- {
432
- if let event = event as? MPChangeShuffleModeCommandEvent {
433
- let enabled = event.shuffleType != .off
434
- callbacks?.remoteChangeShuffleMode(enabled: enabled)
435
- // Update the command center state to reflect the new mode
436
- center.changeShuffleModeCommand.currentShuffleType = event.shuffleType
437
- return MPRemoteCommandHandlerStatus.success
438
- }
439
- return MPRemoteCommandHandlerStatus.commandFailed
140
+ private func disable(_ command: RemoteCommand) {
141
+ let mpCommand = mpCommand(for: command)
142
+ mpCommand.isEnabled = false
143
+ mpCommand.removeTarget(nil)
440
144
  }
441
145
 
442
- private func handleChangePlaybackRateCommandDefault(event: MPRemoteCommandEvent)
443
- -> MPRemoteCommandHandlerStatus
444
- {
445
- if let event = event as? MPChangePlaybackRateCommandEvent {
446
- let rate = Float(event.playbackRate)
447
- callbacks?.remoteChangePlaybackRate(rate: rate)
448
- return MPRemoteCommandHandlerStatus.success
146
+ private func handle(_ event: MPRemoteCommandEvent, for command: RemoteCommand) -> MPRemoteCommandHandlerStatus {
147
+ switch command {
148
+ case .play: callbacks?.remotePlay()
149
+ case .pause: callbacks?.remotePause()
150
+ case .stop: callbacks?.remoteStop()
151
+ case .togglePlayPause: callbacks?.remotePlayPause()
152
+ case .next: callbacks?.remoteNext()
153
+ case .previous: callbacks?.remotePrevious()
154
+ case .skipForward, .skipBackward:
155
+ guard let interval = (event.command as? MPSkipIntervalCommand)?.preferredIntervals.first else {
156
+ return .commandFailed
157
+ }
158
+ if case .skipForward = command {
159
+ callbacks?.remoteJumpForward(interval: interval.doubleValue)
160
+ } else {
161
+ callbacks?.remoteJumpBackward(interval: interval.doubleValue)
162
+ }
163
+ case .changePlaybackPosition:
164
+ guard let event = event as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
165
+ callbacks?.remoteSeek(position: event.positionTime)
166
+ case .changeRepeatMode:
167
+ guard let event = event as? MPChangeRepeatModeCommandEvent else { return .commandFailed }
168
+ callbacks?.remoteChangeRepeatMode(mode: RepeatMode(from: event.repeatType))
169
+ case .changeShuffleMode:
170
+ guard let event = event as? MPChangeShuffleModeCommandEvent else { return .commandFailed }
171
+ callbacks?.remoteChangeShuffleMode(enabled: event.shuffleType != .off)
172
+ case .changePlaybackRate:
173
+ guard let event = event as? MPChangePlaybackRateCommandEvent else { return .commandFailed }
174
+ callbacks?.remoteChangePlaybackRate(rate: event.playbackRate)
449
175
  }
450
- return MPRemoteCommandHandlerStatus.commandFailed
451
- }
452
-
453
- private func getRemoteCommandHandlerStatus(forError error: Error)
454
- -> MPRemoteCommandHandlerStatus
455
- {
456
- error is TrackPlayerError.QueueError
457
- ? MPRemoteCommandHandlerStatus.noSuchContent
458
- : MPRemoteCommandHandlerStatus.commandFailed
176
+ return .success
459
177
  }
460
178
  }