В современных веб-приложениях для потокового видео всё чаще требуется не просто воспроизводить контент, но и анализировать аудиодорожку в реальном времени. Например, строить индикаторы уровня громкости (VU/PPM метры), визуализировать спектрограммы или детектировать тишину. В этой статье разберём, как корректно объединить hls.js и Web Audio API для анализа аудио из HLS-потока в браузере, избежав типичных подводных камней.
Проблематика
При работе с Web Audio API и HTML-медиа-элементами разработчики часто сталкиваются с тремя основными проблемами:
1. Ошибка повторного создания MediaElementAudioSourceNode
Самая распространённая ошибка выглядит так:
Cannot create multiple MediaElementAudioSourceNode from the same HTMLMediaElement
Это происходит потому, что браузер позволяет создать только один MediaElementAudioSourceNode для каждого <video> или <audio> элемента. Попытка создать второй приведёт к ошибке, при этом оригинальный элемент может потерять звук.
2. Политика автовоспроизведения
Современные браузеры блокируют автоматический запуск аудио до первого пользовательского взаимодействия. AudioContext создаётся в состоянии suspended, и разработчику необходимо явно вызвать resume() после жеста пользователя (клик, тап).
3. Синхронизация с загрузкой HLS
HLS-манифест загружается асинхронно через hls.js. Если создать MediaElementAudioSourceNode до того, как манифест полностью загружен и подключён к видеоэлементу, можно получить пустой звук или ошибки инициализации.

Решение: Singleton-паттерн для управления аудиоконтекстом
Чтобы гарантировать единственность AudioContext и корректную работу с MediaElementAudioSourceNode, используем паттерн Singleton. Это обеспечит:
Единственный экземпляр
AudioContextна всё приложениеЕдинственный
MediaElementAudioSourceNodeдля каждого медиаэлементаЦентрализованное управление состоянием аудиографа
Простоту переиспользования в разных компонентах приложения
Базовая реализация
export class HlsAudioService {
private static instance: HlsAudioService;
public audioContext: AudioContext;
public source?: MediaElementAudioSourceNode;
public splitter?: ChannelSplitterNode;
public analysers: AnalyserNode[] = [];
private constructor() {
const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext;
this.audioContext = new AudioContextClass();
}
static getInstance(): HlsAudioService {
if (!HlsAudioService.instance) {
HlsAudioService.instance = new HlsAudioService();
}
return HlsAudioService.instance;
}
async resumeContext(): Promise<void> {
if (this.audioContext.state === 'suspended') {
await this.audioContext.resume();
}
}
}
Здесь мы создаём приватный конструктор, который инициализирует AudioContext с учётом кросс-браузерной совместимости (используем префикс webkit для старых версий Safari). Статический метод getInstance() гарантирует, что во всём приложении будет только один экземпляр сервиса.
Интеграция с hls.js
Ключевой момент — правильная последовательность инициализации. Необходимо дождаться события MANIFEST_PARSED от hls.js перед созданием аудионод:
const videoElement = document.querySelector('video') as HTMLVideoElement;
const hls = new Hls();
const audioService = HlsAudioService.getInstance();
hls.on(Hls.Events.MANIFEST_PARSED, () => {
// Манифест загружен, можно создавать аудиограф
if (!audioService.source) {
audioService.source = audioService.audioContext.createMediaElementSource(videoElement);
setupAudioGraph(audioService);
}
});
hls.loadSource('https://example.com/stream.m3u8');
hls.attachMedia(videoElement);
// Обработка пользовательского жеста
videoElement.addEventListener('play', async () => {
await audioService.resumeContext();
});
Такой подход гарантирует, что MediaElementAudioSourceNode создаётся только после того, как hls.js полностью инициализировал поток, и только один раз.
Построение аудиографа для анализа
После создания source node необходимо построить цепочку обработки. Для стереоанализа типичная архитектура выглядит так:
function setupAudioGraph(service: HlsAudioService) {
const { audioContext, source } = service;
// Разделяем стереосигнал на левый и правый каналы
service.splitter = audioContext.createChannelSplitter(2);
// Создаём анализаторы для каждого канала
const analyserLeft = audioContext.createAnalyser();
const analyserRight = audioContext.createAnalyser();
analyserLeft.fftSize = 2048; // Размер FFT для частотного анализа
analyserRight.fftSize = 2048;
// Строим цепочку: source → splitter → analysers → destination
source!.connect(service.splitter);
service.splitter.connect(analyserLeft, 0); // Левый канал
service.splitter.connect(analyserRight, 1); // Правый канал
// Подключаем к выходу для воспроизведения
analyserLeft.connect(audioContext.destination);
analyserRight.connect(audioContext.destination);
service.analysers = [analyserLeft, analyserRight];
}
ChannelSplitterNode разделяет стереосигнал на моноканалы. Каждый AnalyserNode предоставляет данные о частотах и амплитуде для своего канала в реальном времени. Параметр fftSize определяет детализацию частотного анализа — чем больше значение, тем выше разрешение, но больше нагрузка на процессор.
Обработка типичных edge cases
CORS и кросс-доменные потоки
Если HLS-поток находится на другом домене, MediaElementAudioSourceNode будет выдавать нулевые значения из соображений безопасности. Решение — добавить атрибут crossOrigin к видеоэлементу и убедиться, что сервер отдаёт заголовок Access-Control-Allow-Origin:
videoElement.crossOrigin = 'anonymous';
Без корректной настройки CORS анализ аудио невозможен, хотя воспроизведение будет работать.
Состояние suspended на мобильных устройствах
На мобильных платформах (особенно iOS) AudioContext может переходить в состояние suspended для экономии батареи. Необходимо проверять состояние перед каждым использованием:
async function ensureAudioContextRunning(service: HlsAudioService) {
if (service.audioContext.state === 'suspended') {
await service.audioContext.resume();
console.log('AudioContext resumed');
}
}
Рекомендуется вызывать эту функцию в обработчиках событий play и canplay.
Переключение между потоками
При смене источника HLS (например, переключение качества или канала) не нужно пересоздавать MediaElementAudioSourceNode. Достаточно вызвать hls.loadSource() с новым URL — существующий аудиограф продолжит работать:
function switchStream(newUrl: string) {
hls.loadSource(newUrl);
// source node остаётся прежним, пересоздание не требуется
}
Попытка пересоздать source приведёт к ошибке, так как он уже привязан к элементу.
Оптимизация производительности
Для снижения нагрузки на CPU при визуализации можно использовать несколько техник:
Уменьшение частоты обновления: вместо обновления на каждом кадре (60 FPS) можно ограничить частоту до 30 FPS с помощью requestAnimationFrame и счётчика кадров.
Настройка размера FFT: для простых индикаторов уровня достаточно fftSize = 256, для детальной спектрограммы — 2048 или 4096. Меньшее значение снижает задержку и нагрузку.
Web Workers: можно вынести обработку данных из AnalyserNode в воркер, чтобы не блокировать основной поток. Однако сами аудионоды должны оставаться в основном потоке.
Примеры применения
С помощью полученных данных можно реализовать различные виды визуализации и анализа:
PPM/VU метры — для отображения текущего уровня сигнала с разным временем интеграции
Спектрограммы — визуализация частотного спектра в реальном времени с помощью
getByteFrequencyData()Детектор тишины — анализ амплитуды для определения пауз в аудио
Эквалайзеры — разделение частот на полосы для управления громкостью
Все эти задачи используют данные из AnalyserNode, который предоставляет массив значений от 0 до 255 для каждой частотной полосы.
Совместимость браузеров
Браузер |
Web Audio API |
HLS (нативная) |
HLS.js (MSE) |
|---|---|---|---|
Chrome (Desktop) |
✅ v14+ |
❌ Нет |
✅ Да |
Firefox (Desktop) |
✅ v25+ |
❌ Нет |
✅ Да |
Safari (Desktop) |
✅ v6.1+ |
✅ Да |
⚠️ Не требуется |
Edge |
✅ Полная |
❌ Нет |
✅ Да |
Chrome Mobile |
✅ Полная |
✅ Да (Android) |
✅ Да |
Safari iOS |
✅ v6+ |
✅ Да |
❌ Нет (MSE) |
Firefox Android |
✅ Полная |
❌ Нет |
✅ Да |
Важно: Safari на iOS не поддерживает Media Source Extensions, поэтому hls.js там не работает. К счастью, iOS имеет нативную поддержку HLS, и можно напрямую использовать video.src. Web Audio API при этом работает корректно.
Справочник распространённых ошибок
Ошибка |
Причина |
Решение |
|---|---|---|
Cannot create multiple MediaElementAudioSourceNode |
Попытка создать MediaElementAudioSourceNode дважды для одного элемента |
Использовать Singleton-паттерн для хранения единственной ссылки |
AudioContext was not allowed to start |
AudioContext создан до пользовательского взаимодействия |
Вызвать audioContext.resume() после user gesture (click, touch) |
MediaElementAudioSourceNode outputs zeroes |
CORS-ограничения для кросс-доменного аудио |
Добавить crossOrigin="anonymous" к video элементу |
HLS manifest not loading |
HLS-манифест загружается до инициализации AudioContext |
Дождаться события MANIFEST_PARSED перед созданием source node |
Заключение
Анализ аудио из HLS-потоков в браузере — решаемая задача при правильном подходе. Ключевые моменты:
Singleton для AudioContext предотвращает ошибки повторного создания нод и упрощает управление состоянием
Ожидание события MANIFEST_PARSED от hls.js гарантирует корректную инициализацию
Обработка autoplay policy через
resume()обеспечивает работу на всех платформахНастройка CORS необходима для кросс-доменных потоков
Полный рабочий пример реализации доступен в репозитории github.com/ABurov30/AudioContext, где можно изучить готовую интеграцию всех описанных техник.
Такой подход позволяет создавать надёжные веб-приложения для потокового видео с продвинутым аудиоанализом, не требуя от пользователей установки дополнительных плагинов или расширений.