-
NowPlayingフレームワークについて
NowPlayingは、ロック画面、コントロールセンター、Dynamic Island、CarPlayなどのシステム上の表示領域とアプリのメディア再生を連係させる、Swiftフレームワークです。このフレームワークのオブザーブ可能なAPIを使用して、再生状態を公開し、コマンドに応答する方法を解説します。リモート再生セッションという新機能を使用すると、外部デバイス上で再生されているメディアをアプリで表示できます。同じシステムサーフェス上ですべての再生コントロールも利用できます。
関連する章
- 0:00 - Introduction
- 1:08 - Media sessions
- 5:03 - Remote media sessions
- 10:31 - Media sharing extensions
リソース
-
このビデオを検索
こんにちは Leo Formaggioです Media Frameworkチームの エンジニアです メディアは日常生活において 非常に重要な存在です 帰り道のポッドキャストです ワークアウト中の ハイエナジーなプレイリストです 長いフライト中の映画です 一人で過ごすときも 人とつながるときも メディアは常に身近にあります iPhoneではロック画面に 表示されます コントロールセンターとDynamic Islandにも 表示されます iPhoneを置いて充電すると スタンバイでひと目で確認できます 車に乗るとCarPlayの メイン画面に表示されます これはシステムのnow-playing体験で 全Appleプラットフォームで利用できます Apple Watch Apple Vision Proや Apple TVも対応しています NowPlayingフレームワークを使って アプリのメディアをシステムへ 簡単に統合する方法を紹介します
まずmedia sessions APIから始めます コンテンツをシステムの now-playing体験に表示する方法です
次に他のデバイスで再生中の コンテンツをシステムへ統合する方法を remote media sessionsを使って 説明します
最後にMedia Sharing Extensionが iPhoneから他のデバイスへの メディア再生を簡素化する仕組みです 開発したアプリの例を使って 説明します 集中やリラックスに役立つ 環境音を再生するアプリです アプリからさまざまな音を選択でき 音声の一時停止と再開も可能です コンテンツをシステムに表示するため NowPlayingのmedia sessions APIを 使いました 実装方法をご紹介します こちらがPlayerModelです アプリのオーディオエンジンへの 参照を持つ@Observableクラスで 現在再生中の音を追跡する プロパティも含まれます MediaSessionRepresentableプロトコルは アプリとシステムの間の 契約のようなものです PlayerModelがこれに準拠すると システムはアプリの再生内容と スキップや一時停止などの メディア操作の処理方法を認識できます 各セッション表現には 一意の識別子が必要です contentプロパティは 再生中の音を記述します NowPlayingはMusic Podcastや MovieContentなどの型を提供し アプリが再生するメディアの種類を 表すためのものです 自分のユースケースには GenericContentが最適でした 各コンテンツはcurrent sound.idで 識別されます sound.nameをコンテンツタイトルとして sound.descriptionを サブタイトルとして使いました メディアタイプは.audioか.videoで 私のアプリは音声のみです durationは.continuousに設定しました アプリが環境音を 無限に再生するためです 非同期クロージャを持つ Artworkを提供します システムが特定サイズの画像を 必要とするときに呼ばれます ロック画面での 表示を見てみましょう アートワークとともに音の名前と 説明が表示されます
PlaybackSnapshotプロパティは 現在の再生状態を表示します 環境音は連続再生なので isPlayingで状態を示すだけです 再生時間が定義されたコンテンツでは スナップショットにelapsedTime パラメータも指定します commandsプロパティで アプリがサポートするアクションを定義します 各コマンドにはクロージャがあり ユーザーがそのアクションを実行すると システムが呼び出します 例えばロック画面の 一時停止ボタンをタップすると pauseコマンドのクロージャが呼ばれ プレイヤーを一時停止できます ボタンが変わって 一時停止状態が反映されます iPhoneで再生ボタンをタップすると playコマンドのクロージャが呼ばれ プレイヤーを再開します 同様にロック画面の 次へボタンをタップすると nextコマンドのクロージャが呼ばれます 次の音にスキップでき ロック画面が新しいコンテンツで 更新されます
MediaSessionRepresentableを採用した後 コンテンツをシステムに公開するために もう一つ必要なことがありました MediaSessionはセッション表現と システムを接続するものです PlayerModelを渡して初期化し オーディオエンジンを セットアップする場所に配置します 完了するとMediaSessionが モデルの監視を開始し now-playingサーフェスを 自動的に最新の状態に保ちます これがmedia sessionsを使って アプリのコンテンツをシステムの now-playing体験に統合した方法です 詳細についてはApple Developer Documentationの 「Publishing Media Sessions」を ご覧ください
iPhoneでの再生に加えて オーディオエンジンを スマートスピーカーに対応させ アプリから制御できるようにしました アプリのデバイス選択メニューから 制御したいスピーカーを選びます をタップして 制御を開始します Webサーバを通じて アプリは選択したスピーカーに接続し 再生状態を取得したり コマンドを送信します そのスピーカーの再生コンテンツを システムに表示するために remote media sessions APIを使いました このAPIはapp extensionと プッシュ通知を使って スピーカーの更新を受け取ります このやり取りの流れを 見ていきましょう 誰かがスピーカーを操作すると スピーカーが状態の変化を サーバに通知します サーバはApple Push Notification service(APNs)を使って 更新された状態をプッシュ通知で iPhoneに送信します システムは更新された状態と共に app extensionを起動し プッシュ通知のペイロードから 取得します app extensionはシステムに対して そのセッションの更新された表現を 提供します APNsによるプッシュ通知送信の 詳細については 「Setting up a remote notification server」という記事の developer.apple.comを ご確認ください iPhoneシステムUIからの 操作の場合 システムはapp extension内の コマンドハンドラを呼び出します app extensionがコマンドを サーバに送信します サーバがスピーカーに通知し スピーカーが変化に反応します
アプリへのremote media sessionsの 採用方法を紹介します まずapp extensionを作成しました RemoteMediaSessionExtensionプロトコルに 準拠します セットアップにはNowPlayingの RemoteMediaSessionExtensionConfigurationを使い remote-mediaの extensionPoint識別子も使いました session(:)メソッドはシステムが 操作する必要があるたびに呼ばれ リモートセッション表現に対して UIの更新や操作の処理など さまざまな場面で使われます ここではRemotePlayerStateを使って モデルを作成して返します app extensionを設定したら モデルを使ってRemote Media Sessionを 表現する方法を紹介します こちらがRemotePlayerModelです ServerClientへの参照を持つ @Observableクラスで サーバとの通信に使う クラスです サーバの状態も追跡します これを基盤にRemote Media Session 表現を構築します 各Remote Media Session表現には 一意の識別子が必要です サーバ状態のsessionIDを 使いました contentプロパティはスピーカーで 再生中の音を記述します 今回もGenericContentを使い 音の識別子を渡します 音の名前と説明も渡します メディアタイプは.audioで durationは.continuousです 現在の音の画像を読み込む Artworkオブジェクトを提供します
サーバ状態でスピーカーの isPlayingを確認できます それを使って対応する状態の PlaybackSnapshotを作成します リモートデバイスの再生を 制御しているため 各コマンドクロージャは対応する アクションをサーバに送信します iPhoneで再生をタップすると playコマンドのクロージャが呼ばれます サーバにplayリクエストを送り スピーカーの再生を再開します 同様に次へボタンをタップすると リクエストがサーバに送られ スピーカーが次の音に移ります
RemoteMediaSessionRepresentableの 採用はここまで ローカル再生のmedia sessionsと 非常に似た感覚です 次に残りのプロパティとメソッドの リモートセッション固有の内容を 説明します devicesプロパティはそのセッションで 再生中のデバイスをシステムに伝えます サーバのデバイスリストを MediaDevice値にマッピングします 各デバイスには異なるセッション間で 安定した一意の識別子が必要です デバイス名とデバイスタイプを指定します 私の場合は.speakerです デバイスの音量制御タイプなど 機能のリストも指定します コントロールセンターでの 表示がこちらです デバイス名と音量レベルが 表示されます
システムの音量スライダーで 音量を変更すると volume changeクロージャが更新された 音量レベルと共に呼ばれます ここでサーバに volume changeリクエストを送れます
update(:)関数は 新しい状態のプッシュ通知を 受信したときに呼ばれます スピーカーのコンテンツが 変わった場合などです RemotePlayerStateは私が定義した structで RemoteMediaSessionAttributesに 準拠します サーバ状態と プッシュ通知のペイロードを表します ここで状態変数を 新しいデータで更新します モデルがobservableなため NowPlayingが変更を検出し システムを自動的に更新します これがアプリのremote media sessionを システムに統合した方法です 詳細については 「Publishing remote media sessions」の 記事をご覧ください Media Sharing Extensionについても お話ししたいと思います iPhoneから他のスピーカーやTVへ メディアを再生するAPIセットで 統一されたシステムインターフェース を通じて利用できます Media Sharing Extensionを使うと システムデバイスピッカーを利用でき アプリがサポートする すべてのメディアプロトコルに対応します アプリ内のメディアデバイス 選択が簡素化されます コントロールセンターなどの システムサーフェスにも反映されます
従来はメディアプロトコルに 対応するために SDKをアプリバンドルに 組み込む必要がありました Media Sharing Extensionでは プロトコル実装がアプリの 外部に存在し システムが管理します アプリは再生技術ではなく メディアコンテンツに集中できます
新しいプロトコルが利用可能になっても Media Sharing Extensionで構築した アプリは追加SDKなしで使えます
ローカルとリモートのmedia sessionsを NowPlayingフレームワークでシステムの now-playing体験に統合する方法と Media Sharing Extensionで他のデバイスへの メディア送信を簡素化する方法を解説しました ローカルでメディアを再生したり リモートデバイスを制御するアプリには NowPlayingを採用してコンテンツを ロック画面へ コントロールセンターとそれ以上へ 届けましょう シンプルな統合により メディアの制御が可能になり アプリ外でも使えます Media Sharing Extensionについて 詳しく学びましょう システムのメディアデバイスピッカーを アプリで使えるようになり 他のデバイスへのメディア再生で リーチを広げられます Media Sharing Extensionの 詳細については 「Routing media to third-party devices」 の記事をご覧ください アプリがシステムの now-playing体験に広がるのを楽しみにしています ご視聴ありがとうございました Blue skies!
-
-
1:57 - Existing PlayerModel implementation
import Observation @Observable final class PlayerModel { let player: SoundPlayer var sound: Sound { player.currentSound } init(player: SoundPlayer) { self.player = player } } -
2:06 - Adopt MediaSessionRepresentable
import NowPlaying extension PlayerModel: MediaSessionRepresentable { var id: String { "ambient-sound-session" } var content: (any MediaContentRepresentable)? { return GenericContent( id: sound.id, title: sound.name, subtitle: sound.description, type: .audio, duration: .live, artwork: Artwork(id: sound.id) { size in let data = try await self.artworkData(size: size) return try ArtworkRepresentation(data: data) } ) } var playbackSnapshot: MediaPlaybackSnapshot? { MediaPlaybackSnapshot( state: player.isPlaying ? .playing() : .paused ) } var commands: [MediaCommand] {[ .play { self.player.play() }, .pause { self.player.pause() }, .previous { self.player.previous() }, .next { self.player.next() } ]} } -
4:31 - MediaSession initialization
import NowPlaying struct PlayerController { let player: SoundPlayer let model: PlayerModel let session: MediaSession<PlayerModel> init() { self.player = SoundPlayer() self.model = PlayerModel(player: player) self.session = MediaSession(model) } } -
6:42 - App extension entry point
import ExtensionFoundation import NowPlaying @main final class SampleAppExtension: @MainActor RemoteMediaSessionExtension { var configuration: some AppExtensionConfiguration { RemoteMediaSessionExtensionConfiguration(extension: self) } var extensionPoint: AppExtensionPoint { AppExtensionPoint.Identifier(host: "com.apple.nowplaying", name: "remote-media") } func session(_ state: RemotePlayerState) async throws -> RemotePlayerModel { RemotePlayerModel(state: state) } } -
7:23 - Existing RemotePlayerModel implementation
import Observation @Observable @MainActor final class RemotePlayerModel { let client: ServerClient var state: RemotePlayerState init(state: RemotePlayerState) { self.client = ServerClient(sessionID: state.sessionID) self.state = state } } -
7:40 - Adopt RemoteMediaSessionRepresentable in app extension
import NowPlaying extension RemotePlayerModel: @MainActor RemoteMediaSessionRepresentable { var id: String { state.sessionID } var content: (any MediaContentRepresentable)? { GenericContent( id: state.sound.id, title: state.sound.name, subtitle: state.sound.description, type: .audio, duration: .live, artwork: Artwork(id: state.sound.id) { size in let data = try await self.artworkData(size: size) return try ArtworkRepresentation(data: data) } ) } var playbackSnapshot: MediaPlaybackSnapshot? { MediaPlaybackSnapshot( state: state.isPlaying ? .playing() : .paused ) } var commands: [MediaCommand] {[ .play { try await self.client.send(.play) }, .pause { try await self.client.send(.pause) }, .previous { try await self.client.send(.previous) }, .next { try await self.client.send(.next) } ]} var devices: [MediaDevice] { state.devices.map { device in MediaDevice( id: device.id, name: device.name, type: .speaker, capabilities: [ .absoluteVolume(device.volume) { volume in // send volume change to server } ] ) } } func update(_ state: RemotePlayerState) { self.state = state } }
-
-
- 0:00 - Introduction
Discover the Now Playing system experience, available across all Apple platforms. It allows apps to surface currently playing media info on system surfaces like the Lock Screen, Dynamic Island, and CarPlay.
- 1:08 - Media sessions
Learn how to use the media sessions API to bring audio or video from your app into the system's Now Playing experience by adopting the MediaSessionRepresentable protocol.
- 5:03 - Remote media sessions
Discover how to extend playback control to devices like smart speakers by adopting RemoteMediaSessionRepresentable and utilizing Apple Push Notification service (APNs).
- 10:31 - Media sharing extensions
Find out how Media Sharing Extensions simplify routing media from iPhone to other devices by leveraging the system device picker without needing to embed additional SDKs.