Engage SDK for TV 集成指南

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

准备工作

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

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

  2. com.google.android.engage 库添加到您的应用中:

    在集成过程中,您需要使用不同的 SDK:一个用于移动应用,另一个用于电视应用。

    移动设备

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

    电视

    
      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" />
    

    电视

    
    <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(可选):如果您的应用支持在单个账号中使用多个个人资料,请提供特定用户个人资料的唯一标识符(同样,可以是真实 ID,也可以是经过混淆处理的 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。

为了最大限度地提高观看者在 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 中的特定实体,则需要再次发布所有实体。

延续集群数据应仅针对成人账号提供。 仅当 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. 验证应用错误详情

REST API

Engage SDK 提供 REST API,可在 iOS、Roku TV 等非 Android 平台上提供一致的“继续观看”体验。借助此 API,开发者可以从非 Android 平台为选择启用的用户更新“继续观看”状态。

前提条件

  • 您必须先完成基于设备端 Engage SDK 的集成。此关键步骤会在 Google 的用户 ID 与应用的 AccountProfile 之间建立必要的关联。
  • API 访问权限和身份验证:如需在 Google Cloud 项目中查看和启用该 API,您必须通过许可名单流程。所有 API 请求都需要进行身份验证。

获得访问权限

如需在 Google Cloud 控制台中查看和启用该 API,您的账号需要加入该计划。

  1. Google Workspace 客户 ID 应该可用。如果不可用,您可能还需要设置 Google Workspace 以及您想要用于调用 API 的任何 Google 账号。
  2. 使用与 Google Workspace 关联的电子邮件地址通过 Google Cloud 控制台设置账号。
  3. 创建新项目
  4. 创建用于 API 身份验证的服务账号。创建服务账号后,您将获得以下两项内容:
    • 服务账号 ID。
    • 包含您的服务账号密钥的 JSON 文件。请安全保存此文件,您稍后需要使用它来对客户端进行 API 身份验证。
  5. Workspace 和关联的 Google 账号现在可以使用 REST API。 更改传播完毕后,您会收到通知,告知您服务账号是否可以调用该 API。
  6. 按照以下步骤准备进行委托的 API 调用。

发布接续集群

如需发布视频发现数据,请使用以下语法向 publishContinuationCluster API 发出 POST 请求。

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

其中:

  • package_name:媒体提供方软件包名称
  • accountId:用户账号在您系统中的唯一 ID。它必须与设备端路径中使用的 accountId 相匹配。
  • profileId:用户在您系统中的账号内的个人资料的唯一 ID。必须与设备端路径中使用的 profileId 一致。

不含个人资料的账号的网址为:

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

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

请求正文

字段

类型

必需

说明

实体

MediaEntity 对象列表

内容实体的列表(最多 5 个),系统只会保留前 5 个,其余的会被舍弃。允许使用空列表来表示用户已观看完所有实体。

字段 entities 包含各个 movieEntitytvEpisodeEntity

字段

类型

必需

说明

movieEntity

MovieEntity

表示 ContinuationCluster 中的电影的对象。

tvEpisodeEntity

TvEpisodeEntity

表示 ContinuationCluster 中的电视节目剧集的对象。

实体数组中的每个对象都必须是可用的 MediaEntity 类型之一,即 MovieEntityTvEpisodeEntity,以及通用字段和特定于类型的字段。

以下代码段展示了 publishContinuationCluster API 的请求正文载荷。

{
  "entities": [
    {
      "movieEntity": {
        "watch_next_type": "WATCH_NEXT_TYPE_CONTINUE",
        "name": "Movie1",
        "platform_specific_playback_uris": [
          "https://www.example.com/entity_uri_for_android",
          "https://www.example.com/entity_uri_for_iOS"
        ],
        "poster_images": [
          "http://www.example.com/movie1_img1.png",
          "http://www.example.com/movie1_imag2.png"
        ],
        "last_engagement_time_millis": 864600000,
        "duration_millis": 5400000,
        "last_play_back_position_time_millis": 3241111
      }
    },
    {
      "tvEpisodeEntity": {
        "watch_next_type": "WATCH_NEXT_TYPE_CONTINUE",
        "name": "TV SERIES EPISODE 1",
        "platform_specific_playback_uris": [
          "https://www.example.com/entity_uri_for_android",
          "https://www.example.com/entity_uri_for_iOS"
        ],
        "poster_images": [
          "http://www.example.com/episode1_img1.png",
          "http://www.example.com/episode1_imag2.png"
        ],
        "last_engagement_time_millis": 864600000,
        "duration_millis": 1800000,
        "last_play_back_position_time_millis": 2141231,
        "episode_display_number": "1",
        "season_number": "1",
        "show_title": "title"
      }
    }
  ]
}

删除视频发现数据

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

使用 POST 网址从视频发现数据中移除实体。 如需删除延续集群数据,请使用以下语法向 clearClusters API 发出 POST 请求。

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

其中:

  • package_name:媒体提供方软件包名称。
  • accountId:用户账号在您系统中的唯一 ID。它必须与设备端路径中使用的 accountId 相匹配。
  • profileId:用户在您系统中的账号内的个人资料的唯一 ID。必须与设备端路径中使用的 profileId 一致。

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

{
  "reason": "DELETE_REASON_LOSS_OF_CONSENT"
}

测试

成功发布数据后,请使用用户测试账号验证预期内容是否显示在目标 Google 平台(例如 Google TV 以及 Android 和 iOS 版 Google TV 移动应用)的“继续观看”行中。

在测试中,请允许合理的传播延迟(几分钟),并遵守观看要求,例如观看部分电影或看完一集电视剧。如需了解详情,请参阅面向应用开发者的“接下来观看”准则