미리보기 동영상은 사용자가 TV 앱에 딥 링크로 연결하도록 유도하는 데 효과적인 방법입니다. 짧은 클립부터 전체 영화 트레일러까지 다양한 미리보기가 제공됩니다.
미리보기를 만들 때 다음 가이드라인을 고려하세요.
- 미리보기에 광고를 표시하지 않습니다. 클라이언트 측에서 광고를 연결하는 경우 미리보기 동영상에 광고를 결합하지 마세요. 서버 측에서 광고를 연결하는 경우 광고 없는 동영상을 미리보기용으로 제공하세요.
- 최적의 품질을 위해 미리보기 동영상은 16:9 또는 4:3이어야 합니다. 미리보기 동영상의 권장 크기는 동영상 프로그램 속성을 참고하세요.
- 미리보기 동영상과 포스터 아트의 가로세로 비율이 서로 다른 경우 미리보기를 재생하기 전에 홈 화면에서 동영상의 가로세로 비율에 맞게 포스터 뷰의 크기가 조절됩니다.
동영상이 레터박스 처리되지 않습니다. 예를 들어 포스터 아트 비율이
ASPECT_RATIO_MOVIE_POSTER
(1:1.441)이지만 동영상 비율은 16:9이면 포스터 뷰가 16:9 영역으로 변환됩니다. - 미리보기를 만들면 DRM에 따라 콘텐츠를 공개적으로 액세스하거나 보호할 수 있습니다. 각 사례에 다른 절차가 적용됩니다. 이 페이지에서는 두 가지를 모두 설명합니다.
홈 화면에서 미리보기 재생
ExoPlayer에서 지원하는 동영상 유형을 사용하여 미리보기를 만들고 이 미리보기에 공개적으로 액세스할 수 있다면 홈 화면에서 바로 미리보기를 재생할 수 있습니다.
PreviewProgram을 빌드할 때 아래 예와 같이 공개적으로 액세스 가능한 HTTPS URL과 함께 setPreviewVideoUri()
를 사용합니다. 미리보기는 동영상 또는 오디오일 수 있습니다.
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 리소스 디렉터리에 있으며 매니페스트에서 선언한 리소스의 이름과 일치해야 합니다. 이전 예의 매니페스트 항목을 사용하여 res/xml/previewinputservice.xml
에 빈 tv-input
태그가 있는 XML 파일을 만들 수 있습니다.
<?xml version="1.0" encoding="utf-8"?>
<tv-input/>
TV 입력 프레임워크에 이 태그가 있어야 합니다. 하지만 실시간 채널을 구성하는 데만 사용됩니다. 동영상을 렌더링하므로 태그가 비어 있어야 합니다.
동영상 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 } } }