feat: video call
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="#344054" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.28034 2.21968C2.98745 1.92678 2.51257 1.92677 2.21968 2.21966C1.92678 2.51255 1.92677 2.98743 2.21966 3.28032L3.78662 4.84731C2.72676 5.38269 2 6.48147 2 7.75V16.25C2 18.0449 3.45507 19.5 5.25 19.5H12.75C14.2943 19.5 15.5871 18.4229 15.9179 16.9789L20.7194 21.7805C21.0123 22.0734 21.4872 22.0734 21.7801 21.7805C22.073 21.4876 22.073 21.0127 21.7801 20.7198L3.28034 2.21968ZM17 13.8182L21.5042 18.3226C21.8008 18.1491 22 17.8272 22 17.4588V6.54096C22 6.30368 21.9156 6.07414 21.762 5.89334C21.4043 5.47252 20.7732 5.42132 20.3524 5.77899L17 8.62794V13.8182ZM7.68194 4.5L16 12.8182V7.75C16 5.95508 14.5449 4.5 12.75 4.5H7.68194Z"/>
|
||||
<path d="M2.21968 2.21966C2.51257 1.92677 2.98745 1.92678 3.28034 2.21968L21.7801 20.7198C22.073 21.0127 22.073 21.4876 21.7801 21.7805C21.4872 22.0734 21.0123 22.0734 20.7194 21.7805L2.21966 3.28032C1.92677 2.98743 1.92678 2.51255 2.21968 2.21966Z" fill="#D92D20"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1017 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="#1D2939" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 16.25C16 18.0449 14.5449 19.5 12.75 19.5H5.25C3.45507 19.5 2 18.0449 2 16.25V7.75C2 5.95507 3.45507 4.5 5.25 4.5H12.75C14.5449 4.5 16 5.95507 16 7.75V16.25ZM21.762 5.89334C21.9156 6.07414 22 6.30368 22 6.54096V17.4588C22 18.0111 21.5523 18.4588 21 18.4588C20.7627 18.4588 20.5332 18.3744 20.3524 18.2208L17 15.3709V8.62794L20.3524 5.77899C20.7732 5.42132 21.4043 5.47252 21.762 5.89334Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 509 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="#344054" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.58301 4C3.34037 4 2.33301 5.00736 2.33301 6.25V17.75C2.33301 18.9926 3.34037 20 4.58301 20H20.083C21.3256 20 22.333 18.9926 22.333 17.75V6.25C22.333 5.00736 21.3256 4 20.083 4H4.58301ZM12.8633 7.46515L16.1178 10.7197C16.4107 11.0126 16.4107 11.4874 16.1178 11.7803C15.825 12.0732 15.3501 12.0732 15.0572 11.7803L13.083 9.80614V16.2528C13.083 16.6671 12.7472 17.0028 12.333 17.0028C11.9188 17.0028 11.583 16.6671 11.583 16.2528V9.80793L9.61371 11.78C9.32102 12.0731 8.84614 12.0734 8.55305 11.7807C8.25995 11.488 8.25962 11.0131 8.55231 10.72L11.8023 7.46552C11.9429 7.32471 12.1337 7.24555 12.3327 7.24548C12.5317 7.24541 12.7226 7.32444 12.8633 7.46515Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 777 B |
@@ -44,14 +44,16 @@ const Voice = () => {
|
||||
if (mediaType == "audio") {
|
||||
// 播放远端音频
|
||||
user.audioTrack?.play();
|
||||
// const playing= user.audioTrack?.isPlaying;
|
||||
// const level = user.audioTrack?.getVolumeLevel();
|
||||
// if (level === 0) {
|
||||
// // 远端静音
|
||||
// dispatch(updateVoicingMember({ uid: +user.uid, info: { muted: true } }));
|
||||
// }
|
||||
window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack;
|
||||
}
|
||||
if (mediaType == "video") {
|
||||
const id = `CAMERA_${user.uid}`;
|
||||
console.log("video container id", id);
|
||||
|
||||
// 播放远端视频
|
||||
user.videoTrack?.play(id);
|
||||
window.VOICE_TRACK_MAP[+user.uid] = user.videoTrack;
|
||||
}
|
||||
agoraEngine.on("user-unpublished", (user) => {
|
||||
// 远端用户取消了音频(muted)
|
||||
dispatch(updateVoicingMember({ uid: +user.uid, info: { muted: true } }));
|
||||
@@ -102,8 +104,6 @@ const Voice = () => {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// const { downlinkNetworkQuality } = qlt;
|
||||
// dispatch(updateVoicingNetworkQuality(downlinkNetworkQuality));
|
||||
});
|
||||
});
|
||||
// 有新用户加入
|
||||
@@ -127,13 +127,6 @@ const Voice = () => {
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handlePageUnload, { capture: true });
|
||||
// if (window.VOICE_CLIENT && localTrack) {
|
||||
// localTrack.close();
|
||||
// localTrack = null;
|
||||
// window.VOICE_CLIENT.leave();
|
||||
// dispatch(updateVoicingInfo(null));
|
||||
// }
|
||||
// window.VOICE_CLIENT=null
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -148,9 +141,9 @@ type VoiceProps = {
|
||||
const audioJoin = new Audio(AudioJoin);
|
||||
const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { voicingInfo } = useAppSelector(store => {
|
||||
const { voicingInfo, loginUid } = useAppSelector(store => {
|
||||
return {
|
||||
// loginUid: store.authData.user?.uid,
|
||||
loginUid: store.authData.user?.uid,
|
||||
voicingInfo: store.voice.voicing
|
||||
};
|
||||
});
|
||||
@@ -197,6 +190,25 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
}
|
||||
// setJoining(false);
|
||||
};
|
||||
const openCamera = async () => {
|
||||
if (localTrack) {
|
||||
localTrack.close();
|
||||
localTrack = null;
|
||||
}
|
||||
localTrack = await AgoraRTC.createCameraVideoTrack();
|
||||
await window.VOICE_CLIENT?.publish(localTrack);
|
||||
const id = `CAMERA_${loginUid}`;
|
||||
localTrack?.play(id);
|
||||
};
|
||||
const closeCamera = async () => {
|
||||
if (localTrack) {
|
||||
localTrack.close();
|
||||
localTrack = null;
|
||||
}
|
||||
localTrack = await AgoraRTC.createMicrophoneAudioTrack();
|
||||
await window.VOICE_CLIENT?.publish(localTrack);
|
||||
};
|
||||
|
||||
const leave = async () => {
|
||||
if (window.VOICE_CLIENT && localTrack) {
|
||||
localTrack.close();
|
||||
@@ -250,7 +262,9 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
joining: voicingInfo ? voicingInfo.joining : undefined,
|
||||
joinedAtThisContext,
|
||||
joined: !!voicingInfo,
|
||||
joinVoice
|
||||
joinVoice,
|
||||
openCamera,
|
||||
closeCamera
|
||||
};
|
||||
};
|
||||
export { useVoice };
|
||||
|
||||
@@ -8,6 +8,9 @@ import IconMicOff from '../../assets/icons/mic.off.svg';
|
||||
import IconCallOff from '../../assets/icons/headphone.svg';
|
||||
import IconSoundOn from '../../assets/icons/sound.on.svg';
|
||||
import IconSoundOff from '../../assets/icons/sound.off.svg';
|
||||
import IconCameraOff from '../../assets/icons/camera.off.svg';
|
||||
import IconCamera from '../../assets/icons/camera.svg';
|
||||
import IconScreen from '../../assets/icons/share.screen.svg';
|
||||
import { useVoice } from '../../common/component/Voice';
|
||||
import Signal from '../../common/component/Signal';
|
||||
import Tooltip from '../../common/component/Tooltip';
|
||||
@@ -19,7 +22,7 @@ type Props = {
|
||||
|
||||
const RTCWidget = ({ id, context = "channel" }: Props) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const { leave, voicingInfo, setMute, setDeafen, joining = true } = useVoice({ context, id });
|
||||
const { leave, voicingInfo, setMute, setDeafen, joining = true, openCamera } = useVoice({ context, id });
|
||||
const { loginUser, channelData, userData } = useAppSelector(store => {
|
||||
return {
|
||||
userData: store.users.byId,
|
||||
@@ -28,25 +31,36 @@ const RTCWidget = ({ id, context = "channel" }: Props) => {
|
||||
};
|
||||
});
|
||||
if (!loginUser || !voicingInfo || joining) return null;
|
||||
const name = voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name;
|
||||
if (!name) return null;
|
||||
// const name = voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name;
|
||||
// if (!name) return null;
|
||||
const isReConnecting = voicingInfo.connectionState == "RECONNECTING";
|
||||
return (
|
||||
<div className='bg-gray-100 dark:bg-gray-900 flex flex-col p-2 rounded-3xl m-3 mb-4 text-sm'>
|
||||
{/* {voicingInfo && */}
|
||||
<div className="flex justify-between items-center border-b border-b-gray-200 dark:border-b-gray-800 pb-2">
|
||||
<div className="flex flex-1 items-center gap-1">
|
||||
<Signal strength={voicingInfo.downlinkNetworkQuality} />
|
||||
<div className="flex flex-col">
|
||||
<span className={clsx('text-green-800 font-bold', isReConnecting && `text-red-500`)}>{isReConnecting ? `Voice Reconnecting...` : `Voice Connected`}</span>
|
||||
<span className='text-gray-600 dark:text-gray-400 text-xs truncate max-w-[170px]' >{voicingInfo.context == "channel" ? "Channel" : "DM"} / {voicingInfo.context == "channel" ? channelData[voicingInfo.id].name : userData[voicingInfo.id].name}</span>
|
||||
<div className="border-b border-b-gray-200 dark:border-b-gray-800 pb-2">
|
||||
<div className="flex justify-between items-center">
|
||||
|
||||
<div className="flex flex-1 items-center gap-1">
|
||||
<Signal strength={voicingInfo.downlinkNetworkQuality} />
|
||||
<div className="flex flex-col">
|
||||
<span className={clsx('text-green-800 font-bold', isReConnecting && `text-red-500`)}>{isReConnecting ? `Voice Reconnecting...` : `Voice Connected`}</span>
|
||||
<span className='text-gray-600 dark:text-gray-400 text-xs truncate max-w-[170px]' >{voicingInfo.context == "channel" ? "Channel" : "DM"} / {voicingInfo.context == "channel" ? channelData[voicingInfo.id].name : userData[voicingInfo.id].name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip tip={t("leave_voice")} placement="top">
|
||||
<IconCallOff onClick={leave} role="button" className="fill-red-600" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<button onClick={openCamera} className='rounded-lg py-2 px-5 bg-white/50 dark:bg-black/50 flex items-center gap-1'>
|
||||
<IconCameraOff className="fill-gray-800 dark:fill-gray-200 w-6 h-6" />
|
||||
<span className='text-sm text-gray-800 dark:text-gray-200 font-semibold'>Video</span>
|
||||
</button>
|
||||
<button className='rounded-lg py-2 px-5 bg-white/50 dark:bg-black/50 flex items-center gap-1'>
|
||||
<IconScreen className="fill-gray-800 dark:fill-gray-200 w-6 h-6" />
|
||||
<span className='text-sm text-gray-800 dark:text-gray-200 font-semibold'>Screen</span>
|
||||
</button>
|
||||
</div>
|
||||
<Tooltip tip={t("leave_voice")} placement="top">
|
||||
<IconCallOff onClick={leave} role="button" className="fill-red-600" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* } */}
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<User uid={loginUser.uid} compact />
|
||||
@@ -55,7 +69,6 @@ const RTCWidget = ({ id, context = "channel" }: Props) => {
|
||||
<span className='text-gray-400 text-xs'>#{loginUser.uid}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* {voicingInfo && */}
|
||||
<div className="flex gap-2 px-1">
|
||||
<Tooltip tip={voicingInfo.deafen ? t("undeafen") : t("deafen")} placement="top">
|
||||
{voicingInfo.deafen ? <IconSoundOff role="button" onClick={setDeafen.bind(null, false)} /> : <IconSoundOn role="button" onClick={setDeafen.bind(null, true)} />}
|
||||
@@ -67,7 +80,6 @@ const RTCWidget = ({ id, context = "channel" }: Props) => {
|
||||
<IconMic onClick={setMute.bind(null, true)} role="button" />}
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* } */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,9 @@ import IconHeadphone from '../../../assets/icons/sound.on.svg';
|
||||
import IconHeadphoneOff from '../../../assets/icons/sound.off.svg';
|
||||
import IconMic from '../../../assets/icons/mic.on.svg';
|
||||
import IconMicOff from '../../../assets/icons/mic.off.svg';
|
||||
import IconCameraOff from '../../../assets/icons/camera.off.svg';
|
||||
import IconCamera from '../../../assets/icons/camera.svg';
|
||||
import IconScreen from '../../../assets/icons/share.screen.svg';
|
||||
import StyledButton from '../../../common/component/styled/Button';
|
||||
import IconCallOff from '../../../assets/icons/call.off.svg';
|
||||
import Tooltip from '../../../common/component/Tooltip';
|
||||
@@ -55,29 +58,32 @@ const VoiceManagement = ({ info, setMute, setDeafen, leave }: Props) => {
|
||||
if (!curr) return null;
|
||||
const { muted, speakingVolume = 0 } = membersData[uid];
|
||||
const speaking = speakingVolume > 50;
|
||||
return <li key={uid} className="flex items-center justify-between gap-6 pb-4 ">
|
||||
<div className="flex items-center gap-2 transition-opacity">
|
||||
<div className={clsx("w-8 h-8 flex shrink-0 relative")}>
|
||||
{speaking && <div className="z-10 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 rounded-full bg-green-500 animate-speaking w-[36px] h-[36px]"></div>}
|
||||
<Avatar
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-full h-full rounded-full object-cover z-20"
|
||||
src={curr.avatar}
|
||||
name={curr.name}
|
||||
alt="avatar"
|
||||
/>
|
||||
return <li key={uid} className='pb-4'>
|
||||
<div className="flex items-center justify-between gap-6 pb-2">
|
||||
<div className="flex items-center gap-2 transition-opacity">
|
||||
<div className={clsx("w-8 h-8 flex shrink-0 relative")}>
|
||||
{speaking && <div className="z-10 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 rounded-full bg-green-500 animate-speaking w-[36px] h-[36px]"></div>}
|
||||
<Avatar
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-full h-full rounded-full object-cover z-20"
|
||||
src={curr.avatar}
|
||||
name={curr.name}
|
||||
alt="avatar"
|
||||
/>
|
||||
</div>
|
||||
<span className={nameClass} title={curr?.name}>
|
||||
{curr?.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* {deafen ? <IconHeadphoneOff className="w-4" /> : <IconHeadphone className="w-4" />} */}
|
||||
{muted ? <IconMicOff className="w-4 fill-gray-500 dark:fill-gray-400" /> : <IconMic className="w-4 fill-gray-500 dark:fill-gray-400" />}
|
||||
</div>
|
||||
<span className={nameClass} title={curr?.name}>
|
||||
{curr?.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* {deafen ? <IconHeadphoneOff className="w-4" /> : <IconHeadphone className="w-4" />} */}
|
||||
{muted ? <IconMicOff className="w-4 fill-gray-500 dark:fill-gray-400" /> : <IconMic className="w-4 fill-gray-500 dark:fill-gray-400" />}
|
||||
<div id={`CAMERA_${uid}`} className="w-[98%] h-[120px] rounded overflow-hidden m-auto">
|
||||
{/* camera placeholder */}
|
||||
</div>
|
||||
{/* <User uid={uid} interactive={false} /> */}
|
||||
{/* {userData[uid]?.name} */}
|
||||
</li>;
|
||||
})}
|
||||
|
||||
@@ -95,6 +101,17 @@ const VoiceManagement = ({ info, setMute, setDeafen, leave }: Props) => {
|
||||
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip tip={"Camera"} placement="top">
|
||||
<li role={"button"} className="py-2 px-3 rounded bg-gray-100 dark:bg-gray-900">
|
||||
{muted ? <IconCameraOff className="fill-gray-700 dark:fill-gray-300" /> : <IconCamera className="fill-gray-700 dark:fill-gray-300" />}
|
||||
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip tip={"Share Screen"} placement="top">
|
||||
<li role={"button"} onClick={setMute.bind(null, !muted)} className="py-2 px-3 rounded bg-gray-100 dark:bg-gray-900">
|
||||
<IconScreen className="fill-gray-700 dark:fill-gray-300" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
</ul>
|
||||
<StyledButton onClick={leave} className='bg-red-600 hover:!bg-red-700 text-center'>
|
||||
<Tooltip tip={t("leave_voice")} placement="top" offset={[0, 24]} >
|
||||
|
||||
Reference in New Issue
Block a user