6월 3일의 ⁠#Android11: 베타 버전 출시 행사에 참여하세요.

채널 데이터 작업

TV 입력은 설정 활동에 하나 이상의 채널에 관한 전자 프로그램 가이드(EPG) 데이터를 제공해야 합니다. 또한 업데이트 크기와 해당 업데이트를 처리하는 처리 스레드를 고려하여 데이터를 주기적으로 업데이트해야 합니다. 관련 콘텐츠와 활동을 사용자에게 안내하는 채널의 앱 링크를 제공할 수도 있습니다. 이 과정에서는 이러한 고려사항을 염두에 두고 시스템 데이터베이스에서 채널 및 프로그램 데이터를 만들고 업데이트하는 방법을 설명합니다.

TV 입력 서비스 샘플 앱을 사용해 보세요.

권한 얻기

TV 입력에서 EPG 데이터를 사용하려면 Android 매니페스트 파일에 다음과 같이 쓰기 권한을 선언해야 합니다.

    <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
    

데이터베이스에 채널 등록

Android TV 시스템 데이터베이스는 TV 입력에 사용되는 채널 데이터의 기록을 보관합니다. 설정 활동에서 TvContract.Channels 클래스의 다음과 같은 필드에 채널 데이터를 매핑해야 합니다.

TV 입력 프레임워크는 기존 방송과 오버더톱(OTT) 콘텐츠를 구분하지 않고 모두 처리할 만큼 일반적이지만, 위의 열 외에도 다음 열을 정의하면 기존 방송 채널을 더 잘 식별할 수 있습니다.

채널의 앱 링크 세부정보를 제공하려면 일부 추가 입력란을 업데이트해야 합니다. 앱 링크 입력란에 관한 자세한 내용은 앱 링크 정보 추가를 참조하세요.

인터넷 스트리밍 기반 TV 입력의 경우 각 채널을 고유하게 식별할 수 있도록 위 입력란에 원하는 값을 적절하게 할당합니다.

채널 메타데이터를 백엔드 서버에서 XML, JSON 등의 형식으로 가져온 다음 설정 활동에서 값을 시스템 데이터베이스에 다음과 같이 매핑합니다.

Kotlin

    val values = ContentValues().apply {
        put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.number)
        put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.name)
        put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.originalNetworkId)
        put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.transportStreamId)
        put(TvContract.Channels.COLUMN_SERVICE_ID, channel.serviceId)
        put(TvContract.Channels.COLUMN_VIDEO_FORMAT, channel.videoFormat)
    }
    val uri = context.contentResolver.insert(TvContract.Channels.CONTENT_URI, values)
    

자바

    ContentValues values = new ContentValues();

    values.put(Channels.COLUMN_DISPLAY_NUMBER, channel.number);
    values.put(Channels.COLUMN_DISPLAY_NAME, channel.name);
    values.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.originalNetworkId);
    values.put(Channels.COLUMN_TRANSPORT_STREAM_ID, channel.transportStreamId);
    values.put(Channels.COLUMN_SERVICE_ID, channel.serviceId);
    values.put(Channels.COLUMN_VIDEO_FORMAT, channel.videoFormat);

    Uri uri = context.getContentResolver().insert(TvContract.Channels.CONTENT_URI, values);
    

위 예에서 channel은 백엔드 서버에서 가져온 채널 메타데이터를 보유하는 객체입니다.

채널 및 프로그램 정보 표시

시스템 TV 앱에서는 사용자가 채널을 돌릴 때 그림 1과 같이 채널 및 프로그램 정보를 사용자에게 표시합니다. 채널 및 프로그램 정보가 시스템 TV 앱의 채널 및 프로그램 정보 프레젠터와 작동하도록 하려면 아래 가이드라인을 따르세요.

  1. 채널 번호(COLUMN_DISPLAY_NUMBER)
  2. 아이콘 (TV 입력의 매니페스트에 있는 android:icon)
  3. 프로그램 설명(COLUMN_SHORT_DESCRIPTION)
  4. 프로그램 제목(COLUMN_TITLE)
  5. 채널 로고(TvContract.Channels.Logo)
    • 주변 텍스트와 일치하도록 #EEEEEE 색상 사용
    • 패딩 포함 안 함
  6. 포스터 아트(COLUMN_POSTER_ART_URI)
    • 가로세로 비율(16:9~4:3)

그림 1. 시스템 TV 앱 채널 및 프로그램 정보 프레젠터

시스템 TV 앱에서는 그림 2와 같이 프로그램 가이드를 통해 포스터 아트를 비롯한 동일한 정보를 제공합니다.

그림 2. 시스템 TV 앱 프로그램 가이드

채널 데이터 업데이트

기존 채널 데이터를 업데이트할 때 데이터를 삭제한 후 다시 추가하는 대신 update() 메서드를 사용합니다. 업데이트할 기록을 선택할 때 Channels.COLUMN_VERSION_NUMBERPrograms.COLUMN_VERSION_NUMBER를 사용해 데이터의 현재 버전을 확인할 수 있습니다.

참고: ContentProvider에 채널 데이터를 추가하는 데 시간이 걸릴 수 있습니다. 나머지 채널 데이터를 백그라운드에서 업데이트하도록 EpgSyncJobService를 구성한 경우에만 현재 프로그램(현재 시간 2시간 이내 프로그램)을 추가하세요. 예를 보려면 Android TV 실시간 TV 샘플 앱을 참고하세요.

채널 데이터 일괄 로드

많은 양의 채널 데이터가 포함된 시스템 데이터베이스를 업데이트할 때는 ContentResolverapplyBatch() 또는 bulkInsert() 메서드를 사용합니다. applyBatch() 사용의 예는 다음과 같습니다.

Kotlin

    val ops = ArrayList<ContentProviderOperation>()
    val programsCount = channelInfo.mPrograms.size
    channelInfo.mPrograms.forEachIndexed { index, program ->
        ops += ContentProviderOperation.newInsert(
                TvContract.Programs.CONTENT_URI).run {
            withValues(programs[index])
            withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, programStartSec * 1000)
            withValue(
                    TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
                    (programStartSec + program.durationSec) * 1000
            )
            build()
        }
        programStartSec += program.durationSec
        if (index % 100 == 99 || index == programsCount - 1) {
            try {
                contentResolver.applyBatch(TvContract.AUTHORITY, ops)
            } catch (e: RemoteException) {
                Log.e(TAG, "Failed to insert programs.", e)
                return
            } catch (e: OperationApplicationException) {
                Log.e(TAG, "Failed to insert programs.", e)
                return
            }
            ops.clear()
        }
    }
    

자바

    ArrayList<ContentProviderOperation> ops = new ArrayList<>();
    int programsCount = channelInfo.mPrograms.size();
    for (int j = 0; j < programsCount; ++j) {
        ProgramInfo program = channelInfo.mPrograms.get(j);
        ops.add(ContentProviderOperation.newInsert(
                TvContract.Programs.CONTENT_URI)
                .withValues(programs.get(j))
                .withValue(Programs.COLUMN_START_TIME_UTC_MILLIS,
                        programStartSec * 1000)
                .withValue(Programs.COLUMN_END_TIME_UTC_MILLIS,
                        (programStartSec + program.durationSec) * 1000)
                .build());
        programStartSec = programStartSec + program.durationSec;
        if (j % 100 == 99 || j == programsCount - 1) {
            try {
                getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
            } catch (RemoteException | OperationApplicationException e) {
                Log.e(TAG, "Failed to insert programs.", e);
                return;
            }
            ops.clear();
        }
    }
    

비동기식으로 채널 데이터 처리

서버에서 스트림을 가져오거나 데이터베이스에 액세스하는 등 데이터 조작 시 UI 스레드를 차단해서는 안 됩니다. AsyncTask 사용은 비동기식으로 업데이트를 처리하는 한 가지 방법입니다. 예를 들어 백엔드 서버에서 채널 정보를 로드할 때 다음과 같이 AsyncTask를 사용할 수 있습니다.

Kotlin

    private class LoadTvInputTask(val context: Context) : AsyncTask<Uri, Unit, Unit>() {

        override fun doInBackground(vararg uris: Uri) {
            try {
                fetchUri(uris[0])
            } catch (e: IOException) {
                Log.d("LoadTvInputTask", "fetchUri error")
            }
        }

        @Throws(IOException::class)
        private fun fetchUri(videoUri: Uri) {
            context.contentResolver.openInputStream(videoUri).use { inputStream ->
                Xml.newPullParser().also { parser ->
                    try {
                        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
                        parser.setInput(inputStream, null)
                        sTvInput = ChannelXMLParser.parseTvInput(parser)
                        sSampleChannels = ChannelXMLParser.parseChannelXML(parser)
                    } catch (e: XmlPullParserException) {
                        e.printStackTrace()
                    }
                }
            }
        }
    }
    

자바

    private static class LoadTvInputTask extends AsyncTask<Uri, Void, Void> {

        private Context mContext;

        public LoadTvInputTask(Context context) {
            mContext = context;
        }

        @Override
        protected Void doInBackground(Uri... uris) {
            try {
                fetchUri(uris[0]);
            } catch (IOException e) {
              Log.d("LoadTvInputTask", "fetchUri error");
            }
            return null;
        }

        private void fetchUri(Uri videoUri) throws IOException {
            InputStream inputStream = null;
            try {
                inputStream = mContext.getContentResolver().openInputStream(videoUri);
                XmlPullParser parser = Xml.newPullParser();
                try {
                    parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
                    parser.setInput(inputStream, null);
                    sTvInput = ChannelXMLParser.parseTvInput(parser);
                    sSampleChannels = ChannelXMLParser.parseChannelXML(parser);
                } catch (XmlPullParserException e) {
                    e.printStackTrace();
                }
            } finally {
                if (inputStream != null) {
                    inputStream.close();
                }
            }
        }
    }
    

EPG 데이터를 정기적으로 업데이트해야 하는 경우 WorkManager를 사용해 매일 오전 3시와 같이 유휴 시간 동안 업데이트 프로세스를 실행해 보세요.

데이터 업데이트 작업을 UI 스레드와 분리하는 다른 기술로는 HandlerThread 클래스를 사용하는 것이 있습니다. 또는 LooperHandler 클래스를 사용해 자신만의 기술을 구현할 수도 있습니다. 자세한 내용은 프로세스 및 스레드를 참고하세요.

채널에서는 사용자가 채널 콘텐츠를 시청하는 동안 관련 활동을 쉽게 시작할 수 있도록 앱 링크를 사용할 수 있습니다. 채널 앱은 앱 링크를 통해 관련 정보나 추가 콘텐츠를 표시하는 활동을 시작하여 사용자 참여를 늘립니다. 예를 들어 앱 링크를 사용해 다음 작업을 할 수 있습니다.

  • 사용자가 관련 콘텐츠를 찾고 구매하도록 안내합니다.
  • 현재 재생 중인 콘텐츠에 대한 추가 정보를 제공합니다.
  • 에피소드 형태의 콘텐츠를 시청하는 동안 다음 에피소드를 연속으로 재생합니다.
  • 콘텐츠 재생을 중단하지 않고 사용자가 콘텐츠를 평가하거나 검토하는 등 콘텐츠와 상호작용할 수 있습니다.

사용자가 채널 콘텐츠를 시청하는 동안 선택을 눌러 TV 메뉴를 표시하면 앱 링크가 표시됩니다.

그림 1. 채널 콘텐츠가 표시되는 동안 채널 행에 표시되는 앱 링크 예

사용자가 앱 링크를 선택하면 채널 앱에서 지정한 인텐트 URI를 사용해 시스템에서 활동을 시작합니다. 앱 링크 활동이 실행되는 동안 채널 콘텐츠가 계속 재생됩니다. 사용자는 뒤로를 눌러 채널 콘텐츠로 돌아갈 수 있습니다.

앱 링크 채널 데이터 제공

Android TV에서는 채널 데이터의 정보를 사용해 각 채널의 앱 링크를 자동으로 만듭니다. 앱 링크 정보를 제공하려면 TvContract.Channels 입력란에 다음 세부정보를 지정하세요.

  • COLUMN_APP_LINK_COLOR - 채널의 앱 링크 강조 색상입니다. 강조 색상의 예는 그림 2, 콜아웃 3을 참고하세요.
  • COLUMN_APP_LINK_ICON_URI - 채널의 앱 링크에 있는 앱 배지 아이콘의 URI입니다. 앱 배지 아이콘의 예는 그림 2, 콜아웃 2를 참고하세요.
  • COLUMN_APP_LINK_INTENT_URI - 채널의 앱 링크에 있는 인텐트 URI입니다. URI는 URI_INTENT_SCHEME와 함께 toUri(int)를 사용해 만들 수 있으며, parseUri()를 사용해 URI를 다시 원래 인텐트로 변환할 수 있습니다.
  • COLUMN_APP_LINK_POSTER_ART_URI - 채널의 앱 링크 배경으로 사용된 포스터 아트의 URI입니다. 포스터 이미지의 예는 그림 2, 콜아웃 1을 참고하세요.
  • COLUMN_APP_LINK_TEXT - 채널의 앱 링크를 설명하는 링크 텍스트입니다. 앱 링크 설명의 예는 그림 2, 콜아웃 3을 참고하세요.

그림 2. 앱 링크 세부정보

채널 데이터에서 앱 링크 정보를 지정하지 않으면 시스템에서 기본 앱 링크가 생성됩니다. 시스템에서는 다음과 같이 기본 세부정보를 선택합니다.

  • 인텐트 URI(COLUMN_APP_LINK_INTENT_URI)에는 대개 앱 매니페스트에 정의된 CATEGORY_LEANBACK_LAUNCHER 카테고리의 ACTION_MAIN 활동이 사용됩니다. 이 활동이 정의되어 있지 않으면 작동하지 않는 앱 링크(사용자가 앱 링크를 클릭해도 아무 변화가 없음)가 표시됩니다.
  • 설명 텍스트(COLUMN_APP_LINK_TEXT)에는 '앱 이름 열기'가 사용됩니다. 실행 가능한 앱 링크 인텐트 URI가 정의되어 있지 않으면 '사용 가능한 링크 없음'이 사용됩니다.
  • 강조 색상(COLUMN_APP_LINK_COLOR)에는 기본 앱 색상이 사용됩니다.
  • 포스터 이미지(COLUMN_APP_LINK_POSTER_ART_URI)에는 앱의 홈 화면 배너가 사용됩니다. 앱에서 배너를 제공하지 않으면 기본 TV 앱 이미지가 사용됩니다.
  • 배지 아이콘(COLUMN_APP_LINK_ICON_URI)에는 앱 이름을 표시하는 배지가 사용됩니다. 시스템에서 포스터 이미지에 앱 배너 또는 기본 앱 이미지도 사용 중인 경우 앱 배지가 표시되지 않습니다.

앱의 설정 활동에서 채널의 앱 링크 세부정보를 지정합니다. 이러한 앱 링크 세부정보는 언제든지 업데이트할 수 있으므로 앱 링크가 채널 변경사항과 일치해야 하는 경우 앱 링크 세부정보를 업데이트하고 필요에 따라 ContentResolver.update()를 호출하세요. 채널 데이터 업데이트에 관한 자세한 내용은 채널 데이터 업데이트를 참고하세요.