
Як ми додали системний звук до запису екрана
https://ift.tt/rvlJ64y
У нас вже була запис екрана з накладенням відеокамери, інструментами малювання та паузою/продовженням. Але коли хтось робив огляд продукту з фоновою музикою або навчальне відео з звуками інтерфейсу, запис виходив беззвучним. Браузер захоплював екран, але не звук, який з нього йшов.
Виправлення виявилося дивно простим — два обмеження в одному виклику API. Але зрозуміти, чому ці обмеження існують і як вони взаємодіють з різними браузерами, вимагало більше зусиль, ніж написання коду.
Проблема
API getDisplayMedia() дозволяє веб-додаткам захоплювати екран користувача. Можна запитати звук разом із відео:
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true,
});
Це працює — так би мовити. Прапорець audio: true говорить браузеру, що вам потрібен звук, але браузер вирішує, що саме це означає. У Chrome в діалозі вибору екрану з’являється прапорець «Also share tab audio». Якщо користувач ділиться вкладкою, він отримує аудіо вкладки. Якщо ділиться всім екраном чи вікном, може не бути жодного аудіо.
Справжня проблема полягала в тому, що Chrome за замовчуванням не забезпечує захоплення системного аудіо. Без явного сигналу ви отримуєте звуком з вкладки і ніякого системного аудіо.
Обмеження
Chrome 105+ підтримує два обмеження, які змінюють поведінку:
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true,
systemAudio: "include",
suppressLocalAudioPlayback: true,
});
systemAudio: "include" підказує Chrome пропонувати захоплення системного аудіо у діалозі вибору під час спільного використання екрану, а не лише аудіо вкладки. Коли користувач ділиться всім екраном, Chrome тепер може захоплювати звук з будь-якого застосунку — не лише з вкладок браузера.
suppressLocalAudioPlayback: true вирішує проблему з ехом. Якщо його не встановити, під час запису вкладки, що відтворює звук, користувач чує звук локально і цей звук потрапляє до запису. Після відтворення ви чули б аудіо двічі — з запису та з відлуння, захопленого мікрофоном. Заглушення локального відтворення зменшує вихід аудіо з джерела, щоб воно існувало лише у записі.
Реалізація
Повна зміна складалася з трьох частин: змінної стану, змінених обмежень та кнопки перемикача.
Стан за замовчуванням дорівнює true, оскільки більшість користувачів, які записують екран, хочуть аудіо:
const [systemAudioEnabled, setSystemAudioEnabled] = useState(true);
Виклик getDisplayMedia будує свої опції залежно від цього стану:
const displayMediaOptions: DisplayMediaStreamOptions
& Record<string, unknown> = {
video: true,
audio: systemAudioEnabled,
};
if (systemAudioEnabled) {
displayMediaOptions.systemAudio = "include";
displayMediaOptions.suppressLocalAudioPlayback = true;
}
const screenStream = await navigator.mediaDevices
.getDisplayMedia(displayMediaOptions);
Тип & Record<string, unknown> є практичною вибіркою. Вбудований у TypeScript DisplayMediaStreamOptions ще не включає systemAudio або suppressLocalAudioPlayback — це розширення Chrome, яке ще не було додане до стандартних DOM-типів. Замість створення окремого файлу декларації для двох властивостей ми використовуємо перетин типів, який дозволяє задавати довільні ключі.
Коли аудіо вимикають, ми передаємо audio: false і повністю пропускаємо додаткові обмеження. Браузер не запросить аудіо, і запис буде лише відео.
Сумісність з браузерами
Ось що відбувається в кожному браузері:
| Браузер | systemAudio |
suppressLocalAudioPlayback |
Результат |
|---|---|---|---|
| Chrome 105+ | Підтримується | Підтримується | Захоплення системного + вкладкового аудіо, локальне відтворення придушено |
| Edge 105+ | Підтримується | Підтримується | Так само, як Chrome (на базі Chromium) |
| Firefox | Ігнорується | Ігнорується | Аудіо вкладки може працювати в деяких конфігураціях, системного аудіо немає |
| Safari | Ігнорується | Ігнорується | Аудіо з getDisplayMedia не захоплюється |
Ключове розуміння: браузери мовчки ігнорують невідомі обмеження в getDisplayMedia(). Вони не кидають помилки або не відхиляють проміс. Firefox і Safari просто пропускають systemAudio та suppressLocalAudioPlayback і діють так, ніби їх не було. Це означає, що ми можемо безпечно передавати ці обмеження на всіх браузерах без перевірки підтримки.
Жодних try/catch, жодного if (typeof systemAudio !== 'undefined'), жодного визначення user-agent. API задуманий саме так — невідомі обмеження — це позасіт.
Без змін на бекенді
Наш серверний відеопайплайн уже правильно обробляв аудіо. Команда ffmpeg для композитингу записів екрану + веб-камери використовує:
ffmpeg -i screen.mp4 -i webcam.webm \
-map "0:a?" \
...
Суфікс ? в 0:a? означає «включити аудіодоріжку з входу 0, якщо вона існує; інакше пропустити». Якщо аудіо є — воно включається. Якщо його немає — ffmpeg не помиляється — буде лише відео. Ця схема уже була використана під час композитингу веб-камери, тому системне аудіо проходить через конвеєр без змін.
Етап перекодування також обробляє аудіо прозоро. Виходи MP4 отримують AAC-аудіо. Виходи WebM — копіюють вихідний кодек. Якщо немає аудіодоріжки — обидва шляхи створюють тільки відео.
Перемикач
Ми додали кнопку «Audio On/Off» до інтерфейсу рекордера в режимі очікування, між перемикачем Камера та кнопкою «Почати запис»:
[Camera Off] [Audio On] [Start Recording]
Вона слідує точно такому ж стилю оформлення, як і перемикач Камера — акцентований фон, коли увімкнено, прозорий із рамкою — коли вимкнено. Перемикач з’являється лише у стані очікування та зникає під час запису, підрахунку часу та паузи, оскільки саме ці стани відповідають за відображення Idle UI.
Коли аудіо увімкнене, кнопка читає «Audio On» з акцентованим фоном. Вимкнено — «Audio Off» з прозорим фоном і рамкою. aria-label перемикача змінюється між «Disable system audio» та «Enable system audio» для доступності.
Що з мікрофоном?
Системне аудіо з getDisplayMedia та аудіо мікрофона з getUserMedia — це окремі потоки. Ми навмисно їх не змішуємо. Якщо хтось хоче озвучення над записом екрана, вони використовують накладення веб-камери — воно записує окремий аудіотрек і потім компонується на сервері.
Змішувати два аудіо потоки в браузері вимагатиме Web Audio API AudioContext з MediaStreamAudioDestinationNode, щоб об’єднати джерела. Це значна складність заради мінімальної користі, особливо коли серверна компоновка вже обробляє кілька доріжок чисто.
Зауваження щодо macOS
На macOS Chrome може захоплювати аудіо вкладок, але не системного аудіо з інших застосунків без додаткових дозволів ОС. Обмеження systemAudio: "include" є підказкою браузеру — воно каже діалогу спільного використання пропонувати параметри аудіо. Якщо ОС не підтримує захоплення системного аудіо, браузер просто цього не пропонує. Запис все одно вдається; просто не буде аудіо з неперебраних джерел.
У Windows та Linux системне аудіо зазвичай захоплюється без додаткових налаштувань.
Спробуйте
SendRec — це з відкритим кодом (AGPL-3.0) та самостійно хостований. Запишіть екран з системним аудіо на app.sendrec.eu. Заберіть образ із Docker Hub, перегляньте або керівництво з самостійного розміщення у Self-Hosting Guide, або перегляньте код.
February 24, 2026 at 09:28PM

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