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:
@@ -13,6 +13,24 @@ ARG VITE_API_URL=/api
|
||||
ENV VITE_API_URL=$VITE_API_URL
|
||||
RUN npm run build
|
||||
|
||||
# Pliki MediaPipe do rozmycia tla, kopiowane Z ZAINSTALOWANEGO PAKIETU.
|
||||
#
|
||||
# DLACZEGO NIE Z REPOZYTORIUM
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zestaw WASM wazy kilkanascie MB i musi odpowiadac wersji
|
||||
# `@mediapipe/tasks-vision` wciagnietej przez `@livekit/track-processors`.
|
||||
# Wrzucony do repozytorium puchlby w historii na zawsze, a przy aktualizacji
|
||||
# pakietu rozjezdzalby sie po cichu: rozmycie przestaje dzialac, a nic tego
|
||||
# nie sygnalizuje. Kopiowanie przy budowaniu wiaze pliki z wersja pakietu.
|
||||
#
|
||||
# DLACZEGO NIE Z CDN
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domyslnie biblioteka pobiera je z jsdelivr i storage.googleapis.com.
|
||||
# Instancja w sieci zamknietej nie miałaby wtedy rozmycia wcale, a CSP
|
||||
# musialoby dopuszczac dwa obce hosty. Serwowanie z wlasnej domeny miesci sie
|
||||
# w istniejacym `connect-src 'self'`.
|
||||
RUN mkdir -p /build/dist/mediapipe && cp -r node_modules/@mediapipe/tasks-vision/wasm /build/dist/mediapipe/wasm
|
||||
|
||||
# --- etap 2: runtime ---
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
|
||||
Generated
+36
@@ -13,6 +13,7 @@
|
||||
"@jitsi/react-sdk": "^1.4.4",
|
||||
"@livekit/components-react": "^2.9.24",
|
||||
"@livekit/components-styles": "^1.2.0",
|
||||
"@livekit/track-processors": "^0.7.2",
|
||||
"@mui/x-charts": "^9.12.0",
|
||||
"@stomp/stompjs": "^7.3.0",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
@@ -31,6 +32,7 @@
|
||||
"react-router-dom": "^6.28.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dom-mediacapture-transform": "^0.1.9",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
@@ -1108,6 +1110,25 @@
|
||||
"@bufbuild/protobuf": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@livekit/track-processors": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/track-processors/-/track-processors-0.7.2.tgz",
|
||||
"integrity": "sha512-lzARBKTbBwqycdR/SwTu6//N0l20BzfDd7grxCXl07676SwRApNtZAK1GJjL1m3dCM3KBqH1aVxjMpNcbOw5uQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mediapipe/tasks-vision": "0.10.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/dom-mediacapture-transform": "^0.1.9",
|
||||
"livekit-client": "^1.12.0 || ^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mediapipe/tasks-vision": {
|
||||
"version": "0.10.14",
|
||||
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.14.tgz",
|
||||
"integrity": "sha512-vOifgZhkndgybdvoRITzRkIueWWSiCKuEUXXK6Q4FaJsFvRJuwgg++vqFUMlL0Uox62U5aEXFhHxlhV7Ja5e3Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "9.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.4.0.tgz",
|
||||
@@ -2464,6 +2485,21 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/dom-mediacapture-transform": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.12.tgz",
|
||||
"integrity": "sha512-d7/QsLRwF864A5mgIM/YrfiglHoYn7zgCcAoJgW404r+2DwnNr7EBbLnCWpmOMgH8y0te73L1AV6H1bmauaWFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/dom-webcodecs": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dom-webcodecs": {
|
||||
"version": "0.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.19.tgz",
|
||||
"integrity": "sha512-+yFUIDUaASFz0vQpWBErPxun/Lb+F0TdrlTsj50gm7K797YzEx2TmV6fEiwciHyFNghlZYPgQXH5qRcwMHcJFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@jitsi/react-sdk": "^1.4.4",
|
||||
"@livekit/components-react": "^2.9.24",
|
||||
"@livekit/components-styles": "^1.2.0",
|
||||
"@livekit/track-processors": "^0.7.2",
|
||||
"@mui/x-charts": "^9.12.0",
|
||||
"@stomp/stompjs": "^7.3.0",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
@@ -39,6 +40,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dom-mediacapture-transform": "^0.1.9",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
|
||||
Binary file not shown.
@@ -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