From 14c760d8419bd36c90221621a7a9038edffb3116 Mon Sep 17 00:00:00 2001 From: BlackHole1 <158blackhole@gmail.com> Date: Thu, 26 Aug 2021 11:42:45 +0800 Subject: [PATCH 1/9] feat(desktop): support share screen --- cspell.config.js | 1 + .../src/apiMiddleware/flatServer/index.ts | 4 + .../src/apiMiddleware/share-screen.ts | 126 ++++++++++++++++++ .../src/assets/image/share-screen-active.svg | 17 +++ .../src/assets/image/share-screen.svg | 17 +++ .../ShareScreen/ShareScreen/index.tsx | 28 ++++ .../ShareScreen/ShareScreen/style.less | 10 ++ .../ShareScreenPicker/ScreenList/Utils.ts | 31 +++++ .../ShareScreenPicker/ScreenList/index.tsx | 91 +++++++++++++ .../ShareScreenPicker/ScreenList/style.less | 43 ++++++ .../ShareScreen/ShareScreenPicker/index.tsx | 92 +++++++++++++ .../ShareScreen/ShareScreenPicker/style.less | 26 ++++ .../src/components/ShareScreen/index.ts | 2 + .../src/components/Whiteboard.less | 1 - .../src/pages/BigClassPage/BigClassPage.less | 6 + .../src/pages/BigClassPage/index.tsx | 36 ++++- .../src/pages/OneToOnePage/OneToOnePage.less | 6 + .../src/pages/OneToOnePage/index.tsx | 36 ++++- .../pages/SmallClassPage/SmallClassPage.less | 6 + .../src/pages/SmallClassPage/index.tsx | 36 ++++- .../renderer-app/src/stores/ClassRoomStore.ts | 9 ++ .../renderer-app/src/stores/GlobalStore.ts | 10 ++ .../ListenerOtherUserShareScreen.ts | 49 +++++++ .../src/stores/ShareScreenStore/index.ts | 122 +++++++++++++++++ 24 files changed, 801 insertions(+), 4 deletions(-) create mode 100644 desktop/renderer-app/src/apiMiddleware/share-screen.ts create mode 100644 desktop/renderer-app/src/assets/image/share-screen-active.svg create mode 100644 desktop/renderer-app/src/assets/image/share-screen.svg create mode 100644 desktop/renderer-app/src/components/ShareScreen/ShareScreen/index.tsx create mode 100644 desktop/renderer-app/src/components/ShareScreen/ShareScreen/style.less create mode 100644 desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/Utils.ts create mode 100644 desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/index.tsx create mode 100644 desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/style.less create mode 100644 desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx create mode 100644 desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/style.less create mode 100644 desktop/renderer-app/src/components/ShareScreen/index.ts create mode 100644 desktop/renderer-app/src/stores/ShareScreenStore/ListenerOtherUserShareScreen.ts create mode 100644 desktop/renderer-app/src/stores/ShareScreenStore/index.ts 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..02be286e815 --- /dev/null +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/index.tsx @@ -0,0 +1,91 @@ +import "./style.less"; +import React, { useCallback, 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"; + +interface ScreenListProps { + screenInfo: ScreenInfo; + shareScreenStore: ShareScreenStore; +} + +export const ScreenList = observer(function ShareScreen({ + screenInfo, + shareScreenStore, +}) { + const [activeInfo, setActiveInfo] = useState(""); + + 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..421298dba58 --- /dev/null +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx @@ -0,0 +1,92 @@ +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"; + +interface ShareScreenPickerProps { + shareScreenStore: ShareScreenStore; + handleOk: () => void; +} + +const ShareScreenPickerModel = observer(function ShareScreen({ + shareScreenStore, + handleOk, +}) { + const [confirmLoading, setConfirmLoading] = useState(false); + + useLayoutEffect(() => { + shareScreenStore.resetScreenInfo(); + }, [shareScreenStore]); + + useEffect(() => { + shareScreenStore.updateScreenInfo(); + }, [shareScreenStore]); + + const closeModal = useCallback(() => { + shareScreenStore.updateShowShareScreenPicker(false); + }, [shareScreenStore]); + + const isSelected = + shareScreenStore.isDisplayScreen !== null && shareScreenStore.screenID !== null; + + return ( +
+ + 取消 + , + , + ]} + > +
+ {shareScreenStore.screenInfo ? ( + + ) : ( + + )} +
+
+
+ ); +}); + +export const ShareScreenPicker = observer(function ShareScreen({ + shareScreenStore, + handleOk, +}) { + return shareScreenStore.showShareScreenPicker ? ( + + ) : null; +}); 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..600feca8a34 --- /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 { + 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..5c795e6f954 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(); + shareScreenStore.updateShowShareScreenPicker(false); + }} + /> + +
{renderRealtimePanel()}
@@ -274,6 +295,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(); + shareScreenStore.updateShowShareScreenPicker(false); + }} + /> + +
{renderRealtimePanel()}
@@ -232,6 +253,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(); + shareScreenStore.updateShowShareScreenPicker(false); + }} + /> + +
{renderRealtimePanel()}
@@ -299,6 +320,19 @@ export const SmallClassPage = observer(function SmallClassP } /> )} + + {whiteboardStore.isWritable && !shareScreenStore.existOtherShareScreen && ( + + )} + {whiteboardStore.isWritable && ( { if (this.whiteboardStore.isKicked) { reaction.dispose(); @@ -159,6 +164,10 @@ export class ClassRoomStore { } }); + autorun(() => { + this.shareScreenStore.updateIsWritable(this.whiteboardStore.isWritable); + }); + this.rtm.once(RTMessageType.REMOTE_LOGIN, () => { runInAction(() => { this.isRemoteLogin = true; 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..f59d5f75070 --- /dev/null +++ b/desktop/renderer-app/src/stores/ShareScreenStore/ListenerOtherUserShareScreen.ts @@ -0,0 +1,49 @@ +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.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..6b86d9e5050 --- /dev/null +++ b/desktop/renderer-app/src/stores/ShareScreenStore/index.ts @@ -0,0 +1,122 @@ +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, + }); + + 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 enable(): void { + if (!this.rtcShareScreen || this.isDisplayScreen === null || this.screenID === null) { + return; + } + + this.rtcShareScreen.enable(this.isDisplayScreen, this.screenID); + } +} From 106eae17a197fce20592745b1e14221abf27bfea Mon Sep 17 00:00:00 2001 From: BlackHole1 <158blackhole@gmail.com> Date: Thu, 26 Aug 2021 12:57:33 +0800 Subject: [PATCH 2/9] refactor(desktop): add comment in share screen --- .../stores/ShareScreenStore/ListenerOtherUserShareScreen.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/desktop/renderer-app/src/stores/ShareScreenStore/ListenerOtherUserShareScreen.ts b/desktop/renderer-app/src/stores/ShareScreenStore/ListenerOtherUserShareScreen.ts index f59d5f75070..ac67e382e61 100644 --- a/desktop/renderer-app/src/stores/ShareScreenStore/ListenerOtherUserShareScreen.ts +++ b/desktop/renderer-app/src/stores/ShareScreenStore/ListenerOtherUserShareScreen.ts @@ -17,6 +17,10 @@ export class ListenerOtherUserShareScreen { } 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); From cbb85e0e1d51084effd8b4620aad450653fb99b9 Mon Sep 17 00:00:00 2001 From: BlackHole1 <158blackhole@gmail.com> Date: Thu, 26 Aug 2021 14:43:50 +0800 Subject: [PATCH 3/9] refactor(i18n): add i18n text in desktop screen share --- .../components/ShareScreen/ShareScreenPicker/index.tsx | 8 +++++--- packages/flat-i18n/locales/en.json | 3 ++- packages/flat-i18n/locales/zh-CN.json | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx index 421298dba58..60261ebc05c 100644 --- a/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx @@ -5,6 +5,7 @@ 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; @@ -16,6 +17,7 @@ const ShareScreenPickerModel = observer(function ShareSc handleOk, }) { const [confirmLoading, setConfirmLoading] = useState(false); + const { t } = useTranslation(); useLayoutEffect(() => { shareScreenStore.resetScreenInfo(); @@ -35,7 +37,7 @@ const ShareScreenPickerModel = observer(function ShareSc return (
(function ShareSc width="784px" footer={[ , , ]} > diff --git a/packages/flat-i18n/locales/en.json b/packages/flat-i18n/locales/en.json index d6215a85294..18bcbc71989 100644 --- a/packages/flat-i18n/locales/en.json +++ b/packages/flat-i18n/locales/en.json @@ -352,6 +352,7 @@ "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", + "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..39e32070595 100644 --- a/packages/flat-i18n/locales/zh-CN.json +++ b/packages/flat-i18n/locales/zh-CN.json @@ -352,6 +352,7 @@ "set-camera-error": "无法检测到摄像头,请检查您的设备后重试", "set-mic-error": "无法检测到麦克风,请检查您的设备后重试", "share-screen": { - "browser-not-permission": "请授予浏览器访问屏幕录制的权限" + "browser-not-permission": "请授予浏览器访问屏幕录制的权限", + "choose-share-content": "选择共享内容" } } From 8b98647c64ec322faf816e47c1c7d639567a03e0 Mon Sep 17 00:00:00 2001 From: BlackHole1 <158blackhole@gmail.com> Date: Thu, 26 Aug 2021 15:16:23 +0800 Subject: [PATCH 4/9] fix(desktop): share screen loading invalid --- .../ShareScreen/ShareScreenPicker/index.tsx | 44 +++++++++++++------ .../ShareScreen/ShareScreenPicker/style.less | 2 +- .../src/pages/BigClassPage/index.tsx | 1 - .../src/pages/OneToOnePage/index.tsx | 1 - .../src/pages/SmallClassPage/index.tsx | 1 - .../src/stores/ShareScreenStore/index.ts | 11 +++-- 6 files changed, 38 insertions(+), 22 deletions(-) diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx index 60261ebc05c..6faa9812d87 100644 --- a/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/index.tsx @@ -16,7 +16,6 @@ const ShareScreenPickerModel = observer(function ShareSc shareScreenStore, handleOk, }) { - const [confirmLoading, setConfirmLoading] = useState(false); const { t } = useTranslation(); useLayoutEffect(() => { @@ -50,19 +49,7 @@ const ShareScreenPickerModel = observer(function ShareSc , - , + , ]} >
(function Share ) : 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 index 600feca8a34..3a3898d15b7 100644 --- a/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/style.less +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/style.less @@ -9,7 +9,7 @@ } .footer-button { - width: 86px; + min-width: 86px; height: 32px; } } diff --git a/desktop/renderer-app/src/pages/BigClassPage/index.tsx b/desktop/renderer-app/src/pages/BigClassPage/index.tsx index 5c795e6f954..b90ba9228ff 100644 --- a/desktop/renderer-app/src/pages/BigClassPage/index.tsx +++ b/desktop/renderer-app/src/pages/BigClassPage/index.tsx @@ -195,7 +195,6 @@ export const BigClassPage = observer(function BigClassPage() shareScreenStore={shareScreenStore} handleOk={() => { shareScreenStore.enable(); - shareScreenStore.updateShowShareScreenPicker(false); }} /> diff --git a/desktop/renderer-app/src/pages/OneToOnePage/index.tsx b/desktop/renderer-app/src/pages/OneToOnePage/index.tsx index 75ae52c4168..49c7fae40cd 100644 --- a/desktop/renderer-app/src/pages/OneToOnePage/index.tsx +++ b/desktop/renderer-app/src/pages/OneToOnePage/index.tsx @@ -153,7 +153,6 @@ export const OneToOnePage = observer(function OneToOnePage() shareScreenStore={shareScreenStore} handleOk={() => { shareScreenStore.enable(); - shareScreenStore.updateShowShareScreenPicker(false); }} /> diff --git a/desktop/renderer-app/src/pages/SmallClassPage/index.tsx b/desktop/renderer-app/src/pages/SmallClassPage/index.tsx index daa3397da1f..78d11aa49a4 100644 --- a/desktop/renderer-app/src/pages/SmallClassPage/index.tsx +++ b/desktop/renderer-app/src/pages/SmallClassPage/index.tsx @@ -175,7 +175,6 @@ export const SmallClassPage = observer(function SmallClassP shareScreenStore={shareScreenStore} handleOk={() => { shareScreenStore.enable(); - shareScreenStore.updateShowShareScreenPicker(false); }} /> diff --git a/desktop/renderer-app/src/stores/ShareScreenStore/index.ts b/desktop/renderer-app/src/stores/ShareScreenStore/index.ts index 6b86d9e5050..767ae4cb7a2 100644 --- a/desktop/renderer-app/src/stores/ShareScreenStore/index.ts +++ b/desktop/renderer-app/src/stores/ShareScreenStore/index.ts @@ -113,10 +113,13 @@ export class ShareScreenStore { } public enable(): void { - if (!this.rtcShareScreen || this.isDisplayScreen === null || this.screenID === null) { - return; - } + // same as updateScreenInfo + setTimeout(() => { + if (this.rtcShareScreen && this.isDisplayScreen !== null && this.screenID !== null) { + this.rtcShareScreen.enable(this.isDisplayScreen, this.screenID); + } - this.rtcShareScreen.enable(this.isDisplayScreen, this.screenID); + this.updateShowShareScreenPicker(false); + }, 500); } } From 2d979cc78c1c91767b79a5900706ab71bebfaf28 Mon Sep 17 00:00:00 2001 From: BlackHole1 <158blackhole@gmail.com> Date: Thu, 26 Aug 2021 15:24:33 +0800 Subject: [PATCH 5/9] refactor(desktop): miss destroy share screen --- desktop/renderer-app/src/stores/ClassRoomStore.ts | 2 ++ desktop/renderer-app/src/stores/ShareScreenStore/index.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/desktop/renderer-app/src/stores/ClassRoomStore.ts b/desktop/renderer-app/src/stores/ClassRoomStore.ts index 5176ecf568f..e230b72c981 100644 --- a/desktop/renderer-app/src/stores/ClassRoomStore.ts +++ b/desktop/renderer-app/src/stores/ClassRoomStore.ts @@ -535,6 +535,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/ShareScreenStore/index.ts b/desktop/renderer-app/src/stores/ShareScreenStore/index.ts index 767ae4cb7a2..4787353082f 100644 --- a/desktop/renderer-app/src/stores/ShareScreenStore/index.ts +++ b/desktop/renderer-app/src/stores/ShareScreenStore/index.ts @@ -112,6 +112,11 @@ export class ShareScreenStore { await this.rtcShareScreen?.close(); } + public async destroy(): Promise { + this.listenerOtherUserShareScreen.destroy(); + await this.rtcShareScreen?.destroy(); + } + public enable(): void { // same as updateScreenInfo setTimeout(() => { From 7004b801974390fbd23be8a387896b88e54b069e Mon Sep 17 00:00:00 2001 From: BlackHole1 <158blackhole@gmail.com> Date: Thu, 26 Aug 2021 15:27:21 +0800 Subject: [PATCH 6/9] refactor(desktop): disable some mobx member in share screen --- desktop/renderer-app/src/stores/ShareScreenStore/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desktop/renderer-app/src/stores/ShareScreenStore/index.ts b/desktop/renderer-app/src/stores/ShareScreenStore/index.ts index 4787353082f..d748b8be285 100644 --- a/desktop/renderer-app/src/stores/ShareScreenStore/index.ts +++ b/desktop/renderer-app/src/stores/ShareScreenStore/index.ts @@ -19,6 +19,8 @@ export class ShareScreenStore { makeAutoObservable(this, { element: observable.ref, screenInfo: observable.ref, + rtcShareScreen: observable.ref, + listenerOtherUserShareScreen: observable.ref, }); const roomClient = window.rtcEngine; From a33a279a7e8ffa7616ddf9802d19f90cc5ca35bf Mon Sep 17 00:00:00 2001 From: BlackHole1 <158blackhole@gmail.com> Date: Thu, 26 Aug 2021 16:24:18 +0800 Subject: [PATCH 7/9] fix(desktop): mobx warning when set ShareScreenStore.isWritable --- desktop/renderer-app/src/stores/ClassRoomStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/renderer-app/src/stores/ClassRoomStore.ts b/desktop/renderer-app/src/stores/ClassRoomStore.ts index e230b72c981..fe144abfbe3 100644 --- a/desktop/renderer-app/src/stores/ClassRoomStore.ts +++ b/desktop/renderer-app/src/stores/ClassRoomStore.ts @@ -164,7 +164,7 @@ export class ClassRoomStore { } }); - autorun(() => { + runInAction(() => { this.shareScreenStore.updateIsWritable(this.whiteboardStore.isWritable); }); From dce8020ff3e11bc428479a32536728abc968f4ec Mon Sep 17 00:00:00 2001 From: BlackHole1 <158blackhole@gmail.com> Date: Thu, 26 Aug 2021 17:04:33 +0800 Subject: [PATCH 8/9] refactor(desktop): add not share screen permission tips --- .../ShareScreenPicker/ScreenList/index.tsx | 11 ++++++++++- packages/flat-i18n/locales/en.json | 1 + packages/flat-i18n/locales/zh-CN.json | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/index.tsx b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/index.tsx index 02be286e815..4b5eab134fe 100644 --- a/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/index.tsx +++ b/desktop/renderer-app/src/components/ShareScreen/ShareScreenPicker/ScreenList/index.tsx @@ -1,10 +1,12 @@ import "./style.less"; -import React, { useCallback, useState } from "react"; +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; @@ -16,6 +18,13 @@ export const ScreenList = observer(function ShareScreen({ 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) => { diff --git a/packages/flat-i18n/locales/en.json b/packages/flat-i18n/locales/en.json index 18bcbc71989..524f62a34d8 100644 --- a/packages/flat-i18n/locales/en.json +++ b/packages/flat-i18n/locales/en.json @@ -353,6 +353,7 @@ "set-mic-error": "Failed to turn on microphone", "share-screen": { "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 39e32070595..e3607ee35a9 100644 --- a/packages/flat-i18n/locales/zh-CN.json +++ b/packages/flat-i18n/locales/zh-CN.json @@ -353,6 +353,7 @@ "set-mic-error": "无法检测到麦克风,请检查您的设备后重试", "share-screen": { "browser-not-permission": "请授予浏览器访问屏幕录制的权限", + "desktop-not-permission": "请授予 Flat 访问屏幕录制的权限", "choose-share-content": "选择共享内容" } } From bd2a96b748d7adf7d112e24fbb58bf0c99026ef0 Mon Sep 17 00:00:00 2001 From: BlackHole1 <158blackhole@gmail.com> Date: Thu, 26 Aug 2021 17:43:23 +0800 Subject: [PATCH 9/9] fix(desktop): improve shareScreenStore isWritable --- desktop/renderer-app/src/stores/ClassRoomStore.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/desktop/renderer-app/src/stores/ClassRoomStore.ts b/desktop/renderer-app/src/stores/ClassRoomStore.ts index fe144abfbe3..8984ff9ce65 100644 --- a/desktop/renderer-app/src/stores/ClassRoomStore.ts +++ b/desktop/renderer-app/src/stores/ClassRoomStore.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx"; +import { action, autorun, makeAutoObservable, observable, reaction, runInAction } from "mobx"; import { v4 as uuidv4 } from "uuid"; import dateSub from "date-fns/sub"; import { Rtc as RTCAPI, RtcChannelType } from "../apiMiddleware/Rtc"; @@ -164,9 +164,15 @@ export class ClassRoomStore { } }); - runInAction(() => { - this.shareScreenStore.updateIsWritable(this.whiteboardStore.isWritable); - }); + reaction( + () => this.whiteboardStore.isWritable, + () => { + this.shareScreenStore.updateIsWritable(this.whiteboardStore.isWritable); + }, + { + fireImmediately: true, + }, + ); this.rtm.once(RTMessageType.REMOTE_LOGIN, () => { runInAction(() => {