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

本開發人員指南說明如何使用 Android Sender SDK,在 Android 傳送端應用程式中加入 Google Cast 支援。

行動裝置或筆記型電腦是傳送者,負責控制播放作業;Google Cast 裝置則是接收者,負責在電視上顯示內容。

傳送端架構是指在傳送端執行階段存在的 Cast 類別庫二進位檔和相關聯的資源。傳送端應用程式Cast 應用程式是指在傳送端執行的應用程式。網頁接收器應用程式是指在支援 Cast 的裝置上執行的 HTML 應用程式。

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

應用程式流程

下列步驟說明傳送端 Android 應用程式的一般高階執行流程:

  • Cast 架構會根據 Activity 生命週期,自動啟動 MediaRouter 裝置探索功能。
  • 使用者點按「投放」按鈕時,架構會顯示「投放」對話方塊,並列出已探索到的 Cast 裝置。
  • 使用者選取 Cast 裝置後,架構會嘗試在 Cast 裝置上啟動 Web Receiver 應用程式。
  • 架構會在傳送者應用程式中叫用回呼,確認 Web Receiver 應用程式已啟動。
  • 這個架構會在傳送者和 Web Receiver 應用程式之間建立通訊管道。
  • 架構會使用通訊管道,在網頁接收器上載入及控制媒體播放。
  • 架構會在傳送器和網頁接收器之間同步處理媒體播放狀態:使用者執行傳送器 UI 動作時,架構會將這些媒體控制要求傳遞至網頁接收器;網頁接收器傳送媒體狀態更新時,架構會更新傳送器 UI 的狀態。
  • 使用者點按「投放」按鈕與 Cast 裝置中斷連線時,架構會中斷傳送端應用程式與 Web Receiver 的連線。

如需 Google Cast Android SDK 中所有類別、方法和事件的完整清單,請參閱 Google Cast Android 傳送端 API 參考資料。下列章節將說明如何將 Cast 新增至 Android 應用程式。

設定 Android 資訊清單

應用程式的 AndroidManifest.xml 檔案必須為 Cast SDK 設定下列元素:

uses-sdk

設定 Cast SDK 支援的最低和目標 Android API 級別。 目前最低為 API 級別 23,目標為 API 級別 34。

<uses-sdk
        android:minSdkVersion="23"
        android:targetSdkVersion="34" />

android:theme

根據 Android SDK 最低版本設定應用程式的主題。舉例來說,如果您未實作自己的主題,指定 Lollipop 之前的 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,這個 ID 可用於篩選探索結果,並在啟動 Cast 工作階段時啟動網頁接收器應用程式。

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 UX 小工具

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

  • 簡介疊加層: 架構提供自訂檢視區塊 IntroductoryOverlay, 在第一次有接收器可用時,向使用者顯示這個檢視區塊,吸引他們注意 Cast 按鈕。傳送器應用程式可以自訂標題文字和位置

  • 「投放」按鈕: 無論是否有可用的 Cast 裝置,都會顯示「投放」按鈕。 使用者第一次點按「投放」按鈕時,系統會顯示「投放」對話方塊,列出已探索到的裝置。裝置連線後,使用者點選「投放」按鈕時,系統會顯示目前的媒體中繼資料 (例如標題、錄音室名稱和縮圖),或允許使用者與投放裝置中斷連線。「Cast 鍵」有時也稱為「Cast 圖示」。

  • 迷你遙控器: 使用者投放內容時,如果離開目前內容頁面或展開的遙控器,前往傳送端應用程式中的其他畫面,畫面底部就會顯示迷你遙控器,方便使用者查看目前投放的媒體中繼資料,以及控制播放作業。

  • 擴展控制器: 使用者投放內容時,如果點選媒體通知或迷你控制器,系統會啟動擴展控制器,顯示目前播放的媒體中繼資料,並提供多個按鈕來控制媒體播放。

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

  • 螢幕鎖定: 僅限 Android 裝置。使用者投放內容並前往螢幕鎖定畫面 (或裝置逾時) 時,系統會顯示媒體螢幕鎖定控制項,其中包含目前投放的媒體中繼資料和播放控制項。

以下指南說明如何在應用程式中新增這些小工具。

新增 Cast 按鈕

Android MediaRouter API 的設計宗旨,是在次要裝置上啟用媒體顯示和播放功能。使用 MediaRouter API 的 Android 應用程式應在使用者介面中加入 Cast 按鈕,讓使用者選取媒體路徑,在 Cast 裝置等次要裝置上播放媒體。

這個架構可讓您輕鬆新增 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);
}

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

設定裝置探索

裝置探索完全由 CastContext 管理。初始化 CastContext 時,傳送器應用程式會指定 Web Receiver 應用程式 ID,並可選擇在 CastOptions 中設定 supportedNamespaces,要求進行命名空間篩選。CastContext 會在內部保留對 MediaRouter 的參照,並在下列情況下啟動探索程序:

  • 系統會根據演算法,在裝置探索延遲和電池用量之間取得平衡,因此當傳送端應用程式進入前景時,偶爾會自動啟動探索功能。
  • 「投放」對話方塊隨即開啟。
  • Cast SDK 正在嘗試還原 Cast 工作階段。

關閉 Cast 對話方塊或傳送端應用程式進入背景時,探索程序就會停止。

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 導入了 Cast 工作階段的概念,建立工作階段時,會一併完成連線至裝置、啟動 (或加入) Web Receiver 應用程式、連線至該應用程式,以及初始化媒體控制管道等步驟。如要進一步瞭解 Google Cast 工作階段和 Web Receiver 生命週期,請參閱 Web Receiver 應用程式生命週期指南

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

應用程式可以使用 SessionManagerListener 類別監控工作階段事件,例如建立、暫停、繼續和終止。如果工作階段處於啟用狀態時發生異常/突然終止,架構會自動嘗試從該狀態恢復。

系統會根據 MediaRouter 對話方塊中的使用者手勢,自動建立及終止工作階段。

如要進一步瞭解 Google Cast 啟動錯誤,應用程式可以使用 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
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
    }

    override fun onDestroy() {
        super.onDestroy()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }
}
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();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
}

變更串流裝置

保留工作階段狀態是串流轉移的基礎,使用者可以透過語音指令、Google Home 應用程式或智慧螢幕,在裝置間轉移現有的音訊和視訊串流。媒體會在一部裝置 (來源) 上停止播放,並在另一部裝置 (目的地) 上繼續播放。只要韌體為最新版本,任何 Cast 裝置都能在串流轉移中做為來源或目的地。

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

詳情請參閱「在 Web Receiver 上轉移串流」。

自動重新連線

這個架構提供 ReconnectionService,傳送端應用程式可啟用此架構,處理許多細微的極端情況,例如:

  • 在暫時失去 Wi-Fi 連線時復原
  • 從裝置休眠狀態復原
  • 從應用程式背景執行狀態復原
  • 應用程式當機時復原

這項服務預設為開啟,您可以在 CastOptions.Builder 中關閉。

如果 gradle 檔案中已啟用自動合併功能,這項服務就會自動合併至應用程式的資訊清單。

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

媒體控制功能的運作方式

Cast 架構會淘汰 Cast 2.x 的 RemoteMediaPlayer 類別,改用新的 RemoteMediaClient 類別,這個類別會透過一組更方便的 API 提供相同功能,且不必傳遞 GoogleApiClient。

當應用程式與支援媒體命名空間的網頁接收器應用程式建立 CastSession 時,架構會自動建立 RemoteMediaClient 的例項;應用程式可以呼叫 CastSession 例項上的 getRemoteMediaClient() 方法來存取該例項。

所有向 Web Receiver 發出要求的方法 RemoteMediaClient 都會傳回 PendingResult 物件,可用於追蹤該要求。

RemoteMediaClient 執行個體預期會由應用程式的多個部分共用,事實上,架構的某些內部元件 (例如持續性迷你控制器通知服務) 也會共用。為此,這個執行個體支援註冊多個 RemoteMediaClient.Listener 執行個體。

設定媒體中繼資料

MediaMetadata 類別代表要投放的媒體項目相關資訊。下列範例會建立新的電影 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 播放、暫停或以其他方式控制 Web Receiver 上執行的媒體播放器應用程式。

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 影片格式

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

在多部裝置上接收遙控通知

使用者投放內容時,同一網路上的其他 Android 裝置會收到通知,讓使用者也能控制播放內容。如果裝置收到這類通知,使用者可以在「設定」應用程式中依序前往「Google」>「Google Cast」>「顯示遠端控制通知」,為該裝置關閉通知。(通知會提供「設定」應用程式的捷徑)。詳情請參閱「Google Cast 遙控器通知」。

新增迷你控制器

根據 Cast 設計檢查清單,當使用者離開目前內容頁面,前往傳送端應用程式的其他部分時,傳送端應用程式應提供持續顯示的控制項,也就是迷你控制器。迷你控制器會顯示目前 Cast 工作階段,提醒使用者。輕觸迷你遙控器,即可返回 Google 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 的媒體提供擴充控制器。展開的控制器是迷你控制器的全螢幕版本。

Cast SDK 提供名為 ExpandedControllerActivity 的擴充控制器小工具。這是抽象類別,您必須建立子類別才能新增 Cast 按鈕。

首先,請為擴充控制器建立新的選單資源檔案,提供 Cast 按鈕:

<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 會自動在展開的控制器中顯示「播放/停止」按鈕,取代「播放/暫停」按鈕。

如要使用主題設定外觀,請選擇要顯示的按鈕,並新增自訂按鈕,詳情請參閱「自訂展開式遙控器」。

音量控制項

架構會自動管理傳送端應用程式的音量。架構會自動同步處理傳送端和 Web Receiver 應用程式,因此傳送端 UI 一律會回報 Web Receiver 指定的音量。

使用實體按鈕控制音量

在 Android 裝置上,如果裝置使用 Jelly Bean 以上版本,預設可使用傳送端裝置的實體按鈕,變更網頁接收器上 Cast 工作階段的音量。

Jelly Bean 之前的實體按鈕音量控制

如要在 Jelly Bean 之前的 Android 裝置上,使用實體音量鍵控制 Web Receiver 裝置音量,傳送端應用程式應在活動中覆寫 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();

系統預設會開啟從通知和螢幕鎖定畫面顯示媒體控制項的功能,但您可以呼叫 setNotificationOptions,並在 CastMediaOptions.Builder 中傳遞空值,即可停用這項功能。目前只要開啟通知,螢幕鎖定功能就會一併開啟。

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 會自動代表您要求音訊焦點。

處理錯誤

傳送端應用程式務必處理所有錯誤回呼,並決定 Cast 生命週期各階段的最佳回應。應用程式可以向使用者顯示錯誤對話方塊,也可以決定終止與 Web Receiver 的連線。