feat: 声网插件 正在发言指示器

pull/64/head
moonrailgun 3 years ago
parent 141db8f1cf
commit 076c907a05

@ -53,6 +53,7 @@ export {
useGroupPanelInfo, useGroupPanelInfo,
sendMessage, sendMessage,
showMessageTime, showMessageTime,
joinArray,
} from 'tailchat-shared'; } from 'tailchat-shared';
export { useLocation, useNavigate } from 'react-router'; export { useLocation, useNavigate } from 'react-router';

@ -164,6 +164,8 @@ declare module '@capital/common' {
export const showMessageTime: any; export const showMessageTime: any;
export const joinArray: any;
export const useLocation: any; export const useLocation: any;
export const useNavigate: any; export const useNavigate: any;
@ -244,21 +246,6 @@ declare module '@capital/common' {
export const pluginPanelActions: any; export const pluginPanelActions: any;
interface BasePluginPanelActionProps {
/**
*
*/
name: string;
/**
*
*/
label: string;
/**
* iconify
*/
icon: string;
}
export const regPluginPanelAction: ( export const regPluginPanelAction: (
action: action:
| { | {
@ -311,6 +298,10 @@ declare module '@capital/common' {
export const regPluginGroupTextPanelExtraMenu: any; export const regPluginGroupTextPanelExtraMenu: any;
export const pluginUserExtraInfo: any;
export const regUserExtraInfo: any;
export const useGroupIdContext: () => string; export const useGroupIdContext: () => string;
export const useGroupPanelContext: () => { export const useGroupPanelContext: () => {
@ -488,4 +479,8 @@ declare module '@capital/component' {
}>; }>;
export const Markdown: any; export const Markdown: any;
export const Webview: any;
export const WebviewKeepAlive: any;
} }

@ -65,6 +65,15 @@ export const MeetingView: React.FC<MeetingViewProps> = React.memo((props) => {
useMeetingStore.getState().removeUser(user); useMeetingStore.getState().removeUser(user);
}); });
client.on('volume-indicator', (volumes) => {
useMeetingStore.setState({
volumes: volumes.map((v) => ({
uid: String(v.uid),
level: v.level,
})),
});
});
try { try {
const { _id } = await getJWTUserInfo(); const { _id } = await getJWTUserInfo();
const { data } = await request.post('generateJoinInfo', { const { data } = await request.post('generateJoinInfo', {
@ -75,6 +84,7 @@ export const MeetingView: React.FC<MeetingViewProps> = React.memo((props) => {
await client.join(appId, channelName, token, _id); await client.join(appId, channelName, token, _id);
await client.enableDualStream(); await client.enableDualStream();
client.enableAudioVolumeIndicator();
setStart(true); setStart(true);
} catch (err) { } catch (err) {
showErrorToasts(err); showErrorToasts(err);

@ -0,0 +1,23 @@
import React from 'react';
import { joinArray } from '@capital/common';
import { useMeetingStore } from './store';
import { UserName } from '@capital/component';
import { Translate } from '../translate';
export const SpeakerNames: React.FC = React.memo(() => {
const volumes = useMeetingStore((state) => state.volumes);
const activeUserNames = volumes
.filter((v) => v.level >= 60)
.map((v) => <UserName key={v.uid} userId={v.uid} />);
return (
<span>
<span>{joinArray(activeUserNames, ',')}</span>
{activeUserNames.length > 0
? ' ' + Translate.isSpeaking
: Translate.nomanSpeaking}
</span>
);
});
SpeakerNames.displayName = 'SpeakerNames';

@ -5,7 +5,9 @@ import styled from 'styled-components';
import { useClient, useMicrophoneAndCameraTracks } from './client'; import { useClient, useMicrophoneAndCameraTracks } from './client';
import { useMeetingStore } from './store'; import { useMeetingStore } from './store';
const Root = styled.div` const Root = styled.div<{
active?: boolean;
}>`
width: 95%; width: 95%;
height: auto; height: auto;
position: relative; position: relative;
@ -19,6 +21,10 @@ const Root = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-width: 3px;
border-color: ${(props) => (props.active ? '#7ab157;' : 'transparent')};
transition: border-color 0.2s;
.player { .player {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -36,9 +42,10 @@ export const VideoView: React.FC<{
user: IAgoraRTCRemoteUser; user: IAgoraRTCRemoteUser;
}> = (props) => { }> = (props) => {
const user = props.user; const user = props.user;
const active = useVolumeActive(String(user.uid));
return ( return (
<Root> <Root active={active}>
{user.hasVideo ? ( {user.hasVideo ? (
<AgoraVideoPlayer className="player" videoTrack={user.videoTrack} /> <AgoraVideoPlayer className="player" videoTrack={user.videoTrack} />
) : ( ) : (
@ -55,9 +62,10 @@ export const OwnVideoView: React.FC<{}> = React.memo(() => {
const { ready, tracks } = useMicrophoneAndCameraTracks(); const { ready, tracks } = useMicrophoneAndCameraTracks();
const client = useClient(); const client = useClient();
const mediaPerm = useMeetingStore((state) => state.mediaPerm); const mediaPerm = useMeetingStore((state) => state.mediaPerm);
const active = useVolumeActive(String(client.uid));
return ( return (
<Root> <Root active={active}>
{ready && mediaPerm.video ? ( {ready && mediaPerm.video ? (
<AgoraVideoPlayer className="player" videoTrack={tracks[1]} /> <AgoraVideoPlayer className="player" videoTrack={tracks[1]} />
) : ( ) : (
@ -69,3 +77,12 @@ export const OwnVideoView: React.FC<{}> = React.memo(() => {
); );
}); });
OwnVideoView.displayName = 'OwnVideoView'; OwnVideoView.displayName = 'OwnVideoView';
function useVolumeActive(uid: string) {
const volume = useMeetingStore((state) =>
state.volumes.find((v) => v.uid === uid)
);
const volumeLevel = volume?.level ?? 0;
return volumeLevel >= 60;
}

@ -15,6 +15,16 @@ interface MeetingState {
* *
*/ */
mediaPerm: MediaPerm; mediaPerm: MediaPerm;
/**
*
*/
volumes: {
/**
* , 0~100, 60.
*/
level: number;
uid: string;
}[];
appendUser: (user: IAgoraRTCRemoteUser) => void; appendUser: (user: IAgoraRTCRemoteUser) => void;
removeUser: (user: IAgoraRTCRemoteUser) => void; removeUser: (user: IAgoraRTCRemoteUser) => void;
/** /**
@ -29,6 +39,7 @@ interface MeetingState {
export const useMeetingStore = create<MeetingState>((set) => ({ export const useMeetingStore = create<MeetingState>((set) => ({
users: [], users: [],
mediaPerm: { video: false, audio: false }, mediaPerm: { video: false, audio: false },
volumes: [],
appendUser: (user: IAgoraRTCRemoteUser) => { appendUser: (user: IAgoraRTCRemoteUser) => {
set((state) => ({ set((state) => ({
users: [...state.users, user], users: [...state.users, user],

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ErrorBoundary } from '@capital/component'; import { ErrorBoundary } from '@capital/component';
import { MeetingView, MeetingViewProps } from './MeetingView'; import { MeetingView, MeetingViewProps } from './MeetingView';
import { SpeakerNames } from './SpeakerNames';
const FloatWindow = styled.div` const FloatWindow = styled.div`
z-index: 100; z-index: 100;
@ -17,18 +18,23 @@ const FloatWindow = styled.div`
flex-direction: column; flex-direction: column;
.folder-btn { .folder-btn {
background-color: var(--tc-content-background-color);
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
position: absolute; position: absolute;
bottom: -30px; bottom: -30px;
height: 30px; left: 0;
line-height: 30px; right: 0;
left: 50%; display: flex;
width: 60px;
margin-left: -30px; > div {
text-align: center; background-color: var(--tc-content-background-color);
cursor: pointer; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
border-radius: 0 0 3px 3px; height: 30px;
line-height: 30px;
cursor: pointer;
border-radius: 0 0 3px 3px;
margin: auto;
display: inline-block;
padding: 0 8px;
}
} }
`; `;
@ -50,7 +56,10 @@ export const FloatMeetingWindow: React.FC<MeetingViewProps> = React.memo(
</ErrorBoundary> </ErrorBoundary>
<div className="folder-btn" onClick={() => setFolder(!folder)}> <div className="folder-btn" onClick={() => setFolder(!folder)}>
{folder ? '展开' : '收起'} <div>
<SpeakerNames />
<span style={{ marginLeft: 4 }}>{folder ? '展开' : '收起'}</span>
</div>
</div> </div>
</FloatWindow> </FloatWindow>
); );

@ -9,4 +9,12 @@ export const Translate = {
'zh-CN': '下行网络', 'zh-CN': '下行网络',
'en-US': 'Downlink', 'en-US': 'Downlink',
}), }),
isSpeaking: localTrans({
'zh-CN': '正在发言',
'en-US': 'is Speaking',
}),
nomanSpeaking: localTrans({
'zh-CN': '无人发言',
'en-US': 'No one Speaking',
}),
}; };

@ -164,6 +164,8 @@ declare module '@capital/common' {
export const showMessageTime: any; export const showMessageTime: any;
export const joinArray: any;
export const useLocation: any; export const useLocation: any;
export const useNavigate: any; export const useNavigate: any;
@ -244,21 +246,6 @@ declare module '@capital/common' {
export const pluginPanelActions: any; export const pluginPanelActions: any;
interface BasePluginPanelActionProps {
/**
*
*/
name: string;
/**
*
*/
label: string;
/**
* iconify
*/
icon: string;
}
export const regPluginPanelAction: ( export const regPluginPanelAction: (
action: action:
| { | {
@ -311,6 +298,10 @@ declare module '@capital/common' {
export const regPluginGroupTextPanelExtraMenu: any; export const regPluginGroupTextPanelExtraMenu: any;
export const pluginUserExtraInfo: any;
export const regUserExtraInfo: any;
export const useGroupIdContext: () => string; export const useGroupIdContext: () => string;
export const useGroupPanelContext: () => { export const useGroupPanelContext: () => {
@ -474,12 +465,22 @@ declare module '@capital/component' {
export const ErrorBoundary: any; export const ErrorBoundary: any;
export const UserAvatar: any; export const UserAvatar: React.FC<{
userId: string;
className?: string;
style?: React.CSSProperties;
size?: 'large' | 'small' | 'default' | number;
}>;
export const UserName: React.FC<{ export const UserName: React.FC<{
userId: string; userId: string;
className?: string; className?: string;
style?: React.CSSProperties;
}>; }>;
export const Markdown: any; export const Markdown: any;
export const Webview: any;
export const WebviewKeepAlive: any;
} }

Loading…
Cancel
Save