@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,668 +1,550 @@
|
|
|
1
|
-
# AVKit
|
|
1
|
+
# AVKit patterns
|
|
2
2
|
|
|
3
|
-
Advanced
|
|
3
|
+
Advanced playback recipes for iOS 26 and Swift 6.3.
|
|
4
4
|
|
|
5
5
|
## Contents
|
|
6
6
|
|
|
7
|
-
- [Custom
|
|
8
|
-
- [
|
|
9
|
-
- [Background
|
|
10
|
-
- [
|
|
11
|
-
- [Observing
|
|
12
|
-
- [Video
|
|
13
|
-
- [HDR
|
|
14
|
-
- [SwiftUI
|
|
15
|
-
- [AVPlayerViewController
|
|
7
|
+
- [Custom player with AVPlayerLayer](#custom-player-with-avplayerlayer)
|
|
8
|
+
- [Interstitials](#interstitials)
|
|
9
|
+
- [Background playback](#background-playback)
|
|
10
|
+
- [Errors](#errors)
|
|
11
|
+
- [Observing playback](#observing-playback)
|
|
12
|
+
- [Video frame analysis](#video-frame-analysis)
|
|
13
|
+
- [HDR](#hdr)
|
|
14
|
+
- [SwiftUI player model](#swiftui-player-model)
|
|
15
|
+
- [Wrapping AVPlayerViewController for SwiftUI](#wrapping-avplayerviewcontroller-for-swiftui)
|
|
16
16
|
|
|
17
|
-
## Custom
|
|
17
|
+
## Custom player with AVPlayerLayer
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
controls but gain full control over the interface.
|
|
19
|
+
Drawing video through `AVPlayerLayer` means giving up the system transport
|
|
20
|
+
controls in exchange for complete control over the UI.
|
|
22
21
|
|
|
23
22
|
```swift
|
|
24
23
|
import AVFoundation
|
|
25
24
|
import UIKit
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
@MainActor
|
|
27
|
+
final class FilmSurface: UIView {
|
|
28
|
+
override static var layerClass: AnyClass { AVPlayerLayer.self }
|
|
29
29
|
|
|
30
|
-
var
|
|
30
|
+
var filmLayer: AVPlayerLayer {
|
|
31
|
+
layer as! AVPlayerLayer
|
|
32
|
+
}
|
|
31
33
|
|
|
32
34
|
var player: AVPlayer? {
|
|
33
|
-
get {
|
|
34
|
-
set {
|
|
35
|
+
get { filmLayer.player }
|
|
36
|
+
set { filmLayer.player = newValue }
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
override init(frame: CGRect) {
|
|
38
|
-
super.init(frame:
|
|
39
|
-
|
|
39
|
+
override init(frame rect: CGRect) {
|
|
40
|
+
super.init(frame: rect)
|
|
41
|
+
filmLayer.videoGravity = .resizeAspect
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
required init?(coder: NSCoder) {
|
|
43
|
-
super.init(coder:
|
|
44
|
-
|
|
44
|
+
required init?(coder decoder: NSCoder) {
|
|
45
|
+
super.init(coder: decoder)
|
|
46
|
+
filmLayer.videoGravity = .resizeAspect
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
```
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
The force cast is safe because `layerClass` guarantees the layer type.
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
### Adding PiP to it
|
|
52
54
|
|
|
53
55
|
```swift
|
|
56
|
+
import AVFoundation
|
|
54
57
|
import AVKit
|
|
58
|
+
import UIKit
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
private
|
|
60
|
+
@MainActor
|
|
61
|
+
final class CustomCinemaController: UIViewController, @preconcurrency AVPictureInPictureControllerDelegate {
|
|
62
|
+
private let surface = FilmSurface()
|
|
59
63
|
private let player = AVPlayer()
|
|
64
|
+
private var pip: AVPictureInPictureController?
|
|
65
|
+
private let overlayControls = UIStackView()
|
|
60
66
|
|
|
61
|
-
override func
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
playerView.player = player
|
|
67
|
+
override func loadView() {
|
|
68
|
+
view = surface
|
|
69
|
+
}
|
|
65
70
|
|
|
71
|
+
func load(_ url: URL) {
|
|
72
|
+
surface.player = player
|
|
66
73
|
if AVPictureInPictureController.isPictureInPictureSupported() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
let controller = AVPictureInPictureController(playerLayer: surface.filmLayer)
|
|
75
|
+
controller?.delegate = self
|
|
76
|
+
controller?.canStartPictureInPictureAutomaticallyFromInline = true
|
|
77
|
+
pip = controller
|
|
70
78
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
func play(url: URL) {
|
|
74
|
-
player.replaceCurrentItem(with: AVPlayerItem(url: url))
|
|
79
|
+
let episode = AVPlayerItem(url: url)
|
|
80
|
+
player.replaceCurrentItem(with: episode)
|
|
75
81
|
player.play()
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
@
|
|
79
|
-
guard
|
|
80
|
-
|
|
84
|
+
@objc func pipButtonTapped() {
|
|
85
|
+
guard let pip, pip.isPictureInPicturePossible else { return }
|
|
86
|
+
pip.startPictureInPicture()
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
func pictureInPictureControllerWillStartPictureInPicture(
|
|
86
|
-
_ pictureInPictureController: AVPictureInPictureController
|
|
87
|
-
) {
|
|
88
|
-
// Prepare UI for PiP (e.g., hide custom controls)
|
|
89
|
+
func pictureInPictureControllerWillStartPictureInPicture(_ controller: AVPictureInPictureController) {
|
|
90
|
+
overlayControls.isHidden = true
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
func pictureInPictureControllerDidStopPictureInPicture(
|
|
92
|
-
|
|
93
|
-
) {
|
|
94
|
-
// Restore custom controls
|
|
93
|
+
func pictureInPictureControllerDidStopPictureInPicture(_ controller: AVPictureInPictureController) {
|
|
94
|
+
overlayControls.isHidden = false
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
func pictureInPictureController(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
) {
|
|
101
|
-
// Re-present the player view controller
|
|
102
|
-
completionHandler(true)
|
|
97
|
+
func pictureInPictureController(_ controller: AVPictureInPictureController,
|
|
98
|
+
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler finish: @escaping (Bool) -> Void) {
|
|
99
|
+
finish(true)
|
|
103
100
|
}
|
|
104
101
|
}
|
|
105
102
|
```
|
|
106
103
|
|
|
107
|
-
|
|
104
|
+
- Create the PiP controller only when the device supports PiP.
|
|
105
|
+
- Set its delegate and the automatic-inline flag, then load the item and play.
|
|
106
|
+
- The button action re-checks `isPictureInPicturePossible` before starting.
|
|
107
|
+
- Hide custom controls when PiP starts, show them again when it stops, and
|
|
108
|
+
always call `completionHandler(true)` in the restore callback.
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
### PiP from sample buffers
|
|
111
|
+
|
|
112
|
+
When the app renders frames itself into an `AVSampleBufferDisplayLayer`, build
|
|
113
|
+
the controller from a content source:
|
|
111
114
|
|
|
112
115
|
```swift
|
|
116
|
+
import AVFoundation
|
|
113
117
|
import AVKit
|
|
118
|
+
import CoreMedia
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
let contentSource = AVPictureInPictureController.ContentSource(
|
|
122
|
-
sampleBufferDisplayLayer: displayLayer,
|
|
123
|
-
playbackDelegate: delegate
|
|
124
|
-
)
|
|
125
|
-
let controller = AVPictureInPictureController(contentSource: contentSource)
|
|
126
|
-
return controller
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Only show a custom PiP start affordance after device support is known. When the
|
|
131
|
-
user taps that affordance, check the controller's `isPictureInPicturePossible`
|
|
132
|
-
because support can still be false for the current layer, item, media format,
|
|
133
|
-
or playback state. Start custom PiP only in response to user interaction.
|
|
134
|
-
|
|
135
|
-
The playback delegate must report timing and play/pause state:
|
|
120
|
+
@MainActor
|
|
121
|
+
final class RemoteDesktopPiP: NSObject, @preconcurrency AVPictureInPictureSampleBufferPlaybackDelegate {
|
|
122
|
+
let frameLayer = AVSampleBufferDisplayLayer()
|
|
123
|
+
private(set) var pip: AVPictureInPictureController?
|
|
124
|
+
private var paused = false
|
|
136
125
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
126
|
+
func prepare() {
|
|
127
|
+
if !AVPictureInPictureController.isPictureInPictureSupported() { return }
|
|
128
|
+
let source = AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: frameLayer, playbackDelegate: self)
|
|
129
|
+
pip = AVPictureInPictureController(contentSource: source)
|
|
130
|
+
}
|
|
142
131
|
|
|
143
|
-
func
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
) {
|
|
147
|
-
isPlaying = playing
|
|
132
|
+
func userTappedPiP() {
|
|
133
|
+
guard let pip, pip.isPictureInPicturePossible else { return }
|
|
134
|
+
pip.startPictureInPicture()
|
|
148
135
|
}
|
|
149
136
|
|
|
150
|
-
func
|
|
151
|
-
|
|
152
|
-
) -> CMTimeRange {
|
|
153
|
-
CMTimeRange(start: .zero, duration: duration)
|
|
137
|
+
func pictureInPictureController(_ controller: AVPictureInPictureController, setPlaying playing: Bool) {
|
|
138
|
+
paused = !playing
|
|
154
139
|
}
|
|
155
140
|
|
|
156
|
-
func
|
|
157
|
-
|
|
158
|
-
) -> Bool {
|
|
159
|
-
!isPlaying
|
|
141
|
+
func pictureInPictureControllerTimeRangeForPlayback(_ controller: AVPictureInPictureController) -> CMTimeRange {
|
|
142
|
+
CMTimeRange(start: .negativeInfinity, duration: .positiveInfinity)
|
|
160
143
|
}
|
|
161
144
|
|
|
162
|
-
func
|
|
163
|
-
|
|
164
|
-
didTransitionToRenderSize newRenderSize: CMVideoDimensions
|
|
165
|
-
) {
|
|
166
|
-
// Adjust rendering resolution for PiP window size
|
|
145
|
+
func pictureInPictureControllerIsPlaybackPaused(_ controller: AVPictureInPictureController) -> Bool {
|
|
146
|
+
paused
|
|
167
147
|
}
|
|
168
148
|
|
|
169
|
-
func pictureInPictureController(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
149
|
+
func pictureInPictureController(_ controller: AVPictureInPictureController,
|
|
150
|
+
didTransitionToRenderSize newRenderSize: CMVideoDimensions) {}
|
|
151
|
+
|
|
152
|
+
func pictureInPictureController(_ controller: AVPictureInPictureController,
|
|
153
|
+
skipByInterval skipInterval: CMTime,
|
|
154
|
+
completion completionHandler: @escaping () -> Void) {
|
|
155
|
+
completionHandler()
|
|
176
156
|
}
|
|
177
157
|
|
|
178
|
-
func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(
|
|
179
|
-
_ pictureInPictureController: AVPictureInPictureController
|
|
180
|
-
) -> Bool {
|
|
158
|
+
func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ controller: AVPictureInPictureController) -> Bool {
|
|
181
159
|
false
|
|
182
160
|
}
|
|
183
161
|
}
|
|
184
162
|
```
|
|
185
163
|
|
|
186
|
-
|
|
164
|
+
- Offer a PiP button only once support is confirmed.
|
|
165
|
+
- On tap, check `isPictureInPicturePossible` again; the layer, item, format or
|
|
166
|
+
playback state can still rule PiP out. Start only from the user's tap.
|
|
167
|
+
- The playback delegate must answer the system's questions about timing and
|
|
168
|
+
play state: `setPlaying`, the playable time range (an infinite range here, for
|
|
169
|
+
live content), whether playback is paused, render-size changes, skip requests
|
|
170
|
+
(call the completion), and whether background audio should be blocked.
|
|
187
171
|
|
|
188
|
-
|
|
189
|
-
special playback rules. `AVPlayerViewController` can mark these ranges in the
|
|
190
|
-
timeline and call delegate methods when interstitial playback begins and ends.
|
|
172
|
+
## Interstitials
|
|
191
173
|
|
|
192
|
-
|
|
174
|
+
Interstitials cover ads, legal notices and any stretch of the timeline with
|
|
175
|
+
special rules. `AVPlayerViewController` marks them on the scrubber and tells its
|
|
176
|
+
delegate when one starts and ends.
|
|
193
177
|
|
|
194
178
|
```swift
|
|
179
|
+
import AVFoundation
|
|
195
180
|
import AVKit
|
|
196
181
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
print("Interstitial starts at \(range.start.seconds)s")
|
|
201
|
-
}
|
|
182
|
+
@MainActor
|
|
183
|
+
func adBreaks(in item: AVPlayerItem) -> [CMTimeRange] {
|
|
184
|
+
item.interstitialTimeRanges.map(\.timeRange)
|
|
202
185
|
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
On iOS, valid scheduling sources are either stream-defined interstitials or
|
|
206
|
-
app-created schedules through `AVPlayerInterstitialEventController`. For
|
|
207
|
-
stream-defined breaks, the manifest or source media owns the schedule, and
|
|
208
|
-
AVFoundation exposes it through `playerItem.interstitialTimeRanges` for
|
|
209
|
-
inspection and delegate coordination. Do not assign to
|
|
210
|
-
`playerItem.interstitialTimeRanges` directly as the scheduling recipe.
|
|
211
|
-
|
|
212
|
-
### App-Scheduled Interstitials
|
|
213
|
-
|
|
214
|
-
For app-scheduled interstitial content, use `AVPlayerInterstitialEventController`
|
|
215
|
-
from AVFoundation. Creating a controller schedule causes playback to ignore
|
|
216
|
-
interstitial events present in the source media, so use it only when the app
|
|
217
|
-
owns the schedule.
|
|
218
|
-
|
|
219
|
-
```swift
|
|
220
|
-
import AVFoundation
|
|
221
|
-
|
|
222
|
-
func setupInterstitialEvents(primaryPlayer: AVPlayer) {
|
|
223
|
-
guard let primaryItem = primaryPlayer.currentItem else { return }
|
|
224
|
-
|
|
225
|
-
let controller = AVPlayerInterstitialEventController(primaryPlayer: primaryPlayer)
|
|
226
|
-
|
|
227
|
-
let adAsset = AVURLAsset(url: URL(string: "https://example.com/ad.m3u8")!)
|
|
228
|
-
let adItem = AVPlayerItem(asset: adAsset)
|
|
229
186
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
187
|
+
@MainActor
|
|
188
|
+
func scheduleMidroll(on player: AVPlayer, adURL: URL, at seconds: Double) -> AVPlayerInterstitialEventController? {
|
|
189
|
+
guard let main = player.currentItem else { return nil }
|
|
190
|
+
let controller = AVPlayerInterstitialEventController(primaryPlayer: player)
|
|
191
|
+
let midroll = AVPlayerInterstitialEvent(
|
|
192
|
+
primaryItem: main,
|
|
193
|
+
time: CMTime(seconds: seconds, preferredTimescale: 600)
|
|
233
194
|
)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
controller
|
|
195
|
+
midroll.templateItems = [AVPlayerItem(asset: AVURLAsset(url: adURL))]
|
|
196
|
+
midroll.restrictions = [.requiresPlaybackAtPreferredRateForAdvancement]
|
|
197
|
+
controller.events = [midroll]
|
|
198
|
+
return controller
|
|
238
199
|
}
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Delegate Callbacks for Interstitials
|
|
242
200
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
_
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// About to play interstitial content - disable skip controls
|
|
249
|
-
playerViewController.requiresLinearPlayback = true
|
|
250
|
-
}
|
|
201
|
+
@MainActor
|
|
202
|
+
final class AdGate: NSObject, @preconcurrency AVPlayerViewControllerDelegate {
|
|
203
|
+
func playerViewController(_ viewer: AVPlayerViewController, willPresent adBreak: AVInterstitialTimeRange) {
|
|
204
|
+
viewer.requiresLinearPlayback = true
|
|
205
|
+
}
|
|
251
206
|
|
|
252
|
-
func playerViewController(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
) {
|
|
256
|
-
// Interstitial finished - re-enable seeking
|
|
257
|
-
playerViewController.requiresLinearPlayback = false
|
|
207
|
+
func playerViewController(_ viewer: AVPlayerViewController, didPresent adBreak: AVInterstitialTimeRange) {
|
|
208
|
+
viewer.requiresLinearPlayback = false
|
|
209
|
+
}
|
|
258
210
|
}
|
|
259
211
|
```
|
|
260
212
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
213
|
+
- Read stream-defined breaks by iterating `interstitialTimeRanges` and taking
|
|
214
|
+
each `timeRange`.
|
|
215
|
+
- On iOS there are two valid sources: interstitials defined in the stream, or
|
|
216
|
+
an `AVPlayerInterstitialEventController` schedule. Assigning
|
|
217
|
+
`interstitialTimeRanges` is not a scheduling mechanism.
|
|
218
|
+
- Once `AVPlayerInterstitialEventController(primaryPlayer:)` sets a schedule,
|
|
219
|
+
playback ignores interstitial events embedded in the source media. Use it only
|
|
220
|
+
when the app owns the ad schedule.
|
|
221
|
+
- Lock seeking in `willPresent` and unlock it in `didPresent`.
|
|
264
222
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
```xml
|
|
268
|
-
<key>UIBackgroundModes</key>
|
|
269
|
-
<array>
|
|
270
|
-
<string>audio</string>
|
|
271
|
-
</array>
|
|
272
|
-
```
|
|
223
|
+
## Background playback
|
|
273
224
|
|
|
274
|
-
|
|
225
|
+
- Info.plist: `UIBackgroundModes` array containing `audio`.
|
|
226
|
+
- Set the `.playback` category, activate when playback starts, and hold the
|
|
227
|
+
`AVPlayer` strongly for as long as audio should continue.
|
|
228
|
+
- iOS normally pauses video when its scene moves to the background unless the
|
|
229
|
+
video goes into PiP. Use PiP for video that keeps playing, and the background
|
|
230
|
+
audio mode for sound that should keep going once the app is no longer in front.
|
|
275
231
|
|
|
276
|
-
|
|
277
|
-
and keep a strong reference to the `AVPlayer` for as long as playback should
|
|
278
|
-
continue. iOS generally pauses video when a scene backgrounds unless playback
|
|
279
|
-
moves to PiP. Use PiP for background video continuity; use background audio mode
|
|
280
|
-
for audio that should continue after the user leaves the app.
|
|
232
|
+
## Errors
|
|
281
233
|
|
|
282
|
-
|
|
234
|
+
```swift
|
|
235
|
+
import AVFoundation
|
|
283
236
|
|
|
284
|
-
|
|
237
|
+
enum PlaybackProblem {
|
|
238
|
+
case offline, timedOut, media(Error), unknown
|
|
239
|
+
}
|
|
285
240
|
|
|
286
|
-
|
|
287
|
-
func
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
241
|
+
@MainActor
|
|
242
|
+
func watchStatus(of item: AVPlayerItem, report: @escaping @MainActor (PlaybackProblem?) -> Void) -> NSKeyValueObservation {
|
|
243
|
+
item.observe(\.status, options: [.new]) { observed, _ in
|
|
244
|
+
MainActor.assumeIsolated {
|
|
245
|
+
switch observed.status {
|
|
246
|
+
case .readyToPlay:
|
|
247
|
+
report(nil)
|
|
248
|
+
case .failed:
|
|
249
|
+
report(classify(observed.error))
|
|
250
|
+
case .unknown:
|
|
251
|
+
return
|
|
252
|
+
@unknown default:
|
|
253
|
+
report(.unknown)
|
|
296
254
|
}
|
|
297
|
-
case .unknown:
|
|
298
|
-
break
|
|
299
|
-
@unknown default:
|
|
300
|
-
break
|
|
301
255
|
}
|
|
302
256
|
}
|
|
303
257
|
}
|
|
304
258
|
|
|
305
|
-
func
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
// Offer retry
|
|
313
|
-
}
|
|
314
|
-
case AVFoundationErrorDomain:
|
|
315
|
-
// AVFoundation-specific errors
|
|
316
|
-
break
|
|
317
|
-
default:
|
|
318
|
-
break
|
|
259
|
+
func classify(_ error: Error?) -> PlaybackProblem {
|
|
260
|
+
guard let error = error as NSError? else { return .unknown }
|
|
261
|
+
switch (error.domain, error.code) {
|
|
262
|
+
case (NSURLErrorDomain, NSURLErrorNotConnectedToInternet): return .offline
|
|
263
|
+
case (NSURLErrorDomain, NSURLErrorTimedOut): return .timedOut
|
|
264
|
+
case (AVFoundationErrorDomain, _): return .media(error)
|
|
265
|
+
default: return .media(error)
|
|
319
266
|
}
|
|
320
267
|
}
|
|
321
|
-
```
|
|
322
268
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if let reason = player.reasonForWaitingToPlay {
|
|
335
|
-
switch reason {
|
|
336
|
-
case .toMinimizeStalls:
|
|
337
|
-
// Buffering - show spinner
|
|
338
|
-
break
|
|
339
|
-
case .evaluatingBufferingRate:
|
|
340
|
-
break
|
|
341
|
-
case .noItemToPlay:
|
|
342
|
-
break
|
|
343
|
-
default:
|
|
344
|
-
break
|
|
345
|
-
}
|
|
269
|
+
@MainActor
|
|
270
|
+
func watchStalls(of player: AVPlayer, spinner: @escaping @MainActor (Bool) -> Void) -> NSKeyValueObservation {
|
|
271
|
+
player.observe(\.timeControlStatus, options: [.new]) { observed, _ in
|
|
272
|
+
MainActor.assumeIsolated {
|
|
273
|
+
switch observed.timeControlStatus {
|
|
274
|
+
case .waitingToPlayAtSpecifiedRate:
|
|
275
|
+
spinner(observed.reasonForWaitingToPlay == .toMinimizeStalls)
|
|
276
|
+
case .playing, .paused:
|
|
277
|
+
spinner(false)
|
|
278
|
+
@unknown default:
|
|
279
|
+
spinner(false)
|
|
346
280
|
}
|
|
347
|
-
@unknown default:
|
|
348
|
-
break
|
|
349
281
|
}
|
|
350
282
|
}
|
|
351
283
|
}
|
|
352
284
|
```
|
|
353
285
|
|
|
354
|
-
|
|
286
|
+
- `AVPlayerItem.status` moves from `.unknown` to `.readyToPlay` or `.failed`;
|
|
287
|
+
on failure read `item.error`.
|
|
288
|
+
- Split network errors (`NSURLErrorDomain`: show an offline message for
|
|
289
|
+
`NSURLErrorNotConnectedToInternet`, offer a retry for `NSURLErrorTimedOut`)
|
|
290
|
+
from media errors in `AVFoundationErrorDomain`.
|
|
291
|
+
- `AVPlayer.timeControlStatus` is `.paused`, `.playing` or
|
|
292
|
+
`.waitingToPlayAtSpecifiedRate`. While waiting, `reasonForWaitingToPlay` says
|
|
293
|
+
why: `.toMinimizeStalls` (show a buffering indicator),
|
|
294
|
+
`.evaluatingBufferingRate` or `.noItemToPlay`.
|
|
355
295
|
|
|
356
|
-
|
|
357
|
-
import AVKit
|
|
296
|
+
`AVKitError` codes:
|
|
358
297
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
break
|
|
367
|
-
case .contentRatingUnknown:
|
|
368
|
-
// Content rating could not be determined
|
|
369
|
-
break
|
|
370
|
-
case .contentDisallowedByPasscode:
|
|
371
|
-
// Parental controls restrict this content
|
|
372
|
-
break
|
|
373
|
-
case .contentDisallowedByProfile:
|
|
374
|
-
// MDM profile restricts this content
|
|
375
|
-
break
|
|
376
|
-
case .unknown:
|
|
377
|
-
break
|
|
378
|
-
@unknown default:
|
|
379
|
-
break
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
```
|
|
298
|
+
| Code | Meaning |
|
|
299
|
+
|---|---|
|
|
300
|
+
| `.pictureInPictureStartFailed` | PiP could not start; verify the playback category, the `audio` background mode, device support, `isPictureInPicturePossible`, and that the layer or content source feeding PiP is still active |
|
|
301
|
+
| `.contentRatingUnknown` | the content's rating could not be determined |
|
|
302
|
+
| `.contentDisallowedByPasscode` | blocked by parental restrictions |
|
|
303
|
+
| `.contentDisallowedByProfile` | blocked by a device management profile |
|
|
304
|
+
| `.unknown` | unspecified failure |
|
|
383
305
|
|
|
384
|
-
## Observing
|
|
385
|
-
|
|
386
|
-
### Periodic Time Observer
|
|
306
|
+
## Observing playback
|
|
387
307
|
|
|
388
308
|
```swift
|
|
389
|
-
|
|
390
|
-
let interval = CMTime(seconds: 0.5, preferredTimescale: 600)
|
|
391
|
-
return player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
|
|
392
|
-
let seconds = CMTimeGetSeconds(time)
|
|
393
|
-
// Update progress UI
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Remove when done
|
|
398
|
-
// player.removeTimeObserver(observer)
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
### Boundary Time Observer
|
|
402
|
-
|
|
403
|
-
Trigger actions at specific points in the timeline:
|
|
309
|
+
import AVFoundation
|
|
404
310
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
let
|
|
408
|
-
|
|
409
|
-
|
|
311
|
+
@MainActor
|
|
312
|
+
final class ChapterClock {
|
|
313
|
+
private let player: AVPlayer
|
|
314
|
+
private var ticker: Any?
|
|
315
|
+
private var markers: Any?
|
|
316
|
+
private var endObserver: NSObjectProtocol?
|
|
317
|
+
|
|
318
|
+
init(player: AVPlayer) {
|
|
319
|
+
self.player = player
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
func start(chapters: [CMTime], onTick: @escaping @Sendable (CMTime) -> Void, onChapter: @escaping @Sendable () -> Void) {
|
|
323
|
+
let halfSecond = CMTime(value: 1, timescale: 2)
|
|
324
|
+
ticker = player.addPeriodicTimeObserver(forInterval: halfSecond,
|
|
325
|
+
queue: .main, using: onTick)
|
|
326
|
+
markers = player.addBoundaryTimeObserver(forTimes: chapters.map { NSValue(time: $0) },
|
|
327
|
+
queue: .main, using: onChapter)
|
|
328
|
+
let center = NotificationCenter.default
|
|
329
|
+
endObserver = center.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem,
|
|
330
|
+
queue: .main) { _ in onChapter() }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
func stop() {
|
|
334
|
+
if let ticker { player.removeTimeObserver(ticker) }
|
|
335
|
+
if let markers { player.removeTimeObserver(markers) }
|
|
336
|
+
if let endObserver { NotificationCenter.default.removeObserver(endObserver) }
|
|
337
|
+
ticker = nil
|
|
338
|
+
markers = nil
|
|
339
|
+
endObserver = nil
|
|
410
340
|
}
|
|
411
341
|
}
|
|
412
342
|
```
|
|
413
343
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
object: item,
|
|
421
|
-
queue: .main
|
|
422
|
-
) { _ in
|
|
423
|
-
// Playback finished - show replay button or load next item
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
```
|
|
344
|
+
- `addPeriodicTimeObserver(forInterval:queue:using:)` returns a token; pass it
|
|
345
|
+
to `removeTimeObserver(_:)` when done.
|
|
346
|
+
- `addBoundaryTimeObserver(forTimes:queue:using:)` fires at the listed times,
|
|
347
|
+
wrapped with `NSValue(time:)`.
|
|
348
|
+
- The end of an item is announced with `.AVPlayerItemDidPlayToEndTime`, scoped
|
|
349
|
+
to that item.
|
|
427
350
|
|
|
428
|
-
## Video
|
|
351
|
+
## Video frame analysis
|
|
429
352
|
|
|
430
|
-
`
|
|
431
|
-
|
|
353
|
+
`allowsVideoFrameAnalysis` (default `true`, iOS 16+) lets the user interact
|
|
354
|
+
with a paused frame: select text, look things up, lift a subject. Narrow it with
|
|
355
|
+
`videoFrameAnalysisTypes` (iOS 17+), whose options include `.text`,
|
|
356
|
+
`.visualSearch`, `.subject` and `.machineReadableCode`, plus `.default`.
|
|
432
357
|
|
|
433
358
|
```swift
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
// Enable frame analysis (default is true)
|
|
437
|
-
playerVC.allowsVideoFrameAnalysis = true
|
|
359
|
+
import AVKit
|
|
438
360
|
|
|
439
|
-
|
|
440
|
-
|
|
361
|
+
@MainActor
|
|
362
|
+
func onlyTextAndCodes(_ viewer: AVPlayerViewController) {
|
|
363
|
+
viewer.allowsVideoFrameAnalysis = true
|
|
364
|
+
viewer.videoFrameAnalysisTypes = [.text, .machineReadableCode]
|
|
365
|
+
}
|
|
441
366
|
```
|
|
442
367
|
|
|
443
|
-
## HDR
|
|
368
|
+
## HDR
|
|
444
369
|
|
|
445
|
-
|
|
370
|
+
`AVPlayerViewController.preferredDisplayDynamicRange` (iOS 26+) picks how HDR
|
|
371
|
+
content is shown:
|
|
446
372
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
373
|
+
| Value | Behavior |
|
|
374
|
+
|---|---|
|
|
375
|
+
| `.automatic` | the system decides (default) |
|
|
376
|
+
| `.standard` | force SDR |
|
|
377
|
+
| `.high` | full HDR |
|
|
378
|
+
| `.constrainedHigh` | limited HDR, useful when HDR sits next to SDR content |
|
|
450
379
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
// Full HDR
|
|
455
|
-
playerVC.preferredDisplayDynamicRange = .high
|
|
380
|
+
```swift
|
|
381
|
+
import AVKit
|
|
456
382
|
|
|
457
|
-
|
|
458
|
-
|
|
383
|
+
@MainActor
|
|
384
|
+
func calmHDR(_ viewer: AVPlayerViewController) {
|
|
385
|
+
viewer.preferredDisplayDynamicRange = .constrainedHigh
|
|
386
|
+
}
|
|
459
387
|
```
|
|
460
388
|
|
|
461
|
-
## SwiftUI
|
|
462
|
-
|
|
463
|
-
An `@Observable` manager for coordinating playback state across a SwiftUI app:
|
|
389
|
+
## SwiftUI player model
|
|
464
390
|
|
|
465
391
|
```swift
|
|
466
|
-
import AVKit
|
|
467
392
|
import AVFoundation
|
|
393
|
+
import AVKit
|
|
394
|
+
import Observation
|
|
395
|
+
import SwiftUI
|
|
468
396
|
|
|
469
397
|
@Observable
|
|
470
398
|
@MainActor
|
|
471
|
-
final class
|
|
399
|
+
final class EpisodePlayer {
|
|
472
400
|
private(set) var player: AVPlayer?
|
|
401
|
+
private(set) var position: Double = 0
|
|
473
402
|
private(set) var isPlaying = false
|
|
474
|
-
private(set) var currentTime: TimeInterval = 0
|
|
475
|
-
private(set) var duration: TimeInterval = 0
|
|
476
403
|
private(set) var isBuffering = false
|
|
477
|
-
private(set) var
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
private var
|
|
481
|
-
private var
|
|
482
|
-
|
|
483
|
-
func
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
let
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
404
|
+
private(set) var length: Double = 0
|
|
405
|
+
private(set) var failure: String?
|
|
406
|
+
|
|
407
|
+
@ObservationIgnored private var tick: Any?
|
|
408
|
+
@ObservationIgnored private var observations: [NSKeyValueObservation] = []
|
|
409
|
+
|
|
410
|
+
func load(_ url: URL) {
|
|
411
|
+
tearDown()
|
|
412
|
+
let item = AVPlayerItem(asset: AVURLAsset(url: url))
|
|
413
|
+
let fresh = AVPlayer(playerItem: item)
|
|
414
|
+
player = fresh
|
|
415
|
+
|
|
416
|
+
observations.append(item.observe(\.status) { [weak self] observed, _ in
|
|
417
|
+
let status = observed.status
|
|
418
|
+
let seconds = CMTimeGetSeconds(observed.duration)
|
|
419
|
+
let message = observed.error?.localizedDescription
|
|
420
|
+
Task { @MainActor [weak self] in
|
|
421
|
+
guard let model = self else { return }
|
|
422
|
+
if status == .readyToPlay { model.length = seconds.isFinite ? seconds : 0 }
|
|
423
|
+
if status == .failed { model.failure = message }
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
observations.append(fresh.observe(\.timeControlStatus) { [weak self] observed, _ in
|
|
427
|
+
let state = observed.timeControlStatus
|
|
428
|
+
Task { @MainActor in
|
|
429
|
+
self?.isPlaying = state == .playing
|
|
430
|
+
self?.isBuffering = state == .waitingToPlayAtSpecifiedRate
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
let every = CMTime(value: 1, timescale: 2)
|
|
434
|
+
tick = fresh.addPeriodicTimeObserver(forInterval: every, queue: .main) { [weak self] now in
|
|
435
|
+
let seconds = now.seconds
|
|
436
|
+
Task { @MainActor in self?.position = seconds }
|
|
437
|
+
}
|
|
497
438
|
}
|
|
498
439
|
|
|
499
|
-
func
|
|
500
|
-
player
|
|
440
|
+
func toggle() {
|
|
441
|
+
guard let player else { return }
|
|
442
|
+
isPlaying ? player.pause() : player.play()
|
|
501
443
|
}
|
|
502
444
|
|
|
503
|
-
func
|
|
504
|
-
|
|
505
|
-
player?.seek(to: cmTime, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
445
|
+
func jump(to seconds: Double) {
|
|
446
|
+
player?.seek(to: CMTime(seconds: seconds, preferredTimescale: 600), toleranceBefore: .zero, toleranceAfter: .zero)
|
|
506
447
|
}
|
|
507
448
|
|
|
508
|
-
func
|
|
509
|
-
if let
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
statusObservation?.invalidate()
|
|
514
|
-
timeControlObservation?.invalidate()
|
|
449
|
+
func tearDown() {
|
|
450
|
+
if let tick, let player { player.removeTimeObserver(tick) }
|
|
451
|
+
tick = nil
|
|
452
|
+
observations.forEach { $0.invalidate() }
|
|
453
|
+
observations.removeAll()
|
|
515
454
|
player = nil
|
|
516
|
-
error = nil
|
|
517
455
|
isPlaying = false
|
|
518
|
-
|
|
519
|
-
|
|
456
|
+
isBuffering = false
|
|
457
|
+
position = 0
|
|
458
|
+
length = 0
|
|
459
|
+
failure = nil
|
|
520
460
|
}
|
|
521
461
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
Task { @MainActor in
|
|
525
|
-
guard let self else { return }
|
|
526
|
-
switch item.status {
|
|
527
|
-
case .readyToPlay:
|
|
528
|
-
self.duration = CMTimeGetSeconds(item.duration)
|
|
529
|
-
case .failed:
|
|
530
|
-
self.error = item.error
|
|
531
|
-
case .unknown:
|
|
532
|
-
break
|
|
533
|
-
@unknown default:
|
|
534
|
-
break
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
private func observeTimeControl(of player: AVPlayer) {
|
|
541
|
-
timeControlObservation = player.observe(\.timeControlStatus) { [weak self] player, _ in
|
|
542
|
-
Task { @MainActor in
|
|
543
|
-
guard let self else { return }
|
|
544
|
-
self.isPlaying = player.timeControlStatus == .playing
|
|
545
|
-
self.isBuffering = player.timeControlStatus == .waitingToPlayAtSpecifiedRate
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
private func addPeriodicTimeObserver(to player: AVPlayer) {
|
|
551
|
-
let interval = CMTime(seconds: 0.5, preferredTimescale: 600)
|
|
552
|
-
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
|
|
553
|
-
Task { @MainActor in
|
|
554
|
-
self?.currentTime = CMTimeGetSeconds(time)
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
deinit {
|
|
560
|
-
if let observer = timeObserver, let player {
|
|
561
|
-
player.removeTimeObserver(observer)
|
|
562
|
-
}
|
|
462
|
+
isolated deinit {
|
|
463
|
+
if let tick, let player { player.removeTimeObserver(tick) }
|
|
563
464
|
}
|
|
564
465
|
}
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
### Using the Manager in SwiftUI
|
|
568
466
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
struct VideoScreen: View {
|
|
574
|
-
@State private var manager = VideoPlayerManager()
|
|
467
|
+
struct EpisodeScreen: View {
|
|
468
|
+
let episodeURL: URL
|
|
469
|
+
@State private var model = EpisodePlayer()
|
|
575
470
|
|
|
576
471
|
var body: some View {
|
|
577
|
-
VStack {
|
|
578
|
-
if let
|
|
579
|
-
VideoPlayer(player:
|
|
580
|
-
.
|
|
581
|
-
|
|
582
|
-
HStack {
|
|
583
|
-
Button(manager.isPlaying ? "Pause" : "Play") {
|
|
584
|
-
manager.isPlaying ? manager.pause() : manager.play()
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
Text("\(Int(manager.currentTime))s / \(Int(manager.duration))s")
|
|
588
|
-
}
|
|
472
|
+
VStack(spacing: 12) {
|
|
473
|
+
if let current = model.player {
|
|
474
|
+
VideoPlayer(player: current)
|
|
475
|
+
.aspectRatio(16 / 9, contentMode: .fit)
|
|
589
476
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
477
|
+
HStack {
|
|
478
|
+
Button(model.isPlaying ? "Pause" : "Play") { model.toggle() }
|
|
479
|
+
Text("\(Int(model.position))s / \(Int(model.length))s").monospacedDigit()
|
|
480
|
+
if model.isBuffering { ProgressView() }
|
|
593
481
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
Text("Error: \(error.localizedDescription)")
|
|
597
|
-
.foregroundStyle(.red)
|
|
482
|
+
if let failure = model.failure {
|
|
483
|
+
Text(failure).foregroundStyle(.red)
|
|
598
484
|
}
|
|
599
485
|
}
|
|
600
|
-
.task {
|
|
601
|
-
manager.loadMedia(url: URL(string: "https://example.com/video.m3u8")!)
|
|
602
|
-
}
|
|
486
|
+
.task { model.load(episodeURL) }
|
|
603
487
|
}
|
|
604
488
|
}
|
|
605
489
|
```
|
|
606
490
|
|
|
607
|
-
|
|
491
|
+
- `load(_:)` clears previous state, creates the item and player, and installs
|
|
492
|
+
the status observation, the `timeControlStatus` observation and a periodic
|
|
493
|
+
time observer.
|
|
494
|
+
- Seeking uses zero tolerance on both sides for frame-accurate jumps, with a
|
|
495
|
+
timescale of 600.
|
|
496
|
+
- `tearDown()` removes the time observer, invalidates each
|
|
497
|
+
`NSKeyValueObservation` and resets the published state; the isolated
|
|
498
|
+
`deinit` removes the time observer as well.
|
|
499
|
+
- Observer closures capture `self` weakly and hop to the main actor with
|
|
500
|
+
`Task { @MainActor in ... }`. Duration is read with `CMTimeGetSeconds` once
|
|
501
|
+
the item is ready; buffering is `.waitingToPlayAtSpecifiedRate`.
|
|
608
502
|
|
|
609
|
-
|
|
610
|
-
|
|
503
|
+
## Wrapping AVPlayerViewController for SwiftUI
|
|
504
|
+
|
|
505
|
+
When the app needs the delegate, PiP settings or speed options, a representable
|
|
506
|
+
around `AVPlayerViewController` gives more control than `VideoPlayer`.
|
|
611
507
|
|
|
612
508
|
```swift
|
|
613
|
-
import SwiftUI
|
|
614
509
|
import AVKit
|
|
510
|
+
import SwiftUI
|
|
615
511
|
|
|
616
|
-
struct
|
|
512
|
+
struct FullPlayer: UIViewControllerRepresentable {
|
|
617
513
|
let player: AVPlayer
|
|
618
|
-
var
|
|
619
|
-
var
|
|
620
|
-
var speeds: [AVPlaybackSpeed] = AVPlaybackSpeed.systemDefaultSpeeds
|
|
514
|
+
var speeds = AVPlaybackSpeed.systemDefaultSpeeds
|
|
515
|
+
var allowsPiP = true
|
|
621
516
|
|
|
622
|
-
func
|
|
623
|
-
let controller = AVPlayerViewController()
|
|
624
|
-
controller.player = player
|
|
625
|
-
controller.allowsPictureInPicturePlayback = allowsPiP
|
|
626
|
-
controller.canStartPictureInPictureAutomaticallyFromInline = autoStartPiPFromInline
|
|
627
|
-
controller.speeds = speeds
|
|
628
|
-
controller.delegate = context.coordinator
|
|
629
|
-
return controller
|
|
630
|
-
}
|
|
517
|
+
func makeCoordinator() -> Coordinator { Coordinator() }
|
|
631
518
|
|
|
632
|
-
func
|
|
633
|
-
|
|
519
|
+
func makeUIViewController(context ctx: Context) -> AVPlayerViewController {
|
|
520
|
+
let viewer = AVPlayerViewController()
|
|
521
|
+
viewer.player = player
|
|
522
|
+
viewer.allowsPictureInPicturePlayback = allowsPiP
|
|
523
|
+
viewer.canStartPictureInPictureAutomaticallyFromInline = allowsPiP
|
|
524
|
+
viewer.speeds = speeds
|
|
525
|
+
viewer.delegate = ctx.coordinator
|
|
526
|
+
return viewer
|
|
634
527
|
}
|
|
635
528
|
|
|
636
|
-
func
|
|
529
|
+
func updateUIViewController(_ viewer: AVPlayerViewController, context: Context) {
|
|
530
|
+
viewer.player = player
|
|
531
|
+
}
|
|
637
532
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
) {
|
|
643
|
-
// Handle full-screen entry
|
|
644
|
-
}
|
|
533
|
+
@MainActor
|
|
534
|
+
final class Coordinator: NSObject, @preconcurrency AVPlayerViewControllerDelegate {
|
|
535
|
+
func playerViewController(_ viewer: AVPlayerViewController,
|
|
536
|
+
willBeginFullScreenPresentationWithAnimationCoordinator transition: any UIViewControllerTransitionCoordinator) {}
|
|
645
537
|
|
|
646
|
-
func playerViewController(
|
|
647
|
-
|
|
648
|
-
willEndFullScreenPresentationWithAnimationCoordinator coordinator: any UIViewControllerTransitionCoordinator
|
|
649
|
-
) {
|
|
650
|
-
// Handle full-screen exit
|
|
651
|
-
}
|
|
538
|
+
func playerViewController(_ viewer: AVPlayerViewController,
|
|
539
|
+
willEndFullScreenPresentationWithAnimationCoordinator transition: any UIViewControllerTransitionCoordinator) {}
|
|
652
540
|
|
|
653
|
-
func playerViewController(
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
) {
|
|
657
|
-
completionHandler(true)
|
|
541
|
+
func playerViewController(_ viewer: AVPlayerViewController,
|
|
542
|
+
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler done: @escaping (Bool) -> Void) {
|
|
543
|
+
done(true)
|
|
658
544
|
}
|
|
659
545
|
|
|
660
|
-
func playerViewController(
|
|
661
|
-
|
|
662
|
-
failedToStartPictureInPictureWithError error: any Error
|
|
663
|
-
) {
|
|
664
|
-
print("PiP failed: \(error)")
|
|
665
|
-
}
|
|
546
|
+
func playerViewController(_ viewer: AVPlayerViewController,
|
|
547
|
+
failedToStartPictureInPictureWithError reason: any Error) {}
|
|
666
548
|
}
|
|
667
549
|
}
|
|
668
550
|
```
|