mirror of https://github.com/msgbyte/tailchat
parent
928f1a25b2
commit
feab2c240c
@ -0,0 +1,107 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { getJWTUserInfo, isValidStr, showErrorToasts } from '@capital/common';
|
||||
import type { IAgoraRTCRemoteUser } from 'agora-rtc-react';
|
||||
import { useClient } from './client';
|
||||
import { Videos } from './Videos';
|
||||
import { Controls } from './Controls';
|
||||
import { LoadingSpinner } from '@capital/component';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { request } from '../request';
|
||||
import styled from 'styled-components';
|
||||
import { useMeetingStore } from './store';
|
||||
|
||||
const Root = styled.div`
|
||||
.body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.controller {
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
|
||||
* + * {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface MeetingViewProps {
|
||||
meetingId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
export const MeetingView: React.FC<MeetingViewProps> = React.memo((props) => {
|
||||
const client = useClient();
|
||||
const channelName = props.meetingId;
|
||||
const [start, setStart] = useState<boolean>(false);
|
||||
const initedRef = useRef(false);
|
||||
|
||||
const init = useMemoizedFn(async (channelName: string) => {
|
||||
client.on('user-published', async (user, mediaType) => {
|
||||
await client.subscribe(user, mediaType);
|
||||
console.log('subscribe success');
|
||||
if (mediaType === 'audio') {
|
||||
user.audioTrack?.play();
|
||||
}
|
||||
|
||||
useMeetingStore.getState().updateUserInfo(user);
|
||||
});
|
||||
|
||||
client.on('user-unpublished', async (user, mediaType) => {
|
||||
console.log('unpublished', user, mediaType);
|
||||
await client.unsubscribe(user, mediaType);
|
||||
if (mediaType === 'audio') {
|
||||
user.audioTrack?.stop();
|
||||
}
|
||||
useMeetingStore.getState().updateUserInfo(user);
|
||||
});
|
||||
|
||||
client.on('user-joined', (user) => {
|
||||
console.log('user-joined', user);
|
||||
useMeetingStore.getState().appendUser(user);
|
||||
});
|
||||
|
||||
client.on('user-left', (user) => {
|
||||
console.log('user-left', user);
|
||||
useMeetingStore.getState().removeUser(user);
|
||||
});
|
||||
|
||||
try {
|
||||
const { _id } = await getJWTUserInfo();
|
||||
const { data } = await request.post('generateJoinInfo', {
|
||||
channelName,
|
||||
});
|
||||
|
||||
const { appId, token } = data ?? {};
|
||||
|
||||
await client.join(appId, channelName, token, _id);
|
||||
console.log('client.remoteUsers', client.remoteUsers);
|
||||
setStart(true);
|
||||
} catch (err) {
|
||||
showErrorToasts(err);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (initedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValidStr(channelName)) {
|
||||
init(channelName);
|
||||
initedRef.current = true;
|
||||
}
|
||||
}, [channelName]);
|
||||
|
||||
return (
|
||||
<Root>
|
||||
<div className="body">
|
||||
{start ? <Videos /> : <LoadingSpinner tip={'正在加入通话...'} />}
|
||||
</div>
|
||||
|
||||
<div className="controller">
|
||||
<Controls onClose={props.onClose} />
|
||||
</div>
|
||||
</Root>
|
||||
);
|
||||
});
|
||||
MeetingView.displayName = 'MeetingView';
|
@ -0,0 +1,44 @@
|
||||
import { UserName } from '@capital/component';
|
||||
import { AgoraVideoPlayer, IAgoraRTCRemoteUser } from 'agora-rtc-react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Root = styled.div`
|
||||
width: 95%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
background-color: #333;
|
||||
border-radius: 10px;
|
||||
aspect-ratio: 16/9;
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
|
||||
.player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.name {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const VideoView: React.FC<{
|
||||
user: IAgoraRTCRemoteUser;
|
||||
}> = (props) => {
|
||||
const user = props.user;
|
||||
|
||||
return (
|
||||
<Root>
|
||||
{user.hasVideo && (
|
||||
<AgoraVideoPlayer className="player" videoTrack={user.videoTrack} />
|
||||
)}
|
||||
|
||||
<UserName className="name" userId={String(user.uid)} />
|
||||
</Root>
|
||||
);
|
||||
};
|
||||
VideoView.displayName = 'VideoView';
|
@ -1,34 +1,32 @@
|
||||
import { AgoraVideoPlayer, IAgoraRTCRemoteUser } from 'agora-rtc-react';
|
||||
import { AgoraVideoPlayer } from 'agora-rtc-react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useMicrophoneAndCameraTracks } from './client';
|
||||
import { useMeetingStore } from './store';
|
||||
import { VideoView } from './VideoView';
|
||||
|
||||
export const Videos: React.FC<{
|
||||
users: IAgoraRTCRemoteUser[];
|
||||
}> = React.memo((props) => {
|
||||
const { users } = props;
|
||||
const Root = styled.div`
|
||||
height: 70vh;
|
||||
/* align-self: flex-start; */
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(440px, 1fr));
|
||||
`;
|
||||
|
||||
export const Videos: React.FC = React.memo(() => {
|
||||
const users = useMeetingStore((state) => state.users);
|
||||
const { ready, tracks } = useMicrophoneAndCameraTracks();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="videos">
|
||||
{/* AgoraVideoPlayer component takes in the video track to render the stream,
|
||||
<Root>
|
||||
{/* AgoraVideoPlayer component takes in the video track to render the stream,
|
||||
you can pass in other props that get passed to the rendered div */}
|
||||
<AgoraVideoPlayer className="vid" videoTrack={tracks[1]} />
|
||||
{ready && <AgoraVideoPlayer className="vid" videoTrack={tracks[1]} />}
|
||||
|
||||
{users.length > 0 &&
|
||||
users.map((user) => {
|
||||
if (user.videoTrack) {
|
||||
return (
|
||||
<AgoraVideoPlayer
|
||||
className="vid"
|
||||
videoTrack={user.videoTrack}
|
||||
key={user.uid}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{users.length > 0 &&
|
||||
users.map((user) => {
|
||||
return <VideoView key={user.uid} user={user} />;
|
||||
})}
|
||||
</Root>
|
||||
);
|
||||
});
|
||||
Videos.displayName = 'Videos';
|
||||
|
@ -0,0 +1,50 @@
|
||||
import type { IAgoraRTCRemoteUser } from 'agora-rtc-react';
|
||||
import create from 'zustand';
|
||||
|
||||
interface MeetingState {
|
||||
/**
|
||||
* 本次会议用户列表
|
||||
*/
|
||||
users: IAgoraRTCRemoteUser[];
|
||||
appendUser: (user: IAgoraRTCRemoteUser) => void;
|
||||
removeUser: (user: IAgoraRTCRemoteUser) => void;
|
||||
clearUser: () => void;
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
updateUserInfo: (user: IAgoraRTCRemoteUser) => void;
|
||||
}
|
||||
|
||||
export const useMeetingStore = create<MeetingState>((set) => ({
|
||||
users: [],
|
||||
appendUser: (user: IAgoraRTCRemoteUser) => {
|
||||
set((state) => ({
|
||||
users: [...state.users, user],
|
||||
}));
|
||||
},
|
||||
removeUser: (user: IAgoraRTCRemoteUser) => {
|
||||
set((state) => {
|
||||
return {
|
||||
users: state.users.filter((_u) => _u.uid !== user.uid),
|
||||
};
|
||||
});
|
||||
},
|
||||
clearUser: () => {
|
||||
set({ users: [] });
|
||||
},
|
||||
updateUserInfo: (user: IAgoraRTCRemoteUser) => {
|
||||
set((state) => {
|
||||
const users = [...state.users];
|
||||
const targetUserIndex = state.users.findIndex((u) => u.uid === user.uid);
|
||||
if (targetUserIndex === -1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
users[targetUserIndex] = user;
|
||||
|
||||
return {
|
||||
users,
|
||||
};
|
||||
});
|
||||
},
|
||||
}));
|
@ -0,0 +1,3 @@
|
||||
import { createPluginRequest } from '@capital/common';
|
||||
|
||||
export const request = createPluginRequest('com.msgbyte.agora');
|
Loading…
Reference in New Issue