미리보기 동영상을 통해 사용자에게 TV 앱에 딥 링크로 연결하도록 효과적으로 장려할 수 있습니다. 미리보기는 짧은 클립부터 전체 동영상 예고편까지 다양합니다.
미리보기를 만들 때 다음 가이드라인을 고려하세요.
- 미리보기에 광고를 표시하지 마세요. 클라이언트측에서 광고를 연결하는 경우 미리보기 동영상에 연결하지 마세요. 서버측에서 광고를 연결하는 경우 광고 없는 동영상을 미리보기용으로 제공하세요.
- 최적의 품질을 위해 미리보기 동영상은 16:9 또는 4:3이어야 합니다. 미리보기 동영상의 권장 크기는 동영상 프로그램 속성을 참고하세요.
- 미리보기 동영상과 포스터 아트의 가로세로 비율이 서로 다르면 미리보기 재생 전에 홈 화면에서 동영상 가로세로 비율에 맞춰 포스터 뷰의 크기가 조절됩니다. 동영상이 레터박스 처리되지 않습니다. 예를 들어
포스터 아트 비율이
ASPECT_RATIO_MOVIE_POSTER(1:1.441)이지만 동영상 비율이 16:9이면 포스터 뷰가 16:9 영역으로 전환됩니다. - 미리보기를 만들면 DRM에 따라 콘텐츠를 공개적으로 액세스하거나 보호할 수 있습니다. 각 사례에 다른 절차가 적용됩니다. 이 페이지에서는 두 가지 모두 설명합니다.
홈 화면에서 미리보기 재생
ExoPlayer에서 지원하는 동영상 유형 중 하나를 사용하여 미리보기를 만들고 이 미리보기에 공개적으로 액세스 가능하다면 홈 화면에서 바로 미리보기를 재생할 수 있습니다.
PreviewProgram을 빌드할 때 다음 예와 같이 공개적으로 액세스 가능한 HTTPS URL과 함께 PreviewProgram setPreviewVideoUri()를 사용합니다. 미리보기는
동영상 또는 오디오일 수 있습니다.
Kotlin
val previewVideoUrl = Uri.parse("https://www.example.com/preview.mp4")
val builder = PreviewProgram.Builder()
builder.setChannelId(channelId)
// ...
.setPreviewVideoUri(previewVideoUrl)
자바
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 홈 화면은
Surface를 호출하여 서비스에 onSetSurface()를 전달합니다. 앱은
동영상을 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()
자바
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()
자바
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/<id>
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"
}
}
자바
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/<id>
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
}
}
}