WWDC Quick Look 💓 By SwiftGGTeam
What's new in AVKit

What's new in AVKit

观看原视频

Highlight

AVKit 在 iOS 15 和 macOS Monterey 中让内联视频可自动进入画中画,给 AVSampleBufferDisplayLayer 增加系统画中画支持,并让 Mac Catalyst 视频获得真正的全屏播放体验。

核心内容

一个视频 App 最怕用户离开播放页面。

用户正在看视频,收到一条消息。他滑回主屏幕回复。如果播放器没有画中画(Picture in Picture,简称 PiP),视频就停在原地。用户要再回到 App,找到原页面,继续播放。

过去,AVKit 已经能处理 AVPlayer 驱动的视频。使用 AVPlayerViewController 时,系统控件会帮你接入画中画。自定义播放器可以用 AVPictureInPictureController,但它的核心对象仍然是 AVPlayerLayer

这对自定义播放管线不够。比如 WebRTC、实时流、自定义解码器,画面可能由 AVSampleBufferDisplayLayer 显示。播放状态不在 AVPlayer 里,系统不知道何时暂停、何时快进、时间轴该画到哪里。

WWDC21 的 AVKit 更新补上了这块。Apple 增加了 content source API,让 AVPictureInPictureController 可以服务 AVSampleBufferDisplayLayer。开发者要提供播放状态和响应控制命令,系统负责画中画窗口和交互。

macOS Monterey 还改了另一个痛点。Mac Catalyst App 的视频全屏过去只填满窗口,没有进入真正的屏幕全屏。现在视频可以占满整个屏幕。用户从全屏视频滑回 App 后,播放还能继续,前提是 App 保持播放器对象存活。

详细内容

内联视频自动进入画中画

01:16

Apple 新增了 canStartPictureInPictureAutomaticallyFromInline。它在 AVPlayerViewControllerAVPictureInPictureController 上可用。

import AVKit

let playerViewController = AVPlayerViewController()
playerViewController.canStartPictureInPictureAutomaticallyFromInline = true

关键点:

  • import AVKit 引入 AVKit 的播放器视图控制器。
  • AVPlayerViewController() 创建使用系统播放控件的播放器容器。
  • canStartPictureInPictureAutomaticallyFromInline = true 允许内联播放的视频在用户滑回主屏幕时自动进入画中画。

这个开关只适合主内容视频。逐字稿明确提醒:只有当播放内容是用户主要关注对象时,才把它设为 true

01:40

如果 App 使用自定义 UI,还可以继续用 AVPictureInPictureController

func setupPictureInPicture() {
    // Ensure PiP is supported by current device.
    if AVPictureInPictureController.isPictureInPictureSupported() {
        // Create a new controller, passing the reference to the AVPlayerLayer.
        pictureInPictureController = AVPictureInPictureController(playerLayer: playerLayer)
        pictureInPictureController.delegate = self

        // Observe AVPictureInPictureController.isPictureInPicturePossible to update the PiP
        // button’s enabled state.
    } else {
        // PiP isn't supported by the current device. Disable the PiP button.
        pictureInPictureButton.isEnabled = false
    }
}

关键点:

  • isPictureInPictureSupported() 先检查当前设备是否支持画中画。
  • AVPictureInPictureController(playerLayer: playerLayer) 把画中画控制器和 AVPlayerLayer 绑定。
  • pictureInPictureController.delegate = self 让当前对象接收画中画生命周期回调。
  • pictureInPictureButton.isEnabled = false 在设备不支持时禁用入口,避免给用户一个不可用按钮。

02:11

按钮点击时,只需要根据当前状态调用开始或停止。

@IBAction func togglePictureInPictureMode(_ sender: UIButton) {
    if pictureInPictureController.isPictureInPictureActive {
        pictureInPictureController.stopPictureInPicture()
    } else {
        pictureInPictureController.startPictureInPicture()
    }
}

关键点:

  • @IBAction 把按钮点击连接到这个方法。
  • isPictureInPictureActive 判断当前是否已经处于画中画。
  • stopPictureInPicture() 结束当前画中画。
  • startPictureInPicture() 启动画中画。

AVSampleBufferDisplayLayer 获得画中画支持

02:28

过去的画中画围绕 AVPlayer 内容构建。WWDC21 起,AVPictureInPictureController 支持 AVSampleBufferDisplayLayer

逐字稿给出的流程是:先创建 ContentSource,再用 AVPlayerLayerAVSampleBufferDisplayLayer 配置它。对用户来说,画中画体验一致。对开发者来说,非 AVPlayer 播放需要额外提供播放状态。

02:56

新的委托协议是 AVPictureInPictureSampleBufferPlaybackDelegate

public protocol AVPictureInPictureSampleBufferPlaybackDelegate: NSObjectProtocol{

    // Delegate is responsible for:
    //
    // - Supplying playback state information for PiP UI.
    // - Responding to user input from PiP UI.

}

关键点:

  • AVPictureInPictureSampleBufferPlaybackDelegate 负责把播放状态告诉画中画 UI。
  • Supplying playback state information 对应时间轴、播放中、暂停等状态。
  • Responding to user input 对应用户在画中画窗口里点播放、暂停、跳转。

这一步的原因很直接:AVSampleBufferDisplayLayer 不由 AVPlayer 管理,系统无法从播放器对象读取状态。

处理画中画窗口里的播放控制

03:17

用户在画中画窗口点播放、暂停、快退、快进时,AVKit 会把命令转发给委托。

func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool)

func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void)

关键点:

  • setPlaying playing: Bool 在用户点击播放或暂停按钮时调用。
  • playing 是系统希望播放器切换到的目标状态。
  • skipByInterval skipInterval: CMTime 在用户点击跳转按钮时调用。
  • completionHandler 在跳转处理完成后调用,让系统知道命令已经处理完。

03:31

画中画窗口还需要时间轴。委托用 CMTimeRange 返回可播放范围。

func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange

关键点:

  • 返回值 CMTimeRange 描述当前可播放的时间范围。
  • 逐字稿说明,有限时长的范围应该包含 sample buffer display layer timebase 的当前时间。
  • 无限时长的范围表示直播内容。

03:51

画中画窗口大小会变化。例如用户捏合缩放窗口。AVKit 提供渲染尺寸回调。

func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions)

关键点:

  • didTransitionToRenderSize 在画中画窗口尺寸变化时调用。
  • newRenderSize 是新的渲染尺寸。
  • 逐字稿建议根据这个尺寸选择合适的媒体变体,避免额外解码开销。

04:06

AVKit 会周期性询问当前是否暂停。

func pictureInPictureControllerIsPlaybackPaused(pictureInPictureController: AVPictureInPictureController) -> Bool

关键点:

  • 返回 true 表示播放器当前暂停。
  • 返回 false 表示播放器当前播放。
  • 逐字稿把它类比为 AVPlayertimeControlStatus

Mac Catalyst 获得真正全屏播放

04:23

在 Big Sur 中,Mac Catalyst App 的视频全屏只填满整个窗口。macOS Monterey 改成填满整个屏幕。原生 macOS App 和 Mac Catalyst App 的播放控件也保持一致。

这项行为对所有 Mac Catalyst App 自动生效。用户还能像原生 macOS 全屏一样,滑回 App 窗口。原来的播放器位置会显示占位视图,提示内容正在全屏播放。

06:05

真正需要开发者处理的是生命周期。用户滑回 App 后可能继续浏览列表,导致原来的播放器控制器离开视图层级。如果对象被释放,全屏播放会结束。

func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    coordinator.animate(alongsideTransition: nil) { context in
        // Keep a strong reference to the playerViewController while in full screen.
        self.detachedPlayerViewController = playerViewController
    }
}

关键点:

  • willBeginFullScreenPresentationWithAnimationCoordinator 在即将进入全屏时调用。
  • coordinator.animate 把引用保存安排在转场完成阶段。
  • self.detachedPlayerViewController = playerViewController 保持强引用,防止控制器从视图层级移除后被释放。

06:38

退出全屏后,App 可以释放这个临时引用。

func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator){
    coordinator.animate(alongsideTransition: nil) { context in
        // Stop keeping the playerViewController alive when transition completes,
        self.detachedPlayerViewController = nil
    }
}

关键点:

  • willEndFullScreenPresentationWithAnimationCoordinator 在即将退出全屏时调用。
  • coordinator.animate 等待退出转场结束。
  • self.detachedPlayerViewController = nil 放开强引用,让对象生命周期回到 App 原本的管理逻辑。

06:46

原生 macOS App 使用 AVPlayerView 的委托方法。

func playerViewWillEnterFullScreen(_ playerView: AVPlayerView) {
    // Start keeping the player view alive while it is not in the view hierarchy.
    self.detachedPlayerView = playerView
}

func playerViewWillExitFullScreen(_ playerView: AVPlayerView) {
    // Stop keeping the player view alive.
    self.detachedPlayerView = nil
}

关键点:

  • playerViewWillEnterFullScreenAVPlayerView 进入全屏时调用。
  • self.detachedPlayerView = playerView 保持播放器视图存活。
  • playerViewWillExitFullScreen 在退出全屏时调用。
  • self.detachedPlayerView = nil 释放临时引用。

退出全屏时恢复界面

06:55

用户退出全屏时,App 可以导航回原来的播放页面。AVKit 提供了异步恢复回调。

// Restoring UI when exiting full screen

// iOS / MacCatalyst
func playerViewControllerRestoreUserInterfaceForFullScreenExit(_ playerViewController: AVPlayerViewController) async -> Bool {
	// Custom UI restoration logic
	return true
}

// macOS
func playerViewRestoreUserInterfaceForFullScreenExit(_ playerView: AVPlayerView) async -> Bool {
	// custom UI restore logic here
	return true
}

关键点:

  • playerViewControllerRestoreUserInterfaceForFullScreenExit 用于 iOS 和 Mac Catalyst。
  • playerViewRestoreUserInterfaceForFullScreenExit 用于 macOS。
  • return true 表示界面恢复成功。
  • 逐字稿提醒要尽快返回,避免阻塞从全屏回到内联的转场。
  • 返回 false 表示恢复失败或无法恢复,内容会退出全屏且没有动画。

核心启发

  1. 做什么:给课程视频页启用内联自动画中画。 为什么值得做:用户滑回主屏幕回复消息时,视频可以继续播放。 怎么开始:使用 AVPlayerViewController 时,把 canStartPictureInPictureAutomaticallyFromInline 设为 true,并只在主视频播放时启用。

  2. 做什么:给 WebRTC 或实时视频播放器加系统画中画。 为什么值得做AVSampleBufferDisplayLayer 可以通过 content source API 接入 AVPictureInPictureController怎么开始:为 sample buffer 播放实现 AVPictureInPictureSampleBufferPlaybackDelegate,先处理 setPlayingskipByIntervalisPlaybackPaused

  3. 做什么:根据画中画窗口尺寸切换视频变体。 为什么值得做:画中画窗口很小时,高分辨率解码会造成额外开销。 怎么开始:实现 pictureInPictureController(_:didTransitionToRenderSize:),用 CMVideoDimensions 选择更合适的媒体变体。

  4. 做什么:让 Mac Catalyst 视频 App 支持离开页面后的持续全屏播放。 为什么值得做:用户从全屏视频滑回 App 后,可以继续浏览内容,全屏播放不中断。 怎么开始:在 willBeginFullScreenPresentation 保存 playerViewController 强引用,在 willEndFullScreenPresentation 释放。

  5. 做什么:退出全屏后自动回到视频详情页。 为什么值得做:用户从 Mission Control 或手势回到 App 时,不会丢失播放上下文。 怎么开始:实现 playerViewControllerRestoreUserInterfaceForFullScreenExitplayerViewRestoreUserInterfaceForFullScreenExit,快速完成导航并返回 true

关联 Session

评论

GitHub Issues · utterances