プレビュー動画は、テレビアプリに遷移するディープリンクをユーザーに促す優れた方法です。短いクリップから完全な映画の予告編まで、プレビューにはさまざまな種類があります。
プレビューを作成する際は、次のガイドラインに沿って進めてください。
- プレビュー内に広告を表示しないでください。クライアント サイドで広告を合成する場合は、プレビュー動画には広告を合成しないようにしてください。サーバーサイドで広告を合成する場合は、プレビュー用に広告のない動画を提供してください。
- 最高の品質を実現するため、プレビュー動画は 16:9 または 4:3 にします。プレビュー動画の推奨サイズについては、動画プログラムの属性をご覧ください。
- プレビュー動画とポスターアートのアスペクト比が異なる場合、ホーム画面の機能により、プレビューを再生する前に、ポスタービューのサイズが動画のアスペクト比に変更されます。
動画はレターボックス化されません。たとえば、ポスターアートのアスペクト比が
ASPECT_RATIO_MOVIE_POSTER
(1:1.441)で、動画のアスペクト比が 16:9 の場合、ポスタービューが 16:9 の領域に変換されます。 - プレビューを作成すると、そのコンテンツは、一般公開されるか、DRM で保護されます。それぞれの場合で異なる手順が適用されます。このページでは、両方の場合について説明します。
ホーム画面でプレビューを再生する
ExoPlayer がサポートしている動画タイプを使用してプレビューを作成し、一般公開した場合、そのプレビューはホーム画面上で直接再生できます。
PreviewProgram を作成する場合、以下の例に示すように、setPreviewVideoUri()
を使用して一般公開用の HTTPS URL を指定します。プレビューは動画でも音声でも構いません。
Kotlin
val previewVideoUrl = Uri.parse("https://www.example.com/preview.mp4") val builder = PreviewProgram.Builder() builder.setChannelId(channelId) // ... .setPreviewVideoUri(previewVideoUrl)
Java
Uri previewVideoUrl = Uri.parse("https://www.example.com/preview.mp4"); PreviewProgram.Builder builder = new PreviewProgram.Builder(); builder.setChannelId(channelId) // ... .setPreviewVideoUri(Uri.parse(previewVideoUrl));
プレビューをサーフェス上にレンダリングする
動画が DRM で保護されている場合や、ExoPlayer がサポートしていないメディアタイプの場合、TvInputService
を使用します。Android TV のホーム画面は、onSetSurface()
を呼び出すことで、サービスに Surface
を渡します。アプリは、onTune()
から直接このサーフェスに動画を描画します。
ダイレクト サーフェス レンダリングを使用すると、アプリがレンダリング対象とレンダリング方法を制御できるようになります。チャンネル属性などのメタデータをオーバーレイできます。
マニフェスト内で TvInputService を宣言する
アプリが TvInputService
を実装すると、ホーム画面がプレビューをレンダリングできるようになります。
サービス宣言内に、インテントを使用して実行するアクションとして TvInputService
を指定するインテント フィルタを追加します。また、サービス メタデータを個別の XML リソースとして宣言します。サービス宣言、インテント フィルタ、サービス メタデータ宣言の例を以下に示します。
<service android:name=".rich.PreviewInputService" android:permission="android.permission.BIND_TV_INPUT"> <!-- Required filter used by the system to launch our account service. --> <intent-filter> <action android:name="android.media.tv.TvInputService" /> </intent-filter> <!-- An XML file which describes this input. --> <meta-data android:name="android.media.tv.input" android:resource="@xml/previewinputservice" /> </service>
サービス メタデータを個別の XML ファイルで定義します。
サービス メタデータ ファイルはアプリの XML リソース ディレクトリに存在しますが、マニフェストで宣言したリソースの名前と一致する必要があります。上記の例のマニフェスト エントリを使用して、次のように空の tv-input
タグが含まれる XML ファイルを res/xml/previewinputservice.xml
として作成できます。
<?xml version="1.0" encoding="utf-8"?>
<tv-input/>
テレビ入力フレームワークには、このタグが必要です。ただし、このタグを使用するのは、ライブ チャンネルを設定する場合に限られます。今回は動画をレンダリングするため、このタグは空にする必要があります。
動画 URI を作成する
Android TV のホーム画面ではなくアプリでプレビュー動画をレンダリングするように指定するには、PreviewProgram
用の動画 URI を作成する必要があります。URI は、アプリが対象コンテンツに対して使用する識別子で終わる必要があります。これにより、後で TvInputService
内でコンテンツを取得できるようになります。
識別子が Long
型の場合は、TvContractCompat.buildPreviewProgramUri() を使用します。
Kotlin
val id: Long = 1L // content identifier val componentName = new ComponentName(context, PreviewVideoInputService.class) val previewProgramVideoUri = TvContractCompat.buildPreviewProgramUri(id) .buildUpon() .appendQueryParameter("input", TvContractCompat.buildInputId(componentName)) .build()
Java
Long id = 1L; // content identifier ComponentName componentName = new ComponentName(context, PreviewVideoInputService.class); previewProgramVideoUri = TvContractCompat.buildPreviewProgramUri(id) .buildUpon() .appendQueryParameter("input", TvContractCompat.buildInputId(componentName)) .build();
識別子が Long
型でない場合は、Uri.withAppendedPath()
を使用して URI を作成します。
Kotlin
val previewProgramVideoUri = Uri.withAppendedPath(PreviewPrograms.CONTENT_URI, "content-identifier") .buildUpon() .appendQueryParameter("input", TvContractCompat.buildInputId(componentName)) .build()
Java
previewProgramVideoUri = Uri.withAppendedPath(PreviewPrograms.CONTENT_URI, "content-identifier") .buildUpon() .appendQueryParameter("input", TvContractCompat.buildInputId(componentName)) .build();
アプリが onTune(Uri videoUri)
を呼び出すと、Android TV がプレビュー動画を開始します。
サービスを作成する
次の例は、TvInputService
を拡張して独自の PreviewInputService
を作成する方法を示しています。サービスは再生に MediaPlayer
を使用しますが、コード内では、利用可能な動画プレーヤーであればいずれでも使用できます。
Kotlin
import android.content.Context import android.media.MediaPlayer import android.media.tv.TvInputService import android.net.Uri import android.util.Log import android.view.Surface import java.io.IOException class PreviewVideoInputService : TvInputService() { override fun onCreateSession(inputId: String): TvInputService.Session? { return PreviewSession(this) } private inner class PreviewSession( internal var context: Context ) : TvInputService.Session(context) { internal var mediaPlayer: MediaPlayer? = MediaPlayer() override fun onRelease() { mediaPlayer?.release() mediaPlayer = null } override fun onTune(uri: Uri): Boolean { // Let the TvInputService know that the video is being loaded. notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_TUNING) // Fetch the stream url from the TV Provider database // for content://android.media.tv/preview_program/val id = uri.lastPathSegment // Load your video in the background. retrieveYourVideoPreviewUrl(id) { videoUri -> if (videoUri == null) { Log.d(TAG, "Could not find video $id") notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) } try { mPlayer.setDataSource(getApplicationContext(), videoUri) mPlayer.prepare() mPlayer.start() notifyVideoAvailable() } catch (IOException e) { Log.e(TAG, "Could not prepare media player", e) notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) } } return true } override fun onSetSurface(surface: Surface?): Boolean { mediaPlayer?.setSurface(surface) return true } override fun onSetStreamVolume(volume: Float) { // The home screen may fade in and out the video's volume. // Your player should be updated accordingly. mediaPlayer?.setVolume(volume, volume) } override fun onSetCaptionEnabled(b: Boolean) { // enable/disable captions here } } companion object { private const val TAG = "PreviewInputService" } }
Java
import android.content.Context; import android.media.MediaPlayer; import android.media.tv.TvInputService; import android.net.Uri; import android.support.annotation.Nullable; import android.util.Log; import android.view.Surface; import java.io.IOException; public class PreviewVideoInputService extends TvInputService { private static final String TAG = "PreviewVideoInputService"; @Nullable @Override public Session onCreateSession(String inputId) { return new PreviewSession(this); } private class PreviewSession extends TvInputService.Session { private MediaPlayer mPlayer; PreviewSession(Context context) { super(context); mPlayer = new MediaPlayer(); } @Override public boolean onTune(Uri channelUri) { // Let the TvInputService know that the video is being loaded. notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_TUNING); // Fetch the stream url from the TV Provider database // for content://android.media.tv/preview_program/String id = uri.getLastPathSegment(); // Load your video in the background. retrieveYourVideoPreviewUrl(id, new MyCallback() { public void callback(Uri videoUri) { if (videoUri == null) { Log.d(TAG, "Could not find video" + id); notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); } try { mPlayer.setDataSource(getApplicationContext(), videoUri); mPlayer.prepare(); mPlayer.start(); notifyVideoAvailable(); } catch (IOException e) { Log.e(TAG, "Could not prepare media player", e); notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); } } }); return true; } @Override public boolean onSetSurface(@Nullable Surface surface) { if (mPlayer != null) { mPlayer.setSurface(surface); } return true; } @Override public void onRelease() { if (mPlayer != null) { mPlayer.release(); } mPlayer = null; } @Override public void onSetStreamVolume(float volume) { if (mPlayer != null) { // The home screen may fade in and out the video's volume. // Your player should be updated accordingly. mPlayer.setVolume(volume, volume); } } @Override public void onSetCaptionEnabled(boolean enabled) { // enable/disable captions here } } }