WWDC Quick Look 💓 By SwiftGGTeam
Integrate your media app with HomePod

Integrate your media app with HomePod

元の動画を見る

ハイライト

iOS 17 により、HomePod ユーザーは Siri 経由で iPhone や iPad にインストールされたメディアアプリのコンテンツを直接再生できます。既存の SiriKit Media Intents 対応アプリはコード変更なしで利用可能。システムは Wi-Fi 経由でリクエストをユーザーのデバイスにルーティングし、アプリ起動後に AirPlay で HomePod にコンテンツをストリーミングします。

主要内容

動作の仕組み

ユーザーが HomePod に再生を依頼すると、フローは次のとおりです:

  1. HomePod が音声リクエストを処理し、SiriKit Intent を生成
  2. Intent が Wi-Fi 経由でユーザーの iPhone(プライマリデバイス)に送信
  3. iPhone がアプリを起動し、再生リクエストを処理
  4. 音声が AirPlay 経由で HomePod にストリーミング

デバイスは同一 Wi-Fi ネットワーク上である必要がありますが、物理的な近接は不要です。リビングの HomePod に話しかけても、寝室で充電中の iPhone で動作します。

00:50

対応メディアタイプとリクエスト

対応メディアタイプには音楽、オーディオブック、ポッドキャスト、ラジオ、瞑想などがあります。Play、Add、Affinity(お気に入り/プレイリスト追加)リクエストはすべてプライマリデバイスにルーティングされます。

リクエスト例:

  • “Play jazz on Control Audio” — ジャンル + アプリ名
  • “Play Push It by Dukes on Control Audio” — 曲名 + アプリ名
  • “Play my audiobook” — 前回の再生を再開
  • “Play the latest news” — ポッドキャスト/ニュース系リクエスト
  • “Add this to my playlist” — プレイリストに追加
  • “I like this song” — お気に入りにマーク

02:15

SiriKit Media Intents の実装

import Intents

class PlayMediaIntentHandler: NSObject, INPlayMediaIntentHandling {
    func handle(intent: INPlayMediaIntent, completion: @escaping (INPlayMediaIntentResponse) -> Void) {
        // リクエストの具体的な内容を確認
        if let mediaName = intent.mediaSearch?.mediaName,
           mediaName == "Push It" {
            let mediaItem = INMediaItem(
                identifier: "song-push-it",
                title: "Push It",
                type: .song,
                artwork: nil
            )
            let response = INPlayMediaIntentResponse(
                code: .success,
                userActivity: nil
            )
            response.mediaItems = [mediaItem]
            completion(response)
            return
        }
        
        // 未対応のメディアタイプかどうかを確認
        if intent.mediaSearch?.mediaType == .music {
            let response = INPlayMediaIntentResponse(
                code: .failureRequiringAppLaunch,
                userActivity: nil
            )
            completion(response)
            return
        }
        
        // デフォルト:コンテンツが見つからない
        completion(INPlayMediaIntentResponse(code: .failure, userActivity: nil))
    }
}

キーポイント:

  • 既存の SiriKit Media Intents 対応アプリは変更不要
  • 新規アプリは Intents Extension を追加し、INPlayMediaIntentHandling を実装
  • 具体的なエラー理由(非対応メディアタイプ、ログイン必要など)を返すと Siri がより有用な応答を返す

04:11

レスポンス方式の選択

import Intents

func handle(intent: INPlayMediaIntent, completion: @escaping (INPlayMediaIntentResponse) -> Void) {
    // 方法 1:バックグラウンド再生(オーディオに推奨)
    let backgroundResponse = INPlayMediaIntentResponse(
        code: .handleInApp,
        userActivity: nil
    )
    
    // 方法 2:フォアグラウンドで起動
    let foregroundResponse = INPlayMediaIntentResponse(
        code: .continueInApp,
        userActivity: nil
    )
    
    // オーディオ再生には handleInApp を推奨。電話のロック解除は不要
    completion(backgroundResponse)
}

キーポイント:

  • handleInApp はバックグラウンドでアプリを起動して再生。スマホのロック解除は不要
  • continueInApp はアプリをフォアグラウンドに持ってくる
  • HomePod ではスマホが手元にないことが多いため、バックグラウンド再生の方が体験が良い

07:10

詳細

オーディオセッションの設定

正しいオーディオセッション設定はバックグラウンド再生の基盤です。

import AVFoundation

func configureAudioSession() {
    let session = AVAudioSession.sharedInstance()
    
    do {
        // 再生カテゴリに設定し、バックグラウンド再生をサポート
        try session.setCategory(.playback, mode: .default)
        
        // Podcast/オーディオブックには spokenAudio モードを推奨
        // 割り込み時は音量を下げずに一時停止
        try session.setCategory(.playback, mode: .spokenAudio)
        
        // セッションを有効化
        try session.setActive(true)
    } catch {
        print("Failed to configure audio session: \(error)")
    }
}

キーポイント:

  • セッションをアクティブにする前に category を .playback に設定
  • .spokenAudio モードは中断時にダッキングではなく一時停止。ポッドキャストやオーディオブックに最適
  • 正しく設定しないと、アプリがバックグラウンド(ロック画面など)に入ると再生が停止

08:55

AirPlay 統合

import MediaPlayer

func setupNowPlayingAndRemoteCommands() {
    // 現在の再生情報を設定
    var nowPlayingInfo: [String: Any] = [
        MPMediaItemPropertyTitle: "Push It",
        MPMediaItemPropertyArtist: "Dukes",
        MPNowPlayingInfoPropertyPlaybackRate: 1.0
    ]
    MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    
    // リモート制御コマンドを受信
    let commandCenter = MPRemoteCommandCenter.shared()
    
    commandCenter.playCommand.addTarget { event in
        self.resumePlayback()
        return .success
    }
    
    commandCenter.pauseCommand.addTarget { event in
        self.pausePlayback()
        return .success
    }
    
    commandCenter.nextTrackCommand.addTarget { event in
        self.skipToNext()
        return .success
    }
    
    commandCenter.previousTrackCommand.addTarget { event in
        self.skipToPrevious()
        return .success
    }
}

キーポイント:

  • MPNowPlayingInfoCenter が現在の再生内容をシステムに報告
  • MPRemoteCommandCenter が再生/一時停止/スキップなどのリモートコマンドを受信
  • AirPlay が正常に動作するために両方が必要

10:52

再生中断の処理

import AVFoundation

class AudioPlayerManager: NSObject {
    override init() {
        super.init()
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleInterruption),
            name: AVAudioSession.interruptionNotification,
            object: nil
        )
    }
    
    @objc func handleInterruption(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
              let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
              let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
            return
        }
        
        switch type {
        case .began:
            // 再生を一時停止
            pausePlayback()
        case .ended:
            if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
                let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
                if options.contains(.shouldResume) {
                    // 再生を再開
                    resumePlayback()
                }
            }
        @unknown default:
            break
        }
    }
}

キーポイント:

  • AVAudioSession.interruptionNotification を監視
  • 着信、ナビ案内、Siri リクエストなどが中断をトリガー
  • 中断終了後は shouldResume オプションに基づいて再開

10:28

リクエストルーティングの仕組み

Siri がどのデバイスを使うかの決定方法:

  • リクエストはプライマリ iPhone(Apple ID と探すの設定で位置情報を共有しているデバイス)にルーティング
  • リクエストユーザーは Home に登録し「自分の声を認識」を有効にする必要がある
  • パーソナルリクエストの有効化は不要
  • アプリ初回利用時、Siri がアプリデータへのアクセス許可をリクエスト

11:46

パーソナルアプリ語彙

import Intents

func donatePersonalVocabulary() {
    let intent = INAddMediaIntent(
        mediaSearch: INMediaItem(
            identifier: "playlist-workout",
            title: "Workout Mix",
            type: .playlist,
            artwork: nil
        )
    )
    
    // 個人語彙をシステムへ寄付
    let interaction = INInteraction(intent: intent, response: nil)
    interaction.donate { error in
        if let error = error {
            print("Donation failed: \(error)")
        }
    }
}

キーポイント:

  • パーソナルアプリ語彙でユーザー固有のエンティティをシステムに伝える
  • 個人プレイリスト、購入オーディオブック、お気に入りポッドキャストなどを含む
  • パーソナライズされたリクエストの Siri 認識率を向上

08:30

重要ポイント

  • 何を作るか: メディアアプリへの SiriKit Media Intents 対応

    • なぜ価値があるか: 追加コードなしで HomePod でアプリが利用可能になり、まったく新しい利用シナリオをカバー
    • 始め方: Intents Extension を追加し、INPlayMediaIntentHandling を実装。mediaNamemediaType フィールドを処理
  • 何を作るか: AirPlay 音声体験の最適化

    • なぜ価値があるか: HomePod シナリオは完全に AirPlay に依存。バッファリングとレイテンシが体験に直結
    • 始め方: AVAudioSession.playback カテゴリに設定し、MPNowPlayingInfoCenterMPRemoteCommandCenter を構成。バッファ再生 API で再開遅延を解消
  • 何を作るか: 具体的なエラーフィードバック

    • なぜ価値があるか: 曖昧な「コンテンツが見つかりません」はユーザーを混乱させる。具体的なエラーで問題解決を支援
    • 始め方: 非対応メディアタイプには .failureRequiringAppLaunch を返し、アプリ内で具体理由(「ジャンル非対応」「ログイン必要」など)を表示
  • 何を作るか: ポッドキャスト・オーディオブック向けオーディオセッション最適化

    • なぜ価値があるか: .spokenAudio モードは中断時にダッキングではなく一時停止し、コンテンツを聞き逃さない
    • 始め方: AVAudioSession の mode を .spokenAudio に設定し、interruptionNotification で中断終了後に再生を再開
  • 何を作るか: 認識率向上のためのパーソナル語彙の寄付

    • なぜ価値があるか: ユーザー作成プレイリスト名やお気に入りコンテンツ名は汎用モデルでは認識できない
    • 始め方: INInteraction でユーザー固有のメディア項目を寄付し、Siri にパーソナライズされたコンテンツを学習させる

関連セッション

コメント

GitHub Issues · utterances