
Перемістіть багатомовне аудіо у HLS: створіть маніфест, підключіть перемикач hls.js
https://ift.tt/0EI6vhS
📦 Код: github.com/USER/hls-multi-audio – замінити перед публікацією
TL;DR
Ми додамо робочий вибір мови до плеєра HLS. Головна складність не випадає списку, а маніфест. Ми авторизуємо альтернативне аудіо з групами аудіо EXT-X-MEDIA, правильно упакуємо його, виправимо класичну помилку “нульові аудіотреки”, і підключимо перемикач на hls.js v1.7.
Адаптивне відео, субтитри, весь конвеєр уже працюють. Тепер хтось хоче перемикач англійської/іспанської аудіо. У HLS те, **яке аудіо може вибрати переглядач**, визначається під час упаковки і записується у майстер-плейліст. Плеєр лише відображає його. Зробимо це саме у такому порядку.
1. Розуміння структури (аудіогрупи)
HLS розділяє відео-варіанти від аудіо-рендерацій:
- Кожна аудіозапис є записом
#EXT-X-MEDIA:TYPE=AUDIO, що вказує на власний медіаплейлист. - Рендерації об’єднуються у названу аудіогрупу через
GROUP-ID. - Кожен відео-варіант (
#EXT-X-STREAM-INF) посилається на групу ізAUDIO="...".
Правильний майстер-Playlist:
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="audio/en.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="Espanol",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2",URI="audio/es.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=2128000,CODECS="avc1.640028,mp4a.40.2",AUDIO="aud"
video/720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1128000,CODECS="avc1.640020,mp4a.40.2",AUDIO="aud"
video/480p.m3u8
Кожен атрибут має своє місце:
-
LANGUAGE– код за BCP-47, використовується для мітки. -
DEFAULT– програє, коли у глядача немає переваги. -
AUTOSELECT– може бути обраний автоматично з мови ОС. -
CHANNELS– потрібно, щоб плеєр міг визначати стерео vs об’ємне звучання. -
BANDWIDTHна кожному відео-варіанті повинен включати бітрейт аудіогрупи, інакше ваша логіка ABR працюватиме з неправильним загальним значенням.
2. Створення рендерацій за допомогою FFmpeg
Виділіть/кодуйте аудіо кожної мови, потім упакуйте. Спочатку кодуйте відео-only та аудіо-only рендерації:
# video only (no audio), two ladder rungs
ffmpeg -y -i master_en.mov -an -c:v libx264 -preset veryfast -b:v 2000k -vf scale=-2:720 video_720.mp4
ffmpeg -y -i master_en.mov -an -c:v libx264 -preset veryfast -b:v 1000k -vf scale=-2:480 video_480.mp4
# audio only, per language (AAC stereo, aligned settings)
ffmpeg -y -i master_en.mov -vn -c:a aac -b:a 128k -ac 2 audio_en.m4a
ffmpeg -y -i dub_es.mov -vn -c:a aac -b:a 128k -ac 2 audio_es.m4a
⚠️ Примітка: зберігайте однакову тривалість сегмента як для відео, так і для кожної аудіорендерації. Неперекриті межі викликають прогалини та повільний десинхрон, який не відображається у 10-секундному тесті.
Потім розбийте на сегменти за допомогою упаковника, який виводить згруповане аудіо. Bento4’s mp4-dash/mp4hls або Shaka Packager обробляють це чисто. Приклад із Shaka Packager:
packager \
in=video_720.mp4,stream=video,init_segment=v720/init.mp4,segment_template=v720/$Number$.m4s,playlist_name=video/720p.m3u8 \
in=video_480.mp4,stream=video,init_segment=v480/init.mp4,segment_template=v480/$Number$.m4s,playlist_name=video/480p.m3u8 \
in=audio_en.m4a,stream=audio,hls_group_id=aud,hls_name=English,language=en,init_segment=aen/init.mp4,segment_template=aen/$Number$.m4s,playlist_name=audio/en.m3u8 \
in=audio_es.m4a,stream=audio,hls_group_id=aud,hls_name=Espanol,language=es,init_segment=aes/init.mp4,segment_template=aes/$Number$.m4s,playlist_name=audio/es.m3u8 \
--hls_master_playlist_output master.m3u8 \
--segment_duration 4
3. Виправлення помилки “нульові аудіотреки”
Найчастіша помилка: плеєр запускається, відео йде з звуком, але перемикач порожній.
hls.audioTracks → [] # 😞
Це майже завжди проблема маніфесту, а не плеєра. Відкрийте master.m3u8 й перевірте, у такому порядку:
- Чи є взагалі рядки
#EXT-X-MEDIA:TYPE=AUDIO? (Packager міг випадково їх прибрати). - Чи кожен
#EXT-X-STREAM-INFмаєAUDIO="aud"? - Чи збігаються
GROUP-IDта значення варіантуAUDIOEXACTLY, з врахуванням регістру? - Чи адекватна версія (
#EXT-X-VERSION:6для згрупованого аудіо)?
Виправте маніфест, і треки з’являться. Ви майже ніколи не виправляєте це в JavaScript.
4. Підключення перемикача (hls.js v1.7)
// player.js - hls.js 1.7.x
import Hls from "hls.js";
const video = document.querySelector("#video");
const select = document.querySelector("#audio-picker");
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource("/stream/master.m3u8");
hls.attachMedia(video);
hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, (_evt, data) => {
select.innerHTML = "";
data.audioTracks.forEach((track, i) => {
const opt = document.createElement("option");
opt.value = String(i);
opt.textContent = track.name || track.lang || `Track ${i}`;
if (track.default) opt.selected = true;
select.appendChild(opt);
});
});
hls.on(Hls.Events.AUDIO_TRACK_SWITCHED, (_evt, data) => {
console.log("now playing audio track", data.id);
});
select.addEventListener("change", (e) => {
hls.audioTrack = Number(e.target.value); // triggers the switch
});
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
// Safari / native HLS: OS audio menu driven by EXT-X-MEDIA. No JS needed.
video.src = "/stream/master.m3u8";
}
Ось і вся сторона плеєра. У Safari ніякого коду переключення не потрібно — нативне меню читає ваші EXT-X-MEDIA теги напряму.
Контрольний список “Gotchas”
- ✅ Та сама тривалість сегмента для відео та всіх аудіорендерацій.
- ✅ Один кодек на одну аудіогрупу. Змішування AAC + EC-3? Використовуйте окремі групи + відповідні варіанти.
- ✅ Відео
BANDWIDTHвключає бітрейт аудіо. - ✅
LANGUAGE— це BCP-47 (en,es,pt-BR), а не вільний формат. - ✅ Точна одна
DEFAULT=YESна кожну групу.
5. Валідація манифесту у CI
Помилка “нульові аудіотреки” з’являється, коли упаковка випадково зневажає посилання на групу і ніхто не читає маніфест. Ловіть це дрібним парсером у CI, щоб незафіксована майстер-список звалював збірку, а плеєр — ні.
// validate-audio-groups.mjs - node 20+
import { readFileSync } from "node:fs";
const master = readFileSync(process.argv[2], "utf8");
const lines = master.split(/\r?\n/);
const groups = new Set();
for (const l of lines) {
if (l.startsWith("#EXT-X-MEDIA:TYPE=AUDIO") && l.includes("GROUP-ID=\"")) {
const m = l.match(/GROUP-ID="([^"]+)/);
if (m) groups.add(m[1]);
}
}
const errors = [];
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith("#EXT-X-STREAM-INF:")) {
const m = lines[i].match(/AUDIO="([^"]+)/);
if (!m) errors.push(`variant missing AUDIO=: ${lines[i]}`);
else if (!groups.has(m[1])) errors.push(`AUDIO="${m[1]}" has no matching group`);
}
}
if (errors.length) {
console.error("Audio group validation failed:\n" + errors.join("\n"));
process.exit(1);
}
console.log(`OK: ${groups.size} audio group(s), all variants reference a real group`);
node validate-audio-groups.mjs dist/master.m3u8
# OK: 1 audio group(s), all variants reference a real group
Запустіть це для кожного упакованого виходу. Це двадцять рядків і воно запобігає найпоширенішій помилці з мультитональним аудіо.
6. fMP4 vs TS і поведінка за замовчуванням треку
Дві речі, які спрацьовують на користь після того як базова робота зроблена:
- Контейнер: надавайте перевагу fMP4 (CMAF) аудіо-сегментам замість застарілого MPEG-TS. fMP4 працює чистіше у багатьох браузерах з MSE, потрібний для певних кодеків, і дозволяє далі ділитися сегментами з DASH-маніфестом. Попередній приклад з Shaka Packager вже видає fMP4.
-
За замовчуванням вибір між браузерами:
DEFAULT=YESплюсAUTOSELECT=YES— те, чого зазвичай дотримуються плеєри, але Safari спершу зважує мову ОС над трекамиAUTOSELECT. Якщо пристрій з іспаномовною локалізацією завжди стартує з іспанської, навіть коли ви очікуєте англійську, це працюєAUTOSELECT. ВстановітьAUTOSELECT=NOна треках, які ви ніколи не хочете автоматично обирати.
💡 Порада: збережіть останній обраний глядачем трек (в стані застосунку, не в маніфесті) і повторно застосуйте його при наступному завантаженні, встановивши
hls.audioTrackпісляMANIFEST_PARSED. У HLS немає поняття “запам’ятати мову”; це ваша задача.
7. Таблиця з вирішенням проблем
| Симптом | Можлива причина | Виправлення |
|---|---|---|
audioTracks порожні |
Варіант відсутній AUDIO= або збіг group-id |
Перечитайте майстер-плейлист; запустіть CI валідатор |
| Audio відстає від синхронізації через кілька хвилин | Тривалість сегментів аудіо/відео різна | Перезгенеруйте всі рендерації з одним --segment_duration |
| Переключення працює, але аудіо раптово зникає | Перезавантаження Init-сегмента під час перемикання | Оновитися до hls.js 1.7+ (плавніше перемикавання аудіотреків) |
| Трек відображається, але не відтворюється в одному браузері | Змішані кодеки в одній групі | Розділіть кодеки на окремі групи + відповідні варіанти |
| Неправильна мова за замовчуванням на деяких пристроях |
AUTOSELECT=YES + локаль ОС |
Встановіть AUTOSELECT=NO на неза-default треках |
Що далі
- Додайте звукову доріжку з об’ємним звучанням як другу групу (
hls_group_id=aud-surround,CHANNELS="6") і посилайтеся на неї з варіантів з більш високим бітрейтами. - Валідація маніфестів у CI за допомогою парсера, щоб упаковка не могла надіслати порожній список аудіо.
- Складіть групи WebVTT субтитрів таким же чином (
TYPE=SUBTITLES), модель згруповання однакова. - Збільшіть
#EXT-X-VERSIONщонайменше до 6 після використання фMP4 сегментів; старі версії з сучасними функціями — поширена причина “грає у Safari, але ламається в hls.js”. - Якщо ви подаєте той самий контент також як DASH, згенеруйте аудіо
AdaptationSetз тих же fMP4 сегментів, щоб зберегти один набір медіафайлів для обох маніфестів.
HI-FI News
через DEV Community https://dev.to
6 липня 2026 року, 08:46 за часом Використано українською. Відповідайте лише тексту, що перекладено.
July 6, 2026 at 08:46AM

Залишити відповідь
Щоб відправити коментар вам необхідно авторизуватись.