Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

visionOS向け空間ビデオプレイヤーを実装してみた② ~ VideoPlayerComponent での空間ビデオ再生 ~

1. はじめに

みなさん、こんにちは! 株式会社ヤプリでiOSエンジニアをしています白数 (@cychow_app) です。

前回の記事では、空間ビデオプレイヤーを構築する上で、まず空間ビデオとは何なのか、どのようなメタデータを保持しているのかに焦点を当てて解説しました。
もしご興味がありましたら、前回の記事も一読いただけると嬉しいです。

tech.yappli.io

今回は、本題の空間ビデオのプレイヤーの実装についてご紹介していければと思います。

2. 空間ビデオ再生プレイヤーの実装

2.1 VideoPlayerComponent と視聴モード

再生プレイヤーを実装していく前に、まずどのようなコンポーネントを使用していくかについて見ていきたいと思います。
今回メインで使用していくのが、RealityKit内部で提供されている VideoPlayerComponent です。

developer.apple.com

VideoPlayerComponent は、iOSアプリでも使用する AVPlayer を、visionOSのEntityに対してつなぎ込む役割を担っています。
このComponent自体は、visionOS 1.0+から利用することができるため、手軽にvisionOS向けの動画再生プレイヤーを実装することができます。

また、空間ビデオを再生するにあたり、重要となってくるものが VideoPlayerComponent.ImmersiveViewingModeVideoPlayerComponent.SpatialVideoMode です。

ImmersiveViewingMode

VideoPlayerComponent.ImmersiveViewingMode は、没入体験用のメディアコンテンツを「どの見せ方で再生するか」を指定する列挙型です。
ImmersiveViewingMode のパターンは「portal」、「full」、「progressive」の3つとなっています。

  • portal:没入体験用のメディアコンテンツを「窓」や「ポータル」のように表示するモード。
  • full:視野全体を覆う没入表示モード。没入表示として最も強い見せ方で、完全にコンテンツ側へ入り込む体験になります。
  • progressive:段階的に没入度を深めるモード。Apple Vision Pro上部の Digital Crown で没入度を調整でき、100% では full 相当になります。(ただし 空間ビデオでは通常 progressive は使用しません。)

SpatialVideoMode

続いて VideoPlayerComponent.SpatialVideoMode は、空間ビデオを「通常の立体スクリーン映像として出すか」それとも「空間ビデオらしい奥行き表現付きで出すか」を指定する列挙型です。 パターンは「screen」、「spatial」の2つがあります。

  • screen:空間ビデオを、従来型のステレオビデオをスクリーンに貼る形で表示するモード。つまり、「空間ビデオ特有の奥行きのある映像体験」を無効としているモードとなっています。また、このモードでは ImmersiveViewingMode は効きません。
  • spatial:「空間ビデオ特有の奥行きのある映像体験」を有効にするモード。このモードでは ImmersiveViewingMode の portal / full が利用可能となります。

2.2 いざ実装へ

今回は「空間ビデオ特有の奥行きのある映像体験」をより感じることができる、「窓」や「ポータル」のように表示するportalモードと、完全にコンテンツ側へ入り込むことができるfullモードの2つの再生プレイヤーを構築していきます。
以下の実装ポイントに焦点を当てつつ、実際のソースコードを用いながら解説していければと思います。

【実装ポイント】
① Immersiveモード起動時の VideoPlayerComponent をEntityに対して設定する方法
VideoPlayerComponent の生成と ImmersiveViewingMode 及び SpatialVideoMode の指定方法
③ ビデオプレイヤーのモード変更などを検知する VideoPlayerEvents について

① Immersiveモード起動時の VideoPlayerComponent をEntityに対して設定する方法

まずは、「Immersiveモード起動時の VideoPlayerComponent をEntityに対して設定する方法」です。
はじめに ImmersiveSpace(id:) 部分と、その内部について見ていきたいと思います。

@main
struct EntryPoint: App {
    @State private var appModel = AppModel()

    var body: some Scene {
        ...

        ImmersiveSpace(id: appModel.immersiveSpaceID) {
            ImmersiveView()
                .environment(appModel)
                .onAppear {
                    appModel.immersiveSpaceState = .open
                }
                .onDisappear {
                    appModel.immersiveSpaceState = .closed
                }
        }
        ...
    }
}

今回の場合ですと、 ImmersiveSpace 内部で ImmersiveView というViewを新たに定義しています。
ImmersiveView 内部では、 RealityKit の RealityView で構成されています。さらにその内部で、 ImmersiveSpace 内部で設置する空間ビデオ再生用のEntityを定義していきます。

import RealityKit
import SwiftUI

struct ImmersiveView: View {
    @Environment(AppModel.self) private var appModel

    @State private var videoEntity: Entity?
    
    ...

    var body: some View {
        RealityView { content in
            ...

            let videoEntity = Entity()
            videoEntity.position = [0, 1.35, -1.8]
            videoEntity.components[VideoPlayerComponent.self] = appModel.makeVideoPlayerComponent()
            content.add(videoEntity)

            self.videoEntity = videoEntity
            
            ...
        } 
        
        ...
    }
    ...
}

これにより、VideoPlayerComponent を適用するためのEntityが生成され、Immersiveモード起動時にSpace上に再生画面を表示するようになります。
次に、再生画面の詳細設定について見ていきます。詳細の設定は appModel.makeVideoPlayerComponent() 部分で設定しています。

@MainActor
@Observable
class AppModel {
    ...
    
    func makeVideoPlayerComponent() -> VideoPlayerComponent {
        preparePlayerIfNeeded()
        var component = VideoPlayerComponent(avPlayer: player)
        ...
        return component
    }
    ...
}

上記のコードを見ていても分かる通り、ここで VideoPlayerComponent を呼び出しています。

② VideoPlayerComponent の生成と ImmersiveViewingMode 及び SpatialVideoMode の指定方法

続いて、2つ目のポイントである「 VideoPlayerComponent の生成と ImmersiveViewingMode 及び SpatialVideoMode の指定方法」についてです。 makeVideoPlayerComponent() 内で生成した VideoPlayerComponent に対して、以下の設定を追加していきます。

func makeVideoPlayerComponent() -> VideoPlayerComponent {
    preparePlayerIfNeeded()
    var component = VideoPlayerComponent(avPlayer: player)
    component.desiredImmersiveViewingMode = .full
    component.desiredSpatialVideoMode = .spatial
    return component
}

新たに追加した desiredImmersiveViewingMode 及び desiredSpatialVideoMode の設定項目についてそれぞれ見ていきたいと思います。

まず desiredImmersiveViewingMode は、没入体験用のメディアコンテンツを「どの見せ方で再生するか」を指定するプロパティとなっています。 つまり、冒頭で説明した VideoPlayerComponent.ImmersiveViewingMode のパターンを設定する箇所となっています。

次にdesiredSpatialVideoMode は、空間ビデオを「通常の立体スクリーン映像として出すか」それとも「空間ビデオらしい奥行き表現付きで出すか」を指定するプロパティとなっています。
つまり、 VideoPlayerComponent.SpatialVideoMode のパターンを設定する箇所です。

上記では、 desiredImmersiveViewingMode に .full 、desiredSpatialVideoMode に .spatial が設定されているため、下記のような見え方で空間ビデオが再生されます。
(本来は没入感があり、また奥行きのある映像コンテンツが再生されているのですが、実際にApple Vision Proを装着して見ないと伝わりづらい...)

また、 desiredImmersiveViewingMode には .portal 、desiredSpatialVideoMode には .spatial が設定されている場合は下記のような見え方で空間ビデオが再生されます。

上記の2つのポイントをおさえておくと、比較的簡単に空間ビデオの再生画面を実装することができます。

③ ビデオプレイヤーのモード変更などを検知する VideoPlayerEvents について

そして、3つ目のポイントとして挙げている「ビデオプレイヤーのモード変更などを検知する VideoPlayerEvents について」も最後にご紹介したいと思います。
VideoPlayerEvents は、主に VideoPlayerComponent に何らかの変更が発生した際に、それを検知するためのイベント群です。

Event 概要
ImmersiveViewingModeWillTransition VideoPlayerComponent の Immersive モードが .full へ切り替わる前に飛んでくるイベント
ImmersiveViewingModeDidChange VideoPlayerComponent.ImmersiveViewingMode.full.portal.progressive のいずれかへ変更されたタイミングで飛んでくるイベント
ImmersiveViewingModeDidTransition VideoPlayerComponent の Immersive モードが .full への切り替えを完了したときに飛んでくるイベント
RenderingStatusDidChange VideoPlayerComponent のレンダリング状態が変化したときに飛んでくるイベント
VideoComfortMitigationDidOccur 没入型ビデオの再生中に、システム側の Comfort Mitigation が発動したことを知らせるイベント
ContentTypeDidChange VideoPlayerComponent が扱っているビデオの種類が変わったときに飛んでくるイベント
ViewingModeDidChange VideoPlayerComponent.ViewingMode が変更されたときに飛んでくるイベント
VideoSizeDidChange VideoPlayerComponent が表示しているビデオのサイズ情報が変わったときに飛んでくるイベント
SpatialVideoModeDidChange VideoPlayerComponent.SpatialVideoMode が変更されたときに飛んでくるイベント

上記のEventを下記のように購読することで、検知できます。
これによって、 VideoPlayerComponent に対する様々な変更を検知し、それに応じて簡単に適切な処理を実行することができます。

struct ImmersiveView: View {
    ...
    
    var body: some View {
        RealityView { content in
            ...
            _ = content.subscribe(to: VideoPlayerEvents.ImmersiveViewingModeDidChange.self, on: videoEntity) { event in
                Task { @MainActor in
                    appModel.actualPlayerImmersiveMode = event.currentMode
                }
            }
        }
        ...
    }
    ...
}

3. まとめ

今回は、 VideoPlayerComponent を使って visionOS 上で空間ビデオを再生するための基本的な実装方法を見てきました。
やること自体はそこまで複雑ではなく、 ImmersiveSpace 内で Entity を用意し、その Entity に VideoPlayerComponent を設定することで、空間ビデオ再生の土台を作ることができます。

また、 desiredImmersiveViewingModedesiredSpatialVideoMode を組み合わせることで、「ポータルっぽく見せるのか」「しっかり没入させるのか」「空間ビデオとして奥行きを有効にするのか」といった体験を切り替えられるのも、 VideoPlayerComponent の分かりやすいポイントです。
空間ビデオ再生というと少し身構えてしまいますが、まずは .portal.full を試しながら挙動の違いを見ていくと、かなり理解しやすいと思います。

さらに、実装してみると意外と大事なのが VideoPlayerEvents です。
Immersive モードの変化だけでなく、レンダリング状態、 SpatialVideoMode 、コンテンツ種別などもイベントとして拾えるため、「今プレイヤーがどういう状態なのか」をアプリ側で把握しやすくなります。
このあたりを押さえておくと、表示切り替えに合わせたUI更新や状態管理もしやすくなり、プレイヤー実装がかなり組み立てやすくなります。

空間ビデオは見た目のインパクトが強い一方で、実装としては AVPlayerRealityKit をうまくつないでいく感覚に近いので、visionOSアプリ作成の入口としてもかなり触りやすい内容になっているかなと思います。
この記事が、これから空間ビデオ再生に触れてみたい方の最初の一歩になれば嬉しいです。