Skip to content

Commit

Permalink
feat(desktop): support share screen (#894)
Browse files Browse the repository at this point in the history
* feat(desktop): support share screen

* refactor(desktop): add comment in share screen

* refactor(i18n): add i18n text in desktop screen share

* fix(desktop): share screen loading invalid

* refactor(desktop): miss destroy share screen

* refactor(desktop): disable some mobx member in share screen

* fix(desktop): mobx warning when set ShareScreenStore.isWritable

* refactor(desktop): add not share screen permission tips

* fix(desktop): improve shareScreenStore isWritable
  • Loading branch information
BlackHole1 committed Aug 26, 2021
1 parent 7efc324 commit 0654db7
Show file tree
Hide file tree
Showing 26 changed files with 854 additions and 7 deletions.
1 change: 1 addition & 0 deletions cspell.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ module.exports = {
"lastmile",
"onleave",
"leavechannel",
"videosource",

// css / less
"minlength",
Expand Down
4 changes: 4 additions & 0 deletions desktop/renderer-app/src/apiMiddleware/flatServer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<JoinRoomResult> {
Expand Down
126 changes: 126 additions & 0 deletions desktop/renderer-app/src/apiMiddleware/share-screen.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
return new Promise<void>(resolve => {
this.roomClient.once("videoSourceLeaveChannel", () => {
this.roomClient.videoSourceRelease();
resolve();
});

this.roomClient.videoSourceLeave();
}).finally(() => {
this.enableShareScreenStatus(false);
});
}

public async destroy(): Promise<void> {
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[];
};
17 changes: 17 additions & 0 deletions desktop/renderer-app/src/assets/image/share-screen-active.svg
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 desktop/renderer-app/src/assets/image/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.
Original file line number Diff line number Diff line change
@@ -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<ShareScreenProps>(function ShareScreen({ shareScreenStore }) {
const ref = useRef<HTMLDivElement>(null);

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

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

return <div className={classNameList} ref={ref} />;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.share-screen {
position: absolute;
z-index: 5;
background-color: black;

&.active {
width: 100%;
height: 100%;
}
}
Original file line number Diff line number Diff line change
@@ -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}`,
};
}
};
Original file line number Diff line number Diff line change
@@ -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<ScreenListProps>(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 (
<div className="screen-list">
{screenInfo.displayList.map(info => {
const key = `display-${info.displayId.id}`;
const isActive = activeInfo === key;

return (
<ScreenItem
info={info}
handleClick={isActive ? cancelSelected : onClick}
active={isActive}
key={key}
/>
);
})}
{screenInfo.windowList.map(info => {
const key = `window-${info.windowId}`;
const isActive = activeInfo === key;

return (
<ScreenItem
info={info}
handleClick={isActive ? cancelSelected : onClick}
active={activeInfo === key}
key={key}
/>
);
})}
</div>
);
});

interface ScreenItemProps {
info: ScreenInfo["windowList"][0] | ScreenInfo["displayList"][0];
handleClick: (isDisplay: boolean, id: number) => void;
active: boolean;
}

const ScreenItem = observer<ScreenItemProps>(function ScreenItem({ info, handleClick, active }) {
const screenInfo = getScreenInfo(info);

return (
<>
<div className="screen-item">
<div
className={classNames("screen-image-box", {
active,
})}
onClick={() => handleClick(screenInfo.isDisplay, screenInfo.id)}
>
<img src={uint8ArrayToImageURL(info.image)} alt="screenshots" />
</div>
<span>{screenInfo.name}</span>
</div>
</>
);
});
Loading

0 comments on commit 0654db7

Please sign in to comment.