미리보기 동영상

미리보기 동영상은 사용자가 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
        }
    }
}