Harmony

更新时间:
复制为 MD 格式

本文档将介绍如何在您的Harmony项目中集成 ARTC SDK, 快速实现一个简单的实时音视频互动App,适用于互动直播和视频通话等场景。

功能简介

在开始之前,了解以下几个关键概念会很有帮助:

  • ARTC SDK:这是阿里云的实时音视频产品,帮助开发者快速实现实时音视频互动的SDK。

  • GRTN:阿里云全球实时传输网络,提供超低延时、高音质、安全可靠的音视频通讯服务。

  • 频道:相当于一个虚拟的房间,所有加入同一频道的用户都可以进行实时音视频互动。

  • 主播:可在频道内发布音视频流,并可订阅其他主播发布的音视频流。

  • 观众:可在频道内订阅音视频流,不能发布音视频流。

实现实时音视频互动的基本流程如下:

image
  1. 用户需要调用setChannelProfile(设置频道场景),后调用joinChannel加入频道:

    • 视频通话场景:所有用户都是主播角色,可以进行推流和拉流

    • 互动直播场景:需要调用setClientRole(设置角色),在频道内推流的用户设置主播角色;如果用户只需要拉流,不需要推流,则设置观众角色。

  2. 加入频道后,不同角色的用户有不同的推拉流行为:

    • 所有加入频道内的用户都可以接收频道内的音视频流。

    • 主播角色可以在频道内推音视频流

    • 观众如果需要推流,需要调用setClientRole方法,将用户角色切换成主播,便可以推流。

示例项目

阿里云ARTC SDK提供了开源的实时音视频互动示例项目供客户参考,您可以前往下载或查看示例源码

前提条件

  • 获取 DevEco Studio 5.0.3.900 Release 或以上版本。

  • 获取配套 API Version 12的 HarmonyOS NEXT SDK 或以上版本。

  • 获取配套 API Version 12的 HarmonyOS NEXT 5.0.0.102 操作系统或以上版本,支持音视频的鸿蒙设备,且已开启“允许调试”选项。

  • 如果需要使用真机调试,请参考 鸿蒙官网文档 进行配置。

  • 鸿蒙设备已经连接到 Internet。

  • 注册华为开发者账号 并完成实名认证。

创建项目(可选)

  1. 打开 DevEco-Studio,选择 Create Project

  2. 选择 Application 并选择一个初始模板,在此以 Empty Ability 为例。

image.png

  1. 配置项目信息,包含项目名、包名、项目保存路径、SDK 版本等信息。

image.png

  1. 点击 Finish 完成创建,等待项目同步完成。

集成SDK

ohpm自动集成(推荐)

entry下的oh-package.json文件中配置:

"dependencies": {
    "@aliyun_video_cloud/alivcsdk_artc":"x.y.z",
}

运行命令:

ohpm install @aliyun_video_cloud/alivcsdk_artc

下载SDK手动集成

SDK下载中下载最新版本的Harmony ARTC SDK ,放到工程libs中。在工程中配置引用:

"dependencies": {
    "@aliyun_video_cloud/alivcsdk_artc":"file:./libs/AliVCSDK_ARTC-x.y.z.har",
  }

image

实现步骤

本节介绍如何使用阿里云 ARTC SDK 快速实现一个基础的实时音视频互动应用。你可以先将完整示例代码复制到项目,快速体验功能,再通过以下步骤了解核心 API 的调用。

下图展示了实现音视频互动的基本流程:

image

1、申请权限请求

进入entry/src/main/ets/entryability目录,打开EntryAbiliy.ets文件,添加所需权限。

import { abilityAccessCtrl, AbilityConstant, common, Permissions, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

const permissions: Array<Permissions> = ['ohos.permission.MICROPHONE','ohos.permission.CAMERA','ohos.permission.KEEP_BACKGROUND_RUNNING'];
// 使用UIExtensionAbility:将common.UIAbilityContext 替换为common.UIExtensionContext
function reqPermissionsFromUser(permissions: Array<Permissions>, context: common.UIAbilityContext): void {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
  atManager.requestPermissionsFromUser(context, permissions).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 {
        // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
        return;
      }
    }
    // 授权成功
  }).catch((err: BusinessError) => {
    console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
  })
}

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  }

  onDestroy(): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
    reqPermissionsFromUser(permissions, this.context);
    windowStage.loadContent('pages/Login', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
    });
  }

  onWindowStageDestroy(): void {
    // Main window is destroyed, release UI related resources
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    // Ability has brought to foreground
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    // Ability has back to background
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}


2、鉴权Token

加入ARTC频道需要一个鉴权Token,用于鉴权用户的合法身份,其鉴权Token生成规则详情请参见:Token鉴权。Token 生成有两种方式:单参数方式和多参数方式,不同的Token生成方式需要调用SDK不同的加入频道(joinChannel)的接口。

上线发布阶段

由于Token的生成需要使用AppKey,写死在客户端存在泄露的风险,因此强烈建议线上业务通过业务Server生成下发给客户端。

开发调试阶段

开发调试阶段,如果业务Server还没有生成Token的逻辑,可以暂时参考APIExample上的Token生成逻辑,生成临时Token,其参考代码如下:

import util from '@ohos.util';
import { TokenParams } from 'configmanager/src/main/ets/common/Constants';

export class TokenJsonUtils {
  /**
   * 构建Token JSON对象
   * @param params 参数对象
   * @returns JSON字符串
   */
  static buildTokenJson(params: TokenParams): string {
    const jsonObj: TokenParams = {
      appid: params.appid,
      channelid: params.channelid,
      userid: params.userid,
      nonce: params.nonce,
      timestamp: params.timestamp,
      token: params.token
    };

    return JSON.stringify(jsonObj);
  }

  /**
   * 将JSON字符串进行Base64编码
   * @param jsonString JSON字符串
   * @returns Base64编码字符串
   */
  static async encodeJsonToBase64(jsonString: string): Promise<string> {
    try {
      const encoder = new util.TextEncoder();
      const data = encoder.encodeInto(jsonString);

      const base64 = new util.Base64();
      const encoded = await base64.encodeToString(data);

      return encoded.replace(/\n/g, '').replace(/\r/g, '');
    } catch (error) {
      console.error('Base64编码失败:', error);
      return '';
    }
  }

  /**
   * 完整的Token生成流程
   * @param params Token参数
   * @returns Base64编码的Token JSON
   */
  static generateCompleteToken(params: TokenParams): Promise<string> {
    // 1. 构建JSON
    const jsonString = TokenJsonUtils.buildTokenJson(params);
    console.log('生成的JSON:', jsonString);

    // 2. Base64编码
    const base64String = TokenJsonUtils.encodeJsonToBase64(jsonString);
    console.log('Base64编码结果:', base64String);

    return base64String;
  }
}

3、导入ARTC SDK 相关类

导入 ARTC SDK 相关的类和接口:

import {
  AliRtcEngine,
  AliRtcVideoEncoderConfiguration,
  AliRtcEngineAuthInfo,
  AliRtcEngineEventListener,
  AliRtcChannelProfile,
  AliRtcClientRole,
  AliRtcAudioProfile,
  AliRtcAudioScenario,
  AliRtcVideoMirrorMode,
  AliRtcRotationMode,
  AliRtcVideoEncoderOrientationMode,
  AliRtcVideoTrack,
  AliRtcVideoCanvas,
  AliRtcRenderMode,
  AliRtcRenderMirrorMode,
  AliRtcXComponentController
} from '@aliyun_video_cloud/alivcsdk_artc';

4、创建并初始化引擎

  • 创建RTC引擎

调用getInstance接口创建引擎AliRTCEngine

private rtcEngine: AliRtcEngine | null | undefined;

this.rtcEngine = AliRtcEngine.getInstance('', this.context);
  • 初始化引擎

    • 调用setChannelProfile设置频道为AliRTCSdkInteractiveLive(互动模式)。

      根据具体的业务需求,可以选择适用于互动娱乐场景的互动模式,或者适合一对一或一对多广播的通信模式。正确的模式选择能够确保用户体验的流畅性并有效利用网络资源。您可以根据业务场景选择合适的模式。

      模式

      推流

      拉流

      模式介绍

      互动模式

      1. 有角色限制,只能被赋予主播身份的用户可进行推流操作。

      2. 在整个过程中,参与者可以灵活地切换角色。

      无角色限制所有参与者都拥有拉流的权限。

      1. 在互动模式中,主播加入或退出会议、以及开始推送直播流的事件都会实时通知给观众端,确保观众能够及时了解主播的动态。反之,观众的任何活动不会通告给主播,保持了主播的直播流程不受干扰。

      2. 在互动模式下,主播角色负责进行直播互动,而观众角色则主要接收内容,通常不参与直播的互动过程。若业务需求未来可能发生变化,导致不确定是否需要支持观众的互动参与,建议默认采用互动模式。这种模式具有较高的灵活性,可通过调整用户角色权限来适应不同的互动需求。

      通信模式

      无角色限制,所有参与者都拥有推流权限。

      无角色限制,所有参与者都拥有拉流的权限。

      1. 在通信模式下,会议参与者能够相互察觉到彼此的存在。

      2. 该模式虽然没有区分用户角色,但实际上与互动模式中的主播角色相对应;目的是为了简化操作,让用户能够通过调用更少的API来实现所需的功能。

    • 调用setClientRole设置用户角色为AliRTCSdkInteractive(主播)或者AliRTCSdkLive(观众)。

      说明

      主播角色默认推拉流,观众角色默认关闭预览和推流,只拉流。

      当用户在频道内切换角色时,系统会相应调整音视频流的推流状态:

      • 从主播切换为观众(“下麦”):系统将停止推送本地音视频流,但已订阅的远端流不受影响,用户仍可继续观看其他人的音视频。

      • 从观众切换为主播(“上麦”):系统将开始推送本地音视频流,同时已订阅的远端流保持不变,用户可以继续观看其他参与者的内容。

      // 设置频道模式为互动模式,RTC下都使用AliRTCSdkInteractiveLive
      this.rtcEngine.setChannelProfile(AliRtcChannelProfile.AliEngineInteractiveLive);
      // 设置用户角色,既需要推流也需要拉流使用AliRTCSdkInteractive, 只拉流不推流使用AliRTCSdkLive
      this.rtcEngine.setClientRole(AliRtcClientRole.AliEngineClientRoleInteractive);
  • 设置常用的回调

    SDK 在运行过程中如遇到异常情况,会优先尝试内部重试机制以自动恢复。对于无法自行解决的错误,SDK 会通过预定义的回调接口通知您的应用程序。

    以下是一些 SDK 无法处理、需由应用层监听和响应的关键回调:

    异常发生原因

    回调及参数

    解决方案

    说明

    鉴权失败

    onJoinChannel 回调中的result返回AliRtcErrJoinBadToken

    发生错误时App需要检查Token是否正确。

    在用户主动调用API时,若鉴权失败,系统将在调用API的回调中返回鉴权失败的错误信息。

    鉴权将要过期

    onWillAuthInfoExpire

    发生该异常时App需要重新获取最新的鉴权信息后,再调用refreshAuthInfo刷新鉴权信息。

    鉴权过期错误在两种情况下出现:用户调用API或程序执行期间。因此,错误反馈将通过API回调或通过独立的错误回调通知。

    鉴权过期

    onAuthInfoExpired

    发生该异常时App需要重新入会。

    鉴权过期错误在两种情况下出现:用户调用API或程序执行期间。因此,错误反馈将通过API回调或通过独立的错误回调通知。

    网络连接异常

    onConnectionStatusChange回调返回AliRtcConnectionStatusFailed。

    发生该异常时APP需要重新入会。

    SDK具备一定时间断网自动恢复能力,但若断线时间超出预设阈值,会触发超时并断开连接。此时,App应检查网络状态并指导用户重新加入会议。

    被踢下线

    onBye

    • AliRtcOnByeUserReplaced:当发生该异常时排查用户userid是否相同。

    • AliRtcOnByeBeKickedOut:当发生该异常时,表示被业务踢下线,需要重新入会。

    • AliRtcOnByeChannelTerminated:当发生该异常时,表示房间被销毁,需要重新入会。

    RTC服务提供了管理员可以主动移除参与者的功能。

    本地设备异常

    onLocalDeviceException

    发生该异常时App需要检测权限、设备硬件是否正常。

    RTC服务支持设备检测和异常诊断的能力;当本地设备发生异常时,RTC服务会通过回调的方式通知客户本地设备异常,此时,若SDK无法自行解决问题,则App需要介入以查看设备是否正常。

const listener = new AliRtcEngineEventListener()
listener.onJoinChannel((resultCode: number, channel: string, elapsed: string) => {
  console.info(`加入频道结果: result=${resultCode}, channel=${channel}, userId=${this.UserId}, elapsed=${elapsed}`);

  const resultText = resultCode === 0
    ? `User ${this.UserId} Join ${channel} Success`
    : `Join ${this.UserId} Join ${channel} Failed!,error: ${resultCode}`;

  prompt.showToast({
    message: resultText,
    duration: 2000
  });
})
  .onLeaveChannel((resultCode: number) => {
    console.info(`离开频道结果: result=${resultCode}`);
    prompt.showToast({
      message: 'Leave Channel',
      duration: 2000
    });
  })

// 设置回调
this.rtcEngine.setRtcEngineEventListener(listener);

5、设置音视频属性

  • 设置音频相关属性

调用setAudioProfile设置音频的编码模式和音频场景

this.rtcEngine.setAudioProfile(
  AliRtcAudioProfile.AliEngineHighQualityMode,
  AliRtcAudioScenario.AliEngineSceneDefaultMode
);
  • 设置视频相关属性

可以设置推出去的视频流的分辨率、码率、帧率等信息。

// 设置视频编码配置
const videoConfig: AliRtcVideoEncoderConfiguration = new AliRtcVideoEncoderConfiguration();
videoConfig.dimensions.width = 640;
videoConfig.dimensions.height = 480;
videoConfig.frameRate = 20;
videoConfig.bitrate = 1200;
videoConfig.keyFrameInterval = 2000;
videoConfig.orientationMode = AliRtcVideoEncoderOrientationMode.AliEngineVideoEncoderOrientationModeAdaptive;
videoConfig.min_bitrate = 0;
videoConfig.forceStrictKeyFrameInterval = 0;
videoConfig.mirrorMode = AliRtcVideoMirrorMode.AliEngineVideoMirrorModeDisabled;
videoConfig.rotationMode = AliRtcRotationMode.AliEngineRotationMode_0;
this.rtcEngine.setVideoEncoderConfiguration(videoConfig);

6、设置推拉流属性

设置推送音视频流及默认拉所有用户的流:

  • 调用publishLocalAudioStream推送音频流

  • 调用publishLocalVideoStream推送视频流,如果是语音通话,可以设置成false

// 发布本地音频流
this.rtcEngine.publishLocalAudioStream(true);

// 发布本地视频流
this.rtcEngine.publishLocalVideoStream(true);

// 设置默认订阅所有远端音视频流
this.rtcEngine.setDefaultSubscribeAllRemoteAudioStreams(true);
this.rtcEngine.setDefaultSubscribeAllRemoteVideoStreams(true);

// 明确订阅所有远端音视频流
this.rtcEngine.subscribeAllRemoteAudioStreams(true);
this.rtcEngine.subscribeAllRemoteVideoStreams(true);

说明

SDK默认是自动推拉流模式,默认会推送音视频流及订阅频道内所有用户的音视频流,可以通过调用上面的接口关闭自动推拉流模式。

7、开启本地预览

  • 调用setLocalViewConfig设置本地渲染视图,同时设置本地的视频显示属性。

  • 调用startPreview 方法,开启本地视频预览。

try {
  // 设置本地视图配置
  if (this.aliRtcVideoCanvas) {
    this.rtcEngine.setLocalViewConfig(
      this.aliRtcVideoCanvas,
      this.componentController,
      AliRtcVideoTrack.AliEngineVideoTrackCamera
    );
  }

  // 开始本地视频预览
  this.rtcEngine.startPreview();
  this.ShowPreview = true;

  console.info('本地预览已启动');

} catch (error) {
  console.error('启动预览失败:', error);
}

8、加入频道

调用joinChannel加入频道,如果token是单参数规则生成的,需要调用SDK单参数的joinChannelWithToken接口,如果是多参数规则生成的,需要调用SDK多参数的joinChannel接口。调用完加入频道后,可以在onJoinChannelResult回调中拿到加入频道结果,如果result0,则表示加入频道成功,否则需要检查传进来的Token是否非法。

this.rtcEngine.joinChannelWithToken(token, null, null, 'username');

说明

  • 入会后会按照入会前设定的参数执行相应的推流和拉流。

  • SDK默认会自动推拉流,以减少客户端需要调用的API数量。

9、设置远端视图

在初始化引擎的时候设置对应回调mAliRtcEngine.setRtcEngineNotify,需要在onRemoteTrackAvailableNotify回调中,为远端用户设置远端视图,示例代码如下:

// 获取XComponent的surfaceId
stream.surfaceId = stream.xcomponentController.getXComponentSurfaceId();

// 配置视频画布
if (!stream.canvas) {
  stream.canvas = new AliRtcVideoCanvas();
}

// 设置surfaceId
stream.canvas.surfaceId = stream.surfaceId;
stream.canvas.renderMode = AliRtcRenderMode.AliRtcRenderModeAuto;
stream.canvas.mirrorMode = AliRtcRenderMirrorMode.AliRtcRenderMirrorModeAllNoMirror;

// 设置远程视图配置
this.rtcEngine.setRemoteViewConfig(
  stream.canvas, 
  this.componentController, 
  stream.uid,
  AliRtcVideoTrack.AliRtcVideoTrackCamera
);

10、离开房间并销毁引擎

音视频互动结束,需要离开房间并销毁引擎,按照下列步骤结束音视频互动

  1. 调用 stopPreview 停止视频预览。

  2. 调用leaveChannel离会。

  3. 调用destroy销毁引擎,并释放相关资源。

private void destroyRtcEngine() {
  // 停止预览
  this.rtcEngine.stopPreview();
  // 离开频道
  this.rtcEngine.leaveChannel();
  // 销毁
  AliRtcEngine.destroyInstance();
  this.rtcEngine = null;
}

相关文档

数据结构

AliRtcEngine接口