Vision Pro の サンプル解説 (3) - Destination Video
以下の記事が面白かったので、簡単にまとめました。
前回
1. はじめに
「Destination Video」は、visionOS / iOS / tvOS用のマルチプラットフォームの動画再生アプリです。このアプリは、サポートされているプラットフォームで同様のエクスペリエンスを提供しますが、visionOSの独自機能を活用することで、Immersiveな再生エクスペリエンスも可能になります。
2. インラインプレーヤーでの動画再生
ライブラリ内の動画を選択すると、詳細ビューが表示されます。「Play Video」ボタンで動画をインラインプレイヤーで再生、「Add Up Next」で「Up Next」リストへの追加ができます。
AVPlayerViewController を別のビューの子として表示すると、標準のインラインコントロールが表示されます。showsPlaybackControls に false を指定することで、コントロールなしで再生します。
3. フルウィンドウプレーヤーでの動画再生
visionOSには、「空間オーディオ」とともに「3D動画」を再生する機能があります。これにより視聴体験にさらに深いレベルの没入感が加わります。「3D動画」を再生するには、AVPlayerViewController をウィンドウ全体で表示する必要があります。ユーザーがコンテンツに集中できるように、合理化された再生コントロールが表示されます。
「Destination Video」の「ContentView」には、デフォルトでアプリのライブラリが表示されます。これは、アプリがインライン再生を要求するかフルウィンドウ再生を要求するかを示すプレーヤーモデルのプレゼンテーションプロパティの変更を監視します。プレゼンテーション状態が fullWindow に変わると、ビューはUIを再描画して、ライブラリの代わりにプレーヤー ビューを表示します。
struct ContentView: View {
/// ライブラリの選択パス
@State private var navigationPath = [Video]()
/// 現在ImmersiveSpaceを提示しているかどうか
@State private var isPresentingSpace = false
/// プレーヤー
@Environment(PlayerModel.self) private var player
var body: some View {
switch player.presentation {
case .fullWindow:
// プレーヤーをウィンドウ全体に表示し再生開始
PlayerView()
.onAppear {
player.play()
}
default:
// デフォルトでアプリのコンテンツライブラリを表示
LibraryView(path: $navigationPath, isPresentingSpace: $isPresentingSpace)
}
}
}
詳細ビューで「Play Video」ボタンをクリックすると、アプリはプレーヤーの loadVideo(_:presentation:) を fullWindow 呼び出します。
Button {
/// フルウィンドウでのプレゼンテーション用にメディアアイテムをロード
player.loadVideo(video, presentation: .fullWindow)
} label: {
Label("Play Video", systemImage: "play.fill")
}
プレーヤーは、再生する動画を正常にロードした後、そのプレゼンテーション値を fullWindow に更新します。
visionOS でフルウィンドウ プレーヤーを閉じるには、プレーヤーUIの「Back」ボタンをクリックします。 このアクションを処理するために、アプリの PlayerViewControllerDelegate は、解除を処理する AVPlayerViewControllerDelegate を定義します。
func playerViewController(_ playerViewController: AVPlayerViewController,
willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
// プレーヤーモデルの状態をリセット
player.reset()
}
デリゲートがこの呼び出しを受信すると、プレーヤーからメディアをクリアし、プレゼンテーション状態をデフォルト値にリセットします。その結果、「Destination Video」はライブラリビューを再表示します。
4. 空間オーディオ
メディア再生アプリでは、その機能とオーディオセッションの共通の構成が必要です。 「Configuring your app for media playback」の手順を実行することに加えて、新しい AVAudioSession で、ユーザーの空間オーディオ エクスペリエンスをカスタマイズします。
アプリが再生用の動画を正常に読み込んだ後、現在のプレゼンテーションの空間オーディオエクスペリエンスを設定します。インラインプレーヤービューの場合、ビューの位置からオーディオが発生する、小さく集中したサウンドステージにエクスペリエンスを設定します。動画をウィンドウ全体に表示すると、体験は大きく、完全に没入型のサウンドステージに設定されます。
/// ユーザーが意図した空間オーディオ体験をプレゼンテーションに最適に設定
/// パラメーターのプレゼンテーション: 要求されたプレーヤーのプレゼンテーション
private func configureAudioExperience(for presentation: Presentation) {
#if os(xrOS)
do {
let experience: AVAudioSessionSpatialExperience
switch presentation {
case .inline:
// トレーラーを見る時は、小さく集中したサウンドステージを設定
experience = .headTracked(soundStageSize: .small, anchoringStrategy: .automatic)
case .fullWindow:
// ウィンドウ全体を表示する場合は、サウンドステージサイズを大きく設定
experience = .headTracked(soundStageSize: .large, anchoringStrategy: .automatic)
}
try AVAudioSession.sharedInstance().setIntendedSpatialExperience(experience)
} catch {
logger.error("Unable to set the intended spatial experience. \(error.localizedDescription)")
}
#endif
}
5. ImmersiveSpace
visionOS用の動画再生アプリを構築すると、プレーヤーウィンドウの境界を超えて視聴エクスペリエンスを向上させる新たな機会が提供されます。より高いレベルの没入感を追加するために、サンプルでは、動画を見ている人の周囲のシーンを表示する「ImmersiveSpace」が表示されます。
struct DestinationVideo: App {
var body: some Scene {
// アプリのプライマリウィンドウ
WindowGroup {
ContentView()
}
// 動画を視聴する先を示すImmersiveSpaceを定義
ImmersiveSpace(for: Destination.self) { $destination in
if let destination {
DestinationView(destination)
}
}
// Immersion Styleを.progressiveに設定すると、ユーザーは Digital Crown を使用して体験を調整できるようになる
.immersionStyle(selection: .constant(.progressive), in: .progressive)
}
}
「ImmersiveSpace」には DestinationView が表示され、ユーザーの周囲に表示される球体の内部にテクスチャがマップされます。このアプリは、「Progressive Immersion Style」を使用してそれを表示します。これにより、デジタルクラウンを回すことで没入度を変更できます。
「Destination Video」アプリは、ユーザーが動画の詳細ビューに移動すると「ImmersiveSpace」を自動的に表示し、ライブラリに戻るとその空間を閉じます。アプリは、ナビゲーションパスを監視してナビゲーションイベントがいつ発生するかを判断し、空間を表示または非表示にします。
.onChange(of: navigationPath) {
Task {
// ユーザーがメインライブラリウィンドウに戻ると選択パスは空になる
if navigationPath.isEmpty {
if isSpaceOpen {
// 空間を閉じてその人を現実世界の空間に戻す
await dismissSpace()
isSpaceOpen = false
}
} else {
// NavigationPath には1つの動画があるか、空
guard let video = navigationPath.first else { fatalError() }
// 宛先を開くリクエストを待ち、それに応じて状態を設定
switch await openSpace(value: video.destination) {
case .opened: isSpaceOpen = true
default: isSpaceOpen = false
}
}
}
}
6. 共有視聴体験
アプリの再生エクスペリエンスを向上させる最良の方法の1つは、そのエクスペリエンスを他のユーザーと共有することです。 AVFoundation と Group Activities で、同じ場所にいないユーザーを結び付ける SharePlay エクスペリエンスを構築できます。
「Destination Video」アプリは、デバイスやプラットフォームを超えて他のユーザーと一緒に動画を視聴できるエクスペリエンスを作成します。これは、VideoWatchingActivity という Group Activitiesを定義します。FaceTime通話がアクティブで、フルウィンドウプレーヤーで動画を再生すると、その動画は通話中の全員が再生できるようになります。
アプリの VideoWatchingCoordinator アクターは、「Destination Video」の 「SharePlay」を管理します。 新しい VideoWatchingActivity セッションのアクティブ化を監視し、セッションが開始されると、プレーヤー オブジェクトの AVPlaybackCoordinator に GroupSession インスタンスを設定します。
private var groupSession: GroupSession<VideoWatchingActivity>? {
didSet {
guard let groupSession else { return }
// AVPlayerの再生コーディネーターにグループセッションを設定して、他のデバイスと再生を同期
playbackCoordinator.coordinateWithSession(groupSession)
}
}
プレーヤーがグループ セッションを使用するように構成されている場合、アプリが新しいビデオを読み込むと、FaceTime 通話で他のユーザーと動画を共有できるようになります。
次回
この記事が気に入ったらサポートをしてみませんか?