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

Что такое TV Input Framework
Это официальное решение от Google стандартизирует работу с TV-контентом. Оно дает возможность доступа к информации о TV-каналах в системной базе устройства, взаимодействия с ними:
-
получение списка передач;
-
воспроизведение контента;
-
интеграция в единый TV-интерфейс.
Данные (каналы, программы) предоставляются:
-
системными и пользовательскими
TvInputService(через их реализации); -
системным сервисом TV Provider.
Для устройств с поддержкой DVB (цифрового телевидения) данные могут поступать через аппаратные тюнеры или физические порты приставки.
TV Input Service
Предоставляет доступ к источникам контента.
К таким источникам относятся:
-
Встроенные компоненты, например аппаратный ТВ-тюнер.
-
Внешние устройства, которые могут быть подключены через HDMI.
TV Input Service взаимодействует с ними и получает данные и видеопоток. Например, у нас есть приставка DVB с подключенным аппаратным тюнером. TV Input Service предоставит нам данные об источнике потокового вещания, о каналах и передачах. Реализация собственного сервиса описана в официальной документации.
TVProvider
Является интерфейсом, дающим доступ к базе с каналами и передачами. Через него можно получить всю информацию от источника вещания до возрастного рейтинга. При сканировании каналов с тюнера они записываются в системную базу данных, а через TVProvider мы получаем доступ к ней.
Работа TV-приложения с 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).
Для более детальной информации и тонких моментов можно обратиться к официальной документации, а мы пока идем дальше.
Простые, но важные моменты, которые надо учесть заранее:
-
Только системное приложение может получать доступ ко всему хранилищу каналов и передач.
-
После добавления каналов в TIF они привязываются к вашему пакету приложения, и другое их уже не вытащит.
-
Не забывайте про разрешения.
Для пользовательских сервисов: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 можно двумя способами:
-
Чтобы запросить все доступные источники каналов, укажем
uri = TvContract.Channels.CONTENT_URI. -
Для доступа к конкретному источнику используем идентификатор 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-приложения — важно освоить и другие части:
-
Получение передач и отображение их расписания.
-
Установка правил для телевизионного канала.
-
Воспроизведение контента.
Эти вопросы я разберу в следующих статьях 🙂
ссылка на оригинал статьи https://habr.com/ru/articles/928726/
Добавить комментарий