Engage SDK for TV 集成指南

“继续观看”功能利用接续集群,在一个界面分组中显示来自多个应用的未看完视频以及同一电视剧季的下一集。您可以在此接续集群中展示其实体。按照本指南了解如何使用 Engage SDK 通过“继续观看”体验提升用户互动度。

准备工作

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

  1. 更新为以 API 19 或更高版本为目标平台

  2. 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
      }
    
  3. AndroidManifest.xml 文件中将 Engage 服务环境设为生产环境。

    移动设备

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

    TV

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

    <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
    
  5. 使用后台服务(例如 androidx.work)进行安排,以验证内容发布的可靠性。

  6. 为了提供流畅的观看体验,请在以下事件发生时发布继续观看数据:

    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 像素的图片可能无法显示。

val images = Arrays.asList(
    Image.Builder()
        .setImageUri(Uri.parse("http://www.example.com/entity_image1.png"))
        .setImageHeightInPixel(300)
        .setImageWidthInPixel(169)
        .build(),
    Image.Builder()
        .setImageUri(Uri.parse("http://www.example.com/entity_image2.png"))
        .setImageHeightInPixel(640)
        .setImageWidthInPixel(360)
        .build()
    // Consider adding other images for different form factors
)
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 = 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”“第 2 集”“s1”或“第 1 季”。

如果某个特定电视节目只有 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 是更新/插入 API;它会替换现有内容。如果您需要更新 ContinuationCluster 中的特定实体,则需要重新发布所有实体。

仅应为成人账号提供 ContinuationCluster 数据。仅当 AccountProfile 属于成人时发布。

跨设备同步

SyncAcrossDevices 标志用于控制是否在电视、手机、平板电脑等设备之间同步用户的 ContinuationCluster 数据。默认情况下,跨设备同步处于停用状态。

值:

  • true:系统会在用户的所有设备上共享 ContinuationCluster 数据,以提供流畅的观看体验。我们强烈建议您使用此选项,以获得最佳跨设备体验。
  • false:ContinuationCluster 数据仅限于当前设备。

媒体应用必须提供用于启用/停用跨设备同步的明确设置。向用户说明好处,并存储用户的偏好设置一次,然后在 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(
    DeleteClustersRequest.Builder()
        .setAccountProfile(AccountProfile())
        .setReason(DeleteReason.DELETE_REASON_USER_LOG_OUT)
        .setSyncAcrossDevices(true)
        .build()
)

测试

使用验证应用验证 Engage SDK 集成是否正常运行。此 Android 应用提供了一些工具,可帮助您验证数据并确认是否正确处理了广播 intent。

调用发布 API 后,请通过检查验证应用确认您的数据是否已正确发布。您的接续集群应显示为应用界面中的单独行。

  • 仅在应用的 Android 清单文件中为非生产 build 设置 Engage Service Flag
  • 安装并打开 Engage Verify 应用
  • 如果 isServiceAvailablefalse,请点击“切换”按钮以启用。
  • 输入应用的软件包名称,以便在开始发布后自动查看已发布的数据。
  • 在您的应用中测试以下操作:
    • 登录。
    • 在个人资料之间切换(如果适用)。
    • 开始播放视频,然后暂停播放,或返回首页。
    • 在视频播放期间关闭应用。
    • 从“继续观看”行中移除内容(如果受支持)。
  • 执行每项操作后,请确认您的应用调用了 publishContinuationClusters API,并且数据在验证应用中正确显示。
  • 对于正确实现的实体,验证应用会显示绿色的“一切顺利”对勾标记。

    验证应用成功屏幕截图
    图 1. 验证应用成功
  • 验证应用会标记所有有问题的实体。

    验证应用错误屏幕截图
    图 2. 验证应用错误
  • 如需排查存在错误的实体,请使用电视遥控器在验证应用中选择并点击相应实体。系统会显示具体问题并以红色突出显示,以供您查看(请参阅以下示例)。

    验证应用错误详情
    图 3. 验证应用错误详情

适用于非 Android 平台的集成

“继续观看”功能可集成来自 iOS、Roku TV 等非 Android 平台的内容。借助这些 REST API,开发者可以更新来自非 Android 平台的继续观看数据,从而跨设备提供流畅的继续观看体验。

先决条件

  1. 将 Engage-SDK 集成到设备。
  2. 在 Google Cloud 项目中启用 API。

如需查看和访问 Google Cloud 项目中的 API,请按以下步骤操作。

  1. 如果尚未设置 Google Workspace,请进行设置。
  2. 设置 Google Cloud 控制台(如果尚未设置)。
  3. 在 Google Cloud 控制台中创建一个新项目。
  4. 创建用于 API 身份验证的服务账号。
  5. 使用您的服务账号凭据进行 Delegate API 调用。

发布接续集群

使用 publishContinuationCluster API 将“继续观看”数据发布到 Google TV。

网址

使用 POST 网址进行发布。使用以下语法:

https://tvvideodiscovery.googleapis.com/v1/packages/{package_name}/accounts/{account_id}/profiles/{profile_id}/publishContinuationCluster`

其中:

  • package_name 是应用的软件包名称。
  • account_id 是唯一账号 ID。
  • profile_id 是用于区分同一账号的不同个人资料的唯一个人资料 ID。
请求正文

请求的载荷在 entities 字段中表示。entities 表示内容实体列表,可以是 MovieEntityTVEpisodeEntity。这是必填字段。

以下代码段展示了 publishContinuationCluster API 的示例。

{
  "entities": [
  {
    "movieEntity": {
    "watch_next_type": "WATCH_NEXT_TYPE_CONTINUE",
    "name": "Movie1", //data type = string
    "platform_specific_playback_uris": [],
    "poster_images": [],
    "last_engagement_time_millis": 864600000, //data type = long,
    "duration_millis": 5400000, //90 mins data type = long,
    "last_play_back_position_time_millis": 3241111 //data type = long,
    }
  },
  {
    "tvEpisodeEntity": {
      "watch_next_type": "WATCH_NEXT_TYPE_CONTINUE",
      "name": "TV SERIES EPISODE 1", //data type = string
      "platform_specific_playback_uris": [],
      "poster_images": [],
      "last_engagement_time_millis": 864600000, // data type =long,
      "duration_millis": 1800000,// data type = long,
      "last_play_back_position_time_millis": 2141231, // data type =long,
      "episode_display_number": "1",// data type string,
      "season_number": "1",// data type string,
      "show_title": ""// data type string,
    }
  },
  ],
}

删除视频发现数据

使用 clearClusters API 移除视频发现数据。 使用 clearClusters API 移除视频发现数据。

网址

使用 POST 网址从视频发现数据中移除实体。

https://tvvideodiscovery.googleapis.com/v1/packages/app_pkg_name/accounts/accountID1/profiles/profileID1/clearClusters

其中:

  • package_name 是应用的软件包名称。
  • account_id 是唯一的账号 ID
  • profile_id 是用于区分同一账号的不同付款资料的唯一付款资料 ID。
请求正文

clearClusters API 的载荷仅包含一个字段 reason,其中包含一个 DeleteReason,用于指定移除数据的原因。

{
  "reason": "DELETE_REASON_LOSS_OF_CONSENT" // data type = string
}

Google 平台将显示更新后的内容。