feat(room): кнопка микрофона в мини-окне (PiP)
Document PiP не показывает тулбар комнаты — во время звонка в мини-окне нельзя было замьютить себя, не разворачивая основное окно. Кнопка лежит поверх единственной плитки мини-плеера и использует тот же useTrackToggle, что и тулбар: React-контекст LiveKitRoom не разрывается порталом в pipWindow.document.body, поэтому состояние читается и меняется одинаково что в основном окне, что в мини-плеере.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState, type ReactNode } from 'react'
|
import { useEffect, useState, type ReactNode } from 'react'
|
||||||
import { EyeOff, Users } from 'lucide-react'
|
import { EyeOff, Mic, MicOff, Users } from 'lucide-react'
|
||||||
import { Track, type Participant } from 'livekit-client'
|
import { Track, type Participant } from 'livekit-client'
|
||||||
import {
|
import {
|
||||||
CarouselLayout,
|
CarouselLayout,
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
isTrackReference,
|
isTrackReference,
|
||||||
useRoomContext,
|
useRoomContext,
|
||||||
useSpeakingParticipants,
|
useSpeakingParticipants,
|
||||||
|
useTrackToggle,
|
||||||
useTracks,
|
useTracks,
|
||||||
type TrackReferenceOrPlaceholder,
|
type TrackReferenceOrPlaceholder,
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
@@ -93,6 +94,29 @@ function hasLiveVideo(track: TrackReferenceOrPlaceholder): boolean {
|
|||||||
return isTrackReference(track) && !track.publication.isMuted
|
return isTrackReference(track) && !track.publication.isMuted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Кнопка микрофона поверх единственной плитки мини-плеера (задача A5): в
|
||||||
|
* Document PiP своего тулбара нет вовсе, а звонок продолжается — без неё
|
||||||
|
* замьютить себя можно, только развернув основное окно. `useTrackToggle` —
|
||||||
|
* тот же хук, что и в `RoomToolbar`, читает/меняет состояние ЧЕРЕЗ
|
||||||
|
* `RoomContext`, а не через DOM, поэтому работает одинаково что в основном
|
||||||
|
* окне, что в портале, рендерящем эту сцену внутрь `pipWindow.document.body`
|
||||||
|
* (React-контекст порталом не разрывается, в отличие от DOM-дерева).
|
||||||
|
*/
|
||||||
|
function PipMicToggle() {
|
||||||
|
const mic = useTrackToggle({ source: Track.Source.Microphone })
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
{...mic.buttonProps}
|
||||||
|
className={`room-pip-mic-toggle${mic.enabled ? '' : ' is-off'}`}
|
||||||
|
aria-label={mic.enabled ? 'Выключить микрофон' : 'Включить микрофон'}
|
||||||
|
>
|
||||||
|
{mic.enabled ? <Mic className="lucide" aria-hidden="true" /> : <MicOff className="lucide" aria-hidden="true" />}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Основная сцена конференции: превью остальных участников + крупная плитка
|
* Основная сцена конференции: превью остальных участников + крупная плитка
|
||||||
* активного спикера (FocusLayoutContainer + CarouselLayout при нескольких
|
* активного спикера (FocusLayoutContainer + CarouselLayout при нескольких
|
||||||
@@ -366,6 +390,7 @@ export function RoomStage({
|
|||||||
return (
|
return (
|
||||||
<section className="stage room-single-tile">
|
<section className="stage room-single-tile">
|
||||||
{focusTrack && <RoomParticipantTile trackRef={focusTrack} onStopSharing={handleStopSharing} />}
|
{focusTrack && <RoomParticipantTile trackRef={focusTrack} onStopSharing={handleStopSharing} />}
|
||||||
|
<PipMicToggle />
|
||||||
<RoomAudioRenderer />
|
<RoomAudioRenderer />
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -133,9 +133,32 @@ video[data-lk-source='screen_share'] { object-fit: contain; background: #000; }
|
|||||||
* `.stage-tiles` в основном окне) плюс подстраховочный `min-height`, чтобы
|
* `.stage-tiles` в основном окне) плюс подстраховочный `min-height`, чтобы
|
||||||
* плитка не схлопывалась, если видео ещё не выдало реальные размеры.
|
* плитка не схлопывалась, если видео ещё не выдало реальные размеры.
|
||||||
*/
|
*/
|
||||||
.room-single-tile { flex: 1; min-height: 0; display: flex; }
|
.room-single-tile { position: relative; flex: 1; min-height: 0; display: flex; }
|
||||||
.room-single-tile .lk-participant-tile { flex: 1; min-height: 220px; width: 100%; }
|
.room-single-tile .lk-participant-tile { flex: 1; min-height: 220px; width: 100%; }
|
||||||
|
|
||||||
|
/* Кнопка микрофона мини-плеера (задача A5) — своего тулбара в PiP нет,
|
||||||
|
кнопка лежит поверх единственной плитки левым нижним углом (правый занят
|
||||||
|
штатным `.lk-focus-toggle-button`/нашей булавкой закрепления — тут её нет,
|
||||||
|
в PiP закреплять нечего, но геометрия зеркалит `.room-pin-toggle`). */
|
||||||
|
.room-pip-mic-toggle {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.5rem;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
z-index: 5;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--color-room-mic-on);
|
||||||
|
color: #10331f;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.room-pip-mic-toggle svg { width: 18px; height: 18px; }
|
||||||
|
.room-pip-mic-toggle.is-off { background: var(--color-room-danger-bg); color: #fff; }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ---------- Колонка миниатюр (кнопка «Скрыть» + карусель) ----------
|
* ---------- Колонка миниатюр (кнопка «Скрыть» + карусель) ----------
|
||||||
* Прямой ребёнок `.lk-focus-layout` — вместо самой карусели, потому что над
|
* Прямой ребёнок `.lk-focus-layout` — вместо самой карусели, потому что над
|
||||||
|
|||||||
Reference in New Issue
Block a user