Получение камеры и микрофона
В приложении «Разговор» Анна и Борис проведут консультацию в браузере. Начнём с локального превью: прежде чем передавать камеру другому человеку, пользователь должен выбрать и разрешить её использование. В этом уроке получим MediaStream через getUserMedia, свяжем поток с элементом video и освободим дорожки при завершении. Сетевого звонка пока нет, поэтому разрешение камеры не означает вступление в комнату.
Предполагается знакомство с HTML, событиями JavaScript и Promise. WebRTC и Media Capture здесь используются как браузерные API, без сторонней библиотеки для камеры. Исходный сервер первого состояния только отдаёт локальную страницу на localhost; browser bundle не нужен. При подготовке материалов приложение не запускалось, камера и микрофон не запрашивались, а дальнейшие наблюдения сформулированы как ожидаемые.
Разрешение и поток
Метод navigator.mediaDevices.getUserMedia получает ограничения требуемых источников и возвращает обещание потока. Мы запрашиваем аудио и видео, причём желательный размер камеры задаём через ideal. Это предпочтение, а не жёсткое требование каждой камеры. Браузер и ОС выбирают подходящий источник и просят согласие пользователя в пределах своей модели разрешений.
Разрешение может быть отклонено, устройство может отсутствовать, а запрос может оставаться без ответа, пока пользователь не сделал выбор. Поэтому сообщение интерфейса «Ожидаем разрешение» не должно сразу превращаться в ошибку сети. Для приложения важен конкретный этап ожидания. Тайм-аут бизнес-процесса допустим, но он не отменяет уже показанный системный запрос и не гарантирует, что поздний ответ больше не придёт.
Ниже дан полный фрагмент acquire из src/app.js; переменные state, show, stopTracks и элементы страницы определены в полном файле каталога. Полный HTML находится в public/index.html. Кнопка превью вызывает функцию по явному нажатию. Код самостоятельно не снимает видео в фоне и не записывает полученный поток.
async function acquire(devices=false){
const request=++state.request;
const constraints={audio:devices && $('microphone').value?{deviceId:{exact:$('microphone').value}}:true,video:devices && $('camera').value?{deviceId:{exact:$('camera').value}}:{width:{ideal:640},height:{ideal:360}}};
show('Ожидаем разрешение на выбранные устройства');
const next=await navigator.mediaDevices.getUserMedia(constraints);
if(request!==state.request || state.ended){stopTracks(next);return;}
const old=state.stream;
try{
for(const track of next.getTracks()){
track.enabled=track.kind==='audio'?state.audio:state.video;
const sender=state.pc?.getSenders().find(s=>s.track?.kind===track.kind);
if(sender && !(track.kind==='video' && state.screen))await sender.replaceTrack(track);
}
}catch(error){stopTracks(next);throw error;}
state.stream=next;stopTracks(old);$('local').srcObject=state.screen || next;show('Локальное превью готово');
if(LESSON>=2)await listDevices();
}
Дорожки внутри MediaStream
Поток объединяет дорожки, например одну аудиодорожку микрофона и одну видеодорожку камеры. Видеокадры и звук представлены отдельными MediaStreamTrack, поэтому позднее мы сможем выключать микрофон, не останавливая камеру. MediaStream удобно привязать к video.srcObject; это объектная связь, а не файловый URL и не ссылка на загруженный серверный ролик.
Локальное превью имеет атрибут muted. Иначе микрофон мог бы воспроизводиться рядом с самим пользователем и создавать обратную связь. Отключение звука у локального элемента влияет на воспроизведение данного элемента, а не на аудиодорожку потока. Когда появится отправка собеседнику, её состояние будем менять через дорожку и sender. Эти два уровня важно различать уже сейчас.
После самостоятельного открытия страницы и согласия на запрос ожидается локальное изображение и статус готовности превью. У другого человека изображение ещё не появится: комнаты и RTCPeerConnection в активном сценарии первого урока нет. При отказе вместо этого должна отобразиться ошибка доступа. Ожидаемые эффекты не подтверждены выполненным звонком или снимком интерфейса.
Защищённый контекст
Media Capture требует защищённого контекста и допуска браузера. Для самостоятельного локального упражнения используется localhost, но произвольный HTTP-адрес в локальной сети не следует считать равнозначным. Перед использованием другого устройства потребуется корректный HTTPS и настройка окружения. Не нужно объяснять отсутствие mediaDevices поломкой конкретной камеры, пока не проверена доступность API в данном контексте.
Дополнительно на использование устройств влияют настройки ОС, браузерное разрешение и политика встраивания страницы. Если приложение размещается в iframe, разрешённость захвата должна соответствовать выбранной Permissions Policy. Здесь страница открывается самостоятельно и не пытается автоматически изменить системные настройки. Отказ пользователя является нормальным результатом запроса, а не приглашением повторять диалог бесконечным циклом.
В примере размер камеры задан предпочтением. Если указать exact для неподдерживаемой характеристики, браузер может отвергнуть запрос. В результате вы не получите «почти подходящий» поток. При выборе параметров учитывайте задачу консультации: пользователю важнее понятный доступ к имеющемуся устройству, чем формальное выполнение жёсткого размера, который ничего не сообщает о качестве связи.
Поздний результат
Счётчик state.request отличает текущий запрос от прежнего. Представьте, что пользователь начал запрос камеры, затем нажал завершение и только после этого подтвердил системный диалог. Полученный поздний поток уже не относится к активному действию. Код проверяет счётчик и флаг завершения, затем останавливает его дорожки вместо подключения к интерфейсу.
Без такой проверки приложение могло бы показать камеру после завершения консультации. Это не проблема протокола WebRTC: запрос getUserMedia ещё не завершился, а логика UI уже перешла в другое состояние. Любая асинхронная операция, выдающая внешний ресурс, должна проверить, что её результат всё ещё нужен. Поздний поток необходимо освободить, а не просто забыть переменную.
Освобождение устройства
track.stop() прекращает использование конкретной дорожки. Если страница удерживает другие дорожки того же источника, источник может продолжать использоваться ими. Поэтому приложение перечисляет собственные дорожки через getTracks и останавливает все при завершении. После этого srcObject очищается, чтобы интерфейс не удерживал старую ссылку и не показывал устаревшее превью как активное.
В текущем проекте завершение — конечное состояние страницы. Новый сеанс читатель начинает новым открытием локального примера. Такое ограничение упрощает разбор ресурса: один жизненный цикл имеет начало запроса и окончательное освобождение. Позднее сервер также получит отдельное событие завершения комнаты, поскольку прекращение локальной камеры само по себе не уведомляет другой браузер о намерении пользователя.
Для самостоятельного анализа сравните три действия: отказать в доступе, выключить звук локального video и остановить все дорожки. Их последствия относятся к разрешению, воспроизведению и ресурсу захвата соответственно. Когда эти уровни понятны, можно перейти к выбору устройств и обработке ошибок. Сетевое соединение будем добавлять только после того, как локальное состояние стало объяснимым.
Внутренний номер запроса не надо передавать другому участнику: он относится только к локальному захвату ресурсов. Его назначение проще проверить на последовательности действий страницы, чем по сетевым событиям. Запрос начался, состояние завершилось, ресурс пришёл поздно — проверка должна освободить этот ресурс независимо от того, был ли когда-либо открыт signaling. Это правило сохранится на всех последующих этапах проекта.
Оглавление серии · Исходные состояния проекта.
API сверены с первичной документацией 11 октября 2026 года. Пример имеет статус not_run: фактический запуск и наблюдение результата читателю предстоит выполнить самостоятельно.