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(desktop): support share screen #894

Merged
merged 9 commits into from
Aug 26, 2021
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