Швидка інтеграція Google Chromecast в Android додаток

Добрий день, я Android Team Lead в компанії з розробки мобільних додатків Trinity Digital. Наша компанія існує на ринку три роки і в 2015-му ми увійшли в топ-10 кращих розробників Москви. Наш другий офіс знаходиться в Петрозаводську, там я і керую командою Android-розробників. У цій статті хочу розповісти про те, як швидко додати додаток можливість взаємодіяти з пристроєм Google Chromecast, а саме — відправляти один відео-файл на відтворення і управляти переглядом. Отримати пристрій вдалося завдяки конкурсу Device Lab від Google.

Якщо ви не знайомі з пристроєм Chromecast, то можете почитати оглядову статтю ось тут. Незважаючи на те, що ця стаття про першу версію Chromecast, вона дасть загальне уявлення про всіх сімействі пристроїв і принцип їх роботи.

Додаток, на прикладі якого я розповім про технології — «Рецепти Юлії Висоцької».



Стаття автора Андрія Хитрого, в рамках конкурсу <a href=«special.habrahabr.ru/google/lab>Device Lab від Google».

Це один з найуспішніших наших проектів, що має близько півмільйона користувачів. Додаток являє собою збірник більш ніж 1500 рецептів, в тому числі у відео-форматі, що і дозволило мені інтегрувати в нього Google Chromecast. Отже, почнемо.

Перші кроки
Приступимо до інтеграції Chromecast в наш Android-додаток. Ми розглянемо найпростіший випадок, коли в додатку є Activity, що містить деякий відео контент (один відео файл). Для цього скористаємося бібліотекою CastCompanionLibrary-android, яка спрощує інтеграцію до декількох кроків.

Для початку створимо порожній проект в Android Studio і додамо до файлу app/build.gradle залежність.

dependencies {
compile 'com.google.android.libraries.cast.companionlibrary:ccl:2.8.4'
}

Бібліотека використовує сінглтон VideoCastManager для організації взаємодії. В першу чергу, ми повинні ініціалізувати цей сінглтон за допомогою об'єкта конфігурації. Більшість опцій прокоментовано в коді.

// Core.java
public class Core extends Application {

@Override
public void onCreate() {
super.onCreate();

CastConfiguration options = new CastConfiguration.Builder("CC1AD845")
.enableAutoReconnect() // Відновлення після розриву з'єднання
.enableDebug() // Дозволяємо налагодження, щоб логи були докладними
.enableLockScreen() // Можливість керування на екрані блокування
.enableNotification() // Можливість керування через оприлюднення + можливі дії
.addNotificationAction(CastConfiguration.NOTIFICATION_ACTION_REWIND, false)
.addNotificationAction(CastConfiguration.NOTIFICATION_ACTION_PLAY_PAUSE, true)
.addNotificationAction(CastConfiguration.NOTIFICATION_ACTION_DISCONNECT, true)
.enableWifiReconnection() // Відновлення, після зміни wifi мережі
.setForwardStep(10) // Крок перемотування в секундах
.build();
VideoCastManager.initialize(this, options);
}
}

У конструктор CastConfiguration ми передаємо ідентифікатор Media Receiver. Цей код визначає стилізацію плеєра Chromecast. Ми не будемо зупинятися на ньому більш докладно можна почитати на офіційній сторінці. Інформацію про інших опціях VideoCastManager можна знайти в github.

Зміна маніфесту програми
Варто зазначити, що для коректної роботи управління через оповіщення і на заблокованому екрані. необхідно додати в маніфест додатки оголошення необхідних Activities, Services і Receivers.

У прикладі, який пропонують розробники, я не знайшов цих оголошень, що досить дивно, оскільки без них управління з області оповіщень і на заблокованому екрані не буде працювати, чи буде працювати не коректно.

<receiver android:name="com.google.android.libraries.cast.companionlibrary.remotecontrol.VideoIntentReceiver" >
<intent-filter>
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="com.google.android.libraries.cast.companionlibrary.action.toggleplayback" />
<action android:name="com.google.android.libraries.cast.companionlibrary.action.stop" />
</intent-filter>
</receiver>

<service
android:name="com.google.android.libraries.cast.companionlibrary.notification.VideoCastNotificationService"
android:exported="false" >
<intent-filter>
<action android:name="com.google.android.libraries.cast.companionlibrary.action.toggleplayback" />
<action android:name="com.google.android.libraries.cast.companionlibrary.action.stop" />
<action android:name="com.google.android.libraries.cast.companionlibrary.action.notificationvisibility" />
</intent-filter>
</service>

<service android:name="com.google.android.libraries.cast.companionlibrary.cast.reconnection.ReconnectionService"/>
<activity android:name="com.google.android.libraries.cast.companionlibrary.cast.player.VideoCastControllerActivity"/>


Відтворення одного файлу
Для організації взаємодії між Chromecast і додатком Android бібліотека використовує клас VideoCastConsumerImpl. Спочатку він розрахований для роботи з чергою відеофайлів, але, оскільки наш додаток не передбачає наявність черги, ми дещо змінимо цей клас.

// SingleVideoCastConsumer.java
public abstract class SingleVideoCastConsumer extends VideoCastConsumerImpl {
private AppCompatActivity activity;
private final String videoUrl;
private final String title;
private final String subtitle;
private final String imageUrl;
private final String contentType;

public SingleVideoCastConsumer(AppCompatActivity activity, String videoUrl, String title, String subtitle, String imageUrl, String contentType) {
this.activity = activity;
this.videoUrl = videoUrl;
this.title = title;
this.subtitle = subtitle;
this.imageUrl = imageUrl;
this.contentType = contentType;
}

public abstract void onPlaybackFinished();
public abstract void onQueueLoad(final MediaQueueItem[] items, final int startIndex, final int repeatMode,
final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException;

@Override
public void onMediaQueueUpdated(List<MediaQueueItem>queueItems, MediaQueueItem item, int repeatMode, boolean shuffle) {
// Якщо в черзі більше немає елементів, то оповіщаємо про завершення відтворення
if(queueItems != null && queueItems.size() == 0) {
onPlaybackFinished();
}
}

@Override
public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId,
boolean wasLaunched) {
// Змінити стан кнопки Cast
activity.invalidateOptionsMenu();
// Створюємо метадані типу відеофайл
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);

// Заголовок
movieMetadata.putString(MediaMetadata.KEY_TITLE, title);

// Підзаголовок
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle);

// Картинка, яка буде показана при завантаженні
movieMetadata.addImage(new WebImage(Uri.parse(imageUrl)));

// Створюємо інформацію про медіа контент
MediaInfo info = new MediaInfo.Builder(videoUrl)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(contentType)
.setMetadata(movieMetadata)
.build();

// Створюємо елемент черги медіафайлів
MediaQueueItem item = new MediaQueueItem.Builder(info).build();
try {
// Оновлюємо чергу Chromecast, вона завжди містить 1 елемент, оскільки у нас всього 1 відео
onQueueLoad(new MediaQueueItem[]{item}, 0, MediaStatus.REPEAT_MODE_REPEAT_OFF, null);
} catch (TransientNetworkDisconnectionException e) {
e.printStackTrace();
} catch (NoConnectionException e) {
e.printStackTrace();
}
}

@Override
public void onDisconnected() {
// Змінити стан кнопки Cast
activity.invalidateOptionsMenu();
}
}

Основними методами, на яких варто загострити увагу є onApplicationConnected і onQueueLoad. Як могли помітити, бібліотека використовує MediaInfo, MediaMetadata і MediaQueueItem для роботи з медіа-даними. у методі onApplicationConnected, який буде викликаний як тільки додаток підключиться до Chromecast, ми створимо об'єкт черги і викличемо абстрактний метод onQueueLoad, який пізніше реалізуємо Activity. Опис роботи методів можна знайти у коментарях до коду.

Використання Activity
Наступним (і останнім) кроком буде реалізація нашої Activity.

ublic class MainActivity extends AppCompatActivity {

private VideoCastManager castManager;
private VideoCastConsumer castConsumer;
private Toolbar toolbar;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R. layout.activity_main);

toolbar = (Toolbar) findViewById(R. id.toolbar);
setSupportActionBar(toolbar);

castManager = VideoCastManager.getInstance();
castConsumer = new SingleVideoCastConsumer(this,
http://example.com/somemkvfile.mkv", // посилання на файл
"Jet Packs Was Yes", "Periphery", // підзаголовок і заголовок
"http://fugostudios.com/wp-content/uploads/2012/02/periphery720p-600x338.jpg", // картинка
"video/mkv" // тип файлу
) {
@Override
public void onPlaybackFinished() {
// Відключаємо пристрій
disconnectDevice();
}

@Override
public void onQueueLoad(MediaQueueItem[] items, int startIndex,
int repeatMode, JSONObject customData)
throws TransientNetworkDisconnectionException, NoConnectionException {
// Простий перенесення черги з нашого SingleVideoCastConsumer в castManager
castManager.queueLoad(items, startIndex, repeatMode, customData);
}
};
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R. menu.main, menu);
// Додаємо кнопку Cast в toolbar
castManager.addMediaRouterButton(menu, R. id.media_route_menu_item);
return true;
}

@Override
public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
// Даємо можливість керувати гучністю відтворення за допомогою
// фізичних кнопок
return castManager.onDispatchVolumeKeyEvent(event, 0.05)
|| super.dispatchKeyEvent(event);
}

@Override
protected void onResume() {
// Підключаємо castConsumer і збільшуємо лічильник підключень
if (castManager != null) {
castManager.addVideoCastConsumer(castConsumer);
castManager.incrementUiCounter();
}

super.onResume();
}

@Override
protected void onPause() {
// Зменшуємо лічильник підключень і відключаємо castConsumer
castManager.decrementUiCounter();
castManager.removeVideoCastConsumer(castConsumer);
super.onPause();
}

// З незрозумілої мені причини відключення пристрою без затримки
// не працювало, але якщо використовувати 100-500 мс затримку, то пристрій
// відключається нормально.
private void disconnectDevice() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
castManager.disconnect();
}
},500);
}
}

У нашій Activity немає нічого складного, ми отримуємо VideoCastManager в методі onCreate. У методах onResume і onPause управляємо життєвим циклом нашого підключення до Chromecast. А методи onCreateOptionsMenu і dispatchKeyEvent організують UX частина нашої інтеграції. На жаль, я так і не зрозумів, чому castManager.disconnect() викидав помилку, але яка програма обходиться без милиць.

Design Checklist
Тепер звернемося до дизайну. Більшість з Design Guidelines за нас реалізує вище описана библиотка, але деякі пункти потрібно реалізувати вручну.

  • Стилізація діалогів
  • Показ інтро для користувача
Ми виконували інтеграцію з Google Chromecast у програмі «Рецепти Юлії Висоцької». У цьому додатку присутні відео-рецепти і було б непогано додати можливість показувати їх через Chromecast.

Якщо до рецептом прикріплений відео-файл, то ми даємо можливість користувачеві переглянути його через додаток на його вибір. Це виглядає ось так:


Після інтеграції з Chromecast і при наявності в нашій мережі налаштованого Chromecast екран буде виглядати так:


Показ інтро користувачеві
Тепер нам необхідно показати користувачеві інформацію про те, що Chromecast доступний для стрімінг і він може переглянути відео-рецепт через нього. Для показу цієї інформації ми скористаємося IntroductoryOverlay з тієї ж бібліотеки. Я не буду описувати параметри цього класу, оскільки вони очевидні і нам потрібно вказати тільки супроводжуючий текст. Виглядає це ось так:


Стилізація діалогів

Після того, як користувач натисне на іконку Cast, крім показу відео у нього повинна з'явитися можливість управляти відтворенням через діалоги. Цей функціонал також реалізував у використаній нами бібліотеці і все що нам потрібно, це просто стилізувати діалоги.

Для цього ми будемо використовувати CastConfiguration.Builder і метод setMediaRouteDialogFactory.

options.setMediaRouteDialogFactory(new MediaRouteDialogFactory() {
@NonNull
@Override
public MediaRouteChooserDialogFragment onCreateChooserDialogFragment() {
return new MediaRouteChooserDialogFragment() {
@Override
public MediaRouteChooserDialog onCreateChooserDialog(Context context, Bundle savedInstanceState) {
return new MediaRouteChooserDialog(context, R. style.Theme_MediaRouter_Light);
}
};
}

@NonNull
@Override
public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() {
return new MediaRouteControllerDialogFragment(){
@Override
public MediaRouteControllerDialog onCreateControllerDialog(Context context, Bundle savedInstanceState) {
return new MediaRouteControllerDialog(context, R. style.Theme_MediaRouter_Light);
}
};
}
})

Тут ми вказали два стилю, для діалогу вибору пристрою і для діалогу керування відтворенням.

<style name="Theme.MediaRouter.Light" parent="Base.Theme.AppCompat.Light.Dialog.Alert">
<item name="android:windowNoTitle">false</item>
<item name="mediaRouteButtonStyle">@style/Widget.MediaRouter.Light.MediaRouteButton</item>
<item name="MediaRouteControllerWindowBackground">@drawable/mr_dialog_material_background_light</item>
<item name="mediaRouteOffDrawable">@drawable/ic_cast_off_light</item>
<item name="mediaRouteConnectingDrawable">@drawable/mr_ic_media_route_connecting_mono_light</item>
<item name="mediaRouteOnDrawable">@drawable/ic_cast_on_light</item>
<item name="mediaRouteCloseDrawable">@drawable/mr_ic_close_light</item>
<item name="mediaRoutePlayDrawable">@drawable/mr_ic_play_light</item>
<item name="mediaRoutePauseDrawable">@drawable/mr_ic_pause_light</item>
<item name="mediaRouteCastDrawable">@drawable/mr_ic_cast_light</item>
<item name="mediaRouteAudioTrackDrawable">@drawable/mr_ic_audiotrack_light</item>
<item name="mediaRouteDefaultIconDrawable">@drawable/ic_cast_grey</item>
<item name="mediaRouteBluetoothIconDrawable">@drawable/ic_bluetooth_grey</item>
 <item name="mediaRouteTvIconDrawable">@drawable/ic_tv_light</item>
<item name="mediaRouteSpeakerIconDrawable">@drawable/ic_speaker_light</item>
<item name="mediaRouteSpeakerGroupIconDrawable">@drawable/ic_speaker_group_light</item>
<item name="mediaRouteChooserPrimaryTextStyle">@style/Widget.MediaRouter.ChooserText.Primary.Light</item>
<item name="mediaRouteChooserSecondaryTextStyle">@style/Widget.MediaRouter.ChooserText.Secondary.Light</item>
<item name="mediaRouteControllerTitleTextStyle">@style/Widget.MediaRouter.ControllerText.Title.Dark</item>
<item name="mediaRouteControllerPrimaryTextStyle">@style/Widget.MediaRouter.ControllerText.Primary.Light</item>
<item name="mediaRouteControllerSecondaryTextstyle">@style/Widget.MediaRouter.ControllerText.Secondary.Light</item>
</style> 

Параметри стилю мають говорять назви, ви можете експериментувати з ними, щоб досягти найкращого результату для вашого застосування. У нас вийшло ось так:



Ось ще кілька скріншотів з програми, які показують управління з області оповіщень і на заблокованому екрані:


Сподіваюся, що ви знайшли статтю корисною, повний вихідних код демо проекту лежить на github. Задавайте питання в коментарях, в наступній статті я постараюся зібрати відповіді на часті питання і розповісти про Media Receivers, управління чергою відтворення і стилізації MediaReceivers. Більш повну інформацію про інтеграцію з іншими платформами, а також приклади ви можете знайти на офіційній сторінці.

Більш докладні приклади коду, в тому числі і для інших платформ, можна знайти на тут. Детальну інформацію про принципи взаємодії з користувачем можна знайти на Design Checklist.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.