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

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

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

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

傳送方架構採用非同步回呼設計來通知傳送者 事件的應用程式中,以及轉換應用程式在不同狀態的切換

應用程式流程

下列步驟說明傳送者的一般高階執行流程 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 探索結果,並在投放工作階段 已開始。

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;
    }
}

您必須宣告所實作 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() 時延遲初始化 方法。

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、 要呼叫「投放」按鈕才會向使用者顯示 第一個接收端。寄件者應用程式可以 自訂標題文字和位置 文字

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

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

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

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

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 對話方塊。

如要進一步瞭解 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.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 執行個體,並設定 標題、副標題和兩張圖片

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 執行個體。取得 RemoteMediaClient敬上 目前的 CastSession,然後將 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()敬上 取得目前的執行個體 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 的新類別。

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 設計檢查清單規定傳送者應用程式必須符合下列條件: 通知 以及鎖定 螢幕、 傳送位置,但傳送者應用程式未聚焦。 架構提供了 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();

已開啟通知和螢幕鎖定畫面的媒體控制項,已開啟 。如要停用,請呼叫 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 生命週期各階段的最佳回應。應用程式可顯示 輸出錯誤或中斷連線 網路接收器。