# 实现音视频互动 (/zh-CN/realtime-media/rtc/get-started/quick-start/electron)

> For AI agents: see the complete documentation index at [llms.txt](/llms.txt).

本文介绍如何集成声网实时互动 SDK，通过少量代码从 0 开始实现一个简单的实时互动 App，适用于互动直播和视频通话场景。
首先，你需要了解以下有关音视频实时互动的基础概念：

- 声网实时互动 SDK：由声网开发的、帮助开发者在 App 中实现实时音视频互动的 SDK。
- 频道：用于传输数据的通道，在同一个频道内的用户可以进行实时互动。
- 主播：可以在频道内**发布**音视频，同时也可以**订阅**其他主播发布的音视频。
- 观众：可以在频道内**订阅**音视频，不具备**发布**音视频权限。

更多概念详见[关键概念](/zh-CN/realtime-media/rtc/reference/key-concept)。

下图展示在 App 中实现音视频互动的基本工作流程：

<DocsImage alt="实现音视频互动" src="https://web-cdn.agora.io/docs-portal/img/rtc/quick-start-tech.png" displayWidth="60%" />

1. 所有用户调用 `joinChannel` 方法加入频道，并根据需要设置用户角色：

   - 互动直播：如果用户需要在频道中发流，则设为主播；如果用户只需要收流，则设为观众。
   - 视频通话：将所有用户的角色都设为主播。
2. 加入频道后，不同角色的用户具备不同的行为：

   - 所有用户默认都可以接收频道中的音视频流。
   - 主播可以在频道内发布音视频流。
   - 观众如果需要发流，可在频道内调用 `setClientRole` 方法修改用户角色，使其具备发流权限。

## 前提条件 [#前提条件-7]

- [Node.js](https://nodejs.org/en/download/) 6.9.1 或更高版本。
- 可以访问互联网的 Windows 或 macOS 电脑。如果你的网络环境部署了防火墙，参考[应对防火墙限制](/zh-CN/realtime-media/rtc/build/setup-and-access/firewall)以正常使用声网服务。
- 一个有效的声网账号以及声网项目。请参考[开通 RTC 服务](/zh-CN/realtime-media/rtc/build/setup-and-access/enable-service)从声网控制台获得以下信息：
    - App ID：声网随机生成的字符串，用于识别你的项目。
    - 临时 Token：Token 也称为动态密钥，在客户端加入频道时对用户鉴权。临时 Token 的有效期为 24 小时。

<a id="project"></a>
## 创建项目 [#创建项目-6]

在本地文件夹中为 Electron 项目新建一个目录，并在项目根目录下创建以下文件：

- `package.json`: 用于安装和管理项目依赖项。
- `index.html`：用于设计 App 的用户界面。
- `main.js`：主进程文件。
- `renderer.js`：渲染进程文件，用于实现与声网 Electron SDK 的交互。

## 集成 SDK [#集成-sdk-6]

根据实际情况，在以下集成方式中任选一种，在你的项目中集成 SDK。

<Tabs defaultValue="npm" groupId="integrate" persist>
<TabsList>
  <TabsTrigger value="npm">npm 集成</TabsTrigger>
  <TabsTrigger value="manual">手动集成</TabsTrigger>
</TabsList>

<TabsContent value="npm">

1. 在 `package.json` 文件中添加以下代码：

  - macOS:

    ```json
    {
      "name": "electron-demo-app",
      "version": "0.1.0",
      "author": "your name",
      "description": "My Electron app",
      "main": "main.js",
      "scripts": {
        "start": "electron ."
      },
      "agora_electron": {
        "platform": "darwin",
        "prebuilt": true
      },
      "dependencies": { "agora-electron-sdk": "latest" },
      "devDependencies": {"electron": "latest" }
    }
    ```

  - Windows:

    ```json
    {
      "name": "electron-demo-app",
      "version": "0.1.0",
      "author": "your name",
      "description": "My Electron app",
      "main": "main.js",
      "scripts": {
        "start": "electron ."
      },
      "agora_electron": {
        "platform": "win32",
        "prebuilt": true,
        "arch": "ia32"
      },
      "dependencies": { "agora-electron-sdk": "latest" },
      "devDependencies": {"electron": "latest" }
    }
    ```

  字段的具体含义如下：

  - `agora-electron-sdk`：声网 Electron SDK 的版本号。设为 `latest` 表示获取最新版本的 SDK，其它版本可在 [agora-electron-sdk](https://www.npmjs.com/package/agora-electron-sdk) 中查看。
  - `electron`：Electron 的版本号，支持 5.0.0 及以上版本（包括最新版本）。如果使用搭载 M1 芯片的 macOS 设备，Electron 的版本号需要设为 11.0.0 或以上。
  - （可选）`platform`：默认根据系统选择，macOS 为 `darwin`，Windows 为 `win32`。
  - （可选）`prebuilt`：默认设为 `true`，防止出现 Electron 或 Node.js 版本与 SDK 不兼容的问题。
  - （可选）`arch`：默认根据系统选择。

:::warning[注意]
由于不同版本的 Electron 对本地环境的要求有差异，运行项目时可能因环境不匹配报错。声网推荐你参考 [Electron 官方文档](https://releases.electronjs.org/releases/stable)选择合适的 Electron 和 Node.js 版本。
:::

2. 在终端中进入项目根目录，运行如下命令安装依赖项：

   ```shell
   # macOS
   npm install

   # Windows
   npm install -D --arch=ia32 electron
   npm install
   ```

  :::info[信息]
  - Windows 平台需要先运行 `npm install -D --arch=ia32 electron` 安装 32 位的 Electron，然后运行 `npm install`。否则会收到报错：`Not a valid win32 application`。
    - 如果项目根目录下已有 `node_modules` 文件夹，建议先删除该文件夹，再运行 `npm install`，以免出现报错。
  :::

</TabsContent>

<TabsContent value="manual">

除了使用 npm 集成以外，你也可以进入 [Electron-SDK](https://github.com/AgoraIO-Extensions/Electron-SDK/tree/main/) 手动编译最新的 SDK，集成到应用中。

</TabsContent>
</Tabs>

## 创建用户界面 [#创建用户界面-5]

在 `index.html` 文件中添加以下代码，创建一个只包含本地视频窗口和远端视频窗口的用户界面：

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Electron Quickstart</title>
  </head>
  <body>
    <h1>Electron Quickstart</h1>
    <!--在界面中添加本地视频窗口 -->
    <div
      id="join-channel-local-video"
      style="width: 300px; height: 300px; float: left"
    ></div>
    <!--在界面中添加远端视频窗口 -->
    <div
      id="join-channel-remote-video"
      style="width: 300px; height: 300px; float: left"
    ></div>
  </body>
  <script src="./renderer.js"></script>
</html>
```

## 实现步骤 [#实现步骤-7]

本节介绍如何实现一个实时音视频互动 App。

下图展示了使用声网 RTC SDK 实现音视频互动的基本流程。

<DocsImage alt="实时互动快速开始流程图" src="https://web-cdn.agora.io/docs-portal/img/rtc/quick-start-sequence.svg" displayWidth="70%" />

下面列出了实现实时音视频互动基本流程的完整代码以供参考，你可以先将 `main.js` 和 `renderer.js` 文件的全部内容分别替换为以下代码，参考[测试项目](#测试项目)中的步骤快速体验实时音视频互动的基础功能，再按照实现流程了解其中的核心 API 调用。

<Accordions>
<Accordion title="main.js 示例代码">

```javascript
const { app, BrowserWindow } = require("electron");
const path = require("path");

// 如果使用 Electron 9.x 及以上版本，需要将 allowRendererProcessReuse 设为 false
app.allowRendererProcessReuse = false;

function createWindow() {
    // 创建浏览器窗口
    const mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            // preload: path.join(__dirname, "renderer.js"),
            // 将 nodeIntegration 设置 true 以及 contextIsolation 设为 false
            nodeIntegration: true,
            contextIsolation: false,
        },
    });

    // 加载 index.html 文件的内容
    mainWindow.loadFile("./index.html");
    // 开启开发者工具
    mainWindow.webContents.openDevTools();
}

// 管理 Electron 应用的浏览器窗口
app.whenReady().then(() => {
    createWindow();
    // 如果当前没有窗口打开，则新建一个窗口（适用于 macOS）
    app.on("activate", function () {
        if (BrowserWindow.getAllWindows().length === 0) {
            createWindow();
        }
    });
});

// 如果所有窗口都已关闭，则退出 Electron 应用（适用于 Windows）
app.on("window-all-closed", function () {
    if (process.platform !== "darwin") app.quit();
});
```

</Accordion>

<Accordion title="renderer.js 示例代码">

```javascript
const {
    createAgoraRtcEngine,
    ChannelProfileType,
    ClientRoleType,
    VideoSourceType,
    VideoViewSetupMode,
    } = require("agora-electron-sdk");

let rtcEngine;
let localVideoContainer;
let remoteVideoContainer;
// 填入你的 App ID
const APPID = "<-- Insert App Id -->";
// 填入你的临时 Token
let token = "<-- Insert Token -->";
// 填入生成 Token 时使用的频道名
const channel = "<-- Insert Channel Name -->";
// 用户 ID，并确保其在频道内的唯一性
let uid = 123;

const EventHandles = {
    // 监听本地用户加入频道事件
    onJoinChannelSuccess: ({ channelId, localUid }, elapsed) => {
        console.log('成功加入频道：' + channelId);
        // 本地用户加入频道后，设置本地视频窗口
        rtcEngine.setupLocalVideo({
             sourceType: VideoSourceType.VideoSourceCameraPrimary,
             uid: uid,
             view: localVideoContainer,
             setupMode: VideoViewSetupMode.VideoViewSetupAdd,
        });
    },

    // 监听远端用户加入频道事件
    onUserJoined: ({ channelId, localUid }, remoteUid, elapsed) => {
        console.log('远端用户 ' + remoteUid + ' 已加入');
        // 远端用户加入频道后，设置远端视频窗口
        rtcEngine.setupRemoteVideo(
            {
                sourceType: VideoSourceType.VideoSourceRemote,
                uid: remoteUid,
                view: remoteVideoContainer,
                setupMode: VideoViewSetupMode.VideoViewSetupAdd,
            },
            { channelId },
        );
    },

    // 监听用户离开频道事件
    onUserOffline: ( { channelId, localUid }, remoteUid, reason ) => {
        console.log('远端用户 ' + remoteUid + ' 已离开频道');
        // 远端用户离开频道后，关闭远端视频窗口
        rtcEngine.setupRemoteVideo(
            {
                sourceType: VideoSourceType.VideoSourceRemote,
                uid: remoteUid,
                view: remoteVideoContainer,
                setupMode: VideoViewSetupMode.VideoViewSetupRemove,
            },
        );
    },
};


window.onload = () => {
    const os = require("os");
    const path = require("path");
    localVideoContainer = document.getElementById("join-channel-local-video");
    remoteVideoContainer = document.getElementById("join-channel-remote-video");
    const sdkLogPath = path.resolve(os.homedir(), "./test.log");

    // 创建 RtcEngine 实例
    rtcEngine = createAgoraRtcEngine();

    // 初始化 RtcEngine 实例
    rtcEngine.initialize({
        appId: APPID,
        logConfig: { filePath: sdkLogPath }
    });

    // 注册事件回调
    rtcEngine.registerEventHandler(EventHandles);

    // 启用视频模块
    rtcEngine.enableVideo();

    // 开启本地视频预览
    rtcEngine.startPreview();

    // 使用临时 Token 加入频道
    rtcEngine.joinChannel(token, channel, uid, {
        // 设置频道场景为直播场景
        channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
        // 设置用户角色为主播；如果要将用户角色设置为观众，保持默认值即可
        clientRoleType: ClientRoleType.ClientRoleBroadcaster,
        // 发布麦克风采集的音频
        publishMicrophoneTrack: true,
        // 发布摄像头采集的视频
        publishCameraTrack: true,
        // 自动订阅所有音频流
        autoSubscribeAudio: true,
        // 自动订阅所有视频流
        autoSubscribeVideo: true,
    });
};
```

</Accordion>
</Accordions>

:::info[信息]
- 将 `renderer.js` 文件中的 `<-- Insert App Id -->`、`<-- Insert Token -->` 和 `<-- Insert Channel Name -->` 分别替换为你在控制台获取到的 App ID、临时 Token，以及生成临时 Token 时填入的频道名。
- 如果你的目标平台为 macOS v10.14 及以上，还需参考[获取设备权限](#获取设备权限-macos)在 `main.js` 中补充设备权限相关代码。
:::

### 设计主进程 [#设计主进程]

使用 Electron 框架创建一个应用程序，实现基本的 Electron 项目主进程。主进程中的行为包括：

1. 导入所需模块
2. 创建浏览器窗口
3. 加载 `index.html` 文件
4. 处理应用程序生命周期事件

将以下代码复制到 `main.js` 文件中：

```javascript
const { app, BrowserWindow } = require("electron");
const path = require("path");

// 如果使用 Electron 9.x 及以上版本，需要将 allowRendererProcessReuse 设为 false
app.allowRendererProcessReuse = false;

function createWindow() {
    // 创建浏览器窗口
    const mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            // preload: path.join(__dirname, "renderer.js"),
            // 将 nodeIntegration 设置 true 以及 contextIsolation 设为 false
            nodeIntegration: true,
            contextIsolation: false,
        },
    });

    // 加载 index.html 文件的内容
    mainWindow.loadFile("./index.html");
    // 开启开发者工具
    mainWindow.webContents.openDevTools();
}

// 管理 Electron 应用的浏览器窗口
app.whenReady().then(() => {
    createWindow();
    // 如果当前没有窗口打开，则新建一个窗口（适用于 macOS）
    app.on("activate", function () {
        if (BrowserWindow.getAllWindows().length === 0) {
            createWindow();
        }
    });
});

// 如果所有窗口都已关闭，则退出 Electron 应用（适用于 Windows）
app.on("window-all-closed", function () {
    if (process.platform !== "darwin") app.quit();
});
```

### 获取设备权限 (macOS) [#获取设备权限-macos]

（可选）该步骤仅在目标平台为 macOS 时需要执行。macOS 自 v10.14 起，使用摄像头和麦克风前需要检查并获取设备的使用权限，你需要在主进程中调用 Electron 原生的 [`askForMediaAccess`](https://www.electronjs.org/docs/latest/api/system-preferences#systempreferencesaskformediaaccessmediatype-macos) 方法，来向用户请求摄像头和麦克风权限。

在 `main.js` 文件中添加如下代码：

```javascript
// 检查并获取设备权限
async function checkAndApplyDeviceAccessPrivilege() {
    // 检查并获取摄像头权限
    const cameraPrivilege = systemPreferences.getMediaAccessStatus('camera');
    console.log(`Camera privilege before applying: ${cameraPrivilege}`);
    if (cameraPrivilege !== 'granted') {
        await systemPreferences.askForMediaAccess('camera');
        console.log('Requested camera access from user');
    }

    // 检查并获取麦克风权限
    const micPrivilege = systemPreferences.getMediaAccessStatus('microphone');
    console.log(`Microphone privilege before applying: ${micPrivilege}`);
    if (micPrivilege !== 'granted') {
        await systemPreferences.askForMediaAccess('microphone');
        console.log('Requested microphone access from user');
    }
}

checkAndApplyDeviceAccessPrivilege();
```

### 导入依赖项 [#导入依赖项]

在 `renderer.js` 中导入声网 SDK 中构建 App 所需的模块和函数。

```javascript
const {
    createAgoraRtcEngine,
    ChannelProfileType,
    ClientRoleType,
    VideoSourceType,
    VideoViewSetupMode,
} = require("agora-electron-sdk");
```

### 定义基本信息 [#定义基本信息]

传入从声网控制台获取的 App ID、临时 Token，以及生成临时 Token 时填入的频道名，用于后续初始化引擎和加入频道。

```javascript
// 填入你的 App ID
const APPID = "<-- Insert App Id -->";
// 填入你的临时 Token
let token = "<-- Insert Token -->";
// 填入生成 Token 时使用的频道名
const channel = "<-- Insert Channel Name -->";
// 用户 ID，并确保其在频道内的唯一性
let uid = 123;
```

### 初始化引擎 [#初始化引擎-6]

在 `renderer.js` 中调用 `createAgoraRtcEngine` 方法创建一个 `RtcEngine` 实例，然后调用 `initialize` 初始化引擎。

:::warning[注意]
在初始化 SDK 前，需确保终端用户已经充分了解并同意相关的隐私政策。
:::

```javascript
let rtcEngine;
const os = require("os");
const path = require("path");
const sdkLogPath = path.resolve(os.homedir(), "./test.log");

// 创建 RtcEngine 实例
rtcEngine = createAgoraRtcEngine();

// 初始化 RtcEngine 实例
rtcEngine.initialize({
    appId: APPID,
    logConfig: { filePath: sdkLogPath }
});
```

### 启用视频模块 [#启用视频模块-6]

按照以下步骤启用视频模块：

1. 调用 `enableVideo` 方法，启用视频模块。
2. 调用 `startPreview` 方法，开启本地视频预览。

```javascript
// 启用视频模块
rtcEngine.enableVideo();
// 开启本地视频预览
rtcEngine.startPreview();
```

### 加入频道并发布音视频流 [#加入频道并发布音视频流-6]

调用 `joinChannel` 方法，填入你在控制台获取的临时 Token，以及获取 Token 时填入的频道名加入频道，并在 `options` 参数中设置频道场景、用户角色等信息。

```javascript
// 使用临时 Token 加入频道
rtcEngine.joinChannel(token, channel, uid, {
    // 设置频道场景为直播场景
    channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
    // 设置用户角色为主播；如果要将用户角色设置为观众，保持默认值即可
    clientRoleType: ClientRoleType.ClientRoleBroadcaster,
    // 发布麦克风采集的音频
    publishMicrophoneTrack: true,
    // 发布摄像头采集的视频
    publishCameraTrack: true,
    // 自动订阅所有音频流
    autoSubscribeAudio: true,
    // 自动订阅所有视频流
    autoSubscribeVideo: true,
});
```

### 实现常用回调 [#实现常用回调-6]

调用 `registerEventHandler` 方法注册以下回调事件，触发事件时使用 `console.log` 方法提示结果，并执行特定操作：

- `onJoinChannelSuccess`：成功加入频道回调。本地用户加入频道后，调用 `setupLocalVideo` 设置本地视频窗口。
- `onUserJoined`：远端用户加入当前频道回调。远端用户加入频道后，调用 `setupRemoteVideo` 设置远端视频窗口。
- `onUserOffline`：远端用户离开当前频道回调。远端用户离开频道后，调用 `setupRemoteVideo` 关闭远端视频窗口。

```javascript
const EventHandles = {
    // 监听本地用户加入频道事件
    onJoinChannelSuccess: ({ channelId, localUid }, elapsed) => {
        console.log('成功加入频道：' + channelId);
        // 本地用户加入频道后，设置本地视频窗口
        rtcEngine.setupLocalVideo({
             sourceType: VideoSourceType.VideoSourceCameraPrimary,
             uid: uid,
             view: localVideoContainer,
             setupMode: VideoViewSetupMode.VideoViewSetupAdd,
        });
    },

    // 监听远端用户加入频道事件
    onUserJoined: ({ channelId, localUid }, remoteUid, elapsed) => {
        console.log('远端用户 ' + remoteUid + ' 已加入');
        // 远端用户加入频道后，设置远端视频窗口
        rtcEngine.setupRemoteVideo(
            {
                sourceType: VideoSourceType.VideoSourceRemote,
                uid: remoteUid,
                view: remoteVideoContainer,
                setupMode: VideoViewSetupMode.VideoViewSetupAdd,
            },
            { channelId },
        );
    },

    // 监听用户离开频道事件
    onUserOffline: ( { channelId, localUid }, remoteUid, reason ) => {
        console.log('远端用户 ' + remoteUid + ' 已离开频道');
        // 远端用户离开频道后，关闭远端视频窗口
        rtcEngine.setupRemoteVideo(
            {
                sourceType: VideoSourceType.VideoSourceRemote,
                uid: remoteUid,
                view: remoteVideoContainer,
                setupMode: VideoViewSetupMode.VideoViewSetupRemove,                
            },
        );
    },
};

// 注册事件回调
rtcEngine.registerEventHandler(EventHandles);
```

### 离开频道 [#离开频道-1]

根据场景需要，如关闭应用或应用切换至后台时，调用 `leaveChannel` 离开当前频道。

```javascript
// 停止预览
rtcEngine.stopPreview();
// 离开频道
rtcEngine.leaveChannel();
```

如果你不再需要互动，调用 `release` 方法释放引擎资源。

```javascript
// 取消注册回调
rtcEngine.unregisterEventHandler(EventHandles);
// 销毁引擎
rtcEngine.release();
```

:::error[警告]
- 调用 `release` 后，你将无法再使用 SDK 的所有方法和回调。如需再次使用实时音频互动功能，你必须重新创建一个新的引擎。详见[初始化引擎](#初始化引擎-6)。
- 该方法为同步调用。需要等待引擎资源释放后才能执行其他操作，因此建议在子线程中调用该方法，避免主线程阻塞。
:::

##  测试项目 [#测试项目]

参考以下步骤测试你的 Electron 项目：

1. 确保已经在代码中传入了有效的 `appId`，`channelName` 和 `token`。详见[开通 RTC 服务](/zh-CN/realtime-media/rtc/build/setup-and-access/enable-service)。

2. 在终端中进入项目根目录，运行以下命令：

    ```shell
    npm start
    ```

    项目成功运行后，你会看到一个自动弹出的窗口，在窗口中看到本地的视频画面。

3. 邀请一位朋友使用第二台设备，重复以上步骤，在该设备上安装 App，并打开 App 加入频道。

4. 你的朋友成功加入后，你们可以听见、看见对方。

## 后续步骤 [#后续步骤-5]

- 本文的示例使用了临时 Token 加入频道。在测试或生产环境中，为确保通信安全，声网推荐使用 Token 服务器来生成 Token，详见[使用 Token 鉴权](/zh-CN/realtime-media/rtc/build/setup-and-access/token-authentication)。
- 如果你想要实现极速直播场景，可以在互动直播的基础上，通过修改观众端的延时级别为低延时 (`audienceLatencyLevelLowLatency`)实现。详见[实现极速直播](/zh-CN/realtime-media/rtc/build/advanced-channel/ls-quick-start)。

## 参考信息 [#参考信息-7]

### 示例项目 [#示例项目-7]

声网提供了开源的实时音视频互动示例项目供你参考，你可以前往下载或查看其中的源代码。

<Cards>
  <Card title={<RepositoryCardTitle provider="gitee">JoinChannelVideo</RepositoryCardTitle>} href="https://gitee.com/agoraio-community/Electron-SDK/blob/main/example/src/renderer/examples/basic/JoinChannelVideo/JoinChannelVideo.tsx" />
  <Card title={<RepositoryCardTitle provider="github">JoinChannelVideo</RepositoryCardTitle>} href="https://github.com/AgoraIO-Extensions/Electron-SDK/blob/main/example/src/renderer/examples/basic/JoinChannelVideo/JoinChannelVideo.tsx" />
</Cards>

### 常见问题 [#常见问题-6]

- [Electron 平台常见开发问题](/zh-CN/reference/faq/integration/electron_faq)
- [直播场景下，如何监听远端观众角色用户加入/离开频道的事件？](/zh-CN/reference/faq/integration/audience_event)
- [如何处理视频黑屏问题？](/zh-CN/reference/faq/quality/video_blank)
- [为什么我无法打开摄像头？](/zh-CN/reference/faq/quality/video_camera)
- [如何处理频道相关常见问题？](/zh-CN/reference/faq/integration/channel)
- [如何设置日志文件？](/zh-CN/reference/faq/integration/set_log_file)
- [编译 Xcode 项目时遇到无法打开 framework 的弹窗警告怎么办？](/zh-CN/reference/faq/integration/framework_cannot_be_opened)
- [运行 Electron App 时，控制台提示 “WARNING:Too many active WebGL contexts” 怎么办？](/zh-CN/reference/faq/integration/webgl_context)
- [为什么 SDK 中使用的是动态库而不是静态库？](/zh-CN/reference/faq/integration/dynamic_or_static_library)

### 相关文档 [#相关文档-6]

- [SDK 错误码](/zh-CN/realtime-media/rtc/reference/error-code)
- [频道连接状态管理](/zh-CN/realtime-media/rtc/build/initialize-and-channel/channel-connection)












