支援投放功能的 Android 應用程式

1. 總覽

Google Cast 標誌

本程式碼研究室將說明如何修改現有的 Android 影片應用程式,以在支援 Google Cast 的裝置投放內容。

什麼是 Google Cast?

Google Cast 可讓使用者將行動裝置上的內容投放到電視上。讓使用者能將行動裝置當做電視播放媒體的遙控器。

Google Cast SDK 可讓您擴充應用程式,輕鬆控制電視或音響系統。Cast SDK 可讓您根據 Google Cast 設計檢查清單新增必要的 UI 元件。

我們歸納了 Google Cast 設計檢查清單,目的是讓所有支援的平台都能簡單且可預測的 Cast 使用者體驗。

我們要建構什麼?

完成本程式碼研究室後,您將擁有一部 Android 影片應用程式,可將影片投放到支援 Google Cast 的裝置。

課程內容

  • 如何將 Google Cast SDK 加入範例應用程式。
  • 如何新增「投放」按鈕供選取 Google Cast 裝置。
  • 如何連線至投放裝置並啟動媒體接收器。
  • 如何投放影片。
  • 如何在應用程式中新增 Cast 迷你控制器。
  • 如何支援媒體通知和螢幕鎖定控制項。
  • 如何新增展開控制器。
  • 如何提供簡介重疊。
  • 如何自訂 Cast 小工具。
  • 如何與 Cast Connect 整合

軟硬體需求

  • 最新版 Android SDK
  • Android Studio 3.2 以上版本
  • 一部搭載 Android 4.1 Jelly Bean (API 級別 16) 以上版本的行動裝置。
  • USB 資料傳輸線,可將行動裝置連接至開發電腦。
  • 一部 Google Cast 裝置,例如已設定網際網路連線的 ChromecastAndroid TV
  • 具備 HDMI 輸入端的電視或顯示器。
  • 如要測試 Cast Connect 整合功能,必須使用 Chromecast (支援 Google TV),但程式碼研究室的其餘部分為選用。如果您沒有裝置,可以略過本教學課程結尾的「新增 Cast Connect 支援」步驟。

功能

  • 您必須事先熟悉 Kotlin 和 Android 開發知識。
  • 並具備觀看電視的知識 :)

您將會如何使用這個教學課程?

僅供閱讀 閱讀並完成練習

針對建構 Android 應用程式的經驗,您會給予什麼評價?

初級 中級 達人

針對觀看電視的體驗,你會給予什麼評價?

初級 中級 專業

2. 取得程式碼範例

您可以將所有程式碼範例下載至電腦...

然後將下載的 ZIP 檔案解壓縮

3. 執行範例應用程式

一對圓規的圖示

首先,來看看完成的範例應用程式看起來會是什麼樣子。這款應用程式是基本的影片播放器。使用者只要從清單中選取影片,即可將影片投放到裝置本機或投放到 Google Cast 裝置。

下載程式碼後,下列操作說明會說明如何在 Android Studio 中開啟並執行完成的範例應用程式:

在歡迎畫面中選取「Import Project」,或依序選取「File」>「New」>「Import Project...」選單選項。

從程式碼範例資料夾中選取 「資料夾」圖示app-done 目錄,然後按一下「OK」。

依序點選「File」>「Sync Project with Gradle Files」Android Studio 的「Sync Project with Gradle」按鈕

在 Android 裝置上啟用 USB 偵錯功能;在 Android 4.2 以上版本中,「Developer options」畫面會預設為隱藏。如要啟用這項功能,請依序前往「設定」>「關於手機」,然後輕觸「版本號碼」7 次。返回上一個畫面,依序前往「System」(系統) >「Advanced」(進階),然後依序輕觸靠近底部的「Developer options」和「USB debugging」,即可開啟這項功能。

插入 Android 裝置,然後在 Android Studio 中按一下Android Studio 的「Run」按鈕,綠色三角形指向右邊「Run」按鈕。幾秒後,畫面上應該會顯示名為「投放影片」的影片應用程式。

在影片應用程式中按一下「投放」按鈕,然後選取你的 Google Cast 裝置。

選取影片並按一下播放按鈕。

影片就會開始在 Google Cast 裝置上播放。

畫面會顯示展開的控制器。您可以使用播放/暫停按鈕控製播放。

返回影片清單。

畫面底部現在會顯示迷你控制器。插圖:Android 手機執行「投放影片」應用程式,螢幕底部顯示迷你控制器

按一下迷你控制器中的暫停按鈕,即可在接收器上暫停播放影片。按一下迷你控制器中的播放按鈕,即可再次播放影片。

按一下行動裝置的首頁按鈕。將通知往下拉,現在應該會顯示「投放」工作階段的通知。

鎖定手機和手機解鎖時,螢幕鎖定畫面上應會顯示通知,可控制媒體播放或停止投放。

返回影片應用程式,然後按一下「投放」按鈕,即可停止在 Google Cast 裝置上投放內容。

常見問題

4. 準備 start 專案

Android 手機執行「投放影片」應用程式的插圖

對於你下載的啟動應用程式,我們需要新增 Google Cast 支援功能。以下是本程式碼研究室會使用的 Google Cast 術語:

  • 傳送者應用程式是在行動裝置或筆記型電腦上執行,
  • 接收端應用程式必須在 Google Cast 裝置上執行。

您現在可以使用 Android Studio,以範例專案為基礎進行建構了:

  1. 從程式碼範例下載中選取 「資料夾」圖示app-start 目錄 (在歡迎畫面中依序選取「Import Project」或「File」>「New」>「Import Project...」選項)。
  2. 按一下 Android Studio 的「Sync Project with Gradle」按鈕「Sync Project with Gradle Files」按鈕。
  3. 按一下 Android Studio 的「Run」按鈕,綠色三角形指向右邊「Run」按鈕,執行應用程式並探索 UI。

應用程式設計

應用程式會從遠端網路伺服器擷取影片清單,並提供可供使用者瀏覽的清單。使用者可以選取影片來查看詳細資訊,或在行動裝置上直接播放影片。

應用程式由 VideoBrowserActivityLocalPlayerActivity 這兩個主要活動組成。如要整合 Google Cast 功能,「活動」必須沿用 AppCompatActivity 或其父項 FragmentActivity。之所以會有這項限制,是因為我們必須將 MediaRouteButton (在 MediaRouter 支援資料庫內提供) 新增為 MediaRouteActionProvider,而只有在活動繼承自上述類別時,才能使用此方法。MediaRouter 支援資料庫仰賴提供必要類別的 AppCompat 支援資料庫

VideoBrowserActivity

這項活動包含 Fragment (VideoBrowserFragment)。這份清單是由 ArrayAdapter (VideoListAdapter) 支援。影片清單及其相關中繼資料會以 JSON 檔案的形式儲存在遠端伺服器上。AsyncTaskLoader (VideoItemLoader) 會擷取並處理這個 JSON,藉此建立 MediaItem 物件清單。

MediaItem 物件會為影片及其相關中繼資料建立模型,例如標題、說明、串流網址、輔助圖片和相關聯的文字軌 (如果是隱藏式輔助字幕)。MediaItem 物件會在活動之間傳遞,因此 MediaItem 具有公用方法,將其轉換為 Bundle,反之亦然。

載入器建構 MediaItems 的清單時,會將該清單傳遞至 VideoListAdapter,並在 VideoBrowserFragment 中顯示 MediaItems 清單。使用者會看到一份影片縮圖清單,其中包含每部影片的簡短說明。選取項目後,對應的 MediaItem 會轉換為 Bundle 並傳遞至 LocalPlayerActivity

LocalPlayerActivity

這個活動會顯示特定影片的中繼資料,讓使用者可在行動裝置上播放影片。

這項活動會代管 VideoView、部分媒體控制項,以及用來顯示所選影片說明的文字區域。播放器覆蓋了畫面最上方,留下影片詳細說明,使用者可以播放/暫停影片,或尋找在本機播放影片。

依附元件

我們使用的是 AppCompatActivity,因此需要 AppCompat 支援資料庫。為了管理影片清單,以及以非同步方式取得清單的圖片,我們會使用 Volley 程式庫。

常見問題

5. 新增「投放」按鈕

顯示「投放影片」應用程式的 Android 手機頂端部分插圖;「投放」按鈕顯示在畫面右上角

支援 Cast 的應用程式會在每項活動中顯示「投放」按鈕。按一下「投放」按鈕,就會顯示使用者可選取的投放裝置清單。如果使用者是透過傳送者的裝置本機播放內容,選取投放裝置時,該投放裝置就會開始或繼續播放內容。在投放過程中,使用者隨時可以按一下「投放」按鈕,停止將應用程式投放到投放裝置。使用者必須能夠在應用程式的任何活動中與投放裝置連線或中斷連線,詳情請參閱 Google Cast 設計檢查清單

依附元件

更新應用程式 build.gradle 檔案,加入必要的程式庫依附元件:

dependencies {
    implementation 'androidx.appcompat:appcompat:1.5.0'
    implementation 'androidx.mediarouter:mediarouter:1.3.1'
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
    implementation 'com.google.android.gms:play-services-cast-framework:21.1.0'
    implementation 'com.android.volley:volley:1.2.1'
    implementation "androidx.core:core-ktx:1.8.0"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}

請同步處理專案,確認專案版本沒有發生錯誤。

初始化

Cast 架構具有全域單例模式物件 CastContext,可協調所有投放互動。

您必須實作 OptionsProvider 介面,以便提供初始化 CastContext 單例模式所需的 CastOptions。最重要的選項是接收端應用程式 ID,可用於篩選投放裝置的搜尋結果,以及在投放工作階段開始時啟動接收器應用程式。

自行開發支援 Cast 的應用程式時,您必須註冊為 Cast 開發人員,然後取得應用程式的應用程式 ID。在本程式碼研究室中,我們將使用範例應用程式 ID。

將下列新的 CastOptionsProvider.kt 檔案新增至專案的 com.google.sample.cast.refplayer 套件:

package com.google.sample.cast.refplayer

import android.content.Context
import com.google.android.gms.cast.framework.OptionsProvider
import com.google.android.gms.cast.framework.CastOptions
import com.google.android.gms.cast.framework.SessionProvider

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

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}

現在,請在應用程式 AndroidManifest.xml 檔案的「application」標記內宣告 OptionsProvider

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

延遲初始化 VideoBrowserActivity onCreate 方法中的 CastContext

import com.google.android.gms.cast.framework.CastContext

private var mCastContext: CastContext? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.video_browser)
    setupActionBar()

    mCastContext = CastContext.getSharedInstance(this)
}

LocalPlayerActivity 中加入相同的初始化邏輯。

投放按鈕

現在 CastContext 已初始化,我們需要新增「投放」按鈕,讓使用者能夠選取投放裝置。「投放」按鈕是由 MediaRouter 支援資料庫中的 MediaRouteButton 實作。就像任何可在活動中新增的動作圖示 (使用 ActionBarToolbar) 一樣,您必須先在選單中新增對應的選單項目。

編輯 res/menu/browse.xml 檔案,然後在設定項目前,從選單新增 MediaRouteActionProvider 項目:

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

使用 CastButtonFactoryMediaRouteButton 連接至 Cast 架構,以覆寫 VideoBrowserActivityonCreateOptionsMenu() 方法:

import com.google.android.gms.cast.framework.CastButtonFactory

private var mediaRouteMenuItem: MenuItem? = null

override fun onCreateOptionsMenu(menu: Menu): Boolean {
     super.onCreateOptionsMenu(menu)
     menuInflater.inflate(R.menu.browse, menu)
     mediaRouteMenuItem = CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu,
                R.id.media_route_menu_item)
     return true
}

以類似的方式覆寫 LocalPlayerActivity 中的 onCreateOptionsMenu

按一下 Android Studio 的「Run」按鈕,綠色三角形指向右邊「Run」按鈕,在行動裝置上執行應用程式。你應該會在應用程式的動作列中看到「投放」按鈕。只要按一下該按鈕,畫面就會列出您區域網路中的投放裝置。裝置探索功能是由 CastContext 自動管理。選取你的投放裝置,範例接收器應用程式隨即會在投放裝置上載入。您可以在瀏覽活動和本機播放器活動之間瀏覽,而「投放」按鈕狀態會保持同步。

我們尚未支援任何媒體播放功能,因此你目前還無法在投放裝置上播放影片。按一下「投放」按鈕即可中斷連線。

6. 投放影片內容

Android 手機執行「投放影片」應用程式的插圖

我們將擴充範例應用程式,以便同時在投放裝置上遠端播放影片。為此,我們需要監聽 Cast 架構產生的各種事件。

投放媒體

大致上,如果您想在投放裝置上播放媒體,就必須執行下列操作:

  1. 建立用於建立媒體項目的 MediaInfo 物件。
  2. 連線至投放裝置,然後啟動接收器應用程式。
  3. MediaInfo 物件載入接收器並播放內容。
  4. 追蹤媒體狀態。
  5. 根據使用者互動情形,將播放指令傳送給接收器。

我們已經完成上一節中的步驟 2。輕鬆透過 Cast 架構進行步驟 3。步驟 1 的操作說明,將一個物件對應至另一個物件;MediaInfo 是 Cast 架構可理解的,而 MediaItem 則是應用程式的媒體項目封裝;我們可以輕鬆將 MediaItem 對應至 MediaInfo

範例應用程式 LocalPlayerActivity 已藉由使用此列舉來區分本機和遠端播放:

private var mLocation: PlaybackLocation? = null

enum class PlaybackLocation {
    LOCAL, REMOTE
}

enum class PlaybackState {
    PLAYING, PAUSED, BUFFERING, IDLE
}

想瞭解所有範例播放器邏輯的運作方式,不是本程式碼研究室中的重點。請務必瞭解,應用程式的媒體播放器需要經過修改,才能以類似的方式瞭解這兩個播放位置。

目前,本機播放器一律會在本機播放狀態,因為其尚無「投放狀態」的相關資訊。我們需要根據 Cast 架構中的狀態轉換作業更新 UI。舉例來說,如果我們開始投放,就必須停止本機播放,並停用部分控制項。同樣地,如果我們在發生這類活動時停止投放,就必須轉換為本機播放。為了處理這種情況,我們需要監聽 Cast 架構產生的各種事件。

投放工作階段管理

針對投放架構,「投放」工作階段結合了連線至裝置、啟動 (或加入)、連線至接收器應用程式、以及初始化媒體控制管道 (如適用) 的步驟。媒體控制管道是指 Cast 架構從接收端媒體播放器收發訊息的方式。

當使用者透過「投放」按鈕選取裝置時,「投放」工作階段就會自動開始,並在使用者中斷連線時自動停止投放。網路問題導致重新連線到接收器工作階段時,Cast SDK 也會自動處理問題。

讓我們將 SessionManagerListener 加入 LocalPlayerActivity

import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.SessionManagerListener
...

private var mSessionManagerListener: SessionManagerListener<CastSession>? = null
private var mCastSession: CastSession? = null
...

private fun setupCastListener() {
    mSessionManagerListener = object : SessionManagerListener<CastSession> {
        override fun onSessionEnded(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

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

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

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

        override fun onSessionStartFailed(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionStarting(session: CastSession) {}
        override fun onSessionEnding(session: CastSession) {}
        override fun onSessionResuming(session: CastSession, sessionId: String) {}
        override fun onSessionSuspended(session: CastSession, reason: Int) {}
        private fun onApplicationConnected(castSession: CastSession) {
            mCastSession = castSession
            if (null != mSelectedMedia) {
                if (mPlaybackState == PlaybackState.PLAYING) {
                    mVideoView!!.pause()
                    loadRemoteMedia(mSeekbar!!.progress, true)
                    return
                } else {
                    mPlaybackState = PlaybackState.IDLE
                    updatePlaybackLocation(PlaybackLocation.REMOTE)
                }
            }
            updatePlayButton(mPlaybackState)
            invalidateOptionsMenu()
        }

        private fun onApplicationDisconnected() {
            updatePlaybackLocation(PlaybackLocation.LOCAL)
            mPlaybackState = PlaybackState.IDLE
            mLocation = PlaybackLocation.LOCAL
            updatePlayButton(mPlaybackState)
            invalidateOptionsMenu()
       }
   }
}

LocalPlayerActivity 活動中,我們想要在與投放裝置連線或中斷連線時接收通知,以便切換到本機播放器或離開本機播放器。請注意,不僅連線至行動裝置所執行的應用程式執行個體可能會造成連線中斷,而且還可能會幹擾在其他行動裝置上運作的其他應用程式執行個體。

目前執行中的工作階段,可透過 SessionManager.getCurrentSession() 存取。系統會根據使用者與「投放」對話方塊互動,自動建立並終止工作階段。

我們需要註冊工作階段事件監聽器,並初始化一些會在活動中使用的變數。將 LocalPlayerActivity onCreate 方法變更為:

import com.google.android.gms.cast.framework.CastContext
...

private var mCastContext: CastContext? = null
...

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    mCastContext = CastContext.getSharedInstance(this)
    mCastSession = mCastContext!!.sessionManager.currentCastSession
    setupCastListener()
    ...
    loadViews()
    ...
    val bundle = intent.extras
    if (bundle != null) {
        ....
        if (shouldStartPlayback) {
              ....

        } else {
            if (mCastSession != null && mCastSession!!.isConnected()) {
                updatePlaybackLocation(PlaybackLocation.REMOTE)
            } else {
                updatePlaybackLocation(PlaybackLocation.LOCAL)
            }
            mPlaybackState = PlaybackState.IDLE
            updatePlayButton(mPlaybackState)
        }
    }
    ...
}

正在載入媒體

在 Cast SDK 中,RemoteMediaClient 提供了一組便利的 API,可用來管理接收器上的遠端媒體播放。對於支援媒體播放的 CastSession,SDK 會自動建立 RemoteMediaClient 的執行個體。只要在 CastSession 執行個體上呼叫 getRemoteMediaClient() 方法,即可進行存取。將下列方法新增至 LocalPlayerActivity,即可在接收器上載入目前選取的影片:

import com.google.android.gms.cast.framework.media.RemoteMediaClient
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaLoadOptions
import com.google.android.gms.cast.MediaMetadata
import com.google.android.gms.common.images.WebImage
import com.google.android.gms.cast.MediaLoadRequestData

private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    if (mCastSession == null) {
        return
    }
    val remoteMediaClient = mCastSession!!.remoteMediaClient ?: return
    remoteMediaClient.load( MediaLoadRequestData.Builder()
                .setMediaInfo(buildMediaInfo())
                .setAutoplay(autoPlay)
                .setCurrentTime(position.toLong()).build())
}

private fun buildMediaInfo(): MediaInfo? {
    val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
    mSelectedMedia?.studio?.let { movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, it) }
    mSelectedMedia?.title?.let { movieMetadata.putString(MediaMetadata.KEY_TITLE, it) }
    movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia!!.getImage(0))))
    movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia!!.getImage(1))))
    return mSelectedMedia!!.url?.let {
        MediaInfo.Builder(it)
            .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
            .setContentType("videos/mp4")
            .setMetadata(movieMetadata)
            .setStreamDuration((mSelectedMedia!!.duration * 1000).toLong())
            .build()
    }
}

現在更新各種現有方法,以使用投放工作階段邏輯支援遠端播放:

private fun play(position: Int) {
    startControllersTimer()
    when (mLocation) {
        PlaybackLocation.LOCAL -> {
            mVideoView!!.seekTo(position)
            mVideoView!!.start()
        }
        PlaybackLocation.REMOTE -> {
            mPlaybackState = PlaybackState.BUFFERING
            updatePlayButton(mPlaybackState)
            //seek to a new position within the current media item's new position 
            //which is in milliseconds from the beginning of the stream
            mCastSession!!.remoteMediaClient?.seek(position.toLong())
        }
        else -> {}
    }
    restartTrickplayTimer()
}
private fun togglePlayback() {
    ...
    PlaybackState.IDLE -> when (mLocation) {
        ...
        PlaybackLocation.REMOTE -> {
            if (mCastSession != null && mCastSession!!.isConnected) {
                loadRemoteMedia(mSeekbar!!.progress, true)
            }
        }
        else -> {}
    }
    ...
}
override fun onPause() {
    ...
    mCastContext!!.sessionManager.removeSessionManagerListener(
                mSessionManagerListener!!, CastSession::class.java)
}
override fun onResume() {
    Log.d(TAG, "onResume() was called")
    mCastContext!!.sessionManager.addSessionManagerListener(
            mSessionManagerListener!!, CastSession::class.java)
    if (mCastSession != null && mCastSession!!.isConnected) {
        updatePlaybackLocation(PlaybackLocation.REMOTE)
    } else {
        updatePlaybackLocation(PlaybackLocation.LOCAL)
    }
    super.onResume()
}

如果是 updatePlayButton 方法,請變更 isConnected 變數的值:

private fun updatePlayButton(state: PlaybackState?) {
    ...
    val isConnected = (mCastSession != null
                && (mCastSession!!.isConnected || mCastSession!!.isConnecting))
    ...
}

現在按一下 Android Studio 的「Run」按鈕,綠色三角形指向右邊「Run」按鈕,即可在行動裝置上執行應用程式。連線至投放裝置,然後開始播放影片。接收端應會顯示影片正在播放。

7. 迷你控制器

根據投放設計檢查清單,所有 Cast 應用程式都必須提供迷你控制器,當使用者離開目前的內容頁面時就會顯示。迷你控制器可為目前投放工作階段提供即時存取和顯示提醒。

Android 手機底部插圖,顯示「投放影片」應用程式中的迷你播放器

Cast SDK 提供自訂檢視區塊 MiniControllerFragment,可將顯示迷你控制器的活動新增到應用程式版面配置檔案中。

請在 res/layout/player_activity.xmlres/layout/video_browser.xml 的底部新增下列片段定義:

<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"/>

按一下 Android Studio 的「Run」按鈕,綠色三角形指向右邊「Run」按鈕,即可執行應用程式並投放影片。接收器開始播放後,每個活動底部都會顯示迷你控制器。您可以使用迷你控制器來控制遠端播放。如果您在瀏覽活動和本機播放器活動之間瀏覽,迷你控制器狀態應與接收端媒體播放狀態保持同步。

8. 通知與螢幕鎖定

安裝 Google Cast 設計檢查清單時,傳送者應用程式必須執行來自通知螢幕鎖定畫面的媒體控制項。

插圖:Android 手機顯示通知區中的媒體控制項

Cast SDK 提供 MediaNotificationService,可協助傳送者應用程式為通知和螢幕鎖定畫面建立媒體控制項。服務會自動透過 Gradle 合併至應用程式的資訊清單中。

傳送者投放內容時,MediaNotificationService 會在背景中執行,並顯示通知,當中包含目前投放項目的圖片縮圖和中繼資料、播放/暫停按鈕以及停止按鈕。

初始化 CastContext 時,可以透過 CastOptions 啟用通知和螢幕鎖定控制項。根據預設,系統會開啟通知和螢幕鎖定畫面的媒體控制項。只要通知保持開啟,螢幕鎖定畫面功能就會開啟。

編輯 CastOptionsProvider,並變更 getCastOptions 實作方式以符合以下程式碼:

import com.google.android.gms.cast.framework.media.CastMediaOptions
import com.google.android.gms.cast.framework.media.NotificationOptions

override fun getCastOptions(context: Context): CastOptions {
   val notificationOptions = NotificationOptions.Builder()
            .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
            .build()
    val mediaOptions = CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .build()
   return CastOptions.Builder()
                .setReceiverApplicationId(context.getString(R.string.app_id))
                .setCastMediaOptions(mediaOptions)
                .build()
}

按一下 Android Studio 的「Run」按鈕,綠色三角形指向右邊「Run」按鈕,在行動裝置上執行應用程式。投放影片並離開範例應用程式。接收器上應會顯示目前正在播放的影片的通知。鎖定行動裝置和螢幕鎖定畫面,現在投放裝置會顯示媒體的播放控制項。

插圖:Android 手機在螢幕鎖定畫面上顯示媒體控制項

9. 簡介重疊

根據 Google Cast 設計檢查清單的規定,傳送者應用程式必須向現有使用者導入「投放」按鈕,才能告知傳送者應用程式現已支援投放功能,同時協助初次接觸 Google Cast 的使用者。

插圖:顯示在 Android 版「投放影片」應用程式「投放」按鈕周圍的投放簡介

Cast SDK 提供自訂檢視畫面 IntroductoryOverlay,可用於在使用者首次看到「投放」按鈕時醒目顯示。在 VideoBrowserActivity 中加入以下程式碼:

import com.google.android.gms.cast.framework.IntroductoryOverlay
import android.os.Looper

private var mIntroductoryOverlay: IntroductoryOverlay? = null

private fun showIntroductoryOverlay() {
    mIntroductoryOverlay?.remove()
    if (mediaRouteMenuItem?.isVisible == true) {
       Looper.myLooper().run {
           mIntroductoryOverlay = com.google.android.gms.cast.framework.IntroductoryOverlay.Builder(
                    this@VideoBrowserActivity, mediaRouteMenuItem!!)
                   .setTitleText("Introducing Cast")
                   .setSingleTime()
                   .setOnOverlayDismissedListener(
                           object : IntroductoryOverlay.OnOverlayDismissedListener {
                               override fun onOverlayDismissed() {
                                   mIntroductoryOverlay = null
                               }
                          })
                   .build()
          mIntroductoryOverlay!!.show()
        }
    }
}

現在請新增 CastStateListener,並在投放裝置可用時呼叫 showIntroductoryOverlay 方法,並覆寫 onResumeonPause 方法,使其符合以下條件:onCreate

import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.cast.framework.CastStateListener

private var mCastStateListener: CastStateListener? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.video_browser)
    setupActionBar()
    mCastStateListener = object : CastStateListener {
            override fun onCastStateChanged(newState: Int) {
                if (newState != CastState.NO_DEVICES_AVAILABLE) {
                    showIntroductoryOverlay()
                }
            }
        }
    mCastContext = CastContext.getSharedInstance(this)
}

override fun onResume() {
    super.onResume()
    mCastContext?.addCastStateListener(mCastStateListener!!)
}

override fun onPause() {
    super.onPause()
    mCastContext?.removeCastStateListener(mCastStateListener!!)
}

清除應用程式資料或從裝置中移除該應用程式。接著,按一下 Android Studio 的「Run」按鈕,綠色三角形指向右邊「Run」按鈕,在行動裝置上執行應用程式,接著應該會看到簡介重疊畫面 (如果疊加畫面未顯示,請清除應用程式資料)。

10. 展開控制器

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

在 Android 手機上播放影片的插圖,控制器是展開的控制器重疊

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

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

<?xml version="1.0" encoding="utf-8"?>

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

com.google.sample.cast.refplayer 套件中建立新套件 expandedcontrols。接著,在 com.google.sample.cast.refplayer.expandedcontrols 套件中建立名為 ExpandedControlsActivity.kt 的新檔案。

package com.google.sample.cast.refplayer.expandedcontrols

import android.view.Menu
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
import com.google.sample.cast.refplayer.R
import com.google.android.gms.cast.framework.CastButtonFactory

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

現在,請在 OPTIONS_PROVIDER_CLASS_NAME 上方的 application 標記內的 AndroidManifest.xml 中宣告 ExpandedControlsActivity

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

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

import com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity

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()
}

更新 LocalPlayerActivity loadRemoteMedia 方法,在載入遠端媒體時顯示 ExpandedControlsActivity

import com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity

private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    if (mCastSession == null) {
        return
    }
    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(buildMediaInfo())
                .setAutoplay(autoPlay)
                .setCurrentTime(position.toLong()).build())
}

按一下 Android Studio 的「Run」按鈕,綠色三角形指向右邊「Run」按鈕,在行動裝置上執行應用程式並投放影片。您應該會看到展開的控制器。回到影片清單後,在您按一下迷你控制器時,系統會再次載入展開的控制器。離開應用程式即可查看通知。按一下通知圖片即可載入展開的控制器。

11. 新增 Cast Connect 支援

Cast Connect 程式庫可讓現有的發送者應用程式透過 Cast 通訊協定與 Android TV 應用程式進行通訊。Cast Connect 是以 Cast 基礎架構為基礎,並將 Android TV 應用程式做為接收器使用。

依附元件

注意:如要實作 Cast Connect,play-services-cast-framework 必須為 19.0.0 以上。

LaunchOptions

如要啟動 Android TV 應用程式 (也稱為 Android 接收器),我們需要將 LaunchOptions 物件中的 setAndroidReceiverCompatible 標記設為 true。這個 LaunchOptions 物件會指定接收器的啟動方式,並傳遞至 CastOptionsProvider 類別傳回的 CastOptions。如果將上述標記設為 false,系統將在 Cast 開發人員控制台啟動已定義應用程式 ID 的網路接收器。

CastOptionsProvider.kt 檔案中,將以下內容新增至 getCastOptions 方法:

import com.google.android.gms.cast.LaunchOptions
...
val launchOptions = LaunchOptions.Builder()
            .setAndroidReceiverCompatible(true)
            .build()
return new CastOptions.Builder()
        .setLaunchOptions(launchOptions)
        ...
        .build()

設定啟動憑證

您可以在傳送者端指定 CredentialsData 來代表會議參與者。credentials 是一個可由使用者定義的字串,只要您的 ATV 應用程式能夠解讀即可。CredentialsData 只會在啟動或加入時傳遞至 Android TV 應用程式。如果你在連線時再次設定 Wi-Fi,系統就不會將這部裝置傳送到 Android TV 應用程式。

如要設定啟動憑證 CredentialsData,必須定義並傳遞至 LaunchOptions 物件。將下列程式碼新增至 CastOptionsProvider.kt 檔案的 getCastOptions 方法中:

import com.google.android.gms.cast.CredentialsData
...

val credentialsData = CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build()
val launchOptions = LaunchOptions.Builder()
       ...
       .setCredentialsData(credentialsData)
       .build()

設定 LoadRequest 上的憑證

如果 Web Receiver 應用程式和 Android TV 應用程式以不同方式處理 credentials,您可能需要分別為兩者定義 credentials。為此,請在 LocalPlayerActivity.kt 檔案的 loadRemoteMedia 函式下新增下列程式碼:

remoteMediaClient.load(MediaLoadRequestData.Builder()
       ...
       .setCredentials("user-credentials")
       .setAtvCredentials("atv-user-credentials")
       .build())

視傳送者所投放的接收端應用程式而定,SDK 現在會自動處理目前工作階段要使用的憑證。

正在測試 Cast Connect

在 Chromecast (支援 Google TV) 上安裝 Android TV APK 的步驟

  1. 找出 Android TV 裝置的 IP 位址。通常可以在「設定」>「網路和網際網路」>「(裝置連上的網路名稱)」底下找到這個 ID。畫面右側會顯示詳細資料和裝置這個網路 IP。
  2. 使用終端機透過 ADB 使用裝置的 IP 位址連線至該裝置:
$ adb connect <device_ip_address>:5555
  1. 在終端機視窗中,前往頂層資料夾,以取得您在本程式碼研究室開始時下載的程式碼研究室範例。例如:
$ cd Desktop/android_codelab_src
  1. 請執行下列指令,將這個資料夾中的 .apk 檔案安裝至 Android TV:
$ adb -s <device_ip_address>:5555 install android-tv-app.apk
  1. 現在,你應該可以在 Android TV 裝置的「您的應用程式」選單中,透過「投放影片」名稱找到應用程式。
  2. 返回 Android Studio 專案,然後按一下「Run」按鈕,在實體行動裝置上安裝及執行傳送者應用程式。按一下右上角的「投放」圖示,然後從可用選項中選取您的 Android TV 裝置。Android TV 裝置上應該已啟動 Android TV 應用程式並播放影片,你應該可以使用 Android TV 遙控器控制影片播放。

12. 自訂 Cast 小工具

你可以設定顏色、設定按鈕、文字和縮圖外觀的樣式,以及選擇要顯示的按鈕類型,藉此自訂投放小工具

更新「res/values/styles_castvideo.xml

<style name="Theme.CastVideosTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="mediaRouteTheme">@style/CustomMediaRouterTheme</item>
    <item name="castIntroOverlayStyle">@style/CustomCastIntroOverlay</item>
    <item name="castMiniControllerStyle">@style/CustomCastMiniController</item>
    <item name="castExpandedControllerStyle">@style/CustomCastExpandedController</item>
    <item name="castExpandedControllerToolbarStyle">
        @style/ThemeOverlay.AppCompat.ActionBar
    </item>
    ...
</style>

宣告下列自訂主題:

<!-- Customize Cast Button -->
<style name="CustomMediaRouterTheme" parent="Theme.MediaRouter">
    <item name="mediaRouteButtonStyle">@style/CustomMediaRouteButtonStyle</item>
</style>
<style name="CustomMediaRouteButtonStyle" parent="Widget.MediaRouter.Light.MediaRouteButton">
    <item name="mediaRouteButtonTint">#EEFF41</item>
</style>

<!-- Customize Introductory Overlay -->
<style name="CustomCastIntroOverlay" parent="CastIntroOverlay">
    <item name="castButtonTextAppearance">@style/TextAppearance.CustomCastIntroOverlay.Button</item>
    <item name="castTitleTextAppearance">@style/TextAppearance.CustomCastIntroOverlay.Title</item>
</style>
<style name="TextAppearance.CustomCastIntroOverlay.Button" parent="android:style/TextAppearance">
    <item name="android:textColor">#FFFFFF</item>
</style>
<style name="TextAppearance.CustomCastIntroOverlay.Title" parent="android:style/TextAppearance.Large">
    <item name="android:textColor">#FFFFFF</item>
</style>

<!-- Customize Mini Controller -->
<style name="CustomCastMiniController" parent="CastMiniController">
    <item name="castShowImageThumbnail">true</item>
    <item name="castTitleTextAppearance">@style/TextAppearance.AppCompat.Subhead</item>
    <item name="castSubtitleTextAppearance">@style/TextAppearance.AppCompat.Caption</item>
    <item name="castBackground">@color/accent</item>
    <item name="castProgressBarColor">@color/orange</item>
</style>

<!-- Customize Expanded Controller -->
<style name="CustomCastExpandedController" parent="CastExpandedController">
    <item name="castButtonColor">#FFFFFF</item>
    <item name="castPlayButtonDrawable">@drawable/cast_ic_expanded_controller_play</item>
    <item name="castPauseButtonDrawable">@drawable/cast_ic_expanded_controller_pause</item>
    <item name="castStopButtonDrawable">@drawable/cast_ic_expanded_controller_stop</item>
</style>

13. 恭喜

您已瞭解如何在 Android 上透過 Cast SDK 小工具啟用影片應用程式投放功能。

詳情請參閱「Android 寄件者」開發人員指南。