Быстрая интеграция Google Chromecast в Android приложение

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

Если вы не знакомы с устройством Chromecast, то можете почитать обзорную статью вот тут. Несмотря на то, что эта статья про первую версию Chromecast, она даст общее представление о всем семействе устройств и принципе их работы.

Приложение, на примере которого я расскажу о технологии — «Рецепты Юлии Высоцкой».

ff65f1b666b54de68acdee80d58d7378.jpg

Статья автора Андрея Хитрого, в рамках конкурса «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.

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


    
        
        
        
        
    



    
        
        
        
    




Воспроизведение одного файла


Для организации взаимодействия между 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(ListqueueItems, 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.

Если к рецепту прикреплен видео-файл, то мы даем возможность пользователю просмотреть его через приложение на его выбор. Это выглядит вот так:

bf378329e25a4beeb31dcf777b3cbe7c.png

После интеграции с Chromecast и при наличии в нашей сети настроенного Chromecast экран будет выглядеть так:
90d49f8b80b54fb0abf22e6f53e29276.png

Показ интро пользователю


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

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

После того, как пользователь нажмет на иконку 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);
                            }
                        };
                    }
                })
								

Здесь мы указали два стиля, для диалога выбора устройства и для диалога управления воспроизведением.
 
		

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

564beaf40622415b9c14abcbdf2d4ed9.jpg

Вот еще несколько скриншотов из приложения, которые показывают управление из области оповещений и на заблокированном экране:

a8bcd3f376e14d0f8dc0326d1236ffd8.jpg

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

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

Комментарии (0)

© Habrahabr.ru