出力スイッチャー

出力スイッチャーは、シームレスな転送を可能にする Cast SDK の機能です。 コンテンツのローカル再生とリモート再生の違い(Android 13 以降)目標 送信側アプリでコンテンツの再生先を簡単かつ迅速に制御できるようにすることです。 出力スイッチャーは MediaRouter ライブラリを スマートフォンのスピーカー、ペア設定された Bluetooth デバイス、 キャスト対応デバイスに提供しますユースケースは次のように分類できます。 シナリオ:

出力の実装方法については、以下のサンプルをダウンロードして参考にしてください。 オーディオ アプリの切り替え。サービスに関する手順については、付属の README.md をご覧ください。 サンプルの実行方法を指定します

サンプルをダウンロード

ローカルからリモートとリモートからローカルをサポートするには、出力スイッチャーを有効にする必要があります 手順に沿って作業を進めます。認証に必要な追加の手順や ローカル デバイスのスピーカーとペア設定した Bluetooth との間の転送をサポートする できます。

オーディオ アプリとは、レシーバー アプリで Google Cast for Audio に対応しているアプリのことです。 Google Cast SDK Developer の コンソール

出力スイッチャー UI

出力スイッチャーには、使用可能なローカル デバイスとリモート デバイスが表示されます。 現在のデバイスの状態(デバイスが選択されているかどうかなど) 現在の音量レベルが表示されます。これ以外のデバイスも 別のデバイスをクリックすると、メディアを転送できます。 指定したデバイスで再生します。

既知の問題

  • ローカル再生用に作成されたメディア セッションは破棄され、再作成されます 」が表示されます。

エントリ ポイント

メディアに関する通知

アプリから投稿されたメディア通知を MediaSession: ローカル再生(ローカルでの再生中)、メディア通知の右上 通知チップにそのデバイス名(スマートフォンのスピーカーなど)が 。通知チップをタップすると開く [Output Switcher] ダイアログのシステム UI。

音量の設定

[Output Switcher] ダイアログのシステム UI は、[ デバイスの物理的な音量ボタン、画面下部の設定アイコンをタップします。 [<アプリ名> をプレイ]<キャスト デバイス> であります。

ステップの概要

前提条件

  1. 既存の Android アプリを AndroidX に移行します。
  2. 出力スイッチャーに必要な最小バージョンの Android Sender SDK を使用するようにアプリの build.gradle を更新します。
    dependencies {
      ...
      implementation 'com.google.android.gms:play-services-cast-framework:21.2.0'
      ...
    }
  3. アプリはメディア通知をサポートしています。
  4. Android 13 を搭載したデバイス。

メディア通知の設定

出力スイッチャーを使用するには audio動画アプリ 再生ステータスを表示するメディア通知を作成し、 ローカル再生のメディア コントロールを設定できます。そのためには、Terraform で MediaSessionMediaStyle MediaSession のトークンを使用し、メディア コントロールを 通知を受け取ります。

現在 MediaStyleMediaSession を使用していない場合は、スニペットは 以下に、設定方法と、メディアの設定に関するガイドを示します。 セッション コールバックを audio動画 アプリ:

Kotlin
で確認できます。
// Create a media session. NotificationCompat.MediaStyle
// PlayerService is your own Service or Activity responsible for media playback.
val mediaSession = MediaSessionCompat(this, "PlayerService")

// Create a MediaStyle object and supply your media session token to it.
val mediaStyle = Notification.MediaStyle().setMediaSession(mediaSession.sessionToken)

// Create a Notification which is styled by your MediaStyle object.
// This connects your media session to the media controls.
// Don't forget to include a small icon.
val notification = Notification.Builder(this@PlayerService, CHANNEL_ID)
    .setStyle(mediaStyle)
    .setSmallIcon(R.drawable.ic_app_logo)
    .build()

// Specify any actions which your users can perform, such as pausing and skipping to the next track.
val pauseAction: Notification.Action = Notification.Action.Builder(
        pauseIcon, "Pause", pauseIntent
    ).build()
notification.addAction(pauseAction)
Java
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    // Create a media session. NotificationCompat.MediaStyle
    // PlayerService is your own Service or Activity responsible for media playback.
    MediaSession mediaSession = new MediaSession(this, "PlayerService");

    // Create a MediaStyle object and supply your media session token to it.
    Notification.MediaStyle mediaStyle = new Notification.MediaStyle().setMediaSession(mediaSession.getSessionToken());

    // Specify any actions which your users can perform, such as pausing and skipping to the next track.
    Notification.Action pauseAction = Notification.Action.Builder(pauseIcon, "Pause", pauseIntent).build();

    // Create a Notification which is styled by your MediaStyle object.
    // This connects your media session to the media controls.
    // Don't forget to include a small icon.
    String CHANNEL_ID = "CHANNEL_ID";
    Notification notification = new Notification.Builder(this, CHANNEL_ID)
        .setStyle(mediaStyle)
        .setSmallIcon(R.drawable.ic_app_logo)
        .addAction(pauseAction)
        .build();
}

通知にメディアの情報を入力するには、次の操作を行います。 既存のメディアの メタデータと再生状態 MediaSession に送信します。

MediaSession にメタデータを追加するには、次のコマンドを使用します。 setMetaData() 関連するすべてのアラートと 次の MediaMetadata 定数: 自分のメディアを MediaMetadataCompat.Builder()

Kotlin
で確認できます。
mediaSession.setMetadata(MediaMetadataCompat.Builder()
    // Title
    .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title)

    // Artist
    // Could also be the channel name or TV series.
    .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist)

    // Album art
    // Could also be a screenshot or hero image for video content
    // The URI scheme needs to be "content", "file", or "android.resource".
    .putString(
        MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri)
    )

    // Duration
    // If duration isn't set, such as for live broadcasts, then the progress
    // indicator won't be shown on the seekbar.
    .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration)

    .build()
)
Java
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    mediaSession.setMetadata(
        new MediaMetadataCompat.Builder()
        // Title
        .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title)

        // Artist
        // Could also be the channel name or TV series.
        .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist)

        // Album art
        // Could also be a screenshot or hero image for video content
        // The URI scheme needs to be "content", "file", or "android.resource".
        .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri)

        // Duration
        // If duration isn't set, such as for live broadcasts, then the progress
        // indicator won't be shown on the seekbar.
        .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration)

        .build()
    );
}

再生状態を MediaSession に追加するには、次のコマンドを使用します。 setPlaybackState() 関連するすべてのアラートと PlaybackStateCompat 定数を PlaybackStateCompat.Builder()

Kotlin
で確認できます。
mediaSession.setPlaybackState(
    PlaybackStateCompat.Builder()
        .setState(
            PlaybackStateCompat.STATE_PLAYING,

            // Playback position
            // Used to update the elapsed time and the progress bar.
            mediaPlayer.currentPosition.toLong(),

            // Playback speed
            // Determines the rate at which the elapsed time changes.
            playbackSpeed
        )

        // isSeekable
        // Adding the SEEK_TO action indicates that seeking is supported
        // and makes the seekbar position marker draggable. If this is not
        // supplied seek will be disabled but progress will still be shown.
        .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
        .build()
)
Java
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    mediaSession.setPlaybackState(
        new PlaybackStateCompat.Builder()
            .setState(
                 PlaybackStateCompat.STATE_PLAYING,

                // Playback position
                // Used to update the elapsed time and the progress bar.
                mediaPlayer.currentPosition.toLong(),

                // Playback speed
                // Determines the rate at which the elapsed time changes.
                playbackSpeed
            )

        // isSeekable
        // Adding the SEEK_TO action indicates that seeking is supported
        // and makes the seekbar position marker draggable. If this is not
        // supplied seek will be disabled but progress will still be shown.
        .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
        .build()
    );
}

動画アプリの通知動作

バックグラウンドでのローカル再生に対応していない動画アプリやオーディオ アプリ 特定の動作をするよう、メディア通知の動作を 再生がサポートされていない状況でメディア コマンドを送信する

  • アプリがローカルで再生されているときにメディア通知を送信する 使用できます。
  • ローカル再生を一時停止し、アプリが 説明します。
  • アプリがフォアグラウンドに戻ったら、ローカル再生を再開し、 通知を再投稿する必要があります。

AndroidManifest.xml で出力スイッチャーを有効にする

出力スイッチャーを有効にするには、 MediaTransferReceiver アプリの AndroidManifest.xml に追加する必要があります。そうでない場合、この機能は 有効に設定されず、remote-to-local 機能フラグも無効になります。

<application>
    ...
    <receiver
         android:name="androidx.mediarouter.media.MediaTransferReceiver"
         android:exported="true">
    </receiver>
    ...
</application>

MediaTransferReceiver システムとデバイス間のメディア転送を可能にするブロードキャスト レシーバです。 UI です。詳しくは、MediaTransferReceiver リファレンス をご覧ください。

ローカルからリモート

再生をローカルからリモートに切り替えると、Cast SDK が起動します 自動的にキャスト セッションが削除されます。ただし、アプリは移行元のプラットフォームから ローカルからリモートへ(たとえば、ローカルの再生を停止する) キャスト デバイスにメディアを読み込みます。アプリがキャストをリッスンする必要があります SessionManagerListener 使用 onSessionStarted() および onSessionEnded() キャストを受信したときにアクションを処理します。 SessionManager 使用します。アプリはこれらのコールバックが有効な状態で Output Switcher ダイアログが開いており、アプリがフォアグラウンドではない。

バックグラウンド キャスト用の SessionManagerListener を更新

従来のキャストでは、アプリが がフォアグラウンドにあります。一般的なキャスト エクスペリエンスは、ユーザーが [キャスト] をクリックすると開始されます アイコンをタップし、メディアをストリーミングするデバイスを選択します。この場合、アプリには 登録して SessionManagerListener 場所: onCreate()、または onStart() リスナーの登録を解除し、 onStop() または onDestroy() 追跡します。

出力スイッチャーを使用した新しいキャスト機能により、 バックグラウンドでの キャストが可能です。これは特に、音声や動画などの バックグラウンドでの再生中に通知を送信するアプリ。アプリは サービスの onCreate()SessionManager リスナーを登録する サービスの onDestroy() で登録を解除します。このように、アプリは 次の場合は、常にローカルからリモートへのコールバック(onSessionStarted など)を受け取ります。 ダウンロードされます。

アプリが MediaBrowserService そこで SessionManagerListener を登録することをおすすめします。

Kotlin
で確認できます。
class MyService : Service() {
    private var castContext: CastContext? = null
    protected fun onCreate() {
        castContext = CastContext.getSharedInstance(this)
        castContext
            .getSessionManager()
            .addSessionManagerListener(sessionManagerListener, CastSession::class.java)
    }

    protected fun onDestroy() {
        if (castContext != null) {
            castContext
                .getSessionManager()
                .removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
        }
    }
}
Java
public class MyService extends Service {
  private CastContext castContext;

  @Override
  protected void onCreate() {
     castContext = CastContext.getSharedInstance(this);
     castContext
        .getSessionManager()
        .addSessionManagerListener(sessionManagerListener, CastSession.class);
  }

  @Override
  protected void onDestroy() {
    if (castContext != null) {
       castContext
          .getSessionManager()
          .removeSessionManagerListener(sessionManagerListener, CastSession.class);
    }
  }
}

今回のアップデートにより、ローカルからリモートへのキャストは、 バックグラウンドで動作しており、移行元の環境から Bluetooth デバイスをキャスト デバイスに接続する。

リモートからローカルへ

出力スイッチャーは、リモート再生から またはローカルの Bluetooth デバイスに接続します。この設定を有効にするには、 CastOptionstrue に対する setRemoteToLocalEnabled フラグ。

現在の送信側のデバイスが次のユーザーと既存のセッションに参加する場合: 現在のメディアが使用できるかどうかをアプリで確認する必要がある 転送するには、アプリで onTransferred コールバックを使用する必要があります。 SessionTransferCallbackして SessionState を確認します。

setRemoteToLocalEnabled フラグを設定する

CastOptions は、表示または非表示にする setRemoteToLocalEnabled を提供します。 出力の転送先としての電話スピーカーとローカル Bluetooth デバイス アクティブなキャスト セッションがある場合の切り替えダイアログ。

Kotlin
で確認できます。
class CastOptionsProvider : OptionsProvider {
    fun getCastOptions(context: Context?): CastOptions {
        ...
        return Builder()
            ...
            .setRemoteToLocalEnabled(true)
            .build()
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        ...
        return new CastOptions.Builder()
            ...
            .setRemoteToLocalEnabled(true)
            .build()
  }
}

ローカルでの再生を続行

リモートからローカルへの変換をサポートするアプリは、SessionTransferCallback を登録する必要があります。 イベント発生時に通知を受け取れるようにして、メディアがアップロードされる ローカルで転送して再生を続行できる権限があります。

CastContext#addSessionTransferCallback(SessionTransferCallback) を使用すると、 アプリがその SessionTransferCallback を登録して、onTransferred と 送信者がローカル再生に転送された場合の onTransferFailed コールバック。

アプリがその SessionTransferCallback の登録を解除すると、アプリは SessionTransferCallback を受信します。

SessionTransferCallback は、既存の SessionManagerListener 呼び出され、onSessionEnded がトリガーされた後にトリガーされます。したがって、この順序は リモートからローカルへのコールバックの特徴は次のとおりです。

  1. onTransferring
  2. onSessionEnding
  3. onSessionEnded
  4. onTransferred

出力スイッチャーは、通知を送信するときにメディア通知チップで開けるため、 アプリがバックグラウンドで動作していてキャストしている場合、アプリは バックグラウンド再生に対応しているかどうかによって異なります。イン 転送が失敗した場合は、いつでも onTransferFailed が呼び出されます。 エラーが発生します。

バックグラウンド再生をサポートするアプリ

バックグラウンドでの再生をサポートするアプリ(主にオーディオ アプリ)の場合: Service を使用することをおすすめします(例: MediaBrowserService)。サービス onTransferred コールバックをリッスンし、再生をローカルで再開する必要があります。 アプリがフォアグラウンドまたはバックグラウンドで実行されている。

Kotlin
で確認できます。
class MyService : Service() {
    private var castContext: CastContext? = null
    private var sessionTransferCallback: SessionTransferCallback? = null
    protected fun onCreate() {
        castContext = CastContext.getSharedInstance(this)
        castContext.getSessionManager()
                   .addSessionManagerListener(sessionManagerListener, CastSession::class.java)
        sessionTransferCallback = MySessionTransferCallback()
        castContext.addSessionTransferCallback(sessionTransferCallback)
    }

    protected fun onDestroy() {
        if (castContext != null) {
            castContext.getSessionManager()
                       .removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
            if (sessionTransferCallback != null) {
                castContext.removeSessionTransferCallback(sessionTransferCallback)
            }
        }
    }

    class MySessionTransferCallback : SessionTransferCallback() {
        fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) {
            // Perform necessary steps prior to onTransferred
        }

        fun onTransferred(@SessionTransferCallback.TransferType transferType: Int,
                          sessionState: SessionState?) {
            if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
                // Remote stream is transferred to the local device.
                // Retrieve information from the SessionState to continue playback on the local player.
            }
        }

        fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int,
                             @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) {
            // Handle transfer failure.
        }
    }
}
Java
public class MyService extends Service {
    private CastContext castContext;
    private SessionTransferCallback sessionTransferCallback;

    @Override
    protected void onCreate() {
        castContext = CastContext.getSharedInstance(this);
        castContext.getSessionManager()
                   .addSessionManagerListener(sessionManagerListener, CastSession.class);
        sessionTransferCallback = new MySessionTransferCallback();
        castContext.addSessionTransferCallback(sessionTransferCallback);
    }

    @Override
    protected void onDestroy() {
        if (castContext != null) {
            castContext.getSessionManager()
                       .removeSessionManagerListener(sessionManagerListener, CastSession.class);
            if (sessionTransferCallback != null) {
                castContext.removeSessionTransferCallback(sessionTransferCallback);
            }
        }
    }

    public static class MySessionTransferCallback extends SessionTransferCallback {
        public MySessionTransferCallback() {}

        @Override
        public void onTransferring(@SessionTransferCallback.TransferType int transferType) {
            // Perform necessary steps prior to onTransferred
        }

        @Override
        public void onTransferred(@SessionTransferCallback.TransferType int transferType,
                                  SessionState sessionState) {
            if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
                // Remote stream is transferred to the local device.
                // Retrieve information from the SessionState to continue playback on the local player.
            }
        }

        @Override
        public void onTransferFailed(@SessionTransferCallback.TransferType int transferType,
                                     @SessionTransferCallback.TransferFailedReason int transferFailedReason) {
            // Handle transfer failure.
        }
    }
}

バックグラウンド再生をサポートしていないアプリ

バックグラウンド再生をサポートしていないアプリ(主に動画アプリ)の場合: onTransferred コールバックをリッスンして再生を再開することをおすすめします。 アプリがフォアグラウンドにある場合は、ローカルに作成します。

アプリがバックグラウンドで動作している場合は、再生を一時停止して、 SessionState からの必要な情報(メディア メタデータや再生など) あります。アプリがバックグラウンドからフォアグラウンドで動作している場合、ローカル再生は 保存された情報を維持する必要があります。

Kotlin
で確認できます。
class MyActivity : AppCompatActivity() {
    private var castContext: CastContext? = null
    private var sessionTransferCallback: SessionTransferCallback? = null
    protected fun onCreate() {
        castContext = CastContext.getSharedInstance(this)
        castContext.getSessionManager()
                   .addSessionManagerListener(sessionManagerListener, CastSession::class.java)
        sessionTransferCallback = MySessionTransferCallback()
        castContext.addSessionTransferCallback(sessionTransferCallback)
    }

    protected fun onDestroy() {
        if (castContext != null) {
            castContext.getSessionManager()
                       .removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
            if (sessionTransferCallback != null) {
                castContext.removeSessionTransferCallback(sessionTransferCallback)
            }
        }
    }

    class MySessionTransferCallback : SessionTransferCallback() {
        fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) {
            // Perform necessary steps prior to onTransferred
        }

        fun onTransferred(@SessionTransferCallback.TransferType transferType: Int,
                          sessionState: SessionState?) {
            if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
                // Remote stream is transferred to the local device.

                // Retrieve information from the SessionState to continue playback on the local player.
            }
        }

        fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int,
                             @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) {
            // Handle transfer failure.
        }
    }
}
Java
public class MyActivity extends AppCompatActivity {
  private CastContext castContext;
  private SessionTransferCallback sessionTransferCallback;

  @Override
  protected void onCreate() {
     castContext = CastContext.getSharedInstance(this);
     castContext
        .getSessionManager()
        .addSessionManagerListener(sessionManagerListener, CastSession.class);
     sessionTransferCallback = new MySessionTransferCallback();
     castContext.addSessionTransferCallback(sessionTransferCallback);
  }

  @Override
  protected void onDestroy() {
    if (castContext != null) {
       castContext
          .getSessionManager()
          .removeSessionManagerListener(sessionManagerListener, CastSession.class);
      if (sessionTransferCallback != null) {
         castContext.removeSessionTransferCallback(sessionTransferCallback);
      }
    }
  }

  public static class MySessionTransferCallback extends SessionTransferCallback {
    public MySessionTransferCallback() {}

    @Override
    public void onTransferring(@SessionTransferCallback.TransferType int transferType) {
        // Perform necessary steps prior to onTransferred
    }

    @Override
    public void onTransferred(@SessionTransferCallback.TransferType int transferType,
                               SessionState sessionState) {
      if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
        // Remote stream is transferred to the local device.

        // Retrieve information from the SessionState to continue playback on the local player.
      }
    }

    @Override
    public void onTransferFailed(@SessionTransferCallback.TransferType int transferType,
                                 @SessionTransferCallback.TransferFailedReason int transferFailedReason) {
      // Handle transfer failure.
    }
  }
}