diff --git a/src/app/slices/voice.ts b/src/app/slices/voice.ts index 2e00a420..f5acd23c 100644 --- a/src/app/slices/voice.ts +++ b/src/app/slices/voice.ts @@ -13,6 +13,7 @@ export type VoicingInfo = { muted?: boolean, deafen?: boolean, video?: boolean, + shareScreen?: boolean, joining?: boolean, connectionState?: ConnectionState } & VoiceBasicInfo @@ -21,7 +22,8 @@ export type VoicingMemberInfo = { speakingVolume?: number, muted?: boolean, deafen?: boolean, - video?: boolean + video?: boolean, + shareScreen?: boolean } export type VoicingMembers = { diff --git a/src/common/component/Voice.tsx b/src/common/component/Voice.tsx index 1039b016..0a0cabf4 100644 --- a/src/common/component/Voice.tsx +++ b/src/common/component/Voice.tsx @@ -7,6 +7,7 @@ import { addVoiceMember, removeVoiceMember, updateConnectionState, updateDeafenS import { useAppSelector } from '../../app/store'; import AudioJoin from '../../assets/join.wav'; import { playAgoraVideo } from '../utils'; +import { ShareScreenTrack } from '../../types/global'; AgoraRTC.setLogLevel(process.env.NODE_ENV === 'development' ? 0 : 4); window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {}; window.VIDEO_TRACK_MAP = window.VIDEO_TRACK_MAP ?? {}; @@ -214,6 +215,60 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { })); } }; + const stopShareScreen = async () => { + const localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ShareScreenTrack; + if (localVideoTrack) { + await window.VOICE_CLIENT?.unpublish(localVideoTrack); + if ("close" in localVideoTrack) { + localVideoTrack.close(); + } else { + localVideoTrack[0].close(); + } + // localVideoTrack.close(); + window.VIDEO_TRACK_MAP[loginUid] = null; + // 关闭视频后,需要把视频的高度设置回去 + const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement; + playerEle.classList.remove("h-[120px]"); + dispatch(updateVoicingInfo({ + video: false, + shareScreen: false, + id, + context, + })); + } + }; + const startShareScreen = async () => { + try { + const localVideoTrack = await AgoraRTC.createScreenVideoTrack({ + // 配置屏幕共享编码参数,详情请查看 API 文档。 + encoderConfig: "1080p_1", + // 设置视频传输优化策略为清晰优先或流畅优先。 + optimizationMode: "detail", + }); + + // 取消正在进行的视频 + await closeCamera(); + await window.VOICE_CLIENT?.publish(localVideoTrack); + dispatch(updateVoicingInfo({ + video: false, + shareScreen: true, + id, + context, + })); + // 放到全局变量里 + window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack; + playAgoraVideo(loginUid); + // 监听屏幕共享结束事件 + if ("close" in localVideoTrack) { + localVideoTrack.getMediaStreamTrack().onended = () => { + stopShareScreen(); + }; + } + } catch (error) { + console.log("start share screen error", error); + + } + }; const leave = async () => { const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; @@ -285,7 +340,9 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { joined: !!voicingInfo, joinVoice, openCamera, - closeCamera + closeCamera, + startShareScreen, + stopShareScreen, }; }; export { useVoice }; diff --git a/src/common/utils.tsx b/src/common/utils.tsx index 046f5648..303f82a3 100644 --- a/src/common/utils.tsx +++ b/src/common/utils.tsx @@ -1,6 +1,6 @@ import dayjs from "dayjs"; import BASE_URL, { FILE_IMAGE_SIZE, ContentTypes, KEY_TOKEN, KEY_REFRESH_TOKEN, KEY_EXPIRE } from "../app/config"; -import { ICameraVideoTrack } from "agora-rtc-sdk-ng"; +import { ICameraVideoTrack, ILocalAudioTrack, ILocalVideoTrack } from "agora-rtc-sdk-ng"; import IconPdf from "../assets/icons/file.pdf.svg"; import IconAudio from "../assets/icons/file.audio.svg"; import IconVideo from "../assets/icons/file.video.svg"; @@ -381,7 +381,13 @@ export const playAgoraVideo = (uid: number, videoTrack?: ICameraVideoTrack | nul if (videoTrack) { videoTrack.play(playerEle); } else { - window.VIDEO_TRACK_MAP[uid]?.play(playerEle); + if (!window.VIDEO_TRACK_MAP[uid]) return; + if ("play" in (window.VIDEO_TRACK_MAP[uid] ?? {})) { + window.VIDEO_TRACK_MAP[uid]?.play(playerEle); + } else { + const tracks = window.VIDEO_TRACK_MAP[uid] as [ILocalVideoTrack, ILocalAudioTrack]; + tracks[0].play(playerEle); + } } } }; \ No newline at end of file diff --git a/src/routes/chat/RTCWidget.tsx b/src/routes/chat/RTCWidget.tsx index d847ae26..7da11dac 100644 --- a/src/routes/chat/RTCWidget.tsx +++ b/src/routes/chat/RTCWidget.tsx @@ -22,7 +22,7 @@ type Props = { const RTCWidget = ({ id, context = "channel" }: Props) => { const { t } = useTranslation("chat"); - const { leave, voicingInfo, setMute, setDeafen, joining = true, openCamera, closeCamera } = useVoice({ context, id }); + const { leave, voicingInfo, setMute, setDeafen, joining = true, openCamera, closeCamera, startShareScreen, stopShareScreen } = useVoice({ context, id }); const { loginUser, channelData, userData } = useAppSelector(store => { return { userData: store.users.byId, @@ -54,8 +54,8 @@ const RTCWidget = ({ id, context = "channel" }: Props) => { Video - diff --git a/src/routes/chat/VoiceChat/Dashboard.tsx b/src/routes/chat/VoiceChat/Dashboard.tsx index b293d3bd..7c453905 100644 --- a/src/routes/chat/VoiceChat/Dashboard.tsx +++ b/src/routes/chat/VoiceChat/Dashboard.tsx @@ -10,12 +10,12 @@ type Props = { } const Dashboard = ({ context = "channel", id, visible }: Props) => { - const { voicingInfo, setMute, setDeafen, leave, closeCamera, openCamera } = useVoice({ id, context }); + const { voicingInfo, setMute, setDeafen, leave, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id, context }); // const dispatch = useDispatch(); return
- +
; }; diff --git a/src/routes/chat/VoiceChat/VoiceManagement.tsx b/src/routes/chat/VoiceChat/VoiceManagement.tsx index 26e3c268..c7f3677c 100644 --- a/src/routes/chat/VoiceChat/VoiceManagement.tsx +++ b/src/routes/chat/VoiceChat/VoiceManagement.tsx @@ -24,10 +24,12 @@ type Props = { setDeafen: (param: boolean) => void, leave: () => void, closeCamera: () => void, - openCamera: () => void + openCamera: () => void, + startShareScreen: () => void, + stopShareScreen: () => void } -const VoiceManagement = ({ info, setMute, setDeafen, leave, closeCamera, openCamera }: Props) => { +const VoiceManagement = ({ info, setMute, setDeafen, leave, closeCamera, openCamera, startShareScreen, stopShareScreen }: Props) => { const { t } = useTranslation("chat"); const { userData, voicingMembers } = useAppSelector(store => { return { @@ -43,7 +45,7 @@ const VoiceManagement = ({ info, setMute, setDeafen, leave, closeCamera, openCam }, [voicingMembers.ids]); if (!info) return null; - const { deafen, muted, video } = info; + const { deafen, muted, video, shareScreen } = info; const nameClass = clsx(`text-sm text-gray-500 max-w-[120px] truncate font-semibold dark:text-white`); const members = voicingMembers.ids; const membersData = voicingMembers.byId; @@ -116,7 +118,7 @@ const VoiceManagement = ({ info, setMute, setDeafen, leave, closeCamera, openCam -
  • +
  • diff --git a/src/types/global.d.ts b/src/types/global.d.ts index ba3b91c4..f994b662 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,4 +1,4 @@ -import { IAgoraRTCClient, ICameraVideoTrack, IMicrophoneAudioTrack, IRemoteAudioTrack, IRemoteVideoTrack } from "agora-rtc-sdk-ng"; +import { IAgoraRTCClient, ICameraVideoTrack, ILocalAudioTrack, ILocalVideoTrack, IMicrophoneAudioTrack, IRemoteAudioTrack, IRemoteVideoTrack } from "agora-rtc-sdk-ng"; interface BeforeInstallPromptEvent extends Event { /** @@ -36,10 +36,11 @@ export declare global { [key: number]: IRemoteAudioTrack | IMicrophoneAudioTrack | undefined | null }, VIDEO_TRACK_MAP: { - [key: number]: IRemoteVideoTrack | ICameraVideoTrack | undefined | null + [key: number]: IRemoteVideoTrack | ICameraVideoTrack | ShareScreenTrack | undefined | null }, } interface WindowEventMap { beforeinstallprompt: BeforeInstallPromptEvent; } } +export type ShareScreenTrack = ILocalVideoTrack | [ILocalVideoTrack, ILocalAudioTrack];