미리보기 동영상

미리보기 동영상을 통해 사용자에게 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
        }
    }
}