feat: 增加屏幕共享功能

feat/uniplus
moonrailgun 2 years ago
parent f13478a984
commit 2747e0945e

@ -93,6 +93,7 @@ class AgoraService extends TcService {
this.registerAction('generateJoinInfo', this.generateJoinInfo, { this.registerAction('generateJoinInfo', this.generateJoinInfo, {
params: { params: {
channelName: 'string', channelName: 'string',
userId: { type: 'string', optional: true },
}, },
}); });
this.registerAction('getChannelUserList', this.getChannelUserList, { this.registerAction('getChannelUserList', this.getChannelUserList, {
@ -135,6 +136,7 @@ class AgoraService extends TcService {
generateJoinInfo( generateJoinInfo(
ctx: TcContext<{ ctx: TcContext<{
channelName: string; channelName: string;
userId?: string;
}> }>
) { ) {
const { channelName } = ctx.params; const { channelName } = ctx.params;
@ -147,7 +149,7 @@ class AgoraService extends TcService {
const role = RtcRole.PUBLISHER; const role = RtcRole.PUBLISHER;
const userId = ctx.meta.userId; const userId = ctx.params.userId ?? ctx.meta.userId;
const tokenExpirationInSecond = 3600; // 1h const tokenExpirationInSecond = 3600; // 1h
const privilegeExpirationInSecond = 3600; // 1h const privilegeExpirationInSecond = 3600; // 1h

@ -1,5 +1,6 @@
import { useAsyncFn } from '@capital/common'; import { useAsyncFn } from '@capital/common';
import { IconBtn } from '@capital/component'; import { IconBtn } from '@capital/component';
import { useMemoizedFn } from 'ahooks';
import React from 'react'; import React from 'react';
import { Translate } from '../translate'; import { Translate } from '../translate';
import { import {
@ -8,16 +9,21 @@ import {
createCameraVideoTrack, createCameraVideoTrack,
} from './client'; } from './client';
import { useMeetingStore } from './store'; import { useMeetingStore } from './store';
import { useScreenSharing } from './useScreenSharing';
import { getClientLocalTrack } from './utils'; import { getClientLocalTrack } from './utils';
/**
*
*/
export const Controls: React.FC<{ export const Controls: React.FC<{
onClose: () => void; onClose: () => void;
}> = React.memo((props) => { }> = React.memo((props) => {
const client = useClient(); const client = useClient();
const { startScreenSharing, stopScreenSharing } = useScreenSharing();
const mediaPerm = useMeetingStore((state) => state.mediaPerm); const mediaPerm = useMeetingStore((state) => state.mediaPerm);
const [{ loading }, mute] = useAsyncFn( const [{ loading }, mute] = useAsyncFn(
async (type: 'audio' | 'video') => { useMemoizedFn(async (type: 'audio' | 'video' | 'screensharing') => {
if (type === 'audio') { if (type === 'audio') {
if (mediaPerm.audio === true) { if (mediaPerm.audio === true) {
const track = getClientLocalTrack(client, 'audio'); const track = getClientLocalTrack(client, 'audio');
@ -42,9 +48,17 @@ export const Controls: React.FC<{
} }
useMeetingStore.getState().setMediaPerm({ video: !mediaPerm.video }); useMeetingStore.getState().setMediaPerm({ video: !mediaPerm.video });
} else if (type === 'screensharing') {
if (mediaPerm.screensharing === true) {
// 关闭屏幕共享
await stopScreenSharing();
} else {
// 开始屏幕共享
await startScreenSharing();
}
} }
}, }),
[client, mediaPerm] []
); );
const leaveChannel = async () => { const leaveChannel = async () => {
@ -60,6 +74,23 @@ export const Controls: React.FC<{
return ( return (
<div className="controller"> <div className="controller">
<IconBtn
icon={
mediaPerm.screensharing
? 'mdi:projector-screen-outline'
: 'mdi:projector-screen-off-outline'
}
title={
mediaPerm.screensharing
? Translate.closeScreensharing
: Translate.openScreensharing
}
active={mediaPerm.screensharing}
disabled={loading}
size="large"
onClick={() => mute('screensharing')}
/>
<IconBtn <IconBtn
icon={mediaPerm.video ? 'mdi:video' : 'mdi:video-off'} icon={mediaPerm.video ? 'mdi:video' : 'mdi:video-off'}
title={mediaPerm.video ? Translate.closeCamera : Translate.openCamera} title={mediaPerm.video ? Translate.closeCamera : Translate.openCamera}

@ -5,13 +5,13 @@ import { Videos } from './Videos';
import { Controls } from './Controls'; import { Controls } from './Controls';
import { LoadingSpinner } from '@capital/component'; import { LoadingSpinner } from '@capital/component';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { request } from '../request';
import styled from 'styled-components'; import styled from 'styled-components';
import { useMeetingStore } from './store'; import { useMeetingStore } from './store';
import { NetworkStats } from './NetworkStats'; import { NetworkStats } from './NetworkStats';
import _once from 'lodash/once'; import _once from 'lodash/once';
import type { IAgoraRTCClient } from 'agora-rtc-react'; import type { IAgoraRTCClient } from 'agora-rtc-react';
import { Translate } from '../translate'; import { Translate } from '../translate';
import { request } from '../request';
const Root = styled.div` const Root = styled.div`
.body { .body {
@ -43,7 +43,14 @@ export const MeetingView: React.FC<MeetingViewProps> = React.memo((props) => {
const initedRef = useRef(false); const initedRef = useRef(false);
const init = useMemoizedFn(async (channelName: string) => { const init = useMemoizedFn(async (channelName: string) => {
const { _id } = await getJWTUserInfo();
client.on('user-published', async (user, mediaType) => { client.on('user-published', async (user, mediaType) => {
if (String(user.uid).startsWith(_id)) {
// 不监听自身
return;
}
await client.subscribe(user, mediaType); await client.subscribe(user, mediaType);
console.log('subscribe success'); console.log('subscribe success');
if (mediaType === 'audio') { if (mediaType === 'audio') {

@ -1,7 +1,8 @@
import { UserAvatar, UserName } from '@capital/component'; import { Icon, UserAvatar, UserName } from '@capital/component';
import { AgoraVideoPlayer, IAgoraRTCRemoteUser } from 'agora-rtc-react'; import { AgoraVideoPlayer, IAgoraRTCRemoteUser } from 'agora-rtc-react';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { Translate } from '../translate';
import { useClient } from './client'; import { useClient } from './client';
import { useMeetingStore } from './store'; import { useMeetingStore } from './store';
import { getClientLocalTrack } from './utils'; import { getClientLocalTrack } from './utils';
@ -36,9 +37,54 @@ const Root = styled.div<{
left: 0; left: 0;
bottom: 0; bottom: 0;
padding: 4px 8px; padding: 4px 8px;
color: white;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 6px;
}
.screen-icon {
width: 96px;
height: 96px;
font-size: 96px;
} }
`; `;
/**
* Icon
*/
const VideViewIcon: React.FC<{ uid: string }> = React.memo(({ uid }) => {
if (uid.endsWith('_screen')) {
// 是屏幕共享
return (
<div className="screen-icon">
<Icon icon="mdi:projector-screen-outline" />
</div>
);
} else {
return <UserAvatar size={96} userId={uid} />;
}
});
VideViewIcon.displayName = 'VideViewIcon';
/**
*
*/
const VideViewName: React.FC<{ uid: string }> = React.memo(({ uid }) => {
if (uid.endsWith('_screen')) {
const userId = uid.substring(0, uid.length - '_screen'.length);
return (
<span className="name">
<UserName userId={userId} />
{Translate.someoneScreenName}
</span>
);
} else {
return <UserName className="name" userId={uid} />;
}
});
VideViewName.displayName = 'VideViewName';
export const VideoView: React.FC<{ export const VideoView: React.FC<{
user: IAgoraRTCRemoteUser; user: IAgoraRTCRemoteUser;
}> = (props) => { }> = (props) => {
@ -47,13 +93,13 @@ export const VideoView: React.FC<{
return ( return (
<Root active={active}> <Root active={active}>
{user.hasVideo ? ( {user.hasVideo && user.videoTrack ? (
<AgoraVideoPlayer className="player" videoTrack={user.videoTrack} /> <AgoraVideoPlayer className="player" videoTrack={user.videoTrack} />
) : ( ) : (
<UserAvatar size={96} userId={String(user.uid)} /> <VideViewIcon uid={String(user.uid)} />
)} )}
<UserName className="name" userId={String(user.uid)} /> <VideViewName uid={String(user.uid)} />
</Root> </Root>
); );
}; };
@ -75,10 +121,10 @@ export const OwnVideoView: React.FC<{}> = React.memo(() => {
{mediaPerm.video ? ( {mediaPerm.video ? (
<AgoraVideoPlayer className="player" videoTrack={videoTrack} /> <AgoraVideoPlayer className="player" videoTrack={videoTrack} />
) : ( ) : (
<UserAvatar size={96} userId={String(client.uid)} /> <VideViewIcon uid={String(client.uid)} />
)} )}
<UserName className="name" userId={String(client.uid)} /> <VideViewName uid={String(client.uid)} />
</Root> </Root>
); );
}); });

@ -8,3 +8,7 @@ const config: ClientConfig = {
export const useClient = createClient(config); export const useClient = createClient(config);
export const createCameraVideoTrack = AgoraRTC.createCameraVideoTrack; export const createCameraVideoTrack = AgoraRTC.createCameraVideoTrack;
export const createMicrophoneAudioTrack = AgoraRTC.createMicrophoneAudioTrack; export const createMicrophoneAudioTrack = AgoraRTC.createMicrophoneAudioTrack;
// 屏幕共享
export const useScreenSharingClient = createClient(config);
export const createScreenVideoTrack = AgoraRTC.createScreenVideoTrack;

@ -4,6 +4,7 @@ import create from 'zustand';
interface MediaPerm { interface MediaPerm {
video: boolean; video: boolean;
audio: boolean; audio: boolean;
screensharing: boolean;
} }
interface MeetingState { interface MeetingState {
@ -38,7 +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, screensharing: false },
volumes: [], volumes: [],
appendUser: (user: IAgoraRTCRemoteUser) => { appendUser: (user: IAgoraRTCRemoteUser) => {
set((state) => ({ set((state) => ({

@ -0,0 +1,76 @@
import { getJWTUserInfo } from '@capital/common';
import type { ILocalVideoTrack } from 'agora-rtc-react';
import { useMemoizedFn } from 'ahooks';
import { useEffect } from 'react';
import { request } from '../request';
import {
createScreenVideoTrack,
useClient,
useScreenSharingClient,
} from './client';
import { useMeetingStore } from './store';
/**
*
*/
export function useScreenSharing() {
const client = useClient();
const screenSharingClient = useScreenSharingClient();
useEffect(() => {
() => {
screenSharingClient.leave();
};
}, []);
const startScreenSharing = useMemoizedFn(async () => {
if (!client.channelName) {
return;
}
const track = await createScreenVideoTrack(
{
optimizationMode: 'detail',
},
'auto'
);
let t: ILocalVideoTrack;
if (Array.isArray(track)) {
t = track[0];
} else {
t = track;
}
t.on('track-ended', () => {
// 画面断开时自动触发停止共享(用户点击停止共享按钮)
stopScreenSharing();
});
const channelName = client.channelName;
const { _id } = await getJWTUserInfo();
const uid = _id + '_screen';
const { data } = await request.post('generateJoinInfo', {
channelName,
userId: uid,
});
const { appId, token } = data ?? {};
await screenSharingClient.join(appId, channelName, token, uid);
await screenSharingClient.publish(track);
useMeetingStore.getState().setMediaPerm({ screensharing: true });
});
const stopScreenSharing = useMemoizedFn(async () => {
screenSharingClient.localTracks.forEach((t) => t.close());
await screenSharingClient.unpublish();
await screenSharingClient.leave();
useMeetingStore.getState().setMediaPerm({ screensharing: false });
});
return {
startScreenSharing,
stopScreenSharing,
};
}

@ -18,11 +18,11 @@ export const Translate = {
'en-US': 'No one Speaking', 'en-US': 'No one Speaking',
}), }),
startCall: localTrans({ startCall: localTrans({
'zh-CN': '发起通话', 'zh-CN': '发起/加入通话',
'en-US': 'Start Call', 'en-US': 'Start/Join Call',
}), }),
startCallContent: localTrans({ startCallContent: localTrans({
'zh-CN': '是否通过声网插件在当前会话开启音视频通讯?', 'zh-CN': '是否通过声网插件在当前会话开启/加入音视频通讯?',
'en-US': 'en-US':
'Do you want to enable audio and video communication in the current session through the Agora plugin?', 'Do you want to enable audio and video communication in the current session through the Agora plugin?',
}), }),
@ -63,4 +63,16 @@ export const Translate = {
'zh-CN': '关闭麦克风', 'zh-CN': '关闭麦克风',
'en-US': 'Close Mic', 'en-US': 'Close Mic',
}), }),
openScreensharing: localTrans({
'zh-CN': '开启屏幕共享',
'en-US': 'Open Screensharing',
}),
closeScreensharing: localTrans({
'zh-CN': '关闭屏幕共享',
'en-US': 'Close Screensharing',
}),
someoneScreenName: localTrans({
'zh-CN': ' 的屏幕',
'en-US': "'s Screen",
}),
}; };

Loading…
Cancel
Save