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 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<{
|
const Root = styled.div`
|
||||||
users: IAgoraRTCRemoteUser[];
|
height: 70vh;
|
||||||
}> = React.memo((props) => {
|
/* align-self: flex-start; */
|
||||||
const { users } = props;
|
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 (
|
return (
|
||||||
<div>
|
<Root>
|
||||||
<div className="videos">
|
|
||||||
{/* AgoraVideoPlayer component takes in the video track to render the stream,
|
{/* AgoraVideoPlayer component takes in the video track to render the stream,
|
||||||
you can pass in other props that get passed to the rendered div */}
|
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.length > 0 &&
|
||||||
users.map((user) => {
|
users.map((user) => {
|
||||||
if (user.videoTrack) {
|
return <VideoView key={user.uid} user={user} />;
|
||||||
return (
|
|
||||||
<AgoraVideoPlayer
|
|
||||||
className="vid"
|
|
||||||
videoTrack={user.videoTrack}
|
|
||||||
key={user.uid}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</Root>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
Videos.displayName = 'Videos';
|
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