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

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

行動裝置或筆電是控制播放的「傳送端」,Google Cast 裝置則是將內容顯示在電視上的「接收端」。

「傳送端架構」是指 Cast 類別程式庫二進位檔,以及傳送端在執行階段提供的相關聯資源。「傳送端應用程式」或「Cast 應用程式」是指在傳送端上執行的應用程式。Web 接收端應用程式是指在支援 Cast 的裝置上執行的 HTML 應用程式。

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

應用程式流程

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

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

如需 Google Cast Android SDK 中所有類別、方法和事件的完整清單,請參閱 Android 專用 Google Cast Sender API 參考資料。下列各節將說明在 Android 應用程式中加入 Cast 的步驟。

設定 Android 資訊清單

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

uses-sdk

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

<uses-sdk
        android:minSdkVersion="24"
        android:targetSdkVersion="35" />

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 的例項,其中包含會影響架構行為的選項。其中最重要的是 Web 接收端應用程式 ID,用於篩選探索結果,並在 Cast 工作階段啟動時啟動 Web 接收端應用程式。

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,在首次有可用的接收端時向使用者顯示,提醒使用者注意投放按鈕。傳送端應用程式可以自訂標題文字和位置。

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

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

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

  • 通知: 僅限 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 的參照,並在下列情況下啟動探索程序:

  • 系統會根據演算法平衡裝置探索延遲時間和電池用量,因此當傳送端應用程式進入前景時,有時會自動啟動探索功能。
  • 「投放」對話方塊已開啟。
  • 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 接收端應用程式、連線至該應用程式,以及初始化媒體控制管道等步驟。如要進一步瞭解 Cast 工作階段和 Web 接收端生命週期,請參閱 Web 接收端應用程式生命週期指南。

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

應用程式可以使用 SessionManagerListener 類別監控工作階段事件,例如建立、暫停、繼續和終止。如果工作階段在進行期間異常/突然終止,架構會自動嘗試恢復。

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

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

如需掌握工作階段的狀態變更,可以實作 SessionManagerListener。這個範例會監聽 Activity 中 CastSession 的可用性。

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 接收端上變更串流裝置」。

自動重新連線

這個架構提供 ReconnectionService,傳送端應用程式可啟用此功能,在許多細微的極端情況下處理重新連線,例如:

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

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

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

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

媒體控制功能的運作方式

Cast 架構已淘汰 Cast 2.x 的 RemoteMediaPlayer 類別,改用新的 RemoteMediaClient 類別,該類別在一組更方便的 API 中提供相同功能,且不必傳入 GoogleApiClient。

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

向網路接收端發出要求的所有 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 接收端上執行的媒體播放器應用程式。

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」>「Google Cast」>「顯示遙控器通知」,關閉該裝置的通知。(通知中包含「設定」應用程式的捷徑)。詳情請參閱「投放遙控器通知」。

新增迷你控制器

根據 Cast 設計檢查清單,傳送端應用程式應提供稱為迷你控制器的永久控制項,當使用者離開目前的內容頁面,前往傳送端應用程式的其他部分時,該控制項應會顯示。迷你控制器會向使用者顯示目前 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,並變更 NotificationOptions 和 CastMediaOptions,將目標活動設為新活動:

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 接收端應用程式,確保傳送端 UI 一律回報 Web 接收端指定的音量。

實體按鈕音量控制

在 Android 裝置上,凡是搭載 Jelly Bean 以上版本的裝置,預設都能使用投放端裝置的實體按鈕,變更網路接收端投放工作階段的音量。

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

如要在 Jelly Bean 之前的 Android 裝置上,使用實體音量鍵控制 Web 接收器裝置音量,傳送端應用程式應覆寫其活動中的 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 設計檢查清單要求傳送端應用程式在通知和螢幕鎖定中實作媒體遙控器,以便在傳送端投放內容,但傳送端應用程式未聚焦時使用。這個架構提供 MediaNotificationService 和 MediaIntentReceiver,協助傳送端應用程式在通知和螢幕鎖定畫面中建立媒體遙控器。

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

MediaIntentReceiver 是 BroadcastReceiver,可處理來自通知的使用者動作。

應用程式可以透過 NotificationOptions,設定螢幕鎖定畫面上的通知和媒體遙控器。應用程式可以設定要在通知中顯示哪些控制按鈕,以及使用者輕觸通知時要開啟哪個 Activity。如未明確提供動作,系統會使用預設值 MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK 和 MediaIntentReceiver.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 會自動代表您要求音訊焦點。

處理錯誤

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