支援投放功能的 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 mini 控制器。
  • 如何支援媒體通知和螢幕鎖定控制項。
  • 如何新增展開的控制器。
  • 如何提供簡介重疊廣告。
  • 如何自訂投放小工具。
  • 如何整合 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」>「File」新增 >匯入專案...選單選項。

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

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

在 Android 裝置上啟用 USB 偵錯功能:在 Android 4.2 以上版本中,開發人員選項畫面會預設為隱藏。如要將其顯示出來,請依序前往「Settings」>「About phone」,然後輕觸「Build number」七次。返回上一個畫面,前往「系統」>「系統」進階,然後輕觸靠近底部的「開發人員選項」,然後輕觸「USB 偵錯」開啟這項功能。

插入 Android 裝置,然後按一下 Android Studio 中的「Run」按鈕 Android Studio 的「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,而 VideoListAdapter 會在 VideoBrowserFragment 中顯示 MediaItems 清單。系統會向使用者顯示影片縮圖清單,以及每部影片的簡短說明。選取項目後,對應的 MediaItem 會轉換為 Bundle,並傳遞至 LocalPlayerActivity

LocalPlayerActivity

這類活動會顯示特定影片的中繼資料,並允許使用者透過行動裝置本機播放影片。

活動會代管 VideoView、部分媒體控制項,以及用於顯示所選影片說明的文字區域。播放器會覆蓋螢幕頂端,並在下方騰出空間來顯示影片的詳細說明。使用者可以播放/暫停或跳轉本機影片。

依附元件

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

常見問題

5. 新增投放按鈕

插圖:Android 手機上方,顯示正在執行的投放影片應用程式。畫面右上角顯示「投放」按鈕

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

依附元件

更新 app 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,可協調所有 Cast 互動。

您必須實作 OptionsProvider 介面,才能提供初始化 CastContext 單例所需的 CastOptions。最重要的選項是接收器應用程式 ID,用來篩選 Cast 裝置探索結果,以及在投放工作階段啟動時啟動接收器應用程式。

開發支援 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。步驟 3 很容易透過 Cast 架構完成。步驟 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 工作階段會結合連線至裝置、啟動 (或加入)、連線至接收器應用程式,以及視情況初始化媒體控制管道的步驟。媒體控制管道是 Cast 架構向接收器媒體播放器傳送及接收訊息的方式。

使用者從「投放」按鈕選取裝置後,「投放」工作階段就會自動開始,並在使用者中斷連線時自動停止。由於網路問題而重新連線至接收器工作階段,Cast SDK 會自動處理。

讓我們在 LocalPlayerActivity 中新增 SessionManagerListener

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

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

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 設計檢查清單,所有 Cast 應用程式都必須提供迷你控制器,當使用者離開目前的內容頁面時,該控制器就會顯示。迷你控制器可讓使用者即時存取目前投放工作階段,並顯示可見提醒。

插圖:Android 手機底部顯示 Cast 影片應用程式中的迷你播放器

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。

插圖:顯示 Cast 影片 Android 應用程式中,投放按鈕周圍的 Cast 首頁疊加畫面

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

接著,請修改 onCreate 方法並覆寫 onResumeonPause 方法,以便在 Cast 裝置可用時新增 CastStateListener 並呼叫 showIntroductoryOverlay 方法,以便符合下列條件:

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

清除應用程式資料或從裝置中移除應用程式。接著,按一下「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())
}

按一下「Run」按鈕,即可在行動裝置上執行應用程式並投放影片。Android Studio 的「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 應用程式。如果在連線後再次設定 PIN 碼,就不會傳送到 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 位址。通常會在「設定」>「網路與網際網路」> (裝置連線的網路名稱) 下方。畫面右側會顯示詳細資料和裝置的 IP。
  2. 使用裝置的 IP 位址,以便透過終端機透過 ADB 連線:
$ 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 Sender 開發人員指南。