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
9 changes: 6 additions & 3 deletions src/components/room/camera.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Video, VideoOff } from 'lucide-react';
import { Loader2, Video, VideoOff } from 'lucide-react';
import MediaDeviceDropdown from './media-device-dropdown';

import {
Expand All @@ -10,7 +10,7 @@ import { useMedia } from '@/hooks/use-media';
import MediaControlButton from './media-control-button';

const Camera = () => {
const { toggleCamera } = useMedia();
const { toggleCamera, cameraPending } = useMedia();

const cameraOn = useCameraOn();
const cameraDeviceId = useCameraDeviceId();
Expand All @@ -22,8 +22,11 @@ const Camera = () => {
isActive={cameraOn}
onClick={toggleCamera}
label={cameraOn ? 'Stop' : 'Start'}
disabled={cameraPending}
>
{cameraOn ? (
{cameraPending ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : cameraOn ? (
<Video className="w-5 h-5" />
) : (
<VideoOff className="w-5 h-5" />
Expand Down
24 changes: 23 additions & 1 deletion src/components/room/join/join-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,33 @@ import { useMedia } from '@/hooks/use-media';
import Mic from '../mic';
import Camera from '../camera';
import { useSettingsActions } from '@/store/conf/hooks';
import { toast } from 'sonner';

const Controls = () => {
const { requestCameraAndMicPermissions } = useMedia();
useEffect(() => {
requestCameraAndMicPermissions();
const checkPermissions = async () => {
try {
const [cam, mic] = await Promise.all([
navigator.permissions.query({ name: 'camera' as PermissionName }),
navigator.permissions.query({ name: 'microphone' as PermissionName }),
]);
if (cam.state === 'denied' || mic.state === 'denied') {
const which = cam.state === 'denied' && mic.state === 'denied'
? 'camera and microphone'
: cam.state === 'denied' ? 'camera' : 'microphone';
toast.error(`${which.charAt(0).toUpperCase() + which.slice(1)} access blocked`, {
description: `Allow access to your ${which} in your browser's site settings, then reload.`,
duration: 10000,
});
return;
}
} catch {
// permissions API not supported — fall through to getUserMedia
}
requestCameraAndMicPermissions();
};
checkPermissions();
}, [requestCameraAndMicPermissions]);

const settingsAction = useSettingsActions();
Expand Down
3 changes: 3 additions & 0 deletions src/components/room/media-control-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@ interface MediaControlButtonProps {
children: React.ReactNode;
className?: string;
label?: string;
disabled?: boolean;
}
const MediaControlButton: React.FC<MediaControlButtonProps> = ({
isActive,
onClick,
children,
className,
label,
disabled,
}) => (
<Button
onClick={onClick}
variant={isActive ? 'default' : 'ghost'}
size="icon"
disabled={disabled}
className={cn(
'rounded-xl transition-all duration-200 cursor-pointer text-white bg-linear-to-br',
label ? 'w-14 h-14 flex-col gap-0.5' : 'w-12 h-12',
Expand Down
19 changes: 15 additions & 4 deletions src/components/room/mic.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import { Mic as MicOn, MicOff } from 'lucide-react';
import { Loader2, Mic as MicOn, MicOff } from 'lucide-react';

import { useMicDeviceId, useMicDevices, useMicOn } from '@/store/conf/hooks';
import MediaDeviceDropdown from './media-device-dropdown';
import { useMedia } from '@/hooks/use-media';
import MediaControlButton from './media-control-button';

const Mic = () => {
const { toggleMic } = useMedia();
const { toggleMic, micPending } = useMedia();
const micOn = useMicOn();
const micDeviceId = useMicDeviceId();
const micDevices = useMicDevices();

return (
<div className="flex items-center gap-2">
<MediaControlButton isActive={micOn} onClick={toggleMic} label={micOn ? 'Mute' : 'Unmute'}>
{micOn ? <MicOn className="w-5 h-5" /> : <MicOff className="w-5 h-5" />}
<MediaControlButton
isActive={micOn}
onClick={toggleMic}
label={micOn ? 'Mute' : 'Unmute'}
disabled={micPending}
>
{micPending ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : micOn ? (
<MicOn className="w-5 h-5" />
) : (
<MicOff className="w-5 h-5" />
)}
</MediaControlButton>
<MediaDeviceDropdown
devices={micDevices}
Expand Down
61 changes: 39 additions & 22 deletions src/hooks/use-media.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { types as mediasoupTypes } from 'mediasoup-client';
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Access,
type ConsumerStateData,
Expand All @@ -25,6 +25,7 @@ import {
} from '@/store/conf/hooks';
import { Actions } from '@/types/actions';
import { requestMediaPermissions, type MediaPermissionsError } from 'mic-check';
import { toast } from 'sonner';
import { DEVICE_ERRORS } from '@/lib/constants';

// Hook for media operations
Expand Down Expand Up @@ -388,7 +389,11 @@ export const useMedia = () => {
[mediaService]
);

const [micPending, setMicPending] = useState(false);
const [cameraPending, setCameraPending] = useState(false);

const requestMicPermission = useCallback(() => {
setMicPending(true);
requestMediaPermissions({ audio: true, video: false })
.then(async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
Expand All @@ -401,31 +406,40 @@ export const useMedia = () => {
})
.catch((err: MediaPermissionsError) => {
const type = err?.type || 'DeviceNotFound';
return alert(DEVICE_ERRORS[type]('microphone'));
});
const { title, body } = DEVICE_ERRORS[type]('microphone');
toast.error(title, { description: body });
})
.finally(() => setMicPending(false));
}, [micActions]);

const requestCameraPermission = useCallback(() => {
setCameraPending(true);
requestMediaPermissions({ audio: false, video: true })
.then(async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
const audioInputDevices = devices.filter(
device => device.kind === 'audioinput'
const videoInputDevices = devices.filter(
device => device.kind === 'videoinput'
);
if (!audioInputDevices.length) throw 'Device not found';
cameraActions.setDeviceId(audioInputDevices[0].deviceId);
cameraActions.setDevices(audioInputDevices);
if (!videoInputDevices.length) throw 'Device not found';
cameraActions.setDeviceId(videoInputDevices[0].deviceId);
cameraActions.setDevices(videoInputDevices);
})
.catch((err: MediaPermissionsError) => {
const type = err?.type || 'DeviceNotFound';
return alert(DEVICE_ERRORS[type]('camera'));
});
const { title, body } = DEVICE_ERRORS[type]('camera');
toast.error(title, { description: body });
})
.finally(() => setCameraPending(false));
}, [cameraActions]);

const requestCameraAndMicPermissions = useCallback(() => {
setMicPending(true);
setCameraPending(true);
requestMediaPermissions()
.catch((err: MediaPermissionsError) => {
console.log(err);
const type = err?.type || 'Generic';
const { title, body } = DEVICE_ERRORS[type]('camera');
toast.error(title, { description: body });
})
.finally(async () => {
try {
Expand All @@ -436,32 +450,27 @@ export const useMedia = () => {
const videoInputDevices = devices.filter(
device => device.kind === 'videoinput'
);
// const audioOutputDevices = devices.filter(
// device => device.kind === 'audiooutput'
// );

// soundActions.setDeviceId(
// audioOutputDevices.length ? audioOutputDevices[0].deviceId : null
// );
cameraActions.setDeviceId(
videoInputDevices.length ? videoInputDevices[0].deviceId : null
);
micActions.setDeviceId(
audioInputDevices.length ? audioInputDevices[0].deviceId : null
);

// soundActions.setDevices(audioOutputDevices);
cameraActions.setDevices(videoInputDevices);
micActions.setDevices(audioInputDevices);
} catch (error) {
console.log(error);
} finally {
setMicPending(false);
setCameraPending(false);
}
});
}, [micActions, cameraActions]);

const toggleCamera = useCallback(async () => {
if (!mediaService) return console.log('Media service not intialised');
if (!cameraDeviceId) return requestCameraPermission();
setCameraPending(true);
try {
if (cameraOn) {
await stopUserMedia('camera');
Expand All @@ -471,6 +480,8 @@ export const useMedia = () => {
cameraActions.toggle();
} catch (error) {
console.log(error);
} finally {
setCameraPending(false);
}
}, [
cameraActions,
Expand All @@ -481,9 +492,11 @@ export const useMedia = () => {
startUserMedia,
stopUserMedia,
]);

const toggleMic = useCallback(async () => {
if (!mediaService) return console.log('Media service not intialised');
if (!micDeviceId) return requestCameraPermission();
if (!micDeviceId) return requestMicPermission();
setMicPending(true);
try {
if (micOn) {
await stopUserMedia('mic');
Expand All @@ -493,13 +506,15 @@ export const useMedia = () => {
micActions.toggle();
} catch (error) {
console.log(error);
} finally {
setMicPending(false);
}
}, [
micActions,
micDeviceId,
micOn,
mediaService,
requestCameraPermission,
requestMicPermission,
startUserMedia,
stopUserMedia,
]);
Expand Down Expand Up @@ -557,6 +572,8 @@ export const useMedia = () => {
toggleCamera,
toggleMic,
toggleScreen,
micPending,
cameraPending,
qualityManager,
setConsumerQuality: qualityManager?.setConsumerQuality.bind(qualityManager),
};
Expand Down
Loading