diff --git a/client/.env b/client/.env index 325974f..0f22f67 100644 --- a/client/.env +++ b/client/.env @@ -1,4 +1,4 @@ -# VITE_API_URL=http://192.168.1.23:3000 -# VITE_API_URL=http://192.168.1.224:3000 -VITE_API_URL=https://stream.graff.estate/api -VITE_WEBRTC_URL=https://stream.graff.estate \ No newline at end of file +VITE_API_URL=http://localhost:3000 +VITE_WEBRTC_URL=http://localhost:3001 +# VITE_API_URL=https://stream.graff.estate/api +# VITE_WEBRTC_URL=https://stream.graff.estate \ No newline at end of file diff --git a/client/bun.lock b/client/bun.lock index 9ad6317..cb64456 100644 --- a/client/bun.lock +++ b/client/bun.lock @@ -14,6 +14,8 @@ "react-dom": "^19.1.1", "react-qr-code": "^2.0.18", "react-router": "^7.9.3", + "socket.io-client": "^4.8.1", + "uuid": "^13.0.0", "zustand": "^5.0.8", }, "devDependencies": { @@ -21,6 +23,7 @@ "@types/node": "^24.6.0", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", + "@types/uuid": "^11.0.0", "@vitejs/plugin-react-swc": "^4.1.0", "autoprefixer": "^10.4.21", "eslint": "^9.36.0", @@ -186,6 +189,8 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.3", "", { "os": "win32", "cpu": "x64" }, "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA=="], + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + "@swc/core": ["@swc/core@1.13.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.5", "@swc/core-darwin-x64": "1.13.5", "@swc/core-linux-arm-gnueabihf": "1.13.5", "@swc/core-linux-arm64-gnu": "1.13.5", "@swc/core-linux-arm64-musl": "1.13.5", "@swc/core-linux-x64-gnu": "1.13.5", "@swc/core-linux-x64-musl": "1.13.5", "@swc/core-win32-arm64-msvc": "1.13.5", "@swc/core-win32-ia32-msvc": "1.13.5", "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ=="], "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ=="], @@ -226,6 +231,8 @@ "@types/react-dom": ["@types/react-dom@19.2.0", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg=="], + "@types/uuid": ["@types/uuid@11.0.0", "", { "dependencies": { "uuid": "*" } }, "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.45.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/type-utils": "8.45.0", "@typescript-eslint/utils": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.45.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg=="], @@ -326,6 +333,10 @@ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "engine.io-client": ["engine.io-client@6.6.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w=="], + + "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], + "esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -568,6 +579,10 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "socket.io-client": ["socket.io-client@4.8.1", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ=="], + + "socket.io-parser": ["socket.io-parser@4.2.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" } }, "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -616,6 +631,8 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + "vite": ["vite@7.1.8", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-oBXvfSHEOL8jF+R9Am7h59Up07kVVGH1NrFGFoEG6bPDZP3tGpQhvkBpy5x7U6+E6wZCu9OihsWgJqDbQIm8LQ=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -628,6 +645,8 @@ "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "zustand": ["zustand@5.0.8", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="], @@ -644,6 +663,10 @@ "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + + "engine.io-client/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -652,6 +675,10 @@ "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "socket.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + + "socket.io-parser/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/client/src/components/SessionUsersPanel.tsx b/client/src/components/SessionUsersPanel.tsx index 4b91ca3..3790fb7 100644 --- a/client/src/components/SessionUsersPanel.tsx +++ b/client/src/components/SessionUsersPanel.tsx @@ -72,6 +72,7 @@ function SessionUsersPanel({ isControlDisabled={true} isAdmin={true} // Локальный пользователь - админ своей сессии mediaStream={participant.stream} + hasLocalMediaPermission={hasLocalStream} onMute={() => console.log(`Mute user ${participant.id}`)} onVideoOff={() => console.log(`Video off user ${participant.id}`)} onCanControl={() => diff --git a/client/src/components/popups/ParticipantsPopup.tsx b/client/src/components/popups/ParticipantsPopup.tsx index 2c687c6..7c0558c 100644 --- a/client/src/components/popups/ParticipantsPopup.tsx +++ b/client/src/components/popups/ParticipantsPopup.tsx @@ -12,11 +12,24 @@ import { Fragment, useRef } from "react"; import DraggableContainer from "../DraggableContainer"; import { useWebRTC } from "../../hooks/useWebRTC"; import type { Participant } from "../../lib/webrtc"; +import type { Session } from "../../types/Session"; +import { getGuestId } from "../../lib/guestId"; +import { useMe } from "../../hooks/useAuth"; -export default function ParticipantsPopup() { +interface ParticipantsPopupProps { + session: Session; +} + +export default function ParticipantsPopup({ session }: ParticipantsPopupProps) { const { participants, currentUserId, localStream } = useWebRTC(); + const { data: user } = useMe(); const headerRef = useRef(null); - + + // Определяем, является ли текущий пользователь организатором + const isOrganizer = + !!(session.userId && user?.id === session.userId) || + !!(session.guestId && getGuestId() === session.guestId); + // Добавляем локального пользователя в начало списка const allParticipants: (Participant & { isLocal?: boolean })[] = [ { @@ -39,15 +52,17 @@ export default function ParticipantsPopup() {
{allParticipants.length === 0 ? ( -
+
Нет участников
) : ( allParticipants.map((participant) => ( -
@@ -79,23 +94,38 @@ export default function ParticipantsPopup() { ); } -function ParticipantItem({ - participant, - isLocal -}: { - participant: Participant & { isLocal?: boolean }; +function ParticipantItem({ + participant, + isLocal, + isOrganizer, + session, +}: { + participant: Participant & { isLocal?: boolean }; isLocal: boolean; + isOrganizer: boolean; + session: Session; }) { const parentRef = useRef(null); - + // Проверяем наличие аудио/видео треков - const hasAudio = participant.stream?.getAudioTracks().some(track => track.enabled) ?? false; - const hasVideo = participant.stream?.getVideoTracks().some(track => track.enabled) ?? false; + const hasAudio = + participant.stream?.getAudioTracks().some((track) => track.enabled) ?? + false; + const hasVideo = + participant.stream?.getVideoTracks().some((track) => track.enabled) ?? + false; const isMuted = !hasAudio; const isVideoOff = !hasVideo; - // Определяем статус участника - const status: "admin" | "caution" | undefined = participant.stream ? "admin" : "caution"; + // Определяем, является ли этот конкретный участник организатором сессии + const isThisParticipantOrganizer = + (session.userId && participant.id === session.userId) || + (session.guestId && participant.id === session.guestId); + + // Определяем статус участника для аватара + const status: "admin" | "caution" | undefined = isThisParticipantOrganizer + ? "admin" + : undefined; return (
@@ -103,10 +133,12 @@ function ParticipantItem({
- {isLocal ? "Вы" : participant.name || `Участник ${participant.id.slice(0, 8)}`} + {isLocal + ? "Вы" + : participant.name || `Участник ${participant.id.slice(0, 8)}`} - {isLocal ? "Организатор" : "Участник"} + {isThisParticipantOrganizer ? "Организатор" : "Участник"}
@@ -123,8 +155,8 @@ function ParticipantItem({
)} - {/* Действия только для удаленных участников и только для администратора */} - {!isLocal && ( + {/* Действия только для удаленных участников и только для организатора */} + {!isLocal && isOrganizer && ( void; // Для локального - отправляем изменения + hasLocalMediaPermission?: boolean; // Есть ли у локального пользователя разрешение на медиа } export default function UserCamera({ @@ -50,9 +51,11 @@ export default function UserCamera({ isLocal = false, isSpeaking: remoteSpeaking, onSpeakingChange, + hasLocalMediaPermission = false, }: UserCameraProps) { const ref = useRef(null); - const [isAudioMuted, setIsAudioMuted] = useState(true); // Для удаленных участников - начинаем с muted + // Для удаленных участников: если у локального пользователя есть разрешение на медиа - unmute, иначе mute (для autoplay) + const [isAudioMuted, setIsAudioMuted] = useState(!hasLocalMediaPermission); // Детекция голосовой активности (только для локального пользователя) const { isSpeaking: isVoiceActive } = useVoiceActivity( @@ -62,7 +65,7 @@ export default function UserCamera({ // Для локального - используем локальную детекцию // Для удаленных - используем полученное состояние через Socket.IO const localSpeaking = !isMuted && isVoiceActive; - const isSpeaking = isLocal ? localSpeaking : (remoteSpeaking || false); + const isSpeaking = isLocal ? localSpeaking : remoteSpeaking || false; // Отправляем изменения состояния для локального пользователя useEffect(() => { @@ -78,7 +81,11 @@ export default function UserCamera({ // Логируем для отладки useEffect(() => { console.log( - `[${name}${isLocal ? " (local)" : ""}] isSpeaking: ${isSpeaking}, ringOpacity: ${ringOpacity.toFixed(2)}, isMuted: ${isMuted}` + `[${name}${ + isLocal ? " (local)" : "" + }] isSpeaking: ${isSpeaking}, ringOpacity: ${ringOpacity.toFixed( + 2 + )}, isMuted: ${isMuted}` ); }, [isSpeaking, ringOpacity, name, isMuted, isLocal]); @@ -90,12 +97,6 @@ export default function UserCamera({ ); ref.current.srcObject = mediaStream; - // Убеждаемся что видео muted для autoplay - if (!isLocal) { - ref.current.muted = true; - console.log(`[UserCamera] Set muted=true for remote video ${name}`); - } - // Принудительно запускаем воспроизведение ref.current.play().catch((error) => { console.error(`[UserCamera] Failed to play video for ${name}:`, error); @@ -237,14 +238,13 @@ export default function UserCamera({ }, [name]); const toggleRemoteAudio = () => { - if (!isLocal && ref.current) { + if (!isLocal) { const newMutedState = !isAudioMuted; - ref.current.muted = newMutedState; setIsAudioMuted(newMutedState); console.log( `[UserCamera] ${name} audio ${ newMutedState ? "muted" : "unmuted" - }, video element muted: ${ref.current.muted}` + }` ); } }; @@ -305,15 +305,6 @@ export default function UserCamera({
)} - {/* Подсказка для запуска видео */} - {!isLocal && mediaStream && !isVideoOff && ( -
-
- Кликните для запуска видео -
-
- )} -