feat: share screen
This commit is contained in:
@@ -13,6 +13,7 @@ export type VoicingInfo = {
|
|||||||
muted?: boolean,
|
muted?: boolean,
|
||||||
deafen?: boolean,
|
deafen?: boolean,
|
||||||
video?: boolean,
|
video?: boolean,
|
||||||
|
shareScreen?: boolean,
|
||||||
joining?: boolean,
|
joining?: boolean,
|
||||||
connectionState?: ConnectionState
|
connectionState?: ConnectionState
|
||||||
} & VoiceBasicInfo
|
} & VoiceBasicInfo
|
||||||
@@ -21,7 +22,8 @@ export type VoicingMemberInfo = {
|
|||||||
speakingVolume?: number,
|
speakingVolume?: number,
|
||||||
muted?: boolean,
|
muted?: boolean,
|
||||||
deafen?: boolean,
|
deafen?: boolean,
|
||||||
video?: boolean
|
video?: boolean,
|
||||||
|
shareScreen?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VoicingMembers = {
|
export type VoicingMembers = {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { addVoiceMember, removeVoiceMember, updateConnectionState, updateDeafenS
|
|||||||
import { useAppSelector } from '../../app/store';
|
import { useAppSelector } from '../../app/store';
|
||||||
import AudioJoin from '../../assets/join.wav';
|
import AudioJoin from '../../assets/join.wav';
|
||||||
import { playAgoraVideo } from '../utils';
|
import { playAgoraVideo } from '../utils';
|
||||||
|
import { ShareScreenTrack } from '../../types/global';
|
||||||
AgoraRTC.setLogLevel(process.env.NODE_ENV === 'development' ? 0 : 4);
|
AgoraRTC.setLogLevel(process.env.NODE_ENV === 'development' ? 0 : 4);
|
||||||
window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {};
|
window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {};
|
||||||
window.VIDEO_TRACK_MAP = window.VIDEO_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 leave = async () => {
|
||||||
const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack;
|
const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack;
|
||||||
@@ -285,7 +340,9 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
|||||||
joined: !!voicingInfo,
|
joined: !!voicingInfo,
|
||||||
joinVoice,
|
joinVoice,
|
||||||
openCamera,
|
openCamera,
|
||||||
closeCamera
|
closeCamera,
|
||||||
|
startShareScreen,
|
||||||
|
stopShareScreen,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export { useVoice };
|
export { useVoice };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import BASE_URL, { FILE_IMAGE_SIZE, ContentTypes, KEY_TOKEN, KEY_REFRESH_TOKEN, KEY_EXPIRE } from "../app/config";
|
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 IconPdf from "../assets/icons/file.pdf.svg";
|
||||||
import IconAudio from "../assets/icons/file.audio.svg";
|
import IconAudio from "../assets/icons/file.audio.svg";
|
||||||
import IconVideo from "../assets/icons/file.video.svg";
|
import IconVideo from "../assets/icons/file.video.svg";
|
||||||
@@ -381,7 +381,13 @@ export const playAgoraVideo = (uid: number, videoTrack?: ICameraVideoTrack | nul
|
|||||||
if (videoTrack) {
|
if (videoTrack) {
|
||||||
videoTrack.play(playerEle);
|
videoTrack.play(playerEle);
|
||||||
} else {
|
} 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -22,7 +22,7 @@ type Props = {
|
|||||||
|
|
||||||
const RTCWidget = ({ id, context = "channel" }: Props) => {
|
const RTCWidget = ({ id, context = "channel" }: Props) => {
|
||||||
const { t } = useTranslation("chat");
|
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 => {
|
const { loginUser, channelData, userData } = useAppSelector(store => {
|
||||||
return {
|
return {
|
||||||
userData: store.users.byId,
|
userData: store.users.byId,
|
||||||
@@ -54,8 +54,8 @@ const RTCWidget = ({ id, context = "channel" }: Props) => {
|
|||||||
<IconCamera className={clsx("dark:fill-gray-200 w-6 h-6", voicingInfo.video ? "fill-gray-200" : "fill-gray-800")} />
|
<IconCamera className={clsx("dark:fill-gray-200 w-6 h-6", voicingInfo.video ? "fill-gray-200" : "fill-gray-800")} />
|
||||||
<span>Video</span>
|
<span>Video</span>
|
||||||
</button>
|
</button>
|
||||||
<button className='rounded-lg py-2 px-5 bg-white/50 dark:bg-black/50 flex items-center gap-1'>
|
<button onClick={voicingInfo.shareScreen ? stopShareScreen : startShareScreen} className={clsx('rounded-lg py-2 px-5 flex items-center gap-1', voicingInfo.shareScreen ? "bg-green-700 text-gray-200" : "bg-white/50 dark:bg-black/50 ")}>
|
||||||
<IconScreen className="fill-gray-800 dark:fill-gray-200 w-6 h-6" />
|
<IconScreen className={clsx("dark:fill-gray-200 w-6 h-6", voicingInfo.shareScreen ? "fill-gray-200" : "fill-gray-800")} />
|
||||||
<span>Screen</span>
|
<span>Screen</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Dashboard = ({ context = "channel", id, visible }: 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();
|
// const dispatch = useDispatch();
|
||||||
|
|
||||||
|
|
||||||
return <div className={`h-full flex-col gap-1 w-[226px] overflow-y-scroll overflow-x-hidden p-2 shadow-[inset_1px_0px_0px_rgba(0,_0,_0,_0.1)] ${visible ? "flex" : "hidden"}`}>
|
return <div className={`h-full flex-col gap-1 w-[226px] overflow-y-scroll overflow-x-hidden p-2 shadow-[inset_1px_0px_0px_rgba(0,_0,_0,_0.1)] ${visible ? "flex" : "hidden"}`}>
|
||||||
<VoiceManagement info={voicingInfo} setMute={setMute} setDeafen={setDeafen} leave={leave} closeCamera={closeCamera} openCamera={openCamera} />
|
<VoiceManagement info={voicingInfo} setMute={setMute} setDeafen={setDeafen} leave={leave} closeCamera={closeCamera} openCamera={openCamera} startShareScreen={startShareScreen} stopShareScreen={stopShareScreen} />
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ type Props = {
|
|||||||
setDeafen: (param: boolean) => void,
|
setDeafen: (param: boolean) => void,
|
||||||
leave: () => void,
|
leave: () => void,
|
||||||
closeCamera: () => 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 { t } = useTranslation("chat");
|
||||||
const { userData, voicingMembers } = useAppSelector(store => {
|
const { userData, voicingMembers } = useAppSelector(store => {
|
||||||
return {
|
return {
|
||||||
@@ -43,7 +45,7 @@ const VoiceManagement = ({ info, setMute, setDeafen, leave, closeCamera, openCam
|
|||||||
}, [voicingMembers.ids]);
|
}, [voicingMembers.ids]);
|
||||||
|
|
||||||
if (!info) return null;
|
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 nameClass = clsx(`text-sm text-gray-500 max-w-[120px] truncate font-semibold dark:text-white`);
|
||||||
const members = voicingMembers.ids;
|
const members = voicingMembers.ids;
|
||||||
const membersData = voicingMembers.byId;
|
const membersData = voicingMembers.byId;
|
||||||
@@ -116,7 +118,7 @@ const VoiceManagement = ({ info, setMute, setDeafen, leave, closeCamera, openCam
|
|||||||
</li>
|
</li>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip tip={"Share Screen"} placement="top">
|
<Tooltip tip={"Share Screen"} placement="top">
|
||||||
<li role={"button"} onClick={setMute.bind(null, !muted)} className="py-2 px-3 rounded bg-gray-100 dark:bg-gray-900">
|
<li role={"button"} onClick={shareScreen ? stopShareScreen : startShareScreen} className={clsx("py-2 px-3 rounded ", shareScreen ? "bg-green-700" : "bg-gray-100 dark:bg-gray-900")}>
|
||||||
<IconScreen className="fill-gray-700 dark:fill-gray-300" />
|
<IconScreen className="fill-gray-700 dark:fill-gray-300" />
|
||||||
</li>
|
</li>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
Vendored
+3
-2
@@ -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 {
|
interface BeforeInstallPromptEvent extends Event {
|
||||||
/**
|
/**
|
||||||
@@ -36,10 +36,11 @@ export declare global {
|
|||||||
[key: number]: IRemoteAudioTrack | IMicrophoneAudioTrack | undefined | null
|
[key: number]: IRemoteAudioTrack | IMicrophoneAudioTrack | undefined | null
|
||||||
},
|
},
|
||||||
VIDEO_TRACK_MAP: {
|
VIDEO_TRACK_MAP: {
|
||||||
[key: number]: IRemoteVideoTrack | ICameraVideoTrack | undefined | null
|
[key: number]: IRemoteVideoTrack | ICameraVideoTrack | ShareScreenTrack | undefined | null
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
interface WindowEventMap {
|
interface WindowEventMap {
|
||||||
beforeinstallprompt: BeforeInstallPromptEvent;
|
beforeinstallprompt: BeforeInstallPromptEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export type ShareScreenTrack = ILocalVideoTrack | [ILocalVideoTrack, ILocalAudioTrack];
|
||||||
|
|||||||
Reference in New Issue
Block a user