Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Create media playback experiences using AVKit. Use when adding video players with AVPlayerViewController, enabling Picture-in-Picture, routing media with AirPlay, using SwiftUI VideoPlayer views, configuring transport controls, displaying subtitles and closed captions, or integrating AVFoundation playback with system UI.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 83% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 563% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 161% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 75% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 221% | 0% |
High-level media playback UI built on AVFoundation. Provides system-standard video players, Picture-in-Picture, AirPlay routing, transport controls, and subtitle/caption display. Targets Swift 6.3 / iOS 26+.
Playback apps need an audio session category and the matching background mode when they support background audio, AirPlay, or PiP.
audio value in UIBackgroundModes)
.playbacksetActive(true) until playback begins so you do not interrupt otheraudio prematurely
swiftimport AVFoundation func configureAudioSessionForPlayback() { let session = AVAudioSession.sharedInstance() do { try session.setCategory(.playback, mode: .moviePlayback) } catch { print("Audio session category failed: \(error)") } } func activateAudioSessionWhenPlaybackBegins() { do { try AVAudioSession.sharedInstance().setActive(true) } catch { print("Audio session activation failed: \(error)") } }
swiftimport AVKit // AVPlayerViewController, VideoPlayer, PiP import AVFoundation // AVPlayer, AVPlayerItem, AVAsset
AVPlayerViewController is the standard UIKit player. It provides system playback controls, PiP, AirPlay, subtitles, and frame analysis out of the box. Do not subclass it.
swiftimport AVKit func presentPlayer(from viewController: UIViewController, url: URL) { let player = AVPlayer(url: url) let playerVC = AVPlayerViewController() playerVC.player = player viewController.present(playerVC, animated: true) { player.play() } }
Add AVPlayerViewController as a child view controller for inline playback. Call addChild, add the view with constraints, then call didMove(toParent:).
swiftfunc embedPlayer(in parent: UIViewController, container: UIView, url: URL) { let playerVC = AVPlayerViewController() playerVC.player = AVPlayer(url: url) parent.addChild(playerVC) container.addSubview(playerVC.view) playerVC.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ playerVC.view.leadingAnchor.constraint(equalTo: container.leadingAnchor), playerVC.view.trailingAnchor.constraint(equalTo: container.trailingAnchor), playerVC.view.topAnchor.constraint(equalTo: container.topAnchor), playerVC.view.bottomAnchor.constraint(equalTo: container.bottomAnchor) ]) playerVC.didMove(toParent: parent) }
swiftplayerVC.showsPlaybackControls = true // Show/hide system controls playerVC.videoGravity = .resizeAspect // .resizeAspectFill to crop playerVC.entersFullScreenWhenPlaybackBegins = false playerVC.exitsFullScreenWhenPlaybackEnds = true playerVC.updatesNowPlayingInfoCenter = true // Auto-updates MPNowPlayingInfoCenter
Use contentOverlayView to add non-interactive views (watermarks, logos) between the video and transport controls.
Adopt AVPlayerViewControllerDelegate to respond to full-screen transitions, PiP lifecycle events, interstitial playback, and media selection changes. Use the transition coordinator's animate(alongsideTransition:completion:) to synchronize your UI with full-screen animations.
Observe isReadyForDisplay before showing the player to avoid a black flash:
swiftlet observation = playerVC.observe(\.isReadyForDisplay) { observed, _ in if observed.isReadyForDisplay { // Safe to show the player view } }
The VideoPlayer SwiftUI view wraps AVKit's playback UI.
swiftimport SwiftUI import AVKit struct PlayerView: View { @State private var player: AVPlayer? var body: some View { Group { if let player { VideoPlayer(player: player) .frame(height: 300) } else { ProgressView() } } .task { let url = URL(string: "https://example.com/video.m3u8")! player = AVPlayer(url: url) } } }
Add a SwiftUI overlay above the video content and below the system playback controls. The overlay can be interactive, but it only receives events the system controls do not handle.
swiftVideoPlayer(player: player) { VStack { Spacer() HStack { Image("logo") .resizable() .frame(width: 40, height: 40) .padding() Spacer() } } }
VideoPlayer does not expose all AVPlayerViewController properties. For PiP configuration, delegate callbacks, or playback speed control, wrap AVPlayerViewController in a UIViewControllerRepresentable. See the full pattern in references/avkit-patterns.md.
PiP lets users watch video in a floating window while using other apps. AVPlayerViewController supports PiP automatically once the app is configured, the device supports PiP, and the current AVPlayerItem is playable video content in an AVPlayer-compatible format. Audio-only items, unsupported containers/codecs, or items that are not ready to display video can make PiP unavailable even when app and device setup are correct. For custom player UIs, use AVPictureInPictureController directly.
.playback (see Setup)AVPlayerItem with playable video media, not audio-only contentisPictureInPicturePossiblePiP is enabled by default on AVPlayerViewController. Control automatic activation and inline-to-PiP transitions:
swiftlet playerVC = AVPlayerViewController() playerVC.player = player // PiP enabled by default; set false to disable playerVC.allowsPictureInPicturePlayback = true // Auto-start PiP when app backgrounds (for inline/non-fullscreen players) playerVC.canStartPictureInPictureAutomaticallyFromInline = true
When the user taps the restore button in PiP, implement the delegate method to re-present your player. Call the completion handler with true to signal the system to finish the restore animation.
swiftfunc playerViewController( _ playerViewController: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void ) { // Re-present or re-embed the player view controller present(playerViewController, animated: false) { completionHandler(true) } }
For custom player UIs, use AVPictureInPictureController with an AVPlayerLayer or sample buffer content source. Check device support before creating PiP UI, then check the controller's isPictureInPicturePossible before starting PiP in the current playback context. See references/avkit-patterns.md for full custom player and sample buffer PiP patterns.
swiftguard AVPictureInPictureController.isPictureInPictureSupported() else { return } let pipController = AVPictureInPictureController(playerLayer: playerLayer) pipController.delegate = self pipController.canStartPictureInPictureAutomaticallyFromInline = true // Call this from the user's PiP button action, never automatically. if pipController.isPictureInPicturePossible { pipController.startPictureInPicture() }
Interstitial breaks can come from the media stream/manifest, which AVFoundation exposes through AVPlayerItem.interstitialTimeRanges, or from an app-owned AVPlayerInterstitialEventController schedule. Do not assign interstitialTimeRanges directly on iOS. Use requiresLinearPlayback only to prevent seeking during required ad or legal segments:
swift// During an ad playerVC.requiresLinearPlayback = true // After the ad completes playerVC.requiresLinearPlayback = false
AVPlayerViewController supports AirPlay automatically when app configuration, media, routes, and device support allow external playback. No additional code is required when using the standard player. The system displays the AirPlay button in the transport controls when AirPlay-capable devices are available.
Add a standalone AirPlay route picker button outside the player UI:
swiftimport AVKit func addRoutePicker(to containerView: UIView) { let routePicker = AVRoutePickerView(frame: CGRect(x: 0, y: 0, width: 44, height: 44)) routePicker.activeTintColor = .systemBlue routePicker.prioritizesVideoDevices = true // Show video-capable routes first containerView.addSubview(routePicker) }
AVPlayer allows external playback by default. Leave it enabled for AirPlay, or set it explicitly when code elsewhere may disable it:
swiftplayer.allowsExternalPlayback = true
Set usesExternalPlaybackWhileExternalScreenIsActive only when you want the player to automatically switch to external playback while an external screen mode is active.
Provide user-selectable playback speeds in the player UI:
swiftlet playerVC = AVPlayerViewController() playerVC.speeds = [ AVPlaybackSpeed(rate: 0.5, localizedName: "Half Speed"), AVPlaybackSpeed(rate: 1.0, localizedName: "Normal"), AVPlaybackSpeed(rate: 1.5, localizedName: "1.5x"), AVPlaybackSpeed(rate: 2.0, localizedName: "Double Speed") ]
Use AVPlaybackSpeed.systemDefaultSpeeds to restore the default speed options.
On iOS, use the standard transport controls and AVPlayer.seek(...) for custom app controls. AVPlayerViewController skipping behavior APIs such as isSkipForwardEnabled, isSkipBackwardEnabled, and skippingBehavior are tvOS-focused; keep them out of iOS player implementations.
AVPlayerViewController updates MPNowPlayingInfoCenter automatically by default. Disable this if you manage Now Playing info manually:
swiftplayerVC.updatesNowPlayingInfoCenter = false
AVKit handles subtitle and closed caption display automatically when the media contains appropriate text tracks. Users control subtitle preferences in Settings > Accessibility > Subtitles & Captioning.
swiftlet asset = player.currentItem?.asset if let group = try await asset?.loadMediaSelectionGroup(for: .legible), let english = group.options.first(where: { option in option.locale?.language.languageCode?.identifier == "en" }) { player.currentItem?.select(english, in: group) }
allowedSubtitleOptionLanguages, requiresFullSubtitles, and the AVPlayerViewControllerDelegate media-selection callback are tvOS-only. For iOS, load the asset's .legible media selection group and select an option on the AVPlayerItem when the app needs a default.
Subtitles and closed captions are embedded in HLS manifests. AVKit reads them from AVMediaSelectionGroup on the AVAsset. For local files, use media that already includes legible subtitle or closed-caption tracks, or author those tracks into the playable asset before presenting it with AVKit.
Apple explicitly states this is unsupported. It may cause undefined behavior or crash on future OS versions.
swift// WRONG class MyPlayerVC: AVPlayerViewController { } // Unsupported // CORRECT: Use composition with delegation let playerVC = AVPlayerViewController() playerVC.delegate = coordinator
PiP and background playback depend on the playback audio session category and the Audio, AirPlay, and Picture in Picture background mode.
swift// WRONG: Default audio session let playerVC = AVPlayerViewController() playerVC.player = player // PiP won't work // CORRECT: Configure the category, then activate when playback starts try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback) try AVAudioSession.sharedInstance().setActive(true) let playerVC = AVPlayerViewController() playerVC.player = player
Without restoreUserInterfaceForPictureInPictureStopWithCompletionHandler, the system cannot return the user to your player. Failing to call completionHandler(true) leaves the system in an inconsistent state.
swift// WRONG: No delegate method or missing completionHandler call // User taps restore in PiP -> nothing happens or animation hangs // CORRECT func playerViewController( _ playerViewController: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void ) { present(playerViewController, animated: false) { completionHandler(true) } }
Creating the player eagerly causes performance issues. SwiftUI may recreate the view multiple times.
swift// WRONG: Created on every view init struct PlayerView: View { let player = AVPlayer(url: videoURL) // Re-created on every view evaluation var body: some View { VideoPlayer(player: player) } } // CORRECT: Use @State and defer creation struct PlayerView: View { @State private var player: AVPlayer? var body: some View { VideoPlayer(player: player) .task { player = AVPlayer(url: videoURL) } } }
.playback with mode: .moviePlaybackUIBackgroundModesAVPlayerViewController is not subclassedcompletionHandler(true)isPictureInPicturePossibleAVPlayer deferred to .task in SwiftUI (not created eagerly)canStartPictureInPictureAutomaticallyFromInline set for inline playersrequiresLinearPlayback toggled only during required ad/legal segments.resizeAspect vs .resizeAspectFill)isReadyForDisplay observed before showing the player viewOther measured skills in the registry, with their headline benchmark lift.