Работа с телевизионными каналами на Android TV: учимся использовать TIF в 2025. Стартовый гайд для разработчиков

от автора

Всем привет! Меня зовут Андрей Юрин, я android-разработчик в онлайн-кинотеатре KION. При создании приложения под Android TV у вас наверняка могут возникнуть вопросы: как получить доступ к списку телевизионных каналов и как организовать у себя трансляцию? В этом материале я отвечу на них и расскажу про взаимодействие с телевизором с помощью Android TV Input Framework (TIF), а также получение через него списка доступных каналов. По сути это первый шаг к созданию полноценного TV-приложения.

Что такое TV Input Framework

Это официальное решение от Google стандартизирует работу с TV-контентом. Оно дает возможность доступа к информации о TV-каналах в системной базе устройства, взаимодействия с ними:

  • получение списка передач;

  • воспроизведение контента;

  • интеграция в единый TV-интерфейс.

Данные (каналы, программы) предоставляются:

  • системными и пользовательскими TvInputService (через их реализации);

  •  системным сервисом TV Provider. 

Для устройств с поддержкой DVB (цифрового телевидения) данные могут поступать через аппаратные тюнеры или физические порты приставки.

TV Input Service 

Предоставляет доступ к источникам контента.
К таким источникам относятся:

  1. Встроенные компоненты, например аппаратный ТВ-тюнер.

  2. Внешние устройства, которые могут быть подключены через HDMI.

TV Input Service взаимодействует с ними и получает данные и видеопоток. Например, у нас есть приставка DVB с подключенным аппаратным тюнером. TV Input Service предоставит нам данные об источнике потокового вещания, о каналах и передачах. Реализация собственного сервиса описана в официальной документации.

TVProvider

Является интерфейсом, дающим доступ к базе с каналами и передачами. Через него можно получить всю информацию от источника вещания до возрастного рейтинга. При сканировании каналов с тюнера они записываются в системную базу данных, а через TVProvider мы получаем доступ к ней.

Работа TV-приложения с TIF

Полноценный флоу есть на этой официальной схеме:

Схема работы с TIF: разбираясь в ней с нуля, ты чувствуешь себя как Джерри

Схема работы с TIF: разбираясь в ней с нуля, ты чувствуешь себя как Джерри

Реализации TVInputService предустановлены вендором в прошивке устройства. Их наличие можно проверить с помощью tvInputManager.getTvInputList(). Обычно они уже установлены по умолчанию — если их нет, то нужно искать причину в приложении вашего провайдера или настройках.

Сама работа устройства с TIF выглядит так:

1. TVInputService по расписанию загружают каналы и передачи в системную базу TVProvider:

Данные синхронизируются через реализацию EpgSyncJobService 
(Абстрактный класс, который помогает нам в создании сервиса обновления данных о каналах и передачах. Он получает информацию от TVInputService и  актуализирует ее в TV Provider.)  

2. Далее мы запрашиваем данные об установленных TVInputService
(содержат информацию об источниках, inputId, его типе, активности в данный момент, поддержке записи и т.д.)

3. Далее получаем необходимую информацию о каналах из TVProvider с помощью контракта TvContract. Затем в приложении выводим их на экран.

4. Пользователь взаимодействует с нашим списком:
Мы отслеживаем его клик и передаем данные о нужном канале в систему, которая определяет, к какому сервису TVInputService он относится.

5. Приложение создает сессию для конкретного просмотра TVInputService.Session через TVInputManager, который управляет взаимодействием с источником:

После настройки сессии она определяет, как будем получать поток для данного телеканала (тюнер, url).

Для более детальной информации и тонких моментов можно обратиться к официальной документации, а мы пока идем дальше.

Простые, но важные моменты, которые надо учесть заранее:

  1. Только системное приложение может получать доступ ко всему хранилищу каналов и передач.

  2. После добавления каналов в TIF они привязываются к вашему пакету приложения, и другое их уже не вытащит.

  3. Не забывайте про разрешения.

Для пользовательских сервисов:
android.permission.READ_TV_LISTINGS — доступ к каналам, передачам, расписанию и т. д.

Для системных:
com.android.providers.tv.permission.READ_EPG_DATA — чтение передач;
com.android.providers.tv.permission.WRITE_EPG_DATA — запись передач;
com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA — полный доступ к передачам.

Стандартная реализация

Первое, что нам нужно, — стартовый экран с сеткой, на котором мы выведем наши телеканалы. У него будет три состояния: загрузка, ошибка и успешное получение данных.

// Главный экран для отображения каналов @Composable   fun TVScreen(       viewModel: TVViewModel = koinViewModel(),       navController: NavController,   ) {          val state by viewModel.channelsState.collectAsState()          when (val data = state) {           TVState.Loading -> Loading()           is TVState.Success -> ListChannels(               channels = data.channels,               onClick = {                   navController.navigate(route = TvViewScreen(it))               }           )              is TVState.Error -> Error()       }   }  // Отображение в виде сетки @Composable   private fun ListChannels(       channels: List<ChannelModel>,       onClick: (ChannelModel) -> Unit   ) {       LazyVerticalGrid(           columns = GridCells.Fixed(5),       ) {           items(channels) { channel ->               ChannelItem(channel, onClick)           }       }}       // Карточка канала @Composable   private fun ChannelItem(channel: ChannelModel, onClick: (ChannelModel) -> Unit) {       var isFocused by remember { mutableStateOf(false) }          Box(           modifier = Modifier               .padding(horizontal = 10.dp, vertical = 5.dp)               .sizeIn(minHeight = 200.dp)               .background(                   if (isFocused) {                       Color(0x1A00FFFF)                   } else {                       Color(0xFF00FFFF)                   }               )               .clickable {                   onClick.invoke(channel)               }               .onFocusChanged {                   isFocused = it.isFocused               }               .focusable(),           contentAlignment = Alignment.Center       ) {           Text(text = channel.name.orDefault("no channel"))       }   }  TVViewModel.kt class TVViewModel(       private val tifManager: TifManager   ) : ViewModel() {          private val _channelsState = MutableStateFlow<TVState>(TVState.Loading)       val channelsState = _channelsState.asStateFlow()          init {           initChannels()       }          fun initChannels() {           viewModelScope.launch {               runCatching { tifManager.getAllChannels() }                   .onSuccess { channels ->                       _channelsState.emit(                           TVState.Success(                               channels                                   .filter { it.number != null }                           )                       )                   }                   .onFailure { _channelsState.emit(TVState.Error(it.localizedMessage.orEmpty())) }           }       }   }

Следующим шагом мы создадим TifManager — собственную прослойку между приложением и TIF. Для получения каналов из TIF мы используем TvProvider — компонент Android, позволяющий обмениваться данными между приложениями.

Указать URI можно двумя способами:

  1. Чтобы запросить все доступные источники каналов, укажем uri = TvContract.Channels.CONTENT_URI.

  2. Для доступа к конкретному источнику используем идентификатор tvInputId, например uri = TvContract.buildChannelsUriForInput("com.test.tvinput/.test.TvInputService").

Теперь указываем, что нам необходимо получить:

val projection = arrayOf(       TvContract.Channels.COLUMN_DISPLAY_NAME,     TvContract.Channels.COLUMN_DISPLAY_NUMBER,     TvContract.Channels.COLUMN_INPUT_ID,     TvContract.Channels._ID   )

В этом фрагменте:
TvContract.Channels.COLUMN_DISPLAY_NAME — название канала;

TvContract.Channels.COLUMN_DISPLAY_NUMBER — номер канала;

TvContract.Channels.COLUMN_INPUT_ID — идентификатор, который связывает с источником TV-контента;

TvContract.Channels._ID — Id канала.

Также нужна модель данных, которую мы будем передавать к нам ChannelModel:

data class ChannelModel(       val name: String?,       val number: String?,       val tvInputId: String?,       val id: String,       val channelUri: String   )

Появляется новое поле channelUri. Это идентификатор канала в формате content://, который используется системой. Он нужен для воспроизведения и появляется в процессе получения всех каналов методом TvContract.buildChannelUri(id).

Итоговая реализация выглядит так:

private val uri = TvContract.Channels.CONTENT_URI   private val contentResolver: ContentResolver = context.contentResolver      val projection = arrayOf(       TvContract.Channels.COLUMN_DISPLAY_NAME,       TvContract.Channels.COLUMN_DISPLAY_NUMBER,       TvContract.Channels.COLUMN_INPUT_ID,       TvContract.Channels._ID   )  override suspend fun getAllChannels(): List<ChannelModel> = withContext(Dispatchers.Default) {       val cursor = contentResolver.query(uri, projection, null, null, null)          buildList {           cursor?.use { c ->               val nameIndex = c.getColumnIndex(TvContract.Channels.COLUMN_DISPLAY_NAME)               val numberIndex = c.getColumnIndex(TvContract.Channels.COLUMN_DISPLAY_NUMBER)               val inputIdIndex = c.getColumnIndex(TvContract.Channels.COLUMN_INPUT_ID)               val channelIdIndex = c.getColumnIndex(TvContract.Channels._ID)                  while (c.moveToNext()) {                   val name = c.getString(nameIndex)                   val number = c.getString(numberIndex)                   val inputId = c.getString(inputIdIndex)                   val id = c.getLong(channelIdIndex)                      val channelUri = TvContract.buildChannelUri(id)                      val finalChannelModel = ChannelModel(                       tvInputId = inputId,                       name = name,                       number = number,                       channelUri = channelUri.toString(),                       id = id.toString()                   )                      add(finalChannelModel)               }           } ?: errorChannelNotFound()       }   }

С ней также можно ознакомиться на Github.


На этом примере я показал базовую реализацию работы TV-приложения (получение каналов). Использование Android TIF упрощает разработку, так как он дает готовую инфраструктуру и позволяет не заботиться о реализации своих компонентов и их взаимодействии.

Но это был лишь первый шаг для создания полноценного TV-приложения — важно освоить и другие части:

  1. Получение передач и отображение их расписания.

  2. Установка правил для телевизионного канала.

  3. Воспроизведение контента.

Эти вопросы я разберу в следующих статьях 🙂


ссылка на оригинал статьи https://habr.com/ru/articles/928726/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *