feat: 增加了声网插件用户布局与基于用户维度的视图渲染

并重新封装抽象了组件,并修复了错误的错误边界组件的捕获
pull/64/head
moonrailgun 2 years ago
parent 928f1a25b2
commit feab2c240c

@ -971,6 +971,7 @@ importers:
ahooks: ^3.7.4
react: 18.2.0
styled-components: ^5.3.6
zustand: ^4.1.5
dependencies:
agora-rtc-react: 1.1.3_react@18.2.0
ahooks: 3.7.4_react@18.2.0
@ -978,6 +979,7 @@ importers:
'@types/styled-components': 5.1.26
react: 18.2.0
styled-components: 5.3.6_react@18.2.0
zustand: 4.1.5_react@18.2.0
server/plugins/com.msgbyte.github:
specifiers:
@ -36356,5 +36358,21 @@ packages:
use-sync-external-store: 1.2.0_react@18.2.0
dev: false
/zustand/4.1.5_react@18.2.0:
resolution: {integrity: sha512-PsdRT8Bvq22Yyh1tvpgdHNE7OAeFKqJXUxtJvj1Ixw2B9O2YZ1M34ImQ+xyZah4wZrR4lENMoDUutKPpyXCQ/Q==}
engines: {node: '>=12.7.0'}
peerDependencies:
immer: '>=9.0'
react: '>=16.8'
peerDependenciesMeta:
immer:
optional: true
react:
optional: true
dependencies:
react: 18.2.0
use-sync-external-store: 1.2.0_react@18.2.0
dev: true
/zwitch/1.0.5:
resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}

@ -81,11 +81,9 @@ class AgoraService extends TcService {
}
this.registerLocalDb(require('../models/agora-meeting').default);
this.registerAction('generateToken', this.generateToken, {
this.registerAction('generateJoinInfo', this.generateJoinInfo, {
params: {
channelName: 'string',
appId: { type: 'string', optional: true },
appCert: { type: 'string', optional: true },
},
});
this.registerAction('getChannelUserList', this.getChannelUserList, {
@ -104,18 +102,14 @@ class AgoraService extends TcService {
});
}
generateToken(
generateJoinInfo(
ctx: TcContext<{
channelName: string;
appId?: string;
appCert?: string;
}>
) {
const {
channelName,
appId = this.serverAppId,
appCert = this.serverAppCertificate,
} = ctx.params;
const { channelName } = ctx.params;
const appId = this.serverAppId;
const appCert = this.serverAppCertificate;
if (!appId || !appCert) {
throw new Error('Agora.io AppId/AppCert not init');
@ -139,7 +133,7 @@ class AgoraService extends TcService {
privilegeExpirationInSecond
);
return token;
return { appId, token };
}
/**

@ -14,6 +14,7 @@
"devDependencies": {
"@types/styled-components": "^5.1.26",
"react": "18.2.0",
"styled-components": "^5.3.6"
"styled-components": "^5.3.6",
"zustand": "^4.1.5"
}
}

@ -1,6 +1,7 @@
import { IconBtn } from '@capital/component';
import React, { useState } from 'react';
import { useClient, useMicrophoneAndCameraTracks } from './client';
import { useMeetingStore } from './store';
export const Controls: React.FC<{
onClose: () => void;
@ -38,6 +39,7 @@ export const Controls: React.FC<{
const leaveChannel = async () => {
await client.leave();
client.removeAllListeners();
useMeetingStore.getState().clearUser();
// we close the tracks to perform cleanup
tracks[0].close();
tracks[1].close();

@ -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';

@ -9,9 +9,5 @@ const config: ClientConfig = {
codec: 'vp8',
};
// TODO 应该从本地设置或者远程中获取
export const appId = ''; //ENTER APP ID HERE
export const token = '';
export const useClient = createClient(config);
export const useMicrophoneAndCameraTracks = createMicrophoneAndCameraTracks();

@ -1,5 +1,5 @@
import { showToasts } from '@capital/common';
import { PortalAdd, PortalRemove, ErrorBoundary } from '@capital/component';
import { PortalAdd, PortalRemove } from '@capital/component';
import React from 'react';
import { FloatMeetingWindow } from './window';
@ -21,14 +21,12 @@ export function startFastMeeting(meetingId: string) {
currentMeeting = meetingId;
const key = PortalAdd(
<ErrorBoundary>
<FloatMeetingWindow
meetingId={meetingId}
onClose={() => {
PortalRemove(key);
currentMeeting = null;
}}
/>
</ErrorBoundary>
<FloatMeetingWindow
meetingId={meetingId}
onClose={() => {
PortalRemove(key);
currentMeeting = null;
}}
/>
);
}

@ -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,
};
});
},
}));

@ -1,12 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { getJWTUserInfo, isValidStr, showErrorToasts } from '@capital/common';
import type { IAgoraRTCRemoteUser } from 'agora-rtc-react';
import React, { useState } from 'react';
import styled from 'styled-components';
import { appId, token, useClient } from './client';
import { Videos } from './Videos';
import { Controls } from './Controls';
import { LoadingSpinner } from '@capital/component';
import { useMemoizedFn } from 'ahooks';
import { ErrorBoundary } from '@capital/component';
import { MeetingView, MeetingViewProps } from './MeetingView';
const FloatWindow = styled.div`
z-index: 100;
@ -21,38 +16,6 @@ const FloatWindow = styled.div`
display: flex;
flex-direction: column;
.body {
flex: 1;
.videos {
height: 70vh;
align-self: flex-start;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(440px, 1fr));
justify-items: center;
align-items: center;
.vid {
height: 95%;
width: 95%;
position: relative;
background-color: black;
border-width: 1px;
border-color: #38373a;
border-style: solid;
}
}
}
.controller {
text-align: center;
padding: 10px 0;
* + * {
margin-left: 10px;
}
}
.folder-btn {
background-color: var(--tc-content-background-color);
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
@ -72,92 +35,25 @@ const FloatWindow = styled.div`
/**
*
*/
export const FloatMeetingWindow: React.FC<{
meetingId: string;
onClose: () => void;
}> = React.memo((props) => {
const [folder, setFolder] = useState(false);
const client = useClient();
const channelName = props.meetingId;
const [users, setUsers] = useState<IAgoraRTCRemoteUser[]>([]);
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 === 'video') {
setUsers((prevUsers) => {
return [...prevUsers, user];
});
}
if (mediaType === 'audio') {
user.audioTrack?.play();
}
});
client.on('user-unpublished', (user, type) => {
console.log('unpublished', user, type);
if (type === 'audio') {
user.audioTrack?.stop();
}
if (type === 'video') {
setUsers((prevUsers) => {
return prevUsers.filter((User) => User.uid !== user.uid);
});
}
});
client.on('user-left', (user) => {
console.log('leaving', user);
setUsers((prevUsers) => {
return prevUsers.filter((User) => User.uid !== user.uid);
});
});
const { _id } = await getJWTUserInfo();
try {
await client.join(appId, channelName, token, _id);
setStart(true);
} catch (err) {
showErrorToasts(err);
}
});
useEffect(() => {
if (initedRef.current) {
return;
}
if (isValidStr(channelName)) {
init(channelName);
initedRef.current = true;
}
}, [channelName]);
return (
<FloatWindow
style={{
transform: folder ? 'translateY(-100%)' : 'none',
}}
>
<div className="body">
{start ? (
<Videos users={users} />
) : (
<LoadingSpinner tip={'正在加入通话...'} />
)}
</div>
<div className="controller">
<Controls onClose={props.onClose} />
</div>
<div className="folder-btn" onClick={() => setFolder(!folder)}>
{folder ? '展开' : '收起'}
</div>
</FloatWindow>
);
});
export const FloatMeetingWindow: React.FC<MeetingViewProps> = React.memo(
(props) => {
const [folder, setFolder] = useState(false);
return (
<FloatWindow
style={{
transform: folder ? 'translateY(-100%)' : 'none',
}}
>
<ErrorBoundary>
<MeetingView {...props} />
</ErrorBoundary>
<div className="folder-btn" onClick={() => setFolder(!folder)}>
{folder ? '展开' : '收起'}
</div>
</FloatWindow>
);
}
);
FloatMeetingWindow.displayName = 'FloatMeetingWindow';

@ -0,0 +1,3 @@
import { createPluginRequest } from '@capital/common';
export const request = createPluginRequest('com.msgbyte.agora');
Loading…
Cancel
Save