Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
10 changes: 8 additions & 2 deletions src/components/room/grid/my-tile.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,6 +22,7 @@ const MyTile: React.FC<PeerTileProps> = ({ layout }) => {
const cameraOn = useCameraOn();
const cameraDeviceId = useCameraDeviceId();
const peerMe = usePeerMe();
const peerMeCondition = usePeerConditionsById(peerMe?.id || getPeerId());

useEffect(() => {
if (!cameraOn || !videoRef.current) return;
Expand All @@ -32,7 +34,11 @@ const MyTile: React.FC<PeerTileProps> = ({ layout }) => {
if (!peerMe) return null;
return (
<div
className=" bg-linear-to-br from-white/5 to-white/2 border border-white/10 backdrop-blur-xl rounded-lg overflow-hidden flex flex-col relative transition-all duration-300 ease-in-out"
className={cn(
`bg-linear-to-br from-white/5 to-white/2 border border-white/10 backdrop-blur-xl
rounded-lg overflow-hidden flex flex-col relative transition-all duration-300 ease-in-out`,
peerMeCondition?.isSpeaking && ' border-blue-500'
)}
style={{ width: `${layout.width}px`, height: `${layout.height}px` }}
>
{/* Video/Avatar Area */}
Expand Down
13 changes: 11 additions & 2 deletions src/components/room/grid/peer-tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -15,6 +19,7 @@ export const PeerTile: React.FC<PeerTileProps> = ({ peerId, layout }) => {
const videoRef = useRef<HTMLVideoElement>(null);
const peerData = usePeerOthersById(peerId);
const media = usePeerMediasById(peerId);
const peerMeCondition = usePeerConditionsById(peerId);

useEffect(() => {
if (!media?.camera || !videoRef.current) return;
Expand All @@ -31,7 +36,11 @@ export const PeerTile: React.FC<PeerTileProps> = ({ peerId, layout }) => {

return (
<div
className=" bg-linear-to-br from-white/5 to-white/2 border border-white/10 backdrop-blur-xl rounded-lg overflow-hidden flex flex-col relative transition-all duration-300 ease-in-out"
className={cn(
`bg-linear-to-br from-white/5 to-white/2 border border-white/10 backdrop-blur-xl
rounded-lg overflow-hidden flex flex-col relative transition-all duration-300 ease-in-out`,
peerMeCondition?.isSpeaking && ' border-blue-500'
)}
style={{ width: `${layout.width}px`, height: `${layout.height}px` }}
>
{/* Video/Avatar Area */}
Expand Down
61 changes: 61 additions & 0 deletions src/components/room/my-audio.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLAudioElement>(null);
const screenAudioRef = useRef<HTMLAudioElement>(null);
const peerMe = usePeerMe();
const micOn = useMicOn();
const speechEventsRef = useRef<hark.Harker>(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 (
<>
<audio ref={screenAudioRef} autoPlay muted />
<audio ref={audioRef} autoPlay muted />
</>
);
};

export default MyAudio;
2 changes: 2 additions & 0 deletions src/components/room/peer-audio-list.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { usePeerOthersKeys } from '@/store/conf/hooks';
import PeerAudio from './peer-audio';
import MyAudio from './my-audio';

const PeerAudioList = () => {
const peerIds = usePeerOthersKeys();
return (
<>
<MyAudio />
{peerIds.map(id => (
<PeerAudio key={id} peerId={id} />
))}
Expand Down
31 changes: 27 additions & 4 deletions src/components/room/peer-audio.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
import { useMedia } from '@/hooks/use-media';
import { usePeerMediasById } from '@/store/conf/hooks';
import { usePeerActions, usePeerMediasById } from '@/store/conf/hooks';
import { useEffect, useRef } from 'react';
import hark from 'hark';

const PeerAudio = ({ peerId }: { peerId: string }) => {
const { getConsumer } = useMedia();
const audioRef = useRef<HTMLAudioElement>(null);
const screenAudioRef = useRef<HTMLAudioElement>(null);
const media = usePeerMediasById(peerId);
const speechEventsRef = useRef<hark.Harker>(null);
const peerActions = usePeerActions();

// mic
useEffect(() => {
if (!media?.mic || !audioRef.current) return;
if (!media?.mic || !audioRef.current) {
if (speechEventsRef.current) {
speechEventsRef.current.stop();
}
return;
}

const consumer = getConsumer(peerId, 'mic');
if (!consumer) return;
const { track } = consumer;
const stream = new MediaStream([track]);

audioRef.current.srcObject = stream;
}, [media?.mic, getConsumer, peerId]);
speechEventsRef.current = hark(stream, {});

speechEventsRef.current.on('speaking', () => {
peerActions.updateCondition(peerId, { isSpeaking: true });
});
speechEventsRef.current.on('stopped_speaking', () => {
peerActions.updateCondition(peerId, { isSpeaking: false });
});

return () => {
if (speechEventsRef.current) {
speechEventsRef.current.stop();
}
};
}, [media?.mic, getConsumer, peerActions, peerId]);

// screen audio
useEffect(() => {
Expand All @@ -26,9 +49,9 @@ const PeerAudio = ({ peerId }: { peerId: string }) => {
if (!consumer) return;
const { track } = consumer;
const stream = new MediaStream([track]);

screenAudioRef.current.srcObject = stream;
}, [media?.screenAudio, getConsumer, peerId]);

return (
<>
<audio ref={screenAudioRef} autoPlay />
Expand Down
1 change: 0 additions & 1 deletion src/context/service-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
error: string | null;
}

export const ServiceContext = createContext<ServiceContextType>({

Check warning on line 14 in src/context/service-context.tsx

View workflow job for this annotation

GitHub Actions / Lint, Build, and Test (20.x)

Fast refresh only works when a file only exports components. Move your React context(s) to a separate file
mediaService: null,
signalingService: null,
isInitializing: true,
Expand Down Expand Up @@ -46,10 +46,9 @@
// Initialize media service
const newMediaService = await MediaService.start(newSignalingService);
setMediaService(newMediaService);
console.log('signaling and media service started');
setError(null);
} catch (error) {
console.error('Service initialization error:', error);

Check warning on line 51 in src/context/service-context.tsx

View workflow job for this annotation

GitHub Actions / Lint, Build, and Test (20.x)

Unexpected console statement
setError(
error instanceof Error
? error.message
Expand Down
2 changes: 0 additions & 2 deletions src/hooks/use-room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ export const useRoom = () => {
const data = ValidationSchema.createConsumerData.parse(args);
await createConsumer(data);
callback({ status: 'success' });
callback({ status: 'success' });
},

[Actions.ConsumerPaused]: async args => {
Expand All @@ -129,7 +128,6 @@ export const useRoom = () => {
},
[Actions.SendChat]: async args => {
const data = ValidationSchema.sendChat.parse(args);
console.log(data);
chatActions.addChat(data);
},
}),
Expand Down
1 change: 0 additions & 1 deletion src/hooks/use-signaling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export const useSignaling = () => {
signalingService.sendMessage({
action: Actions.Heartbeat,
});
console.log('Send heart beat');
}, [signalingService]);

const sendMessage = useCallback(
Expand Down
7 changes: 7 additions & 0 deletions src/store/conf/slices/peer-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ export const createPeerSlice: StateCreator<
state.peers.conditions = {};
state.peers.positions = [];
state.peers.screens = [];

const myPeerId = state.peers.me?.id;
if (myPeerId) {
state.peers.medias[myPeerId] = { id: myPeerId };
state.peers.conditions[myPeerId] = { id: myPeerId };
}

return state;
}),
});
Loading