---
name: avkit
description: "AVKit playback UI: AVPlayerViewController, SwiftUI VideoPlayer, Picture-in-Picture, AirPlay routing, transport controls, subtitles and closed captions, audio session and background mode, AVFoundation playback in system UI. Use when building a video player or adding PiP, AirPlay, transport controls or captions."
metadata:
  source: multi-agent-pipeline
---

# AVKit

AVKit is the playback UI layer over AVFoundation. It supplies the standard
player, Picture-in-Picture (PiP), AirPlay, transport controls and subtitle and
caption display. Code here is written for iOS 26 with Swift 6.3.

Covers: PiP for standard and custom players, AirPlay routing with
`AVRoutePickerView`, playback speeds and interstitial ads.

## Setup

### Audio session and background mode

Background audio, AirPlay and PiP all depend on two things: an audio session
in the playback category, and the background mode that goes with it.

1. In Signing & Capabilities, add Background Modes and tick "Audio, AirPlay,
   and Picture in Picture". That adds `audio` to `UIBackgroundModes`.
2. Set the category to `.playback`, with mode `.moviePlayback` for film and
   video content.
3. Activate the session only when playback actually starts, so the app does not
   silence other audio before the user presses play.

```swift
import AVFoundation

enum PlaybackAudio {
    static func configure() {
        do {
            let session = AVAudioSession.sharedInstance()
            try session.setCategory(.playback, mode: .moviePlayback)
        } catch {
            assertionFailure("Audio session category failed: \(error)")
        }
    }

    static func activateForPlayback() throws {
        try AVAudioSession.sharedInstance().setActive(true)
    }
}
```

### Imports

- `import AVKit` for `AVPlayerViewController`, `VideoPlayer` and PiP.
- `import AVFoundation` for `AVPlayer`, `AVPlayerItem` and `AVAsset`.

## AVPlayerViewController

The standard UIKit player. It brings the system controls, PiP, AirPlay,
subtitles and video frame analysis. Apple does not support subclassing it;
configure it and use its delegate instead.

```swift
import AVFoundation
import AVKit
import UIKit

final class LessonViewController: UIViewController {
    private let playerScreen = AVPlayerViewController()

    func presentFullScreen(_ url: URL) {
        let viewer = AVPlayerViewController()
        viewer.player = AVPlayer(url: url)
        present(viewer, animated: true) {
            viewer.player?.play()
        }
    }

    func embed(_ url: URL, in holder: UIView) {
        playerScreen.player = AVPlayer(url: url)
        addChild(playerScreen)
        holder.addSubview(playerScreen.view)
        playerScreen.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            playerScreen.view.topAnchor.constraint(equalTo: holder.topAnchor),
            playerScreen.view.bottomAnchor.constraint(equalTo: holder.bottomAnchor),
            playerScreen.view.leadingAnchor.constraint(equalTo: holder.leadingAnchor),
            playerScreen.view.trailingAnchor.constraint(equalTo: holder.trailingAnchor)
        ])
        playerScreen.didMove(toParent: self)
    }
}
```

### Key properties

| Property | Effect |
|---|---|
| `showsPlaybackControls` | shows or hides the system controls |
| `videoGravity` | `.resizeAspect` fits the frame, `.resizeAspectFill` crops to fill |
| `entersFullScreenWhenPlaybackBegins` | goes full screen when play starts |
| `exitsFullScreenWhenPlaybackEnds` | leaves full screen at the end |
| `updatesNowPlayingInfoCenter` | keeps `MPNowPlayingInfoCenter` in sync automatically |
| `contentOverlayView` | holds non-interactive views (logos, watermarks) between the video and the controls |

### Delegate

`AVPlayerViewControllerDelegate` reports full-screen transitions, the PiP
lifecycle, ad interstitials and changes to the selected media options. To move your own
UI in step with the full-screen animation, use the transition coordinator:

```swift
import AVKit
import UIKit

@MainActor
final class FullScreenSync: NSObject, @preconcurrency AVPlayerViewControllerDelegate {
    var chrome: UIView?

    func playerViewController(_ playerViewController: AVPlayerViewController,
                              willBeginFullScreenPresentationWithAnimationCoordinator coordinator: any UIViewControllerTransitionCoordinator) {
        coordinator.animate(alongsideTransition: { _ in self.chrome?.alpha = 0 }, completion: nil)
    }
}
```

### Showing the player only when it has a frame

Observe `isReadyForDisplay` and reveal the player when it turns `true`; this
avoids a black flash.

```swift
import AVKit

@MainActor
func revealWhenReady(_ viewer: AVPlayerViewController) -> NSKeyValueObservation {
    viewer.view.alpha = 0
    return viewer.observe(\.isReadyForDisplay, options: [.new]) { observed, _ in
        MainActor.assumeIsolated {
            if observed.isReadyForDisplay { observed.view.alpha = 1 }
        }
    }
}
```

## SwiftUI VideoPlayer

`VideoPlayer` is the SwiftUI view for AVKit's playback UI.

```swift
import AVKit
import SwiftUI

struct TrailerView: View {
    let source: URL
    @State private var player: AVPlayer?

    var body: some View {
        Group {
            if let player {
                VideoPlayer(player: player) {
                    VStack {
                        Spacer()
                        Text("Preview")
                            .font(.caption)
                            .padding(6)
                            .background(.ultraThinMaterial, in: Capsule())
                    }
                }
                .frame(height: 240)
            } else {
                ProgressView()
            }
        }
        .task {
            player = AVPlayer(url: source)
        }
    }
}
```

- The trailing closure is a video overlay. It sits above the picture and below
  the system controls. It may contain interactive views, but those only receive
  events that the system controls did not handle.
- `VideoPlayer` exposes only part of what `AVPlayerViewController` offers. For
  PiP settings, delegate callbacks or playback speeds, wrap
  `AVPlayerViewController` in a `UIViewControllerRepresentable` (see
  [the patterns reference](references/avkit-patterns.md#wrapping-avplayerviewcontroller-for-swiftui)).

## Picture-in-Picture

`AVPlayerViewController` provides PiP by itself once three conditions hold:
setup is done, the hardware can do PiP, and the `AVPlayerItem` now loaded holds
playable video in a format AVPlayer handles. Audio-only items, unsupported
containers or codecs, and items not yet ready to display can all leave PiP
unavailable even when setup is correct. Custom player UIs drive
`AVPictureInPictureController` themselves.

### Prerequisites

1. Audio session category `.playback`.
2. Background mode `audio` enabled (shown in Xcode as "Audio, AirPlay, and Picture in Picture").
3. A ready `AVPlayerItem` containing playable video, not only audio.
4. A playback context that allows PiP; custom players watch
   `isPictureInPicturePossible`.

### Standard player

PiP is on by default. Turn it off with `allowsPictureInPicturePlayback = false`.
For an inline (not full-screen) player, set
`canStartPictureInPictureAutomaticallyFromInline = true` to enter PiP
automatically when the app goes to the background.

### Restoring the app UI when PiP ends

When the user taps the button to return from PiP, the app must put the player
back on screen and then pass `true` to the completion handler so the system
can finish its animation.

```swift
import AVKit
import UIKit

@MainActor
final class PiPReturn: NSObject, @preconcurrency AVPlayerViewControllerDelegate {
    weak var host: UIViewController?

    func playerViewController(_ playerViewController: AVPlayerViewController,
                              restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
        guard let host, playerViewController.presentingViewController == nil else {
            completionHandler(true)
            return
        }
        host.present(playerViewController, animated: false) {
            completionHandler(true)
        }
    }
}
```

### Custom players

Build an `AVPictureInPictureController` from an `AVPlayerLayer` or from a
sample-buffer content source.

- Check `AVPictureInPictureController.isPictureInPictureSupported()` before
  showing any PiP control.
- Check `isPictureInPicturePossible` right before `startPictureInPicture()`.
- Call `startPictureInPicture()` only from the user's tap on a PiP button,
  never on your own.
- The controller also has `canStartPictureInPictureAutomaticallyFromInline` and
  a `delegate`.

Full layer-based and sample-buffer examples are in
[the patterns reference](references/avkit-patterns.md#custom-player-with-avplayerlayer).

### Ads and linear playback

Interstitial breaks come either from the stream itself (HLS interstitials,
which the item reports through `interstitialTimeRanges`) or from a schedule the app owns,
built with `AVPlayerInterstitialEventController`. On iOS,
`interstitialTimeRanges` is read-only; do not try to assign it.

Set `requiresLinearPlayback = true` only while a required ad or legal segment
plays, to block seeking, and set it back to `false` afterwards.

## AirPlay

`AVPlayerViewController` supports AirPlay with no extra code when the setup,
media, available routes and device allow it. The AirPlay button appears in the
controls whenever a receiver is available.

### AVRoutePickerView

For an AirPlay button outside the player, use `AVRoutePickerView`:

```swift
import AVKit
import UIKit

@MainActor
func castButton() -> AVRoutePickerView {
    let picker = AVRoutePickerView(frame: CGRect(origin: .zero, size: CGSize(width: 44, height: 44)))
    picker.activeTintColor = .systemTeal
    picker.prioritizesVideoDevices = true
    return picker
}
```

`prioritizesVideoDevices = true` lists video-capable receivers first.

### External playback

- `AVPlayer.allowsExternalPlayback` is `true` by default. Leave it on for
  AirPlay, or set it explicitly if other code might have turned it off.
- `usesExternalPlaybackWhileExternalScreenIsActive` is only for making the
  player switch to external playback automatically while an external screen is
  in use. It is not something AirPlay generally needs.

## Transport controls and speed

### Custom speeds

```swift
import AVKit

@MainActor
func offerLectureSpeeds(_ viewer: AVPlayerViewController) {
    viewer.speeds = [
        AVPlaybackSpeed(rate: 0.75, localizedName: "0.75x"),
        AVPlaybackSpeed(rate: 1, localizedName: "1x"),
        AVPlaybackSpeed(rate: 1.25, localizedName: "1.25x"),
        AVPlaybackSpeed(rate: 1.75, localizedName: "1.75x")
    ]
}

@MainActor
func restoreDefaultSpeeds(_ viewer: AVPlayerViewController) {
    viewer.speeds = AVPlaybackSpeed.systemDefaultSpeeds
}
```

### Skipping and seeking

On iOS rely on the standard transport controls, and use `AVPlayer.seek(...)`
behind any custom buttons. `isSkipForwardEnabled`, `isSkipBackwardEnabled` and
`skippingBehavior` are tvOS APIs; keep them out of iOS code.

### Now Playing

`AVPlayerViewController` updates `MPNowPlayingInfoCenter` by default. Set
`updatesNowPlayingInfoCenter = false` if the app manages Now Playing itself.

## Subtitles and closed captions

When the media has text tracks, AVKit shows subtitles and captions
automatically according to the user's choices in Settings > Accessibility >
Subtitles & Captioning.

```swift
import AVFoundation

@MainActor
func chooseSubtitles(language code: String, for item: AVPlayerItem) async throws {
    let media = item.asset
    guard let legible = try await media.loadMediaSelectionGroup(for: .legible) else { return }
    let match = legible.options.first { $0.locale?.language.languageCode?.identifier == code }
    item.select(match, in: legible)
}
```

- `allowedSubtitleOptionLanguages`, `requiresFullSubtitles` and the delegate
  callback for media selection exist only on tvOS. On iOS, select within the item's
  `.legible` group as above.
- HLS carries subtitles and captions in its manifests, and AVKit reads them
  from the `AVAsset`'s `AVMediaSelectionGroup`.
- Local files need the legible tracks already inside the file, or authored
  into the asset.

## Common mistakes

- **Subclassing `AVPlayerViewController`.** Apple does not support it and it
  can break on a future OS. Compose it and use the delegate.
- **Leaving the default audio session.** PiP will not work. Set
  `.playback` with `.moviePlayback`, and activate when playback begins.
- **No PiP restore handler, or no `completionHandler(true)`.** Without the
  handler the user cannot get back to the player; without the completion call
  the restore does nothing or the animation hangs.
- **`AVPlayer` as a stored property of a SwiftUI view.** It is recreated every
  time the view value is rebuilt. Hold an optional in `@State` and create it in
  `.task`.

## Review checklist

- [ ] Audio session is `.playback` with `.moviePlayback`
- [ ] The session is activated only when playback starts
- [ ] `UIBackgroundModes` contains `audio`
- [ ] `AVPlayerViewController` is not subclassed
- [ ] PiP was tried with real, supported video media
- [ ] The PiP restore delegate method exists and calls `completionHandler(true)`
- [ ] Custom PiP checks device support and `isPictureInPicturePossible`
- [ ] Custom PiP starts only from an explicit user action
- [ ] SwiftUI creates the `AVPlayer` in `.task`
- [ ] Inline players set `canStartPictureInPictureAutomaticallyFromInline`
- [ ] `requiresLinearPlayback` is `true` only while a mandatory ad or legal notice plays
- [ ] tvOS-only skipping APIs are absent from iOS code
- [ ] External playback is not switched off where AirPlay is expected
- [ ] Subtitle selection was tested with media that has real text tracks
- [ ] `videoGravity` suits the content
- [ ] `isReadyForDisplay` gates when the player appears
- [ ] Streaming errors (HLS failures, timeouts) are handled

## References

- [AVKit patterns](references/avkit-patterns.md) - custom AVPlayerLayer player with PiP, sample-buffer PiP, interstitials, background playback, errors, playback observation, frame analysis, HDR, SwiftUI player model, representable wrapper
- [AVKit](https://developer.apple.com/documentation/avkit)
- [AVPlayerViewController](https://developer.apple.com/documentation/avkit/avplayerviewcontroller)
- [VideoPlayer](https://developer.apple.com/documentation/avkit/videoplayer)
- [AVPictureInPictureController](https://developer.apple.com/documentation/avkit/avpictureinpicturecontroller)
- [AVRoutePickerView](https://developer.apple.com/documentation/avkit/avroutepickerview)
- [AVPlaybackSpeed](https://developer.apple.com/documentation/avkit/avplaybackspeed)
- [Apple guide: media playback configuration](https://developer.apple.com/documentation/avfoundation/configuring-your-app-for-media-playback)
- [Apple guide: PiP in the standard player](https://developer.apple.com/documentation/avkit/adopting-picture-in-picture-in-a-standard-player)
- [Apple guide: video in the standard interface](https://developer.apple.com/documentation/avkit/playing-video-content-in-a-standard-user-interface)
