diff --git a/web/flat-web/package.json b/web/flat-web/package.json
index e8f8736b51f..b348ee12b8d 100644
--- a/web/flat-web/package.json
+++ b/web/flat-web/package.json
@@ -32,6 +32,7 @@
"@ant-design/icons": "^4.6.2",
"@loadable/component": "^5.15.0",
"agora-rtc-sdk-ng": "^4.5.0",
+ "agora-rtm-sdk": "^1.4.3",
"antd": "^4.15.4",
"eventemitter3": "^4.0.7",
"mobx-react-lite": "^3.2.0",
@@ -42,7 +43,7 @@
"white-web-sdk": "^2.12.15"
},
"scripts": {
- "postinstall": "esbuild-dev ./scripts/white-web-sdk.ts",
+ "postinstall": "esbuild-dev ./scripts/post-install.ts",
"start": "vite --open",
"build": "vite build",
"serve": "vite preview",
diff --git a/web/flat-web/scripts/agora-rtm-sdk.ts b/web/flat-web/scripts/agora-rtm-sdk.ts
new file mode 100644
index 00000000000..74401f6f070
--- /dev/null
+++ b/web/flat-web/scripts/agora-rtm-sdk.ts
@@ -0,0 +1,17 @@
+/**
+ * Get rid of "process.env.NODE_ENV" replace error in agora-rtm-sdk.
+ *
+ * @TODO: Remove this file when agora-rtm-sdk fix the code.
+ */
+
+///
+
+// https://vitejs.dev/guide/env-and-mode.html#production-replacement
+
+import fs from "fs";
+// NOTE: `import.meta.resolve` is still experimental
+const file = require.resolve("agora-rtm-sdk");
+const code = fs.readFileSync(file, "utf-8");
+const modified = code.replace("process.env.NODE_ENV", "process\u200b.env.NODE_ENV");
+fs.writeFileSync(file, modified);
+console.log("agora-rtm-sdk: done!");
diff --git a/web/flat-web/scripts/post-install.ts b/web/flat-web/scripts/post-install.ts
new file mode 100644
index 00000000000..37d2148b519
--- /dev/null
+++ b/web/flat-web/scripts/post-install.ts
@@ -0,0 +1,2 @@
+import "./white-web-sdk";
+import "./agora-rtm-sdk";
diff --git a/web/flat-web/scripts/white-web-sdk.ts b/web/flat-web/scripts/white-web-sdk.ts
index 9f365fc8c9a..8f63027fbed 100644
--- a/web/flat-web/scripts/white-web-sdk.ts
+++ b/web/flat-web/scripts/white-web-sdk.ts
@@ -47,7 +47,7 @@ function hackAndReplaceMainScript(script: string, main: string): void {
// webpack will add polyfill under the hood so it is ok, but not rollup/esbuild
script = script.replace(/=require\([^)]+\)/g, "=void 0");
fs.writeFileSync(path.resolve(sdkPath, main), script);
- console.log("hack: done!");
+ console.log("white-web-sdk: done!");
}
if (fs.existsSync(pkgJSON)) {
diff --git a/web/flat-web/src/apiMiddleware/CloudRecording.ts b/web/flat-web/src/apiMiddleware/CloudRecording.ts
index 12fecd57432..5ffef5ed731 100644
--- a/web/flat-web/src/apiMiddleware/CloudRecording.ts
+++ b/web/flat-web/src/apiMiddleware/CloudRecording.ts
@@ -76,7 +76,7 @@ export class CloudRecording {
height: 360,
fps: 15,
bitrate: 500,
- defaultUserBackgroundImage: process.env.CLOUD_RECORDING_DEFAULT_AVATAR,
+ defaultUserBackgroundImage: import.meta.env.CLOUD_RECORDING_DEFAULT_AVATAR,
},
},
},
diff --git a/web/flat-web/src/apiMiddleware/Rtm.ts b/web/flat-web/src/apiMiddleware/Rtm.ts
index 6b59ceecaea..58a63bb5210 100644
--- a/web/flat-web/src/apiMiddleware/Rtm.ts
+++ b/web/flat-web/src/apiMiddleware/Rtm.ts
@@ -2,7 +2,7 @@ import AgoraRTM, { RtmChannel, RtmClient } from "agora-rtm-sdk";
import polly from "polly-js";
import { v4 as uuidv4 } from "uuid";
import { AGORA, NODE_ENV } from "../constants/Process";
-import { EventEmitter } from "events";
+import { EventEmitter } from "eventemitter3";
import { RoomStatus } from "./flatServer/constants";
import { generateRTMToken } from "./flatServer/agora";
import { globalStore } from "../stores/GlobalStore";
@@ -145,7 +145,7 @@ export declare interface Rtm {
}
// eslint-disable-next-line no-redeclare
-export class Rtm extends EventEmitter {
+export class Rtm extends EventEmitter {
public static MessageType = AgoraRTM.MessageType;
public client: RtmClient;
diff --git a/web/flat-web/src/apiMiddleware/rtc/avatar.ts b/web/flat-web/src/apiMiddleware/rtc/avatar.ts
new file mode 100644
index 00000000000..a6bab91cb6a
--- /dev/null
+++ b/web/flat-web/src/apiMiddleware/rtc/avatar.ts
@@ -0,0 +1,154 @@
+import type {
+ IAgoraRTCClient,
+ IAgoraRTCRemoteUser,
+ ICameraVideoTrack,
+ IMicrophoneAudioTrack,
+ IRemoteAudioTrack,
+ IRemoteVideoTrack,
+ ITrack,
+} from "agora-rtc-sdk-ng";
+import AgoraRTC from "agora-rtc-sdk-ng";
+import type { User } from "../../stores/UserStore";
+import type { RtcRoom } from "./room";
+
+export interface RtcAvatarParams {
+ rtc: RtcRoom;
+ userUUID: string;
+ avatarUser: User;
+}
+
+/**
+ * @example
+ * const avatar = new RtcAvatar({ rtc, userUUID, avatarUser })
+ * avatar.element = el
+ * avatar.setCamera(true)
+ */
+export class RtcAvatar {
+ private readonly rtc: RtcRoom;
+
+ public readonly userUUID: string;
+ public readonly avatarUser: User;
+ public element?: HTMLElement;
+ public audioTrack?: ITrack;
+ public videoTrack?: ITrack;
+
+ private readonly isLocal: boolean;
+ private readonly remoteAudioTrack: Promise;
+ private readonly remoteVideoTrack: Promise;
+
+ private resolveRemoteAudioTrack?: (value: IRemoteAudioTrack) => void;
+ private resolveRemoteVideoTrack?: (value: IRemoteVideoTrack) => void;
+
+ constructor({ rtc, userUUID, avatarUser }: RtcAvatarParams) {
+ this.rtc = rtc;
+ this.userUUID = userUUID;
+ this.avatarUser = avatarUser;
+ this.isLocal = userUUID === avatarUser.userUUID;
+ this.remoteAudioTrack = new Promise(resolve => {
+ this.resolveRemoteAudioTrack = resolve;
+ });
+ this.remoteVideoTrack = new Promise(resolve => {
+ this.resolveRemoteVideoTrack = resolve;
+ });
+ if (!this.isLocal) {
+ this.setupExistingTracks();
+ this.client.on("user-published", this.onUserPublished);
+ }
+ }
+
+ private get client(): IAgoraRTCClient {
+ return this.rtc.client!;
+ }
+
+ private async setupExistingTracks(): Promise {
+ const exist = this.client.remoteUsers.find(e => e.uid === this.avatarUser.rtcUID);
+ if (exist) {
+ if (exist.hasAudio) {
+ const audioTrack = await this.client.subscribe(exist, "audio");
+ this.resolveRemoteAudioTrack?.(audioTrack);
+ this.resolveRemoteAudioTrack = undefined;
+ }
+ if (exist.hasVideo) {
+ const videoTrack = await this.client.subscribe(exist, "video");
+ this.resolveRemoteVideoTrack?.(videoTrack);
+ this.resolveRemoteVideoTrack = undefined;
+ }
+ }
+ }
+
+ public destroy(): void {
+ if (!this.isLocal && this.client) {
+ this.client.off("user-published", this.onUserPublished);
+ }
+ this.resolveRemoteAudioTrack = undefined;
+ this.resolveRemoteVideoTrack = undefined;
+ }
+
+ private onUserPublished = async (
+ user: IAgoraRTCRemoteUser,
+ mediaType: "video" | "audio",
+ ): Promise => {
+ if (user.uid === this.avatarUser.rtcUID) {
+ const track = await this.client.subscribe(user, mediaType);
+ if (mediaType === "audio") {
+ this.resolveRemoteAudioTrack?.(track as IRemoteAudioTrack);
+ this.resolveRemoteAudioTrack = undefined;
+ } else {
+ this.resolveRemoteVideoTrack?.(track as IRemoteVideoTrack);
+ this.resolveRemoteVideoTrack = undefined;
+ }
+ }
+ };
+
+ public async setCamera(enable: boolean): Promise {
+ try {
+ if (this.isLocal) {
+ const videoTrack = this.videoTrack as ICameraVideoTrack | undefined;
+ if (videoTrack) {
+ videoTrack.setEnabled(enable);
+ } else if (enable) {
+ const videoTrack = await AgoraRTC.createCameraVideoTrack({
+ encoderConfig: { width: 288, height: 216 },
+ });
+ this.videoTrack = videoTrack;
+ this.element && videoTrack.play(this.element);
+ await this.client.publish([videoTrack]);
+ }
+ } else {
+ if (!this.videoTrack && enable) {
+ const videoTrack = await this.remoteVideoTrack;
+ this.videoTrack = videoTrack;
+ this.element && videoTrack.play(this.element);
+ }
+ }
+ } catch (error) {
+ console.info("setCamera failed", error);
+ }
+ }
+
+ public async setMic(enable: boolean): Promise {
+ try {
+ if (this.isLocal) {
+ const audioTrack = this.audioTrack as IMicrophoneAudioTrack | undefined;
+ if (audioTrack) {
+ audioTrack.setEnabled(enable);
+ } else if (enable) {
+ const audioTrack = await AgoraRTC.createMicrophoneAudioTrack();
+ this.audioTrack = audioTrack;
+ audioTrack.play();
+ await this.client.publish(audioTrack);
+ }
+ } else {
+ if (!this.audioTrack && enable) {
+ const audioTrack = await this.remoteAudioTrack;
+ this.audioTrack = audioTrack;
+ audioTrack.play();
+ }
+ }
+ } catch (error) {
+ console.info("setMic failed", error);
+ }
+ }
+}
+
+(window as any).RtcAvatar = RtcAvatar;
diff --git a/web/flat-web/src/apiMiddleware/rtc/room.ts b/web/flat-web/src/apiMiddleware/rtc/room.ts
new file mode 100644
index 00000000000..6281a2b27f7
--- /dev/null
+++ b/web/flat-web/src/apiMiddleware/rtc/room.ts
@@ -0,0 +1,75 @@
+import AgoraRTC, { IAgoraRTCClient } from "agora-rtc-sdk-ng";
+import { AGORA } from "../../constants/Process";
+import { globalStore } from "../../stores/GlobalStore";
+import { generateRTCToken } from "../flatServer/agora";
+
+AgoraRTC.setLogLevel(/* WARNING */ 2);
+
+export enum RtcChannelType {
+ Communication = 0,
+ Broadcast = 1,
+}
+
+/**
+ * Flow:
+ * ```
+ * join() -> now it has `client`
+ * getLatency() -> number
+ * destroy()
+ * ```
+ */
+export class RtcRoom {
+ public client?: IAgoraRTCClient;
+
+ private roomUUID?: string;
+
+ public async join({
+ roomUUID,
+ isCreator,
+ rtcUID,
+ channelType,
+ }: {
+ roomUUID: string;
+ isCreator: boolean;
+ rtcUID: number;
+ channelType: RtcChannelType;
+ }): Promise {
+ if (this.client) {
+ await this.destroy();
+ }
+
+ const mode = channelType === RtcChannelType.Communication ? "rtc" : "live";
+ this.client = AgoraRTC.createClient({ mode, codec: "vp8" });
+
+ this.client.on("token-privilege-will-expire", this.renewToken);
+
+ await this.client.setClientRole(
+ channelType === RtcChannelType.Broadcast && !isCreator ? "audience" : "host",
+ );
+ const token = globalStore.rtcToken || (await generateRTCToken(roomUUID));
+ await this.client.join(AGORA.APP_ID, roomUUID, token, rtcUID);
+
+ this.roomUUID = roomUUID;
+ }
+
+ public getLatency(): number {
+ return this.client?.getRTCStats().RTT ?? NaN;
+ }
+
+ public async destroy(): Promise {
+ if (this.client) {
+ this.client.off("token-privilege-will-expire", this.renewToken);
+ await this.client.leave();
+ this.client = undefined;
+ }
+ }
+
+ private renewToken = async (): Promise => {
+ if (this.client && this.roomUUID) {
+ const token = await generateRTCToken(this.roomUUID);
+ await this.client.renewToken(token);
+ }
+ };
+}
+
+(window as any).RtcRoom = RtcRoom;
diff --git a/web/flat-web/src/apiMiddleware/rtc/testing.ts b/web/flat-web/src/apiMiddleware/rtc/testing.ts
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/web/flat-web/src/constants/Process.ts b/web/flat-web/src/constants/Process.ts
index 8770e081b41..d35d0d9e0fb 100644
--- a/web/flat-web/src/constants/Process.ts
+++ b/web/flat-web/src/constants/Process.ts
@@ -1,4 +1,4 @@
-export const NODE_ENV = process.env.NODE_ENV;
+export const NODE_ENV = import.meta.env.MODE;
export const NETLESS = Object.freeze({
APP_IDENTIFIER: import.meta.env.NETLESS_APP_IDENTIFIER,
diff --git a/web/flat-web/src/stores/ClassRoomStore.ts b/web/flat-web/src/stores/ClassRoomStore.ts
index c618d90285a..9348aae6caf 100644
--- a/web/flat-web/src/stores/ClassRoomStore.ts
+++ b/web/flat-web/src/stores/ClassRoomStore.ts
@@ -3,7 +3,7 @@ import { message } from "antd";
import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx";
import { v4 as uuidv4 } from "uuid";
import dateSub from "date-fns/sub";
-import { Rtc as RTCAPI, RtcChannelType } from "../apiMiddleware/Rtc";
+import { RtcRoom as RTCAPI, RtcChannelType } from "../apiMiddleware/rtc/room";
import {
ClassModeType,
NonDefaultUserProp,
@@ -30,11 +30,11 @@ import { globalStore } from "./GlobalStore";
import { NODE_ENV } from "../constants/Process";
import { useAutoRun } from "../utils/mobx";
import { User, UserStore } from "./UserStore";
-import type { AgoraNetworkQuality, RtcStats } from "agora-electron-sdk/types/Api/native_type";
import { errorTips } from "../components/Tips/ErrorTips";
import { WhiteboardStore } from "./WhiteboardStore";
import { RouteNameType, usePushHistory } from "../utils/routes";
import { useSafePromise } from "../utils/hooks/lifecycle";
+import { NetworkQuality } from "agora-rtc-sdk-ng";
export type { User } from "./UserStore";
@@ -118,6 +118,7 @@ export class ClassRoomStore {
this.rtcChannelType = config.recordingConfig.channelType ?? RtcChannelType.Communication;
this.rtc = new RTCAPI();
+ (window as any).rtc = this.rtc;
this.rtm = new RTMAPI();
this.cloudRecording = new CloudRecording({ roomUUID: config.roomUUID });
@@ -229,7 +230,6 @@ export class ClassRoomStore {
if (this.isRecording) {
await this.stopRecording();
}
- this.rtc.leave();
} catch (e) {
console.error(e);
this.updateCalling(true);
@@ -496,13 +496,11 @@ export class ClassRoomStore {
}
public onRTCEvents(): void {
- this.rtc.rtcEngine.on("rtcStats", this.checkDelay);
- this.rtc.rtcEngine.on("networkQuality", this.checkNetworkQuality);
+ this.rtc.client?.on("network-quality", this.checkNetworkQuality);
}
public offRTCEvents(): void {
- this.rtc.rtcEngine.off("rtcStats", this.checkDelay);
- this.rtc.rtcEngine.off("networkQuality", this.checkNetworkQuality);
+ this.rtc.client?.off("network-quality", this.checkNetworkQuality);
}
public async destroy(): Promise {
@@ -953,21 +951,11 @@ export class ClassRoomStore {
window.clearTimeout(this._collectChannelStatusTimeout);
}
- private checkDelay = action((stats: RtcStats): void => {
- this.networkQuality.delay = stats.lastmileDelay;
- });
-
private checkNetworkQuality = action(
- (
- uid: number,
- uplinkQuality: AgoraNetworkQuality,
- downlinkQuality: AgoraNetworkQuality,
- ): void => {
- if (uid === 0) {
- // current user
- this.networkQuality.uplink = uplinkQuality;
- this.networkQuality.downlink = downlinkQuality;
- }
+ ({ uplinkNetworkQuality, downlinkNetworkQuality }: NetworkQuality): void => {
+ this.networkQuality.uplink = uplinkNetworkQuality;
+ this.networkQuality.downlink = downlinkNetworkQuality;
+ this.networkQuality.delay = this.rtc.getLatency();
},
);
}
diff --git a/web/flat-web/src/stores/WhiteboardStore.ts b/web/flat-web/src/stores/WhiteboardStore.ts
index dd8eb2a538a..12ac388faa1 100644
--- a/web/flat-web/src/stores/WhiteboardStore.ts
+++ b/web/flat-web/src/stores/WhiteboardStore.ts
@@ -126,7 +126,7 @@ export class WhiteboardStore {
},
floatBar: true,
isWritable: this.isWritable,
- disableNewPencil: false,
+ disableNewPencil: true,
hotKeys: {
...DefaultHotKeys,
changeToSelector: "s",
diff --git a/web/flat-web/src/utils/UploadTaskManager/index.ts b/web/flat-web/src/utils/UploadTaskManager/index.ts
index 8204b9b5495..dece9f190c2 100644
--- a/web/flat-web/src/utils/UploadTaskManager/index.ts
+++ b/web/flat-web/src/utils/UploadTaskManager/index.ts
@@ -58,7 +58,7 @@ export class UploadTaskManager {
runInAction(() => {
this.uploadingMap.set(task.uploadID, task);
});
- if (process.env.NODE_ENV === "development") {
+ if (import.meta.env.DEV) {
console.log(`[cloud storage]: UploadTaskManager uploads "${task.file.name}"`);
}
void task.upload().then(() => this.finishUpload(task));
diff --git a/web/flat-web/tsconfig.json b/web/flat-web/tsconfig.json
index 27cc31d742b..ef731cffac2 100644
--- a/web/flat-web/tsconfig.json
+++ b/web/flat-web/tsconfig.json
@@ -10,7 +10,7 @@
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
- "types": ["vite/client", "@types/node"]
+ "types": ["vite/client"]
},
"exclude": ["node_modules"],
"include": ["src/**/*.ts", "src/**/*.tsx", "typings/*.ts"]
diff --git a/yarn.lock b/yarn.lock
index 3842e6ad44e..acaad2f9090 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1342,6 +1342,11 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/standalone@7.13.9":
+ version "7.13.9"
+ resolved "https://registry.npmjs.org/@babel/standalone/-/standalone-7.13.9.tgz#38e78673e19f5077f4b294ad346780634e59e276"
+ integrity sha512-9ZpIl8rXXQqm5OdsQVhlBSPttwabiHGPGrl5N2bIxB9XS2NJW+1oiPFgMMNd+57C/xVwsEwD2mzv0KValOwlQA==
+
"@babel/standalone@7.14.1", "@babel/standalone@^7.13.12":
version "7.14.1"
resolved "https://registry.npmjs.org/@babel/standalone/-/standalone-7.14.1.tgz#2c5f6908f03108583eea75bdcc94eb29e720fbac"
@@ -3900,6 +3905,11 @@ agora-rtm-sdk@^1.4.1:
resolved "https://registry.npmjs.org/agora-rtm-sdk/-/agora-rtm-sdk-1.4.1.tgz#be10d1cdfd52cf527ea714146287bdc7ff6efaa1"
integrity sha512-D27CSWIOCS1RDYxH7dtnCJNsM7VIuBYX8W4mqt3yIkPnTpoRFGB1n43ao3eSHaFIRpsdCefjw3KcV5xIAAYvdA==
+agora-rtm-sdk@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.npmjs.org/agora-rtm-sdk/-/agora-rtm-sdk-1.4.3.tgz#cfdbf95b65a1b36ae83d1cd857d96e6e3fed3734"
+ integrity sha512-0CwobiGaT+vlc594vby6vOAzbkhbW9D/v7rBYcTwC/2mDfDgPR9ZWAU/xHPW/vJkVHMMPxQ2J7WY0Gtag65U8w==
+
airbnb-js-shims@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/airbnb-js-shims/-/airbnb-js-shims-2.2.1.tgz#db481102d682b98ed1daa4c5baa697a05ce5c040"