实现音视频通话

更新时间: 2025/06/11 16:45:39

网易云信音视频通话产品的基本功能包括高质量的实时音视频通话。当您成功初始化 SDK 之后,您可以简单体验本产品的基本业务流程。本文为您展示音视频通话提供的基本业务流程。

前提条件

请确认您已完成以下操作:

示例代码

网易云信为您提供完整的 创建界面实现基础音视频通话 的示例代码作为参考,您可以直接拷贝用于运行测试。

单击查看运行 SDK 前的 module.json5 权限配置。

entry/src/main/module.json5 中增加 requestPermissions

JSON"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET" // 网络权限
  },
  {
    "name": "ohos.permission.MICROPHONE" // 麦克风权限
  },
  {
    "name": "ohos.permission.CAMERA" // 相机权限
  }
]
单击查看实现音视频通话的完整示例代码。
TypeScriptimport common from '@ohos.app.ability.common';
import { NERtcVideoView , NERtcSDK, NERtcConstants, NERtcCallbackEx} from '@nertc/nertc_sdk';
import prompt from '@ohos.promptAction';
import AbilityAccessCtrl from '@ohos.abilityAccessCtrl';
class User {
  uid: bigint = BigInt(0);
  width?: number = 150;
  height?: number = 150;
  local?: boolean = false; // Is yourself?
}

interface LoginInfo {
  cname: string;
  uid: string;
}
interface Delegate {
  onUserJoin?:(uid: bigint) => void
  onUserLeave?:(uid: bigint, reason: number) => void
}
@Entry
@Component
struct Index {
  @State message: string = 'Hello World';
  TAG: string = "VideoCall"
  context = getContext(this) as common.UIAbilityContext;
  login: LoginInfo = {
    cname: "1392",
    uid: Math.floor(Math.random() * 10000).toString()
  };
  joinFlag: boolean = false
  @State local?: User = undefined
  @State remote?: User = undefined
  delegate: Delegate ={
    onUserJoin:(uid: bigint): void => {
      console.info(this.TAG, `User: ${uid} joined.`)
      if(this.remote == undefined) {
        this.remote = { uid: uid }
        console.info(this.TAG, 'Create Remote User.')
      }
    },

    onUserLeave:(uid: bigint, reason: number): void => {
      console.info(this.TAG, `User: ${uid} leave.`)
      if(this.remote && this.remote.uid === uid) {
        this.remote = undefined
      }
    }
  }
  onPageShow() {
    console.info(this.TAG, '=== CallPage show ===')
    this.RequestPermission()
  }

  build() {
    Row() {
      Column() {
        Text("单击屏幕加入房间")
        Flex({ direction: FlexDirection.Column }) {
          Stack({ alignContent: Alignment.BottomStart }) {
            Stack({ alignContent: Alignment.TopEnd }){

              if(this.remote) {
                NERtcVideoView ({
                  sCanvasId: String(this.remote.uid),
                  onLoad: (() => {
                    if(this.remote) {
                      console.info(this.TAG, `User: ${String(this.remote?.uid)} surface create.`)
                      this.attach(this.remote)
                    }
                  }),
                  onDestroy: (() => {
                    if(this.remote) {
                      console.info(this.TAG, `User: ${String(this.remote?.uid)} surface release.`)
                    }
                  })
                }).width('100%').height('100%')
              }

              if(this.local) {
                NERtcVideoView({
                  sCanvasId: String(this.local.uid),
                  onLoad: (() => {
                    console.info(this.TAG, `User: ${this.local?.uid} surface create.`)
                    if(this.local) this.attach(this.local)
                  }),
                  onDestroy: (() => {
                    console.info(this.TAG, `User: ${this.local?.uid} surface release.`)
                  })
                }).width(this.remote ? 200 : '100%').height(this.remote ? 200 : '100%')
              }

            }.width('100%').height('100%')
          }.width('100%').height('100%')
        }.onClick(() => {
          if(!this.joinFlag) {
            //单击屏幕入会
            let option: NERtcConstants.NERtcOption = { logLevel: NERtcConstants.LogLevel.INFO }
            NERtcSDK.getInstance().init(getContext(), "your-app-key", new ChatCallback(this.delegate), option)
            NERtcSDK.getInstance().enableLocalVideo(true, NERtcConstants.NERtcVideoStreamType.kNERtcVideoStreamTypeMain)
            NERtcSDK.getInstance().joinChannel('', this.login.cname, BigInt(this.login.uid))
            this.local = { uid: BigInt(this.login.uid ?? 0) }
            this.joinFlag = !this.joinFlag
          }else{
            NERtcSDK.getInstance().leaveChannel()
            NERtcSDK.getInstance().release()
            this.joinFlag = !this.joinFlag
          }
        })
      }
      .width('100%')
    }
    .height('100%')
  }

  attach(user: User) {

    let canvas: NERtcConstants.NERtcVideoCanvas = { canvasId: String(user.uid) }
    let local = String(user.uid) === this.login?.uid
    console.info(this.TAG, ' local' + user.uid + 'local2' + this.login?.uid)
    if(local) {
      let ret = NERtcSDK.getInstance().setupLocalVideoCanvas(canvas)
      console.info(this.TAG, 'setLocalVideoCanvas ret:' + ret)
    } else {
      let ret = NERtcSDK.getInstance().setupRemoteVideoCanvas(canvas, user.uid)
      console.info(this.TAG, 'setupRemoteVideoCanvas ret:' + ret)
      NERtcSDK.getInstance().subscribeRemoteVideo(user.uid, true, NERtcConstants.NERtcVideoStreamType.kNERtcVideoStreamTypeMain, NERtcConstants.NERtcRemoteVideoSubscribeType.kNERtcRemoteVideoSubscribeTypeHigh)
    }
  }

  async RequestPermission() {
    let atManager = AbilityAccessCtrl.createAtManager();
    let context = getContext(this);
    try {
      atManager.requestPermissionsFromUser(
        context,
        ["ohos.permission.CAMERA","ohos.permission.MICROPHONE"],
        (err, data) =>
        {
          if(err) {
            prompt.showToast({message: ` 请求权限失败: ${err}`})
            return
          }
          let micGrant: boolean = data.authResults[1] === AbilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
          let cameraGrant: boolean = data.authResults[0] === AbilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;

          if(!micGrant) {
            prompt.showToast({ message: "麦克风权限未授予" })
          }
          if(!cameraGrant) {
            prompt.showToast({message: "摄像头权限未授予" })
          }
        })
    } catch(err) {
      prompt.showToast({ message: `请求权限失败: ${JSON.stringify(err)}`})
    }
  }
}
class ChatCallback extends NERtcCallbackEx {
  delegate: Delegate
  constructor(delegate: Delegate) {
    super()
    this.delegate = delegate;
  }
  onJoinChannel(result: number, channelId: bigint, elapsed: bigint, uid: bigint): void {
    prompt.showToast({ message: 'Join channel async result:' + result, duration: 2000 })
  }

  onLeaveChannel(result: number): void {

  }

  onUserJoined(uid: bigint, extraInfo?: NERtcConstants.NERtcUserJoinExtraInfo): void {
    if(this.delegate) {
      if (!this.delegate.onUserJoin) throw new Error("onUserJoin not find");
      this.delegate?.onUserJoin(uid)
    }
  }

  onUserLeave(uid: bigint, reason: number, extraInfo?: NERtcConstants.NERtcUserLeaveExtraInfo): void {
    if(this.delegate) {
      if (!this.delegate.onUserLeave) throw new Error("onUserJoin not find");
      this.delegate?.onUserLeave(uid, reason)
    }
  }

  onUserAudioStart(uid: bigint): void {

  }

  onUserAudioStop(uid: bigint): void {

  }

  onUserVideoStart(uid: bigint, maxProfile: number): void {

  }

  onUserVideoStop(uid: bigint): void {

  }
  onLastmileQuality(quality: number):void {

  }
  onLastmileProbeResult(result: NERtcConstants.LiteSDKProbeResult):void {

  }
  onDisconnect(reason: number): void {

  }

  onClientRoleChange(oldRole: number, newRole: number): void {

  }

  onRecvSEIMsg(userId: bigint, data: Uint8Array, dataSize: number): void {

  }
}

实现流程

音频通话

实现 音频通话 的 API 调用时序如下图所示。

sequenceDiagram
    autonumber
    participant 您的 App
    participant 网易云信 RTC SDK
    participant 网易云信服务端

    您的 App->>网易云信 RTC SDK: initialize 初始化 NERtcEngine
    您的 App->>网易云信 RTC SDK: joinChannel 加入房间
    网易云信 RTC SDK->>网易云信服务端: 请求加入房间
    网易云信 RTC SDK-->>您的 App: onJoinChannel
    您的 App->>网易云信 RTC SDK: leaveChannel 离开房间
    网易云信 RTC SDK->>网易云信服务端: 请求离开房间
    您的 App->>网易云信 RTC SDK: release 销毁实例

视频通话

实现 视频通话 的 API 调用时序如下图所示。

sequenceDiagram
    autonumber
    participant 您的 App
    participant 网易云信 RTC SDK
    participant 网易云信服务端

    您的 App->>网易云信 RTC SDK: initialize 初始化 NERtcEngine

    Note over 您的 App, 网易云信 RTC SDK: 设置本地视图
    您的 App->>网易云信 RTC SDK: setupLocalVideoCanvas
    您的 App->>网易云信 RTC SDK: enableLocalVideo

    Note over 您的 App, 网易云信 RTC SDK: 加入房间
    您的 App->>网易云信 RTC SDK: joinChannel
    网易云信 RTC SDK->>网易云信服务端: 请求加入房间
    网易云信 RTC SDK-->>您的 App: onJoinChannel

    Note over 您的 App, 网易云信 RTC SDK: 设置远端视图
    Note right of 网易云信 RTC SDK: 远端用户加入房间
    网易云信 RTC SDK-->>您的 App: onUserJoined 远端用户加入房间的回调
    网易云信 RTC SDK-->>您的 App: onUserVideoStart 远端用户发布视频流的回调
    您的 App->>网易云信 RTC SDK: setupRemoteVideoCanvas 设置远端视频画布
    您的 App->>网易云信 RTC SDK: subscribeRemoteVideoStream 订阅远端视频流
    网易云信服务端-->>您的 App : onFirstVideoFrameDecoded 已接收到远端视频首帧并完成解码的回调

    Note over 您的 App, 网易云信 RTC SDK: 离开房间
    您的 App->>网易云信 RTC SDK: leaveChannel
    网易云信 RTC SDK->>网易云信服务端: 请求离开房间

    您的 App->>网易云信 RTC SDK: release 销毁实例

实现音视频通话

一:(可选)创建音视频通话界面

您可以参考此步骤根据业务场景创建相应的音视频通话界面,若您已实现相应界面,请忽略该步骤。

实现基础的音视频通话,建议您参考 示例代码 完成界面创建。

二:导入类

在您的工程中 oh-package.json5 文件中添加对 NERTC SDK 的依赖:

JSON "dependencies": {
    '@nertc/nertc_sdk': "file:./src/main/libs/nertc_sdk.har"
  }

在您的工程中对应实现音视频通话的 ets 文件里添加如下代码先导入以下重要类:

TypeScriptimport { NERtcVideoView , NERtcSDK, NERtcConstants, NERtcCallbackEx} from '@nertc/nertc_sdk';

三:初始化

默认情况下,请在导入后的文件中先执行 init 方法完成初始化。

您需要将 App_Key 替换为您的应用对应的 App Key。

示例代码 如下:

TypeScriptclass ChatCallback extends NERtcCallbackEx {
  //...
}

initializeSDK() {
    NERtcSDK.getInstance().init(getContext(), "your-app-key", new ChatCallback(), option);
     ...
}

为了实现标准音视频通话业务,您还需要在初始化时 注册相关必要回调,建议您请在初始化方法中传入原型为 NERtcCallbackEx 的以下回调,并增加相应必要的处理。

TypeScript//NERtcCallbackEx 重要回调

//本端用户加入房间结果回调
onJoinChannel(result: number, channelId: bigint, elapsed: bigint, uid: bigint): void {
    if (result == NERtcConstants.ErrorCode.NO_ERROR) {
      // 加入房间成功
    } else {
      // 加入房间失败,退出页面
    }
}

//本端用户离开房间回调
onLeaveChannel(result: number): void {

}

//远端用户加入房间
onUserJoined(uid: bigint, extraInfo?: NERtcConstants.NERtcUserJoinExtraInfo): void {

}

//远端用户离开房间
onUserLeave(uid: bigint, reason: number, extraInfo?: NERtcConstants.NERtcUserLeaveExtraInfo): void {

}

//远端用户打开音频
onUserAudioStart(uid: bigint): void {

}

//远端用户关闭音频
onUserAudioStop(uid: bigint): void {

}

//远端用户打开视频,建议在此按需设置画布及订阅视频
onUserVideoStart(uid: bigint, maxProfile: number): void {

}

//远端用户关闭视频,可释放之前绑定的画布
onUserVideoStop(uid: bigint): void {

}

//与服务器断连,退出页面
onDisconnect(reason: number): void {

}

四:设置本地视图

初始化成功后,可以设置本地视图,来预览本地图像。您可以根据业务需要实现加入房间之前预览或加入房间后预览。

  • 若您想设置画布渲染参数,可以调用 setScalingType 方法设置渲染缩放模式或调用 setMirror 方法设置镜像模式。
  • 若您想调整摄像头的相关参数,请参考 视频设备管理 进行设置。
  • 在加入房间前,默认预览分辨率为 640*480,您可以通过 setLocalVideoConfig 接口的 width height 参数调整采集分辨率。
  • 实现加入房间前预览。

    1. 调用 setupLocalVideoCanvasstartVideoPreview(streamType) 方法,在加入房间前设置本地视图,预览本地图像。需要注意的一点是传入给 NERTC SDK canvasId 需要和画布 NERtcVideoView 中的 sCanvasId 保持一致,示例代码是通过用户 ID 来保证的。

      示例代码 如下:

      TypeScriptclass User {
          uid: bigint = BigInt(0);
          width?: number = 150;
          height?: number = 150;
          local?: boolean = false; // Is yourself?
      }
      attach(user: User) {
          let canvas: NERtcConstants.NERtcVideoCanvas = { canvasId: String(user.uid) }
          let local = String(user.uid) === this.login?.uid
          console.info(this.TAG, ' local' + user.uid + 'local2' + this.login?.uid)
          if(local) {
              let ret = NERtcSDK.getInstance().setupLocalVideoCanvas(canvas)
              console.info(this.TAG, 'setLocalVideoCanvas ret:' + ret)
              }
          }
      NERtcSDK.getInstance().startVideoPreview(NERtcConstants.NERtcVideoStreamType.kNERtcVideoStreamTypeMain)
      NERtcVideoView({
          sCanvasId: String(this.local.uid),
          onLoad: (() => {
              console.info(this.TAG, `User: ${this.local?.uid} surface create.`)
              if(this.local) this.attach(this.local) //调用 attach 方法绑定画布
          }),
          onDestroy: (() => {
              console.info(this.TAG, `User: ${this.local?.uid} surface release.`)
          })
      }).width(this.remote ? 200 : '100%').height(this.remote ? 200 : '100%')
      
      
    2. 若要结束预览,或者准备加入房间时,调用 stopVideoPreview(streamType) 方法停止预览。

      stopVideoPreview(streamType)streamType 参数请与 startVideoPreview(streamType) 的保持一致,即同为主流或辅流的开启和停止预览。

  • 实现加入房间后预览。

    调用 setupLocalVideoCanvas 设置本地视图,再调用 enableLocalVideo(streamType) 方法进行视频的采集发送与预览。成功加入房间后,即可预览本地图像。

    示例代码 如下:

    TypeScript  class User {
          uid: bigint = BigInt(0);
          width?: number = 150;
          height?: number = 150;
          local?: boolean = false; // Is yourself?
      }
      attach(user: User) {
        let canvas: NERtcConstants.NERtcVideoCanvas = { canvasId: String(user.uid) }
        let local = String(user.uid) === this.login?.uid
        console.info(this.TAG, ' local' + user.uid + 'local2' + this.login?.uid)
        if(local) {
          let ret = NERtcSDK.getInstance().setupLocalVideoCanvas(canvas)
          console.info(this.TAG, 'setLocalVideoCanvas ret:' + ret)
        }
        }
        NERtcSDK.getInstance().enableLocalVideo(true, NERtcConstants.NERtcVideoStreamType.kNERtcVideoStreamTypeMain)
        NERtcVideoView({
          sCanvasId: String(this.local.uid),
          onLoad: (() => {
            console.info(this.TAG, `User: ${this.local?.uid} surface create.`)
            if(this.local) this.attach(this.local) //调用 attach 方法绑定画布
          }),
          onDestroy: (() => {
            console.info(this.TAG, `User: ${this.local?.uid} surface release.`)
          })
        }).width(this.remote ? 200 : '100%').height(this.remote ? 200 : '100%')
    
    

五:加入房间

加入房间前,请确保已完成初始化相关事项。若您的业务中涉及呼叫邀请等机制,建议通过 信令 实现,总体实现流程请参考 一对一会话操作流程,具体呼叫邀请机制的实现请参考 邀请机制

调用 joinChannel 方法加入房间。

示例代码 如下:

TypeScriptNERtcSDK.getInstance().joinChannel(token,channelName,uid,channelOptions);

参数说明

参数 说明
token 安全认证签名(NERTC Token)。
  • 调试模式下:可设置为 null。产品默认为安全模式,您可以在网易云信控制台将鉴权模式修改为调试模式,具体请参考 Token 鉴权
    调试模式的安全性不高,请在产品正式上线前修改为安全模式。
  • 产品正式上线后:请设置为已获取的 Token。安全模式下必须设置为获取到的 Token。若未传入正确的 Token 将无法进入房间。

    推荐使用安全模式

channelName 房间名称,长度为 1 ~ 64 字节。目前支持以下 89 个字符:a-z, A-Z, 0-9, space, !#$%&()+-:;≤.,>? @[]^_{|}~"。
设置相同房间名称的用户会进入同一个通话房间。
您也可以在加入通道前,通过 创建房间 接口创建房间。加入房间时,若传入的 {channelName} 未事先创建,则网易云信服务器内部将为其自动创建一个名为 {channelName} 的通话房间。
uid 用户的唯一标识 ID,为数字串,房间内每个用户的 uid 必须是唯一的。此 uid 为用户在您应用中的 ID,请在您的业务服务器上自行管理并维护。
channelOptions 加入房间时可以设置携带一些特定信息,包括高级权限密钥。默认值为 NULL,具体请参考 `NERtcJoinChannelOptions`。
  • SDK 发起加入房间请求后,服务器会进行响应,您可以通过 NERtcCallbackonJoinChannel 回调监听加入房间的结果,同时该回调会抛出当前通话房间的 channelId 与加入房间总耗时(毫秒)。其中 channelId 即音视频通话的 ID,建议您在业务层保存该数据,以便于后续问题排查。

  • 成功加入房间之后,您可以通过监听 onConnectionStateChanged 回调实时监控自己在本房间内的连接状态。

六:设置远端视图并发起订阅

音视频通话过程中,除了要显示本地的视频画面,通常也要显示参与互动的其他连麦者/主播的远端视频画面。

单击展开查看设置远端视图并订阅的完整示例代码。
TypeScript//对方开启视频,按需设置画布及订阅视频

let ret = NERtcSDK.getInstance().setupRemoteVideoCanvas(canvas, user.uid)
console.info(this.TAG, 'setupRemoteVideoCanvas ret:' + ret)
NERtcSDK.getInstance().subscribeRemoteVideo(user.uid, true, NERtcConstants.NERtcVideoStreamType.kNERtcVideoStreamTypeMain, NERtcConstants.NERtcRemoteVideoSubscribeType.kNERtcRemoteVideoSubscribeTypeHigh)
  1. 监听远端用户进出房间。

    当远端用户加入房间时,本端会触发 onUserJoined 回调,并抛出对方的 uid。

    当本端加入房间后,也会通过此回调抛出通话房间内已有的其他用户。

  2. 设置远端视频画布。

    在监听到远端用户加入房间或发布视频流后,本端可以调用 setupRemoteVideoCanvas 方法设置远端用户视频画布,用于显示其视频画面。

    示例代码 如下:

    TypeScriptlet ret = NERtcSDK.getInstance().setupRemoteVideoCanvas(canvas, user.uid)
    console.info(this.TAG, 'setupRemoteVideoCanvas ret:' + ret)
    
  3. 监听远端视频流发布。

    当房间中的其他用户发布视频流时,本端会触发 onUserVideoStart 回调。

  4. 订阅远端视频流。

    在监听到远端用户发布视频流后,本端可以调用 subscribeRemoteVideoStream 方法对其发起视频流的订阅,来将对方的视频流渲染到视频画布上。

    示例代码 如下:

    TypeScriptNERtcSDK.getInstance().subscribeRemoteVideo(user.uid, true, NERtcConstants.NERtcVideoStreamType.kNERtcVideoStreamTypeMain, NERtcConstants.NERtcRemoteVideoSubscribeType.kNERtcRemoteVideoSubscribeTypeHigh)
    
  5. 监听远端用户离开房间或关闭视频功能。

七:音频流

在 NERTC SDK 中,本地音频的采集发布和远端音频订阅播放是默认启动的,正常情况下无需开发者主动干预。

八:退出通话房间

调用 leaveChannel 方法退出通话房间。

示例代码 如下:

TypeScriptNERtcSDK.getInstance().leaveChannel()

NERtcCallback 提供 onLeaveChannel 回调来监听当前用户退出房间的结果。

九:销毁实例

当确定 App 短期内不再使用音视频通话实例时,可以调用 release 方法释放对应的对象资源。

示例代码 如下:

TypeScript// 销毁实例
NERtcSDK.getInstance().release();
此文档是否对你有帮助?
有帮助
去反馈
  • 前提条件
  • 示例代码
  • 实现流程
  • 音频通话
  • 视频通话
  • 实现音视频通话
  • 一:(可选)创建音视频通话界面
  • 二:导入类
  • 三:初始化
  • 四:设置本地视图
  • 五:加入房间
  • 六:设置远端视图并发起订阅
  • 七:音频流
  • 八:退出通话房间
  • 九:销毁实例