react-native-audio-browser 0.2.0-next.27 → 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 (139) hide show
  1. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +8 -4
  2. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +3 -4
  3. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +13 -0
  4. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +2 -2
  5. package/ios/AVPlayerEngine.swift +334 -0
  6. package/ios/AVPlayerEngineCallbacks.swift +15 -0
  7. package/ios/Browser/BrowseGate.swift +58 -0
  8. package/ios/Browser/BrowserConfig.swift +31 -89
  9. package/ios/Browser/BrowserManager.swift +68 -487
  10. package/ios/Browser/NavigationErrorState.swift +41 -0
  11. package/ios/Browser/PageResolver.swift +227 -0
  12. package/ios/Browser/RequestLayer.swift +90 -0
  13. package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +175 -107
  14. package/ios/CarPlay/CPInterfaceController+Safe.swift +11 -24
  15. package/ios/CarPlay/CarPlayArtworkResolver.swift +35 -83
  16. package/ios/CarPlay/CarPlayController.swift +42 -1442
  17. package/ios/CarPlay/CarPlayImageLoader.swift +42 -83
  18. package/ios/CarPlay/CarPlayListItemFactory.swift +9 -23
  19. package/ios/CarPlay/CarPlayNowPlayingManager.swift +119 -129
  20. package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
  21. package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
  22. package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
  23. package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
  24. package/ios/CarPlay/CarPlaySession.swift +209 -0
  25. package/ios/CarPlay/CarPlayTemplates.swift +63 -0
  26. package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
  27. package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
  28. package/ios/CarPlay/RNABMediaAddHandler.swift +2 -18
  29. package/ios/CarPlay/RNABMediaAffinityHandler.swift +5 -21
  30. package/ios/CarPlay/RNABMediaIntentHandler.swift +2 -3
  31. package/ios/Extension/NavigationError+Conversion.swift +7 -4
  32. package/ios/Extension/UIImage+Adaptive.swift +12 -0
  33. package/ios/Extension/URLError+Connectivity.swift +15 -0
  34. package/ios/Http/HttpClient.swift +63 -145
  35. package/ios/HybridAudioBrowser+Browser.swift +153 -0
  36. package/ios/HybridAudioBrowser+Options.swift +144 -0
  37. package/ios/HybridAudioBrowser+Player.swift +397 -0
  38. package/ios/HybridAudioBrowser+PlayerCallbacks.swift +190 -0
  39. package/ios/HybridAudioBrowser+Stubs.swift +46 -0
  40. package/ios/HybridAudioBrowser.swift +74 -1687
  41. package/ios/Model/NitroTypeStubs.swift +18 -1
  42. package/ios/Model/PlayerUpdateOptions.swift +1 -1
  43. package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
  44. package/ios/Observer/ItemObserver.swift +90 -0
  45. package/ios/Observer/PlayerObserver.swift +59 -0
  46. package/ios/Option/SessionCategories.swift +41 -92
  47. package/ios/Player/AudioSessionController.swift +177 -0
  48. package/ios/Player/DeferredSeek.swift +70 -0
  49. package/ios/Player/ErrorStatus.swift +27 -0
  50. package/ios/Player/MediaLoader.swift +24 -11
  51. package/ios/Player/NowPlayingUpdater.swift +157 -58
  52. package/ios/Player/PlaybackFailure.swift +125 -0
  53. package/ios/Player/PlaybackPersistence.swift +73 -0
  54. package/ios/Player/Player.swift +1083 -0
  55. package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
  56. package/ios/Player/PlayerEngine.swift +89 -0
  57. package/ios/Player/PlayerStatusTypes.swift +2 -6
  58. package/ios/Player/QueueManager.swift +52 -59
  59. package/ios/Player/RetryLoop.swift +261 -0
  60. package/ios/Player/SleepTimerManager.swift +1 -1
  61. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +2 -3
  62. package/ios/RemoteCommand/RemoteCommandController.swift +85 -367
  63. package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -356
  64. package/ios/Tests/DeferredSeekTests.swift +70 -0
  65. package/ios/Tests/EmitterTests.swift +29 -0
  66. package/ios/Tests/GenerationMemoTests.swift +121 -0
  67. package/ios/Tests/LRUCacheTests.swift +4 -5
  68. package/ios/Tests/MediaLoaderTests.swift +12 -33
  69. package/ios/Tests/ObserverTests.swift +88 -0
  70. package/ios/Tests/PlaybackErrorTests.swift +76 -0
  71. package/ios/Tests/PlaybackFailureTests.swift +189 -0
  72. package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
  73. package/ios/Tests/PlayerPolicyTests.swift +405 -0
  74. package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +298 -248
  75. package/ios/Tests/PlayingStateTests.swift +42 -0
  76. package/ios/Tests/QueueManagerTests.swift +76 -107
  77. package/ios/Tests/ReadyValueTests.swift +73 -0
  78. package/ios/Tests/RemoteCommandControllerTests.swift +10 -30
  79. package/ios/Tests/RemoteCommandTests.swift +2 -4
  80. package/ios/Tests/RequestLayerTests.swift +112 -0
  81. package/ios/Tests/RetryLoopTests.swift +270 -0
  82. package/ios/Tests/Support/TestTypes.swift +111 -112
  83. package/ios/Tests/TrackIdTemplateTests.swift +1 -1
  84. package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
  85. package/ios/TrackSelector.swift +1 -1
  86. package/ios/Util/ArtworkImageFetcher.swift +42 -0
  87. package/ios/Util/Emitter.swift +42 -0
  88. package/ios/Util/GenerationMemo.swift +36 -0
  89. package/ios/Util/LRUCache.swift +23 -162
  90. package/ios/Util/NetworkMonitor.swift +1 -1
  91. package/ios/Util/ReadyValue.swift +56 -0
  92. package/ios/Util/SFSymbolRenderer.swift +35 -29
  93. package/ios/Util/SVGProcessor.swift +0 -10
  94. package/lib/commonjs/web/NativeAudioBrowser.js +1 -1
  95. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +1 -1
  96. package/lib/module/web/NativeAudioBrowser.js +1 -1
  97. package/lib/module/web/TrackPlayer/PlayingStateFactory.js +1 -1
  98. package/lib/typescript/src/types/browser.d.ts +3 -3
  99. package/package.json +1 -1
  100. package/src/types/browser.ts +3 -3
  101. package/src/web/NativeAudioBrowser.ts +1 -1
  102. package/src/web/TrackPlayer/PlayingStateFactory.test.ts +1 -1
  103. package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
  104. package/ios/Bridge.h +0 -8
  105. package/ios/Browser/MediaResolveComposer.swift +0 -114
  106. package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
  107. package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
  108. package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
  109. package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
  110. package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
  111. package/ios/Observer/PlayerStateObserver.swift +0 -50
  112. package/ios/Observer/PlayerTimeObserver.swift +0 -55
  113. package/ios/Option/PitchAlgorithms.swift +0 -17
  114. package/ios/Option/TimeEventFrequency.swift +0 -18
  115. package/ios/Player/LoadSeekCoordinator.swift +0 -99
  116. package/ios/Player/MediaLoaderDelegate.swift +0 -11
  117. package/ios/Player/PlaybackCoordinator.swift +0 -761
  118. package/ios/Player/PlaybackEffectHandler.swift +0 -54
  119. package/ios/Player/PlaybackErrorHandler.swift +0 -175
  120. package/ios/Player/PlaybackStateStore.swift +0 -27
  121. package/ios/Player/PlayingStateManager.swift +0 -33
  122. package/ios/Player/RetryManager.swift +0 -384
  123. package/ios/Player/SeekCompletionHandler.swift +0 -5
  124. package/ios/Player/SleepTimerHandling.swift +0 -28
  125. package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
  126. package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -327
  127. package/ios/Tests/MediaResolveTests.swift +0 -126
  128. package/ios/Tests/OnceValueTests.swift +0 -79
  129. package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -549
  130. package/ios/Tests/PlaybackStateStoreTests.swift +0 -49
  131. package/ios/Tests/PlayerTimeObserverTests.swift +0 -37
  132. package/ios/Tests/PlayingStateManagerTests.swift +0 -57
  133. package/ios/Tests/RetryManagerTests.swift +0 -178
  134. package/ios/TrackPlayer.swift +0 -1042
  135. package/ios/TrackPlayerCallbacks.swift +0 -53
  136. package/ios/Util/MetadataAdapter.swift +0 -131
  137. package/ios/Util/OnceValue.swift +0 -104
  138. /package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +0 -0
  139. /package/ios/{Browser → CarPlay}/SectionPresentation.swift +0 -0
@@ -5,7 +5,7 @@ import type { PlaybackState, PlayingState } from '../../features'
5
5
  * player's intent (`playWhenReady`) and its current {@link PlaybackState}.
6
6
  *
7
7
  * A faithful port of the native derivations (android `PlayingStateFactory.derive`,
8
- * ios `PlayingStateManager.update`) so all platforms agree on what "playing" and
8
+ * ios `Player.playingState`) so all platforms agree on what "playing" and
9
9
  * "buffering" mean for the now-playing UI.
10
10
  */
11
11
  export function derivePlayingState(
package/ios/Bridge.h DELETED
@@ -1,8 +0,0 @@
1
- //
2
- // Bridge.h
3
- // audio-browser
4
- //
5
- // Created by puckey on 10/12/2025
6
- //
7
-
8
- #pragma once
@@ -1,114 +0,0 @@
1
- import Foundation
2
-
3
- /// Pure, Nitro-free composition logic for the media `resolve` layer.
4
- ///
5
- /// The two pieces that need testing in isolation from the Nitro bridge are:
6
- /// 1. the variant unwrap — `resolve(track)` returns a `RequestConfig` either
7
- /// synchronously (`.first`) or via a `Promise` (`.second`); both arms must
8
- /// yield the same `RequestConfig`; and
9
- /// 2. the merge — the resolved config is layered over the base with
10
- /// override-wins per field (and per-key dict merge for headers/query).
11
- ///
12
- /// Production (`BrowserManager+URLResolution.swift`) builds the Nitro
13
- /// `RequestConfig` values and calls into `MediaResolveComposer.merge` /
14
- /// `.unwrap`; the SPM test target exercises the same functions against the
15
- /// stub `RequestConfigLike` value type. Keeping the logic here (rather than in
16
- /// the Nitro-only extension) is what makes it reachable from tests.
17
- enum MediaResolveComposer {
18
- /// A minimal field set mirroring `RequestConfig`, used so the merge logic can
19
- /// be expressed once and shared between the Nitro types and the test stub.
20
- struct RequestConfigLike: Equatable {
21
- var method: String?
22
- var path: String?
23
- var baseUrl: String?
24
- var headers: [String: String]?
25
- var query: [String: String]?
26
- var body: String?
27
- var contentType: String?
28
- var userAgent: String?
29
-
30
- init(
31
- method: String? = nil,
32
- path: String? = nil,
33
- baseUrl: String? = nil,
34
- headers: [String: String]? = nil,
35
- query: [String: String]? = nil,
36
- body: String? = nil,
37
- contentType: String? = nil,
38
- userAgent: String? = nil,
39
- ) {
40
- self.method = method
41
- self.path = path
42
- self.baseUrl = baseUrl
43
- self.headers = headers
44
- self.query = query
45
- self.body = body
46
- self.contentType = contentType
47
- self.userAgent = userAgent
48
- }
49
- }
50
-
51
- /// The unwrapped result of a `resolve(track)` variant.
52
- enum ResolveVariant<Config> {
53
- /// `.first` — a synchronous config.
54
- case sync(Config)
55
- /// `.second` — a config delivered via an async closure (a Promise in prod).
56
- case async(() async throws -> Config)
57
- }
58
-
59
- /// Unwraps a sync/async config into a concrete config, awaiting the async arm
60
- /// if needed. (Production no longer uses a Nitro variant — `resolve`/`resolveSync`
61
- /// are separate fields — but this stays as a pure, testable composition helper.)
62
- static func unwrap<Config>(_ variant: ResolveVariant<Config>) async throws -> Config {
63
- switch variant {
64
- case let .sync(config):
65
- config
66
- case let .async(makeConfig):
67
- try await makeConfig()
68
- }
69
- }
70
-
71
- /// Run-both resolve composition: the async-resolved config first, then the
72
- /// sync-resolved merged over it (sync winning) via `combine`. `nil` when neither
73
- /// produced a config. Generic so it is shared verbatim by production (Nitro
74
- /// `RequestConfig` + `mergeRequestConfig`) and tests (`RequestConfigLike` +
75
- /// `merge`) — one implementation, exercised on both sides.
76
- static func composeResolved<C>(
77
- async asyncResolved: C?,
78
- sync syncResolved: C?,
79
- combine: (C, C) -> C,
80
- ) -> C? {
81
- switch (asyncResolved, syncResolved) {
82
- case (nil, nil): nil
83
- case let (value?, nil): value
84
- case let (nil, value?): value
85
- case let (a?, s?): combine(a, s)
86
- }
87
- }
88
-
89
- /// Override-wins per-field merge. Headers and query merge per-key (override
90
- /// key wins). Matches `BrowserManager.mergeRequestConfig`.
91
- static func merge(base: RequestConfigLike, override: RequestConfigLike) -> RequestConfigLike {
92
- RequestConfigLike(
93
- method: override.method ?? base.method,
94
- path: override.path ?? base.path,
95
- baseUrl: override.baseUrl ?? base.baseUrl,
96
- headers: mergeDicts(base.headers, override.headers),
97
- query: mergeDicts(base.query, override.query),
98
- body: override.body ?? base.body,
99
- contentType: override.contentType ?? base.contentType,
100
- userAgent: override.userAgent ?? base.userAgent,
101
- )
102
- }
103
-
104
- /// Per-key dict merge with override-wins; nil-safe (returns the non-nil side,
105
- /// or nil when both are nil). Matches `BrowserManager.mergeDicts`.
106
- static func mergeDicts(_ base: [String: String]?, _ override: [String: String]?) -> [String: String]? {
107
- switch (base, override) {
108
- case (nil, nil): nil
109
- case let (base?, nil): base
110
- case let (nil, override?): override
111
- case let (base?, override?): base.merging(override) { _, new in new }
112
- }
113
- }
114
- }
@@ -1,78 +0,0 @@
1
- import Foundation
2
- @preconcurrency import MediaPlayer
3
-
4
- /**
5
- Enum representing MPMediaItemProperties.
6
- Docs for each property is taken from [Apple docs](https://developer.apple.com/documentation/mediaplayer/mpmediaitem/general_media_item_property_keys)
7
- */
8
- enum MediaItemProperty: NowPlayingInfoKeyValue {
9
- /**
10
- The performing artist(s) for a media item—which may vary from the primary artist for the album that a media item belongs to.
11
-
12
- For example, if the album artist is “Joseph Fable,” the artist for one of the songs in the album may be “Joseph Fable featuring Thomas Smithson”. Value is an NSString object.
13
- */
14
- case artist(String?)
15
-
16
- /**
17
- The title (or name) of the media item.
18
-
19
- This property is unrelated to the MPMediaItemPropertyAlbumTitle property. Value is an NSString object.
20
- */
21
- case title(String?)
22
-
23
- /**
24
- The title of an album.
25
-
26
- This property contains the album title, such as “Live On Mars”, as opposed to the title of an individual song on the album, such as “Crater Dance (radio edit)” (which you specify using the MPMediaItemPropertyTitle property). Value is an NSString object.
27
- */
28
- case albumTitle(String?)
29
-
30
- /**
31
- The playback duration of the media item.
32
- Value is an NSNumber object representing a duration in seconds as an TimeInterval.
33
- */
34
- case duration(TimeInterval?)
35
-
36
- /**
37
- The artwork image for the media item.
38
- */
39
- case artwork(MPMediaItemArtwork?)
40
-
41
- var key: String {
42
- switch self {
43
- case .artist:
44
- MPMediaItemPropertyArtist
45
-
46
- case .title:
47
- MPMediaItemPropertyTitle
48
-
49
- case .albumTitle:
50
- MPMediaItemPropertyAlbumTitle
51
-
52
- case .duration:
53
- MPMediaItemPropertyPlaybackDuration
54
-
55
- case .artwork:
56
- MPMediaItemPropertyArtwork
57
- }
58
- }
59
-
60
- var value: Any? {
61
- switch self {
62
- case let .artist(artist):
63
- artist
64
-
65
- case let .title(title):
66
- title
67
-
68
- case let .albumTitle(title):
69
- title
70
-
71
- case let .duration(duration):
72
- duration != nil ? NSNumber(floatLiteral: duration!) : nil
73
-
74
- case let .artwork(artwork):
75
- artwork
76
- }
77
- }
78
- }
@@ -1,6 +0,0 @@
1
- import Foundation
2
-
3
- protocol NowPlayingInfoKeyValue {
4
- var key: String { get }
5
- var value: Any? { get }
6
- }
@@ -1,220 +0,0 @@
1
- import Foundation
2
- @preconcurrency import MediaPlayer
3
-
4
- /**
5
- Enum representing MPNowPlayingInfoProperties.
6
- Docs for each property is taken from [Apple docs](https://developer.apple.com/documentation/mediaplayer/mpnowplayinginfocenter/additional_metadata_properties).
7
- */
8
- enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
9
- /**
10
- The identifier of the collection the now playing item belongs to.
11
- The identifier can be an album, artist, playlist, etc.
12
- */
13
- case collectionIdentifier(String?)
14
-
15
- /**
16
- The available language option groups for the now playing item.
17
- Value is an array of MPNowPlayingInfoLanguageOptionGroup items. Only one language option in a given group can be played at a time.
18
- */
19
- case availableLanguageOptions([MPNowPlayingInfoLanguageOptionGroup]?)
20
-
21
- /**
22
- The URL pointing to the now playing item's underlying asset.
23
- This constant is used by the system UI when video thumbnails or audio waveform visualizations are applicable.
24
- */
25
- case assetUrl(URL?)
26
-
27
- /**
28
- The total number of chapters in the now-playing item.
29
- Value is an NSNumber object configured as an NSUInteger.
30
- */
31
- case chapterCount(UInt64?)
32
-
33
- /**
34
- The number corresponding to the chapter currently being played.
35
- Value is an NSNumber object configured as an NSUInteger. Chapter numbering uses zero-based indexing. If you want the first chapter in the now-playing item to be displayed as “Chapter 1,” for example, set the chapter number to 0.
36
- */
37
- case chapterNumber(UInt64?)
38
-
39
- /**
40
- The currently active language options for the now playing item.
41
- Value is an array of MPNowPlayingInfoLanguageOption items.
42
- */
43
- case currentLanguageOptions([MPNowPlayingInfoLanguageOption]?)
44
-
45
- /**
46
- The default playback rate for the now playing item.
47
- Value is an NSNumber object configured as a double. Set this property if your app is playing a media item at a playback rate other than 1.0 as its default rate.
48
- */
49
- case defaultPlaybackRate(Double?)
50
-
51
- /**
52
- The elapsed time of the now playing item, in seconds.
53
- Value is an NSNumber object configured as a double. Elapsed time is automatically calculated, by the system, from the previously provided elapsed time and the playback rate. Do not update this property frequently—it is not necessary
54
- */
55
- case elapsedPlaybackTime(Double?)
56
-
57
- /**
58
- The opaque identifier that uniquely identifies the now playing item, even through app relaunches.
59
- This is only used to reference the item to the now playing app and can be in any format.
60
- */
61
- case externalContentIdentifier(Any?)
62
-
63
- /**
64
- The opaque identifier that uniquely identifies the profile the now playing item is played from, even through app relaunches.
65
- This is only used to reference the profile to the now playing app and can be in any format.
66
- */
67
- case externalUserProfileIdentifier(Any?)
68
-
69
- /**
70
- Denotes whether the now playing item is a live stream.
71
- Value is an NSNumber object configured as a boolean. A value of 1.0 indicates the now playing item is a live stream.
72
- */
73
- case isLiveStream(Bool?)
74
-
75
- /**
76
- The media type of the now playing item.
77
- Value is an NSNumber object configured as a MPNowPlayingInfoMediaType.
78
- */
79
- case mediaType(MPNowPlayingInfoMediaType?)
80
-
81
- /**
82
- The current progress of the now playing item.
83
- Value is an NSNumber object configured as a float. A value of 0.0 indicates the item has not been watched while a value of 1.0 indicates the item has been fully watched. This is a high-level indicator. Use MPNowPlayingInfoPropertyElapsedPlaybackTime for fine-detailed information on how much of the item has been watched.
84
- */
85
- case playbackProgress(Float?)
86
-
87
- /**
88
- The total number of items in the app’s playback queue.
89
- Value is an NSNumber object configured as an NSUInteger.
90
- */
91
- case playbackQueueCount(UInt64?)
92
-
93
- /**
94
- The index of the now-playing item in the app’s playback queue.
95
- Value is an NSNumber object configured as an NSUInteger. The playback queue uses zero-based indexing. If you want the first item in the queue to be displayed as “item 1 of 10,” for example, set the item’s index to 0.
96
- */
97
- case playbackQueueIndex(UInt64?)
98
-
99
- /**
100
- The playback rate of the now-playing item, with a value of 1.0 indicating the normal playback rate.
101
- Value is an NSNumber object configured as a double. The default value is 1.0. A playback rate value of 2.0 means twice the normal playback rate; a piece of media played at this rate would take half as long to play to completion. A value of 0.5 means half the normal playback rate; a piece of media played at this rate would take twice as long to play to completion.
102
- */
103
- case playbackRate(Double?)
104
-
105
- /**
106
- The service provider associated with the now-playing item.
107
- Value is a unique NSString that identifies the service provider for the now-playing item. If the now-playing item belongs to a channel or subscription service, this key can be used to coordinate various types of now-playing content from the service provider.
108
- */
109
- case serviceIdentifier(String?)
110
-
111
- var key: String {
112
- switch self {
113
- case .collectionIdentifier:
114
- MPNowPlayingInfoCollectionIdentifier
115
-
116
- case .availableLanguageOptions:
117
- MPNowPlayingInfoPropertyAvailableLanguageOptions
118
-
119
- case .assetUrl:
120
- MPNowPlayingInfoPropertyAssetURL
121
-
122
- case .chapterCount:
123
- MPNowPlayingInfoPropertyChapterCount
124
-
125
- case .chapterNumber:
126
- MPNowPlayingInfoPropertyChapterNumber
127
-
128
- case .currentLanguageOptions:
129
- MPNowPlayingInfoPropertyCurrentLanguageOptions
130
-
131
- case .defaultPlaybackRate:
132
- MPNowPlayingInfoPropertyDefaultPlaybackRate
133
-
134
- case .elapsedPlaybackTime:
135
- MPNowPlayingInfoPropertyElapsedPlaybackTime
136
-
137
- case .externalContentIdentifier:
138
- MPNowPlayingInfoPropertyExternalContentIdentifier
139
-
140
- case .externalUserProfileIdentifier:
141
- MPNowPlayingInfoPropertyExternalUserProfileIdentifier
142
-
143
- case .isLiveStream:
144
- MPNowPlayingInfoPropertyIsLiveStream
145
-
146
- case .mediaType:
147
- MPNowPlayingInfoPropertyMediaType
148
-
149
- case .playbackProgress:
150
- MPNowPlayingInfoPropertyPlaybackProgress
151
-
152
- case .playbackQueueCount:
153
- MPNowPlayingInfoPropertyPlaybackQueueCount
154
-
155
- case .playbackQueueIndex:
156
- MPNowPlayingInfoPropertyPlaybackQueueIndex
157
-
158
- case .playbackRate:
159
- MPNowPlayingInfoPropertyPlaybackRate
160
-
161
- case .serviceIdentifier:
162
- MPNowPlayingInfoPropertyServiceIdentifier
163
- }
164
- }
165
-
166
- var value: Any? {
167
- switch self {
168
- case let .collectionIdentifier(identifier):
169
- identifier
170
-
171
- case let .availableLanguageOptions(options):
172
- options
173
-
174
- case let .assetUrl(url):
175
- url
176
-
177
- case let .chapterCount(count):
178
- count != nil ? NSNumber(value: count!) : nil
179
-
180
- case let .chapterNumber(number):
181
- number != nil ? NSNumber(value: number!) : nil
182
-
183
- case let .currentLanguageOptions(options):
184
- options
185
-
186
- case let .defaultPlaybackRate(rate):
187
- rate != nil ? NSNumber(floatLiteral: rate!) : nil
188
-
189
- case let .elapsedPlaybackTime(time):
190
- time != nil ? NSNumber(floatLiteral: time!) : nil
191
-
192
- case let .externalContentIdentifier(id):
193
- id
194
-
195
- case let .externalUserProfileIdentifier(id):
196
- id
197
-
198
- case let .isLiveStream(status):
199
- status
200
-
201
- case let .mediaType(type):
202
- type != nil ? NSNumber(value: type!.rawValue) : nil
203
-
204
- case let .playbackProgress(progress):
205
- progress != nil ? NSNumber(value: progress!) : nil
206
-
207
- case let .playbackQueueCount(count):
208
- count != nil ? NSNumber(value: count!) : nil
209
-
210
- case let .playbackQueueIndex(index):
211
- index != nil ? NSNumber(value: index!) : nil
212
-
213
- case let .playbackRate(rate):
214
- rate != nil ? NSNumber(floatLiteral: rate!) : nil
215
-
216
- case let .serviceIdentifier(id):
217
- id != nil ? NSString(string: id!) : nil
218
- }
219
- }
220
- }
@@ -1,119 +0,0 @@
1
- import AVFoundation
2
- import Foundation
3
-
4
- /**
5
- Observes player item notifications and invokes callbacks passed at initialization.
6
- */
7
- @MainActor class PlayerItemNotificationObserver {
8
- private let notificationCenter: NotificationCenter = .default
9
-
10
- private(set) weak var observingAVItem: AVPlayerItem?
11
- private(set) var isObserving: Bool = false
12
-
13
- private let onDidPlayToEndTime: @MainActor () -> Void
14
- private let onFailedToPlayToEndTime: @MainActor (Error?) -> Void
15
- private let onPlaybackStalled: @MainActor () -> Void
16
-
17
- init(
18
- onDidPlayToEndTime: @escaping @MainActor () -> Void,
19
- onFailedToPlayToEndTime: @escaping @MainActor (Error?) -> Void,
20
- onPlaybackStalled: @escaping @MainActor () -> Void,
21
- ) {
22
- self.onDidPlayToEndTime = onDidPlayToEndTime
23
- self.onFailedToPlayToEndTime = onFailedToPlayToEndTime
24
- self.onPlaybackStalled = onPlaybackStalled
25
- }
26
-
27
- /**
28
- Will start observing notifications from an AVPlayerItem.
29
-
30
- - parameter avItem: The AVPlayerItem to observe.
31
- - important: Cannot observe more than one item at a time.
32
- */
33
- func startObserving(item avItem: AVPlayerItem) {
34
- stopObservingCurrentItem()
35
- observingAVItem = avItem
36
- isObserving = true
37
- notificationCenter.addObserver(
38
- self,
39
- selector: #selector(avItemDidPlayToEndTime(_:)),
40
- name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
41
- object: avItem,
42
- )
43
- notificationCenter.addObserver(
44
- self,
45
- selector: #selector(avItemFailedToPlayToEndTime(_:)),
46
- name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
47
- object: avItem,
48
- )
49
- notificationCenter.addObserver(
50
- self,
51
- selector: #selector(avItemPlaybackStalled(_:)),
52
- name: NSNotification.Name.AVPlayerItemPlaybackStalled,
53
- object: avItem,
54
- )
55
- }
56
-
57
- /**
58
- Stop receiving notifications for the current AVPlayerItem.
59
- */
60
- func stopObservingCurrentItem() {
61
- guard let observingAVItem, isObserving else {
62
- return
63
- }
64
- notificationCenter.removeObserver(
65
- self,
66
- name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
67
- object: observingAVItem,
68
- )
69
- notificationCenter.removeObserver(
70
- self,
71
- name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
72
- object: observingAVItem,
73
- )
74
- notificationCenter.removeObserver(
75
- self,
76
- name: NSNotification.Name.AVPlayerItemPlaybackStalled,
77
- object: observingAVItem,
78
- )
79
- self.observingAVItem = nil
80
- isObserving = false
81
- }
82
-
83
- @objc private nonisolated func avItemDidPlayToEndTime(_ notification: Notification) {
84
- let identity = (notification.object as? AVPlayerItem).map(ObjectIdentifier.init)
85
- Task { @MainActor in
86
- guard self.isObserving(identity) else { return }
87
- self.onDidPlayToEndTime()
88
- }
89
- }
90
-
91
- @objc private nonisolated func avItemFailedToPlayToEndTime(_ notification: Notification) {
92
- // Extract the error from the notification's userInfo
93
- // AVPlayerItemFailedToPlayToEndTimeErrorKey contains the actual error
94
- let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error
95
- let identity = (notification.object as? AVPlayerItem).map(ObjectIdentifier.init)
96
- Task { @MainActor in
97
- guard self.isObserving(identity) else { return }
98
- self.onFailedToPlayToEndTime(error)
99
- }
100
- }
101
-
102
- // Fired when the item's buffer empties mid-playback. iOS exposes no
103
- // rebuffer-vs-initial signal otherwise, and AVPlayer can stay parked in
104
- // `.waitingToPlayAtSpecifiedRate` after data returns — the listener nudges it.
105
- @objc private nonisolated func avItemPlaybackStalled(_ notification: Notification) {
106
- let identity = (notification.object as? AVPlayerItem).map(ObjectIdentifier.init)
107
- Task { @MainActor in
108
- guard self.isObserving(identity) else { return }
109
- self.onPlaybackStalled()
110
- }
111
- }
112
-
113
- /// True when `identity` still names the observed item. Notifications are posted off the main
114
- /// thread, so the main-actor hop can land after the observer switched items — without this
115
- /// check the old item's late delivery (e.g. a stale failure) reaches the new item's handlers.
116
- private func isObserving(_ identity: ObjectIdentifier?) -> Bool {
117
- identity != nil && observingAVItem.map(ObjectIdentifier.init) == identity
118
- }
119
- }
@@ -1,133 +0,0 @@
1
- @preconcurrency import AVFoundation
2
- import Foundation
3
-
4
- // AVTimedMetadataGroup is not Sendable, but we only use it on the main thread
5
- // via AVPlayerItemMetadataOutputPushDelegate (queue: .main)
6
- extension AVTimedMetadataGroup: @retroactive @unchecked Sendable {}
7
-
8
- /**
9
- Observes player item property changes and invokes callbacks passed at initialization.
10
- Uses modern block-based KVO for type-safe observation with automatic cleanup.
11
- */
12
- @MainActor final class PlayerItemPropertyObserver: NSObject {
13
- private var observations: [NSKeyValueObservation] = []
14
- private var currentMetadataOutput: AVPlayerItemMetadataOutput?
15
-
16
- private(set) weak var observingAVItem: AVPlayerItem?
17
-
18
- private let onDurationUpdate: @MainActor (Double) -> Void
19
- private let onPlaybackLikelyToKeepUpUpdate: @MainActor (Bool) -> Void
20
- private let onStatusChange: @MainActor (AVPlayerItem.Status, Error?) -> Void
21
- // Note: AVTimedMetadataGroup is not Sendable, but this callback is always invoked
22
- // on the main thread via AVPlayerItemMetadataOutputPushDelegate (queue: .main)
23
- private let onTimedMetadataReceived: @MainActor ([AVTimedMetadataGroup]) -> Void
24
-
25
- init(
26
- onDurationUpdate: @escaping @MainActor (Double) -> Void,
27
- onPlaybackLikelyToKeepUpUpdate: @escaping @MainActor (Bool) -> Void,
28
- onStatusChange: @escaping @MainActor (AVPlayerItem.Status, Error?) -> Void,
29
- onTimedMetadataReceived: @escaping @MainActor ([AVTimedMetadataGroup]) -> Void,
30
- ) {
31
- self.onDurationUpdate = onDurationUpdate
32
- self.onPlaybackLikelyToKeepUpUpdate = onPlaybackLikelyToKeepUpUpdate
33
- self.onStatusChange = onStatusChange
34
- self.onTimedMetadataReceived = onTimedMetadataReceived
35
- }
36
-
37
- /**
38
- Start observing an AVPlayerItem. Will remove self as observer from old item, if any.
39
-
40
- - parameter avItem: The AVPlayerItem to observe.
41
- */
42
- func startObserving(item avItem: AVPlayerItem) {
43
- stopObservingCurrentItem()
44
-
45
- observingAVItem = avItem
46
-
47
- observations = [
48
- avItem.observe(\.duration, options: [.new]) { [weak self] item, _ in
49
- let seconds = item.duration.seconds
50
- let identity = ObjectIdentifier(item)
51
- Task { @MainActor in
52
- guard let self, self.isObserving(identity) else { return }
53
- self.onDurationUpdate(seconds)
54
- }
55
- },
56
- avItem.observe(\.loadedTimeRanges, options: [.new]) { [weak self] item, _ in
57
- if let duration = item.loadedTimeRanges.first?.timeRangeValue.duration {
58
- let seconds = duration.seconds
59
- let identity = ObjectIdentifier(item)
60
- Task { @MainActor in
61
- guard let self, self.isObserving(identity) else { return }
62
- self.onDurationUpdate(seconds)
63
- }
64
- }
65
- },
66
- avItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { [weak self] item, _ in
67
- let isLikely = item.isPlaybackLikelyToKeepUp
68
- let identity = ObjectIdentifier(item)
69
- Task { @MainActor in
70
- guard let self, self.isObserving(identity) else { return }
71
- self.onPlaybackLikelyToKeepUpUpdate(isLikely)
72
- }
73
- },
74
- avItem.observe(\.status, options: [.new]) { [weak self] item, _ in
75
- let status = item.status
76
- let error = item.error
77
- let identity = ObjectIdentifier(item)
78
- Task { @MainActor in
79
- guard let self, self.isObserving(identity) else { return }
80
- self.onStatusChange(status, error)
81
- }
82
- },
83
- ]
84
-
85
- // Create and add a new metadata output to the AVPlayerItem
86
- let metadataOutput = AVPlayerItemMetadataOutput()
87
- metadataOutput.setDelegate(self, queue: .main)
88
- avItem.add(metadataOutput)
89
- currentMetadataOutput = metadataOutput
90
- }
91
-
92
- func stopObservingCurrentItem() {
93
- observations.removeAll()
94
-
95
- if let observingAVItem {
96
- observingAVItem.removeAllMetadataOutputs()
97
- }
98
-
99
- observingAVItem = nil
100
- currentMetadataOutput = nil
101
- }
102
-
103
- /// True when `identity` still names the observed item. KVO fires off the main thread, so
104
- /// the main-actor hop can land after the observer switched items — without this check a
105
- /// stale delivery (e.g. the old item's `.failed`) is attributed to the freshly loaded one.
106
- private func isObserving(_ identity: ObjectIdentifier) -> Bool {
107
- observingAVItem.map(ObjectIdentifier.init) == identity
108
- }
109
- }
110
-
111
- extension PlayerItemPropertyObserver: AVPlayerItemMetadataOutputPushDelegate {
112
- nonisolated func metadataOutput(
113
- _ output: AVPlayerItemMetadataOutput,
114
- didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup],
115
- from _: AVPlayerItemTrack?,
116
- ) {
117
- // Delegate is called on main thread (queue: .main), so we can assume isolation
118
- MainActor.assumeIsolated {
119
- if output == currentMetadataOutput {
120
- onTimedMetadataReceived(groups)
121
- }
122
- }
123
- }
124
- }
125
-
126
- extension AVPlayerItem {
127
- @MainActor
128
- func removeAllMetadataOutputs() {
129
- for output in outputs.filter({ $0 is AVPlayerItemMetadataOutput }) {
130
- remove(output)
131
- }
132
- }
133
- }