diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a987d5d..d98d02be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==} diff --git a/server/plugins/com.msgbyte.agora/services/agora.service.dev.ts b/server/plugins/com.msgbyte.agora/services/agora.service.dev.ts index b2981a42..0a9ed249 100644 --- a/server/plugins/com.msgbyte.agora/services/agora.service.dev.ts +++ b/server/plugins/com.msgbyte.agora/services/agora.service.dev.ts @@ -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 }; } /** diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/package.json b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/package.json index 53cee1a2..21254933 100644 --- a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/package.json +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/package.json @@ -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" } } diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Controls.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Controls.tsx index cc84f9f3..934f74e3 100644 --- a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Controls.tsx +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Controls.tsx @@ -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(); diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/MeetingView.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/MeetingView.tsx new file mode 100644 index 00000000..b0dff7e9 --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/MeetingView.tsx @@ -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 = React.memo((props) => { + const client = useClient(); + const channelName = props.meetingId; + const [start, setStart] = useState(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 ( + +
+ {start ? : } +
+ +
+ +
+
+ ); +}); +MeetingView.displayName = 'MeetingView'; diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/VideoView.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/VideoView.tsx new file mode 100644 index 00000000..fb368b9d --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/VideoView.tsx @@ -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 ( + + {user.hasVideo && ( + + )} + + + + ); +}; +VideoView.displayName = 'VideoView'; diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Videos.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Videos.tsx index 8f0edc93..8df06b2f 100644 --- a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Videos.tsx +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/Videos.tsx @@ -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 ( -
-
- {/* 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 */} - + {ready && } - {users.length > 0 && - users.map((user) => { - if (user.videoTrack) { - return ( - - ); - } else { - return null; - } - })} -
-
+ {users.length > 0 && + users.map((user) => { + return ; + })} + ); }); Videos.displayName = 'Videos'; diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/client.ts b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/client.ts index 9848a126..fe2a3a45 100644 --- a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/client.ts +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/client.ts @@ -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(); diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/index.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/index.tsx index 30850def..4ea58bc1 100644 --- a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/index.tsx +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/index.tsx @@ -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( - - { - PortalRemove(key); - currentMeeting = null; - }} - /> - + { + PortalRemove(key); + currentMeeting = null; + }} + /> ); } diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/store.ts b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/store.ts new file mode 100644 index 00000000..5b8a279d --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/store.ts @@ -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((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, + }; + }); + }, +})); diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/window.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/window.tsx index e9e450c6..4103b140 100644 --- a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/window.tsx +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/window.tsx @@ -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([]); - const [start, setStart] = useState(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 ( - -
- {start ? ( - - ) : ( - - )} -
- -
- -
- -
setFolder(!folder)}> - {folder ? '展开' : '收起'} -
-
- ); -}); +export const FloatMeetingWindow: React.FC = React.memo( + (props) => { + const [folder, setFolder] = useState(false); + + return ( + + + + + +
setFolder(!folder)}> + {folder ? '展开' : '收起'} +
+
+ ); + } +); FloatMeetingWindow.displayName = 'FloatMeetingWindow'; diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/request.ts b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/request.ts new file mode 100644 index 00000000..4809debc --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/request.ts @@ -0,0 +1,3 @@ +import { createPluginRequest } from '@capital/common'; + +export const request = createPluginRequest('com.msgbyte.agora');