WWDC Quick Look 💓 By SwiftGGTeam
Support immersive video playback in visionOS apps

Support immersive video playback in visionOS apps

观看原视频

Highlight

visionOS 26 把 Quick Look、AVKit、RealityKit 三个框架打通到同一组沉浸式视频 profile:APMP 180/360/Wide-FOV、Apple Immersive Video 与 Spatial Video。


核心内容

visionOS 1 时代,开发者只能播放 2D 和 3D 视频。Spatial video 出现后,立体内容总算有了自然的呈现方式,但一旦遇到 180、360、Wide-FOV,或者 Apple Immersive Video 这类 profile,App 几乎只能自己写 RealityKit 渲染。Quick Look 不识别格式,AVKit 没有沉浸态切换 API,每个团队都在重复造轮子。

visionOS 26 把这件事合并到了一处。Jamal 和 Michael 在 session 里把同一组 profile 接进了三个框架:Quick Look 用来零代码预览,AVKit 用来获得标准播放控件加沉浸过渡,RealityKit 用来做游戏内嵌或自定义 UI。三者的选择标准很清晰——按需要的可控程度从高到低排:RealityKit > AVKit > Quick Look。

落到 API 层面:AVKit 引入 AVExperienceController,提供 Expanded、Immersive 两种新体验和一个 Delegate Protocol;RealityKit 给 VideoPlayerComponent 加了 desiredImmersiveViewingMode(portal/progressive/full)和 desiredSpatialVideoMode(spatial/screen);Quick Look 则在 PreviewApplicationQLPreviewController 上自动支持新 profile,已接入的 App 不改代码即可使用。系统还会对 APMP 内容自动做 comfort mitigation,检测到高运动时降低沉浸度。


详细内容

AVKit 的 Expanded 配置:让沉浸式内容以 portal 形式展示。 Expanded 是占满整个 UIScene 的体验。visionOS 26 给它加了 AutomaticTransitionToImmersive,默认 .default 会自动切到沉浸态,设为 .none 则保留 portal 处理(05:50):

import AVKit

let controller = AVPlayerViewController()

let experienceController = controller.experienceController
experienceController.allowedExperiences = .recommended(including: [.expanded, .immersive])

experienceController.configuration.expanded.automaticTransitionToImmersive = .none

await experienceController.transition(to: .expanded)

关键点:

  • allowedExperiences = .recommended(including:):声明本平台推荐集合,并确保 .expanded.immersive 都在其中。
  • configuration.expanded.automaticTransitionToImmersive = .none:拒绝系统自动切沉浸,让 AVPlayerViewController 用 portal 渲染沉浸式内容。
  • await experienceController.transition(to: .expanded):异步切到 Expanded,完成后才能保证状态稳定。

显式切到 Immersive 体验。 不靠自动触发,自己决定何时进入沉浸态(06:53):

import AVKit

let controller = AVPlayerViewController()

let experienceController = controller.experienceController
experienceController.allowedExperiences = .recommended(including: [.immersive])

let myScene = getMyPreferredWindowUIScene()
experienceController.configuration.placement = .over(scene: myScene)

await experienceController.transition(to: .immersive)

关键点:

  • 视图未挂到 hierarchy 时,必须通过 configuration.placement = .over(scene:) 指定沉浸内容覆盖在哪个 window scene 上。
  • 已经在 hierarchy 中时可以省略 placement,AVExperienceController 会用所在 scene 作为目标。
  • transition(to: .immersive) 由系统接管动画,自然过渡进沉浸态。

Delegate 三方法负责响应转场。 转场可以由用户、App 逻辑或系统发起,所以三个回调都要看(08:13):

func experienceController(_ controller: AVExperienceController, didChangeAvailableExperiences availableExperiences: AVExperienceController.Experiences)

func experienceController(_ controller: AVExperienceController, prepareForTransitionUsing context: AVExperienceController.TransitionContext) async

func experienceController(_ controller: AVExperienceController, didChangeTransitionContext context: AVExperienceController.TransitionContext)

关键点:

  • didChangeAvailableExperiences:内容类型变化时哪些体验可用。喂入 2D 内容时,Immersive 会从可用集合里消失。
  • prepareForTransitionUsing:异步方法,转场即将发生时用来做最后一次准备,例如保存当前 UI 状态。
  • didChangeTransitionContext:转场结束后调用,确认新状态生效。

RealityKit 的 progressive 模式:用 Digital Crown 调沉浸度。 APMP 和 Apple Immersive Video 在 visionOS 26 推荐用 progressive 而不是 full,因为 progressive 才能配合 comfort mitigation(14:20):

import AVFoundation
import RealityKit
import SwiftUI

@main
struct ImmersiveVideoApp: App {
    var body: some Scene {
        ImmersiveSpace {
            ProgressiveVideoView()
        }
        .immersionStyle(selection: .constant(.progressive(0.1...1, initialAmount: 1.0)), in: .progressive)
    }
}

关键点:

  • 必须放进 ImmersiveSpace,否则 mesh 扩展时会被 window scene 边界裁剪。
  • .immersionStyle.progressive 必须和 desiredImmersiveViewingMode = .progressive 配套,两边类型要匹配。
  • 范围 0.1...1 而非 0...1:避免用户把沉浸度拉到 0 让内容消失。
  • initialAmount: 1.0 等于 full immersion,启动即满沉浸。

Spatial video 的特殊处理:immersive 渲染不是 head-locked。 给 spatial video 套 portal 时,VideoPlayerComponent 的 mesh 默认 1 米高,要 scale 0.4 缩进窗口。切到 immersive full 后,mesh 由组件控制大小,但因为不是 head-locked,必须自己设位置(19:02):

import AVFoundation
import RealityKit
import SwiftUI

struct PortalSpatialVideoView: View {
    var body: some View {
        RealityView { content in
            let url = Bundle.main.url(forResource: "MySpatialVideo", withExtension: "mov")!
            let player = AVPlayer(url: url)
            let videoEntity = Entity()
            var videoPlayerComponent = VideoPlayerComponent(avPlayer: player)
            videoPlayerComponent.desiredViewingMode = .stereo
            videoPlayerComponent.desiredSpatialVideoMode = .spatial
            videoPlayerComponent.desiredImmersiveViewingMode = .full
            videoEntity.position = [0, 1.5, -1]
            videoEntity.components.set(videoPlayerComponent)
            content.add(videoEntity)
        }
    }
}

关键点:

  • desiredViewingMode = .stereo:spatial video 的默认值,写出来更明确。
  • desiredSpatialVideoMode = .spatial:开启完整 spatial styling,效果与 Photos app 一致;切换为 .screen 则退回传统立体渲染。
  • desiredImmersiveViewingMode = .full:spatial video 的 immersive 渲染只支持 full,而非 progressive。
  • videoEntity.position = [0, 1.5, -1]:地面上方 1.5 米、前方 1 米;更稳的做法是用 head anchor 锁定到观看者前方。

让 spatial video 在系统环境里也能渲染。.mixed 沉浸样式 + .coexist 行为修饰(19:46):

@main
struct SpatialVideoApp: App {
    var body: some Scene {
        ImmersiveSpace {
            ContentSimpleView()
        }
        .immersionStyle(selection: .constant(.mixed), in: .mixed)
        .immersiveEnvironmentBehavior(.coexist)
    }
}

关键点:

  • spatial video immersive 渲染要叠在 passthrough 之上,所以用 .mixed 而不是 .progressive.full
  • .immersiveEnvironmentBehavior(.coexist) 是 visionOS 26 新 scene modifier,让 spatial video 在系统环境(如 Mt. Hood)中也能渲染。

Comfort mitigation 事件。 APMP 高运动场景下系统会自动降低沉浸度,App 收到事件不需要做任何动作,但可以借此更新 UI 提示用户(21:40):

switch event.comfortMitigation {
case .reduceImmersion:
    // Default behavior
    break
case .play:
    // No action
    break
case .pause:
    // Show custom pause dialog
    break
}

关键点:

  • reduceImmersion 只在 progressive 渲染下生效,portal 渲染本身就够舒适,不会触发。
  • pause 是少见的场景,可以借此弹出自定义提示。
  • 事件只是信号,系统已经做完处理;UI 上是否反馈给用户由 App 决定。

核心启发

1. 按可控程度选框架,不要默认 RealityKit。 多数 App 想做的只是「让用户进沉浸态看视频」,AVKit 的 Expanded + Immersive 体验已经覆盖标准播放控件、转场动画和 comfort mitigation,写的代码比 RealityKit 少一个数量级。只有当视频要嵌进游戏世界,或者要叠加大量自定义 UI 时,才走 RealityKit 路线。

为什么值得做:减少自己维护渲染状态的成本,避免写错 mesh、scale、placement。 怎么开始:先用 AVPlayerViewController + experienceController 跑通一个 Immersive 转场,再决定是否需要 RealityKit。

2. APMP 视频用 progressive,不要用 full。 session 明确说 visionOS 26 起 progressive 是 APMP 和 AIV 的推荐模式,原因有两个:用户能用 Digital Crown 拨沉浸度(保持与外界连接),系统能在高运动时执行 reduceImmersion。

为什么值得做:full 模式拿不到 comfort mitigation,遇到高运动内容用户容易不适。 怎么开始:把 ImmersiveSpace 的 .immersionStyle 改成 .progressive(0.1...1, initialAmount: 1.0),VideoPlayerComponent 的 desiredImmersiveViewingMode 改成 .progressive,两边必须一起改。

3. 把 Quick Look 当作免费支持。 已经接入 PreviewApplicationQLPreviewController 的 App,在 visionOS 26 上不改代码就能预览 APMP、AIV、spatial photo/video。对没有播放器的 App(笔记、文档、聊天)来说这是零成本升级。

为什么值得做:用户拿到沉浸式素材时,不用导出到第三方 App 就能看。 怎么开始:检查 App 当前的预览实现是否走 Quick Look,没走的话花半天迁移过去。

4. 监听 ImmersiveViewingModeWillTransition,转场期间隐藏冲突 UI。 进/出沉浸态有动画时段,自定义 UI 如果一直显示,容易和视频运动冲突,造成立体噪声。在 WillTransition 里隐藏,DidTransition 里恢复。

为什么值得做:减小观看不适,提升体感品质。 怎么开始:在 RealityKit 视图里订阅这两个事件,配合 SwiftUI 的状态切换 UI 透明度。


关联 Session

评论

GitHub Issues · utterances