Игра Яндекс Практикума
Игра Яндекс Практикума
Игра Яндекс Практикума

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

Отредактировано

18К открытий18К показов

Рассказывает Николай Коломийцев, технический директор и 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-страницы:

			On Air: Rockabilly Radio (136 connections)
     
        Artist: Mack Stevens
        Track: In The Groove (143)
		

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

			// С помощью JSOUP получаем все содержание страницы выше
Document doc = Jsoup.connect(Const.TRACK_INFO_URL).get();
// Так как счет начинается с 0, мы получаем второй  элемент:
// как видите, это имя исполнителя
после чего обрезаем первые 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(" Track: ");
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/0f38adb5ecc1be3f.png,
"size" : small
},
-{
"#text" :
https://lastfm-img2.akamaized.net/i/u/64s/0f38adb5ec8911be3f.png,
"size" : medium
},
-{
"#text" :
https://lastfm-img2.akamaized.net/i/u/174s/0f38adb911be3f.png,
"size" : large
},
-{
"#text" :
https://lastfm-img2.akamaized.net/i/u/300x300/0f38d818911be3f.png,
"size" : extralarge
},
-{
"#text" :
https://lastfm-img2.akamaized.net/i/u/0f38adb5ec1be3f.png,
"size" : mega
},
-{
"#text" :
https://lastfm-img2.akamaized.net/i/u/arQ/0f38a818911be3f.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)) {
   // Здесь можно изменить изображение на кнопке,
   // проиграть звук, остановить сервис и многое другое
}
		

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

Онлайн радио для Android: пошаговое руководство 1
Онлайн радио для Android: пошаговое руководство 2
Онлайн радио для Android: пошаговое руководство 3
Следите за новыми постами
Следите за новыми постами по любимым темам
18К открытий18К показов