适用于 TV 的 Engage SDK 集成指南

“继续观看”功能利用 延续簇 在一个 UI 分组中展示来自多个应用未完成的视频和同一电视季中待观看的下一集。您可以在此延续簇中展示它们的实体。请遵循本指南,了解如何通过使用 Engage SDK 的“继续观看”体验来提升用户互动。

第 1 步:准备工作

开始之前,请完成以下步骤

确保您的应用针对此集成所用的 API 级别为 19 或更高版本

  1. com.google.android.engage 库添加到您的应用

    集成中需要使用独立的 SDK:一个用于移动应用,一个用于 TV 应用。

    移动

    
      dependencies {
        implementation 'com.google.android.engage:engage-core:1.5.5
      }
    

    TV

    
      dependencies {
        implementation 'com.google.android.engage:engage-tv:1.0.2
      }
    
  2. AndroidManifest.xml 文件中将 Engage 服务环境设置为生产模式。

    移动

    
    <meta-data
          android:name="com.google.android.engage.service.ENV"
          android:value="PRODUCTION">
    </meta-data>
    

    TV

    
    <meta-data
        android:name="com.google.android.engage.service.ENV"
        android:value="PRODUCTION">
    </meta-data>
    
  3. 为 TV APK 添加 WRITE_EPG_DATA 权限

      <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
    
  4. 通过使用后台服务(例如 androidx.work)进行调度,确保可靠的内容发布。

  5. 为提供无缝观看体验,请在发生以下事件时发布继续观看数据

    1. 首次登录:当用户首次登录时,发布其数据可确保其观看历史记录立即可用。
    2. 个人资料创建或切换(多个人资料应用):如果您的应用支持多个个人资料,请在用户创建或切换个人资料时发布数据。这可确保每个用户都有个性化体验。
    3. 视频播放中断:为帮助用户从上次离开的地方继续观看,请在他们暂停或停止视频时,或在播放期间应用退出时发布数据。
    4. “继续观看”托盘更新(如果支持):当用户从其“继续观看”托盘中移除某个项目时,通过发布更新数据来反映该更改。这可确保托盘保持相关性和个性化。
    5. 视频完成
      1. 对于电影,请从“继续观看”托盘中移除已完成的电影。如果电影是某个系列的一部分,请添加下一部电影以保持用户参与度。
      2. 对于剧集,请移除已完成的剧集,如果可用,添加系列中的下一集,以鼓励继续观看。

集成

AccountProfile

为了在 Google TV 上提供个性化的“继续观看”体验,请提供帐号和个人资料信息。使用 AccountProfile 来提供

  1. 帐号 ID:代表用户在您应用中帐号的唯一标识符。可以是实际的帐号 ID,也可以是经过适当混淆的版本。

  2. 个人资料 ID(可选):如果您的应用支持单个帐号内的多个个人资料,请为特定的用户个人资料提供唯一标识符(同样可以是真实的或经过混淆的)。

// If your app only supports account
val accountProfile = AccountProfile.Builder()
      .setAccountId("your_users_account_id")
      .build()
// If your app supports both account and profile
val accountProfile = AccountProfile.Builder()
      .setAccountId("your_users_account_id")
      .setProfileId("your_users_profile_id")
.build()

创建实体

SDK 定义了不同的实体来表示每种项目类型。“延续簇”支持以下实体

  1. MovieEntity
  2. TvEpisodeEntity
  3. LiveStreamingVideoEntity
  4. VideoClipEntity

为这些实体指定特定于平台的 URI 和海报图片。

此外,如果您尚未创建,请为每个平台(例如 Android TV、Android 或 iOS)创建播放 URI。这样,当用户在每个平台上继续观看时,应用会使用目标播放 URI 来播放视频内容。

// Required. Set this when you want continue watching entities to show up on
// Google TV
val playbackUriTv =
          PlatformSpecificUri.Builder()
              .setPlatformType(PlatformType.TYPE_ANDROID_TV)
              .setActionUri(Uri.parse("https://www.example.com/entity_uri_for_tv"))
              .build()

// Required. Set this when you want continue watching entities to show up on
// Google TV Android app, Entertainment Space, Playstore Widget
val playbackUriAndroid =
          PlatformSpecificUri.Builder()
              .setPlatformType(PlatformType.TYPE_ANDROID_MOBILE)
              .setActionUri(Uri.parse("https://www.example.com/entity_uri_for_android"))
              .build()
// Optional. Set this when you want continue watching entities to show up on
// Google TV iOS app
val playbackUriIos =
          PlatformSpecificUri.Builder()
              .setPlatformType(PlatformType.TYPE_IOS)
              .setActionUri(Uri.parse("https://www.example.com/entity_uri_for_ios"))
              .build()
 val platformSpecificPlaybackUris =
          Arrays.asList(playbackUriTv, playbackUriAndroid, playbackUriIos)

海报图片需要 URI 和像素尺寸(高度和宽度)。通过提供多张海报图片来支持不同的外形设备,但请确保所有图片都保持 16:9 的纵横比和至少 200 像素的高度,以便正确显示“继续观看”实体,尤其是在 Google 的 娱乐空间 内。高度小于 200 像素的图片可能无法显示。


Image image1 = new Image.Builder()
            .setImageUri(Uri.parse("http://www.example.com/entity_image1.png");)
            .setImageHeightInPixel(300)
            .setImageWidthInPixel(169)
            .build()
Image image2 = new Image.Builder()
            .setImageUri(Uri.parse("http://www.example.com/entity_image2.png");)
            .setImageHeightInPixel(640)
            .setImageWidthInPixel(360)
            .build()
// And other images for different form factors.
val images = Arrays.asList(image1, image2)
MovieEntity

此示例展示了如何创建包含所有必填字段的 MovieEntity

val movieEntity = MovieEntity.Builder()
   .setWatchNextType(WatchNextType.TYPE_CONTINUE)
   .setName("Movie name")
   .addPlatformSpecificPlaybackUri(platformSpecificPlaybackUris)
   .addPosterImages(images)
   // Timestamp in millis for sample last engagement time 12/1/2023 00:00:00
   .setLastEngagementTimeMillis(1701388800000)
   // Suppose the duration is 2 hours, it is 72000000 in milliseconds
   .setDurationMills(72000000)
   // Suppose last playback offset is 1 hour, 36000000 in milliseconds
   .setLastPlayBackPositionTimeMillis(36000000)
   .build()

提供类型和内容分级等详细信息,可让 Google TV 以更动态的方式展示您的内容,并将其与合适的观看者联系起来。

val genres = Arrays.asList("Action", "Science fiction");
val rating1 = RatingSystem.Builder().setAgencyName("MPAA").setRating("PG-13").build();
val contentRatings = Arrays.asList(rating1);
val movieEntity = MovieEntity.Builder()
    ...
    .addGenres(genres)
    .addContentRatings(contentRatings)
    .build()

实体会自动保留 60 天,除非您指定更短的有效期。只有在您需要在此默认期限之前移除实体时,才设置自定义有效期。

// Set the expiration time to be now plus 30 days in milliseconds
val expirationTime = new DisplayTimeWindow.Builder()
             .setEndTimestampMillis(now().toMillis()+2592000000).build()
val movieEntity = MovieEntity.Builder()
    ...
    .addAvailabilityTimeWindow(expirationTime)
    .build()
TvEpisodeEntity

此示例展示了如何创建包含所有必填字段的 TvEpisodeEntity

val tvEpisodeEntity = TvEpisodeEntity.Builder()
    .setWatchNextType(WatchNextType.TYPE_CONTINUE)
    .setName("Episode name")
    .addPlatformSpecificPlaybackUri(platformSpecificPlaybackUris)
    .addPosterImages(images)
    // Timestamp in millis for sample last engagement time 12/1/2023 00:00:00
    .setLastEngagementTimeMillis(1701388800000)
    .setDurationMills(72000000) // 2 hours in milliseconds
    // 45 minutes and 15 seconds in milliseconds is 2715000
    .setLastPlayBackPositionTimeMillis(2715000)
    .setEpisodeNumber("2")
    .setSeasonNumber("1")
    .setShowTitle("Title of the show")
    .build();

剧集编号字符串(例如 "2")和季编号字符串(例如 "1")在显示在继续观看卡片上之前会展开为正确的形式。请注意,它们应该是数字字符串,不要填写“e2”或“episode 2”,也不要填写“s1”或“season 1”。

如果某个电视节目只有一个季,请将季编号设置为 1。

为了最大限度地提高观看者在 Google TV 上找到您内容的机会,请考虑提供额外数据,例如类型、内容分级和可用时间窗口,因为这些详细信息可以增强显示和过滤选项。

val genres = Arrays.asList("Action", "Science fiction")
val rating1 = RatingSystem.Builder().setAgencyName("MPAA").setRating("PG-13").build()
val contentRatings = Arrays.asList(rating1)
val tvEpisodeEntity = TvEpisodeEntity.Builder()
    ...
    .addGenres(genres)
    .addContentRatings(contentRatings)
    .setSeasonTitle("Season Title")
    .setShowTitle("Show Title)
    .build();
VideoClipEntity

以下是创建包含所有必填字段的 VideoClipEntity 的示例。

VideoClipEntity 代表用户生成的短片,例如 YouTube 视频。

val videoClipEntity = VideoClipEntity.Builder()
    .setPlaybackUri(Uri.parse("https://www.example.com/uri_for_current_platform")
    .setWatchNextType(WatchNextType.TYPE_CONTINUE)
    .setName("Video clip name")
    .addPlatformSpecificPlaybackUri(platformSpecificPlaybackUris)
    .addPosterImages(images)
    // Timestamp in millis for sample last engagement time 12/1/2023 00:00:00
    .setLastEngagementTimeMillis(1701388800000)
    .setDurationMills(600000) //10 minutes in milliseconds
    .setLastPlayBackPositionTimeMillis(300000) //5 minutes in milliseconds
    .addContentRating(contentRating)
    .build();

您可以选择设置创作者、创作者图片、创建时间(毫秒)或可用时间窗口。

LiveStreamingVideoEntity

以下是创建包含所有必填字段的 LiveStreamingVideoEntity 的示例。

val liveStreamingVideoEntity = LiveStreamingVideoEntity.Builder()
    .setPlaybackUri(Uri.parse("https://www.example.com/uri_for_current_platform")
    .setWatchNextType(WatchNextType.TYPE_CONTINUE)
    .setName("Live streaming name")
    .addPlatformSpecificPlaybackUri(platformSpecificPlaybackUris)
    .addPosterImages(images)
    // Timestamp in millis for sample last engagement time 12/1/2023 00:00:00
    .setLastEngagementTimeMillis(1701388800000)
    .setDurationMills(72000000) //2 hours in milliseconds
    .setLastPlayBackPositionTimeMillis(36000000) //1 hour in milliseconds
    .addContentRating(contentRating)
    .build();

您可以选择设置直播流实体的开始时间、广播公司、广播公司图标或可用时间窗口。

有关属性和要求的详细信息,请参阅 API 参考

提供延续簇数据

AppEngagePublishClient 负责发布延续簇。您可以使用 publishContinuationCluster() 方法发布 ContinuationCluster 对象。

首先,您应该使用 isServiceAvailable() 来检查服务是否可用于集成。

client.publishContinuationCluster(
    PublishContinuationClusterRequest
        .Builder()
        .setContinuationCluster(
            ContinuationCluster
                .Builder()
                .setAccountProfile(accountProfile)
                .addEntity(movieEntity1)
                .addEntity(movieEntity2)
                .addEntity(tvEpisodeEntity1)
                .addEntity(tvEpisodeEntity2)
                .setSyncAcrossDevices(true)
                .build()
        )
        .build();
)

当服务收到请求时,将在一个事务中执行以下操作

  • 来自开发者伙伴的现有 ContinuationCluster 数据将被移除。
  • 来自请求的数据将被解析并存储在更新后的 ContinuationCluster 中。

如果发生错误,整个请求将被拒绝,并保持现有状态。

发布 API 是 upsert API;它会替换现有内容。如果您需要更新延续簇中的某个特定实体,则需要再次发布所有实体。

延续簇数据仅应提供给成年帐号。仅当 AccountProfile 属于成年人时才发布。

跨设备同步

SyncAcrossDevices 标志

此标志控制用户的延续簇数据是否跨设备(TV、手机、平板电脑等)同步。它默认为 false,这意味着默认情况下禁用跨设备同步。

  • true:延续簇数据在用户所有设备之间共享,以提供无缝观看体验。我们强烈建议选择此选项以获得最佳跨设备体验。
  • false:延续簇数据仅限于当前设备。

媒体应用必须提供明确的设置来启用/禁用跨设备同步。向用户解释其好处,并存储一次用户的偏好,然后相应地应用于 publishContinuationCluster。

// Example to allow cross device syncing.
client.publishContinuationCluster(
    PublishContinuationClusterRequest
        .Builder()
        .setContinuationCluster(
            ContinuationCluster
                .Builder()
                .setAccountProfile(accountProfile)
                .setSyncAcrossDevices(true)
                .build();
        )
        .build();
)

为了充分利用我们的跨设备功能,请确保您的应用获得用户同意并将 SyncAcrossDevices 启用为 true。这允许内容在设备之间无缝同步,从而带来更好的用户体验并提高互动。例如,实施此功能的合作伙伴发现“继续观看”点击量增加了 40%,因为他们的内容显示在多台设备上。

删除视频发现数据

要在标准 60 天保留期之前手动从 Google TV 服务器删除用户数据,请使用 client.deleteClusters() 方法。收到请求后,服务将删除帐号资料或整个帐号的所有现有视频发现数据。

DeleteReason 枚举定义了数据删除的原因。以下代码在注销时移除继续观看数据。


// If the user logs out from your media app, you must make the following call
// to remove continue watching data from the current google TV device,
// otherwise, the continue watching data will persist on the current
// google TV device until 60 days later.
client.deleteClusters(
  new DeleteClustersRequest.Builder()
        .setAccountProfile(AccountProfile())
        .setReason(DeleteReason.DELETE_REASON_USER_LOG_OUT)
        .setSyncAcrossDevices(true)
        .build()
)

测试

使用验证应用确保您的 Engage SDK 集成正常工作。此 Android 应用 提供了工具,可帮助您验证数据并确认广播 intent 已正确处理。

调用发布 API 后,通过检查验证应用来确认您的数据是否已正确发布。您的延续簇应作为应用界面中的一个独立行显示。

  • 确保您的应用 Android Manifest 文件中的 Engage 服务标志 设置为生产模式。
  • 安装并打开 Engage Verify 应用
  • 如果 isServiceAvailablefalse,请点击“切换”按钮启用。
  • 输入您应用的软件包名称,一旦开始发布,即可自动查看已发布数据。
  • 在您的应用中测试以下操作
    • 登录。
    • 切换个人资料(如果适用)。
    • 开始播放视频,然后暂停,或返回主页。
    • 在视频播放期间关闭应用。
    • 从“继续观看”行中移除项目(如果支持)。
  • 每次操作后,确认您的应用调用了 publishContinuationClusters API,并且数据在验证应用中正确显示。
  • 验证应用将为正确实现的实体显示绿色的“一切正常”检查标记。

    Verification App Success Screenshot
    图 1. 验证应用成功
  • 验证应用将标记任何有问题的实体。

    Verification App Error Screenshot
    图 2. 验证应用错误
  • 要排查有错误的实体,请使用您的电视遥控器在验证应用中选择并点击该实体。具体问题将以红色显示并突出显示,供您查看(参见以下示例)。

    Verification App error details
    图 3. 验证应用错误详情