Всем привет! Меня зовут Дмитрий Булгаков, я Android-разработчик в HiFi-стриминге Звук, и я продолжаю рассказывать вам о том, как можно создать аудиоплеер в приложении. Во второй части гайда мы поговорим об использовании ExoPlayer.

Добро пожаловать на разбор!

Использование ExoPlayer

Data Source

Итак, начнем! 

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

Для загрузки потоковых данных (или стрима) вы можете использовать популярный в Android проектах сетевой клиент OkHttp. В целом, выбор сетевого клиента больше зависит от вашего проекта, чем от потребностей ExoPlayer: при использовании общего клиента для всех сетевых запросов в приложении вам будет проще контролировать распределение сетевых ресурсов между различными частями приложения. Например, так можно делать запросы аудиоконтента более приоритетными, чтобы пользователь скорее услышал свои любимые треки. 

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

Примерно так выглядит основной интерфейс DataSource в плеере. 

public interface DataSource {

    long open(DataSpec dataSpec) throws IOException;

    int read(byte[] buffer, int offset, int length) throws IOException;

    void close() throws IOException;

    void addTransferListener(TransferListener transferListener);

    @Nullable
    Uri getUri();

}

Мы используем простой интерфейс, решающий несколько задач:

  • Умение открывать поток данных по некоторому DataSpec, где определены параметры для запроса данных из сети или файлов. Вы можете сделать запрос и получить InputStream, который будет сохранён в DataSource для последующего чтения. 

  • Навык чтения данных. Для этого DataSource может использовать заранее сохранённый InputStream и просто получать байты из него.

  • Умение закрываться при завершении воспроизведения, чтобы освободить используемые ресурсы. Здесь сохранённый InputStream можно закрыть и забыть.

  • При воспроизведении потоковых данных DataSource может оповещать слушателей, заданных с помощью addTransferListener, о прогрессе чтения данных. Например, это позволит плееру измерять количество данных полученных в единицу времени и выполнять измерение пропускной способности сети - полезно для воспроизведения адаптивного контента.

  • Возможность переопределения метода getUri для внутренней реализации плеера. Тут вы можете просто вернуть Uri (Uniform Resource Identifier), который использовался в методе open.

У нас в Звуке используется несколько собственных технологий хранения данных: кастомное кеширование, скачивание и шифрование аудиоданных. Поэтому мы работаем с кастомной реализацией интерфейса DataSource. Но в простых случаях вам будет достаточно дефолтных реализаций из ExoPlayer — CacheDataSource, DefaultHttpDataSource и других, подходящих для различных ситуаций.

Так выглядит фабрика, создающая DataSource для загрузки данных из сети.

val dataSourceFactory = DefaultHttpDataSource.Factory()

val customDataSourceFactory = DataSource.Factory {
    YourCustomDataSource()
}

Extractor

Любой используемый вами DataSource загружает данные в виде какого-либо формата. 

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

Простейшая иллюстрация работы экстрактора в ExoPlayer.
Простейшая иллюстрация работы экстрактора в ExoPlayer.

Мы знаем, что экстракторы умеют извлекать данные из форматов. Клиент отправляет данные в экстрактор и получает данные на выходе.

Для воспроизведения файлов формата mp3 в ExoPlayer используется Mp3Extractor, а для работы с flac используется FlacExtractor (название экстракторов соответствует формату).

По умолчанию ExoPlayer использует DefaultExtractorsFactory, поддерживающий различные форматы медиа. Это и MP4, и MP3, и FLAC, и даже MIDI и JPEG. Нам такое обилие экстракторов может не понадобиться, поэтому для экономии памяти и ресурсов просто создадим свою фабрику экстракторов, которая будет поддерживать только нужные нам форматы.

val extractorsFactory = ExtractorsFactory {
    arrayOf(
        Mp3Extractor(),
        FlacExtractor()
    )
}

Audio Render

Давайте поговорим об использовании рендеров. Аудиоданные в форматах хранятся не в сыром виде, а в закодированном. Для воспроизведения необходимо их декодировать, а для этого в ExoPlayer и существуют рендеры. 

В самых первых версиях Android система не поддерживала алгоритмы для кодирования и декодирования медиаданных. Зачастую разработчики приложения должны были сами заботиться об интеграции этих алгоритмов, а это означало работу с NDK и увеличивало сложность разработки плеера во много раз. 

Ситуация изменилась с выходом Android 4.3 (API 18). В системе наконец-то появился MediaCodec, который позволял упростить разработку плеера, предоставляя интерфейс для работы с некоторыми кодеками. 

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

Раньше система не поддерживала все кодеки, но их поддержка активно развивается до сих пор, давая разработчику возможность интеграции самых продвинутых технологий. Например, в Android 9 появилась поддержка многоканального кодека AAC (xHE-AAC), позволившая работать сразу с 8-ю каналами вместо обычного стерео или моно. 

На данный момент практически все необходимые для работы кодеки нам доступны. К примеру, MediaCodec. Этот кодек применяется в ExoPlayer для создания рендера (его полное название MediaCodecAudioRender). Мы можем использовать его в первоначальном виде, либо передать в него кастомные слушатели для получения информации о прогрессе работы. 

В простейшем случае ExoPlayer для создания рендера использует DefaultRenderersFactory, который создаёт MediaCodecAudioRender «под капотом». Если нашему приложению необходима поддержка дополнительных кодеков, мы можем сделать кастомную фабрику, создающую нужные нам рендеры.

val renderersFactory = RenderersFactory { 
    eventHandler, _, rendererListener, _, _ ->
    arrayOf(
        MediaCodecAudioRenderer(
            context,
            MediaCodecSelector.DEFAULT,
            eventHandler,
            rendererListener
        ),
        LibflacAudioRenderer(
            eventHandler,
            rendererListener
        )
    )
}

Итак, теперь у нас есть DataSource, есть Extractor и AudioRender. А как нам использовать всё это для воспроизведения контента?

Сборка ExoPlayer

Мы подошли к ответственному моменту — сборке самого плеера. На этом этапе нужно просто соединить созданные ранее компоненты воедино. Для создания плеера нам понадобится использовать один из билдеров ExoPlayer. 

val exoPlayerBuilder = ExoPlayer.Builder(context)

Простейший из них использует лишь один аргумент — Context. Это знакомый каждому Android-разработчику контекст ресурсов и состояния приложения или активности.

Следующим шагом нам нужно задать renderersFactory. Объекты dataSourceFactory и extractorsFactory соединяются с помощью DefaultMediaSourceFactory и передаются в билдер. Благодаря этому плеер может создать MediaSource, из которого он и будет воспроизводить данные. Дальше нужно только передать билдеру все компоненты.

exoPlayerBuilder.setRenderersFactory(renderersFactory)

val mediaSourceFactory = DefaultMediaSourceFactory(
    dataSourceFactory, extractorsFactory
)

exoPlayerBuilder.setMediaSourceFactory(mediaSourceFactory)

val exoPlayer = exoPlayerBuilder.build()

И наш плеер готов к воспроизведению!

Теперь давайте создадим MediaItem для воспроизведения контента из сети. Подготовим плеер к воспроизведению заданного контента, вызвав prepare. ExoPlayer, благодаря использованию заданного ему DefaultMediaSourceFactory, создаст MediaSource. 

val mediaItem = MediaItem.fromUri("https://some.musicstreaming.com/play")

exoPlayer.setMediaItem(mediaItem)

exoPlayer.prepare()

exoPlayer.play()

Готовый плеер начнёт воспроизведение после вызова play. И всё, можно уже радовать пользователя воспроизведением контента!

Вот мы и разобрались с основными принципами использования ExoPlayer. 

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

Если у вас появились вопросы по содержимому статьи — задавайте их в комментариях! С радостью на все отвечу. 

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


  1. ganzmavag
    03.06.2024 08:41

    Интересна тема аудиовыходов в Android. В PowerAMP их штук пять на выбор, интересно какие между ними реальные различия.