本文介绍如何使用 Agora RDC SDK 快速实现远程控制功能。
实现远程控制的步骤如下:
join
创建并加入频道。使用同一频道名称的 app 客户端默认加入同一频道。App 客户端加入频道需要以下信息:
开始前,请确保你的开发环境满足以下条件:
在你的项目文件夹下打开命令行工具,执行如下命令创建一个名为 rdc-client-vanilla-ts
的项目。
如果不确定你的 npm 版本,你可以执行
npm -v
命令获取版本。
# npm 6.x
npm init vite@latest rdc-client-vanilla-ts --template vanilla-ts
# npm 7+
npm init vite@latest rdc-client-vanilla-ts -- --template vanilla-ts
本节介绍如何使用 Agora RDC SDK 在你的 app 里实现远程控制。
参考以下步骤,将 Agora RDC Electron SDK 集成到你的项目中。
package.json
文件。{
"name": "rdc-client-vanilla-ts",
"version": "0.0.0",
"main": "build/electron/main.js",
"scripts": {
"start": "concurrently \"vite\" \"tsc -p electron && cross-env NODE_ENV=development electron .\"",
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"currently": "0.0.8",
"typescript": "^4.4.4",
"vite": "^2.7.2",
"electron": "^16.0.5",
"electron-builder": "^22.14.5",
"agora-rdc-electron": "latest", // Agora RDC Electron SDK 的版本号。设为 latest 表示获取最新版本的 SDK。
"agora-electron-sdk": "latest", // Agora RTC Electron SDK 的版本号。设为 latest 表示获取最新版本的 SDK。如果项目中已集成 RTC Native SDK,则忽略该步骤。
"vite-plugin-commonjs-externals": "^0.1.1"
},
"dependencies": {
"axios": "^0.24.0", // 用于实现 HTTP 请求
"query-string": "^7.0.1" // 用于解析和字符串化 URL
}
}
如果你的项目需要使用 RTC Web SDK,请参考如下代码:
{
"name": "rdc-client-vanilla-ts",
"version": "0.0.0",
"main": "build/electron/main.js",
"scripts": {
"start": "concurrently \"vite\" \"tsc -p electron && cross-env NODE_ENV=development electron .\"",
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"currently": "0.0.8",
"typescript": "^4.4.4",
"vite": "^2.7.2",
"electron": "^16.0.5",
"electron-builder": "^22.14.5",
"agora-rdc-webrtc-electron": "latest", // Agora RDC Electron SDK 的版本号。设为 latest 表示获取最新版本的 SDK。
"agora-rtc-sdk-ng": "latest", // Agora RTC Electron SDK 的版本号。设为 latest 表示获取最新版本的 SDK。如果项目中已集成 RTC Web SDK,则忽略该步骤。
"vite-plugin-commonjs-externals": "^0.1.1"
},
"dependencies": {
"axios": "^0.24.0", // 用于实现 HTTP 请求
"query-string": "^7.0.1" // 用于解析和字符串化 URL
}
}
npm install
如果你是国内开发者,你可以执行如下命令设置镜像。
npm set ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/
npm set ELECTRON_BUILDER_BINARIES_MIRROR=http://npm.taobao.org/mirrors/electron-builder-binaries/
参考以下步骤,实现用户的登录界面。
rm -rf index.html src/style.css src/main.ts favicon.svg
landing.html
文件。将以下代码复制到该文件中,创建用户登录界面。<!DOCTYPE html>
<html>
<head>
<title>RDC Client Landing</title>
<style>
.form-item {
margin-top: 16px;
}
.notice {
color: red;
}
</style>
</head>
<body>
<div>
<form>
<div class="form-item">
<label>* Role: </label>
<select name="role" id="role">
<option value="">Please choose an role</option>
<option value="1">HOST</option>
<option value="2">CONTROLLED</option>
</select>
</div>
<div class="form-item">
<label>* Channel: </label>
<input name="channel" id="channel" placeholder="Please input channel" />
</div>
<div class="form-item">
<label>* Name: </label>
<input name="name" id="name" placeholder="Please input your name" />
</div>
<div class="form-item">
<button id="submit" type="submit">JOIN</button>
</div>
</form>
</div>
<script type="module" src="/src/landing.ts"></script>
</body>
</html>
src
文件夹下创建名为 landing.ts
的文件。将以下代码复制到该文件中,实现用户登录界面具体逻辑。// 本示例以 Agora RDC Electron SDK 结合 Agora RTC Web SDK 为例
import {RDCRoleType} from "agora-rdc-webrtc-electron";
import {joinSession} from "./api";
(() => {
type State = {
role: RDCRoleType;
channel: string;
name: string;
};
const state: Partial<State> = {};
// 获取用户选择的角色
const handleRoleChange = () => {
const el: HTMLSelectElement | null = document.querySelector("#role");
if (!el) {
return;
}
state.role = Number(el.value);
};
// 获取用户输入的频道名称
const handleChannelInput = () => {
const el: HTMLSelectElement | null = document.querySelector("#channel");
if (!el) {
return;
}
state.channel = el.value;
};
// 获取用户输入的用户名
const handleNameInput = () => {
const el: HTMLSelectElement | null = document.querySelector("#name");
if (!el) {
return;
}
state.name = el.value;
};
// 提交表单并获得 userId
const handleSubmit = (event: Event) => {
event.preventDefault();
const {role, channel, name} = state;
const noticeEl = document.createElement("div");
noticeEl.setAttribute("class", "notice");
document.querySelector(".notice")?.remove();
const formEl = document.querySelector("form");
// 如果用户未选择角色,则提示 "Please select role"。
if (!role) {
noticeEl.innerText = "Please select role!";
formEl?.before(noticeEl);
return;
}
// 如果用户未输入频道,则提示 "Please input channel"。
if (!channel) {
noticeEl.innerText = "Please input channel!";
formEl?.before(noticeEl);
return;
}
// 如果用户未输入用户名,则提示 "Please input name"。
if (!name) {
noticeEl.innerText = "Please input name!";
formEl?.before(noticeEl);
return;
}
// 发起加入会话的 HTTP 请求
joinSession({
channel,
name,
role,
}).then(({data: {userId}}) => {
// 如果角色是 HOST,跳转到主控端界面
if (role === RDCRoleType.HOST) {
window.location.assign(`host.html?userId=${userId}`);
}
// 如果角色是 CONTROLLED,跳转到被控端界面
if (role === RDCRoleType.CONTROLLED) {
window.location.assign(`controlled.html?userId=${userId}`);
}
});
};
// 添加事件监听
const bindEvents = () => {
const roleEl: HTMLSelectElement | null = document.querySelector("#role");
const channelEl: HTMLInputElement | null = document.querySelector("#channel");
const nameEl: HTMLInputElement | null = document.querySelector("#name");
const submitEl: HTMLButtonElement | null = document.querySelector("#submit");
if (!roleEl || !channelEl || !nameEl) {
return;
}
roleEl.addEventListener("change", handleRoleChange);
channelEl.addEventListener("input", handleChannelInput);
nameEl.addEventListener("input", handleNameInput);
submitEl?.addEventListener("click", handleSubmit);
};
bindEvents();
})();
vite.config.js
文件,将以下代码复制到该文件中,用于配置 Vite 项目。const {resolve} = require("path");
const {defineConfig} = require("vite");
const commonjsExternals = require("vite-plugin-commonjs-externals").default;
module.exports = defineConfig({
build: {
rollupOptions: {
input: {
landing: resolve(__dirname, "landing.html"),
},
},
},
// 'agora-rdc-core', 'agora-rdc-webrtc-electron', 'agora-rtc-sdk-ng' 作为 external
plugins: [commonjsExternals({externals: ["agora-rdc-core", "agora-rdc-webrtc-electron", "agora-rtc-sdk-ng"]})],
// 排除 Vite 的优化
optimizeDeps: {
exclude: ["agora-rdc-core", "agora-rdc-webrtc-electron", "agora-rtc-sdk-ng"],
},
});
参考以下步骤,实现基本的 Electron 项目主进程。
electron
的文件夹,在 electron
文件夹下创建名为 tsconfig.json
的配置文件。将以下代码复制到该文件中:{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"outDir": "../build",
"rootDir": "../",
"noEmitOnError": true,
"typeRoots": ["node_modules/@types"]
}
}
electron
文件夹下创建名为 main.ts
的文件。将以下代码复制该文件中,实现项目主进程:import {app, BrowserWindow} from "electron";
import registerHandlers from "agora-rdc-webrtc-electron/lib/electron/registerHandlers";
// 注册事件处理器
registerHandlers();
const __DEV__ = process.env.NODE_ENV === "development";
const createWindow = async () => {
let mainWindow: BrowserWindow | null = null;
// 创建浏览器窗口
mainWindow = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
webSecurity: false,
// 允许使用 node 模块
nodeIntegration: true,
// 允许使用 v8 内核
contextIsolation: false,
},
});
if (__DEV__) {
// 开发模式下,加载 DevServer 上的资源
mainWindow.loadURL("http://localhost:3000/landing.html");
}
if (!__DEV__) {
// 非开发模式下,加载本地已经 build 完成的资源
mainWindow.loadFile("build/landing.html");
}
if (__DEV__) {
// 开发模式下,启用调试工具
mainWindow.webContents.openDevTools();
}
};
app.whenReady().then(createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
参考以下步骤,实现对即将加入 RDC 频道的用户的动态鉴权。
在 src
文件夹下创建名为 api.ts
的文件,将以下代码复制到该文件中,实现 Token 鉴权。
import {RDCRoleType} from "agora-rdc-webrtc-electron";
import axios, {AxiosResponse} from "axios";
const API_HOST: string = "https://rdc-api.gz3.agoralab.co";
// 定义加入会话参数接口
export interface JoinSessionParams {
// 频道名称
channel: string;
// 加入频道的用户名
name: string;
// 加入频道的用户角色
role: RDCRoleType;
}
// 定义加入会话接口
export interface JoinedSession {
//
userId: number;
}
// 定义属性接口
export interface Profile {
// 用户 ID
userId: string;
// 屏幕流 ID
screenStreamId: number;
// 加入频道的用户名
name: string;
// 加入频道的用户角色
rdcRole: RDCRoleType;
}
// 定义会话接口
export interface Session extends Profile {
// 在控制台为项目获取的 AppId
appId: string;
// 频道名称
channel: string;
// 你在服务端生成的 RTM Token,用于对即将登录 RTM 系统的用户进行鉴权。
userToken: string;
// 你在服务端生成的 RTC Token,用于对即将加入 RTC 频道的用户进行鉴权。
screenStreamToken: string;
}
// 加入会话
export const joinSession = (prams: JoinSessionParams) =>
axios.post<JoinSessionParams, AxiosResponse<JoinedSession>>(`${API_HOST}/api/session`, prams);
// 获取加入会话后的用户 ID
export const fetchSession = (userId: string) => axios.get<Session>(`${API_HOST}/api/session/${userId}`);
// 获取会话内所有用户 ID
export const fetchProfiles = (userId: string) => axios.get<Profile[]>(`${API_HOST}/api/session/${userId}/profiles`);
本示例实现主控端和被控端两端的逻辑,你可以根据业务需要选择实现其中一端。远程控制的 API 使用时序见下图:
参考如下步骤,实现被控端具体逻辑。
controlled.html
的文件,将以下代码复制到该文件中,实现被控端用户界面。<!DOCTYPE html>
<html>
<head>
<title>RDC Client</title>
</head>
<body>
<div id="notice"></div>
<script type="module" src="/src/controlled.ts"></script>
</body>
</html>
src
文件夹下创建名为 controlled.ts
的文件,将以下代码复制到该文件中,实现被控端具体逻辑。import {AgoraRemoteDesktopControl, RDCRoleType} from "agora-rdc-webrtc-electron";
import AgoraRTC, {IAgoraRTCRemoteUser} from "agora-rtc-sdk-ng";
import qs from "querystring";
import {fetchProfiles, fetchSession, Profile} from "./api";
(async () => {
// 从 URL 查询参数中获取 userId
const {userId} = qs.parse(window.location.search.replace("?", "")) as {userId: string};
// 获取用户信息
const {
data: {appId, userToken, channel, screenStreamId, screenStreamToken},
} = await fetchSession(userId);
let profiles: Profile[] = [];
// 创建 Agora RTC 实例
const rtcEngine = AgoraRTC.createClient({codec: "h264", mode: "rtc", role: "host"});
// 创建 Agora RDC 实例
const rdcEngine = AgoraRemoteDesktopControl.create(rtcEngine, {role: RDCRoleType.CONTROLLED, appId});
// 定义加入频道事件
const handleUserJoin = (user: IAgoraRTCRemoteUser) => {
fetchProfiles(userId).then(({data}) => {
profiles = data;
});
};
// 定义离开频道事件
const handleUserLeft = (user: IAgoraRTCRemoteUser) => {
const profile = profiles.find(p => p.screenStreamId === user.uid);
if (!profile) {
return;
}
// 退出远程控制
rdcEngine.quitControl(profile.userId, profile.rdcRole);
const noticeEl = document.querySelector("#notice");
if (noticeEl) {
noticeEl.remove();
}
};
// 定义请求控制事件
const handleRequestControl = async (userId: string) => {
const profile = profiles.find(p => p.userId === userId);
if (!profile) {
return;
}
// 获取显示器信息
const displays = await rdcEngine.getDisplays();
// 授权指定用户的远程控制
rdcEngine.authorizeControl(userId, displays[0]);
const noticeEl = document.querySelector("#notice");
if (noticeEl) {
// 提示用户计算机已被控制
noticeEl.innerHTML = `Your personal computer is controlled by ${profile.name}`;
}
};
// 定义退出远程控制的事件
const handleQuitControl = (userId: string) => {
const profile = profiles.find(p => p.userId === userId);
if (!profile) {
return;
}
// 退出远程控制
rdcEngine.quitControl(profile.userId, profile.rdcRole);
const noticeEl = document.querySelector("#notice");
if (noticeEl) {
// 移除提示信息
noticeEl.remove();
}
};
// 绑定事件
const bindEvents = () => {
rtcEngine.on("user-joined", handleUserJoin);
rtcEngine.on("user-left", handleUserLeft);
rdcEngine.on("rdc-request-control", handleRequestControl);
rdcEngine.on("rdc-quit-control", handleQuitControl);
};
bindEvents();
// 加入频道
rdcEngine.join(userId, userToken, channel, screenStreamId, screenStreamToken);
})();
vite.config.js
文件的 input
配置项后添加如下代码。{
...
input: {
...
controlled: resolve(__dirname, 'controlled.html'),
...
}
}
host.html
的文件,并复制如下代码该文件中,创建主控端界面。<!DOCTYPE html>
<html>
<head>
<title>RDC Client</title>
<style>
body {
padding: 0;
margin: 0;
}
.controll-area {
height: calc(100vh - 80px);
width: 100vw;
background: #000;
}
</style>
</head>
<body>
<div id="user-list"></div>
<div id="control-area" class="controll-area"></div>
<script type="module" src="/src/host.ts"></script>
</body>
</html>
src
文件夹下创建名为 host.ts
的文件,将以下代码复制到该文件中,实现主控端具体逻辑。import {AgoraRemoteDesktopControl, RDCRoleType} from "agora-rdc-webrtc-electron";
import AgoraRTC, {IAgoraRTCRemoteUser} from "agora-rtc-sdk-ng";
import qs from "querystring";
import {fetchProfiles, fetchSession, Profile} from "./api";
(async () => {
// 从 URL 查询参数中获取 userId
const {userId} = qs.parse(window.location.search.replace("?", "")) as {userId: string};
// 获取用户信息
const {
data: {appId, userToken, channel, screenStreamId, screenStreamToken},
} = await fetchSession(userId);
let profiles: Profile[] = [];
// 创建 Agora RTC 实例
const rtcEngine = AgoraRTC.createClient({codec: "h264", mode: "rtc", role: "host"});
// 创建 Agora RDC 实例
const rdcEngine = AgoraRemoteDesktopControl.create(rtcEngine, {role: RDCRoleType.HOST, appId});
// 定义加入频道的事件
const handleUserJoin = (user: IAgoraRTCRemoteUser) => {
fetchProfiles(userId).then(({data}) => {
const profile = data.find(p => p.screenStreamId === user.uid);
if (profile) {
profiles.push(profile);
}
renderUserProfiles(profiles);
});
};
// 定义离开频道的事件
const handleUserLeft = (user: IAgoraRTCRemoteUser) => {
const profile = profiles.find(p => p.screenStreamId === user.uid);
if (!profile) {
return;
}
profiles = profiles.filter(p => p.screenStreamId !== user.uid);
// 渲染用户列表
renderUserProfiles(profiles);
// 退出远程控制
rdcEngine.quitControl(profile.userId, profile.rdcRole);
};
// 定义退出远程控制事件
const handleQuitControlByUserId = (userId: string) => {
const profile = profiles.find(p => p.userId === userId);
if (!profile) {
return;
}
rdcEngine.quitControl(profile.userId, profile.rdcRole, profile.screenStreamId);
};
// 定义开始远程控制事件
const handleTakeControl = (userId: string) => {
const profile = profiles.find(p => p.userId === userId);
const attachEl = document.querySelector<HTMLDivElement>("#control-area");
if (!profile || !attachEl) {
return;
}
rdcEngine.takeControl(profile.userId, profile.screenStreamId, attachEl);
};
// 定义发起控制请求事件
const handleRequestControl = (el: HTMLButtonElement) => {
const userId = el.getAttribute("data-userId");
const profile = profiles.find(p => p.userId === userId);
if (!profile) {
return;
}
rdcEngine.requestControl(profile.userId);
};
// 定义退出远程控制事件
const handleQuitControl = (el: HTMLButtonElement) => {
const userId = el.getAttribute("data-userId");
if (!userId) {
return;
}
handleQuitControlByUserId(userId);
};
// 绑定事件
const bindEvents = () => {
rtcEngine.on("user-joined", handleUserJoin);
rtcEngine.on("user-left", handleUserLeft);
rdcEngine.on("rdc-request-control-authorized", handleTakeControl);
};
// 绑定 DOM 事件
const bindDOMEvents = () => {
document.querySelectorAll<HTMLButtonElement>(".request-control").forEach(el => {
el.addEventListener("click", () => handleRequestControl(el));
});
document.querySelectorAll<HTMLButtonElement>(".quit-control").forEach(el => {
el.addEventListener("click", () => handleQuitControl(el));
});
};
// 渲染用户列表
const renderUserProfiles = (profiles: Profile[]) => {
const userListEl = document.querySelector("#user-list");
if (!userListEl) {
return;
}
userListEl.innerHTML = `
<ul>
${profiles
.filter(profile => profile.rdcRole === RDCRoleType.CONTROLLED)
.map(
profile =>
`<li>
<span>${profile.name}</span>
<button class="request-control" data-userId=${profile.userId}>Request Control</button>
<button class="quit-control" data-userId=${profile.userId}>Quit Control</button>
</li>`,
)
.join("")}
</ul>
`;
bindDOMEvents();
};
bindEvents();
// 加入频道
rdcEngine.join(userId, userToken, channel, screenStreamId, screenStreamToken);
})();
vite.config.js
文件的 input
配置项后添加如下代码。{
...
input: {
...
host: resolve(__dirname, 'host.html'),
...
}
}
执行如下命令运行你的 Electron 项目:
yarn start
项目成功运行后,你会看到一个自动弹出的窗口。你可以设置角色并加入频道,与相同 App ID、频道名和 Token 的对端实现互通。