开发者控制台

开始使用Fire TV集成SDK

开始使用Fire TV集成SDK

下文说明了将Fire TV集成SDK集成到您的Fire TV应用中时的须知事项。

先决条件

  • 访问您的Fire TV应用的源代码。
  • 支持此集成的Fire TV设备。请咨询您的亚马逊联系人,获取当前支持的设备类型列表。
  • 必须在目录引入流程中加入您的应用,以便Fire TV通过内容个性化数据识别您的应用传递的内容ID。
  • 您的应用必须为每位客户共享权利,以便Fire TV在内容发现体验中显示授权提供方。如需更多文档,请联系您的亚马逊联系人。

资源

集成步骤

步骤1:在您的应用中添加SDK

首先下载Fire TV集成SDK。这是一个存档,其中包含了Javadocs以及名为com.amazon.tv.developer.content.sdk.jar的jar文件。将此jar文件复制到应用中的libs目录。此目录必须与您的build.gradle文件处于相同的程序包级别。复制或添加文件后,将以下行添加到build.gradle文件的dependencies部分。

dependencies {
    ...
    compileOnly files('libs/com.amazon.tv.developer.content.sdk.jar')
}

将您的IDE与Gradle更改同步,以便您可以使用SDK函数。

接下来,将以下uses-library行添加到应用标签内的AndroidManifest.xml文件中。

<application
    android:label="您的应用"
   ...>
    <uses-library    
    android:name="com.amazon.tv.developer.sdk.content"
    android:required="false" />
</application>

现在将以下权限添加到您的AndroidManifest.xml文件。

<uses-permission android:name="com.amazon.tv.developer.sdk.content.USE_SDK" />

步骤2:实现库检查函数

为确保设备具有正确的库,请在使用SDK代码之前实现以下函数以进行检查。为此,在Android的程序包管理器上调用hasSystemFeature函数,它是标准Android API的一部分,您可以在任何Android电视平台上在尝试调用SDK之前使用它。

按如下方式实现库检查函数:

public boolean isFTVIntegrationSDKSupportedOnDevice(Context context) {
   PackageManager pm = context.getPackageManager();
   return pm.hasSystemFeature("com.amazon.tv.developer.sdk.content");
}
fun isFTVIntegrationSDKSupportedOnDevice(context: Context): Boolean {
    val pm = context.packageManager
    return pm.hasSystemFeature("com.amazon.tv.developer.sdk.content")
}

在任何功能中使用此SDK,此方法都必须返回true。否则,当您尝试访问任何SDK类时,您将收到ClassNotFound异常。

步骤3:调用SDK

从示例事件开始,该事件表示客户已开始观看某段内容。要构建并发送事件,请使用以下代码:

    if (isFTVIntegrationSDKSupportedOnDevice(getContext())) {
        AmazonPlaybackEvent playbackEvent = AmazonPlaybackEvent
            .builder()
            .playbackPositionMs(0)
            .creditsPositionMs(1000)
            .state(AmazonPlaybackState.PLAYING)
            .durationMs(2000)
            .contentId(AmazonContentId.builder()
                .id("string")
                .namespace(AmazonContentId.NAMESPACE_CDF_ID)
                .build())
            .profileId(AmazonProfileId.builder()
                .id("myProfileId1")
                .namespace(AmazonProfileId.NAMESPACE_APP_INTERNAL)
                .build())
            .buildActiveEvent();

        AmazonPlaybackReceiver.getInstance(getContext()).addPlaybackEvent(playbackEvent);
    }
if (isFTVIntegrationSDKSupportedOnDevice(context)) {
    val playbackEvent = AmazonPlaybackEvent.Builder()
            .setPlaybackPositionMs(0)
            .setCreditsPositionMs(1000)
            .setState(AmazonPlaybackState.PLAYING)
            .setDurationMs(2000)
            .setContentId(AmazonContentId.Builder("string").setNamespace(AmazonContentId.NAMESPACE_CDF_ID).build())
            .setProfileId(AmazonProfileId.Builder("myProfileId1").setNamespace(AmazonProfileId.NAMESPACE_APP_INTERNAL).build())
            .buildActiveEvent()
    AmazonPlaybackReceiver.getInstance(context).addPlaybackEvent(playbackEvent)
}

步骤4:验证集成

触发您构建的示例事件代码以在应用中运行。成功运行代码后,查看日志以验证SDK已链接到您的应用并且正在处理消息。

您可以通过运行以下命令,在命令行中监视这些日志:

adb logcat | grep AmazonPlaybackReceiver
adb logcat | findstr AmazonPlaybackReceiver

您收到的有关步骤3中显示的事件的日志消息应与以下内容类似:

AmazonPlaybackReceiver: Fire TV Integration SDK function: addPlaybackEvent invoked by package: com.example.app with data: AmazonPlaybackEvent(durationMs=2000, playbackPositionMs=0, creditsPositionMs=1000, state=0, eventTimestampMs=0, profileId=AmazonProfileId(id=myProfileId1, namespace=app_internal), isOffDevice=false)

步骤5:为后台或设备外数据实现数据拉取服务

要允许亚马逊从您的应用提取数据,请从SDK实现抽象服务。该服务定义了服务设置和连接所需的一切,您只需要实现向接收器对象发送数据的函数。该对象允许您根据需要分块共享数据,以防止将大型列表加载到内存中。

您可以实现提取服务,以从应用中拉取数据,如下所示:

public class MyAmznDataIntegrationService extends AmazonDataIntegrationService {
    private List <CustomerSubscription> getCustomerSubscriptions() {
        //用于检索客户活动的内部逻辑
    }

    private List < AmazonPlaybackEvent > convertToAmazonPlaybackEvents(List <CustomerTitleWatched> titlesWatched) {
        List <AmazonPlaybackEvent> amznPlaybackEvents = new ArrayList<>();
        for (CustomerTitleWatched titleWatched: titlesWatched) {
            amznPlaybackEvents.add(AmazonPlaybackEvent.builder()
                .contentId(AmazonContentId.builder()
                    .id(titleWatched.getAmznCatalogId())
                    .namespace(AmazonContentId.NAMESPACE_CDF_ID)
                    .build())
                .playbackPositionMs(titleWatched.getCurrentPlaybackPosition())
                .durationMs(titleWatched.getDuration())
                .creditsPositionMs(titlewatched.getCreditPosition())
                .state(AmazonPlaybackState.EXIT)
                .profileId(AmazonProfileId.builder()
                    .id("myProfileId1")
                    .namespace(AmazonProfileId.NAMESPACE_APP_INTERNAL)
                    .build())
                .eventTimestampMs(titleWatched.getLastWatchTime())
                .buildOffDeviceEvent());
        }
        return amznPlaybackEvents;
    }
    @Override
    public void getRecentPlaybackEventsSince(AmazonPlaybackReceiver playbackReceiver, long startingTimestampMs) {
        List <CustomerTitleWatched> customerTitlesWatched = retrieveCustomerWatchActivity();
        List <AmazonPlaybackEvent> amznPlaybackEvents = convertToAmazonPlaybackEvents(customerTitlesWatched);
        playbackReceiver.addPlaybackEvents(amznPlaybackEvents);
    }
    @Override
    public void getAllContentEntitlements(AmazonEntitlementReceiver entitlementReceiver) {
        ...
    }
    @Override
    public void getAllSubscriptionEntitlements(AmazonEntitlementReceiver receiver) {
        ...
    }
    @Override
    public void getAllCustomerListEntries(AmazonCustomerListReceiver customerListReceiver, int type) {
        ...
    }
}
class MyAmznDataIntegrationService : AmazonDataIntegrationService {
    private fun getCustomerSubscriptions(): List<CustomerSubscription> {
        //用于检索客户活动的内部逻辑
    }

    private fun convertToAmazonPlaybackEvents(titlesWatched: List<CustomerTitleWatched>): List<AmazonPlaybackEvent> {
        val amznPlaybackEvents = arrayListOf<AmazonPlaybackEvent>()
        for (titleWatched in titlesWatched) {
            amznPlaybackEvents.add(AmazonPlaybackEvent.Builder()
                .contentId(AmazonContentId.Builder()
                    .id(titleWatched.amznCatalogId)
                    .namespace(AmazonContentId.NAMESPACE_CDF_ID)
                    .build())
                .playbackPositionMs(titleWatched.currentPlaybackPosition)
                .durationMs(titleWatched.duration)
                .creditsPositionMs(titleWatched.creditPosition)
                .state(AmazonPlaybackState.EXIT)
                .profileId(AmazonProfileId.Builder()
                    .id("myProfileId1")
                    .namespace(AmazonProfileId.NAMESPACE_APP_INTERNAL)
                    .build())
                .eventTimestampMs(titleWatched.lastWatchTime)
                .buildOffDeviceEvent())
        }
        return amznPlaybackEvents
    }

    override fun getRecentPlaybackEventsSince(playbackReceiver: AmazonPlaybackReceiver, startingTimestampMs: Long) {
        val customerTitlesWatched = retrieveCustomerWatchActivity()
        val amznPlaybackEvents = convertToAmazonPlaybackEvents(customerTitlesWatched)
        playbackReceiver.addPlaybackEvents(amznPlaybackEvents)
    }

    override fun getAllContentEntitlements(entitlementReceiver: AmazonEntitlementReceiver) {
        // 用于检索所有内容权利的实现
    }

    override fun getAllSubscriptionEntitlements(receiver: AmazonEntitlementReceiver) {
        // 用于检索所有订阅权利的实现
    }

    override fun getAllCustomerListEntries(customerListReceiver: AmazonCustomerListReceiver, type: Int) {
        // 用于检索所有客户列表条目的实现
    }
}

为确保您的服务只能由Fire TV访问,请使用com.amazon.tv.developer.sdk.content.READ_DATA_SERVICE实施保护。这会仅允许Fire TV获得权限,并将防止其他组件调用您的服务。

为此,请将服务定义添加到AndroidManifest.xml中:

  <service
android:name="com.amazon.tv.developer.sdk.content.sample.SampleIntegrationService"
 android:exported="true"
 android:permission="com.amazon.tv.developer.sdk.content.READ_DATA_SERVICE"/> 

添加元数据块,以便将服务用于数据集成,从而让Fire TV系统能够找到服务并根据需要连接该服务。此外,请确保该值是完全限定的服务名称。

<application
    <meta-data android:name="com.amazon.tv.developer.sdk.content.data_integration_service_class"
    android:value="com.amazon.tv.developer.sdk.content.sample.SampleIntegrationService"
    />
</application>

步骤6:将SDK调用作为应用内功能的一部分实现

对于每种数据类型,请查看数据类型的何时发送部分,以了解您需要在代码中的哪个位置调用该SDK。

例如,当客户将项目添加到他们的观看列表时,我们希望您通过相关信息调用addCustomerListEntry API。您需要先在应用中找到用于将项目添加到观看列表的代码,然后将对Fire TV集成SDK的API调用作为此逻辑的一部分加入。

实现详情

访问实例

SDK由五个接收器类组成,用于接收不同类型的数据:

  • AmazonPlaybackReceiver
  • AmazonContentEntitlementReceiver
  • AmazonCustomerListReceiver
  • AmazonContentInteractionReceiver
  • AmazonSubscriptionEntitlementReceiver

要访问其中一个接收器的实例,请调用相应的静态函数getInstance。此实例可用于内联调用函数(如步骤3中的示例所示),也可以存储该实例并根据需要重新使用。

亚马逊内容ID

该SDK支持跨不同命名空间进行内容识别,因此必须通过关联的命名空间传递所有内容ID,以便亚马逊正确识别内容项目。亚马逊支持以下命名空间:

命名空间 描述
cdf_id 这是ID空间,因为数据是在您的亚马逊目录集成中共享的。这些是您在每段内容上指定的ID。该命名空间特别对应于目录数据格式 (CDF) 的CommonWorkType/ID字段。

亚马逊个人资料ID

所有数据类型都允许您共享关联的个人资料ID。由于存在不同的个人资料类型,请指定命名空间,亚马逊目前支持以下个人资料ID命名空间:

命名空间 描述
app_internal 此命名空间为字符串形式,表示您正在共享的有效个人资料的内部个人资料ID。此ID用于识别您应用中的特定个人资料,因此我们可以将活动归因于正确的Fire TV个人资料。请勿提供您在内部使用的实际ID,并且提供的值不应能够识别客户。我们建议使用您的内部个人资料ID的哈希值,该值在所有设备上都应相同。另外,请勿向我们发送客户提供的个人资料名称。即使您的应用不使用个人资料,也要提供一致的值。

列表共享

SDK具备共享功能,适用于客户列表和内容权利列表。列表有三种操作类型:

  • Add (添加) - 这表示向列表中新增条目的递增更新。
  • Remove (删除) - 这表示从列表中删除条目的递减更新。
  • Set (设置) - 这表示用您提供的条目列表替换现有状态。

当客户与应用中的内容进行交互并执行添加或删除操作时,可以使用AddRemove。当客户首次登录电视(前提是您怀疑列表未同步)以及发送设备外活动时,可以使用Set更新。

传递列表时的限制

如果列表足够小,可以安全地存入内存(少于10,000个项目),则可以毫无顾虑地将整个列表传递给Fire TV数据服务。SDK会处理向数据服务传送数据所需的任何批处理。如果数据列表太大而无法放入内存,或者您想限制内存占用,则可按照递增的形式发送数据。这对您的应用和提供数据的后端服务之间的分页协议非常有效。参见分块共享列表部分中的代码示例。

共享大型数据集

Set列表操作可能需要传递大量数据。如果您的数据过多,无法合理地放入应用内的内存个人资料中,请使用AmazonSetListStatus参数来处理大型数据集。

AmazonSetListStatus参数可用于对数据进行基本分页,其中包含两个静态值:

  • ITEMS_PENDING表示预计该列表会有更多数据。
  • COMPLETE表示该列表已完成,并且最近通过ITEMS_PENDING收到的所有消息都将用于构建客户的完整列表。

分块共享列表

此示例假设您从后端检索页面中的数据。系统会与ITEMS_PENDING标记共享数据,直到我们到达数据的最后一页并发送COMPLETE标记。

  private int getIndividualEntitlementsFromServiceWithPages() {
     // 用于检索相当于个人权利的数据页数的内部逻辑。
 }

 private void getIndividualEntitlementsFromServiceByPage(int i, ServiceCallback callback) {
     //用于调用服务并接收单页数据的内部逻辑

     callback.onReceiveData(data);
 }

 private void List <AmazonContentEntitlement> convertToAmznContentEntitlements(Data data) {
     //在后端将数据转换为亚马逊形式
 }

 public void shareLargeList() {
     AmazonContentEntitlementReceiver receiver = AmazonContentEntitlementReceiver.getInstance(getContext());
     int pages = getIndividualEntitlementsFromServiceWithPages();
     for (int i = 0; i < pages; i++) {
         ServiceCallback callback = new ServiceCallback() {
             onReceiveData(data) {
                 List < AmazonContentEntitlement > entitlements = convertToAmznContentEntitlements(data);
                 if (i == pages - 1) {
                     receiver.setContentEntitlements(AmazonSetListStatus.COMPLETE, entitlements);
                 } else {
                     receiver.setContentEntitlements(AmazonSetListStatus.ITEMS_PENDING, entitlements);
                 }
             }
         }

         getIndividualEntitlementsFromServiceByPage(i, callback);
     }
 }
private fun getIndividualEntitlementsFromServiceWithPages(): Int {
    // 用于检索相当于个人权利的数据页数的逻辑
}

private fun getIndividualEntitlementsFromServiceByPage(i: Int, callback: ServiceCallback) {
    // 用于调用服务并接收单页数据的内部逻辑
    callback.onReceiveData(data)
}

private fun convertToAmznContentEntitlements(data: Data): List<AmazonContentEntitlement> {
    // 在后端将数据转换为亚马逊形式
}

public fun shareLargeList() {
    val receiver = AmazonContentEntitlementReceiver.getInstance(context)
    val pages = getIndividualEntitlementsFromServiceWithPages()
    for (i in 0 until pages) {
        val callback = ServiceCallback { data ->
            val entitlements = convertToAmznContentEntitlements(data)
            if (i == pages - 1) {
                receiver.setContentEntitlements(AmazonSetListStatus.COMPLETE, entitlements)
            } else {
                receiver.setContentEntitlements(AmazonSetListStatus.ITEMS_PENDING, entitlements)
            }
        }
        getIndividualEntitlementsFromServiceByPage(index, callback)
    }
}

异常处理

对SDK的调用为“一劳永逸”,这意味着您在发送数据时不会收到任何错误。但是,如果您缺少所需数据或数据无效,则在创建模型对象时可能会收到RuntimeException

后续步骤

接下来,让我们从观看活动开始,来看看各种数据类型。


Last updated: 2024年2月27日