В современных веб-приложениях для потокового видео всё чаще требуется не просто воспроизводить контент, но и анализировать аудиодорожку в реальном времени. Например, строить индикаторы уровня громкости (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, где можно изучить готовую интеграцию всех описанных техник.
Такой подход позволяет создавать надёжные веб-приложения для потокового видео с продвинутым аудиоанализом, не требуя от пользователей установки дополнительных плагинов или расширений.
