Перейти к содержанию

Демонстрация экрана и возврат камеры

При консультации Анне может понадобиться показать Борису выбранное окно с материалом. Для этого не требуется создавать независимую вторую встречу: можно заменить отправляемую video track у существующего sender. В этом уроке добавим getDisplayMedia, явный выбор экрана и возврат камеры. Пользовательский выбор источника останется браузерным действием, которое приложение не может незаметно сохранить как вечное разрешение.

Состояние lesson-09 продолжает кнопки mute и добавляет «Показать экран». Запрашивается только видео экрана; системное аудио в данном проекте не захватывается. Микрофон продолжает использовать прежнюю отдельную дорожку. Такое ограничение помогает понять замену источника и не обещает одинаковую поддержку захвата звука вкладки на всех браузерах и ОС.

Явное действие пользователя

getDisplayMedia вызывается по нажатию кнопки. Браузер показывает выбор доступной поверхности, а человек решает, какое окно или экран предоставить. Приложение не должно автоматически запускать диалог во время reconnect, открытия страницы или получения чужого signaling-сообщения. Для запроса также важны защищённый контекст и требования пользовательской активации, описанные браузерным API.

Не считайте прошлое согласие на камеру согласием на экран. Это отдельный ресурс с другими последствиями: на нём могут находиться личные уведомления и материалы других приложений. Системный диалог выбирает источник, а интерфейс должен ясно показывать текущую демонстрацию и дать понятное завершение. В учебной странице локальное превью меняется на screen stream, чтобы пользователь видел выбранную поверхность.

Ниже дан полный shareScreen из src/app.js. Переменная state.pc существует только после разрешённой готовности пары; видеопередатчик уже добавлен в предыдущих уроках. После получения потока проверяется поколение, поскольку человек мог завершить консультацию, пока выбирал окно. Поздний поток в такой ситуации останавливается.

async function shareScreen(){
 if(!state.pc || state.screen)throw new Error('Нужно активное соединение без текущей демонстрации');
 const generation=state.generation;
 const screen=await navigator.mediaDevices.getDisplayMedia({video:true,audio:false});
 if(generation!==state.generation || state.ended){stopTracks(screen);return;}
 const sender=state.pc.getSenders().find(s=>s.track?.kind==='video'),track=screen.getVideoTracks()[0];
 track.enabled=state.video;
 try{await sender.replaceTrack(track);}catch(error){stopTracks(screen);throw error;}
 state.screen=screen;$('local').srcObject=screen;track.addEventListener('ended',()=>restoreCamera().catch(report),{once:true});
}

Замена дорожки

replaceTrack меняет источник у существующего RTCRtpSender. В нашем проекте найден единственный video sender. При подходящем новом источнике операция может пройти без полного нового offer/answer. Но она является асинхронной и способна отказать, например если замена требует параметров за пределами согласованного режима. Поэтому код ожидает результат и при отказе освобождает полученную screen track.

Нельзя сначала присвоить state.screen, объявить успешную демонстрацию, а затем проигнорировать rejection. Тогда UI показывал бы экран как отправляемый, хотя sender мог остаться на камере. В примере состояние активной демонстрации меняется после успешной замены. Для выпуска важно также показывать отказ пользовательским сообщением и сохранять возможность продолжить обычную консультацию.

После самостоятельного выбора поверхности и успешного replaceTrack удалённый собеседник должен получать кадры выбранного экрана через существующее видеонаправление. При отмене диалога камера должна оставаться рабочим источником. Эти ветви ожидаются по API; экран при авторстве не захватывался, сетевой сеанс не создавался. В статье нет изображения окна, выдаваемого за подтверждённую демонстрацию.

Завершение системной кнопкой

Пользователь может остановить демонстрацию средствами браузера или ОС. Это проявляется завершением screen track, а не обязательно нажатием нашей кнопки. Поэтому на дорожку добавлен обработчик ended, который вызывает restoreCamera. Приложение должно учитывать оба пути завершения и не оставлять удалённый sender с давно закончившимся источником без понятного возврата.

Функция восстановления сначала снимает активную ссылку на screen stream, затем пытается заменить video sender прежней camera track. В блоке finally она останавливает screen tracks и возвращает локальное превью камеры. Если peer уже закрыт или старой камеры больше нет, полноценный возврат может не состояться. Ошибка требует сообщения и нового явного действия, а не выдуманной надписи «Камера восстановлена».

Повторное восстановление должно быть безопасным. Во время очистки stop и позднего системного события функция может получить уже освобождённое состояние. Поэтому ссылка фиксируется локально, а active state очищается. Для более сложного интерфейса уместна единая state machine операции замены, чтобы несколько параллельных запросов не управляли sender независимо. Здесь демонстрация допускается только одна одновременно.

Камера во время экрана

Мы сохраняем camera stream, чтобы быстро вернуться к нему. Это означает, что камера может продолжать оставаться захваченной, хотя sender передаёт экран. Такое поведение должно быть известно пользователю. Если продукт предпочитает полностью отпускать камеру на время демонстрации, возвращение потребует нового getUserMedia и обработки разрешения; нельзя одновременно обещать освобождение и мгновенный возврат без нового ресурса.

Состояние video mute применяется к screen track. Если пользователь ранее выключил видео, демонстрация не должна самопроизвольно включить отправку кадров. При дальнейшем переключении video кнопка меняет как camera, так и screen track. Это сохраняет единое намерение UI, но физические источники остаются разными объектами, каждый из которых потребуется остановить при окончательном завершении.

Возвращение после обрыва

Новая пара после reconnect создаёт новый peer. Если демонстрация ещё активна, контроллер должен явно решить, какой video source добавить новому sender. Иначе локальное превью показывало бы экран, а собеседник получал бы камеру. В финальных состояниях применяется текущая активная screen track; она не переполучается скрытым повторным системным запросом. При прекращении исходной дорожки новый экран выбирается только пользователем.

Политика передачи после обрыва зависит от продукта. Некоторым консультациям нужен явный вопрос «Продолжить показ», другим достаточно сохранённого активного источника. Оба варианта требуют понятного состояния и не означают нового вечного разрешения браузера. Учебный код демонстрирует сохранение имеющегося ресурса, а не автоматическое получение любого экрана без выбора.

Для самостоятельного анализа сравните отмену выбора, отказ replaceTrack, остановку системной кнопкой и окончательное завершение комнаты. В каждом случае принадлежащие приложению дорожки должны получить определённый жизненный цикл. Результат урока — управляемая замена видеоисточника с явным выбором и возвратом, которая не смешивает захват экрана, переговоры и конечный уход пользователя.

При возврате камеры сохраняется и её прежнее пользовательское значение enabled. Если человек отключил видео во время демонстрации, восстановление источника не должно трактоваться как разрешение снова показывать изображение. Эта небольшая деталь объясняет, зачем контроллер хранит намерение отдельно от активного stream. Переход источника меняет объект дорожки, а решение включать или отключать отправку остаётся действием пользователя.

Оглавление серии · Исходные состояния проекта.

API сверены с первичной документацией 11 октября 2026 года. Пример имеет статус not_run: фактический запуск и наблюдение результата читателю предстоит выполнить самостоятельно.