Przycisk w sali, ktory kazdy z wlasna kamera wlacza i wylacza sam, w trakcie spotkania. Pokazuje sie wylacznie osobie z wlaczona kamera, wiec uczestnik bez prawa publikacji obrazu nigdy go nie zobaczy. `@livekit/track-processors` wchodzi jako nowa zaleznosc, poza lista z CLAUDE.md: uzgodnione. Ladowany leniwie, bo ciagnie za soba MediaPipe: import statyczny doliczalby kilka MB do pakietu KAZDEGO wejscia na sale, takze uczestnikom bez kamery. Sprawdzone na zbudowanym pakiecie: MediaPipe siedzi w osobnym chunku, glowny pakiet nie urosl. Pliki modelu ida z WLASNEJ domeny, nie z CDN: instancja w sieci zamknietej ma miec rozmycie tak samo, a CSP nie musi dopuszczac obcych hostow. Model (244 kB) lezy w repozytorium, a zestaw WASM kopiuje sie przy budowaniu obrazu z `node_modules`: wazy kilkanascie MB i musi odpowiadac wersji pakietu, wiec w repozytorium puchlby w historii i po cichu rozjezdzal sie przy kazdej aktualizacji. Podmiany tla na obraz swiadomie NIE ma. Segmentacja obcina wlosy i dlonie przy slabym swietle, a kafelek wideo jest za maly, zeby cokolwiek na tle bylo czytelne. Reklama ma wlasne miejsce w banerach, ktore sa przy tym mierzone. `VideoConference` zostaje nietkniety: przycisk jest obok, wiec aktualizacja biblioteki nie wymaga odtwarzania niczego u nas. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { useLocalParticipant } from '@livekit/components-react'
|
||||
import { LocalVideoTrack, Track } from 'livekit-client'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Icon } from '../../components/Icon'
|
||||
|
||||
/**
|
||||
* Przelacznik rozmycia tla wlasnej kamery.
|
||||
*
|
||||
* DLACZEGO OSOBNY PRZYCISK, A NIE WLASNY PASEK NARZEDZI
|
||||
* ---------------------------------------------------------------------------
|
||||
* `VideoConference` zostaje nietkniety, zgodnie z zasada, ze nie budujemy
|
||||
* wlasnego interfejsu wideo. Ten przycisk dokladamy obok, wiec aktualizacja
|
||||
* biblioteki nie wymaga odtwarzania niczego u nas.
|
||||
*
|
||||
* DLACZEGO LADOWANIE JEST LENIWE
|
||||
* ---------------------------------------------------------------------------
|
||||
* `@livekit/track-processors` ciagnie za soba MediaPipe, czyli kilka MB kodu
|
||||
* i WASM. Import statyczny doliczalby to do pakietu KAZDEGO wejscia na sale,
|
||||
* takze uczestnikom, ktorzy kamery nie maja wcale. Modul wczytuje sie dopiero
|
||||
* przy pierwszym klinieciu.
|
||||
*
|
||||
* DLACZEGO PLIKI IDA Z WLASNEJ DOMENY
|
||||
* ---------------------------------------------------------------------------
|
||||
* Domyslnie biblioteka pobiera WASM z jsdelivr, a model ze storage.googleapis.
|
||||
* Instancja w sieci zamknietej nie miałaby wtedy rozmycia, a CSP musialoby
|
||||
* dopuszczac dwa obce hosty. Pliki leza u nas: model w `public/mediapipe`,
|
||||
* zestaw WASM kopiowany przy budowaniu obrazu z `node_modules`.
|
||||
*
|
||||
* DLACZEGO WYLACZAMY PRZEZ `switchTo`, A NIE `stopProcessor`
|
||||
* ---------------------------------------------------------------------------
|
||||
* Zatrzymanie procesora potrafi zostawic zamrozona klatke, bo sciezka jest
|
||||
* przepinana w locie. Tryb `disabled` przepuszcza obraz bez zmian i nie
|
||||
* przerywa publikacji.
|
||||
*/
|
||||
export function BackgroundBlurToggle({ className = '' }: { className?: string }) {
|
||||
const { cameraTrack } = useLocalParticipant()
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
/*
|
||||
* Procesor przezywa wylaczenie rozmycia, bo `switchTo` jest tansze niz
|
||||
* budowanie go od nowa: MediaPipe wczytuje wtedy model ponownie i obraz
|
||||
* zacina sie na chwile przy kazdym przelaczeniu.
|
||||
*/
|
||||
const processorRef = useRef<{ switchTo: (options: { mode: string }) => Promise<void> } | null>(null)
|
||||
|
||||
const track = cameraTrack?.track
|
||||
const isCamera = cameraTrack?.source === Track.Source.Camera
|
||||
const available = track instanceof LocalVideoTrack && isCamera && !cameraTrack?.isMuted
|
||||
|
||||
/*
|
||||
* Wylaczona kamera zabiera sciezke, wiec procesor przestaje istniec razem
|
||||
* z nia. Bez tego przycisk pokazywalby rozmycie jako wlaczone po ponownym
|
||||
* wlaczeniu kamery, kiedy obraz jest juz zwykly.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!available) {
|
||||
processorRef.current = null
|
||||
setEnabled(false)
|
||||
}
|
||||
}, [available])
|
||||
|
||||
const toggle = useCallback(async () => {
|
||||
if (!(track instanceof LocalVideoTrack) || busy) return
|
||||
|
||||
setBusy(true)
|
||||
setError(null)
|
||||
try {
|
||||
if (!processorRef.current) {
|
||||
const { BackgroundProcessor, supportsBackgroundProcessors } = await import(
|
||||
'@livekit/track-processors'
|
||||
)
|
||||
|
||||
if (!supportsBackgroundProcessors()) {
|
||||
setError('Ta przeglądarka nie obsługuje rozmycia tła.')
|
||||
return
|
||||
}
|
||||
|
||||
const processor = BackgroundProcessor({
|
||||
mode: 'background-blur',
|
||||
blurRadius: 10,
|
||||
assetPaths: {
|
||||
tasksVisionFileSet: '/mediapipe/wasm',
|
||||
modelAssetPath: '/mediapipe/selfie_segmenter.tflite',
|
||||
},
|
||||
})
|
||||
|
||||
await track.setProcessor(processor)
|
||||
processorRef.current = processor as unknown as typeof processorRef.current
|
||||
setEnabled(true)
|
||||
return
|
||||
}
|
||||
|
||||
const next = !enabled
|
||||
await processorRef.current.switchTo({ mode: next ? 'background-blur' : 'disabled' })
|
||||
setEnabled(next)
|
||||
} catch (e) {
|
||||
// Najczestsza przyczyna to slaby sprzet albo brak WebGL: mowimy o skutku,
|
||||
// a nie o nazwie wyjatku, ktora uzytkownikowi nic nie mowi.
|
||||
setError('Nie udało się włączyć rozmycia tła.')
|
||||
console.error('Rozmycie tla:', e)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}, [track, busy, enabled])
|
||||
|
||||
// Bez wlasnej kamery nie ma czego rozmywac: uczestnik bez prawa publikacji
|
||||
// obrazu nie zobaczy tego przycisku wcale.
|
||||
if (!available) return null
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
disabled={busy}
|
||||
aria-pressed={enabled}
|
||||
className={`inline-flex min-h-[44px] items-center gap-2 rounded-md border px-3 text-sm transition-colors duration-fast ease-standard disabled:opacity-60 ${
|
||||
enabled
|
||||
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||
: 'border-slate-300 bg-white text-slate-700 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<Icon name={busy ? 'loader' : 'droplet'} size={16} />
|
||||
{busy ? 'Chwileczkę...' : enabled ? 'Rozmycie tła włączone' : 'Rozmyj tło'}
|
||||
</button>
|
||||
|
||||
{error && <p className="mt-1 text-xs text-alarm-600">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user