diff --git a/package-lock.json b/package-lock.json index 347d996..ad64e63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "hark": "^1.2.3", "immer": "^10.0.2", "lucide-react": "^0.541.0", "mediasoup-client": "3.17.0", @@ -44,6 +45,7 @@ "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/hark": "^1.2.5", "@types/jest": "^30.0.0", "@types/node": "^24.3.0", "@types/react": "^19.1.10", @@ -2925,6 +2927,13 @@ "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", "license": "MIT" }, + "node_modules/@types/hark": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/hark/-/hark-1.2.5.tgz", + "integrity": "sha512-PyVF1PIrbKHeC1+6W/HZoMT40DLoQ0d1FAxuv7Vcg+XBfmpio8qr+y29chxYutJpra/K2MWXiXm/lScEv2E9pg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -5147,6 +5156,15 @@ "url": "https://opencollective.com/mediasoup" } }, + "node_modules/hark": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/hark/-/hark-1.2.3.tgz", + "integrity": "sha512-u68vz9SCa38ESiFJSDjqK8XbXqWzyot7Cj6Y2b6jk2NJ+II3MY2dIrLMg/kjtIAun4Y1DHF/20hfx4rq1G5GMg==", + "license": "MIT", + "dependencies": { + "wildemitter": "^1.2.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -7717,6 +7735,11 @@ "node": ">=8" } }, + "node_modules/wildemitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/wildemitter/-/wildemitter-1.2.1.tgz", + "integrity": "sha512-UMmSUoIQSir+XbBpTxOTS53uJ8s/lVhADCkEbhfRjUGFDPme/XGOb0sBWLx5sTz7Wx/2+TlAw1eK9O5lw5PiEw==" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 505e687..f698d77 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "hark": "^1.2.3", "immer": "^10.0.2", "lucide-react": "^0.541.0", "mediasoup-client": "3.17.0", @@ -53,6 +54,7 @@ "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/hark": "^1.2.5", "@types/jest": "^30.0.0", "@types/node": "^24.3.0", "@types/react": "^19.1.10", diff --git a/src/components/room/grid/my-tile.tsx b/src/components/room/grid/my-tile.tsx index eebd76a..7254e73 100644 --- a/src/components/room/grid/my-tile.tsx +++ b/src/components/room/grid/my-tile.tsx @@ -1,11 +1,12 @@ import React, { useEffect, useRef } from 'react'; import { Mic, MicOff } from 'lucide-react'; import type { Layout } from '@/types'; -import { cn, getInitials } from '@/lib/utils'; +import { cn, getInitials, getPeerId } from '@/lib/utils'; import { useCameraDeviceId, useCameraOn, useMicOn, + usePeerConditionsById, usePeerMe, } from '@/store/conf/hooks'; import { useMedia } from '@/hooks/use-media'; @@ -21,6 +22,7 @@ const MyTile: React.FC = ({ layout }) => { const cameraOn = useCameraOn(); const cameraDeviceId = useCameraDeviceId(); const peerMe = usePeerMe(); + const peerMeCondition = usePeerConditionsById(peerMe?.id || getPeerId()); useEffect(() => { if (!cameraOn || !videoRef.current) return; @@ -32,7 +34,11 @@ const MyTile: React.FC = ({ layout }) => { if (!peerMe) return null; return (
{/* Video/Avatar Area */} diff --git a/src/components/room/grid/peer-tile.tsx b/src/components/room/grid/peer-tile.tsx index d44546f..bca6ce6 100644 --- a/src/components/room/grid/peer-tile.tsx +++ b/src/components/room/grid/peer-tile.tsx @@ -2,7 +2,11 @@ import React, { useEffect, useRef } from 'react'; import { Mic, MicOff } from 'lucide-react'; import type { Layout } from '@/types'; import { cn, getInitials } from '@/lib/utils'; -import { usePeerMediasById, usePeerOthersById } from '@/store/conf/hooks'; +import { + usePeerConditionsById, + usePeerMediasById, + usePeerOthersById, +} from '@/store/conf/hooks'; import { useMedia } from '@/hooks/use-media'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; @@ -15,6 +19,7 @@ export const PeerTile: React.FC = ({ peerId, layout }) => { const videoRef = useRef(null); const peerData = usePeerOthersById(peerId); const media = usePeerMediasById(peerId); + const peerMeCondition = usePeerConditionsById(peerId); useEffect(() => { if (!media?.camera || !videoRef.current) return; @@ -31,7 +36,11 @@ export const PeerTile: React.FC = ({ peerId, layout }) => { return (
{/* Video/Avatar Area */} diff --git a/src/components/room/my-audio.tsx b/src/components/room/my-audio.tsx new file mode 100644 index 0000000..4d5a39a --- /dev/null +++ b/src/components/room/my-audio.tsx @@ -0,0 +1,61 @@ +import { useMedia } from '@/hooks/use-media'; +import { useMicOn, usePeerActions, usePeerMe } from '@/store/conf/hooks'; +import { useEffect, useRef } from 'react'; +import hark from 'hark'; + +const MyAudio = () => { + const { getTrack } = useMedia(); + const audioRef = useRef(null); + const screenAudioRef = useRef(null); + const peerMe = usePeerMe(); + const micOn = useMicOn(); + const speechEventsRef = useRef(null); + const peerActions = usePeerActions(); + + // mic + useEffect(() => { + if (!micOn || !audioRef.current || !peerMe?.id) { + if (speechEventsRef.current) { + speechEventsRef.current.stop(); + } + return; + } + const track = getTrack('mic'); + if (!track) return; + const stream = new MediaStream([track]); + + audioRef.current.srcObject = stream; + speechEventsRef.current = hark(stream, {}); + + speechEventsRef.current.on('speaking', () => { + peerActions.updateCondition(peerMe.id, { isSpeaking: true }); + }); + speechEventsRef.current.on('stopped_speaking', () => { + peerActions.updateCondition(peerMe.id, { isSpeaking: false }); + }); + + return () => { + if (speechEventsRef.current) { + speechEventsRef.current.stop(); + } + }; + }, [micOn, getTrack, peerActions, peerMe?.id]); + + // screen audio + useEffect(() => { + if (!screenAudioRef.current) return; + const track = getTrack('screenAudio'); + if (!track) return; + const stream = new MediaStream([track]); + screenAudioRef.current.srcObject = stream; + }, [getTrack]); + + return ( + <> +