Онлайн радио для Android: пошаговое руководство

Рассказывает Николай Коломийцев, технический директор и Android-разработчик LevelTop.org 


Привет, типичные! В этом руководстве расскажу вам о том, как создать свое приложение в Android.

Начну сразу с сути, поэтому определимся с функционалом:

  1. Проигрывание потокового аудио с помощью ExoPlayer.
  2. Парсинг HTML страницы.
  3. Интеграция API Last.fm.
  4. Подключение сервиса для управления проигрыванием из “шторки”.
  5. Работа с кастомными библиотеками.

С требованиями разобрались, теперь самое сложное интересное — реализация.

Весь код вы можете найти на GitHub, здесь же я уделю внимание только основным моментам.

Думаю, что SDK у вас установлено и новые проекты вы создавать умеете, поэтому создадим пустой (blank) проект и добавим библиотеки в build.gradle:

// Animations library
compile 'com.wang.avi:library:2.1.3'
// Connectivity library
compile 'org.jsoup:jsoup:1.8.3'
// Library for load and show images
compile 'com.squareup.picasso:picasso:2.5.2'
// Library for playing audio
compile 'com.google.android.exoplayer:exoplayer:r1.5.3'

Теперь коротко пройдемся по классам:

  1. Player — класс для инициализации и управления нашим ExoPlayer.
  2. NotificationService — класс для проигрывания аудио в фоне и отображения уведомления в шторке.
  3. Const — класс для описания ссылок на аудио и прочего.
  4. CircularSeekBar — класс, который я позаимствовал на GitHub, добавляет нам изогнутый SeekBar.
  5. GetTrackInfo — здесь мы обращаемся к Last.fm, а также парсим HTML страницу.
  6. MainActivity — главный класс приложения, выполняющий функции отрисовки экрана и инициализации методов.

Также добавим пару layout-файлов для шторки и главного экрана, drawables можно найти здесь:

И добавим нашему Manifest несколько разрешений и служб:

<!--Permisions for internet connection and vibrate function-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" />

<!--Service for player and notification-->
<service android:name=".NotificationService" />

Теперь давайте получим ключ Last.fm API, он нужен нам для того, чтобы по имени исполнителя найти его фотографию и показать ее на главном экране. Для этого нужно перейти на страницу создания аккаунта и войти или зарегистрироваться, после чего вам потребуется создать приложение. Эта операция займет 30 секунд, и мы наконец доберемся до API KEY, его вам нужно ввести в поле LAST_FM_KEY класса Const.java:

public static String LAST_FM_KEY = "YOUR_API_KEY";

Далее предлагаю перейти к способу получения ссылки на прямую трансляцию, ее я беру отсюда. Для этого нам нужно запустить радио и, нажав правой кнопкой мыши в Chrome, выбрать пункт “посмотреть код”, после чего выбрать вкладку Network и найти ссылку с самым длинным timeline. Это и будет наш стрим, он уже добавлен в класс Const — аналогичным способом я получил ссылку на HTML-страницу с именем исполнителя и названием трека. В этом коде много костылей, так как парсить HTML — это само по себе странное занятие, но все же я постараюсь его объяснить:

Здесь вы можете видеть получаемый мною нужный фрагмент HTML-страницы:

<div class="boxed"
     <p><strong>On Air: Rockabilly Radio (136 connections)</strong></p>
     <p>
        <strong>Artist:</strong> Mack Stevens<br/>
        <strong>Track:</strong> In The Groove (143)<br/>
     </p>
</div>

А это его парсинг, надеюсь, что комментарии будут информативны:

// С помощью JSOUP получаем все содержание страницы выше
Document doc = Jsoup.connect(Const.TRACK_INFO_URL).get();
// Так как счет начинается с 0, мы получаем второй <p> элемент: как видите, это имя исполнителя
после чего обрезаем первые 29 символов оставляя начало сразу с его имени.
String first_letter = doc.getElementsByClass("boxed").select("p").get(1).toString().substring(29, doc.getElementsByClass("boxed").select("p").get(1).toString().length());

/* 
После этого с помощью функции split получаем массив из 2-х элементов c текстом до введенной
в виде аргумента функции строки и текстом после, первый элемент массива это "чистое" имя исполнителя,
а второй еще нужно "очистить" от ненужного "br" и "p" в конце
*/
String[] parts = first_letter.split("<br> <strong>Track:</strong> ");
String first = parts[0];
MainActivity.artist = first;
String second = parts[1];
// Здесь мы удаляем лишние символы путем простого обрезания строки
MainActivity.track = second.substring(0, second.length() - 9);

// После чего отправляем запрос Last.fm для получения информации об исполнителе и выводим ее на экран
readJsonFromUrl("http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist=" + MainActivity.artist.replace(" ", "%20") + "&api_key="+Const.LAST_FM_KEY+"&format=json".replace(" ", "%20"));

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

Далее парсинг JSON и получение фото пользователя.

Вот пример отправляемого Last.fm JSON-ответа для певицы Adele:

{
"artist" : -{
"name" : Adele,
"mbid" : cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493,
"url" : https://www.last.fm/music/Adele,
"image" : -[
-{
"#text" : https://lastfm-img2.akamaized.net/i/u/34s/0f38adb5ecc8ac0a0e26cd818911be3f.png,
"size" : small
},
-{
"#text" : https://lastfm-img2.akamaized.net/i/u/64s/0f38adb5ecc8ac0a0e26cd818911be3f.png,
"size" : medium
},
-{
"#text" : https://lastfm-img2.akamaized.net/i/u/174s/0f38adb5ecc8ac0a0e26cd818911be3f.png,
"size" : large
},
-{
"#text" : https://lastfm-img2.akamaized.net/i/u/300x300/0f38adb5ecc8ac0a0e26cd818911be3f.png,
"size" : extralarge
},
-{
"#text" : https://lastfm-img2.akamaized.net/i/u/0f38adb5ecc8ac0a0e26cd818911be3f.png,
"size" : mega
},
-{
"#text" : https://lastfm-img2.akamaized.net/i/u/arQ/0f38adb5ecc8ac0a0e26cd818911be3f.png,
"size" : 
}

Все, что нас с вами здесь интересует — это изображение размера “mega”, парсить мы его будем таким образом:

// Создаем JSONObject и передаем ему на вход наш ответ от Last.fm
   JSONObject dataJsonObj = new JSONObject(sb.toString());
// После чего проверяем, не ошибка ли пришла
   if (!isError(dataJsonObj)) {
// Если все хорошо, парсим 5й элемент массива, а именно фото размера "mega"
      if (dataJsonObj.optJSONObject("artist").optJSONArray("image").optJSONObject(4).optString("#text").toString().length() > 10) {
         MainActivity.album = dataJsonObj.optJSONObject("artist").optJSONArray("image").optJSONObject(4).optString("#text");
      } else MainActivity.album = "";
   }

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

static ExoPlayer exoPlayer;
static TrackRenderer audioRenderer;

    public static void start(String URL, Context context)
    {
        // Объявляем URI со ссылкой на наш стрим, либо любой аудио файл в сети
        Uri URI = Uri.parse(URL);
        FrameworkSampleSource sampleSource = new FrameworkSampleSource(context,URI, null);
        audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, null, true);
        // Инициализируем плеер
        exoPlayer = ExoPlayer.Factory.newInstance(1);
        exoPlayer.prepare(audioRenderer);
        // Говорим ему начинать проигрывание аудио, как только будет окончена буферизация
        exoPlayer.setPlayWhenReady(true);
        // Добавляем listener для того, чтобы знать когда он начал играть
        exoPlayer.addListener(new ExoPlayer.Listener() {
            @Override
            public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
                // playbackState 4 означает готовность и начало проигрывания, здесь можно убрать диалог загрузки
                if(playbackState == 4)
                {
                    MainActivity.playing_animation.setVisibility(View.VISIBLE);
                    MainActivity.loading_animation.setVisibility(View.GONE);
                    MainActivity.control_button.setVisibility(View.VISIBLE);
                    MainActivity.control_button.setImageResource(R.drawable.pause);
                }
            }

            @Override
            public void onPlayWhenReadyCommitted() {

            }

            @Override
            public void onPlayerError(ExoPlaybackException error) {

            }
        });
    }
    // Функция для остановки проигрывания, после которой нужно будет вызвать start()
    public static void stop()
    {
        if(exoPlayer!=null) {
            exoPlayer.stop();
        }
    }
    
    // Установка громкости также присутствует и принимает значения от 0.0 до 1.0
    public static void setVolume(float volume)
    {
        if(exoPlayer!= null) {
            exoPlayer.sendMessage(audioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, volume);
        }
    }

Финальный этап урока — это уведомление в шторке, сервис и то, каким образом это работает.

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

// При нажатии на кнопку проигрывания вызывается эта функция, и мы запускаем сервис, описанный ранее в Manifest
public void startPlayerService() {
    Intent serviceIntent = new Intent(MainActivity.this, NotificationService.class);
    serviceIntent.setAction(Const.ACTION.STARTFOREGROUND_ACTION);
    startService(serviceIntent);
}

Сервис управляется с помощью Intent. Когда пользователь нажимает на кнопку “play / pause” в шторке, сервис отправляет Intent сам себе и обрабатывает его, так мы отправляем Intent при нажатии:

views.setOnClickPendingIntent(R.id.buttonID, pplayIntent);
PendingIntent pplayIntent = PendingIntent.getService(this, 0,
        playIntent, 0);
Intent playIntent = new Intent(this, NotificationService.class);
playIntent.setAction(Const.ACTION.PLAY_ACTION);

А так обрабатываем в onStartCommand:

if (intent.getAction().equals(Const.ACTION.PLAY_ACTION)) {
   // Здесь можно изменить изображение на кнопке, проиграть звук, остановить сервис и многое другое
}

Поздравляю! Мы разработали клиент-серверное приложение с использованием дополнительных библиотек, воспроизведением аудио, а также работой с сервисами. В конечном итоге получится нечто подобное тому, что вы видите на скриншотах:

kolomiytsev_3 kolomiytsev_2 kolomiytsev_1