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.
- package/README.md +22 -18
- package/android/build.gradle +13 -10
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +76 -21
- package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
- package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
- package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +1 -2
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +1 -2
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +6 -7
- package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +11 -11
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +9 -5
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +0 -1
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +2 -4
- package/android/src/main/java/com/audiobrowser/player/Player.kt +30 -8
- package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
- package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +2 -1
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
- package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -7
- package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +52 -1
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +167 -8
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +2 -1
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +2 -4
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +100 -56
- package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
- package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
- package/ios/Browser/BrowserManager.swift +2 -27
- package/ios/Browser/SearchDrillIn.swift +1 -1
- package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
- package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
- package/ios/HybridAudioBrowser.swift +87 -56
- package/ios/Model/NitroTypeStubs.swift +18 -2
- package/ios/Model/RemoteCommand.swift +18 -0
- package/ios/Model/TrackPlayerError.swift +2 -2
- package/ios/Option/SessionCategories.swift +3 -2
- package/ios/Player/MediaIntentCriteria.swift +4 -4
- package/ios/Player/MediaLoader.swift +3 -1
- package/ios/Player/PlaybackCoordinator.swift +4 -1
- package/ios/Player/PlaybackErrorHandler.swift +4 -4
- package/ios/Player/QueueManager.swift +10 -3
- package/ios/Player/RetryManager.swift +79 -18
- package/ios/Player/ShuffleOrder.swift +10 -0
- package/ios/Player/SleepTimerManager.swift +43 -15
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +9 -6
- package/ios/Tests/LRUCacheTests.swift +111 -0
- package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
- package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
- package/ios/Tests/MediaLoaderTests.swift +10 -13
- package/ios/Tests/PlaybackCoordinatorTests.swift +10 -7
- package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
- package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
- package/ios/Tests/QueueManagerTests.swift +50 -4
- package/ios/Tests/RemoteCommandControllerTests.swift +156 -0
- package/ios/Tests/RemoteCommandTests.swift +67 -0
- package/ios/Tests/RetryManagerTests.swift +89 -0
- package/ios/Tests/SectionScopeTests.swift +2 -2
- package/ios/Tests/SleepTimerManagerTests.swift +22 -19
- package/ios/Tests/Support/ManualSleepTimerScheduler.swift +47 -0
- package/ios/Tests/Support/TestTypes.swift +1 -1
- package/ios/Tests/TabBarEntriesTests.swift +7 -7
- package/ios/TrackPlayer.swift +6 -1
- package/ios/TrackPlayerCallbacks.swift +5 -38
- package/ios/Util/LRUCache.swift +4 -1
- package/lib/commonjs/AudioBrowser.js +0 -11
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/CLAUDE.md +52 -0
- package/lib/commonjs/features/browser.js +1 -1
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/carConnection.js +1 -1
- package/lib/commonjs/features/equalizer.js +1 -1
- package/lib/commonjs/features/errors.js +3 -3
- package/lib/commonjs/features/favorites.js +1 -1
- package/lib/commonjs/features/gate.js +16 -1
- package/lib/commonjs/features/gate.js.map +1 -1
- package/lib/commonjs/features/metadata.js +20 -3
- package/lib/commonjs/features/metadata.js.map +1 -1
- package/lib/commonjs/features/network.js +1 -1
- package/lib/commonjs/features/nowPlaying.js +1 -1
- package/lib/commonjs/features/output.js +1 -1
- package/lib/commonjs/features/playback/playWhenReady.js +1 -1
- package/lib/commonjs/features/playback/playing.js +1 -1
- package/lib/commonjs/features/playback/progress.js +1 -1
- package/lib/commonjs/features/playback/state.js +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/commonjs/features/playback/volume.js +1 -1
- package/lib/commonjs/features/player/options.js +48 -34
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +15 -8
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +21 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -0
- package/lib/commonjs/features/queue/activeTrack.js +1 -1
- package/lib/commonjs/features/queue/queue.js +2 -2
- package/lib/commonjs/features/queue/repeatMode.js +1 -1
- package/lib/commonjs/features/queue/shuffle.js +1 -1
- package/lib/commonjs/features/remoteControls.js +13 -92
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +13 -1
- package/lib/commonjs/features/sleepTimer.js.map +1 -1
- package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
- package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
- package/lib/commonjs/utils/LazyNativeEmitter.js +8 -5
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/web/NativeAudioBrowser.js +1 -1
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +1 -1
- package/lib/module/AudioBrowser.js +1 -1
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/CLAUDE.md +52 -0
- package/lib/module/features/browser.js +1 -1
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/carConnection.js +1 -1
- package/lib/module/features/equalizer.js +1 -1
- package/lib/module/features/errors.js +3 -3
- package/lib/module/features/favorites.js +1 -1
- package/lib/module/features/gate.js +16 -1
- package/lib/module/features/gate.js.map +1 -1
- package/lib/module/features/metadata.js +20 -3
- package/lib/module/features/metadata.js.map +1 -1
- package/lib/module/features/network.js +1 -1
- package/lib/module/features/nowPlaying.js +1 -1
- package/lib/module/features/output.js +1 -1
- package/lib/module/features/playback/playWhenReady.js +1 -1
- package/lib/module/features/playback/playing.js +1 -1
- package/lib/module/features/playback/progress.js +1 -1
- package/lib/module/features/playback/state.js +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +0 -5
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/module/features/playback/volume.js +1 -1
- package/lib/module/features/player/options.js +47 -32
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +14 -7
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +17 -0
- package/lib/module/features/player/validateOptions.js.map +1 -0
- package/lib/module/features/queue/activeTrack.js +1 -1
- package/lib/module/features/queue/queue.js +2 -2
- package/lib/module/features/queue/repeatMode.js +1 -1
- package/lib/module/features/queue/shuffle.js +1 -1
- package/lib/module/features/remoteControls.js +13 -92
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +13 -1
- package/lib/module/features/sleepTimer.js.map +1 -1
- package/lib/module/test-utils/reactNativeStub.js +58 -0
- package/lib/module/test-utils/reactNativeStub.js.map +1 -0
- package/lib/module/utils/LazyNativeEmitter.js +8 -5
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/web/NativeAudioBrowser.js +1 -1
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts +0 -1
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/carConnection.d.ts +1 -1
- package/lib/typescript/src/features/equalizer.d.ts +1 -1
- package/lib/typescript/src/features/errors.d.ts +3 -3
- package/lib/typescript/src/features/favorites.d.ts +1 -1
- package/lib/typescript/src/features/gate.d.ts +18 -2
- package/lib/typescript/src/features/gate.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +20 -3
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/network.d.ts +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
- package/lib/typescript/src/features/output.d.ts +1 -1
- package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
- package/lib/typescript/src/features/playback/playing.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +1 -1
- package/lib/typescript/src/features/playback/volume.d.ts +1 -1
- package/lib/typescript/src/features/player/options.d.ts +87 -37
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +14 -4
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +9 -0
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
- package/lib/typescript/src/features/queue/activeTrack.d.ts +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +2 -2
- package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
- package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts +23 -22
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +8 -4
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -1
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +5 -5
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +11 -11
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +5 -5
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +5 -5
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JOptions.hpp +5 -5
- package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
- package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +36 -51
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +6 -6
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +8 -8
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
- package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
- package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
- package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
- package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
- package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
- package/package.json +12 -4
- package/src/AudioBrowser.ts +1 -1
- package/src/CLAUDE.md +52 -0
- package/src/features/battery.test.ts +79 -0
- package/src/features/browser.ts +1 -1
- package/src/features/carConnection.ts +1 -1
- package/src/features/equalizer.ts +1 -1
- package/src/features/errors.ts +3 -3
- package/src/features/favorites.ts +1 -1
- package/src/features/gate.ts +16 -1
- package/src/features/hook-wiring.test.ts +254 -0
- package/src/features/metadata.ts +20 -3
- package/src/features/network.ts +1 -1
- package/src/features/nowPlaying.ts +1 -1
- package/src/features/output.ts +1 -1
- package/src/features/playback/playWhenReady.ts +1 -1
- package/src/features/playback/playing.ts +1 -1
- package/src/features/playback/progress.test.ts +169 -0
- package/src/features/playback/progress.ts +1 -1
- package/src/features/playback/state.ts +1 -1
- package/src/features/playback/trackPlaybackTime.test.ts +16 -6
- package/src/features/playback/trackPlaybackTime.ts +0 -5
- package/src/features/playback/volume.ts +1 -1
- package/src/features/player/options.ts +99 -57
- package/src/features/player/setup.test.ts +12 -4
- package/src/features/player/setup.ts +19 -9
- package/src/features/player/validateOptions.ts +19 -0
- package/src/features/queue/activeTrack.ts +1 -1
- package/src/features/queue/queue.ts +2 -2
- package/src/features/queue/repeatMode.ts +1 -1
- package/src/features/queue/shuffle.ts +1 -1
- package/src/features/remoteControls.ts +13 -92
- package/src/features/sleepTimer.test.ts +172 -0
- package/src/features/sleepTimer.ts +9 -1
- package/src/nitro-stub-drift.test.ts +237 -0
- package/src/public-surface.test.ts +107 -0
- package/src/specs/audio-browser.nitro.ts +1 -1
- package/src/test-utils/reactNativeStub.ts +60 -0
- package/src/utils/LazyNativeEmitter.ts +9 -6
- package/src/utils/useDebug.test.ts +182 -0
- package/src/utils/useNativeUpdatedValue.test.ts +103 -0
- package/src/web/NativeAudioBrowser.ts +1 -1
- package/src/web/browser/BrowserManager.ts +1 -1
- package/ios/Model/MediaURL.swift +0 -67
- package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
- package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
- package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
|
@@ -16,6 +16,10 @@ import os.log
|
|
|
16
16
|
/// Only safe when the caller blocks until the MainActor work completes (DispatchQueue.main.sync).
|
|
17
17
|
private struct UncheckedSendableBox<T>: @unchecked Sendable { let value: T }
|
|
18
18
|
|
|
19
|
+
/// `@unchecked Sendable` because it must be: the spec's requirements are all
|
|
20
|
+
/// nonisolated and Nitro calls them synchronously on the JS thread, so the class
|
|
21
|
+
/// can't be `@MainActor`. Isolation is therefore per-property, and `@unchecked`
|
|
22
|
+
/// means the compiler won't tell you when a new one needs it.
|
|
19
23
|
public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
20
24
|
private let logger = Logger(subsystem: "com.audiobrowser", category: "AudioBrowser")
|
|
21
25
|
|
|
@@ -32,6 +36,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
32
36
|
|
|
33
37
|
// MARK: - Private Properties
|
|
34
38
|
|
|
39
|
+
/// Main-confined by convention, not annotation: `deinit` reads it as a
|
|
40
|
+
/// safety net and can't hop. Reach it from `onMainActor` / `@MainActor`.
|
|
35
41
|
private var player: TrackPlayer?
|
|
36
42
|
private let networkMonitor = NetworkMonitor()
|
|
37
43
|
private let playbackStateStore = PlaybackStateStore()
|
|
@@ -43,27 +49,31 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
43
49
|
private var mediaServicesResetObserver: NSObjectProtocol?
|
|
44
50
|
/// Re-applies our resolved audio-session category. Captured at setup so a
|
|
45
51
|
/// media-services reset (which clears the category) can restore it.
|
|
46
|
-
private var applyAudioSessionCategory: () -> Void = {}
|
|
47
|
-
private var nowPlayingOverride: NowPlayingUpdate?
|
|
52
|
+
@MainActor private var applyAudioSessionCategory: () -> Void = {}
|
|
53
|
+
@MainActor private var nowPlayingOverride: NowPlayingUpdate?
|
|
48
54
|
/// When false, the now-playing surface uses the raw track fields (override + formatter ignored).
|
|
49
|
-
private var nowPlayingMetadataEnabled = true
|
|
55
|
+
@MainActor private var nowPlayingMetadataEnabled = true
|
|
50
56
|
/// Customizes the now-playing title/subtitle from the track + live timed metadata + playback state.
|
|
51
|
-
private var nowPlayingMetadataFormatter: ((_ params: FormatNowPlayingParams) -> Promise<NowPlayingUpdate?>)?
|
|
52
|
-
/// Keep the media session controllable through a terminal error (see `keepSessionAliveOnError`).
|
|
53
|
-
private var keepSessionAliveOnError = true
|
|
57
|
+
@MainActor private var nowPlayingMetadataFormatter: ((_ params: FormatNowPlayingParams) -> Promise<NowPlayingUpdate?>)?
|
|
54
58
|
/// Initial player state staged before the player exists — from setup options or the imperative
|
|
55
59
|
/// setters called pre-setup. Strict last-write-wins; consumed when the player comes up.
|
|
56
|
-
private var pendingPlayWhenReady: Bool?
|
|
57
|
-
private var pendingRepeatMode: RepeatMode?
|
|
60
|
+
@MainActor private var pendingPlayWhenReady: Bool?
|
|
61
|
+
@MainActor private var pendingRepeatMode: RepeatMode?
|
|
58
62
|
/// Latest live timed (ICY/ID3) metadata, passed to the formatter. Cleared on track change.
|
|
59
|
-
private var latestTimedMetadata: TimedMetadata?
|
|
60
|
-
private let playerOptions = PlayerUpdateOptions()
|
|
63
|
+
@MainActor private var latestTimedMetadata: TimedMetadata?
|
|
64
|
+
@MainActor private let playerOptions = PlayerUpdateOptions()
|
|
61
65
|
|
|
62
66
|
/// Configured playback rates for the playback-rate capability (for CarPlay rate cycling)
|
|
63
|
-
var playbackRates: [Double] { playerOptions.playbackRates }
|
|
64
|
-
var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
|
|
65
|
-
var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] {
|
|
66
|
-
|
|
67
|
+
@MainActor var playbackRates: [Double] { playerOptions.playbackRates }
|
|
68
|
+
@MainActor var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
|
|
69
|
+
@MainActor var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] {
|
|
70
|
+
playerOptions.carPlayNowPlayingButtons
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Written from the JS thread, the cooperative pool and MainActor, with a
|
|
74
|
+
/// `didSet` that calls back into Nitro. MainActor-isolated so the compiler
|
|
75
|
+
/// requires an `onMainActor` hop at every access.
|
|
76
|
+
@MainActor private var lastNavigationError: NavigationError? {
|
|
67
77
|
didSet {
|
|
68
78
|
// Skip if both nil (no real change)
|
|
69
79
|
guard oldValue != nil || lastNavigationError != nil else { return }
|
|
@@ -71,7 +81,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
/// Isolated for the same reason as ``lastNavigationError``.
|
|
85
|
+
@MainActor private var lastFormattedNavigationError: FormattedNavigationError? {
|
|
75
86
|
didSet {
|
|
76
87
|
// Skip if both nil (no real change)
|
|
77
88
|
guard oldValue != nil || lastFormattedNavigationError != nil else { return }
|
|
@@ -156,6 +167,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
156
167
|
androidControllerOfflineError: nil, carPlayLoadingTitle: nil, resolveAlbumUrl: nil, formatNavigationError: nil,
|
|
157
168
|
) {
|
|
158
169
|
didSet {
|
|
170
|
+
// Copied here, on the setter's thread: the Task below outlives this call,
|
|
171
|
+
// and Nitro can reassign `configuration` — a C++ struct holding
|
|
172
|
+
// std::string/std::function — from the JS thread while it runs.
|
|
173
|
+
let configuredPath = configuration.path
|
|
159
174
|
onMainActor { browserManager.config = BrowserConfig(from: configuration) }
|
|
160
175
|
|
|
161
176
|
// Query tabs and navigate to initial path after config is set (matches Kotlin behavior)
|
|
@@ -166,12 +181,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
166
181
|
Task {
|
|
167
182
|
let tabs = try? await browserManager.queryTabs()
|
|
168
183
|
// Navigate to configured path, first tab, or "/"
|
|
169
|
-
let initialPath =
|
|
184
|
+
let initialPath = configuredPath ?? tabs?.first?.url ?? "/"
|
|
170
185
|
// Clear error before navigation (matches Kotlin clearNavigationError())
|
|
171
|
-
|
|
172
|
-
lastNavigationError = nil
|
|
173
|
-
lastFormattedNavigationError = nil
|
|
174
|
-
}
|
|
186
|
+
clearNavigationError()
|
|
175
187
|
do {
|
|
176
188
|
try await browserManager.navigate(initialPath)
|
|
177
189
|
} catch {
|
|
@@ -370,7 +382,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
370
382
|
private func handleNavigationError(_ error: Error, path: String) {
|
|
371
383
|
let navError = NavigationError.from(error)
|
|
372
384
|
|
|
373
|
-
lastNavigationError = navError
|
|
385
|
+
onMainActor { lastNavigationError = navError }
|
|
374
386
|
|
|
375
387
|
// Format the error (async if using JS callback, sync for defaults)
|
|
376
388
|
let defaultFormatted = navError.defaultFormatted()
|
|
@@ -380,27 +392,40 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
380
392
|
// The result callbacks are @Sendable: a plain closure would inherit MainActor
|
|
381
393
|
// isolation from this block, and Nitro resolves its promises synchronously on the
|
|
382
394
|
// JS thread — Swift 6.2's dynamic isolation check then traps on closure entry.
|
|
383
|
-
//
|
|
395
|
+
// They therefore hop back explicitly, like every other writer.
|
|
384
396
|
DispatchQueue.main.async { [weak self] in
|
|
385
397
|
formatter(params)
|
|
386
398
|
.then { @Sendable [weak self] customDisplay in
|
|
387
|
-
self?.
|
|
399
|
+
self?.setFormattedNavigationError(customDisplay ?? defaultFormatted)
|
|
388
400
|
}
|
|
389
401
|
.catch { @Sendable [weak self] _ in
|
|
390
|
-
self?.
|
|
402
|
+
self?.setFormattedNavigationError(defaultFormatted)
|
|
391
403
|
}
|
|
392
404
|
}
|
|
393
405
|
} else {
|
|
394
|
-
|
|
406
|
+
setFormattedNavigationError(defaultFormatted)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/// Callable from any thread — the Nitro promise callbacks below resolve on
|
|
411
|
+
/// the JS thread.
|
|
412
|
+
private func setFormattedNavigationError(_ formatted: FormattedNavigationError?) {
|
|
413
|
+
onMainActor { lastFormattedNavigationError = formatted }
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/// Synchronous on purpose: the `didSet`s notify JS, which has to happen
|
|
417
|
+
/// before the navigation that may set them again.
|
|
418
|
+
private func clearNavigationError() {
|
|
419
|
+
onMainActor {
|
|
420
|
+
lastNavigationError = nil
|
|
421
|
+
lastFormattedNavigationError = nil
|
|
395
422
|
}
|
|
396
423
|
}
|
|
397
424
|
|
|
398
425
|
// MARK: - Browser Methods
|
|
399
426
|
|
|
400
427
|
public func navigatePath(path: String) throws {
|
|
401
|
-
|
|
402
|
-
lastNavigationError = nil
|
|
403
|
-
lastFormattedNavigationError = nil
|
|
428
|
+
clearNavigationError()
|
|
404
429
|
Task {
|
|
405
430
|
do {
|
|
406
431
|
try await browserManager.navigate(path)
|
|
@@ -411,7 +436,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
411
436
|
}
|
|
412
437
|
|
|
413
438
|
public func navigateTrack(track: Track) throws {
|
|
414
|
-
|
|
439
|
+
// @MainActor: `player` is main-confined, and `trackSelector.select` is
|
|
440
|
+
// MainActor anyway — a bare Task read the reference off the cooperative
|
|
441
|
+
// pool, racing the assignment in `setupPlayer`.
|
|
442
|
+
Task { @MainActor in
|
|
415
443
|
guard let player else { return }
|
|
416
444
|
let result = await trackSelector.select(track: track, player: player)
|
|
417
445
|
switch result {
|
|
@@ -445,9 +473,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
445
473
|
}
|
|
446
474
|
|
|
447
475
|
private func navigateToBrowsableUrl(_ url: String) {
|
|
448
|
-
|
|
449
|
-
lastNavigationError = nil
|
|
450
|
-
lastFormattedNavigationError = nil
|
|
476
|
+
clearNavigationError()
|
|
451
477
|
Task {
|
|
452
478
|
do {
|
|
453
479
|
try await browserManager.navigate(url)
|
|
@@ -457,7 +483,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
457
483
|
}
|
|
458
484
|
}
|
|
459
485
|
|
|
460
|
-
public func
|
|
486
|
+
public func search(query: String) throws -> Promise<[Track]> {
|
|
461
487
|
Promise.async { [weak self] in
|
|
462
488
|
guard let self else { return [] }
|
|
463
489
|
let resolved = try await browserManager.search(query)
|
|
@@ -470,11 +496,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
470
496
|
}
|
|
471
497
|
|
|
472
498
|
public func getNavigationError() throws -> NavigationError? {
|
|
473
|
-
lastNavigationError
|
|
499
|
+
onMainActor { lastNavigationError }
|
|
474
500
|
}
|
|
475
501
|
|
|
476
502
|
public func getFormattedNavigationError() throws -> FormattedNavigationError? {
|
|
477
|
-
lastFormattedNavigationError
|
|
503
|
+
onMainActor { lastFormattedNavigationError }
|
|
478
504
|
}
|
|
479
505
|
|
|
480
506
|
/// Internal signal sent on `externalContentChangedEmitter` to tell CarPlay to
|
|
@@ -601,7 +627,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
601
627
|
// built-in so the gate page still renders rather than leaking content.
|
|
602
628
|
return GateOutcome(gated: true, chrome: defaultChrome ?? Self.builtInGate)
|
|
603
629
|
}
|
|
604
|
-
guard decision.gated else { return GateOutcome(gated: false, chrome: nil) }
|
|
630
|
+
guard decision.gated else { return GateOutcome(gated: false, chrome: nil) } // explicit allow
|
|
605
631
|
return GateOutcome(gated: true, chrome: decision.gate ?? defaultChrome ?? Self.builtInGate)
|
|
606
632
|
}
|
|
607
633
|
|
|
@@ -649,7 +675,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
649
675
|
category: AVAudioSession.Category,
|
|
650
676
|
mode: AVAudioSession.Mode,
|
|
651
677
|
policy: AVAudioSession.RouteSharingPolicy,
|
|
652
|
-
options: AVAudioSession.CategoryOptions
|
|
678
|
+
options: AVAudioSession.CategoryOptions,
|
|
653
679
|
) = options.ios?.resolveAudioSessionConfig()
|
|
654
680
|
?? (category: .playback, mode: .default, policy: .default, options: [])
|
|
655
681
|
// Capture category application once so a media-services reset (which
|
|
@@ -733,16 +759,16 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
733
759
|
self?.onSleepTimerChanged(state)
|
|
734
760
|
}
|
|
735
761
|
|
|
736
|
-
// Now-playing: whether to manage metadata, the optional formatter that owns the
|
|
737
|
-
// lines
|
|
762
|
+
// Now-playing: whether to manage metadata, and the optional formatter that owns the
|
|
763
|
+
// rendered lines.
|
|
738
764
|
nowPlayingMetadataEnabled = options.autoUpdateNowPlayingMetadata ?? true
|
|
739
765
|
nowPlayingMetadataFormatter = options.nowPlayingMetadataFormatter
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
// (
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
|
|
766
|
+
// `options.keepSessionAliveOnError` is deliberately ignored here: iOS needs no masking.
|
|
767
|
+
// A terminal error resolves the playback state to *paused*
|
|
768
|
+
// (PlaybackCoordinator.applySideEffects), the now-playing info is retained (never cleared
|
|
769
|
+
// on error), and next/previous stay enabled (gated on queue position, not state). So the
|
|
770
|
+
// session stays controllable through errors by default — the behavior Android achieves via
|
|
771
|
+
// InterceptingPlayer is already the iOS norm.
|
|
746
772
|
|
|
747
773
|
// The bundled runtime options and initial state are part of the atomic launch
|
|
748
774
|
// description. Stage the state first so setup options and earlier pre-setup setter
|
|
@@ -819,7 +845,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
819
845
|
}
|
|
820
846
|
|
|
821
847
|
public func getOptions() throws -> Options {
|
|
822
|
-
playerOptions.toOptions()
|
|
848
|
+
onMainActor { playerOptions.toOptions() }
|
|
823
849
|
}
|
|
824
850
|
|
|
825
851
|
public func setPlaybackIntervalEnabled(enabled: Bool) {
|
|
@@ -1121,11 +1147,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1121
1147
|
/// metadata can't stomp it mid-flash. Reverted by a NATIVE timer — JS
|
|
1122
1148
|
/// timers pause with a backgrounded host on Android, and the lock screen
|
|
1123
1149
|
/// is exactly the backgrounded case — and cleared early on track change.
|
|
1124
|
-
private var nowPlayingFlash: NowPlayingUpdate?
|
|
1125
|
-
private var nowPlayingFlashRevert: DispatchWorkItem?
|
|
1150
|
+
@MainActor private var nowPlayingFlash: NowPlayingUpdate?
|
|
1151
|
+
@MainActor private var nowPlayingFlashRevert: DispatchWorkItem?
|
|
1126
1152
|
|
|
1127
1153
|
/// Pending debounced audio-session release (see `playerShouldReleaseSession`).
|
|
1128
|
-
private var sessionReleaseWork: DispatchWorkItem?
|
|
1154
|
+
@MainActor private var sessionReleaseWork: DispatchWorkItem?
|
|
1129
1155
|
|
|
1130
1156
|
public func flashNowPlaying(update: NowPlayingUpdate, durationMs: Double) throws {
|
|
1131
1157
|
onMainActor {
|
|
@@ -1151,6 +1177,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1151
1177
|
}
|
|
1152
1178
|
}
|
|
1153
1179
|
|
|
1180
|
+
@MainActor
|
|
1154
1181
|
private func cancelNowPlayingFlash() {
|
|
1155
1182
|
nowPlayingFlashRevert?.cancel()
|
|
1156
1183
|
nowPlayingFlashRevert = nil
|
|
@@ -1354,8 +1381,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1354
1381
|
guard let self else { return }
|
|
1355
1382
|
// Re-apply our category (the reset cleared it), then have the player
|
|
1356
1383
|
// recreate itself and reconnect the current stream.
|
|
1357
|
-
self.
|
|
1358
|
-
|
|
1384
|
+
self.onMainActor {
|
|
1385
|
+
self.applyAudioSessionCategory()
|
|
1386
|
+
self.player?.handleMediaServicesReset()
|
|
1387
|
+
}
|
|
1359
1388
|
}
|
|
1360
1389
|
}
|
|
1361
1390
|
|
|
@@ -1394,7 +1423,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1394
1423
|
|
|
1395
1424
|
public func supportsOutputSwitcher() throws -> Bool {
|
|
1396
1425
|
// iOS can always present the system route picker (AVRoutePickerView).
|
|
1397
|
-
|
|
1426
|
+
true
|
|
1398
1427
|
}
|
|
1399
1428
|
|
|
1400
1429
|
public func openOutputPicker() throws {
|
|
@@ -1586,6 +1615,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1586
1615
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: release)
|
|
1587
1616
|
}
|
|
1588
1617
|
|
|
1618
|
+
@MainActor
|
|
1589
1619
|
private func cancelSessionRelease() {
|
|
1590
1620
|
sessionReleaseWork?.cancel()
|
|
1591
1621
|
sessionReleaseWork = nil
|
|
@@ -1786,7 +1816,8 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1786
1816
|
// URL (parent container → siblings + selected index). Fall back to the single
|
|
1787
1817
|
// track when the url isn't contextual or expansion fails.
|
|
1788
1818
|
if let url = state.track.url,
|
|
1789
|
-
let expanded = try? await browser.browserManager.expandQueueFromContextualUrl(url)
|
|
1819
|
+
let expanded = try? await browser.browserManager.expandQueueFromContextualUrl(url)
|
|
1820
|
+
{
|
|
1790
1821
|
player.setQueue(expanded.tracks, initialIndex: expanded.selectedIndex, startPositionMs: startMs, playWhenReady: true)
|
|
1791
1822
|
} else {
|
|
1792
1823
|
player.setQueue([track], initialIndex: 0, startPositionMs: startMs, playWhenReady: true)
|
|
@@ -1795,7 +1826,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1795
1826
|
completion(true)
|
|
1796
1827
|
} else {
|
|
1797
1828
|
browser.logger.error("resume: nothing playing and nothing persisted → no-op")
|
|
1798
|
-
completion(false)
|
|
1829
|
+
completion(false) // nothing playing and nothing persisted
|
|
1799
1830
|
}
|
|
1800
1831
|
return
|
|
1801
1832
|
}
|
|
@@ -1812,14 +1843,14 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1812
1843
|
title: criteria.title,
|
|
1813
1844
|
playlist: criteria.playlist,
|
|
1814
1845
|
// .currentlyPlaying can't reach here — isResume routed it to resume.
|
|
1815
|
-
reference: criteria.reference == .my ? .my : .unknown
|
|
1846
|
+
reference: criteria.reference == .my ? .my : .unknown,
|
|
1816
1847
|
)
|
|
1817
1848
|
|
|
1818
1849
|
// Search is *finding* new content — refused when the gate decides this
|
|
1819
1850
|
// request is gated (resume above is unaffected: the gate never blocks
|
|
1820
1851
|
// hearing, and the gate check stays after the resume check).
|
|
1821
1852
|
let outcome = await browser.gateDecision(
|
|
1822
|
-
for: NativeGateRequest(reason: .search, path: nil, search: params)
|
|
1853
|
+
for: NativeGateRequest(reason: .search, path: nil, search: params),
|
|
1823
1854
|
)
|
|
1824
1855
|
guard !outcome.gated else {
|
|
1825
1856
|
browser.logger.info("handlePlayMediaIntent: search refused — gated")
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
/// These stubs declare `Equatable` for test ergonomics — the generated
|
|
6
6
|
/// Nitro types do NOT. Shared `ios/` sources must not rely on conformances
|
|
7
7
|
/// that exist only here: `swift test` compiles, the app build breaks.
|
|
8
|
+
///
|
|
9
|
+
/// Property names and types ARE checked against `nitrogen/generated/ios/swift`
|
|
10
|
+
/// by `src/nitro-stub-drift.test.ts`, which runs in the JS CI job — neither
|
|
11
|
+
/// native job can see both files at once. Declaring fewer properties than the
|
|
12
|
+
/// real type is fine and deliberate; spelling one differently is what that
|
|
13
|
+
/// test catches. Conformances and default values are still on you.
|
|
8
14
|
|
|
9
15
|
struct ArtworkVariants: Equatable {
|
|
10
16
|
var light: String
|
|
@@ -23,7 +29,7 @@
|
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
struct Track: Equatable {
|
|
26
|
-
var id: String
|
|
32
|
+
var id: String?
|
|
27
33
|
var url: String?
|
|
28
34
|
var src: String?
|
|
29
35
|
var request: TrackRequest?
|
|
@@ -63,9 +69,19 @@
|
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
enum HttpMethod {
|
|
73
|
+
case get
|
|
74
|
+
case post
|
|
75
|
+
case put
|
|
76
|
+
case patch
|
|
77
|
+
case delete
|
|
78
|
+
case head
|
|
79
|
+
case options
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
struct ImageSource: Equatable {
|
|
67
83
|
var uri: String
|
|
68
|
-
var method:
|
|
84
|
+
var method: HttpMethod?
|
|
69
85
|
var headers: [String: String]?
|
|
70
86
|
var body: String?
|
|
71
87
|
}
|
|
@@ -48,6 +48,24 @@ enum RemoteCommand: CustomStringConvertible, Equatable {
|
|
|
48
48
|
|
|
49
49
|
var key: String { description }
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
Commands in `self` that are absent from `next` and so should be disabled.
|
|
53
|
+
|
|
54
|
+
Compared by `key`, not by `==`. `RemoteCommand` is `Equatable` including its
|
|
55
|
+
associated values, but every case with associated values maps onto a single
|
|
56
|
+
`MPRemoteCommand` addressed by `key`: `.skipForward([15])` and
|
|
57
|
+
`.skipForward([30])` are two different values of one command. Diffing by `==`
|
|
58
|
+
would report the old interval as removed and disable the command that the new
|
|
59
|
+
interval had just re-enabled.
|
|
60
|
+
*/
|
|
61
|
+
static func commandsToDisable(
|
|
62
|
+
enabled: [RemoteCommand],
|
|
63
|
+
replacedBy next: [RemoteCommand],
|
|
64
|
+
) -> [RemoteCommand] {
|
|
65
|
+
let nextKeys = Set(next.map(\.key))
|
|
66
|
+
return enabled.filter { !nextKeys.contains($0.key) }
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
/**
|
|
52
70
|
All values in an array for convenience.
|
|
53
71
|
Don't use for associated values.
|
|
@@ -103,10 +103,10 @@ public extension TrackPlayerError.PlaybackError {
|
|
|
103
103
|
static func kind(forHttpStatus status: Int) -> PlaybackErrorKind {
|
|
104
104
|
switch status {
|
|
105
105
|
case 404, 410: .notFound
|
|
106
|
-
case 500...599: .serverError
|
|
106
|
+
case 500 ... 599: .serverError
|
|
107
107
|
// Every other 4xx is the server refusing us — auth, geo-blocking, a
|
|
108
108
|
// rate limit. All of them mean "you can't have this stream", not "retry".
|
|
109
|
-
case 400...499: .rejected
|
|
109
|
+
case 400 ... 499: .rejected
|
|
110
110
|
default: .unknown
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -99,7 +99,7 @@ extension NativeIOSSetupOptions {
|
|
|
99
99
|
category: AVAudioSession.Category,
|
|
100
100
|
mode: AVAudioSession.Mode,
|
|
101
101
|
policy: AVAudioSession.RouteSharingPolicy,
|
|
102
|
-
options: AVAudioSession.CategoryOptions
|
|
102
|
+
options: AVAudioSession.CategoryOptions,
|
|
103
103
|
) {
|
|
104
104
|
let category = self.category
|
|
105
105
|
.flatMap { SessionCategory(rawValue: $0.stringValue) }?
|
|
@@ -113,7 +113,8 @@ extension NativeIOSSetupOptions {
|
|
|
113
113
|
let options = (self.categoryOptions ?? [])
|
|
114
114
|
.reduce(into: AVAudioSession.CategoryOptions()) { acc, option in
|
|
115
115
|
if let mapped = SessionCategoryOptions(rawValue: option.stringValue)?
|
|
116
|
-
.mapConfigToAVAudioSessionCategoryOptions()
|
|
116
|
+
.mapConfigToAVAudioSessionCategoryOptions()
|
|
117
|
+
{
|
|
117
118
|
acc.insert(mapped)
|
|
118
119
|
}
|
|
119
120
|
}
|
|
@@ -43,7 +43,7 @@ public struct MediaIntentCriteria: Sendable {
|
|
|
43
43
|
artist: String? = nil,
|
|
44
44
|
album: String? = nil,
|
|
45
45
|
title: String? = nil,
|
|
46
|
-
playlist: String? = nil
|
|
46
|
+
playlist: String? = nil,
|
|
47
47
|
) {
|
|
48
48
|
self.query = query
|
|
49
49
|
self.reference = reference
|
|
@@ -90,7 +90,7 @@ public struct MediaIntentCriteria: Sendable {
|
|
|
90
90
|
mediaTypeMode: String?,
|
|
91
91
|
reference: Reference,
|
|
92
92
|
hasMediaType: Bool,
|
|
93
|
-
appName: String
|
|
93
|
+
appName: String?,
|
|
94
94
|
) -> MediaIntentCriteria {
|
|
95
95
|
let name = (mediaName ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
96
96
|
let trimmedNonEmpty: (String?) -> String? = {
|
|
@@ -115,12 +115,12 @@ public struct MediaIntentCriteria: Sendable {
|
|
|
115
115
|
hasGenres: !genreNames.isEmpty,
|
|
116
116
|
hasMediaType: hasMediaType,
|
|
117
117
|
matchesAppName: queryMatchesAppName(query, appName: appName),
|
|
118
|
-
searchMode: mediaTypeMode,
|
|
118
|
+
searchMode: mediaTypeMode, // mode is the vertical, verbatim — no derivation
|
|
119
119
|
genre: genre,
|
|
120
120
|
artist: artist,
|
|
121
121
|
album: album,
|
|
122
122
|
title: title,
|
|
123
|
-
playlist: playlist
|
|
123
|
+
playlist: playlist,
|
|
124
124
|
)
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -25,7 +25,9 @@ final class MediaLoader {
|
|
|
25
25
|
|
|
26
26
|
weak var delegate: MediaLoaderDelegate?
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/// The in-flight resolve. Readable so tests can await the resolve-then-load
|
|
29
|
+
/// chain instead of sleeping past it; only this type starts and cancels it.
|
|
30
|
+
private(set) var mediaResolverTask: Task<Void, Never>?
|
|
29
31
|
private var metadataLoadTask: Task<Void, Never>?
|
|
30
32
|
private var playableLoadTask: Task<Void, Never>?
|
|
31
33
|
private var url: URL?
|
|
@@ -76,7 +76,10 @@ class PlaybackCoordinator {
|
|
|
76
76
|
/// the window/attempt counters otherwise reset only on track change.
|
|
77
77
|
/// Overridable in tests.
|
|
78
78
|
var healthyPlaybackDuration: TimeInterval = 20
|
|
79
|
-
|
|
79
|
+
|
|
80
|
+
/// The pending refill. Readable so tests can await the window instead of
|
|
81
|
+
/// sleeping past it; only this type arms and cancels it.
|
|
82
|
+
private(set) var healthyPlaybackTask: Task<Void, Never>?
|
|
80
83
|
|
|
81
84
|
/// Playback rate (1.0 = normal speed).
|
|
82
85
|
var rate: Float = 1.0
|
|
@@ -8,7 +8,7 @@ import os.log
|
|
|
8
8
|
var isEnabled: Bool { get }
|
|
9
9
|
/// Whether the current load has produced audio — selects the retry budget.
|
|
10
10
|
var hasPlayed: Bool { get set }
|
|
11
|
-
func isRetryable(_ error: Error?) -> Bool
|
|
11
|
+
func isRetryable(_ error: Error?, httpStatusCode: Int?) -> Bool
|
|
12
12
|
func attemptRetry(startFromCurrentTime: Bool) async -> Bool
|
|
13
13
|
func reset()
|
|
14
14
|
}
|
|
@@ -62,7 +62,7 @@ public enum PlaybackErrorContext {
|
|
|
62
62
|
logger.error("[\(String(describing: context))] failure with nil error")
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
if retryHandler.isEnabled, retryHandler.isRetryable(error) {
|
|
65
|
+
if retryHandler.isEnabled, retryHandler.isRetryable(error, httpStatusCode: httpStatusCode) {
|
|
66
66
|
onRetryingError?(PlaybackErrorHandler.classify(
|
|
67
67
|
error: error, fallback: context.fallbackError, httpStatusCode: httpStatusCode,
|
|
68
68
|
))
|
|
@@ -138,7 +138,7 @@ public enum PlaybackErrorContext {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
if let httpStatusCode, !(200...299).contains(httpStatusCode) {
|
|
141
|
+
if let httpStatusCode, !(200 ... 299).contains(httpStatusCode) {
|
|
142
142
|
return .httpStatus(httpStatusCode)
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -155,7 +155,7 @@ public enum PlaybackErrorContext {
|
|
|
155
155
|
/// with HTTP statuses — so only in-range values count, and the newest wins:
|
|
156
156
|
/// a live stream reconnects, and older entries describe earlier attempts.
|
|
157
157
|
public static func httpStatusCode(fromErrorStatusCodes codes: [Int]) -> Int? {
|
|
158
|
-
codes.last { (100...599).contains($0) }
|
|
158
|
+
codes.last { (100 ... 599).contains($0) }
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/// The stream was fetched but cannot be decoded — unknown container,
|
|
@@ -45,8 +45,15 @@ class QueueManager {
|
|
|
45
45
|
/// The repeat mode for the queue. Plain property — no side effects.
|
|
46
46
|
var repeatMode: RepeatMode = .off
|
|
47
47
|
|
|
48
|
-
/// Whether shuffle mode is enabled.
|
|
49
|
-
var shuffleEnabled: Bool = false
|
|
48
|
+
/// Whether shuffle mode is enabled.
|
|
49
|
+
var shuffleEnabled: Bool = false {
|
|
50
|
+
didSet {
|
|
51
|
+
// Re-pin on enable: the existing order has the current track wherever it happened to land,
|
|
52
|
+
// so without this, turning shuffle on near the end of the queue ends it right away.
|
|
53
|
+
guard shuffleEnabled, shuffleEnabled != oldValue else { return }
|
|
54
|
+
shuffleOrder.reshuffle(keepingFirst: currentIndex)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
50
57
|
|
|
51
58
|
// MARK: - Computed Properties
|
|
52
59
|
|
|
@@ -206,7 +213,7 @@ class QueueManager {
|
|
|
206
213
|
currentIndex = clampedIndex
|
|
207
214
|
queueSourcePath = sourcePath
|
|
208
215
|
tracks = newTracks
|
|
209
|
-
shuffleOrder = ShuffleOrder(length: newTracks.count)
|
|
216
|
+
shuffleOrder = ShuffleOrder(length: newTracks.count, firstIndex: clampedIndex)
|
|
210
217
|
return true
|
|
211
218
|
}
|
|
212
219
|
|