Смотрю и слушаю где хочу. Интегрируем Chromecast в Android-приложение
На улице я часто слушаю аудиокниги и подкасты со смартфона. Когда прихожу домой, мне хочется продолжить слушать их на Android TV или Google Home. Но далеко не все приложения поддерживают Chromecast. А было бы удобно.
По статистике Google за последние 3 года, количество девайсов на Android TV увеличилось в 4 раза, а число партнеров-производителей уже превысило сотню: «умные» телевизоры, колонки, TV-приставки. Все они поддерживают Chromecast. Но в маркете ещё много приложений, которым явно не хватает интеграции с ним.
В этой статье я хочу поделиться своим опытом интеграции Chromecast в Android-приложение для воспроизведения медиа-контента.
Как это работает
Если вы впервые слышите слово «Chromecast», то постараюсь вкратце рассказать. С точки зрения пользования, это выглядит примерно так:
- Пользователь слушает музыку или смотрит видео через приложение или веб-сайт.
- В локальной сети появляется Chromecast-девайс.
- В интерфейсе плеера должна появиться соответствующая кнопка.
- Нажав её, пользователь выбирает нужный девайс из списка. Это может быть Nexus Player, Android TV или «умная» колонка.
- Дальше воспроизведение продолжается именно с этого девайса.
Технически происходит примерно следующее:
- Google Services отслеживают наличие Chromecast девайсов в локальной сети посредством бродкастинга.
- Если к вашему приложению подключен MediaRouter, то вам придёт событие об этом.
- Когда пользователь выбирает Cast-девайс, и подключается к нему, открывается новая медиа-сессия (CastSession).
- Уже в созданную сессию мы будем передавать контент для воспроизведения.
Звучит очень просто.
Интеграция
У Google есть свой SDK для работы с Chromecast, но он плохо покрыт документацией, а его код обфусцирован. Поэтому многие вещи пришлось проверять методом тыка. Давайте обо всём по порядку.
Инициализация
Для начала нам надо подключить Cast Application Framework и MediaRouter:
Затем Cast Framework должен получить идентификатор приложения (об этом позже), и типы поддерживаемого медиаконтента. То есть если у нас приложение воспроизводит только видео, то кастинг на колонку Google Home будет невозможен, и в списке девайсов её не будет. Для этого нужно создать реализацию OptionsProvider:
И объявить его в Manifest:
Регистрируем приложение
Чтобы Chromecast мог работать с нашим приложением, его необходимо зарегистрировать в Google Cast SDK Developers Console. Для этого потребуется аккаунт Chromecast разработчика (не путать с аккаунтом разработчика Google Play). При регистрации придётся внести разовый взнос в 5$. После публикации ChromeCast Application нужно немного подождать.
В консоли можно изменить внешний вид Cast-плеера для девайсов с экраном и посмотреть аналитику кастинга в рамках приложения.
MediaRouter
MediaRouteFramework – это механизм, который позволяет находить все удалённые устройства воспроизведения вблизи пользователя. Это может быть не только Chromecast, но и удалённые дисплеи и колонки с использованием сторонних протоколов. Но нас интересует именно Chromecast.
В MediaRouteFramework есть View, которая отражает состояние медиароутера. Есть два способа её подключить:
А из кода требуется всего лишь зарегистрировать кнопку в CastButtonFactory. тогда в нее будет прокидываться текущее состояние медиароутера:
Теперь, когда приложение зарегистрировано, и MediaRouter настроен, можно подключаться к ChromeCast-девайсам и открывать сессии к ним.
Кастинг медиаконтента
ChromeCast поддерживает три основных вида контента:
В зависимости от настроек плеера, типа медиаконтента и cast-девайса, интерфейс плеера может отличаться.
CastSession
Итак, пользователь выбрал нужный девайс, CastFramework открыл новую сессию. Теперь наша задача заключается в том, чтобы отреагировать на это и передать девайсу информацию для воспроизведения.
Чтобы узнать текущее состояние сессии и подписаться на обновление этого состояния, воспользуемся объектом SessionManager:
А ещё можем узнать, нет ли открытой сессии в данный момент:
У нас есть два основных условия, при которых мы можем начинать кастинг:
- Сессия уже открыта.
- Есть контент для кастинга.
При каждом из этих двух событий можем проверять состояние, и если всё в порядке, то начинать кастить.
Кастинг
Теперь, когда у нас есть что кастить и куда кастить, можем перейти к самому главному. Помимо всего прочего, у CastSession есть объект RemoteMediaClient, который отвечает за состояние воспроизведения медиаконтента. С ним и будем работать.
Создадим MediaMetadata, где будет храниться информация об авторе, альбоме и т. д. Очень похоже на то, что мы передаём в MediaSession, когда начинаем локальное воспроизведение.
Параметров у MediaMetadata много, и их лучше посмотреть в документации. Приятно удивило, что можно добавить изображение не через bitmap, а просто ссылкой внутри WebImage.
Объект MediaInfo несёт информацию о метаданных контента и будет говорить о том, откуда медиаконтент брать, какого он типа, как его проигрывать:
Напомню, что contentType – это тип контента по спецификации MIME.
Также в MediaInfo можно передать рекламные вставки:
- setAdBreakClips – принимает список рекламных роликов AdBreakClipInfo с указанием ссылок на контент, заголовка, тайминга и временем, через которое реклама становится пропускаемой.
- setAdBreaks – информация о разметке и тайминге рекламных вставок.
В MediaLoadOptions мы описываем то, как будем обрабатывать медиапоток (скорость, начальная позиция). Также документация говорит, что через setCredentials можно передать заголовок запроса для авторизации, но у меня запросы от Chromecast не включали в себя заявленные поля для авторизации.
После того как всё готово, мы можем отдать все данные в RemoteMediaClient, и Chromecast начнёт воспроизведение. Важно поставить локальное воспроизведение на паузу.
Обработка событий
Видео заиграло, а что дальше? Что если пользователь нажмёт паузу на телевизоре? Чтобы узнавать о событиях, происходящих со стороны Chromecast, у RemoteMediaClient есть обратные вызовы:
Узнать текущий прогресс тоже просто:
Опыт интеграции с существующим плеером
В приложении, над которым я работал, уже был готовый медиаплеер. Стояла задача интегрировать в него поддержку Chromecast. В основе медиаплеера лежал State Machine, и первой мыслью было добавить новое состояние: «CastingState». Но эта идея сразу была отвергнута, потому что каждое состояние плеера отражает состояние воспроизведения, и не важно, что служит реализацией ExoPlayer или ChromeCast.
Тогда пришла идея сделать некую систему делегатов с расстановкой приоритетов и обработкой «жизненного цикла» плеера. Все делегаты могут получать события о состоянии плеера: Play, Pause и т.д. — но только ведущий делегат будет воспроизводить медиаконтент.
У нас есть примерно такой интерфейс плеера:
Внутри у него будет лежать State Machine с таким множеством состояний:
- Empty — начальное состояние до инициализации.
- Preparing — плеер инициализирует воспроизведение медиаконтента.
- Prepared — медиаданные загружены и готовы к воспроизведению.
- Playing
- Paused
- WaitingForNetwork
- Error
Раньше каждое состояние при инициализации отдавало команду в ExoPlayer. Теперь оно будет отдавать команду в список Playing-делегатов, и «Ведущий» делегат сможет его обработать. Поскольку делегат реализует все функции плеера, то его тоже можно наследовать от интерфейса плеера и при необходимости использовать отдельно. Тогда абстрактный делегат будет выглядеть так:
Для примера я упростил интерфейсы. В реальности событий немного больше.
Делегатов может быть сколько угодно, как и источников воспроизведения. А делегат для Chromecast может выглядеть примерно так:
Прежде чем отдать команду о воспроизведении, нам надо определиться с ведущим делегатом. Для этого они добавляются в порядке приоритета в плеер, и каждый из них может отдавать состояние своей готовности в методе readyForLeading(). Полный код примера можно увидеть на GitHub.
Есть ли жизнь после ChromeCast
После того как я интегрировал поддержку Chromecast в приложение, мне стало приятнее приходить домой и наслаждаться аудиокнигами не только через наушники, но и через Google Home. Что касается архитектуры, то реализация плееров в разных приложениях может различаться, поэтому не везде такой подход будет уместен. Но для нашей архитектуры это подошло. Надеюсь, эта статья была полезной, и в ближайшем будущем появится больше приложений, умеющих интегрироваться с цифровым окружением!
Источник