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.
- package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +8 -4
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +3 -4
- package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +13 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +2 -2
- package/ios/AVPlayerEngine.swift +334 -0
- package/ios/AVPlayerEngineCallbacks.swift +15 -0
- package/ios/Browser/BrowseGate.swift +58 -0
- package/ios/Browser/BrowserConfig.swift +31 -89
- package/ios/Browser/BrowserManager.swift +68 -487
- package/ios/Browser/NavigationErrorState.swift +41 -0
- package/ios/Browser/PageResolver.swift +227 -0
- package/ios/Browser/RequestLayer.swift +90 -0
- package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +175 -107
- package/ios/CarPlay/CPInterfaceController+Safe.swift +11 -24
- package/ios/CarPlay/CarPlayArtworkResolver.swift +35 -83
- package/ios/CarPlay/CarPlayController.swift +42 -1442
- package/ios/CarPlay/CarPlayImageLoader.swift +42 -83
- package/ios/CarPlay/CarPlayListItemFactory.swift +9 -23
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +119 -129
- package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
- package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
- package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
- package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
- package/ios/CarPlay/CarPlaySession.swift +209 -0
- package/ios/CarPlay/CarPlayTemplates.swift +63 -0
- package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
- package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
- package/ios/CarPlay/RNABMediaAddHandler.swift +2 -18
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +5 -21
- package/ios/CarPlay/RNABMediaIntentHandler.swift +2 -3
- package/ios/Extension/NavigationError+Conversion.swift +7 -4
- package/ios/Extension/UIImage+Adaptive.swift +12 -0
- package/ios/Extension/URLError+Connectivity.swift +15 -0
- package/ios/Http/HttpClient.swift +63 -145
- package/ios/HybridAudioBrowser+Browser.swift +153 -0
- package/ios/HybridAudioBrowser+Options.swift +144 -0
- package/ios/HybridAudioBrowser+Player.swift +397 -0
- package/ios/HybridAudioBrowser+PlayerCallbacks.swift +190 -0
- package/ios/HybridAudioBrowser+Stubs.swift +46 -0
- package/ios/HybridAudioBrowser.swift +74 -1687
- package/ios/Model/NitroTypeStubs.swift +18 -1
- package/ios/Model/PlayerUpdateOptions.swift +1 -1
- package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
- package/ios/Observer/ItemObserver.swift +90 -0
- package/ios/Observer/PlayerObserver.swift +59 -0
- package/ios/Option/SessionCategories.swift +41 -92
- package/ios/Player/AudioSessionController.swift +177 -0
- package/ios/Player/DeferredSeek.swift +70 -0
- package/ios/Player/ErrorStatus.swift +27 -0
- package/ios/Player/MediaLoader.swift +24 -11
- package/ios/Player/NowPlayingUpdater.swift +157 -58
- package/ios/Player/PlaybackFailure.swift +125 -0
- package/ios/Player/PlaybackPersistence.swift +73 -0
- package/ios/Player/Player.swift +1083 -0
- package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
- package/ios/Player/PlayerEngine.swift +89 -0
- package/ios/Player/PlayerStatusTypes.swift +2 -6
- package/ios/Player/QueueManager.swift +52 -59
- package/ios/Player/RetryLoop.swift +261 -0
- package/ios/Player/SleepTimerManager.swift +1 -1
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +2 -3
- package/ios/RemoteCommand/RemoteCommandController.swift +85 -367
- package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -356
- package/ios/Tests/DeferredSeekTests.swift +70 -0
- package/ios/Tests/EmitterTests.swift +29 -0
- package/ios/Tests/GenerationMemoTests.swift +121 -0
- package/ios/Tests/LRUCacheTests.swift +4 -5
- package/ios/Tests/MediaLoaderTests.swift +12 -33
- package/ios/Tests/ObserverTests.swift +88 -0
- package/ios/Tests/PlaybackErrorTests.swift +76 -0
- package/ios/Tests/PlaybackFailureTests.swift +189 -0
- package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
- package/ios/Tests/PlayerPolicyTests.swift +405 -0
- package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +298 -248
- package/ios/Tests/PlayingStateTests.swift +42 -0
- package/ios/Tests/QueueManagerTests.swift +76 -107
- package/ios/Tests/ReadyValueTests.swift +73 -0
- package/ios/Tests/RemoteCommandControllerTests.swift +10 -30
- package/ios/Tests/RemoteCommandTests.swift +2 -4
- package/ios/Tests/RequestLayerTests.swift +112 -0
- package/ios/Tests/RetryLoopTests.swift +270 -0
- package/ios/Tests/Support/TestTypes.swift +111 -112
- package/ios/Tests/TrackIdTemplateTests.swift +1 -1
- package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
- package/ios/TrackSelector.swift +1 -1
- package/ios/Util/ArtworkImageFetcher.swift +42 -0
- package/ios/Util/Emitter.swift +42 -0
- package/ios/Util/GenerationMemo.swift +36 -0
- package/ios/Util/LRUCache.swift +23 -162
- package/ios/Util/NetworkMonitor.swift +1 -1
- package/ios/Util/ReadyValue.swift +56 -0
- package/ios/Util/SFSymbolRenderer.swift +35 -29
- package/ios/Util/SVGProcessor.swift +0 -10
- package/lib/commonjs/web/NativeAudioBrowser.js +1 -1
- package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +1 -1
- package/lib/module/web/NativeAudioBrowser.js +1 -1
- package/lib/module/web/TrackPlayer/PlayingStateFactory.js +1 -1
- package/lib/typescript/src/types/browser.d.ts +3 -3
- package/package.json +1 -1
- package/src/types/browser.ts +3 -3
- package/src/web/NativeAudioBrowser.ts +1 -1
- package/src/web/TrackPlayer/PlayingStateFactory.test.ts +1 -1
- package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
- package/ios/Bridge.h +0 -8
- package/ios/Browser/MediaResolveComposer.swift +0 -114
- package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
- package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
- package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
- package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
- package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
- package/ios/Observer/PlayerStateObserver.swift +0 -50
- package/ios/Observer/PlayerTimeObserver.swift +0 -55
- package/ios/Option/PitchAlgorithms.swift +0 -17
- package/ios/Option/TimeEventFrequency.swift +0 -18
- package/ios/Player/LoadSeekCoordinator.swift +0 -99
- package/ios/Player/MediaLoaderDelegate.swift +0 -11
- package/ios/Player/PlaybackCoordinator.swift +0 -761
- package/ios/Player/PlaybackEffectHandler.swift +0 -54
- package/ios/Player/PlaybackErrorHandler.swift +0 -175
- package/ios/Player/PlaybackStateStore.swift +0 -27
- package/ios/Player/PlayingStateManager.swift +0 -33
- package/ios/Player/RetryManager.swift +0 -384
- package/ios/Player/SeekCompletionHandler.swift +0 -5
- package/ios/Player/SleepTimerHandling.swift +0 -28
- package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
- package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -327
- package/ios/Tests/MediaResolveTests.swift +0 -126
- package/ios/Tests/OnceValueTests.swift +0 -79
- package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -549
- package/ios/Tests/PlaybackStateStoreTests.swift +0 -49
- package/ios/Tests/PlayerTimeObserverTests.swift +0 -37
- package/ios/Tests/PlayingStateManagerTests.swift +0 -57
- package/ios/Tests/RetryManagerTests.swift +0 -178
- package/ios/TrackPlayer.swift +0 -1042
- package/ios/TrackPlayerCallbacks.swift +0 -53
- package/ios/Util/MetadataAdapter.swift +0 -131
- package/ios/Util/OnceValue.swift +0 -104
- /package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +0 -0
- /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 `
|
|
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,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,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
|
-
}
|