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

本開發人員指南說明如何為您的 Android 新增 Google Cast 支援 傳送者應用程式。

行動裝置或筆記型電腦是控製播放的傳送者, Google Cast 裝置是一種接收器,用於在電視上顯示內容。

傳送者架構是指 Cast 類別程式庫二進位檔,以及相關聯的 向傳送者顯示資源。寄件者應用程式投放應用程式 代表在傳送方上執行的應用程式。網路接收器應用程式 是指在支援 Cast 裝置上執行的 HTML 應用程式。

傳送方架構採用非同步回呼設計來通知傳送者 事件應用程式,以及在 Cast 應用程式生命週期的不同狀態間轉換。

應用程式流程

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

  • Cast 架構會自動啟動 MediaRouter敬上 根據 Activity 生命週期探索裝置。
  • 當使用者按一下「投放」按鈕時,架構就會顯示「投放」 對話方塊顯示偵測到的投放裝置清單。
  • 使用者選取投放裝置時,架構會嘗試啟動 Cast 裝置上的 Web Receiver 應用程式
  • 此架構會在傳送端應用程式中叫用回呼,以確認 Web 接收器應用程式已啟動。
  • 這個架構可在傳送者和網頁之間建立通訊管道 接收端應用程式。
  • 該架構使用通訊管道來載入及控制媒體 並透過 Web Receiver 播放
  • 架構會同步處理傳送者和傳送端的媒體播放狀態 網路接收端:當使用者進行傳送者 UI 動作時,架構會通過 向 Web 接收器發出的媒體控制要求 傳送媒體狀態更新,此架構會更新傳送者 UI 的狀態。
  • 當使用者點選「投放」按鈕中斷與投放裝置的連線時, 這個架構會中斷傳送端應用程式與 Web Receiver 的連結。

查看 Google Cast 的所有課程、方法和活動完整清單 Android SDK,請參閱 Google Cast Sender API 參考資料: Android: 以下各節將說明將 Cast 新增至 Android 應用程式的步驟。

設定 Android 資訊清單

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

uses-sdk

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

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

android:theme

請依據最低 Android SDK 版本設定應用程式主題。舉例來說 您不是實作自己的主題,則應使用 指定的最低 Android SDK 版本時,請設為 Theme.AppCompat 前身為 Lollipop

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

初始化 Cast 結構定義

此架構有一個全域單例模式物件 CastContext,該座標 所有架構的互動

您的應用程式必須實作 OptionsProvider敬上 介面,提供初始化 CastContext 單例模式OptionsProvider 提供 CastOptions ,其中包含會影響架構行為的選項。最常出現 重要的是網路接收端應用程式 ID 探索結果,並在投放工作階段 已開始。

KotlinJava
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
    }
}
敬上
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;
    }
}

您必須宣告所實作 OptionsProvider 的完整名稱 做為傳送端應用程式的 AndroidManifest.xml 檔案的中繼資料欄位:

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

CastContext 會在 CastContext.getSharedInstance() 時延遲初始化 方法。

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

Cast 使用者體驗小工具

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

  • 簡介重疊: 架構提供自訂檢視 IntroductoryOverlay, 要呼叫「投放」按鈕才會向使用者顯示 第一個接收端。寄件者應用程式可以 自訂標題文字和位置 文字

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

  • Mini Controller: 使用者投放內容且離開目前畫面時 內容頁面,或展開控制器連往傳送者應用程式的其他畫面, 螢幕底部顯示迷你控制器 查看目前投放的媒體中繼資料,並控製播放作業。

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

  • 通知: 僅限 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" />
敬上
KotlinJava
// 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
}
敬上
// 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>
敬上
KotlinJava
// 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)
}
敬上
// 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 時,傳送端應用程式會指定網路接收器 應用程式 ID,並可選擇性地透過設定要求命名空間篩選 supportedNamespaces 英吋 CastOptionsCastContext 會保留對 MediaRouter 內部的參照,且將以 探索過程:

  • 根據經過特別設計的演算法,能平衡裝置探索延遲時間和 因此系統會在以下時間過後自動啟動探索功能: 傳送者應用程式進入前景。
  • 「投放」對話方塊已開啟。
  • Cast SDK 正嘗試復原投放工作階段。

「投放」對話方塊關閉或 訊息傳送者應用程式進入背景。

KotlinJava
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
    }
}
敬上
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 對話方塊。

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

如果您需要掌握工作階段的狀態變更, SessionManagerListener。這個範例會監聽 Activity 中的 CastSession

KotlinJava
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)
    }
}
敬上
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.Listener敬上 方法是使用 CastSession#addCastListener。 然後呼叫 CastSession#getCastDevice()敬上 在 onDeviceNameChanged 回呼期間執行這個動作

詳情請見 透過網路接收器轉移串流裝置 瞭解詳情

自動重新連線

架構提供了 ReconnectionService敬上 傳送者應用程式可啟用這個 API 以處理重新連線 極端案例,例如:

  • 在 Wi-Fi 連線暫時中斷的情況下復原
  • 從裝置睡眠狀態復原
  • 從背景執行復原程序
  • 在應用程式當機時復原

這項服務預設為啟用,但可在 CastOptions.Builder

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

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

媒體控制的運作方式

Cast 架構淘汰了 RemoteMediaPlayer敬上 Cast 2.x 類別提供的類別,改用新類別 RemoteMediaClient, 它為一組更加便利的 API 提供了相同的功能 不必傳入 GoogleApiClient

當您的應用程式建立 CastSession敬上 Web Receiver 應用程式 (支援媒體命名空間) 架構會自動建立 RemoteMediaClient;應用程式 對 CastSession 呼叫 getRemoteMediaClient() 方法,即可存取該函式 執行個體。

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

RemoteMediaClient」的例項應可由以下人員共用: 應用程式的多個部分,以及 例如永久的迷你控制器 通知服務。 為此,這個執行個體支援註冊多個 RemoteMediaClient.Listener

設定媒體中繼資料

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

KotlinJava
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))))
敬上
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 執行個體。取得 RemoteMediaClient敬上 目前的 CastSession,然後將 MediaInfo 載入 RemoteMediaClient。使用 RemoteMediaClient 即可播放、暫停等 控制在 Web Receiver 上執行的媒體播放器應用程式。

KotlinJava
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())
敬上
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()敬上 取得目前的執行個體 VideoInfo。 這個執行個體包含 HDR 電視格式的類型和顯示高度 以像素為單位4K 格式的變化版本是以常數表示 HDR_TYPE_*

對多部裝置發送遠端控制通知

當使用者投放內容時,連上相同網路的其他 Android 裝置將取得 方便他們控製播放內容的通知擁有裝置的所有人 如果收到通知,您可以在「設定」中為裝置關閉相關通知 應用程式 >Google Cast >顯示遙控器通知。 (通知中會包含「設定」應用程式的捷徑)。詳情請參閱 投放遙控器通知

新增迷你控制器

根據 Cast Design 檢查清單、 傳送者應用程式應提供永久性控制項,稱為 mini 控制器 當使用者離開目前的內容網頁, 另一個部分迷你控制器提供顯眼的提醒 當前投放工作階段的使用者只要輕觸迷你控制器 使用者即可返回 Cast 全螢幕展開控制器檢視畫面。

這個架構提供自訂 View (MinControllerFragment) 在每個活動的版面配置檔案底部 小控制器

<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 的新類別。

KotlinJava
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
    }
}
敬上
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 將目標活動設為新活動:

KotlinJava
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()
}
敬上
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 方法,以顯示您的 遠端媒體載入時的新活動:

KotlinJava
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()
    )
}
敬上
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():

KotlinJava
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
敬上
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

KotlinJava
// 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()
敬上
// 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。 目前,只要通知處於啟用狀態,系統就會開啟螢幕鎖定功能 已開啟。

KotlinJava
// ... 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()
敬上
// ... 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 生命週期各階段的最佳回應。應用程式可顯示 輸出錯誤或中斷連線 網路接收器。