Первопричина: LocalVideoControlButton/LocalAudioControlButton на mounted() подписываются на useDevices и держат подписку весь звонок; initializeDevices безусловно грабит камеру (getUserMedia video:true) для превью, хотя кнопкам нужен лишь список устройств. Камера отпускалась только по завершении звонка → индикатор горел при выключенном видео (медиа-конвейер звонка свой трек останавливает корректно, но превью-подсистема — нет). Фикс: разделены подписка на СПИСОК устройств и на ЖИВОЙ превью-поток. streamSubscribersCount + единая точка контроля в updateAudio/VideoStream (без превью-подписчика поток не грабится и останавливается — защищает и от watch). Кнопки звонка: requestStream:false. Диалог настроек — без изменений (превью работает). enableDeviceEvents использует enumerateDevices (без камеры). Сборка OK. НЕ деплоено — ждёт тест-фазу (docs/f7-fixes/camera-led-on-with-video-off.md).
4.2 KiB
Фикс: лампочка камеры горит при выключенном видео в звонке
Ветка: fix/camera-led-preview · Статус: реализовано, СБОРКА OK, ждёт тест-фазу (не деплоено).
Симптом
Пользователь входит в конференцию с выключенной камерой (или выключает её). Своего видео в звонке нет — но индикатор камеры ноутбука ГОРИТ, будто камера используется. Приватностный баг.
Первопричина (по исходникам, высокая уверенность)
Два независимых слоя работы с камерой:
- Медиа-конвейер звонка (
utils/media/pipeline/MediaDevicesSource.js) — при выключении видео вызываетsetVideoAllowed(false)→track.stop(). Камеру отпускает КОРРЕКТНО. - Превью-подсистема (
composables/useDevices.js) — используется диалогом настроек И контрол-кнопками звонка.initializeDevices()БЕЗУСЛОВНО грабит камеру:updateVideoStream()→getUserMedia({video:true}). Отпускает только когда отписались ВСЕ.
Виновник — контрол-кнопки: LocalVideoControlButton.vue и LocalAudioControlButton.vue на
mounted() зовут subscribeToDevices() и держат подписку ВЕСЬ звонок (beforeUnmount = конец
звонка). Первый же подписчик поднимает initializeDevices() → камера включается и горит всё время,
хотя кнопкам нужен лишь СПИСОК устройств для меню выбора, а не живой поток. У кнопок нет <video>
превью — поток не показывается, просто удерживает устройство.
Проверено: MediaDevicesManager.enableDeviceEvents() берёт список через enumerateDevices() —
камеру НЕ трогает. Значит список для меню доступен без превью-потока.
Фикс
Разделили в useDevices два вида подписки:
- список устройств (нужен всем, камеру не трогает);
- живой превью-поток (нужен только диалогу настроек, где есть
<video>).
Изменения:
useDevices.js: новый счётчикstreamSubscribersCount;subscribeToDevices({requestStream})/unsubscribeFromDevices({requestStream}); ЕДИНАЯ точка контроля —updateAudioStream()/updateVideoStream()приstreamSubscribersCount===0не грабят и останавливают поток (защищает и отwatch(videoInputId));initializeDevicesподнимает потоки только при наличии превью-подписчика.LocalVideoControlButton.vue,LocalAudioControlButton.vue:subscribeToDevices({requestStream:false}).MediaSettings.vue: без изменений — по умолчаниюrequestStream:true(превью в диалоге работает).
Что проверить в тест-фазе
- Вход в звонок с выключенной камерой → лампочка НЕ горит; видео нет. (главный кейс)
- Диалог настроек мультимедиа → превью камеры/микрофона работает; при закрытии камера гаснет.
- Включение видео в звонке → камера работает, видео идёт; выключение → лампочка гаснет.
- Смена камеры/микрофона в меню кнопки во время звонка.
- Виртуальный фон в превью настроек.
- Несколько вкладок/повторные входы — нет «залипшего» потока.