Files
f7_talk/docs/f7-fixes/camera-led-on-with-video-off.md
T
glb_adm 557d2f0864 fix(камера): не держать превью-камеру весь звонок (лампочка горела при выкл. видео)
Первопричина: 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).
2026-07-08 08:20:19 +00:00

4.2 KiB

Фикс: лампочка камеры горит при выключенном видео в звонке

Ветка: fix/camera-led-preview · Статус: реализовано, СБОРКА OK, ждёт тест-фазу (не деплоено).

Симптом

Пользователь входит в конференцию с выключенной камерой (или выключает её). Своего видео в звонке нет — но индикатор камеры ноутбука ГОРИТ, будто камера используется. Приватностный баг.

Первопричина (по исходникам, высокая уверенность)

Два независимых слоя работы с камерой:

  1. Медиа-конвейер звонка (utils/media/pipeline/MediaDevicesSource.js) — при выключении видео вызывает setVideoAllowed(false)track.stop(). Камеру отпускает КОРРЕКТНО.
  2. Превью-подсистема (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 (превью в диалоге работает).

Что проверить в тест-фазе

  1. Вход в звонок с выключенной камерой → лампочка НЕ горит; видео нет. (главный кейс)
  2. Диалог настроек мультимедиа → превью камеры/микрофона работает; при закрытии камера гаснет.
  3. Включение видео в звонке → камера работает, видео идёт; выключение → лампочка гаснет.
  4. Смена камеры/микрофона в меню кнопки во время звонка.
  5. Виртуальный фон в превью настроек.
  6. Несколько вкладок/повторные входы — нет «залипшего» потока.