Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): support share screen #881

Merged
merged 2 commits into from
Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/flat-i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -350,5 +350,8 @@
"nav-settings": "Settings",
"servers": "Server",
"set-camera-error": "Failed to turn on camera",
"set-mic-error": "Failed to turn on microphone"
"set-mic-error": "Failed to turn on microphone",
"share-screen": {
"browser-not-permission": "Please grant your browser access to screen recording"
}
}
5 changes: 4 additions & 1 deletion packages/flat-i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -350,5 +350,8 @@
"nav-settings": "设置",
"servers": "服务器",
"set-camera-error": "无法检测到摄像头,请检查您的设备后重试",
"set-mic-error": "无法检测到麦克风,请检查您的设备后重试"
"set-mic-error": "无法检测到麦克风,请检查您的设备后重试",
"share-screen": {
"browser-not-permission": "请授予浏览器访问屏幕录制的权限"
}
}
3 changes: 2 additions & 1 deletion web/flat-web/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ module.exports = {
"no-unused-vars": "off",
"no-useless-computed-key": "warn",
"no-useless-concat": "warn",
"no-useless-constructor": "warn",
"no-useless-constructor": "off",
"no-useless-escape": "warn",
"no-useless-rename": [
"warn",
Expand Down Expand Up @@ -205,6 +205,7 @@ module.exports = {
"@typescript-eslint/no-redeclare": "off",
"no-use-before-define": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-useless-constructor": ["error"],
},

overrides: [
Expand Down
2 changes: 0 additions & 2 deletions web/flat-web/src/apiMiddleware/Rtm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,5 +475,3 @@ export class Rtm extends EventEmitter<keyof RTMEvents> {
return response.json();
}
}

export default Rtm;
4 changes: 4 additions & 0 deletions web/flat-web/src/apiMiddleware/flatServer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ export interface JoinRoomResult {
whiteboardRoomUUID: string; // 白板的 room uuid
rtcUID: number; // rtc 的 uid
rtcToken: string; // rtc token
rtcShareScreen: {
uid: number;
token: string;
};
rtmToken: string; // rtm token
}

Expand Down
4 changes: 3 additions & 1 deletion web/flat-web/src/apiMiddleware/rtc/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class RtcRoom {
isCreator: boolean;
rtcUID: number;
channelType: RtcChannelType;
}): Promise<void> {
}): Promise<IAgoraRTCClient> {
if (this.client) {
await this.destroy();
}
Expand All @@ -54,6 +54,8 @@ export class RtcRoom {
await this.client.join(AGORA.APP_ID, roomUUID, token, rtcUID);

this.roomUUID = roomUUID;

return this.client;
}

public getLatency(): number {
Expand Down
97 changes: 97 additions & 0 deletions web/flat-web/src/apiMiddleware/rtc/share-screen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import AgoraRTC, { IAgoraRTCClient } from "agora-rtc-sdk-ng";
import type { ILocalVideoTrack } from "agora-rtc-sdk-ng";
import { AGORA } from "../../constants/Process";
import { globalStore } from "../../stores/GlobalStore";
import EventEmitter from "eventemitter3";

export class RTCShareScreen {
public shareScreenClient?: IAgoraRTCClient;

private localScreenTrack?: ILocalVideoTrack;

public constructor(
private readonly roomUUID: string,
private readonly enableShareScreenStatus: (enable: boolean) => void,
private readonly existOtherUserStream: () => boolean,
) {}

public async enable(): Promise<void> {
try {
if (!globalStore.rtcShareScreen) {
return;
}

this.localScreenTrack = await AgoraRTC.createScreenVideoTrack({}, "disable");

this.localScreenTrack.once("track-ended", () => {
// user click browser comes with cancel button
this.close();
});

this.shareScreenClient = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });

this.assertNotHasOtherUserShareScreenStream();

await this.shareScreenClient.join(
AGORA.APP_ID,
this.roomUUID,
globalStore.rtcShareScreen.token,
globalStore.rtcShareScreen.uid,
);

await this.shareScreenClient.publish(this.localScreenTrack);

this.enableShareScreenStatus(true);
} catch (error) {
if (RTCShareScreen.isAgoraRTCPermissionError(error)) {
if (RTCShareScreen.browserNotPermission(error.message)) {
shareScreenEvents.emit("browserNotPermission");
} else {
// user click cancel
}
} else {
console.error(error);
}

this.close().catch(console.error);
}
}

public async close(): Promise<void> {
try {
if (this.localScreenTrack) {
await this.localScreenTrack.close();

if (this.shareScreenClient) {
await this.shareScreenClient.unpublish(this.localScreenTrack);
}

this.localScreenTrack = undefined;
}

if (this.shareScreenClient) {
await this.shareScreenClient.leave();
}
} catch (error) {
console.error(error);
} finally {
this.enableShareScreenStatus(false);
}
}

private assertNotHasOtherUserShareScreenStream(): void {
if (this.existOtherUserStream()) {
throw new Error("exist other user share screen stream");
}
}

private static isAgoraRTCPermissionError(error: any): error is Error {
return "code" in error && "message" in error && error.code === "PERMISSION_DENIED";
}

private static browserNotPermission(message: string): boolean {
return message.indexOf("by system") !== -1;
}
}

export const shareScreenEvents = new EventEmitter<"browserNotPermission">();
44 changes: 44 additions & 0 deletions web/flat-web/src/components/ShareScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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";
import { message } from "antd";
import { shareScreenEvents } from "../../apiMiddleware/rtc/share-screen";
import { useTranslation } from "react-i18next";

interface ShareScreenProps {
shareScreenStore: ShareScreenStore;
}

export const ShareScreen = observer<ShareScreenProps>(function ShareScreen({ shareScreenStore }) {
const ref = useRef<HTMLDivElement>(null);
const { t } = useTranslation();

useEffect(() => {
if (ref.current) {
shareScreenStore.updateElement(ref.current);
}
}, [shareScreenStore]);

useEffect(() => {
const onBrowserNotPermission = (): void => {
void message.error(t("share-screen.browser-not-permission"));
};

shareScreenEvents.on("browserNotPermission", onBrowserNotPermission);

return () => {
shareScreenEvents.off("browserNotPermission", onBrowserNotPermission);
};
}, [t]);

const classNameList = useMemo(() => {
return classNames("share-screen", {
active: shareScreenStore.existOtherUserStream,
});
}, [shareScreenStore.existOtherUserStream]);

return <div className={classNameList} ref={ref} />;
});
9 changes: 9 additions & 0 deletions web/flat-web/src/components/ShareScreen/style.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.share-screen {
position: relative;
z-index: 5;

&.active {
width: 100%;
height: 100%;
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions web/flat-web/src/components/TopBarRightBtn/icons/share-screen.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions web/flat-web/src/components/TopBarRightBtn/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import followActiveSVG from "./icons/follow-active.svg";
import followSVG from "./icons/follow.svg";
import hideSideActiveSVG from "./icons/hide-side-active.svg";
import hideSideSVG from "./icons/hide-side.svg";
import shareScreenSVG from "./icons/share-screen.svg";
import shareScreenActiveSVG from "./icons/share-screen-active.svg";

const Icons = {
exit: exitSVG,
"follow-active": followActiveSVG,
follow: followSVG,
"hide-side-active": hideSideActiveSVG,
"hide-side": hideSideSVG,
"share-screen": shareScreenSVG,
"share-screen-active": shareScreenActiveSVG,
};

export interface TopBarRightBtnProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
Expand Down
1 change: 0 additions & 1 deletion web/flat-web/src/components/Whiteboard.less
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.whiteboard-container {
position: relative;
width: 100%;
height: 100%;

Expand Down
6 changes: 6 additions & 0 deletions web/flat-web/src/pages/BigClassPage/BigClassPage.less
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
overflow: hidden;
display: flex;
flex-direction: column;

.container {
position: relative;
width: 100%;
height: 100%;
}
}

.realtime-content {
Expand Down
27 changes: 26 additions & 1 deletion web/flat-web/src/pages/BigClassPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { useAutoRun, useReaction } from "../../utils/mobx";
import { RouteNameType, RouteParams } from "../../utils/routes";
import { runtime } from "../../utils/runtime";
import { BigClassAvatar } from "./BigClassAvatar";
import { ShareScreen } from "../../components/ShareScreen";

const recordingConfig: RecordingConfig = Object.freeze({
channelType: RtcChannelType.Broadcast,
Expand Down Expand Up @@ -85,8 +86,11 @@ export const BigClassPage = observer<BigClassPageProps>(function BigClassPage()
const params = useParams<RouteParams<RouteNameType.BigClassPage>>();

const classRoomStore = useClassRoomStore(params.roomUUID, params.ownerUUID, recordingConfig);
const shareScreenStore = classRoomStore.shareScreenStore;

const whiteboardStore = classRoomStore.whiteboardStore;
const globalStore = useContext(GlobalStoreContext);

const { confirm, ...exitConfirmModalProps } = useExitRoomConfirmModal(classRoomStore);

const [speakingJoiner, setSpeakingJoiner] = useState<User | undefined>(() =>
Expand Down Expand Up @@ -175,7 +179,10 @@ export const BigClassPage = observer<BigClassPageProps>(function BigClassPage()
right={renderTopBarRight()}
/>
<div className="realtime-content">
<Whiteboard whiteboardStore={whiteboardStore} />
<div className="container">
<ShareScreen shareScreenStore={shareScreenStore} />
<Whiteboard whiteboardStore={whiteboardStore} />
</div>
{renderRealtimePanel()}
</div>
<ExitRoomConfirm isCreator={classRoomStore.isCreator} {...exitConfirmModalProps} />
Expand Down Expand Up @@ -271,6 +278,19 @@ export const BigClassPage = observer<BigClassPageProps>(function BigClassPage()
}
/>
)}

{whiteboardStore.isWritable && !shareScreenStore.existOtherUserStream && (
<TopBarRightBtn
title="Share Screen"
icon={
shareScreenStore.enableShareScreenStatus
? "share-screen-active"
: "share-screen"
}
onClick={handleShareScreen}
/>
)}

{whiteboardStore.isWritable && (
<TopBarRightBtn
title="Vision control"
Expand All @@ -279,6 +299,7 @@ export const BigClassPage = observer<BigClassPageProps>(function BigClassPage()
onClick={handleRoomController}
/>
)}

{/* <TopBarRightBtn
title="Docs center"
icon="folder"
Expand Down Expand Up @@ -378,6 +399,10 @@ export const BigClassPage = observer<BigClassPageProps>(function BigClassPage()
}
}

function handleShareScreen(): void {
void shareScreenStore.toggle();
}

function handleSideOpenerSwitch(): void {
openRealtimeSide(isRealtimeSideOpen => !isRealtimeSideOpen);
}
Expand Down
6 changes: 6 additions & 0 deletions web/flat-web/src/pages/OneToOnePage/OneToOnePage.less
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
overflow: hidden;
display: flex;
flex-direction: column;

.container {
position: relative;
width: 100%;
height: 100%;
}
}

.one-to-one-realtime-content {
Expand Down
Loading