將投放功能整合至 Android 應用程式

本開發人員指南說明如何使用 Android 寄件者 SDK 為 Android 傳送端應用程式新增 Google Cast 支援。

行動裝置或筆記型電腦是控製播放內容的「傳送者」,而 Google Cast 裝置則是用來在電視上顯示內容的「接收器」

「傳送者架構」是指 Cast 類別程式庫二進位檔,以及傳送者在執行階段提供的相關資源。傳送者應用程式投放應用程式也有在傳送者上執行的應用程式。網路接收器應用程式是指在支援 Cast 的裝置上運作的 HTML 應用程式。

傳送者架構採用非同步回呼設計,可通知事件應用程式的傳送者,並轉換 Cast 應用程式生命週期的各個狀態。

應用程式流程

下列步驟會說明寄件者 Android 應用程式的一般高階執行流程:

  • Cast 架構會根據 Activity 生命週期自動啟動 MediaRouter 裝置探索。
  • 當使用者按一下「投放」按鈕時,該架構會顯示「投放」對話方塊,其中包含發現的投放裝置清單。
  • 當使用者選取投放裝置時,架構會嘗試在投放裝置上啟動 Web 接收器應用程式。
  • 該架構會在傳送端應用程式中叫用回呼,確認 Web Receiver 應用程式已經啟動。
  • 該架構會在傳送者與網路接收器應用程式之間建立通訊管道。
  • 這個架構使用通訊管道在網路接收器上載入及控制媒體播放。
  • 該架構會同步處理傳送者與網路接收器之間的媒體播放狀態:當使用者傳送傳送者 UI 動作時,該架構會將這些媒體控制要求傳送至網路接收器,而網路接收器傳送媒體狀態更新時,該架構就會更新傳送者 UI 的狀態。
  • 當使用者按一下「投放」按鈕,中斷與投放裝置的連線時,該架構會將傳送者應用程式與 Web 接收器通訊。

如需 Google Cast Android SDK 中所有類別、方法和事件的完整清單,請參閱 Android 適用的 Google Cast send API API 參考資料。以下各節將說明將投放功能新增至 Android 應用程式的步驟。

設定 Android 資訊清單

應用程式的 AndroidManifest.xml 檔案需要針對 Cast SDK 設定下列元素:

uses-sdk

設定 Cast SDK 支援的 Android API 級別下限和目標。最小值是 API 級別 21,目標為 API 級別 28。

<uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="28" />

android:theme

根據最低 Android SDK 版本設定應用程式主題。舉例來說,如果您並未實作自己的主題,當指定最低 Android SDK 最低版本的 Android SDK 版本時,應使用 Theme.AppCompat 的變化版本。

<application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat" >
       ...
</application>

初始化 Cast Context

這個架構有全域單例模式物件 CastContext,負責協調所有架構的互動。

您的應用程式必須實作 OptionsProvider 介面,並提供初始化 CastContext 單例模式所需的選項。OptionsProvider 提供 CastOptions 的執行個體,其中包含影響架構行為的選項。其中最重要的是網路接收器應用程式 ID,可用來在啟動投放工作階段時篩選探索結果,並啟動 Web Receiver 應用程式。

Kotlin
class CastOptionsProvider : OptionsProvider {
    override fun getCastOptions(context: Context): CastOptions {
        return Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

您必須在執行應用程式的 AndroidManifest.xml 檔案中,將實作的 OptionsProvider 完整名稱宣告為中繼資料欄位:

<application>
    ...
    <meta-data
        android:name=
            "com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
        android:value="com.foo.CastOptionsProvider" />
</application>

呼叫 CastContext.getSharedInstance() 時,延遲初始化 CastContext

Kotlin
class MyActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val castContext = CastContext.getSharedInstance(this)
    }
}
Java
public class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        CastContext castContext = CastContext.getSharedInstance(this);
    }
}

Cast 使用者體驗小工具

Cast 架構提供的小工具符合 Cast 設計檢查清單:

  • 簡介重疊:這個架構提供自訂檢視畫面 IntroductoryOverlay,使用者可以在首次使用接收器時,立即呼叫投放按鈕。寄件者應用程式可以自訂標題文字的文字和位置

  • 投放按鈕:當系統發現支援應用程式的接收器時,便會顯示「投放」按鈕。當使用者先按一下「投放」按鈕時,系統會顯示投放對話方塊,列出找到的裝置。當使用者在裝置連線期間按下「投放」按鈕時,會顯示目前媒體的中繼資料 (例如標題、錄音室的名稱和縮圖) 或允許使用者中斷與投放裝置的連線。

  • 迷你控制器:當使用者投放內容,而且離開目前內容頁面或寄件者應用程式的其他畫面後,畫面底部會顯示迷你控制器,讓使用者查看目前投放的媒體中繼資料,並控製播放。

  • 展開的控制器:當使用者投放內容時,如果按一下媒體通知或迷你控制器,系統就會展開展開的控制器,顯示目前正在播放的媒體中繼資料,並提供多個按鈕來控制媒體播放。

  • 通知:僅限 Android。當使用者投放內容並離開傳送端應用程式時,系統會顯示媒體通知,當中會顯示目前投放的媒體中繼資料和播放控制項。

  • 螢幕鎖定:僅限 Android。當使用者將內容投放至導航畫面 (或裝置逾時) 時,系統會顯示媒體鎖定畫面控制項,當中會顯示目前正在投放的媒體中繼資料和播放控制項。

以下指南說明如何將這些小工具加入應用程式。

新增投放按鈕

Android MediaRouter API 可讓您在次要裝置上啟用媒體顯示和播放功能。使用 MediaRouter API 的 Android 應用程式應在使用者介面中加入「投放」按鈕,讓使用者選取媒體路徑,以便在投放裝置 (例如投放裝置) 上播放媒體內容。

這個架構可讓您輕鬆將 MediaRouteButton 新增為 Cast button。請先在定義選單的 xml 檔案中新增選單項目或 MediaRouteButton,並使用 CastButtonFactory 以透過架構進行連結。

// To add a Cast button, add the following snippet.
// menu.xml
<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
    app:showAsAction="always" />
Kotlin
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.kt
override fun onCreateOptionsMenu(menu: Menu): Boolean {
    super.onCreateOptionsMenu(menu)
    menuInflater.inflate(R.menu.main, menu)
    CastButtonFactory.setUpMediaRouteButton(
        applicationContext,
        menu,
        R.id.media_route_menu_item
    )
    return true
}
Java
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.java
@Override public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                            menu,
                                            R.id.media_route_menu_item);
    return true;
}

如果 Activity 沿用自 FragmentActivity,您可以在版面配置中加入 MediaRouteButton

// activity_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center_vertical"
   android:orientation="horizontal" >

   <androidx.mediarouter.app.MediaRouteButton
       android:id="@+id/media_route_button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_weight="1"
       android:mediaRouteTypes="user"
       android:visibility="gone" />

</LinearLayout>
Kotlin
// MyActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_layout)

    mMediaRouteButton = findViewById<View>(R.id.media_route_button) as MediaRouteButton
    CastButtonFactory.setUpMediaRouteButton(applicationContext, mMediaRouteButton)

    mCastContext = CastContext.getSharedInstance(this)
}
Java
// MyActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_layout);

   mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
   CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton);

   mCastContext = CastContext.getSharedInstance(this);
}

如要使用主題設定「投放」按鈕的外觀,請參閱自訂投放按鈕

設定裝置探索功能

裝置探索功能則由 CastContext 完全代管。初始化 CastContext 時,傳送者應用程式會指定 Web 接收器應用程式 ID,並視需要設定 CastOptions 中的 supportedNamespaces 要求命名空間篩選。CastContext 會在內部保留 MediaRouter 的參照,並在傳送者應用程式進入前景時啟動探索程序,並在傳送者進入背景時停止。

Kotlin
class CastOptionsProvider : OptionsProvider {
    companion object {
        const val CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace"
    }

    override fun getCastOptions(appContext: Context): CastOptions {
        val supportedNamespaces: MutableList<String> = ArrayList()
        supportedNamespaces.add(CUSTOM_NAMESPACE)

        return CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
class CastOptionsProvider implements OptionsProvider {
    public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace";

    @Override
    public CastOptions getCastOptions(Context appContext) {
        List<String> supportedNamespaces = new ArrayList<>();
        supportedNamespaces.add(CUSTOM_NAMESPACE);

        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build();
        return castOptions;
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

工作階段管理的運作方式

Cast SDK 推出了投放工作階段的概念,這個架構結合了裝置連線步驟、啟動 (或加入) 網路接收器應用程式、連線至該應用程式,以及初始化媒體控制管道的步驟。如要進一步瞭解投放工作階段和網路接收器的生命週期,請參閱網路接收器的應用程式生命週期指南

工作階段是由 SessionManager 類別管理,應用程式可透過 CastContext.getSessionManager() 存取。個別工作階段以 Session 類別的子類別表示。舉例來說,CastSession 代表投放裝置的工作階段。應用程式可透過 SessionManager.getCurrentCastSession() 存取目前使用中的投放工作階段。

您的應用程式可以使用 SessionManagerListener 類別來監控工作階段事件,例如建立、暫停、繼續和終止。工作階段會在工作階段開始時自動嘗試從異常/中斷終止來恢復。

系統會在 MediaRouter 對話方塊中建立並自動操控工作階段,以回應使用者手勢。

如要進一步瞭解投放開始錯誤,應用程式可以使用 CastContext#getCastReasonCodeForCastStatusCode(int),將工作階段的啟動錯誤轉換為 CastReasonCodes。請注意,部分工作階段開始錯誤 (例如 CastReasonCodes#CAST_CANCELLED) 是預期的行為,不應記錄為錯誤。

如果您需要查看工作階段的狀態變更,可以實作 SessionManagerListener。這個範例會監聽 ActivityCastSession 的可用性。

Kotlin
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

        override fun onSessionStarted(session: CastSession?, sessionId: String) {
            invalidateOptionsMenu()
        }

        override fun onSessionStartFailed(session: CastSession?, error: Int) {
            val castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error)
            // Handle error
        }

        override fun onSessionSuspended(session: CastSession?, reason Int) {}

        override fun onSessionResuming(session: CastSession?, sessionId: String) {}

        override fun onSessionResumed(session: CastSession?, wasSuspended: Boolean) {
            invalidateOptionsMenu()
        }

        override fun onSessionResumeFailed(session: CastSession?, error: Int) {}

        override fun onSessionEnding(session: CastSession?) {}

        override fun onSessionEnded(session: CastSession?, error: Int) {
            finish()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onPause() {
        super.onPause()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
        mCastSession = null
    }
}
Java
public class MyActivity extends Activity {
    private CastContext mCastContext;
    private CastSession mCastSession;
    private SessionManager mSessionManager;
    private SessionManagerListener<CastSession> mSessionManagerListener =
            new SessionManagerListenerImpl();

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession session) {}
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionStartFailed(CastSession session, int error) {
            int castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error);
            // Handle error
        }
        @Override
        public void onSessionSuspended(CastSession session, int reason) {}
        @Override
        public void onSessionResuming(CastSession session, String sessionId) {}
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumeFailed(CastSession session, int error) {}
        @Override
        public void onSessionEnding(CastSession session) {}
        @Override
        public void onSessionEnded(CastSession session, int error) {
            finish();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCastContext = CastContext.getSharedInstance(this);
        mSessionManager = mCastContext.getSessionManager();
    }
    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
    @Override
    protected void onPause() {
        super.onPause();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
        mCastSession = null;
    }
}

變更串流裝置

保留工作階段狀態是串流傳輸的基礎,使用者可透過語音指令、Google Home 應用程式或智慧螢幕,在不同裝置上移動現有的音訊和視訊串流。在某裝置上 (來源) 停止播放,並在另一部裝置上 (目的地) 繼續播放。任何搭載最新韌體的投放裝置都可以在串流轉移作業中做為來源或目的地。

如要在串流或展開期間取得新的目標裝置,請使用 CastSession#addCastListener 註冊 Cast.Listener。然後在 onDeviceNameChanged 回呼期間呼叫 CastSession#getCastDevice()

詳情請參閱在網路接收器上轉移串流一文。

自動重新連線

架構提供的 ReconnectionService 可讓寄件者應用程式啟用,以處理許多細微的極端情況,例如:

  • 暫時停止 Wi-Fi 連線
  • 從裝置睡眠時復原
  • 從背景應用程式復原
  • 在應用程式停止運作時復原

根據預設,系統會開啟這項服務,您可以在 CastOptions.Builder 中關閉。

如果您已在 Gradle 檔案中啟用自動合併功能,這項服務可自動合併到應用程式的資訊清單中。

如果有媒體工作階段,架構會啟動服務,並在媒體工作階段結束時停止服務。

媒體控制選項的運作方式

Cast 架構淘汰了 Cast 2.x 的 RemoteMediaPlayer 類別,並改用新的類別 RemoteMediaClient,該類別在一組更便利的 API 中提供相同的功能,並避免需要傳遞 GoogleApiClient。

如果您的應用程式使用支援媒體命名空間的 Web Receiver 應用程式建立 CastSession,架構會自動建立 RemoteMediaClient 的執行個體;您的應用程式可以存取 CastSession 執行個體上的 getRemoteMediaClient() 方法來存取該物件。

所有向網路接收器發出要求的 RemoteMediaClient 方法都會傳回一個 PendingResult 物件,可用於追蹤該要求。

RemoteMediaClient 的執行個體可能由應用程式的多個部分共用,並確實取決於架構的某些內部元件,例如永久的小型控制器通知服務。因此,這個執行個體支援註冊 RemoteMediaClient.Listener 的多個執行個體。

設定媒體中繼資料

MediaMetadata 類別代表您要投放的媒體項目相關資訊。以下範例會建立新的電影媒體執行個體,並設定名稱、副標題和兩張圖片。

Kotlin
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle())
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio())
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(0))))
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(1))))
Java
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle());
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio());
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0))));
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));

如要瞭解如何將媒體中繼資料與圖片搭配使用,請參閱圖片選取一文。

載入媒體

應用程式可以載入媒體項目,如以下程式碼所示。首先,搭配媒體的中繼資料使用 MediaInfo.Builder,以建構 MediaInfo 執行個體。從目前的 CastSession 取得 RemoteMediaClient,然後將 MediaInfo 載入 RemoteMediaClient。使用 RemoteMediaClient 播放、暫停及操控在網頁接收器上執行的媒體播放器應用程式。

Kotlin
val mediaInfo = MediaInfo.Builder(mSelectedMedia.getUrl())
    .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
    .setContentType("videos/mp4")
    .setMetadata(movieMetadata)
    .setStreamDuration(mSelectedMedia.getDuration() * 1000)
    .build()
val remoteMediaClient = mCastSession.getRemoteMediaClient()
remoteMediaClient.load(MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build())
Java
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl())
        .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
        .setContentType("videos/mp4")
        .setMetadata(movieMetadata)
        .setStreamDuration(mSelectedMedia.getDuration() * 1000)
        .build();
RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());

另請參閱使用媒體曲目一節。

4K 影片格式

如要查看媒體的視訊格式,請使用 MediaStatus 中的 getVideoInfo() 取得 VideoInfo 目前的例項。這個執行個體包含 HDR 電視格式的類型,以及螢幕的高度和寬度 (以像素為單位)。4K 格式的變化版本會以常數 HDR_TYPE_* 表示。

從遠端控制多部裝置的通知

當使用者投放內容時,相同網路上的其他 Android 裝置會收到通知,以便他們控製播放作業。任何人只要在裝置上收到這類通知,都能在 Google Play「設定」應用程式 > Google Cast >「顯示遠端控制通知」進行關閉。 (通知包括「設定」應用程式的捷徑)。詳情請參閱「投放遙控器控制項通知」。

新增迷你控制器

根據 Cast 設計檢查清單,寄件者應用程式應提供稱為迷你控制器的持續性控制選項,當使用者離開目前的內容頁面時,應在傳送者應用程式的其他部分間看到。迷你控制器可向使用者傳送目前的投放工作階段提醒。透過迷你控制器,使用者可以返回「全螢幕全螢幕展開」檢視畫面。

這個架構提供自訂 View MiniControllerFragment,您可以在當中顯示您要顯示迷你控制器的每個活動檔案底部。

<fragment
    android:id="@+id/castMiniController"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:visibility="gone"
    class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment" />

當傳送端應用程式正在播放影片或音訊直播時,SDK 會自動在迷你控制器上顯示播放/停止按鈕。

如要設定這個自訂檢視區塊的標題和子標題,以及選擇按鈕,請參閱自訂迷你控制器

新增展開的控制器

Google Cast 設計檢查清單要求一個傳送者應用程式,提供投放媒體的展開控制器。展開的控制器是迷你控制器的全螢幕版本。

Cast SDK 針對展開的控制器提供名為 ExpandedControllerActivity 的小工具。 這是一個子類別,您必須加入子類別才能新增「投放」按鈕。

首先,為展開的控制器建立新的選單資源檔案,以提供「投放」按鈕:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
            android:id="@+id/media_route_menu_item"
            android:title="@string/media_route_menu_title"
            app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
            app:showAsAction="always"/>

</menu>

建立可擴充 ExpandedControllerActivity 的新類別。

Kotlin
class ExpandedControlsActivity : ExpandedControllerActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.expanded_controller, menu)
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
        return true
    }
}
Java
public class ExpandedControlsActivity extends ExpandedControllerActivity {
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.expanded_controller, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}

現在,請在 application 標記的應用程式資訊清單中宣告新活動:

<application>
...
<activity
        android:name=".expandedcontrols.ExpandedControlsActivity"
        android:label="@string/app_name"
        android:launchMode="singleTask"
        android:theme="@style/Theme.CastVideosDark"
        android:screenOrientation="portrait"
        android:parentActivityName="com.google.sample.cast.refplayer.VideoBrowserActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
    </intent-filter>
</activity>
...
</application>

編輯 CastOptionsProvider 並變更 NotificationOptionsCastMediaOptions,將目標活動設為新活動:

Kotlin
override fun getCastOptions(context: Context): CastOptions? {
    val notificationOptions = NotificationOptions.Builder()
        .setTargetActivityClassName(ExpandedControlsActivity::class.java.name)
        .build()
    val mediaOptions = CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name)
        .build()

    return CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build()
}
Java
public CastOptions getCastOptions(Context context) {
    NotificationOptions notificationOptions = new NotificationOptions.Builder()
            .setTargetActivityClassName(ExpandedControlsActivity.class.getName())
            .build();
    CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .setExpandedControllerActivityClassName(ExpandedControlsActivity.class.getName())
            .build();

    return new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setCastMediaOptions(mediaOptions)
            .build();
}

更新 LocalPlayerActivity loadRemoteMedia 方法,以便在載入遠端媒體時顯示新活動:

Kotlin
private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    val remoteMediaClient = mCastSession?.remoteMediaClient ?: return

    remoteMediaClient.registerCallback(object : RemoteMediaClient.Callback() {
        override fun onStatusUpdated() {
            val intent = Intent(this@LocalPlayerActivity, ExpandedControlsActivity::class.java)
            startActivity(intent)
            remoteMediaClient.unregisterCallback(this)
        }
    })

    remoteMediaClient.load(
        MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position.toLong()).build()
    )
}
Java
private void loadRemoteMedia(int position, boolean autoPlay) {
    if (mCastSession == null) {
        return;
    }
    final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
    if (remoteMediaClient == null) {
        return;
    }
    remoteMediaClient.registerCallback(new RemoteMediaClient.Callback() {
        @Override
        public void onStatusUpdated() {
            Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class);
            startActivity(intent);
            remoteMediaClient.unregisterCallback(this);
        }
    });
    remoteMediaClient.load(new MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position).build());
}

當傳送端應用程式正在播放影片或音訊串流時,SDK 會自動在展開控制器中顯示播放/停止按鈕。

如要使用主題設定外觀,請選擇要顯示的按鈕並新增自訂按鈕,請參閱自訂展開的控制器

音量控制

該架構會自動管理傳送方應用程式的音量。該架構會自動同步處理傳送者和網路接收器應用程式,讓傳送者 UI 一律回報網路接收器指定的磁碟區。

實體按鈕音量控制

在 Android 裝置上,系統會根據裝置上的裝置實體按鈕,為使用 Jelly Bean 以上版本的裝置變更網頁接收器的投放工作階段音量。

在 Jelly Bean 之前使用實體按鈕調整音量

如要在搭載 Jelly Bean 的 Android 裝置上控制使用實體音量金鑰控制網頁接收器裝置,則傳送者應用程式應在其活動中覆寫 dispatchKeyEvent,並呼叫 CastContext.onDispatchVolumeKeyEventBeforeJellyBean()

Kotlin
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
Java
class MyActivity extends FragmentActivity {
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
            || super.dispatchKeyEvent(event);
    }
}

在通知和螢幕鎖定畫面新增媒體控制項

如果是 Android 裝置,Google Cast 設計檢查清單要求一個傳送者應用程式,在通知中實作媒體控制項,並在螢幕鎖定畫面中有傳送者的投放畫面,但傳送者並未聚焦。該架構提供 MediaNotificationServiceMediaIntentReceiver,協助傳送方應用程式在通知和螢幕鎖定畫面中建立媒體控制項。

MediaNotificationService 會在傳送者投放時執行,並顯示含有圖片縮圖、目前投放項目、播放/暫停按鈕和停止按鈕的通知。

MediaIntentReceiverBroadcastReceiver,可處理通知中的使用者動作。

應用程式可透過 NotificationOptions 設定螢幕鎖定畫面上的通知和媒體控制項。 應用程式可設定要在通知中顯示的控制按鈕,以及當使用者輕觸通知時,要開啟哪些 Activity。如未明確提供動作,系統會使用預設值 MediaIntentReceiver.ACTION_TOGGLE_PLAYBACKMediaIntentReceiver.ACTION_STOP_CASTING

Kotlin
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
val buttonActions: MutableList<String> = ArrayList()
buttonActions.add(MediaIntentReceiver.ACTION_REWIND)
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK)
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD)
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING)

// Showing "play/pause" and "stop casting" in the compat view of the notification.
val compatButtonActionsIndices = intArrayOf(1, 3)

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
val notificationOptions = NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
    .build()
Java
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
List<String> buttonActions = new ArrayList<>();
buttonActions.add(MediaIntentReceiver.ACTION_REWIND);
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK);
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD);
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING);

// Showing "play/pause" and "stop casting" in the compat view of the notification.
int[] compatButtonActionsIndices = new int[]{1, 3};

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
NotificationOptions notificationOptions = new NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity.class.getName())
    .build();

根據預設,系統會顯示通知和螢幕鎖定畫面的媒體控制項,並呼叫 CastMediaOptions.Builder 中的空值的 setNotificationOptions 即可停用此功能。目前,只要已開啟通知,螢幕鎖定功能就會開啟。

Kotlin
// ... continue with the NotificationOptions built above
val mediaOptions = CastMediaOptions.Builder()
    .setNotificationOptions(notificationOptions)
    .build()
val castOptions: CastOptions = Builder()
    .setReceiverApplicationId(context.getString(R.string.app_id))
    .setCastMediaOptions(mediaOptions)
    .build()
Java
// ... continue with the NotificationOptions built above
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .build();
CastOptions castOptions = new CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build();

當傳送端應用程式正在播放影片或音訊串流時,SDK 會自動在通知控制項上顯示播放/停止按鈕,而不是鎖定螢幕控制項。

注意:如要在配備 Lollipop 的裝置上顯示螢幕鎖定控制項,RemoteMediaClient 會自動代您要求音訊焦點。

處理錯誤

請務必讓傳送端應用程式處理所有錯誤回呼,並決定轉換生命週期各階段的最佳回應。應用程式可以向使用者顯示錯誤對話方塊,也可以決定要拆卸與網路接收器的連線。