Что можно узнать при разработке аудио плеера для разных браузеров

Эта история началась примерно 1.5 года назад. Связана она с проигрыванием музыки в различных браузерах и платформах, на которых они запускаются. Путь полный «боли и страдания» осознания того, что легкая на первый взгляд задача может оказаться не такой уж и легкой, а «незначительные» детали, которым не придаешь значения в самом начале, могут повлиять на всё.

Незначительные детали для самых любопытных :)

1. Подгрузка данных о каждом следующем треке из сети.
2. На каждый элемент аудио: new Audio () или 


Предыстория


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

Вот и я во время работы над новым MVP столкнулся с различными особенностями в отношении проигрывания аудио в браузерах.

А началось всё с того, что нужно было сделать плавное сведение (crossfade) двух треков при воспроизведении — это первая особенность. Наша команда хотела сделать смену треков как на радио. И вторая особенность — каждый последующий трек запрашивается из сети.

7ho64hihtgz3atuttk1vm0mucpq.png

Изыскания


Тогда почти во всех наших проектах использовалась библиотека Sound Manager 2.

Практически сразу понимаешь, что воспроизведение одновременно двух аудио файлов на мобильных устройствах не везде одинаково работает!

В Chrome (~62 версия) для ПК треки воспроизводились как надо. На мобильных устройствах (тоже в Chrome) воспроизведение треков работало, но только при активном экране. Когда экран блокировался, следующий трек за текущим играющим не воспроизводился. Что касается iOS / macOS — воспроизведение работало аналогичным образом. Больше информации можно получить тут — раздел «Единичный аудиопоток».

Так начались хождения за три моря поиски информации по крупицам об особенностях работы браузеров с аудио.

Хорошо, пробую решение с Web Audio без использования каких-либо библиотек. Да, эта технология предназначена для иных целей: синтез, обработка звука, для игр и т.д., нежели простое воспроизведение треков. Но ради эксперимента нужно было попробовать, так как она позволяет компоновать звуки из разных источников на один один звуковой выход — колонки/наушники/динамик телефона/и т.п. Есть ребята, которые целенаправленно занимаются исследованием возможностями воспроизведения звука на мобильных устройствах с использованием Web Audio API.

После реализации выяснились определенные нюансы.

Во-первых, необходимо дожидаться полной загрузки всего трека. При медленном соединении с интернетом будут заметны паузы из-за того, что второй трек может не успеть загрузиться к моменту окончания первого трека. Полной загрузки можно избежать, если использовать связку с HTML5 Audio тегами, которые будут выступать в качестве источников звука для Web Audio, но в этом случае снова становится невозможным воспроизведение двух звуков одновременно.

Во-вторых, если загружать трек по сети фрагментами и декодировать их программно, то это увеличивает нагрузку на CPU. Для ПК было приемлемо, а вот для мобильных устройств критично.

В-третьих, возникли проблемы с декодированием. Если на клиент приходили фрагменты mp3/ogg/wav файлов, то эти кусочки спокойно декодировались и воспроизводились. Но если в браузер приходили чанки mp4 файла, который выступал контейнером для HE-AAC, то их тогда декодировать не удалось. Это в некоторой степени касается и браузера Opera, в котором от версии к версии нестабильно работает воспроизведение MP3 файлов — то воспроизводит, то выдает ошибку, что данный формат не поддерживается.

В-четвертых, не отображалось / не менялось название трека на заблокированном экране на плашке с нативным аудио плеером (на iPad), в т.ч. при переключении между треками. Возможно из-за того, что для тестов использовался iPad с 9 версией iOS — другого на тот момент не было.

В итоге, на данном этапе от Web Audio пришлось отказаться. Всё-таки crossfade не для браузеров, стандартные музыкальные композиций в хорошем качестве достаточно много весят.

Раз от crossfade отказываемся, то реализуем простой fade in и fade out, в начале и в конце музыкального трека соответственно.

Код на позапрошлом шаге был немного доработан и протестирован. В результате тестов всплыли различные нюансы (показаны в таблице). Все это с использованием библиотеки Sound Manager 2.

b5bjwgh2mds3pn1x8ghbp3nuyue.png

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

z_nr0g8j0zxskeevs2e3loddlok.png

Активация вкладки

В браузере Safari 9+ звук при активации вкладки не всегда появляется.


Из этого можно предположить, что выполнение JS в фоне подвергается throttling«у или поток выполнения полностью останавливается (события и таймеры). Однако, позже станет понятно, что это был отчасти корректный вывод. Ниже будет рассмотрен еще 1 нюанс, связанный с воспроизведением треков и осознанием почему звук не появляется.

Ремарка

Для работы с прогрессом (progressbar), например, его отрисовкой для трека, хорошо использовать requestAnimationFrame вместо setInterval/setTimeout. Можно избежать накопительного эффекта при деактивации (background tab) и последующей активации вкладки и временного ее подвисания, связанного с выполнением всех вычислений и перерисовок состояния прогресса.


В этот же момент возник вопрос:, а как же быть с автовоспроизведением треков на ПК и на мобильных устройствах?

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

Что касается Safari в отношении автоматического воспроизведения при загрузке страницы, то это невозможно, нужно взаимодействие пользователя со страницей, как и на мобильных устройствах. Это касается как видеоконтента, так и аудиоконтента.

И так, на тот момент было следующее:

  1. нельзя (не желательно) воспроизводить два и более звуков одновременно;
  2. для псевдо «автовоспроизведения» трека необходимо разрешение пользователя — первое взаимодействие, позже это было названо «Продать пальчик устройству»;
  3. в фоне (background tab / lock screen) JS (все зависит от браузера):
    либо замирает полностью;
    либо подвергается throttling«у;
    либо работает также, как и при активной вкладке;
  4. можно автоматически стартовать воспроизведение без звука, но непонятно зачем (для аудио контента)?
  5. где-то далеко начинает маячить мысль, а как сделать так, чтобы JS в фоне продолжал выполняться?


В дело пошли другие библиотеки реализующие функции плеера с предположением, что возможно там есть решение для этой задачи. Несмотря на то, что было просмотрено множество issues на GitHub с описание проблем при воспроизведении треков в различных браузерах, все же была надежда на то, что вот-вот доберешься до сути: почему не работает и как сделать, чтобы работало. Как оказалось, нет…

Несколько примеров кода с видео демонстрацией работы библиотек:

  1. Sound Manager 2 — github pages, github репозиторий, видео: macOS Safari 12; iOS Safari 10 при разблокированном экране
  2. Howler
    Howler v2.0.9 — github pages, github репозиторий, видео: macOS Safari 12, iOS Safari 10
    Howler v2.0.15 — github pages, github репозиторий, видео: macOS Safari 12
    Howler v2.1.1 — github pages, github репозиторий, видео: macOS Safari 12, iOS Safari 10


Для macOS запись видео сделана без звука, поэтому нужно смотреть на индикатор громкости — изображение динамика, на вкладке.

В репозитории доступно больше видео примеров.

В интерактивном примере для Howler v2.1.1 — иногда можно услышать несколько звуков одновременно, это связано с добавлением пула разблокированных пользователем audio элементов (в будущих версиях библиотеки это должны исправить).

В чем причина неработоспособности этих библиотек?

Выше я писал: «В фоне (background tab) JS либо замирает полностью, либо подвергается throttling«у». Так вот тут всплывает другой момент: библиотеки в коде используют создание новых аудио объектов через new Audio (). Если они создаются динамически, т.е. не используется уже существующий аудио объект, и при этом пользователь никак не взаимодействует с сайтом, неактивна вкладка или заблокирован экран, то некоторые браузеры могут посчитать, что воспроизводить звук от этого аудио элемента не следует, пока вкладка не будет снова активна или пользователь не совершит какое-либо действие.

Пример теста на github pages и в репозитории на github с использованием new Audio (). Видео: macOS Safari 12; iOS Safari 10 с разблокированным экраном.

Похоже, что какого-то универсального инструмента не существует и нужно искать какое-то другое компромиссное решение.

Дальше садимся с ребятами из команды обсудить, а что действительно важно в работе аудио плеера? Ибо продолжать эксперименты можно было бы до бесконечности, но нужно двигаться вперед.

Сначала были выявлены важные моменты, которые мешали достичь желаемого результата:

  1. в браузере Safari на macOS не воспроизводятся треки при неактивной вкладке;
  2. отсутствует возможность слушать музыку в фоне (при заблокированном экране) на смартфонах, работающих на iOS и Android, хотелось бы избежать агрессивного перенаправления пользователей в мобильное приложение (в дальнейшем), так как предыдущий опыт показывает, что довольно большая часть пользователей не желает ставить мобильное приложение;
  3. плеер некорректно работает с динамическим плейлистом, т.е. когда заранее не известно, какой будет следующий трек.


Далее это позволило сформулировать цели, которые было необходимо достичь:

  1. обеспечить работу плеера в фоновом режиме — в различных браузерах и на различных платформах;
  2. позволить пользователю самому выбирать чем пользоваться: прослушивать музыку на сайте или в мобильном приложении;
  3. обеспечить возможность использовать плеер (или подход) в различных будущих проектах.


Начался новый этап поиска решения поставленной задачи. На этом этапе уже не использовались различные библиотеки, все исследования велись с использование HTML5 Audio. Итогом стало то, что был найден вариант с использованием dedicated workers. iOS это решение победить опять не позволило — воспроизведение в фоне не работает, зато получилось добиться работоспособности в Android (Chrome, Opera, Safari).

Пример теста HTML5 Audio + Dedicated Workers на github pages и в репозитории на github.

При инициализации Worker’а запрашиваются данные о текущем треке. Worker также занимается отправкой сигнала на получением состояния прогресса — сколько времени трек играет — из основного потока и на основе этих данных решает когда запросить данные о следующем треке из сети.

lnt0shcslejecuvcenkh-rlv28y.png

Также в то время был протестирован следующий пример (github pages, репозиторий на github), когда HTML5 audio тег встраивается в DOM (видео: macOS Safari 12, iOS Safari 10) и у него просто подменяется SRC при переключении между треками. На сегодняшний день на macOS в 12 Safari этот пример работает. К сожалению, сейчас нет возможности проверить работоспособность этого примера на macOS в Safari 10 и 11 версии, но на тот момент при проведении тестов этот пример не работал (autoplay policies, autoplay restrictions).

Если подытожить, то для iOS и macOS браузер Safari не считает новый экземпляр аудио элемента активированным пользователем, если он был создан в фоновом режиме внутри какого-либо события, например, ajax, setTimeout, onended.

Далее, что касается воспроизведения треков в iOS Safari и iOS Chrome, была найдена возможность воспроизводить треки в фоновом режиме (при заблокированном экране) только с использованием HLS. Для платформ iOS и macOS этот формат является стандартом и вещание поддерживается операционной системой. Для Android Chrome и Edge также доступна нативная реализация. А для ПК в Chrome — можно использовать программные обработчики, например, hls.js, Bitmovin Player и т.д.

По ссылке на github репозиторий доступен пример кода, который охватывает самый простой вариант использования — простое проигрывание генерируемого на сервере потока воспроизведения без возможности перемотки, переключения на следующий трек и т.д. Представлены примеры с использованием: тега audio, тега video, библиотеки hls.js, и плеера от Bitmovin. Для запуска требуется Node.js.

Выводы


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

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

Третий момент, обязательно обращайте внимание на аудиторию вашего сервиса — с каких браузеров и операционных систем приходят ваши пользователи. Это довольно легко отследить с помощью различных метрик и систем мониторинга ошибок. Такой подход позволит понять какие платформы и браузеры поддерживать важно, а на поддержку каких можно не тратить силы.

И напоследок


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

Описание можно увидеть по ссылке на github.

© Habrahabr.ru