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

> 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` 方法修改用户角色，使其具备发流权限。

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

在实现功能以前，请按照以下要求准备开发环境：

- DevEco Studio 5.1.0 Release 及以上版本，已包含所需的 HarmonyOS SDK。
- 运行 API 12 及以上版本 HarmonyOS 的设备。
- 华为开发者账号。可在[华为开发者联盟](https://developer.huawei.com/consumer/cn/)注册账号。
- 两台支持 HarmonyOS 的设备，建议使用 Mate 60 Pro 及更高性能的设备。
- 可以访问互联网的计算机。如果你的网络环境部署了防火墙，参考[应对防火墙限制](/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。

:::warning[注意]
      临时 Token 的有效期是 24 小时。Token 过期会导致加入频道失败。
      :::

## 创建项目 [#创建项目-5]

本小节介绍如何创建项目并为项目添加体验实时互动所需的权限。

1. 参考[创建和运行 Hello World](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/hello_world-0000001054516888-V2)，创建一个新工程。

2. 打开 **entry/src/main** 中的 `module.json5` 文件，在 `module` 模块中声明网络和设备权限：

    ```json
    "requestPermissions": [
      // 访问互联网的权限
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:Internet_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "always"  // 始终需要该权限
        }
      },
      // 访问麦克风的权限
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:Audio_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "always"  // 始终需要该权限
        }
      },
      // 访问摄像头的权限
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:Video_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"  // 仅在使用时需要该权限
        }
      }
    ]
    ```

3. 打开 **src/main/resources/base/element** 中的 `string.json` 文件，添加如下代码，定义所需要的权限。

    ```json
    {
      "name": "Internet_reason",
      "value": "access internet"
    },
    {
      "name": "Audio_reason",
      "value": "access audio"
    },
    {
      "name": "Video_reason",
      "value": "access video"
    }
    ```

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

1. 在[下载](/zh-CN/realtime-media/rtc/reference/downloads)页面下载最新版本的鸿蒙视频 SDK，并在本地解压。
2. 打开解压文件，将 `AgoraRtcSdk.har` 文件复制到项目 **entry -> libs** 目录下（libs 文件夹需要手动创建）。
3. 在 **entry** 下的 `oh-package.json5` 文件中添加如下依赖，然后点击 **Sync** 开始同步。

    ```json
    "dependencies": {
      "@shengwang/rtc-full":"file:./libs/AgoraRtcSdk.har"
    }
    ```

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

本小节介绍如何实现一个实时音视频互动 App。你可以先复制完整的示例代码到你的项目中，快速体验实时音视频互动的基础功能，再按照实现步骤了解核心 API 调用。

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

<DocsImage alt="实现流程" src="https://web-cdn.agora.io/docs-portal/img/rtc/quick-start-sequence.svg" displayWidth="70%" />

下面列出了一段实现实时互动基本流程的完整代码以供参考。复制以下代码到 `pages/Index.ets` 文件中替换原有内容，即可快速体验实时互动基础功能。

:::info[信息]
在 `appId`、`token` 和 `channelName` 字段中传入你在控制台获取到的 App ID、临时 Token，以及生成临时 Token 时填入的频道名。
:::

<Accordions>
<Accordion title="实现实时音视频互动示例代码">

```arkts
import {
    ChannelMediaOptions,
    Constants,
    RtcEngine,
    RtcEngineConfig,
    RtcStats,
    VideoCanvas,
} from "AgoraRtcSdk"
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct Index {
    private rtcEngine: RtcEngine | null = null;
    @State isJoined: boolean = false;
    private channel = "hmostest";

    build() {
        Flex({ direction: FlexDirection.Column }) {
            Stack({ alignContent: Alignment.TopStart }) {
                XComponent({
                    // id 必须在当前页面是唯一的
                    id: 'local',
                    // type 必须设置为 surface
                    type: 'surface',
                    // libraryname 必须设置为 Constants。AGORA_LIB_NAME
                    libraryname: Constants.AGORA_LIB_NAME,
                })
                    .width('100%')
                    .height('100%')
                    .backgroundColor(Color.Gray)
                    .align(Alignment.TopStart)

                XComponent({
                    id: 'remote',
                    type: 'surface',
                    libraryname: Constants.AGORA_LIB_NAME,
                })
                    .onLoad(() => {
                    })
                    .width('33%')
                    .height('25%')
                    .margin({ top: 10, left: 10 })
                    .align(Alignment.TopStart)

            }.width("100%")
            .height("85%")
            .margin({ top: 10, bottom: 10 })

            Row() {
                Button("Join")
                    .enabled(!this.isJoined)
                    .fontSize(20)
                    .width("40%")
                    .margin({ left: "5%", right: "5%" })
                    .align(Alignment.Center)
                    .onClick(() => {
                        this.initEngineAndJoinChannel();
                    })
                Button("Leave")
                    .enabled(this.isJoined)
                    .fontSize(20)
                    .width("40%")
                    .align(Alignment.Center)
                    .onClick(() => {
                        this.rtcEngine!.stopPreview();
                        this.rtcEngine!.leaveChannel();
                    })
                    .margin({ left: "5%", right: "5%" })
            }.align(Alignment.Center)
            .margin({ top: "5%", bottom: "5%" })
        }
    }

    initEngineAndJoinChannel() {
        if (this.rtcEngine === null) {
            let config: RtcEngineConfig = new RtcEngineConfig();
            let context = getContext(this) as common.UIAbilityContext;
            config.mContext = context;
            config.mAppId = "<#Your App ID#>";
            config.mEventHandler = {};
            config.mEventHandler.onUserJoined = (uid: number, collapse: number) => {
                console.info("mEventHandler.onUserJoined: " + uid + " , " + collapse);
                let canvas: VideoCanvas = new VideoCanvas('remote');
                canvas.uid = uid;
                this.rtcEngine!.setupRemoteVideo(canvas);
            };
            config.mEventHandler.onUserOffline = (uid: number, reason: number) => {
                console.info("mEventHandler.onUserOffline: " + uid + " , " + reason);
            };
            config.mEventHandler.onJoinChannelSuccess = (cid: string, uid: number, elapsed: number) => {
                console.info("mEventHandler.onJoinChannelSuccess: " + cid + " , " + uid);
                this.isJoined = true;
            };
            config.mEventHandler.onLeaveChannel = (stats: RtcStats | null) => {
                console.info("mEventHandler.onLeaveChannel ");
                this.isJoined = false;
            };

            this.rtcEngine = RtcEngine.create(config);
            this.rtcEngine.enableVideo();

            // 设置本地视图
            // local 为用于渲染本地图像的 XComponent 的 ID
            let canvas: VideoCanvas = new VideoCanvas("local");
            canvas.uid = 0;
            canvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN;
            this.rtcEngine.setupLocalVideo(canvas);
            this.rtcEngine.startPreview();
        }
        let option: ChannelMediaOptions = new ChannelMediaOptions();
        option.autoSubscribeAudio = true;
        option.autoSubscribeVideo = true;
        option.publishCameraTrack = true;
        option.publishMicrophoneTrack = true;
        option.channelProfile = Constants.ChannelProfile.LIVE_BROADCASTING;
        option.clientRoleType = Constants.ClientRole.BROADCASTER;
        // joinChannelWithOptions(token:string, channelId:string, uid:number, options:ChannelMediaOptions)
        // uid 为 0，表示由服务器分配 uid，分配的 uid 将通过 onJoinChannelSuccess 回调返回
        this.rtcEngine.joinChannelWithOptions("<#Your Token#>", this.channel, 0, option);
    }

    aboutToDisappear(): void {
        if (this.rtcEngine != null) {
            RtcEngine.destroy();
            this.rtcEngine = null;
        }
    }
}
```

</Accordion>
</Accordions>

### 处理权限请求 [#处理权限请求-1]

本小节介绍如何获取设备的摄像头、录音等权限。

1. 在项目中打开 **entryability** -> `EntryAbility.ets` 文件，在 `EntryAbility` 之前添加权限申请，示例代码如下所示。

    启动应用程序时，检查是否已在 App 中授予了实现实时互动所需的权限。

    ```arkts
    const permissions: Array<Permissions> = ['ohos.permission.INTERNET','ohos.permission.MICROPHONE',"ohos.permission.CAMERA"];


    async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
      let atManager = abilityAccessCtrl.createAtManager();
      let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
      // 获取应用程序的 accessTokenID
      let tokenId: number = 0;
      try {
        let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
        let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
        tokenId = appInfo.accessTokenId;
      } catch (error) {
        let err: BusinessError = error as BusinessError;
        console.error(`Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`);
      }
      // 校验 App 是否被授予权限
      try {
        grantStatus = await atManager.checkAccessToken(tokenId, permission);
      } catch (error) {
        let err: BusinessError = error as BusinessError;
        console.error(`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
      }
      return grantStatus;
    }

    export default class EntryAbility extends UIAbility {
      onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
        let context = this.context;
        let atManager = abilityAccessCtrl.createAtManager();
        // requestPermissionsFromUser 会判断权限的授权状态来决定是否唤起弹窗
        for(let i=0;i<permissions.length;i++) {
          let permission: Permissions = permissions[i]
          let granted : boolean = false;
          let toGrantedPerm: Array<Permissions> = new Array<Permissions>();
          toGrantedPerm.push(permission)
          checkAccessToken(permission).then((value)=>{
            if(value == abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED){
              granted = true;
              hilog.info(0x0000, 'testTag', '%{public}s', 'Permission granted');
            } else {
              atManager.requestPermissionsFromUser(context, toGrantedPerm).then((data) => {
                let grantStatus: Array<number> = data.authResults;
                let length: number = grantStatus.length;
                for (let i = 0; i < length; i++) {
                  if (grantStatus[i] === 0) {
                    // 用户授权，可以继续访问目标操作
                  } else {
                    // 用户拒绝授权，提示用户必须授权才能访问当前页面的功能，并引导用户到系统设置中打开相应的权限
                    hilog.info(0x0000, 'testTag', '%{public}s', 'Permission denied');
                    return;
                  }
                }
                // 授权成功
              }).catch((err: BusinessError) => {
                hilog.info(0x0000, 'testTag', '%{public}s',`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
              })
            }
          })
        }
      }

    }
    ```

### 导入声网组件 [#导入声网组件-2]

打开 `src/main/ets/pages/Index.ets` 文件，导入如下所需的声网 SDK 组件。

```arkts
import {
    ChannelMediaOptions,
    Constants,
    RtcEngine,
    RtcEngineConfig,
    RtcStats,
    VideoCanvas,
} from "@shengwang/rtc-full"
```

### 添加视频渲染组件 [#添加视频渲染组件]

在 `Index.ets` 文件中添加视频渲染组件 `XComponent`。

```arkts
@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    Row() {
      Column() {
        XComponent({
            // id 必须在当前页面是唯一的
            id: 'local',
            // type 必须设置为 surface
            type: 'surface',
            // libraryname 必须设置为 Constants.AGORA_LIB_NAME
            libraryname: Constants.AGORA_LIB_NAME,
        })
        .width('100%')
        .height('100%')
        .align(Alignment.TopStart)
        XComponent({
          id: 'remote',
          type: 'surface',
          libraryname: Constants.AGORA_LIB_NAME,
        })
          .width('100%')
          .height('50%')
          .margin({ bottom: 10 })

        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}
```

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

调用 `create` 方法初始化 `RtcEngine`。

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

```arkts
let config:RtcEngineConfig = new RtcEngineConfig();
let context = getContext(this) as common.UIAbilityContext;
config.mContext = context;
// 在这里输入你在声网控制台中获取的 App ID
config.mAppId = "<#Your App ID#>";
config.mEventHandler = {};

// 创建并初始化 RtcEngine
this.rtcEngine = RtcEngine.create(config);
```

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

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

1. 调用 `enableVideo` 方法，启用视频模块。
2. 调用 `setupLocalVideo` 方法初始化本地视图，同时设置本地的视频显示属性。
3. 调用 `startPreview` 方法，开启本地视频预览。

```arkts
this.rtcEngine.enableVideo();

// 设置本地视图
// local 为用于渲染本地图像的 XComponent 的 ID
let canvas: VideoCanvas = new VideoCanvas("local");
canvas.uid = 0;
canvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN;
canvas.mirrorMode = VideoCanvas.VIDEO_MIRROR_MODE_ENABLED;
this.rtcEngine.setupLocalVideo(canvas);
this.rtcEngine.startPreview();
```

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

调用 `joinChannelWithOptions` 加入频道。在 `options` 中进行如下配置：

- 设置频道场景为 `BROADCASTING`（直播场景），并将用户角色设置为 `BROADCASTER`（主播）或 `AUDIENCE`（观众）。
- 将 `publishMicrophoneTrack` 和 `publishCameraTrack` 设置为 `true`，发布麦克风采集的音频和摄像头采集的视频。
- 将 `autoSubscribeAudio` 和 `autoSubscribeVideo` 设置为 `true`，自动订阅所有音视频流。

```arkts
let option:ChannelMediaOptions  = new ChannelMediaOptions();
// 自动订阅所有音频流
option.autoSubscribeAudio = true;
// 自动订阅所有视频流
option.autoSubscribeVideo = true;
// 发布摄像头采集的视频
option.publishCameraTrack = true;
// 发布麦克风采集的音频
option.publishMicrophoneTrack = true;
// 设置频道场景为直播
option.channelProfile = Constants.ChannelProfile.LIVE_BROADCASTING;
// 设置用户角色为主播；如果要将用户角色设置为观众，保持默认值即可
option.clientRoleType = Constants.ClientRole.BROADCASTER;
// 使用临时 Token 加入频道，在这里传入你的项目的 Token 和频道名
// uid 为 0，表示由服务器分配 uid，分配的 uid 将通过 onJoinChannelSuccess 回调返回
this.rtcEngine.joinChannelWithOptions(<#Your Token#>, <#Your Channel Name#>, 0,option);
```

### 设置远端视图 [#设置远端视图-2]

调用 `setupRemoteVideo` 方法初始化远端用户视图，同时设置远端用户的视图在本地显示属性。你可以通过 `onUserJoined` 回调获取远端用户的 `uid`。

```arkts
config.mEventHandler.onUserJoined = (uid: number, collapse: number) => {
  // 设置远端渲染
  // remote 为用于渲染远端图像的 XComponent 的 ID
  let canvas:VideoCanvas = new VideoCanvas('remote');
  canvas.uid = uid;
  this.rtcEngine!.setupRemoteVideo(canvas);
};
```

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

根据使用场景，定义必要的回调。以下示例代码展示如何实现 `onJoinChannelSuccess` 和 `onUserOffline` 回调。

```arkts
config.mEventHandler.onJoinChannelSuccess = (cid: string, uid: number, elapsed: number) => {
  console.info("mEventHandler.onJoinChannelSuccess: " + cid + " , " + uid);
};

config.mEventHandler.onUserOffline = (uid: number, reason: number) => {
  console.info("mEventHandler.onUserOffline: " + uid + " , " + reason);
};
```

### 开始音视频互动 [#开始音视频互动-4]

在 `onClick` 中调用一系列方法加载界面布局、检查 App 是否获取实时互动所需权限，并加入频道开始音视频互动。

```arkts
Button("Join")
.enabled(!this.isJoined)
.fontSize(20)
.width("40%")
.margin({ left: "5%", right: "5%" })
.align(Alignment.Center)
.onClick(() => {
    this.initEngineAndJoinChannel();
})
```

### 结束音视频互动 [#结束音视频互动-4]

按照以下步骤结束音视频互动：

1. 调用 `stopPreview` 停止视频预览。
2. 调用 `leaveChannel` 离开当前频道，释放所有会话相关的资源。
3. 调用 `destroy` 销毁引擎，并释放声网 SDK 中使用的所有资源。
   :::warning[注意]
   - 调用 `destroy` 后，你将无法再使用 SDK 的所有方法和回调。如需再次使用实时音视频互动功能，你必须重新创建一个新的引擎。详见[初始化引擎](#初始化引擎-5)。
       - 该方法为同步调用。需要等待引擎资源释放后才能执行其他操作，因此建议在子线程中调用该方法，避免主线程阻塞。
   :::

    ```arkts
    this.rtcEngine!.stopPreview();
    this.rtcEngine!.leaveChannel();
    RtcEngine.destroy();
    this.rtcEngine = null;
    ```

## 调试 App [#调试-app-3]

按照以下步骤测试直播 App：

1. 开启鸿蒙 NEXT 设备的开发者选项，打开 USB 调试，通过 USB 连接线将鸿蒙 NEXT 设备接入电脑。

2. 在 DevEcho Studio 中，点击 **Sync and Refresh Project**进行同步。

3. 待同步成功后，点击 **Run 'app'** 开始编译。片刻后，App 便会安装到你的鸿蒙 NEXT 设备上。

4. 启动 App，授予录音和摄像头权限，如果你将用户角色设置为主播，便会在本地视图中看到自己。

5. 使用第二台鸿蒙 NEXT 设备，重复以上步骤，在该设备上安装 App、打开 App 加入频道，观察测试结果：
    - 如果两台设备均作为主播加入频道，则可以看到对方并且听到对方的声音。
    - 如果两台设备分别作为主播和观众加入，则主播可以在本地视频窗口看到自己；观众可以在远端视频窗口看到主播、并听到主播的声音。

<DocsImage alt="快速开始" src="https://web-cdn.agora.io/docs-portal/img/rtc/harmony-quick-start-inchannel.png" displayWidth="40%" />

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

在完成音视频互动后，你可以阅读以下文档进一步了解：

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

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

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

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

<Cards>
  <Card title={<RepositoryCardTitle provider="gitee">JoinVideoChannel</RepositoryCardTitle>} href="https://gitee.com/agoraio-community/API-Examples/tree/main/HarmonyOS/APIExample/entry/src/main/ets/pages/basic" />
  <Card title={<RepositoryCardTitle provider="github">JoinVideoChannel</RepositoryCardTitle>} href="https://github.com/Shengwang-Community/API-Examples/tree/main/HarmonyOS/APIExample/entry/src/main/ets/pages/basic" />
</Cards>

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

- [直播场景下，如何监听远端观众角色用户加入/离开频道的事件？](/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)
- [为什么部分鸿蒙版本 App 锁屏或切后台音视频采集无效？](/zh-CN/reference/faq/quality/harmonyos_background)

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

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
















