開発者コンソール

ライブTVのリソース

ライブTVのリソース

以下のベストプラクティスやコードサンプルなどの参考資料は、実装フェーズにおけるライブTV統合の詳細を理解するうえで役立ちます。

許可リストへのパッケージの追加

許可リストによって、Fire TVの閲覧・検索時にチャンネルを表示できるアプリが決まります。

ベストプラクティス

以下は、Fire TVで快適なライブTVエクスペリエンスを提供するのに役立つ製品および実装のガイドラインです。

  • 簡単に登録できるようにして、適宜トライアルを促す。たとえば、アプリの登録フォームを簡素化したり、登録に電話番号を利用したりします。
  • チャンネルラインナップのTvContract.Channels.Logoには、透明なモノクロロゴを使用する。
  • ディープリンクフローを最適化して、全画面再生を2.5秒以内に開始する。
  • 複数のチャンネルを操作しているときは常に一括アクションを使用する。
  • 必要に応じてAmazonカタログを活用し、統合を簡素化する。
  • 完全なスケジュールを提供することや推奨される画像サイズを使用することよりも、メタデータ読み込みのパフォーマンスの最適化に重点を置く。
  • JobScheduler(英語のみ)またはWorkManager(英語のみ)を使用して、視聴権限が適切であることを定期的に確認する。これにより、アプリがフォアグラウンドで実行されていなくても、閲覧・検索されるチャンネルが視聴権限のあるチャンネルと常に同期されるようになります。
  • カスタムのチャンネル順序が使用されている場合を除き、視聴権限のあるチャンネルのリストが一部だけ変更された場合は、すべてのチャンネルを削除して再度追加するのではなく、必要な部分を更新するようにする。
  • COLUMN_DISPLAY_NAME(英語のみ)に、Fire TVのUIに表示されるチャンネル表示名を指定する。Fire TVでは最大25文字の英数字が表示されますが、文字数がこの制限を超えると、完全なチャンネル名は表示されません。この上限は、半角・全角のどちらの文字セットにも適用されます。表示されない例: The Walking Dead Universe(最大文字数内 - 表示される)/Short Name(表示される)/Extremely Long Station Name(表示されない)/韓流・華流韓流・華流韓(最大文字数 - 表示される)
  • 変更を行う前に、毎回、TV入力フレームワーク(TIF)データベースに照会して、このデータベースに既に存在するチャンネルを確認する。
  • チャンネルを挿入する前に、そのチャンネルがまだ存在していないことを確認する。チャンネルが既に存在する場合は、メタデータが最新であるかどうかを確認します。データベースの更新操作は、メタデータの更新が必要な場合にのみ行う必要があります。
  • データベースカーソルがnullであることを確認する。カーソルがnullである場合は、入力IDを持つすべてのチャンネルに対して削除リクエストを送信してから、チャンネルを再度挿入します。

コード例

このセクションには、ライブTVの統合に関連するサンプルコードが記載されています。

次のコードはTVContractUtilsから抜粋したもので、CDFステーションIDとディープリンクをTVデータベースに挿入する方法を示しています。

/**
 * 外部IDのタイプを格納する変数。サービスメタデータのマッチングに使用されます。有効なタイプは
 * 「EXTERNAL_ID_TYPE_」で始まる名前の定数として以下で定義されます。
 * Nullまたは無効なデータを使用すると、サービスメタデータのマッチングに失敗します。
 */
private final static String EXTERNAL_ID_TYPE = "externalIdType";
 
/**
 * 外部IDの値を格納する変数。サービスメタデータのマッチングに使用されます。
 * Nullまたは無効なデータを使用すると、サービスメタデータのマッチングに失敗します。
 */
private final static String EXTERNAL_ID_VALUE = "externalIdValue";

/**
 * 外部プレーヤーへの再生のディープリンクURI。
 * Nullまたは無効なデータを入力すると、デフォルトの動作(Fire TVのネイティブプレーヤーとの統合)が適用されます。
 */
private final static String PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri";

public void populateChannel(Context context) {
    /**
     * 再生ディープリンクURIのコントラクト。
     * Intent.URI_INTENT_SCHEMEを使用して、インテントからURIを作成したり、URIを元のインテントに戻したりします。
     */
    Intent playbackDeepLinkIntent = new Intent(); // アプリで作成したチャンネル用の実際のディープリンクインテント
    String playbackDeepLinkUri = playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME);
    
    /**
     * チャンネルのcontentValues BLOBを作成します。
     */
    ContentValues values = new ContentValues();  // すべてのチャンネルデータを格納します
    ContentResolver resolver = context.getContentResolver();
    values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, "<実際の表示名>");
    values.put(TvContract.Channels.COLUMN_INPUT_ID, "<実際の入力ID>");
    
    try {
        String jsonString = new JSONObject()
            .put(EXTERNAL_ID_TYPE, "<実際のIDタイプ>") // Amazonカタログの一意の名前空間に置き換えてください(例:"test_cdf2")
            .put(EXTERNAL_ID_VALUE, "<実際のID値>") // チャンネルに関連付けられた一意のCDFステーションIDに置き換えてください(例:"station-001"、"station-child-001"、"station-002"、"station-child-002"、"station-003"、"station-child-003")
            .put(PLAYBACK_DEEP_LINK_URI, playbackDeepLinkUri)
            .toString();
    
        // チャンネルのcontentValuesにJSON文字列を追加します。
        values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.getBytes());
    } catch (JSONException e) {
        Log.e(TAG, "BLOBにデータを追加するときにエラーが発生しました " + e);
    }
    
    Uri uri = resolver.insert(TvContract.Channels.CONTENT_URI, values);
}


fun populateChannel(context: Context) {
        /**
         * 再生ディープリンクURIのコントラクト。
         * Intent.URI_INTENT_SCHEMEを使用して、インテントからURIを作成したり、URIを元のインテントに戻したりします。
         */
        val playbackDeepLinkIntent =
            Intent() // アプリで作成したチャンネル用の実際のディープリンクインテント
        val playbackDeepLinkUri: String = playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME)

        /**
         * チャンネルのcontentValues BLOBを作成します。
         */
        val values = ContentValues() // すべてのチャンネルデータを格納します
        val resolver: ContentResolver = context.getContentResolver()
        values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, "<実際の表示名>")
        values.put(TvContract.Channels.COLUMN_INPUT_ID, "<実際の入力ID>")
        try {
            val jsonString: String = JSONObject()
                .put(
                    EXTERNAL_ID_TYPE,
                    "<実際のIDタイプ>"
                ) // Amazonカタログの一意の名前空間に置き換えてください(例:"test_cdf2")
                .put(
                    EXTERNAL_ID_VALUE,
                    "<実際のID値>"
                ) // チャンネルに関連付けられた一意のCDFステーションIDに置き換えてください(例:"station-001"、"station-child-001"、"station-002"、"station-child-002"、"station-003"、"station-child-003")
                .put(PLAYBACK_DEEP_LINK_URI, playbackDeepLinkUri)
                .toString()

            // チャンネルのcontentValuesにJSON文字列を追加します。
            values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.toByteArray())
        } catch (e: JSONException) {
            Log.e(TAG, "BLOBにデータを追加するときにエラーが発生しました $e")
        }
        val uri: Uri = resolver.insert(TvContract.Channels.CONTENT_URI, values)
    }


companion object {
    /**
     * 外部IDのタイプを格納する変数。サービスメタデータのマッチングに使用されます。有効なタイプは
     * 「EXTERNAL_ID_TYPE_」で始まる名前の定数として以下で定義されます。
     * Nullまたは無効なデータを使用すると、サービスメタデータのマッチングに失敗します。
     */
    private const val EXTERNAL_ID_TYPE = "externalIdType"

    /**
     * 外部IDの値を格納する変数。サービスメタデータのマッチングに使用されます。
     * Nullまたは無効なデータを使用すると、サービスメタデータのマッチングに失敗します。
     */
    private const val EXTERNAL_ID_VALUE = "externalIdValue"

    /**
     * 外部プレーヤーへの再生のディープリンクURI。
     * Nullまたは無効なデータを入力すると、デフォルトの動作(Fire TVのネイティブプレーヤーとの統合)が適用されます。
     */
    private const val PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri"
}

次のコードはTVContractUtilsから抜粋したもので、Gracenote IDとディープリンクをTVデータベースに挿入する方法を示しています。

/**
 * 外部IDのタイプを格納する変数。サービスメタデータのマッチングに使用されます。有効なタイプは
 * 「EXTERNAL_ID_TYPE_」で始まる名前の定数として以下で定義されます。
 * Nullまたは無効なデータを使用すると、サービスメタデータの
 * マッチングに失敗します
 */
private final static String EXTERNAL_ID_TYPE = "externalIdType";

/**
 * 外部IDの値を格納する変数。サービスメタデータのマッチングに使用されます。
 * Nullまたは無効なデータを使用すると、サービスメタデータのマッチングに失敗します。
 */
private final static String EXTERNAL_ID_VALUE = "externalIdValue";

/**
 * 外部プレーヤーへの再生のディープリンクURI。
 * Nullまたは無効なデータを入力すると、デフォルトの動作(Fire TVのネイティブプレーヤーとの統合)が適用されます。
 */
private final static String PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri";

// Gracenote入力タイプのID
private final static String GRACENOTE_ID = "gracenote_ontv"; // onTV用Gracenote ID
private final static String GRACENOTE_GVD = "gracenote_gvd"; // GVD用Gracenote ID

// 再生ディープリンクURIのコントラクト​
// Intent.URI_INTENT_SCHEMEを使用して、インテントからURIを作成したり、URIを元のインテントに戻したりします
Intent playbackDeepLinkIntent = new Intent(); // アプリで作成されます
String playbackDeepLinkUri = playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME);

// BLOBを作成します
ContentValues values = new ContentValues();  // すべてのチャンネルデータを格納します
ContentResolver resolver = context.getContentResolver();
values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, "<実際の表示名>");
values.put(TvContract.Channels.COLUMN_INPUT_ID, "<実際の入力ID>");
try {
    String jsonString = new JSONObject()
                  .put(EXTERNAL_ID_TYPE, "<実際のIDタイプ>") // GRACENOTE_XXXに置き換えます
                  .put(EXTERNAL_ID_VALUE, "<実際のID値>") // チャンネルに関連付けられたGracenote ID値に置き換えます
                  .put(PLAYBACK_DEEP_LINK_URI, playbackDeepLinkUri).toString();

    values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.getBytes());
} catch (JSONException e) {
    Log.e(TAG, "BLOBにデータを追加するときにエラーが発生しました " + e);
}

Uri uri = resolver.insert(TvContract.Channels.CONTENT_URI, values);
import android.app.Activity
import android.content.ContentValues
import android.content.Intent
import android.media.tv.TvContract
import android.util.Log
import org.json.JSONException
import org.json.JSONObject

/**
 * 外部IDのタイプを格納する変数。サービスメタデータのマッチングに使用されます。有効な
 * タイプは「EXTERNAL_ID_TYPE_」で始まる名前の定数として以下で定義されます。Nullまたは無効なデータを使用すると、
 * サービスメタデータのマッチングに失敗します
 */
private const val EXTERNAL_ID_TYPE = "externalIdType"
/**
 * 外部IDの値を格納する変数。サービスメタデータのマッチングに使用されます。Null
 * または無効なデータを使用すると、サービスメタデータのマッチングに失敗します
 */
private const val EXTERNAL_ID_VALUE = "externalIdValue"
/**
 * 外部プレーヤーへの再生のディープリンクURI。Nullまたは無効なデータを使用すると、
 * Fire TVのネイティブプレーヤーとの統合がデフォルトとなります
 */
private const val PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri"
// Gracenote入力タイプのID
private const val GRACENOTE_ID = "gracenote_ontv" // onTV用Gracenote ID
private const val GRACENOTE_GVD = "gracenote_gvd" // GVD用Gracenote ID



class SetupActivity : Activity() {
    private fun insertChannel(): Long? {
        // 再生ディープリンクURIのコントラクト​
        // Intent.URI_INTENT_SCHEMEを使用して、インテントからURIを作成したり、URIを元のインテントに戻したりします
        val playbackDeepLinkIntent = Intent() // アプリで作成されます
        val playbackDeepLinkUri = playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME)

        val jsonString: String? = try {
             JSONObject()
                .put(EXTERNAL_ID_TYPE, "<実際のIDタイプ>") // GRACENOTE_XXXに置き換えます
                .put(
                    EXTERNAL_ID_VALUE,
                    "<実際のID値>"
                ) // チャンネルに関連付けられたGracenote ID値に置き換えます
                .put(PLAYBACK_DEEP_LINK_URI, playbackDeepLinkUri)
                .toString()
        } catch (e: JSONException) {
            Log.e(TAG, "BLOBにデータを追加するときにエラーが発生しました", e)
            null
        }

        // BLOBを作成します
        val values = ContentValues().apply { // すべてのチャンネルデータを格納します
            put(TvContract.Channels.COLUMN_DISPLAY_NAME, "<実際の表示名>")
            put(TvContract.Channels.COLUMN_INPUT_ID, "<実際の入力ID>")
            if (jsonString != null) {
                put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.toByteArray())
            }
        }

        val uri = contentResolver.insert(TvContract.Channels.CONTENT_URI, values)

        Log.i("SetupActivity", "チャンネルを挿入しました。 URI:$uri")
        return uri?.lastPathSegment?.toLongOrNull()
    }
}

private val TAG = "MyTAG"

ペアレンタルコントロールへの対応

次のコードは、ペアレンタルコントロールをリッスンし、ライブプレビュー再生に反映させる方法を示しています。

private TvContentRating mBlockedRating = null;

    @Override
    public boolean onTune(final Uri channelUri) {
        ...
        if (mTvInputManager.isParentalControlsEnabled()) {
            // サーフェスで音声や画像が再生されないようにします
            mBlockedRating = <content_rating>;
            //1.全画面再生のときに、ユーザーにPINの入力を求めるプロンプトを表示します
            //2.[放映中のチャンネル] 行を閲覧しているときに
            //    番組画像が再生サーフェスで表示されないようにします
            notifyContentBlocked(mBlockedRating);
        } else {
            // 再生が開始されます
            notifyContentAllowed();
        }
        ...
    }

    @Override
    public void onUnblockContent(final TvContentRating unblockedRating) {
        // ユーザーのPIN入力により、指定のレーティングに該当するコンテンツのブロックが
        // 正常に解除されました
        if (unblockedRating.unblockContent(mBlockedRating)) {
            // 再生が開始されます
            notifyContentAllowed();
        }
    }
import android.content.Context
import android.media.tv.TvContentRating
import android.media.tv.TvInputManager
import android.media.tv.TvInputService
import android.net.Uri
import android.view.Surface

private const val TAG = "MyTag"

private class PreviewSession(context: Context) :
    TvInputService.Session(context) {

    private val tvInputManager: TvInputManager = TODO()



    override fun onTune(channelUri: Uri): Boolean {
        if (tvInputManager.isParentalControlsEnabled) {
            // サーフェスで音声や画像が再生されないようにします
            val blockedRating = getContentRating(channelUri)
            //1.全画面再生のときに、ユーザーにPINの入力を求めるプロンプトを表示します
            //2.[放映中のチャンネル] 行を閲覧しているときに
            //    番組画像が再生サーフェスで表示されないようにします
            notifyContentBlocked(blockedRating)
        } else {
            // 再生が開始されます
            notifyContentAllowed()
        }
        return true
    }

    override fun onUnblockContent(unblockedRating: TvContentRating) {
        // ユーザーのPIN入力により、指定のレーティングに該当するコンテンツのブロックが
        // 正常に解除されました
        if (unblockedRating.unblockContent(mBlockedRating)) { // <-- これは何か?
            // 再生が開始されます
            notifyContentAllowed();
        }
    }
}

private fun getContentRating(channelUri: Uri): TvContentRating = TODO()

アプリバナーの提供

ライブTVの設定にアプリバナーを表示するには、パッケージマネージャーを通じてアプリバナーを提供する必要があります。

// AndroidManifest.xml内
<application
    android:allowBackup="false"
    android:label="@string/app_name"
    android:banner="@drawable/app_icon_banner"
    tools:replace="android:allowBackup, allow:label, android:theme" >

    <meta-data
        android:name="****"
        android:value="true"
    />
</application>

バナーをテストするには、次のサンプルコードを参照してください。

Drawable appDrawable = null;
try {
    String packageName = "****"; // ****を実際のパッケージ名に置き換えます
    PackageManager packageManager = getContext().getPackageManager();
    appDrawable = packageManager.getApplicationBanner(packageName);
} catch (PackageManager.NameNotFoundException e) {
    Log.i(TAG, "パッケージのアプリバナーが見つかりません:" + packageName);
}
val packageName = "****" // ****を実際のパッケージ名に置き換えます
val appDrawable: Drawable? = try {
    packageManager.getApplicationBanner(packageName)
} catch (e: PackageManager.NameNotFoundException) {
    Log.i("SetupActivity", "パッケージのアプリバナーが見つかりません:$packageName")
    null
}

BroadcastReceiverを登録してINITIALIZE_PROGRAMSアクションをリッスンする方法

次のコードでは、AndroidManifest.xml<receiver>要素を追加し、インテントフィルターを使用してINITIALIZE_PROGRAMSアクションをリッスンする方法を示します。

<receiver android:name=".InitializationReceiver"
    android:exported="true"
    >
    <intent-filter>
        <action android:name="android.media.tv.action.INITIALIZE_PROGRAMS" />
    </intent-filter>
</receiver>

BroadcastReceiverを拡張するreceiverクラスを作成し、onReceiveメソッドを実装します。ここで、視聴権限があるチャンネルのみをTIFのローカルチャンネルデータベースにプッシュする初期化ロジックを含めることができます。

public class InitializationReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // チャンネルをデバイスにプッシュするロジックをここに挿入します
        // 注: インストール後、視聴権限があるチャンネルをデバイスにプッシュする場合のみ使用します
    }
}

詳細な実装例は、ライブTV対応サンプルアプリを参照してください。

実装をテストするには、以下のいずれかの前提条件を選択します。

  • デバイスが既に接続されており、APKがまだサイドローディングされていないことを前提とする場合は、新しいターミナルを開き、適切なフィルターを指定してadb logcatを実行します。onReceiveロジックに関連するログが表示されます。別のターミナルで、adb install <APKのパス>を実行します。インストールが成功すると、新しいTV入力が追加された時点でINITIALIZE_PROGRAMSがブロードキャストされるため、関連するログが表示され始めます。
  • APKが置き換えられることを前提とする場合(apk install -r <APKのパス>)、INITIALIZE_PROGRAMSアクションは自動的にブロードキャストされません。onReceiveロジックをテストするには、次のコードスニペットを参照してください。
adb shell am broadcast -a android.media.tv.action.INITIALIZE_PROGRAMS -n com.example.android.sampletvinput/.InitializationReceiver

ライブTV対応サンプルアプリ

ライブTVが統合されたサンプルアプリは、GitHub(github.com/amzn/ftv-livetv-sample-tv-app)で入手できます。このライブTV対応サンプルアプリは、GoogleのサンプルTVアプリをベースとしています。Fire TVへのライブTV統合のリファレンスとして、このサンプルアプリを使用できます。

ライブアプリのロケールサポート

サンプルアプリがサポートされるロケールは、 US、CA、UK、DE、JP、ES、INのみです。その他マーケットプレイスでのサポートは間もなく開始されます。

サンプルアプリを読み込むには

  1. https://github.com/amzn/ftv-livetv-sample-tv-appにアクセスして、[Clone or download] をクリックし、[Download ZIP] をクリックします。ダウンロードファイルを解凍します。

    ライブTVを統合するためのサンプルコードがアプリに表示されます。結果を確認するには、次の手順で説明するように、ADBを使用してapp-debug.apkファイルをFire TVにサイドロードします。

  2. ADBを使用してFire TVに接続します。

    既にデバッグが有効でADBがインストールされている場合は、[設定] > [デバイスとソフトウェア](または [My Fire TV])> [バージョン情報] > [ネットワーク] からFire TVのIPアドレスを取得し、以下を実行してFire TVのIPアドレスをカスタマイズします。

    adb connect 123.456.7.89:5555
    

    123.456.7.89をFire TVのIPアドレスに置き換えてください(コンピューターはFire TVと同じWi-Fiネットワーク上にある必要があるため、企業のVPNを利用していて接続に問題がある場合は、VPNから切断してみてください)。

  3. ビルドされたサンプルアプリのAPKをインストールします。

    adb install -r AndroidTvSampleInput/app/build/outputs/apk/app-debug.apk
    

    レスポンスは次のとおりです。

    Performing Streamed Install
    Success
    

    なお、このサンプルアプリは、本来、スタンドアロンのアプリとして起動するものではありません。その代わり、Fire TVデバイスで利用できるライブTVチャンネルのコードが組み込まれています。

  4. Fire TVデバイスで、[設定] > [アプリケーション] > [インストール済みアプリケーションを管理] の順に移動します。[Sample TV Inputs] を選択します。[アプリを起動] をクリックします。

    Sample TV Inputs
    Sample TV Inputs

    Amazon開発者ポータルが表示されます。

    Amazon Fire TVサイト
    Amazon Fire TVサイト
  5. Fire TVリモコンのホームボタンを押して、この画面から戻ります。次に、[設定] > [ライブTV] > [チャンネル提供元を同期] > [Amazon Sample TV Input] の順に選択します。

    サンプルチャンネルが読み込まれます。

    チャンネル提供元の同期
    チャンネル提供元の同期
  6. 同期が完了したら、ホームボタンを押します。これで、チャンネルが [放映中のチャンネル] 行と番組表に表示されます。

    [放映中のチャンネル] 行は次のようになります。

    Fire TVの [放映中のチャンネル] 行
    Fire TVの [放映中のチャンネル]

    番組表は次のようになります。

    Fire TV番組表
    Fire TV番組表

    Fire TVの番組表に移動するには、ホーム画面に移動し、[放映中のチャンネル] 行まで下にスクロールします。次に、リモコンのメニューボタンを押し、[番組表] をクリックします。リモコンのマイクボタンを押して、「番組表」と言う方法もあります。


Last updated: 2025年5月5日