Google 致力于为黑人社区推动种族平等。查看具体举措

Cast Connect 与 ATV 应用

googlecastnew500.png

在此 codelab 中,您将学习如何修改现有的 Android TV 应用,使您现有的 Cast 发送器应用可以投射内容并与 Android TV 应用通信。

什么是 Google Cast 和 Cast Connect?

Google Cast 让用户可以将移动设备上的内容投射到电视上。典型的 Google Cast 会话由以下两个组件构成:发送器应用和接收器应用。发送器应用(例如移动应用或 YouTube.com 等网站)会启动和控制 Cast 接收器应用的播放功能。Cast 接收器应用是可在 Chromecast 和 Android TV 设备上运行的 HTML 5 应用。

Cast 会话中的所有状态几乎都存储在接收器应用中。当状态更新时(例如,加载了新的媒体内容),系统就会以广播的形式向所有发送器发送一条媒体状态通知。这些广播中包含 Cast 会话的当前状态。发送器应用将使用此媒体状态,在其界面中显示播放信息。

Cast Connect 在此基础架构之上构建,并以 Android TV 应用作为接收器。通过使用 Cast Connect 库,您的 Android TV 应用就可以如同 Cast 接收器应用一般接收信息和广播媒体状态。

构建目标

完成此 Codelab 之后,您将可以使用 Cast 发送器应用将视频投射到 Android TV 应用中。Android TV 应用还可以通过 Cast 协议与发送器应用通信。

学习内容

  • 如何将 Cast Connect 库添加到示例 ATV 应用。
  • 如何连接 Cast 发送器和启动 ATV 应用。
  • 如何通过 Cast 发送器应用在 ATV 应用中启动媒体播放。
  • 如何从 ATV 应用将媒体状态发送到 Cast 发送器应用。

所需条件

您可以将所有示例代码下载到您的计算机

下载源代码

解压缩下载的 zip 文件。

首先,我们来看一下完成后的示例应用是什么样子。Android TV 应用使用 Leanback 界面和一个基本的视频播放器。用户可以从列表中选择视频,选择的视频将在电视上进行播放。若使用配套的手机发送器应用,用户还可以将视频投射到 Android TV 应用中。

f9f98aa234e84bae.png

注册开发者设备

为了能够在应用开发中使用 Cast Connect 功能,您必须注册 Android TV 设备内置 Chromecast 的序列号,在 Cast Developer Console 中您将需要使用该序列号。您可以在 Android TV 上通过以下方法查找该序列号:设置 > 设备首选项 > 内置 Chromecast > 序列号。请注意,该序列号与物理设备的序列号不同,必须通过上述方法获取。

f74dfe19bc459b76.png

如果未注册,出于安全原因,Cast Connect 将仅适用于从 Google Play 商店安装的应用。启动注册过程 15 分钟后,重启设备。

安装 Android 发送器应用

为了在移动设备上测试发送请求,我们提供了一个名为“Cast Videos”的简单发送器应用。我们将使用 adb 安装 APK。如果您已经安装了其他版本的 Cast Videos,请先从设备上的所有个人资料中卸载该版本,然后再继续操作。

  1. 在 Android 手机上启用开发者选项和 USB 调试
  2. 插入 USB 数据线,将 Android 手机与开发计算机相连接。
  3. mobile-sender.apk 安装到您的 Android 手机上。

93e35a0f0332f290.png

  1. 您可以在 Android 手机上找到 Cast Videos 发送器应用。e29d89df484d9661.png

d6a0435ec3bac0af.png

安装 Android TV 应用

下面介绍如何在 Android Studio 中打开和运行已完成的示例应用:

  1. 在欢迎屏幕上选择 Import Project 或选择 File > New > Import Project... 菜单选项。
  2. 从示例代码文件夹中选择 android_studio_folder.pngapp-done 目录,然后点击“OK”。
  3. 点击 File > 1791b5212a8947d.png Sync Project with Gradle Files
  4. 在您的 Android TV 设备上启用开发者选项和 USB 调试
  5. adb 与您的 Android TV 设备连接,然后设备应显示在 Android Studio 中。7bcf00bfb6877ad5.png
  6. 点击 execute.pngRun 按钮,几秒钟后,您应该会看到屏幕上出现一个名为 Cast Connect Codelab 的 ATV 应用。

玩转 Cast Connect 与 ATV 应用

  1. 转到 Android TV 主屏幕。
  2. 打开 Android 手机上的 Cast Videos 发送器应用。点击“投射”按钮 f77992b2cf0422a2.png,然后选择您的 ATV 设备。
  3. Cast Connect Codelab ATV 应用将在您的 ATV 上启动,同时您的发送器上的“投射”按钮将显示该应用已连接 303287388679d79b.png
  4. 从 ATV 应用中选择一个视频,该视频将会在您的 ATV 上开始播放。
  5. 在手机上,您的发送器应用的底部此时会显示一个迷你控制器。您可以使用播放/暂停按钮来控制播放。
  6. 从手机中选择并播放一个视频。视频将在 ATV 上开始播放,展开后的控制器会显示在手机发送器应用上。
  7. 锁定手机后进行解锁时,您应该会在锁定屏幕上看到一条通知,用于控制媒体播放或停止投射。

a20257e816c913a.png

现在,我们已验证完成后的应用对 Cast Connect 的集成,接下来需要向您下载的启动应用中添加 Cast Connect 支持。现在,您可以使用 Android Studio 在启动项目的基础上进行项目构建了:

  1. 在欢迎屏幕上选择 Import Project 或选择 File > New > Import Project... 菜单选项。
  2. 从示例代码文件夹中选择 android_studio_folder.pngapp-start 目录,然后点击“确定”。
  3. 点击文件 > 1791b5212a8947d.png Sync Project with Gradle Files
  4. 选择 ATV 设备,然后点击 execute.pngRun 按钮以运行应用并浏览界面。7bcf00bfb6877ad5.png

f9f98aa234e84bae.png

应用设计

此应用可提供供用户浏览的视频列表。用户可以选择要在 Android TV 上播放的视频。此应用包含两个主要 activity:MainActivityPlaybackActivity

MainActivity

此 activity 包含一个 fragment (MainFragment)。系统会在 MovieList 类中配置视频列表及其关联的元数据,并调用 setupMovies() 方法来构建 Movie 对象列表。

Movie 对象表示包含标题、说明、图片缩略图和视频网址的视频实体。每个 Movie 对象都绑定到 CardPresenter,以显示带有标题和工作室的视频缩略图,并传递给 ArrayObjectAdapter

选择某一项后,对应的 Movie 对象会被传递到 PlaybackActivity

PlaybackActivity

此 activity 包含一个 fragment (PlaybackVideoFragment),用于托管一个带有 ExoPlayerVideoView、一些媒体控件,以及一个用于展示对选定视频的说明的文本区域,用户通过使用它可在 Android TV 上播放视频。用户可以使用遥控器来播放/暂停视频或者跳转视频播放。

Cast Connect 的前提条件

Cast Connect 使用新版本的 Google Play 服务,要求您必须更新 ATV 应用才能使用 AndroidX 命名空间。

如需在 Android TV 应用中支持 Cast Connect,您必须创建并支持媒体会话中的事件。Cast Connect 库会根据媒体会话的状态生成媒体状态。Cast Connect 库还会使用媒体会话,在收到发送器发送来的特定信息(例如暂停)时发出信号。

依赖项

更新应用的 build.gradle 文件以包含必要的库依赖项:

dependencies {
    ....

    // Cast Connect libraries and dependencies
    implementation 'com.google.android.gms:play-services-cast-tv:17.0.0'
    implementation 'com.google.android.gms:play-services-cast:19.0.0'
}

同步项目以确认项目构建没有错误。

初始化

CastReceiverContext 是一个单例对象,用于协调所有 Cast 交互。您必须实现 ReceiverOptionsProvider 界面,以在系统初始化 CastReceiverContext 时提供 CastReceiverOptions

创建 CastReceiverOptionsProvider.java 文件并将下列类添加到项目中:

package com.google.sample.cast.castconnect;

import android.content.Context;
import com.google.android.gms.cast.tv.CastReceiverOptions;
import com.google.android.gms.cast.tv.ReceiverOptionsProvider;

public class CastReceiverOptionsProvider implements ReceiverOptionsProvider {
    @Override
    public CastReceiverOptions getOptions(Context context) {
        return new CastReceiverOptions.Builder(context)
                .setStatusText("Cast Connect Codelab")
                .build();
    }
}

然后在该应用的 AndroidManifest.xml 文件的 <application> 标记中指定接收器选项提供程序:

<application>
  ...

  <meta-data
    android:name="com.google.android.gms.cast.tv.RECEIVER_OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.google.sample.cast.castconnect.CastReceiverOptionsProvider" />
</application>

如需从您的 Cast 发送器连接您的 ATV 应用,请选择要启动的 activity。在此 Codelab 中,我们将在 Cast 会话启动时,启动该应用的 MainActivity。在 AndroidManifest.xml 文件中,将启动 intent 过滤器添加在 MainActivity 中。

<activity android:name=".MainActivity">
  ...
  <intent-filter>
    <action android:name="com.google.android.gms.cast.tv.action.LAUNCH" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

Cast Receiver 上下文生命周期

您应该在应用启动时启动 CastReceiverContext,并在应用移至后台时停止 CastReceiverContext。我们建议您使用 androidx.lifecycle 库中的 LifecycleObserver 来管理调用 CastReceiverContext.start()CastReceiverContext.stop()

打开 MyApplication.java,通过在应用的 onCreate 方法中调用 initInstance() 来初始化投射上下文。在 AppLifeCycleObserver 类中,在应用恢复时,通过调用 start() 启动 CastReceiverContext;在应用暂停时,通过调用 stop() 停止 CastReceiverContext:

package com.google.sample.cast.castconnect;

...

import com.google.android.gms.cast.tv.CastReceiverContext;

public class MyApplication extends Application {

    private static final String LOG_TAG = "MyApplication";

    @Override
    public void onCreate() {
        super.onCreate();
        CastReceiverContext.initInstance(this);
        ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
    }

    public static class AppLifecycleObserver implements DefaultLifecycleObserver {
        @Override
        public void onResume(@NonNull LifecycleOwner owner) {
            Log.d(LOG_TAG, "onResume");
            CastReceiverContext.getInstance().start();
        }

        @Override
        public void onPause(@NonNull LifecycleOwner owner) {
            Log.d(LOG_TAG, "onPause");
            CastReceiverContext.getInstance().stop();
        }
    }
}

将 MediaSession 连接到 MediaManager

MediaManagerCastReceiverContext 单例的一个属性,用于管理媒体状态,处理加载 intent,将来自发送器的媒体命名空间消息转换为媒体命令,并将媒体状态发送回发送器。

创建 MediaSession 时,您还需要向 MediaManager 提供当前的 MediaSession 令牌,以便它知道要在何处发送命令以及检索媒体播放状态。在将令牌设置为 MediaManager 之前,请确保已初始化 MediaSession

import com.google.android.gms.cast.tv.CastReceiverContext;
...

public class PlaybackVideoFragment extends VideoSupportFragment {

    private CastReceiverContext castReceiverContext;
    ...

    private void initializePlayer() {
        if (mPlayer == null) {
            ...
            mMediaSession = new MediaSessionCompat(getContext(), LOG_TAG);
            ...

            castReceiverContext = CastReceiverContext.getInstance();
            if (castReceiverContext != null) {
                MediaManager mediaManager = castReceiverContext.getMediaManager();
                mediaManager.setSessionCompatToken(mMediaSession.getSessionToken());
            }
        }
    }
}

在因播放挂起而释放 MediaSession 时,您应在 MediaManager 上设置 null 令牌:

private void releasePlayer() {
    if (mMediaSession != null) {
        mMediaSession.release();
    }
    if (castReceiverContext != null) {
        MediaManager mediaManager = castReceiverContext.getMediaManager();
        mediaManager.setSessionCompatToken(null);
    }
    ...
}

运行示例应用

点击 execute.pngRun 按钮,然后在 ATV 设备上部署应用,关闭应用并返回 ATV 主屏幕。在您的发送器上,点击投射按钮 f77992b2cf0422a2.png,然后选择您的 ATV 设备。您会看到 ATV 应用已在 ATV 设备上启动,并且投射按钮状态已连接。

通过 intent,使用您在开发者控制台中定义的软件包名称发送加载命令。您需要在 Android TV 应用中添加以下预定义的 intent 过滤器,以指定接收此 intent 的目标 activity。在 AndroidManifest.xml 文件中,将加载 intent 过滤器添加到 PlayerActivity

<activity android:name="com.google.sample.cast.castconnect.PlaybackActivity"
          android:launchMode="singleTask">
  <intent-filter>
     <action android:name="com.google.android.gms.cast.tv.action.LOAD"/>
     <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

在 Android TV 上处理加载请求

现在,activity 已配置为接收包含加载请求的 intent,我们需要对其进行处理。

当 activity 启动时,应用会调用名为 processIntent 的专有方法。此方法包含用于处理传入 intent 的逻辑。若要处理加载请求,我们需要修改此方法,并通过调用 MediaManager 实例的 onNewIntent 方法发送 intent 进行进一步处理。如果 MediaManager 检测到 intent 是加载请求,则会从此 intent 中提取 MediaLoadRequestData 对象并调用 MediaLoadCommandCallback.onLoad()。修改 PlaybackVideoFragment 中的 processIntent 方法,以处理包含加载请求的 intent:

public void processIntent(Intent intent) {
    MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager();
    // Pass intent to Cast SDK
    if (mediaManager.onNewIntent(intent)) {
        return;
    }

    // Clears all overrides in the modifier.
    mediaManager.getMediaStatusModifier().clear();

    // If the SDK doesn't recognize the intent, handle the intent with your own logic.
    ...
}

接下来,我们将扩展抽象类 MediaLoadCommandCallback,这将替换 MediaManager 调用的 onLoad() 方法。此方法用于接收加载请求的数据,并将其转换为 Movie 对象。转换后,影片将由本地播放器播放。然后,系统会利用 MediaLoadRequest 更新 MediaManager,并向连接的发送器广播 MediaStatus。在 PlaybackVideoFragment 中创建名为 MyMediaLoadCommandCallback 的专有嵌套类:

private class MyMediaLoadCommandCallback extends MediaLoadCommandCallback {
    @Override
    public Task<MediaLoadRequestData> onLoad(String senderId, MediaLoadRequestData mediaLoadRequestData) {
        Toast.makeText(getActivity(), "onLoad()", Toast.LENGTH_SHORT).show();

        if (mediaLoadRequestData == null) {
            // Throw MediaException to indicate load failure.
            return Tasks.forException(new MediaException(
                    new MediaError.Builder()
                            .setDetailedErrorCode(MediaError.DetailedErrorCode.LOAD_FAILED)
                            .setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
                            .build()));
        }

        return Tasks.call(() -> {
            play(convertLoadRequestToMovie(mediaLoadRequestData));

            // Update media metadata and state
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            mediaManager.setDataFromLoad(mediaLoadRequestData);
            mediaManager.broadcastMediaStatus();

            // Return the resolved MediaLoadRequestData to indicate load success.
            return mediaLoadRequestData;
        });
    }
}

private Movie convertLoadRequestToMovie(MediaLoadRequestData mediaLoadRequestData) {
    if (mediaLoadRequestData == null) {
        return null;
    }
    MediaInfo mediaInfo = mediaLoadRequestData.getMediaInfo();
    if (mediaInfo == null) {
        return null;
    }

    String videoUrl = mediaInfo.getContentId();
    if (mediaInfo.getContentUrl() != null) {
        videoUrl = mediaInfo.getContentUrl();
    }

    MediaMetadata metadata = mediaInfo.getMetadata();
    Movie movie = new Movie();
    movie.setVideoUrl(videoUrl);
    if (metadata != null) {
        movie.setTitle(metadata.getString(MediaMetadata.KEY_TITLE));
        movie.setDescription(metadata.getString(MediaMetadata.KEY_SUBTITLE));
        movie.setCardImageUrl(metadata.getImages().get(0).getUrl().toString());
    }
    return movie;
}

已定义回调后,我们需要将它注册到 MediaManager 中。回调必须在调用 MediaManager.onNewIntent() 之前完成注册。在播放器初始化时添加 setMediaLoadCommandCallback

private void initializePlayer() {
    if (mPlayer == null) {
        ...
        mMediaSession = new MediaSessionCompat(getContext(), LOG_TAG);
        ...

        castReceiverContext = CastReceiverContext.getInstance();
        if (castReceiverContext != null) {
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            mediaManager.setSessionCompatToken(mMediaSession.getSessionToken());
            mediaManager.setMediaLoadCommandCallback(new MyMediaLoadCommandCallback());
        }
    }
}

运行示例应用

点击 execute.pngRun 按钮,然后在 ATV 设备上部署应用。在您的发送器上,点击投射按钮 f77992b2cf0422a2.png,然后选择您的 ATV 设备。ATV 应用将在 ATV 设备上启动。在移动设备上选择视频,该视频将在 ATV 上开始播放。检查您是否在显示播放控件的手机上收到了一条通知。尝试使用这些控件,例如使用暂停时,ATV 设备上的视频应被暂停。

此时,当前应用可支持与媒体会话兼容的基本命令,例如播放、暂停和跳转。但是,有一些 Cast 控件命令无法在媒体会话中使用。您需要注册 MediaCommandCallback 才能支持那些 Cast 控件命令。

在播放器初始化时,使用 setMediaCommandCallbackMyMediaCommandCallback 添加到 MediaManager 实例:

private void initializePlayer() {
        ...

        castReceiverContext = CastReceiverContext.getInstance();
        if (castReceiverContext != null) {
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            ...
            mediaManager.setMediaCommandCallback(new MyMediaCommandCallback());
        }
}

若要支持那些 Cast 控件命令,需创建 MyMediaCommandCallback 类以替换这些方法,例如 onQueueUpdate()

private class MyMediaCommandCallback extends MediaCommandCallback {
        @Override
        public Task<Void> onQueueUpdate(String senderId, QueueUpdateRequestData queueUpdateRequestData) {
            Toast.makeText(getActivity(), "onQueueUpdate()", Toast.LENGTH_SHORT).show();

            // Queue Prev / Next
            if (queueUpdateRequestData.getJump() != null) {
                Toast.makeText(getActivity(),
                        "onQueueUpdate(): Jump = " + queueUpdateRequestData.getJump(),
                        Toast.LENGTH_SHORT).show();
            }

            return super.onQueueUpdate(senderId, queueUpdateRequestData);
        }
}

修改媒体状态

Cast Connect 从媒体会话中获取基本媒体状态。若要支持高级功能,您的 Android TV 应用可以通过 MediaStatusModifier 指定和替换其他状态属性。MediaStatusModifier 将始终按您在 CastReceiverContext 中设置的 MediaSession 进行操作。

例如,在触发 onLoad 回调时指定 setMediaCommandSupported

private class MyMediaLoadCommandCallback extends MediaLoadCommandCallback {
    @Override
    public Task<MediaLoadRequestData> onLoad(String senderId, MediaLoadRequestData mediaLoadRequestData) {
        Toast.makeText(getActivity(), "onLoad()", Toast.LENGTH_SHORT).show();

        ...

        return Tasks.call(() -> {
            play(convertLoadRequestToMovie(mediaLoadRequestData));

            // Update media metadata and state
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            mediaManager.setDataFromLoad(mediaLoadRequestData);

            // Use MediaStatusModifier to provide additional information for Cast senders.
            mediaManager.getMediaStatusModifier()
                        .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT, true)
                        .setIsPlayingAd(false);

            mediaManager.broadcastMediaStatus();

            // Return the resolved MediaLoadRequestData to indicate load success.
            return mediaLoadRequestData;
        });
    }
}

在发送前拦截 MediaStatus

类似于 Web Receiver SDK 的 MessageInterceptor,您可以在 MediaManager 中指定 MediaStatusWriter,以完成对您的 MediaStatus 进行的其他修改,然后再将它广播给相连接的发送器。

例如,您可以先在 MediaStatus 中设置自定义数据,然后再发送给手机发送器:

MediaManager mediaManager = castReceiverContext.getMediaManager();
...

// Use MediaStatusInterceptor to process the MediaStatus before sending out.
mediaManager.setMediaStatusInterceptor(mediaStatusWriter -> {
    try {
        mediaStatusWriter.setCustomData(new JSONObject("{myData: 'CustomData'}"));
    } catch (JSONException e) {
        e.printStackTrace();
    }
});

现在,您已了解如何使用 Cast Connect 库对 Android TV 应用启用投射。

如需了解详情,请参阅开发者指南:https://developers.google.com/cast/docs/android_tv_receiver