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 という 3 つのフレームワークが、同一の immersive video 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 になると、アプリ側で RealityKit のレンダリングをほぼ自前実装するしかありませんでした。Quick Look はフォーマットを認識せず、AVKit には immersive 状態への切替 API が無く、各チームが車輪の再発明を繰り返していたのです。

visionOS 26 ではこれらが一箇所に集約されます。Jamal と Michael はセッションの中で、同じ profile セットを 3 つのフレームワークに接続して見せました。Quick Look はコードを書かずにプレビューでき、AVKit は標準の再生コントロールと immersive 遷移を提供し、RealityKit はゲーム内への埋め込みやカスタム UI 構築に使えます。3 つの選び分けの基準は明快で、必要な制御度が高い順に RealityKit > AVKit > Quick Look となります。

API レイヤに落とし込むと、AVKit には AVExperienceController が導入され、Expanded と Immersive という 2 つの新体験と Delegate Protocol が用意されました。RealityKit では VideoPlayerComponentdesiredImmersiveViewingMode (portal/progressive/full) と desiredSpatialVideoMode (spatial/screen) が追加されています。Quick Look は PreviewApplicationQLPreviewController で新 profile を自動的にサポートし、すでに統合済みのアプリはコードを変更せずに利用できます。さらにシステムは APMP コンテンツに対して自動的に comfort mitigation を行い、激しい動きを検知すると immersive 度合いを下げます。


詳細

AVKit の Expanded 構成: immersive コンテンツを portal 形式で表示する。 Expanded は UIScene 全体を占有する体験です。visionOS 26 では AutomaticTransitionToImmersive が追加され、デフォルトの .default では自動的に immersive 状態に遷移し、.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: システムによる immersive への自動遷移を拒否し、AVPlayerViewController に portal で immersive コンテンツをレンダリングさせます。
  • await experienceController.transition(to: .expanded): Expanded への遷移は非同期で行われ、完了してはじめて状態の安定が保証されます。

Immersive 体験への明示的な遷移。 自動トリガに頼らず、いつ 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:) で immersive コンテンツがどの window scene の上に重なるかを必ず指定します。
  • すでに hierarchy 内にある場合は placement を省略でき、AVExperienceController が所属する scene をターゲットとして利用します。
  • transition(to: .immersive) を呼ぶとシステムがアニメーションを引き受け、自然に immersive 状態へ遷移します。

Delegate の 3 メソッドが転換に応答する。 遷移はユーザー、アプリのロジック、システムのいずれからも発火しうるため、3 つのコールバックすべてを押さえる必要があります (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 で immersive 度合いを調整する。 APMP と Apple Immersive Video は visionOS 26 で full ではなく progressive の利用が推奨されます。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.progressivedesiredImmersiveViewingMode = .progressive とセットで使う必要があり、両者の型は一致させます。
  • 範囲は 0...1 ではなく 0.1...1 にします。ユーザーが immersive 度合いを 0 まで下げてコンテンツが消えてしまうのを避けるためです。
  • initialAmount: 1.0 は full immersion と同等で、起動時から完全に immersive な状態になります。

Spatial video の特殊な扱い: immersive レンダリングは head-locked ではない。 Spatial video を portal で表示する場合、VideoPlayerComponent の mesh はデフォルトで高さ 1m なので、scale 0.4 でウィンドウに収めます。Immersive full に切り替えるとサイズはコンポーネント側が制御しますが、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 アプリと同じ見た目になります。.screen に切り替えると従来の立体レンダリングに戻ります。
  • desiredImmersiveViewingMode = .full: spatial video の immersive レンダリングは progressive ではなく full のみサポートします。
  • videoEntity.position = [0, 1.5, -1]: 床から 1.5m、前方 1m の位置です。より安定させたいなら head anchor を使って観察者の正面に固定するのが良いでしょう。

Spatial video をシステム環境内でもレンダリングする。 .mixed immersion style と .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 の上に重ねるため、.progressive.full ではなく .mixed を使います。
  • .immersiveEnvironmentBehavior(.coexist) は visionOS 26 で追加された scene modifier で、spatial video をシステム環境 (Mt. Hood など) の中でもレンダリングできるようにします。

Comfort mitigation のイベント。 APMP の動きが激しいシーンでは、システムが自動的に immersive 度合いを下げます。アプリはイベントを受け取っても何もしなくて構いませんが、これを契機に 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 上ユーザーにフィードバックするかどうかはアプリ側の判断です。

重要な示唆

1. 制御度でフレームワークを選ぶ。デフォルトで RealityKit にしない。 多くのアプリがやりたいのは「ユーザーを immersive 状態に連れて行って動画を見せる」ことだけで、AVKit の Expanded + Immersive 体験はすでに標準の再生コントロール、遷移アニメーション、comfort mitigation までカバーしています。書くコードの量は RealityKit より一桁少なくなります。動画をゲームの世界に埋め込んだり、大量のカスタム UI を重ねる必要があるときだけ RealityKit ルートを選びましょう。

なぜ価値があるか: レンダリング状態を自分で管理するコストを減らし、mesh、scale、placement の書き間違いを避けられます。 どう始めるか: まず AVPlayerViewController + experienceController で Immersive 遷移を一通り動かし、それから RealityKit が必要かを判断します。

2. APMP 動画には progressive を使い、full は使わない。 セッションでは visionOS 26 以降 progressive が APMP と AIV の推奨モードであると明言されています。理由は 2 つで、ユーザーが Digital Crown で immersive 度合いを調整できる (外界とのつながりを保てる) こと、システムが激しい動きの場面で reduceImmersion を実行できることです。

なぜ価値があるか: full モードでは comfort mitigation が得られず、激しい動きのコンテンツに当たるとユーザーが不快を感じやすくなります。 どう始めるか: ImmersiveSpace の .immersionStyle.progressive(0.1...1, initialAmount: 1.0) に、VideoPlayerComponent の desiredImmersiveViewingMode.progressive に変更します。両者は必ずセットで変える必要があります。

3. Quick Look は無料サポートとして取り込む。 すでに PreviewApplicationQLPreviewController を組み込んでいるアプリは、visionOS 26 上ではコードを変更せずに APMP、AIV、spatial photo/video をプレビューできます。プレイヤーを持たないアプリ (メモ、ドキュメント、チャット) にとっては、ゼロコストのアップグレードです。

なぜ価値があるか: ユーザーが immersive 素材を受け取った際、サードパーティアプリにエクスポートしなくても閲覧できるようになります。 どう始めるか: アプリの現在のプレビュー実装が Quick Look 経由かを確認し、そうでなければ半日かけて移行します。

4. ImmersiveViewingModeWillTransition を購読し、遷移中に競合する UI を隠す。 Immersive 状態の出入りにはアニメーション期間があり、カスタム UI を出しっぱなしにしておくと動画の動きとぶつかって立体ノイズが発生しやすくなります。WillTransition で隠し、DidTransition で戻しましょう。

なぜ価値があるか: 視聴の不快感を減らし、体感品質を引き上げられます。 どう始めるか: RealityKit のビューでこの 2 つのイベントを購読し、SwiftUI の状態と連動させて UI の透明度を切り替えます。


関連 Session

コメント

GitHub Issues · utterances