채널 데이터로 작업

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)

Java

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 샘플 앱을 참고하세요.

채널 데이터 일괄 로드

대량의 채널 데이터로 시스템 데이터베이스를 업데이트할 때는 ContentResolver applyBatch() 또는 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()
    }
}

Java

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()
                }
            }
        }
    }
}

Java

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_INTENT_SCHEME와 함께 toUri(int)를 사용하여 URI를 만들고 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)에는 'app-name 열기'가 사용됩니다. 실행 가능한 앱 링크 인텐트 URI가 정의되어 있지 않으면 시스템은 '사용 가능한 링크 없음'을 사용합니다.
  • 강조 색상(COLUMN_APP_LINK_COLOR)에는 기본 앱 색상이 사용됩니다.
  • 포스터 이미지(COLUMN_APP_LINK_POSTER_ART_URI)에는 앱의 홈 화면 배너가 사용됩니다. 앱에서 배너를 제공하지 않으면 시스템은 기본 TV 앱 이미지를 사용합니다.
  • 배지 아이콘(COLUMN_APP_LINK_ICON_URI)에는 앱 이름을 표시하는 배지가 사용됩니다. 시스템에서 포스터 이미지에 앱 배너 또는 기본 앱 이미지도 사용하는 경우 앱 배지가 표시되지 않습니다.

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