支援 Android 應用程式投放功能

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

1. 總覽

Google Cast 標誌

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

什麼是 Google Cast?

Google Cast 可讓使用者將行動裝置的內容投放到電視上。之後,使用者就能利用行動裝置做為電視媒體的遙控器。

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

提供 Google Cast 設計檢查清單,以便在所有支援的平台上提供簡單且可預測的 Cast 使用者體驗。

我們要建構的是什麼?

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

課程內容

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

軟硬體需求

  • 最新的 Android SDK
  • Android Studio 3.2 以上版本
  • 一部搭載 Android 4.1 以上版本 Jelly Bean (API 級別 16) 的行動裝置。
  • 一根用於連接行動裝置和開發電腦的 USB 資料傳輸線。
  • Google Cast 裝置,例如已設定網路連線的 ChromecastAndroid TV
  • 具備 HDMI 輸入端的電視或螢幕。
  • 必須使用 Chromecast (支援 Google TV) 才能測試 Cast Connect 整合作業,但在程式碼研究室的其餘部分則為選用功能。如果沒有電視,歡迎在本教學課程結束時略過新增 Cast Connect 支援步驟。

功能

  • 您必須先具備之前的 Kotlin 和 Android 開發知識。
  • 您也需要具備先前觀看電視節目的知識 :)

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

唯讀閱讀 閱讀並完成練習

針對打造 Android 應用程式的體驗,你會給予什麼評價?

新手 中級 專業知識

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

新手 中級 專業知識

2. 取得程式碼範例

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

將下載的 ZIP 檔案解壓縮。

3. 執行範例應用程式

一對指南針的圖示

首先,請查看完成的範例應用程式外觀。這款應用程式是基本的影片播放器。使用者可以選取清單中的影片,然後在裝置上在裝置上播放影片,或將其投放到 Google Cast 裝置。

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

在歡迎畫面中選取「Import Project」(匯入專案) 或「File > New > Import Project...」選單選項。

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

依序點選「File &gt」(檔案 &gt);Android Studio ' Sync Project with Gradle' 按鈕「Sync Project with Gradle Files」(使用 Gradle 檔案同步處理專案)

在 Android 裝置上啟用 USB 偵錯功能。在 Android 4.2 及以上版本中,預設的開發人員選項畫面為隱藏。如要取消顯示,請依序前往 [設定] > [關於手機],然後輕觸 [版本號碼] 七次。返回上一個畫面,依序前往「System > Advanced」,然後輕觸底部的「Developer options」(開發人員選項),然後輕觸「USB 偵錯」開啟這項功能。

插入 Android 裝置,然後按一下 Android Studio 中的 Android Studio 的「執行」按鈕,綠色三角形指向右側「執行」按鈕。幾秒鐘後,您就會看到名為「投放影片」的影片應用程式。

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

選取影片並點選播放按鈕。

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

系統會顯示已展開的控制器。您可以使用播放/暫停按鈕控製播放功能。

返回影片清單。

現在,畫面底部會顯示迷你控制器。插圖:一支 Android 手機放送「'投放影片'」應用程式,其迷你控制器位於畫面底部

按一下迷你控制器中的暫停按鈕,以暫停接收端上的影片。按一下迷你控制器中的播放按鈕,即可繼續播放影片。

按一下行動裝置主畫面按鈕。將通知向下拉後,畫面上會顯示投放工作階段的通知。

鎖定手機後,你會在螢幕鎖定畫面上看到通知,控制媒體播放或停止投放。

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

常見問題

4. 準備起始專案

插圖:一支 Android 手機執行了「#39;投放影片'」應用程式

我們需要針對你下載的啟動應用程式新增 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」(執行) 按鈕來執行應用程式並探索 UI。

應用程式設計

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

這個應用程式包含兩個主要活動:VideoBrowserActivityLocalPlayerActivity。如要整合 Google Cast 功能,活動必須沿用 AppCompatActivity 或其父項 FragmentActivity。這項限制的原因是,我們必須將 MediaRouteButton (在 MediaRouter 支援資料庫中提供) 做為 MediaRouteActionProvider,並且只有在活動沿用上述類別時才能運作。MediaRouter 支援資料庫依附於 AppCompat 支援資料庫,並提供必要類別。

影片瀏覽器活動

這項活動含有 Fragment (VideoBrowserFragment)。這份清單是由 ArrayAdapter (VideoListAdapter) 提供支援。影片清單及其相關中繼資料是由遠端伺服器代管為 JSON 檔案。AsyncTaskLoader (VideoItemLoader) 會擷取這個 JSON 並處理,以便建立 MediaItem 物件清單。

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

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

本機 PlayActivity

這項活動會顯示特定影片的中繼資料,並允許使用者在行動裝置上播放影片。

這個活動會代管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 已初始化,請新增「投放」按鈕,讓使用者選取投放裝置。投放按鈕是由 MediaRouteButtonMediaRouter 支援資料庫實作。就像任何可新增至活動的動作圖示 (使用 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

按一下「Run」(執行) Android Studio 的「執行」按鈕,綠色三角形指向右側 按鈕,即可在行動裝置上執行應用程式。應用程式的動作列中會顯示「投放」按鈕,當你按一下該按鈕時,即可列出區域網路上的投放裝置。CastContext 會自動管理裝置探索作業。選取投放裝置,取樣裝置應用程式隨即會在投放裝置上載入。你可以在瀏覽活動與本機播放器活動之間進行導覽,同時讓「投放」按鈕狀態保持同步。

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

6. 投放影片內容

插圖:一支 Android 手機執行了「#39;投放影片&#39;」應用程式

我們也會擴大範例應用程式的投放範圍,以便透過 Cast 裝置從遠端播放影片。因此必須監聽投放架構產生的各種事件。

投放媒體

整體來說,如果你想在投放裝置上播放媒體,必須完成以下步驟:

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

我們已經按照上一節的步驟步驟 2 完成。步驟 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 架構產生的各種事件,才能進行處理。

投放工作階段管理

如果是投放架構,「投放」工作階段會結合連線至裝置、啟動 (或加入)、連線至接收器應用程式,以及視情況初始化媒體控制頻道的步驟。媒體控制頻道是 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 活動中,我們想瞭解你在與 Cast 裝置連線或中斷連線時,可以切換使用本機播放器或從本機播放器連線。請注意,不僅在行動裝置上執行的應用程式執行個體也會中斷連線,也可能影響 (或其他) 應用程式在其他行動裝置上運作的狀況。

目前使用中的工作階段可透過 SessionManager.getCurrentSession() 存取。系統會自動建立工作階段,並根據使用者與 Cast 對話方塊的互動情形自動關閉。

我們必須註冊工作階段監聽器,並初始化要在活動中使用的部分變數。將 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」(執行) 按鈕,在行動裝置上執行應用程式。連線至投放裝置,然後播放影片。您應該會在接收器中看見播放影片。

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」(執行) 按鈕,執行應用程式並投放影片。在接收器上開始播放時,每個活動底部會顯示迷你控制器。你可以使用迷你控制器控制遠端播放。如果您在瀏覽活動與本機播放器活動之間進行導覽,迷你控制器狀態應與接收器媒體播放狀態保持同步。

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

按一下「Run」(執行) Android Studio 的「執行」按鈕,綠色三角形指向右側 按鈕,即可在行動裝置上執行應用程式。投放影片,然後離開範例應用程式。當接收器正在播放影片時,系統應該會顯示通知。鎖定行動裝置後,鎖定畫面應顯示投放裝置媒體播放的控制項。

插圖: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 方法,方法是在投放 Cast 裝置時修改 onCreate 方法,並覆寫 onResumeonPause 方法,以符合以下內容:

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 的「執行」按鈕,綠色三角形指向右側[執行] 按鈕,在行動裝置上執行應用程式,這時您應該會看到簡介簡介 (如果未顯示重疊元素,請清除應用程式資料)。

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 的「執行」按鈕,綠色三角形指向右側 按鈕,在行動裝置上執行應用程式及投放影片。您應該會看到展開的控制器。返回影片清單,當您點選迷你控制器時,系統會重新載入展開的控制器。離開應用程式查看通知。按一下通知圖片即可載入展開的控制器。

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 Developer Console 中針對定義的應用程式 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 應用程式。如果連線時再次設定,系統不會將內容傳送到 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 分別定義 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. 自訂投放小工具

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

更新「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 傳送者開發人員指南。