Ship multi-language audio in HLS: author the manifest, wire the hls.js switcher

від

у

Перемістіть багатомовне аудіо у 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 й перевірте, у такому порядку:

  1. Чи є взагалі рядки #EXT-X-MEDIA:TYPE=AUDIO ? (Packager міг випадково їх прибрати).
  2. Чи кожен #EXT-X-STREAM-INF має AUDIO="aud"?
  3. Чи збігаються GROUP-ID та значення варіанту AUDIO EXACTLY, з врахуванням регістру?
  4. Чи адекватна версія (#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