diff --git a/cspell.config.js b/cspell.config.js index 67d9b721dd5..740a4be633e 100644 --- a/cspell.config.js +++ b/cspell.config.js @@ -34,6 +34,7 @@ module.exports = { "lastmile", "onleave", "leavechannel", + "videosource", // css / less "minlength", diff --git a/desktop/renderer-app/src/apiMiddleware/flatServer/index.ts b/desktop/renderer-app/src/apiMiddleware/flatServer/index.ts index 1415975d80e..8b85d280ea6 100644 --- a/desktop/renderer-app/src/apiMiddleware/flatServer/index.ts +++ b/desktop/renderer-app/src/apiMiddleware/flatServer/index.ts @@ -131,6 +131,10 @@ export interface JoinRoomResult { rtcUID: number; // rtc 的 uid rtcToken: string; // rtc token rtmToken: string; // rtm token + rtcShareScreen: { + uid: number; + token: string; + }; } export function joinRoom(uuid: string): Promise { diff --git a/desktop/renderer-app/src/apiMiddleware/share-screen.ts b/desktop/renderer-app/src/apiMiddleware/share-screen.ts new file mode 100644 index 00000000000..c529e705f24 --- /dev/null +++ b/desktop/renderer-app/src/apiMiddleware/share-screen.ts @@ -0,0 +1,126 @@ +import { globalStore } from "../stores/GlobalStore"; +import { AGORA } from "../constants/Process"; +import type AgoraSDK from "agora-electron-sdk"; + +export class RTCShareScreen { + public constructor( + private readonly roomUUID: string, + private readonly roomClient: AgoraSDK, + private readonly enableShareScreenStatus: (enable: boolean) => void, + private readonly existOtherUserStream: () => boolean, + ) {} + + public enable(isDisplayScreen: boolean, screenID: number): void { + if (!globalStore.rtcShareScreen) { + return; + } + + if (this.existOtherUserStream()) { + return; + } + + this.enableShareScreenStatus(true); + + const rect = { x: 0, y: 0, width: 0, height: 0 }; + const videoSourceParam = { + width: 0, + height: 0, + bitrate: 500, + frameRate: 5, + captureMouseCursor: false, + windowFocus: false, + excludeWindowList: [], + excludeWindowCount: 0, + }; + + this.roomClient.once("videoSourceJoinedSuccess", () => { + this.roomClient.videoSourceSetVideoProfile(43, false); + + if (isDisplayScreen) { + this.roomClient.videoSourceStartScreenCaptureByScreen( + { + // @ts-ignore + id: screenID, + }, + rect, + videoSourceParam, + ); + } else { + this.roomClient.videoSourceStartScreenCaptureByWindow( + screenID, + rect, + videoSourceParam, + ); + } + }); + + this.roomClient.videoSourceInitialize(AGORA.APP_ID); + + this.roomClient.videoSourceSetChannelProfile(1); + + this.roomClient.videoSourceJoin( + globalStore.rtcShareScreen.token, + this.roomUUID, + "", + globalStore.rtcShareScreen.uid, + ); + } + + public close(): Promise { + return new Promise(resolve => { + this.roomClient.once("videoSourceLeaveChannel", () => { + this.roomClient.videoSourceRelease(); + resolve(); + }); + + this.roomClient.videoSourceLeave(); + }).finally(() => { + this.enableShareScreenStatus(false); + }); + } + + public async destroy(): Promise { + return await this.close(); + } + + public getScreenInfo(): ScreenInfo { + const displayList = this.roomClient.getScreenDisplaysInfo() as ScreenDisplaysInfo[]; + const windowList = this.roomClient.getScreenWindowsInfo() as ScreenWindowsInfo[]; + + return { + displayList, + windowList, + }; + } +} + +export type ScreenDisplaysInfo = { + readonly displayId: { + readonly id: number; + }; + readonly width: number; + readonly height: number; + readonly image: Uint8Array; + readonly isActive: boolean; + readonly isBuiltin: boolean; + readonly isMain: boolean; +}; + +export type ScreenWindowsInfo = { + readonly windowId: number; + readonly name: string; // e.g: Google (website title) + readonly ownerName: string; // e.g: Google Chrome.app + readonly width: number; + readonly height: number; + readonly originWidth: number; + readonly originHeight: number; + readonly image: Uint8Array; + readonly isActive: boolean; + readonly isBuiltin: boolean; + readonly isMain: boolean; +}; + +export type ScreenInfo = { + displayList: ScreenDisplaysInfo[]; + windowList: ScreenWindowsInfo[]; +}; diff --git a/desktop/renderer-app/src/assets/image/share-screen-active.svg b/desktop/renderer-app/src/assets/image/share-screen-active.svg new file mode 100644 index 00000000000..b03eba980d4 --- /dev/null +++ b/desktop/renderer-app/src/assets/image/share-screen-active.svg @@ -0,0 +1,17 @@ + + + follow备份 2 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/desktop/renderer-app/src/assets/image/share-screen.svg b/desktop/renderer-app/src/assets/image/share-screen.svg new file mode 100644 index 00000000000..14b5852959b --- /dev/null +++ b/desktop/renderer-app/src/assets/image/share-screen.svg @@ -0,0 +1,17 @@ + + + follow备份 2 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreen/index.tsx b/desktop/renderer-app/src/components/ShareScreen/ShareScreen/index.tsx new file mode 100644 index 00000000000..afe716f1afa --- /dev/null +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreen/index.tsx @@ -0,0 +1,28 @@ +import "./style.less"; + +import React, { useEffect, useMemo, useRef } from "react"; +import { observer } from "mobx-react-lite"; +import classNames from "classnames"; +import type { ShareScreenStore } from "../../../stores/ShareScreenStore"; + +interface ShareScreenProps { + shareScreenStore: ShareScreenStore; +} + +export const ShareScreen = observer(function ShareScreen({ shareScreenStore }) { + const ref = useRef(null); + + useEffect(() => { + if (ref.current) { + shareScreenStore.updateElement(ref.current); + } + }, [shareScreenStore]); + + const classNameList = useMemo(() => { + return classNames("share-screen", { + active: shareScreenStore.existOtherShareScreen, + }); + }, [shareScreenStore.existOtherShareScreen]); + + return
; +}); diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreen/style.less b/desktop/renderer-app/src/components/ShareScreen/ShareScreen/style.less new file mode 100644 index 00000000000..80827401720 --- /dev/null +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreen/style.less @@ -0,0 +1,10 @@ +.share-screen { + position: absolute; + z-index: 5; + background-color: black; + + &.active { + width: 100%; + height: 100%; + } +} diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/Utils.ts b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/Utils.ts new file mode 100644 index 00000000000..f78d11f4a08 --- /dev/null +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/Utils.ts @@ -0,0 +1,31 @@ +import { ScreenInfo } from "../../../../apiMiddleware/share-screen"; + +export const uint8ArrayToImageURL = (buffer: Uint8Array): string => { + return URL.createObjectURL( + new Blob([buffer.buffer], { + type: "image/png", + }), + ); +}; + +export const getScreenInfo = ( + info: ScreenInfo["windowList"][0] | ScreenInfo["displayList"][0], +): { + id: number; + isDisplay: boolean; + name: string; +} => { + if ("displayId" in info) { + return { + id: info.displayId.id, + isDisplay: true, + name: "Desktop", + }; + } else { + return { + id: info.windowId, + isDisplay: false, + name: `${info.ownerName} - ${info.name}`, + }; + } +}; diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/index.tsx b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/index.tsx new file mode 100644 index 00000000000..4b5eab134fe --- /dev/null +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/index.tsx @@ -0,0 +1,100 @@ +import "./style.less"; +import React, { useCallback, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { ScreenInfo } from "../../../../apiMiddleware/share-screen"; +import { getScreenInfo, uint8ArrayToImageURL } from "./Utils"; +import classNames from "classnames"; +import { ShareScreenStore } from "../../../../stores/ShareScreenStore"; +import { message } from "antd"; +import { useTranslation } from "react-i18next"; + +interface ScreenListProps { + screenInfo: ScreenInfo; + shareScreenStore: ShareScreenStore; +} + +export const ScreenList = observer(function ShareScreen({ + screenInfo, + shareScreenStore, +}) { + const [activeInfo, setActiveInfo] = useState(""); + const { t } = useTranslation(); + + useEffect(() => { + if (screenInfo.windowList.length === 0) { + void message.error(t("share-screen.desktop-not-permission")); + } + }, [screenInfo.windowList, t]); + + const onClick = useCallback( + (isDisplay: boolean, id: number) => { + setActiveInfo(`${isDisplay ? "display" : "window"}-${id}`); + shareScreenStore.updateIsDisplayScreen(isDisplay); + shareScreenStore.updateScreenID(id); + }, + [shareScreenStore], + ); + + const cancelSelected = useCallback(() => { + setActiveInfo(""); + shareScreenStore.updateIsDisplayScreen(null); + shareScreenStore.updateScreenID(null); + }, [shareScreenStore]); + + return ( +
+ {screenInfo.displayList.map(info => { + const key = `display-${info.displayId.id}`; + const isActive = activeInfo === key; + + return ( + + ); + })} + {screenInfo.windowList.map(info => { + const key = `window-${info.windowId}`; + const isActive = activeInfo === key; + + return ( + + ); + })} +
+ ); +}); + +interface ScreenItemProps { + info: ScreenInfo["windowList"][0] | ScreenInfo["displayList"][0]; + handleClick: (isDisplay: boolean, id: number) => void; + active: boolean; +} + +const ScreenItem = observer(function ScreenItem({ info, handleClick, active }) { + const screenInfo = getScreenInfo(info); + + return ( + <> +
+
handleClick(screenInfo.isDisplay, screenInfo.id)} + > + screenshots +
+ {screenInfo.name} +
+ + ); +}); diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/style.less b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/style.less new file mode 100644 index 00000000000..c600a2d797c --- /dev/null +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/style.less @@ -0,0 +1,43 @@ +.screen-list { + display: flex; + flex-wrap: wrap; + + .screen-item { + width: 240px; + height: 192px; + margin-right: 16px; + + &:nth-child(3n + 3) { + margin-right: 0; + } + + .screen-image-box { + padding: 12px 20px; + border-radius: 8px; + border: 1px solid #DBE1EA; + cursor: pointer; + + &.active { + border: 2px solid #3381FF; + padding: 11px 19px; + } + + & > img { + width: 200px; + height: 125px; + } + } + + span { + display: block; + margin-top: 5px; + padding: 0 4px; + font-size: 14px; + text-align: center; + color: #7A7B7C; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx new file mode 100644 index 00000000000..6faa9812d87 --- /dev/null +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx @@ -0,0 +1,110 @@ +import "./style.less"; +import React, { useCallback, useEffect, useLayoutEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Button, Modal, Spin } from "antd"; +import { ShareScreenStore } from "../../../stores/ShareScreenStore"; +import { ScreenList } from "./ScreenList"; +import classNames from "classnames"; +import { useTranslation } from "react-i18next"; + +interface ShareScreenPickerProps { + shareScreenStore: ShareScreenStore; + handleOk: () => void; +} + +const ShareScreenPickerModel = observer(function ShareScreen({ + shareScreenStore, + handleOk, +}) { + const { t } = useTranslation(); + + useLayoutEffect(() => { + shareScreenStore.resetScreenInfo(); + }, [shareScreenStore]); + + useEffect(() => { + shareScreenStore.updateScreenInfo(); + }, [shareScreenStore]); + + const closeModal = useCallback(() => { + shareScreenStore.updateShowShareScreenPicker(false); + }, [shareScreenStore]); + + const isSelected = + shareScreenStore.isDisplayScreen !== null && shareScreenStore.screenID !== null; + + return ( +
+ + {t("cancel")} + , + , + ]} + > +
+ {shareScreenStore.screenInfo ? ( + + ) : ( + + )} +
+
+
+ ); +}); + +export const ShareScreenPicker = observer(function ShareScreen({ + shareScreenStore, + handleOk, +}) { + return shareScreenStore.showShareScreenPicker ? ( + + ) : null; +}); + +interface ConfirmButtonProps { + isSelected: boolean; + handleOk: () => void; +} + +const ConfirmButton = observer(function ConfirmButton({ + isSelected, + handleOk, +}) { + const [confirmLoading, setConfirmLoading] = useState(false); + const { t } = useTranslation(); + + return ( + + ); +}); diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/style.less b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/style.less new file mode 100644 index 00000000000..3a3898d15b7 --- /dev/null +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/style.less @@ -0,0 +1,26 @@ +.share-screen-picker-model { + .ant-modal-header { + border-bottom: none; + } + + .ant-modal-footer { + border-top: none; + padding: 16px 16px; + } + + .footer-button { + min-width: 86px; + height: 32px; + } +} + +.share-screen-picker { + height: 450px; + overflow-y: scroll; + + &.loading { + display: flex; + justify-content: center; + align-items: center + } +} diff --git a/desktop/renderer-app/src/components/ShareScreen/index.ts b/desktop/renderer-app/src/components/ShareScreen/index.ts new file mode 100644 index 00000000000..d54eb6bd11e --- /dev/null +++ b/desktop/renderer-app/src/components/ShareScreen/index.ts @@ -0,0 +1,2 @@ +export { ShareScreen } from "./ShareScreen"; +export { ShareScreenPicker } from "./ShareScreenPicker"; diff --git a/desktop/renderer-app/src/components/Whiteboard.less b/desktop/renderer-app/src/components/Whiteboard.less index b67f8a9404f..11fbf83d7b9 100644 --- a/desktop/renderer-app/src/components/Whiteboard.less +++ b/desktop/renderer-app/src/components/Whiteboard.less @@ -1,5 +1,4 @@ .whiteboard-container { - position: relative; width: 100%; height: 100%; diff --git a/desktop/renderer-app/src/pages/BigClassPage/BigClassPage.less b/desktop/renderer-app/src/pages/BigClassPage/BigClassPage.less index db6df38e1e6..28c62624328 100644 --- a/desktop/renderer-app/src/pages/BigClassPage/BigClassPage.less +++ b/desktop/renderer-app/src/pages/BigClassPage/BigClassPage.less @@ -4,6 +4,12 @@ overflow: hidden; display: flex; flex-direction: column; + + .container { + position: relative; + width: 100%; + height: 100%; + } } .realtime-content { diff --git a/desktop/renderer-app/src/pages/BigClassPage/index.tsx b/desktop/renderer-app/src/pages/BigClassPage/index.tsx index d52ca8767b2..b90ba9228ff 100644 --- a/desktop/renderer-app/src/pages/BigClassPage/index.tsx +++ b/desktop/renderer-app/src/pages/BigClassPage/index.tsx @@ -44,6 +44,7 @@ import { RouteNameType, RouteParams } from "../../utils/routes"; import { BigClassAvatar } from "./BigClassAvatar"; import "./BigClassPage.less"; import { runtime } from "../../utils/runtime"; +import { ShareScreen, ShareScreenPicker } from "../../components/ShareScreen"; const recordingConfig: RecordingConfig = Object.freeze({ channelType: RtcChannelType.Broadcast, @@ -89,6 +90,8 @@ export const BigClassPage = observer(function BigClassPage() const classRoomStore = useClassRoomStore(params.roomUUID, params.ownerUUID, recordingConfig); const whiteboardStore = classRoomStore.whiteboardStore; + const shareScreenStore = classRoomStore.shareScreenStore; + const globalStore = useContext(GlobalStoreContext); const { confirm, ...exitConfirmModalProps } = useExitRoomConfirmModal(classRoomStore); @@ -169,6 +172,14 @@ export const BigClassPage = observer(function BigClassPage() return ; } + function handleShareScreen(): void { + if (shareScreenStore.enableShareScreenStatus) { + shareScreenStore.close().catch(console.error); + } else { + shareScreenStore.updateShowShareScreenPicker(true); + } + } + return (
(function BigClassPage() right={renderTopBarRight()} />
- +
+ + { + shareScreenStore.enable(); + }} + /> + +
{renderRealtimePanel()}
@@ -274,6 +294,19 @@ export const BigClassPage = observer(function BigClassPage() } /> )} + + {whiteboardStore.isWritable && !shareScreenStore.existOtherShareScreen && ( + + )} + {whiteboardStore.isWritable && ( (function OneToOnePage() const classRoomStore = useClassRoomStore(params.roomUUID, params.ownerUUID, recordingConfig); const whiteboardStore = classRoomStore.whiteboardStore; + const shareScreenStore = classRoomStore.shareScreenStore; + const globalStore = useContext(GlobalStoreContext); const { confirm, ...exitConfirmModalProps } = useExitRoomConfirmModal(classRoomStore); @@ -127,6 +130,14 @@ export const OneToOnePage = observer(function OneToOnePage() return ; } + function handleShareScreen(): void { + if (shareScreenStore.enableShareScreenStatus) { + shareScreenStore.close().catch(console.error); + } else { + shareScreenStore.updateShowShareScreenPicker(true); + } + } + return (
(function OneToOnePage() right={renderTopBarRight()} />
- +
+ + { + shareScreenStore.enable(); + }} + /> + +
{renderRealtimePanel()}
@@ -232,6 +252,19 @@ export const OneToOnePage = observer(function OneToOnePage() } /> )} + + {whiteboardStore.isWritable && !shareScreenStore.existOtherShareScreen && ( + + )} + {whiteboardStore.isWritable && ( (function SmallClassP ClassModeType.Interaction, ); const whiteboardStore = classRoomStore.whiteboardStore; + const shareScreenStore = classRoomStore.shareScreenStore; + const globalStore = useContext(GlobalStoreContext); const { confirm, ...exitConfirmModalProps } = useExitRoomConfirmModal(classRoomStore); @@ -148,6 +151,14 @@ export const SmallClassPage = observer(function SmallClassP return ; } + function handleShareScreen(): void { + if (shareScreenStore.enableShareScreenStatus) { + shareScreenStore.close().catch(console.error); + } else { + shareScreenStore.updateShowShareScreenPicker(true); + } + } + return (
(function SmallClassP /> {renderAvatars()}
- +
+ + { + shareScreenStore.enable(); + }} + /> + +
{renderRealtimePanel()}
@@ -299,6 +319,19 @@ export const SmallClassPage = observer(function SmallClassP } /> )} + + {whiteboardStore.isWritable && !shareScreenStore.existOtherShareScreen && ( + + )} + {whiteboardStore.isWritable && ( { if (this.whiteboardStore.isKicked) { reaction.dispose(); @@ -159,6 +164,16 @@ export class ClassRoomStore { } }); + reaction( + () => this.whiteboardStore.isWritable, + () => { + this.shareScreenStore.updateIsWritable(this.whiteboardStore.isWritable); + }, + { + fireImmediately: true, + }, + ); + this.rtm.once(RTMessageType.REMOTE_LOGIN, () => { runInAction(() => { this.isRemoteLogin = true; @@ -526,6 +541,8 @@ export class ClassRoomStore { promises.push(this.stopRecording()); + promises.push(this.shareScreenStore.destroy()); + this.leaveRTC(); this.whiteboardStore.destroy(); diff --git a/desktop/renderer-app/src/stores/GlobalStore.ts b/desktop/renderer-app/src/stores/GlobalStore.ts index 6baee3f630e..fd5485595ef 100644 --- a/desktop/renderer-app/src/stores/GlobalStore.ts +++ b/desktop/renderer-app/src/stores/GlobalStore.ts @@ -22,6 +22,10 @@ export class GlobalStore { public whiteboardRoomToken: string | null = null; public rtcToken: string | null = null; public rtcUID: number | null = null; + public rtcShareScreen: { + uid: number; + token: string; + } | null = null; public rtmToken: string | null = null; public region: Region | null = null; @@ -50,6 +54,7 @@ export class GlobalStore { | "rtcToken" | "rtmToken" | "rtcUID" + | "rtcShareScreen" | "region" > >, @@ -60,6 +65,7 @@ export class GlobalStore { "rtcToken", "rtmToken", "rtcUID", + "rtcShareScreen", "region", ] as const; for (const key of keys) { @@ -82,6 +88,10 @@ export class GlobalStore { public hideRecordHintTips = (): void => { this.isShowRecordHintTips = false; }; + + public isShareScreenUID = (uid: number): boolean => { + return this.rtcShareScreen?.uid === uid; + }; } export const globalStore = new GlobalStore(); diff --git a/desktop/renderer-app/src/stores/ShareScreenStore/ListenerOtherUserShareScreen.ts b/desktop/renderer-app/src/stores/ShareScreenStore/ListenerOtherUserShareScreen.ts new file mode 100644 index 00000000000..ac67e382e61 --- /dev/null +++ b/desktop/renderer-app/src/stores/ShareScreenStore/ListenerOtherUserShareScreen.ts @@ -0,0 +1,53 @@ +import type AgoraSDK from "agora-electron-sdk"; +import { globalStore } from "../GlobalStore"; + +export class ListenerOtherUserShareScreen { + private readonly existOtherUserStream: (exist: boolean) => void; + private shareScreenUID = NaN; + + public constructor( + private readonly roomClient: AgoraSDK, + private readonly isLocalStream: () => boolean, + onExistOtherUserStream: (exist: boolean) => void, + ) { + this.existOtherUserStream = onExistOtherUserStream; + + this.roomClient.on("userJoined", this.onShareScreenPublish.bind(this)); + this.roomClient.on("userOffline", this.onShareScreenUnPublish.bind(this)); + } + + public render(element: HTMLElement): void { + // this is a bug in agora sdk, when the `desktop` screen sharing is done, + // and then the `web` side does the screen sharing, + // the `desktop` will have a black screen. + // this is because the sdk has `mute` the remote screen sharing stream + this.roomClient.muteRemoteVideoStream(this.shareScreenUID, false); + this.roomClient.setupRemoteVideo(this.shareScreenUID, element, undefined); + this.roomClient.setupViewContentMode(this.shareScreenUID, 1, undefined); + } + + public stop(element: HTMLElement): void { + this.roomClient.destroyRender(this.shareScreenUID, undefined); + this.roomClient.destroyRenderView(this.shareScreenUID, undefined, element); + } + + public destroy(): void { + this.shareScreenUID = NaN; + this.roomClient.off("userJoined", this.onShareScreenPublish.bind(this)); + this.roomClient.off("userOffline", this.onShareScreenUnPublish.bind(this)); + } + + private onShareScreenPublish(uid: number): void { + if (globalStore.isShareScreenUID(uid) && !this.isLocalStream()) { + this.shareScreenUID = uid; + this.existOtherUserStream(true); + } + } + + private onShareScreenUnPublish(uid: number): void { + if (globalStore.isShareScreenUID(uid) && !this.isLocalStream()) { + this.shareScreenUID = NaN; + this.existOtherUserStream(false); + } + } +} diff --git a/desktop/renderer-app/src/stores/ShareScreenStore/index.ts b/desktop/renderer-app/src/stores/ShareScreenStore/index.ts new file mode 100644 index 00000000000..d748b8be285 --- /dev/null +++ b/desktop/renderer-app/src/stores/ShareScreenStore/index.ts @@ -0,0 +1,132 @@ +import { autorun, makeAutoObservable, observable, reaction, runInAction } from "mobx"; +import { RTCShareScreen, ScreenInfo } from "../../apiMiddleware/share-screen"; +import { ListenerOtherUserShareScreen } from "./ListenerOtherUserShareScreen"; + +export class ShareScreenStore { + public enableShareScreenStatus = false; + public rtcShareScreen?: RTCShareScreen; + public listenerOtherUserShareScreen: ListenerOtherUserShareScreen; + public screenInfo: ScreenInfo | null = null; + public existOtherShareScreen = false; + public showShareScreenPicker = false; + public isDisplayScreen: boolean | null = null; + public screenID: number | null = null; + public isWritable = false; + + private element: HTMLElement | null = null; + + public constructor(roomUUID: string) { + makeAutoObservable(this, { + element: observable.ref, + screenInfo: observable.ref, + rtcShareScreen: observable.ref, + listenerOtherUserShareScreen: observable.ref, + }); + + const roomClient = window.rtcEngine; + + this.listenerOtherUserShareScreen = new ListenerOtherUserShareScreen( + roomClient, + () => this.enableShareScreenStatus, + exist => { + runInAction(() => { + this.existOtherShareScreen = exist; + }); + }, + ); + + this.rtcShareScreen = new RTCShareScreen( + roomUUID, + roomClient, + (enable: boolean) => { + runInAction(() => { + this.enableShareScreenStatus = enable; + }); + }, + () => this.existOtherShareScreen, + ); + + reaction( + () => ({ + existOtherShareScreen: this.existOtherShareScreen, + element: this.element, + }), + ({ existOtherShareScreen, element }) => { + if (element === null) { + return; + } + + if (existOtherShareScreen) { + this.listenerOtherUserShareScreen.render(element); + } else { + this.listenerOtherUserShareScreen.stop(element); + } + }, + ); + + autorun(() => { + // when permission is recalled, close share screen + if (!this.isWritable && this.enableShareScreenStatus) { + this.close().catch(console.error); + } + }); + } + + public updateElement(element: HTMLElement): void { + this.element = element; + } + + public updateShowShareScreenPicker(show: boolean): void { + this.showShareScreenPicker = show; + } + + public updateScreenInfo(): void { + // getScreenInfo is a synchronous method that blocks the whole process. + // in order to let the loading component load first, a manual delay is used here. + setTimeout(() => { + runInAction(() => { + if (this.rtcShareScreen) { + this.screenInfo = this.rtcShareScreen.getScreenInfo(); + } else { + this.screenInfo = null; + } + }); + }, 100); + } + + public resetScreenInfo(): void { + this.screenInfo = null; + } + + public updateIsDisplayScreen(isDisplay: boolean | null): void { + this.isDisplayScreen = isDisplay; + } + + public updateScreenID(id: number | null): void { + this.screenID = id; + } + + public updateIsWritable(isWritable: boolean): void { + this.isWritable = isWritable; + } + + public async close(): Promise { + await this.rtcShareScreen?.close(); + } + + public async destroy(): Promise { + this.listenerOtherUserShareScreen.destroy(); + await this.rtcShareScreen?.destroy(); + } + + public enable(): void { + // same as updateScreenInfo + setTimeout(() => { + if (this.rtcShareScreen && this.isDisplayScreen !== null && this.screenID !== null) { + this.rtcShareScreen.enable(this.isDisplayScreen, this.screenID); + } + + this.updateShowShareScreenPicker(false); + }, 500); + } +} diff --git a/packages/flat-i18n/locales/en.json b/packages/flat-i18n/locales/en.json index d6215a85294..524f62a34d8 100644 --- a/packages/flat-i18n/locales/en.json +++ b/packages/flat-i18n/locales/en.json @@ -352,6 +352,8 @@ "set-camera-error": "Failed to turn on camera", "set-mic-error": "Failed to turn on microphone", "share-screen": { - "browser-not-permission": "Please grant your browser access to screen recording" + "browser-not-permission": "Please grant your browser access to screen recording", + "desktop-not-permission": "Please grant Flat access to screen recording", + "choose-share-content": "Choose what to share" } } diff --git a/packages/flat-i18n/locales/zh-CN.json b/packages/flat-i18n/locales/zh-CN.json index 28160b2687c..e3607ee35a9 100644 --- a/packages/flat-i18n/locales/zh-CN.json +++ b/packages/flat-i18n/locales/zh-CN.json @@ -352,6 +352,8 @@ "set-camera-error": "无法检测到摄像头,请检查您的设备后重试", "set-mic-error": "无法检测到麦克风,请检查您的设备后重试", "share-screen": { - "browser-not-permission": "请授予浏览器访问屏幕录制的权限" + "browser-not-permission": "请授予浏览器访问屏幕录制的权限", + "desktop-not-permission": "请授予 Flat 访问屏幕录制的权限", + "choose-share-content": "选择共享内容" } }