@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
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/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,46 +1,42 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
Patterns for media playback with AVPlayer, streaming HLS content, audio
|
|
4
|
-
session configuration, background audio, Now Playing integration, remote
|
|
5
|
-
command handling, and Picture-in-Picture.
|
|
1
|
+
# Audio and video playback
|
|
6
2
|
|
|
7
3
|
## Contents
|
|
8
4
|
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [Background
|
|
16
|
-
- [Now Playing
|
|
17
|
-
- [
|
|
18
|
-
- [Picture
|
|
5
|
+
- [Players and where they render](#players-and-where-they-render)
|
|
6
|
+
- [Assets and items](#assets-and-items)
|
|
7
|
+
- [Transport controls](#transport-controls)
|
|
8
|
+
- [Watching state and time](#watching-state-and-time)
|
|
9
|
+
- [HLS streaming](#hls-streaming)
|
|
10
|
+
- [The audio session](#the-audio-session)
|
|
11
|
+
- [Background audio](#background-audio)
|
|
12
|
+
- [Now Playing](#now-playing)
|
|
13
|
+
- [Remote commands](#remote-commands)
|
|
14
|
+
- [Picture in Picture](#picture-in-picture)
|
|
15
|
+
- [Documentation](#documentation)
|
|
19
16
|
|
|
20
|
-
##
|
|
17
|
+
## Players and where they render
|
|
21
18
|
|
|
22
|
-
`AVPlayer`
|
|
23
|
-
|
|
24
|
-
transport controls, or `AVPlayerLayer` for a custom player interface.
|
|
19
|
+
`AVPlayer` drives playback of one asset at a time. Pick the surface by how
|
|
20
|
+
much UI you want to own:
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
- `AVPlayerViewController` (AVKit) brings the system transport controls.
|
|
23
|
+
- `AVPlayerLayer` draws frames only; you build the controls.
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
SwiftUI:
|
|
30
26
|
|
|
31
27
|
```swift
|
|
32
|
-
import SwiftUI
|
|
33
28
|
import AVKit
|
|
34
29
|
|
|
35
|
-
struct
|
|
36
|
-
let
|
|
30
|
+
struct TrailerView: View {
|
|
31
|
+
let trailer: URL
|
|
37
32
|
@State private var player: AVPlayer?
|
|
38
33
|
|
|
39
34
|
var body: some View {
|
|
40
35
|
VideoPlayer(player: player)
|
|
41
36
|
.onAppear {
|
|
42
|
-
|
|
43
|
-
player
|
|
37
|
+
let fresh = AVPlayer(url: trailer)
|
|
38
|
+
player = fresh
|
|
39
|
+
fresh.play()
|
|
44
40
|
}
|
|
45
41
|
.onDisappear {
|
|
46
42
|
player?.pause()
|
|
@@ -50,652 +46,410 @@ struct VideoPlayerView: View {
|
|
|
50
46
|
}
|
|
51
47
|
```
|
|
52
48
|
|
|
53
|
-
|
|
49
|
+
UIKit:
|
|
54
50
|
|
|
55
51
|
```swift
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
func presentVideo(url: URL) {
|
|
63
|
-
player = AVPlayer(url: url)
|
|
64
|
-
let controller = AVPlayerViewController()
|
|
65
|
-
controller.player = player
|
|
66
|
-
present(controller, animated: true) {
|
|
67
|
-
self.player?.play()
|
|
68
|
-
}
|
|
69
|
-
}
|
|
52
|
+
@MainActor
|
|
53
|
+
func presentTrailer(_ trailer: URL, from host: UIViewController) {
|
|
54
|
+
let screen = AVPlayerViewController()
|
|
55
|
+
screen.player = AVPlayer(url: trailer)
|
|
56
|
+
host.present(screen, animated: true) { screen.player?.play() }
|
|
70
57
|
}
|
|
71
58
|
```
|
|
72
59
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
For full control over the player UI, embed an `AVPlayerLayer`:
|
|
60
|
+
Custom surface:
|
|
76
61
|
|
|
77
62
|
```swift
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
final class PlayerView: UIView {
|
|
82
|
-
override class var layerClass: AnyClass { AVPlayerLayer.self }
|
|
83
|
-
|
|
84
|
-
var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
|
|
63
|
+
final class FilmView: UIView {
|
|
64
|
+
override static var layerClass: AnyClass { AVPlayerLayer.self }
|
|
65
|
+
private var film: AVPlayerLayer { unsafeDowncast(layer, to: AVPlayerLayer.self) }
|
|
85
66
|
|
|
86
67
|
var player: AVPlayer? {
|
|
87
|
-
get {
|
|
88
|
-
set {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
playerLayer.videoGravity = .resizeAspect
|
|
68
|
+
get { film.player }
|
|
69
|
+
set {
|
|
70
|
+
film.player = newValue
|
|
71
|
+
film.videoGravity = .resizeAspect
|
|
72
|
+
}
|
|
93
73
|
}
|
|
94
74
|
}
|
|
95
75
|
```
|
|
96
76
|
|
|
97
|
-
##
|
|
98
|
-
|
|
99
|
-
`AVAsset` represents the static media (duration, tracks, metadata).
|
|
100
|
-
`AVPlayerItem` adds the dynamic state (current time, buffering status) needed
|
|
101
|
-
for playback.
|
|
77
|
+
## Assets and items
|
|
102
78
|
|
|
103
|
-
|
|
104
|
-
|
|
79
|
+
An `AVAsset` is the media itself: duration, tracks, metadata. An `AVPlayerItem`
|
|
80
|
+
wraps it with the moving parts of playback such as current time and buffer
|
|
81
|
+
state.
|
|
105
82
|
|
|
106
83
|
```swift
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Local file
|
|
110
|
-
let localURL = Bundle.main.url(forResource: "intro", withExtension: "mp4")!
|
|
111
|
-
let localItem = AVPlayerItem(url: localURL)
|
|
112
|
-
|
|
113
|
-
// Remote file
|
|
114
|
-
let remoteURL = URL(string: "https://example.com/video.mp4")!
|
|
115
|
-
let remoteItem = AVPlayerItem(url: remoteURL)
|
|
84
|
+
let quick = AVPlayerItem(url: bundledOrRemoteURL)
|
|
116
85
|
|
|
117
|
-
|
|
118
|
-
let asset = AVURLAsset(url:
|
|
119
|
-
|
|
120
|
-
|
|
86
|
+
func preparedItem(for source: URL) async throws -> AVPlayerItem? {
|
|
87
|
+
let asset = AVURLAsset(url: source, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
|
|
88
|
+
let (length, tracks, playable) = try await asset.load(.duration, .tracks, .isPlayable)
|
|
89
|
+
guard playable, !tracks.isEmpty, length.isNumeric else { return nil }
|
|
90
|
+
return AVPlayerItem(asset: asset)
|
|
91
|
+
}
|
|
121
92
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
let tracks = try await asset.load(.tracks)
|
|
125
|
-
let isPlayable = try await asset.load(.isPlayable)
|
|
93
|
+
let deck = AVPlayer(playerItem: quick)
|
|
94
|
+
deck.replaceCurrentItem(with: AVPlayerItem(url: nextEpisodeURL))
|
|
126
95
|
|
|
127
|
-
let
|
|
128
|
-
let player = AVPlayer(playerItem: item)
|
|
96
|
+
let playlist = AVQueuePlayer(items: episodeURLs.map { AVPlayerItem(url: $0) })
|
|
129
97
|
```
|
|
130
98
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
let nextItem = AVPlayerItem(url: nextVideoURL)
|
|
137
|
-
player.replaceCurrentItem(with: nextItem)
|
|
138
|
-
player.play()
|
|
139
|
-
```
|
|
99
|
+
- `AVPlayerItem(url:)` accepts both bundle files and remote URLs.
|
|
100
|
+
- Load asset properties asynchronously with `load(_:)` (iOS 15+) before you
|
|
101
|
+
depend on them.
|
|
102
|
+
- Keep one player alive and swap media with `replaceCurrentItem(with:)`.
|
|
103
|
+
- `AVQueuePlayer` plays a list and moves to the next item by itself.
|
|
140
104
|
|
|
141
|
-
|
|
105
|
+
## Transport controls
|
|
142
106
|
|
|
143
107
|
```swift
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Automatically advances to the next item
|
|
148
|
-
```
|
|
108
|
+
deck.play()
|
|
109
|
+
deck.pause()
|
|
110
|
+
deck.rate = 1.5
|
|
149
111
|
|
|
150
|
-
|
|
112
|
+
await deck.seek(to: CMTime(seconds: 42, preferredTimescale: 600))
|
|
113
|
+
await deck.seek(to: CMTime(seconds: 42, preferredTimescale: 600), toleranceBefore: .zero, toleranceAfter: .zero)
|
|
151
114
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
player.
|
|
155
|
-
|
|
156
|
-
// Pause
|
|
157
|
-
player.pause()
|
|
158
|
-
|
|
159
|
-
// Set playback rate (1.0 = normal, 2.0 = 2x, 0.5 = half speed)
|
|
160
|
-
player.rate = 1.5
|
|
161
|
-
|
|
162
|
-
// Seek to a specific time
|
|
163
|
-
let targetTime = CMTime(seconds: 30, preferredTimescale: 600)
|
|
164
|
-
await player.seek(to: targetTime)
|
|
165
|
-
|
|
166
|
-
// Seek with tolerance (for precise seeking, e.g., scrubbing)
|
|
167
|
-
await player.seek(
|
|
168
|
-
to: targetTime,
|
|
169
|
-
toleranceBefore: .zero,
|
|
170
|
-
toleranceAfter: .zero
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
// Seek to a percentage of duration
|
|
174
|
-
func seekToPercentage(_ percentage: Double) async {
|
|
175
|
-
guard let duration = player.currentItem?.duration,
|
|
176
|
-
duration.isNumeric else { return }
|
|
177
|
-
let targetSeconds = duration.seconds * percentage
|
|
178
|
-
let target = CMTime(seconds: targetSeconds, preferredTimescale: 600)
|
|
179
|
-
await player.seek(to: target)
|
|
115
|
+
func jump(_ player: AVPlayer, toFraction fraction: Double) async {
|
|
116
|
+
guard let total = player.currentItem?.duration, total.isNumeric else { return }
|
|
117
|
+
await player.seek(to: CMTime(seconds: total.seconds * fraction, preferredTimescale: 600))
|
|
180
118
|
}
|
|
181
119
|
```
|
|
182
120
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
### Periodic Time Observer
|
|
121
|
+
`rate` 1.0 is normal speed, 2.0 double, 0.5 half. The zero-tolerance seek is
|
|
122
|
+
exact and is what a scrubber wants; the plain seek is cheaper.
|
|
186
123
|
|
|
187
|
-
|
|
124
|
+
## Watching state and time
|
|
188
125
|
|
|
189
126
|
```swift
|
|
190
|
-
import
|
|
191
|
-
|
|
192
|
-
@Observable
|
|
193
|
-
@MainActor
|
|
194
|
-
final class PlayerManager {
|
|
195
|
-
let player = AVPlayer()
|
|
196
|
-
var currentTime: Double = 0
|
|
197
|
-
var duration: Double = 0
|
|
198
|
-
var isPlaying = false
|
|
199
|
-
|
|
200
|
-
private var timeObserver: Any?
|
|
201
|
-
|
|
202
|
-
func startObserving() {
|
|
203
|
-
// Fire every 0.5 seconds on the main queue
|
|
204
|
-
let interval = CMTime(seconds: 0.5, preferredTimescale: 600)
|
|
205
|
-
timeObserver = player.addPeriodicTimeObserver(
|
|
206
|
-
forInterval: interval,
|
|
207
|
-
queue: .main
|
|
208
|
-
) { [weak self] time in
|
|
209
|
-
guard let self else { return }
|
|
210
|
-
self.currentTime = time.seconds
|
|
211
|
-
self.duration = self.player.currentItem?.duration.seconds ?? 0
|
|
212
|
-
self.isPlaying = self.player.timeControlStatus == .playing
|
|
213
|
-
}
|
|
214
|
-
}
|
|
127
|
+
import Combine
|
|
215
128
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
129
|
+
@Observable @MainActor
|
|
130
|
+
final class PlaybackMonitor {
|
|
131
|
+
private(set) var elapsed = 0.0
|
|
132
|
+
private(set) var total = 0.0
|
|
133
|
+
private(set) var isPlaying = false
|
|
134
|
+
private(set) var failure: Error?
|
|
135
|
+
private(set) var stallReason: AVPlayer.WaitingReason?
|
|
136
|
+
@ObservationIgnored private var ticker: Any?
|
|
137
|
+
@ObservationIgnored private var bag: Set<AnyCancellable> = []
|
|
138
|
+
@ObservationIgnored private let player: AVPlayer
|
|
139
|
+
|
|
140
|
+
init(player: AVPlayer) {
|
|
141
|
+
self.player = player
|
|
142
|
+
ticker = player.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 2),
|
|
143
|
+
queue: .main) { [weak self] now in
|
|
144
|
+
MainActor.assumeIsolated {
|
|
145
|
+
guard let self else { return }
|
|
146
|
+
self.elapsed = now.seconds
|
|
147
|
+
self.total = player.currentItem?.duration.seconds ?? 0
|
|
148
|
+
self.isPlaying = player.timeControlStatus == .playing
|
|
149
|
+
}
|
|
220
150
|
}
|
|
151
|
+
player.publisher(for: \.status)
|
|
152
|
+
.receive(on: DispatchQueue.main)
|
|
153
|
+
.sink { [weak self] status in
|
|
154
|
+
if status == .failed { self?.failure = player.error }
|
|
155
|
+
}
|
|
156
|
+
.store(in: &bag)
|
|
157
|
+
player.publisher(for: \.timeControlStatus)
|
|
158
|
+
.receive(on: DispatchQueue.main)
|
|
159
|
+
.sink { [weak self] mode in
|
|
160
|
+
self?.stallReason = mode == .waitingToPlayAtSpecifiedRate ? player.reasonForWaitingToPlay : nil
|
|
161
|
+
}
|
|
162
|
+
.store(in: &bag)
|
|
163
|
+
NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
|
|
164
|
+
.receive(on: DispatchQueue.main)
|
|
165
|
+
.sink { _ in player.seek(to: .zero) }
|
|
166
|
+
.store(in: &bag)
|
|
221
167
|
}
|
|
222
168
|
|
|
223
|
-
|
|
224
|
-
if let
|
|
225
|
-
|
|
226
|
-
}
|
|
169
|
+
func stop() {
|
|
170
|
+
if let ticker { player.removeTimeObserver(ticker) }
|
|
171
|
+
ticker = nil
|
|
227
172
|
}
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### Observing Player Status with KVO
|
|
232
|
-
|
|
233
|
-
Check player and item readiness before playing:
|
|
234
|
-
|
|
235
|
-
```swift
|
|
236
|
-
import AVFoundation
|
|
237
|
-
import Combine
|
|
238
173
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
player.publisher(for: \.status)
|
|
243
|
-
.sink { status in
|
|
244
|
-
switch status {
|
|
245
|
-
case .readyToPlay:
|
|
246
|
-
print("Ready to play")
|
|
247
|
-
case .failed:
|
|
248
|
-
print("Failed: \(player.error?.localizedDescription ?? "")")
|
|
249
|
-
case .unknown:
|
|
250
|
-
print("Status unknown")
|
|
251
|
-
@unknown default:
|
|
252
|
-
break
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
.store(in: &cancellables)
|
|
256
|
-
|
|
257
|
-
// Observe buffering state
|
|
258
|
-
player.publisher(for: \.timeControlStatus)
|
|
259
|
-
.sink { status in
|
|
260
|
-
switch status {
|
|
261
|
-
case .playing: print("Playing")
|
|
262
|
-
case .paused: print("Paused")
|
|
263
|
-
case .waitingToPlayAtSpecifiedRate:
|
|
264
|
-
print("Buffering: \(player.reasonForWaitingToPlay?.rawValue ?? "")")
|
|
265
|
-
@unknown default: break
|
|
266
|
-
}
|
|
174
|
+
isolated deinit {
|
|
175
|
+
if let ticker { player.removeTimeObserver(ticker) }
|
|
267
176
|
}
|
|
268
|
-
.store(in: &cancellables)
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### Detecting Playback End
|
|
272
|
-
|
|
273
|
-
```swift
|
|
274
|
-
NotificationCenter.default.addObserver(
|
|
275
|
-
forName: .AVPlayerItemDidPlayToEndTime,
|
|
276
|
-
object: player.currentItem,
|
|
277
|
-
queue: .main
|
|
278
|
-
) { _ in
|
|
279
|
-
// Playback finished -- loop, show replay button, or advance
|
|
280
|
-
player.seek(to: .zero) // Loop
|
|
281
177
|
}
|
|
282
178
|
```
|
|
283
179
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
180
|
+
- Inside `MainActor.assumeIsolated` the properties need an explicit `self.`:
|
|
181
|
+
the implicit-self shorthand after `guard let self` does not reach a nested
|
|
182
|
+
closure.
|
|
183
|
+
- The periodic observer (every 0.5 s here) feeds progress UI. Remove its token
|
|
184
|
+
with `removeTimeObserver(_:)` on stop and on teardown.
|
|
185
|
+
- `status` moves to `.readyToPlay` or `.failed` (read `player.error`); it starts
|
|
186
|
+
as `.unknown`.
|
|
187
|
+
- `timeControlStatus` is `.playing`, `.paused` or
|
|
188
|
+
`.waitingToPlayAtSpecifiedRate`, the buffering state, with the cause in
|
|
189
|
+
`reasonForWaitingToPlay`.
|
|
190
|
+
- `.AVPlayerItemDidPlayToEndTime` on the current item marks the end; seek to
|
|
191
|
+
`.zero` to loop.
|
|
192
|
+
- `AVPlayer` is main-actor bound in the current SDK, so hop to the main queue
|
|
193
|
+
before touching it from publishers.
|
|
194
|
+
|
|
195
|
+
## HLS streaming
|
|
196
|
+
|
|
197
|
+
Give `AVPlayer` an `.m3u8` URL and AVFoundation handles adaptive bitrate,
|
|
198
|
+
buffering and failover. It picks the variant from bandwidth, device capability
|
|
199
|
+
and display resolution. You can cap it:
|
|
302
200
|
|
|
303
201
|
```swift
|
|
304
|
-
let
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
// Limit peak bitrate (bits per second)
|
|
310
|
-
item.preferredPeakBitRate = 2_000_000 // 2 Mbps
|
|
311
|
-
|
|
312
|
-
// For forward buffering duration
|
|
313
|
-
item.preferredForwardBufferDuration = 5 // seconds; 0 = system default
|
|
202
|
+
let live = AVPlayerItem(url: streamURL)
|
|
203
|
+
live.preferredMaximumResolution = CGSize(width: 1280, height: 720)
|
|
204
|
+
live.preferredPeakBitRate = 2_000_000
|
|
205
|
+
live.preferredForwardBufferDuration = 5
|
|
314
206
|
```
|
|
315
207
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
Configure `AVAudioSession` to tell the system how your app intends to use
|
|
319
|
-
audio. This affects audio routing, mixing behavior, and background playback.
|
|
208
|
+
`preferredPeakBitRate` is in bits per second (2 Mbps above). A forward buffer of
|
|
209
|
+
0 leaves the choice to the system.
|
|
320
210
|
|
|
321
|
-
|
|
322
|
-
[AVAudioSession.Category](https://sosumi.ai/documentation/avfaudio/avaudiosession/category-swift.struct)
|
|
211
|
+
## The audio session
|
|
323
212
|
|
|
324
|
-
|
|
213
|
+
`AVAudioSession` states what the app does with sound, which decides routing,
|
|
214
|
+
mixing with other apps and background playback.
|
|
325
215
|
|
|
326
|
-
| Category |
|
|
216
|
+
| Category | Behaviour | Typical use |
|
|
327
217
|
|---|---|---|
|
|
328
|
-
| `.playback` |
|
|
329
|
-
| `.playAndRecord` |
|
|
330
|
-
| `.ambient` |
|
|
331
|
-
| `.soloAmbient` |
|
|
218
|
+
| `.playback` | keeps playing with the silent switch on, can continue in background | music, podcasts, video |
|
|
219
|
+
| `.playAndRecord` | input and output at once | calls, recording while monitoring |
|
|
220
|
+
| `.ambient` | mixes with other audio, muted by the silent switch | game effects, incidental sound |
|
|
221
|
+
| `.soloAmbient` | the default: silences other audio, muted by the silent switch | |
|
|
332
222
|
|
|
333
223
|
```swift
|
|
334
224
|
import AVFAudio
|
|
335
225
|
|
|
336
|
-
func
|
|
337
|
-
let
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// Media playback: audio continues with silent switch, supports background
|
|
341
|
-
try session.setCategory(
|
|
342
|
-
.playback,
|
|
343
|
-
mode: .default,
|
|
344
|
-
options: []
|
|
345
|
-
)
|
|
346
|
-
} else {
|
|
347
|
-
// Mix with other apps (e.g., game sounds over user's music)
|
|
348
|
-
try session.setCategory(
|
|
349
|
-
.ambient,
|
|
350
|
-
mode: .default,
|
|
351
|
-
options: [.mixWithOthers]
|
|
352
|
-
)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
try session.setActive(true)
|
|
226
|
+
func enableMixedEffects() throws {
|
|
227
|
+
let audio = AVAudioSession.sharedInstance()
|
|
228
|
+
try audio.setCategory(.ambient, options: .mixWithOthers)
|
|
229
|
+
try audio.setActive(true)
|
|
356
230
|
}
|
|
357
231
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
.playAndRecord,
|
|
361
|
-
|
|
362
|
-
options: [.defaultToSpeaker, .allowBluetooth]
|
|
363
|
-
)
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
### Handling Audio Interruptions
|
|
367
|
-
|
|
368
|
-
```swift
|
|
369
|
-
NotificationCenter.default.addObserver(
|
|
370
|
-
forName: AVAudioSession.interruptionNotification,
|
|
371
|
-
object: AVAudioSession.sharedInstance(),
|
|
372
|
-
queue: .main
|
|
373
|
-
) { notification in
|
|
374
|
-
guard let info = notification.userInfo,
|
|
375
|
-
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
376
|
-
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
|
|
377
|
-
return
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
switch type {
|
|
381
|
-
case .began:
|
|
382
|
-
// Pause playback -- system has interrupted audio
|
|
383
|
-
player.pause()
|
|
384
|
-
case .ended:
|
|
385
|
-
let options = info[AVAudioSessionInterruptionOptionKey] as? UInt ?? 0
|
|
386
|
-
if AVAudioSession.InterruptionOptions(rawValue: options).contains(.shouldResume) {
|
|
387
|
-
player.play()
|
|
388
|
-
}
|
|
389
|
-
@unknown default:
|
|
390
|
-
break
|
|
391
|
-
}
|
|
232
|
+
func enableVideoCall() throws {
|
|
233
|
+
let audio = AVAudioSession.sharedInstance()
|
|
234
|
+
try audio.setCategory(.playAndRecord, mode: .videoChat, options: [.allowBluetoothHFP, .defaultToSpeaker])
|
|
235
|
+
try audio.setActive(true)
|
|
392
236
|
}
|
|
393
237
|
```
|
|
394
238
|
|
|
395
|
-
|
|
239
|
+
`.allowBluetooth` is deprecated in the iOS 26 SDK; `.allowBluetoothHFP` is its
|
|
240
|
+
replacement.
|
|
241
|
+
|
|
242
|
+
Interruptions and route changes:
|
|
396
243
|
|
|
397
244
|
```swift
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
245
|
+
func observeAudioEvents(pause: @escaping @Sendable () -> Void, resume: @escaping @Sendable () -> Void) {
|
|
246
|
+
let center = NotificationCenter.default
|
|
247
|
+
center.addObserver(forName: AVAudioSession.interruptionNotification, object: nil, queue: .main) { note in
|
|
248
|
+
guard let raw = note.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
249
|
+
let kind = AVAudioSession.InterruptionType(rawValue: raw) else { return }
|
|
250
|
+
switch kind {
|
|
251
|
+
case .began:
|
|
252
|
+
pause()
|
|
253
|
+
case .ended:
|
|
254
|
+
let flags = note.userInfo?[AVAudioSessionInterruptionOptionKey] as? UInt ?? 0
|
|
255
|
+
if AVAudioSession.InterruptionOptions(rawValue: flags).contains(.shouldResume) { resume() }
|
|
256
|
+
@unknown default:
|
|
257
|
+
break
|
|
258
|
+
}
|
|
407
259
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
260
|
+
center.addObserver(forName: AVAudioSession.routeChangeNotification, object: nil, queue: .main) { note in
|
|
261
|
+
guard let raw = note.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
|
262
|
+
AVAudioSession.RouteChangeReason(rawValue: raw) == .oldDeviceUnavailable else { return }
|
|
263
|
+
pause()
|
|
412
264
|
}
|
|
413
265
|
}
|
|
414
266
|
```
|
|
415
267
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
1. Enable the `audio` background mode in your app's capabilities.
|
|
421
|
-
2. Configure `AVAudioSession` with the `.playback` category.
|
|
422
|
-
|
|
423
|
-
### Info.plist Configuration
|
|
424
|
-
|
|
425
|
-
```xml
|
|
426
|
-
<key>UIBackgroundModes</key>
|
|
427
|
-
<array>
|
|
428
|
-
<string>audio</string>
|
|
429
|
-
</array>
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
Or enable "Audio, AirPlay, and Picture in Picture" in Xcode's Signing &
|
|
433
|
-
Capabilities tab.
|
|
434
|
-
|
|
435
|
-
### Activating Background Audio
|
|
268
|
+
Pause when an interruption begins; resume at its end only if the options say
|
|
269
|
+
`.shouldResume`. When headphones are unplugged (`.oldDeviceUnavailable`),
|
|
270
|
+
pause: the Human Interface Guidelines expect it.
|
|
436
271
|
|
|
437
|
-
|
|
438
|
-
import AVFAudio
|
|
272
|
+
## Background audio
|
|
439
273
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
try session.setCategory(.playback, mode: .default)
|
|
443
|
-
try session.setActive(true)
|
|
444
|
-
}
|
|
445
|
-
```
|
|
274
|
+
Two things are needed together: the `audio` background mode and the
|
|
275
|
+
`.playback` category.
|
|
446
276
|
|
|
447
|
-
|
|
277
|
+
- Info.plist: `UIBackgroundModes` containing `audio`, or the Xcode capability
|
|
278
|
+
"Audio, AirPlay, and Picture in Picture".
|
|
448
279
|
- Call `setCategory` before `setActive`.
|
|
449
|
-
-
|
|
450
|
-
|
|
451
|
-
-
|
|
280
|
+
- `.ambient` and `.soloAmbient` stop in the background; only `.playback` keeps
|
|
281
|
+
going.
|
|
282
|
+
- When finished, give audio back to other apps:
|
|
452
283
|
|
|
453
284
|
```swift
|
|
454
|
-
|
|
455
|
-
try? AVAudioSession.sharedInstance().setActive(
|
|
456
|
-
false,
|
|
457
|
-
options: .notifyOthersOnDeactivation
|
|
458
|
-
)
|
|
459
|
-
}
|
|
285
|
+
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
460
286
|
```
|
|
461
287
|
|
|
462
|
-
## Now Playing
|
|
463
|
-
|
|
464
|
-
Update `MPNowPlayingInfoCenter` so the system displays track information on
|
|
465
|
-
the lock screen, Control Center, and connected accessories (CarPlay, AirPods).
|
|
288
|
+
## Now Playing
|
|
466
289
|
|
|
467
|
-
|
|
290
|
+
`MPNowPlayingInfoCenter` fills the Lock Screen, Control Center and accessories
|
|
291
|
+
such as CarPlay and AirPods.
|
|
468
292
|
|
|
469
293
|
```swift
|
|
470
294
|
import MediaPlayer
|
|
471
295
|
|
|
472
|
-
func
|
|
473
|
-
|
|
474
|
-
artist: String,
|
|
475
|
-
albumTitle: String? = nil,
|
|
476
|
-
duration: TimeInterval,
|
|
477
|
-
currentTime: TimeInterval,
|
|
478
|
-
artwork: UIImage? = nil
|
|
479
|
-
) {
|
|
480
|
-
var info: [String: Any] = [
|
|
296
|
+
func publishNowPlaying(title: String, artist: String, album: String, length: Double, cover: UIImage) {
|
|
297
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = [
|
|
481
298
|
MPMediaItemPropertyTitle: title,
|
|
482
299
|
MPMediaItemPropertyArtist: artist,
|
|
483
|
-
|
|
484
|
-
|
|
300
|
+
MPMediaItemPropertyAlbumTitle: album,
|
|
301
|
+
MPMediaItemPropertyPlaybackDuration: length,
|
|
302
|
+
MPNowPlayingInfoPropertyElapsedPlaybackTime: 0.0,
|
|
485
303
|
MPNowPlayingInfoPropertyPlaybackRate: 1.0,
|
|
486
|
-
MPNowPlayingInfoPropertyMediaType: MPNowPlayingInfoMediaType.audio.rawValue
|
|
304
|
+
MPNowPlayingInfoPropertyMediaType: MPNowPlayingInfoMediaType.audio.rawValue,
|
|
305
|
+
MPMediaItemPropertyArtwork: MPMediaItemArtwork(boundsSize: cover.size) { _ in cover },
|
|
487
306
|
]
|
|
488
|
-
|
|
489
|
-
if let albumTitle {
|
|
490
|
-
info[MPMediaItemPropertyAlbumTitle] = albumTitle
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if let artwork {
|
|
494
|
-
info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
|
|
495
|
-
boundsSize: artwork.size
|
|
496
|
-
) { _ in artwork }
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
|
|
500
307
|
}
|
|
501
308
|
|
|
502
|
-
|
|
503
|
-
func updateElapsedTime(_ seconds: TimeInterval, rate: Float = 1.0) {
|
|
309
|
+
func refreshProgress(elapsed: Double, rate: Double) {
|
|
504
310
|
var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
|
|
505
|
-
info[MPNowPlayingInfoPropertyElapsedPlaybackTime] =
|
|
311
|
+
info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsed
|
|
506
312
|
info[MPNowPlayingInfoPropertyPlaybackRate] = rate
|
|
507
313
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
|
|
508
314
|
}
|
|
509
315
|
```
|
|
510
316
|
|
|
511
|
-
|
|
317
|
+
While playing, update elapsed time and rate by copying the current dictionary,
|
|
318
|
+
changing the two values and assigning it back.
|
|
512
319
|
|
|
513
|
-
|
|
514
|
-
(play, pause, skip, seek). Without these, the system controls won't work.
|
|
320
|
+
## Remote commands
|
|
515
321
|
|
|
516
|
-
|
|
322
|
+
Lock Screen, Control Center and headphone buttons do nothing until the app
|
|
323
|
+
registers handlers.
|
|
517
324
|
|
|
518
325
|
```swift
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
let
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
// Pause
|
|
532
|
-
commandCenter.pauseCommand.isEnabled = true
|
|
533
|
-
commandCenter.pauseCommand.addTarget { _ in
|
|
534
|
-
player.pause()
|
|
535
|
-
return .success
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Toggle play/pause (headphone button, etc.)
|
|
539
|
-
commandCenter.togglePlayPauseCommand.isEnabled = true
|
|
540
|
-
commandCenter.togglePlayPauseCommand.addTarget { _ in
|
|
541
|
-
if player.timeControlStatus == .playing {
|
|
542
|
-
player.pause()
|
|
543
|
-
} else {
|
|
544
|
-
player.play()
|
|
545
|
-
}
|
|
326
|
+
@MainActor
|
|
327
|
+
func wireRemote(to player: AVPlayer) {
|
|
328
|
+
let remote = MPRemoteCommandCenter.shared()
|
|
329
|
+
let failed = MPRemoteCommandHandlerStatus.commandFailed
|
|
330
|
+
|
|
331
|
+
remote.playCommand.isEnabled = true
|
|
332
|
+
remote.playCommand.addTarget { _ in player.play(); return MPRemoteCommandHandlerStatus.success }
|
|
333
|
+
remote.pauseCommand.isEnabled = true
|
|
334
|
+
remote.pauseCommand.addTarget { _ in player.pause(); return MPRemoteCommandHandlerStatus.success }
|
|
335
|
+
remote.togglePlayPauseCommand.isEnabled = true
|
|
336
|
+
remote.togglePlayPauseCommand.addTarget { _ in
|
|
337
|
+
player.timeControlStatus == .playing ? player.pause() : player.play()
|
|
546
338
|
return .success
|
|
547
339
|
}
|
|
548
340
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
return .commandFailed
|
|
555
|
-
}
|
|
556
|
-
let currentTime = player.currentTime().seconds
|
|
557
|
-
let target = CMTime(
|
|
558
|
-
seconds: currentTime + skipEvent.interval,
|
|
559
|
-
preferredTimescale: 600
|
|
560
|
-
)
|
|
561
|
-
player.seek(to: target)
|
|
341
|
+
remote.skipForwardCommand.preferredIntervals = [15]
|
|
342
|
+
remote.skipForwardCommand.addTarget { event in
|
|
343
|
+
guard let skip = event as? MPSkipIntervalCommandEvent else { return failed }
|
|
344
|
+
let target = player.currentTime().seconds + skip.interval
|
|
345
|
+
player.seek(to: CMTime(seconds: target, preferredTimescale: 600))
|
|
562
346
|
return .success
|
|
563
347
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
guard let skipEvent = event as? MPSkipIntervalCommandEvent else {
|
|
570
|
-
return .commandFailed
|
|
571
|
-
}
|
|
572
|
-
let currentTime = player.currentTime().seconds
|
|
573
|
-
let target = CMTime(
|
|
574
|
-
seconds: max(0, currentTime - skipEvent.interval),
|
|
575
|
-
preferredTimescale: 600
|
|
576
|
-
)
|
|
577
|
-
player.seek(to: target)
|
|
348
|
+
remote.skipBackwardCommand.preferredIntervals = [15]
|
|
349
|
+
remote.skipBackwardCommand.addTarget { event in
|
|
350
|
+
guard let skip = event as? MPSkipIntervalCommandEvent else { return failed }
|
|
351
|
+
let target = max(0, player.currentTime().seconds - skip.interval)
|
|
352
|
+
player.seek(to: CMTime(seconds: target, preferredTimescale: 600))
|
|
578
353
|
return .success
|
|
579
354
|
}
|
|
580
355
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
guard let positionEvent = event as? MPChangePlaybackPositionCommandEvent else {
|
|
585
|
-
return .commandFailed
|
|
586
|
-
}
|
|
587
|
-
let target = CMTime(
|
|
588
|
-
seconds: positionEvent.positionTime,
|
|
589
|
-
preferredTimescale: 600
|
|
590
|
-
)
|
|
591
|
-
player.seek(to: target)
|
|
356
|
+
remote.changePlaybackPositionCommand.addTarget { event in
|
|
357
|
+
guard let scrub = event as? MPChangePlaybackPositionCommandEvent else { return failed }
|
|
358
|
+
player.seek(to: CMTime(seconds: scrub.positionTime, preferredTimescale: 600))
|
|
592
359
|
return .success
|
|
593
360
|
}
|
|
594
361
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
commandCenter.previousTrackCommand.isEnabled = false
|
|
362
|
+
remote.nextTrackCommand.isEnabled = false
|
|
363
|
+
remote.previousTrackCommand.isEnabled = false
|
|
598
364
|
}
|
|
599
|
-
```
|
|
600
365
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
commandCenter.togglePlayPauseCommand.removeTarget(nil)
|
|
609
|
-
commandCenter.skipForwardCommand.removeTarget(nil)
|
|
610
|
-
commandCenter.skipBackwardCommand.removeTarget(nil)
|
|
611
|
-
commandCenter.changePlaybackPositionCommand.removeTarget(nil)
|
|
366
|
+
func unwireRemote() {
|
|
367
|
+
let remote = MPRemoteCommandCenter.shared()
|
|
368
|
+
for command in [remote.playCommand, remote.pauseCommand, remote.togglePlayPauseCommand,
|
|
369
|
+
remote.skipForwardCommand, remote.skipBackwardCommand,
|
|
370
|
+
remote.changePlaybackPositionCommand] {
|
|
371
|
+
command.removeTarget(nil)
|
|
372
|
+
}
|
|
612
373
|
}
|
|
613
374
|
```
|
|
614
375
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
`
|
|
618
|
-
when the user navigates away. Requires the `audio` background mode.
|
|
376
|
+
`togglePlayPauseCommand` is the headphone button. Disabling a command you do
|
|
377
|
+
not support (next and previous track here) hides it from the system UI. On
|
|
378
|
+
teardown, `removeTarget(nil)` on every command you registered.
|
|
619
379
|
|
|
620
|
-
|
|
621
|
-
[Adopting Picture in Picture in a Custom Player](https://sosumi.ai/documentation/avkit/adopting-picture-in-picture-in-a-custom-player)
|
|
380
|
+
## Picture in Picture
|
|
622
381
|
|
|
623
|
-
|
|
382
|
+
`AVPictureInPictureController` keeps video floating while the person uses other
|
|
383
|
+
apps. It depends on the `audio` background mode.
|
|
624
384
|
|
|
625
|
-
`AVPlayerViewController`
|
|
626
|
-
|
|
385
|
+
`AVPlayerViewController` handles PiP by itself once that capability is on;
|
|
386
|
+
`allowsPictureInPicturePlayback` is `true` by default. A custom player wires it
|
|
387
|
+
up:
|
|
627
388
|
|
|
628
389
|
```swift
|
|
629
|
-
let controller = AVPlayerViewController()
|
|
630
|
-
controller.player = player
|
|
631
|
-
controller.allowsPictureInPicturePlayback = true // true by default
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
### With a Custom Player
|
|
635
|
-
|
|
636
|
-
```swift
|
|
637
|
-
import AVKit
|
|
638
|
-
|
|
639
|
-
@Observable
|
|
640
390
|
@MainActor
|
|
641
|
-
final class
|
|
642
|
-
private var
|
|
643
|
-
var
|
|
644
|
-
var
|
|
645
|
-
|
|
646
|
-
func
|
|
647
|
-
guard AVPictureInPictureController.isPictureInPictureSupported() else {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
pipController?.delegate = self
|
|
653
|
-
|
|
654
|
-
// Auto-start PiP when app goes to background
|
|
655
|
-
pipController?.canStartPictureInPictureAutomaticallyFromInline = true
|
|
391
|
+
final class FloatingVideo: NSObject, @preconcurrency AVPictureInPictureControllerDelegate {
|
|
392
|
+
private var pip: AVPictureInPictureController?
|
|
393
|
+
private(set) var isFloating = false
|
|
394
|
+
var onRestore: (() -> Void)?
|
|
395
|
+
|
|
396
|
+
func attach(to film: AVPlayerLayer) {
|
|
397
|
+
guard AVPictureInPictureController.isPictureInPictureSupported() else { return }
|
|
398
|
+
let controller = AVPictureInPictureController(playerLayer: film)
|
|
399
|
+
controller?.delegate = self
|
|
400
|
+
controller?.canStartPictureInPictureAutomaticallyFromInline = true
|
|
401
|
+
pip = controller
|
|
656
402
|
}
|
|
657
403
|
|
|
658
|
-
func
|
|
659
|
-
guard let
|
|
660
|
-
|
|
661
|
-
pipController.stopPictureInPicture()
|
|
662
|
-
} else {
|
|
663
|
-
pipController.startPictureInPicture()
|
|
664
|
-
}
|
|
404
|
+
func toggle() {
|
|
405
|
+
guard let pip else { return }
|
|
406
|
+
pip.isPictureInPictureActive ? pip.stopPictureInPicture() : pip.startPictureInPicture()
|
|
665
407
|
}
|
|
666
408
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
nonisolated func pictureInPictureControllerWillStartPictureInPicture(
|
|
670
|
-
_ controller: AVPictureInPictureController
|
|
671
|
-
) {
|
|
672
|
-
Task { @MainActor in isPiPActive = true }
|
|
409
|
+
nonisolated func pictureInPictureControllerWillStartPictureInPicture(_ floating: AVPictureInPictureController) {
|
|
410
|
+
Task { @MainActor in isFloating = true }
|
|
673
411
|
}
|
|
674
412
|
|
|
675
|
-
nonisolated func pictureInPictureControllerDidStopPictureInPicture(
|
|
676
|
-
|
|
677
|
-
) {
|
|
678
|
-
Task { @MainActor in isPiPActive = false }
|
|
413
|
+
nonisolated func pictureInPictureControllerDidStopPictureInPicture(_ floating: AVPictureInPictureController) {
|
|
414
|
+
Task { @MainActor in isFloating = false }
|
|
679
415
|
}
|
|
680
416
|
|
|
681
|
-
|
|
417
|
+
func pictureInPictureController(
|
|
682
418
|
_ controller: AVPictureInPictureController,
|
|
683
|
-
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler
|
|
684
|
-
completionHandler: @escaping (Bool) -> Void
|
|
419
|
+
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
|
|
685
420
|
) {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
// Navigate back to the player view
|
|
689
|
-
completionHandler(true)
|
|
690
|
-
}
|
|
421
|
+
onRestore?()
|
|
422
|
+
completionHandler(true)
|
|
691
423
|
}
|
|
692
424
|
}
|
|
693
425
|
```
|
|
694
426
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
-
|
|
700
|
-
|
|
701
|
-
|
|
427
|
+
`canStartPictureInPictureAutomaticallyFromInline` starts PiP on its own when
|
|
428
|
+
the app moves to the background. The restore callback must bring the player UI
|
|
429
|
+
back (or navigate to it) and then call the completion handler.
|
|
430
|
+
AVKit delivers these callbacks on the main thread. The restore method stays
|
|
431
|
+
main-actor isolated under the `@preconcurrency` conformance (Swift checks the
|
|
432
|
+
thread at run time) instead of hopping through a `Task`: the completion handler
|
|
433
|
+
is not `Sendable`, and Swift 6 rejects sending it into a task.
|
|
434
|
+
|
|
435
|
+
### PiP checklist
|
|
436
|
+
|
|
437
|
+
- [ ] `UIBackgroundModes` contains `audio`
|
|
438
|
+
- [ ] The audio session category is `.playback`
|
|
439
|
+
- [ ] `isPictureInPictureSupported()` is checked before setup
|
|
440
|
+
- [ ] The restore delegate method is implemented
|
|
441
|
+
- [ ] Its completion handler is always called; skipping it leaves the system
|
|
442
|
+
waiting
|
|
443
|
+
|
|
444
|
+
## Documentation
|
|
445
|
+
|
|
446
|
+
- [AVPlayer](https://developer.apple.com/documentation/avfoundation/avplayer)
|
|
447
|
+
- [AVPlayerViewController](https://developer.apple.com/documentation/avkit/avplayerviewcontroller)
|
|
448
|
+
- [AVPlayerItem](https://developer.apple.com/documentation/avfoundation/avplayeritem)
|
|
449
|
+
- [AVAsset](https://developer.apple.com/documentation/avfoundation/avasset)
|
|
450
|
+
- [AVAudioSession](https://developer.apple.com/documentation/avfaudio/avaudiosession)
|
|
451
|
+
- [AVAudioSession.Category](https://developer.apple.com/documentation/avfaudio/avaudiosession/category)
|
|
452
|
+
- [MPNowPlayingInfoCenter](https://developer.apple.com/documentation/mediaplayer/mpnowplayinginfocenter)
|
|
453
|
+
- [MPRemoteCommandCenter](https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter)
|
|
454
|
+
- [AVPictureInPictureController](https://developer.apple.com/documentation/avkit/avpictureinpicturecontroller)
|
|
455
|
+
- [Custom player PiP article](https://developer.apple.com/documentation/avkit/adopting-picture-in-picture-in-a-custom-player)
|