移动端Harmony推流

本文介绍如何使用移动端Harmony SDK来支持实时记录场景下的音频识别流程。

前提条件

SDK关键接口

  • initialize:初始化SDK。

    /**
    * 初始化SDK,SDK可多实例,请先释放后再次进行初始化。请勿在UI线程调用,可能会引起阻塞。
    * @param callback:事件监听回调,参见下文具体回调。
    * @param parameters:json string形式的初始化参数,参见下方说明或接口说明:https://help.aliyun.com/document_detail/173298.html。
    * @param level:log打印级别,值越小打印越多。
    * @param save_log:是否保存log为文件,存储目录为ticket中的debug_path字段值。注意,log文件无上限,请注意持续存储导致磁盘存满。
    * @return:参见错误码:https://help.aliyun.com/document_detail/459864.html。
    */
    public initialize(callback:INativeNuiCallback ,
    			parameters:string ,
    			level:number ,
    			save_log:boolean=false ):number

    其中,parameters详细说明:

    参数

    类型

    是否必选

    说明

    workspace

    String

    工作目录路径,SDK从该路径读取配置文件。

    app_key

    String

    必须填“default”。

    token

    String

    必须填“default”。

    url

    String

    创建听悟实时记录任务时返回的会议MeetingJoinUrl作为音频流推送地址,在后续实时音频流识别时通过该地址进行推流。

    service_mode

    String

    必须填“1”,表示启用在线功能。

    device_id

    String

    设备标识,唯一表示一台设备(如Mac地址/SN/UniquePsuedoID等)。

    debug_path

    String

    debug目录。当初始化SDK时的save_log参数取值为true时,该目录用于保存日志文件。

    save_wav

    String

    当初始化SDK时的save_log参数取值为true时,该参数生效。表示是否保存音频debug,该数据保存在debug目录中,需要确保debug_path有效可写。

    注意,音频文件无上限,请注意持续存储导致磁盘存满。

    其中,INativeNuiCallback类型包含如下回调。

    • onNuiAudioStateChanged:根据音频状态进行录音功能的开关。

      /**
       * 当start/stop/cancel等接口调用时,SDK通过此回调通知App进行录音的开关操作。
       * @param state:录音需要的状态(打开/停止/关闭)
       */
      onNuiAudioStateChanged:(state:Constants.AudioState)=>void
    • onNuiNeedAudioData:在回调中提供音频数据。

      /**
       * 开始识别时,此回调被连续调用,App需要在回调中进行语音数据填充。
       * @param buffer:填充语音的存储区。
       * @return:实际填充的字节数。
       */
      
      onNuiNeedAudioData:(buffer:ArrayBuffer)=>number;
    • onNuiEventCallback:SDK事件回调。

      /**
       * SDK主要事件回调
       * @param event:回调事件,参见如下事件列表。
       * @param resultCode:参见错误码,在出现EVENT_ASR_ERROR事件时有效。
       * @param arg2:保留参数。
       * @param kwsResult:语音唤醒功能(暂不支持)。
       * @param asrResult:语音识别结果。
       */
      onNuiEventCallback:(event:Constants.NuiEvent, resultCode:number, arg2:number, kwsResult:KwsResult,
      							asrResult:AsrResult)=>void;

      事件列表:

      名称

      说明

      EVENT_VAD_START

      检测到人声起点。

      EVENT_VAD_END

      检测到人声尾点。

      EVENT_ASR_PARTIAL_RESULT

      语音识别中间结果。

      EVENT_ASR_ERROR

      根据错误码信息判断出错原因。

      EVENT_MIC_ERROR

      录音错误,表示SDK连续2秒未收到任何音频,可检查录音系统是否正常。

      EVENT_SENTENCE_START

      实时语音识别事件,表示检测到一句话开始。

      EVENT_SENTENCE_END

      实时语音识别事件,表示检测到一句话结束,返回一句完整的结果。

      EVENT_SENTENCE_SEMANTICS

      暂不使用。

      EVENT_RESULT_TRANSLATED

      翻译结果。

      EVENT_TRANSCRIBER_COMPLETE

      停止语音识别后最终事件

    • onNuiAudioRMSChanged:音频能量值回调。

      /**
       * 音频能量值回调
       * @param val: 音频数据能量值回调,范围-160至0,一般用于UI展示语音动效
       */
      onNuiAudioRMSChanged:(val:number)=>number;

  • setParams:以JSON格式设置SDK参数。

    /**
     * 以JSON格式设置参数
     * @param params:参见接口说明:https://help.aliyun.com/document_detail/173298.html。
     * @return:参见错误码:https://help.aliyun.com/document_detail/459864.html。
     */
    public setParams(params:string):number

    params详细说明:

    参数

    类型

    是否必选

    说明

    service_type

    Integer

    必须填“4”。此为需要请求的语音服务类型,听悟实时推流为“4”。

    nls_config

    JsonObject

    访问语音服务相关的参数配置,详见如下。

    nls_config.sr_format

    String

    必须填“pcm”。对应的《CreateTask - 创建听悟任务》中,创建听悟任务时也请指定音频流数据的编码格式为pcm。

    nls_config.sample_rate

    Integer

    音频采样率,默认值:16000Hz。对应的《CreateTask - 创建听悟任务》中,创建听悟任务时也请指定音频流数据的采样率,当前支持 8000 和 16000。

  • startDialog:开始识别。

    /**
     * 开始识别
     * @param vad_mode:多种模式,对于识别场景,请使用P2T。
     * @param dialog_params:json string形式的对话参数,参见接口说明:https://help.aliyun.com/document_detail/173298.html。
     * @return:参见错误码:https://help.aliyun.com/document_detail/459864.html。
     */
    public startDialog(vad_mode:Constants.VadMode, dialog_params:string):number
  • stopDialog:结束识别。

    /**
     * 结束识别,调用该接口后,服务端将返回最终识别结果并结束任务。
     * @return:参见错误码:https://help.aliyun.com/document_detail/459864.html。
     */
    public stopDialog():number
  • cancelDialog:立即结束识别。

    /**
     * 立即结束识别,调用该接口后,不等待服务端返回最终识别结果就立即结束任务。
     * @return:参见错误码:https://help.aliyun.com/document_detail/459864.html。
     */
    public cancelDialog():number
  • release:释放SDK。

    /**
     * 释放SDK资源
     * @return:参见错误码:https://help.aliyun.com/document_detail/459864.html。
     */
    public release():number
  • GetVersion:获得当前SDK版本信息。

    /**
     * 获得当前SDK版本信息
     * @return: 字符串形式的SDK版本信息
     */
    public GetVersion():string

调用步骤

重要

请下载后在听悟的样例初始化代码中将Appkey和Token置为default,url置为您创建听悟实时记录返回的会议MeetingJoinUrl。

  1. 初始化SDK、录音实例。

  2. 根据业务需求设置参数。

  3. 调用startDialog开始识别。

  4. 根据音频状态回调onNuiAudioStateChanged,打开录音机。

  5. 在onNuiNeedAudioData回调中提供录音数据。

  6. 在EVENT_SENTENCE_START事件回调中表示当前开始识别一个句子,在EVENT_ASR_PARTIAL_RESULT事件回调中获取识别中间结果,在EVENT_SENTENCE_END事件回调中获得这句话完整的识别结果和各相关信息,在EVENT_RESULT_TRANSLATED事件回调中获得翻译结果。

  7. 调用stopDialog结束识别。并从EVENT_TRANSCRIBER_COMPLETE事件回调确认已停止识别。

  8. 结束调用,使用release接口释放SDK资源。

代码示例

说明

您如果有多例需求,也可以直接new对象进行使用。也可采用GetInstance获得单例。

NUI SDK初始化

//定义类NativeNuiCallbackHandle 实现回调接口INativeNuiCallback
class NativeNuiCallbackHandle implements INativeNuiCallback{
  //内部实现INativeNuiCallback中的5个接口函数
  //此处省略
}

let context = getContext(this) as common.UIAbilityContext;
this.filesDir = context.filesDir;
this.resourceDir = context.resourceDir;

//这里获得资源路径, 由于资源文件存放在工程的resfiles目录下,所以使用沙箱路径下的resfiles目录
let asset_path:string = this.resourceDir+"/resources_cloud"
//由于用户无法直接操作设备目录,因此调试路径设置为APP所在的沙箱路径下的公共目录filesDir
let debug_path:string = this.filesDir

//初始化SDK,注意用户需要在genInitParams中填入相关ID信息才可以使用。
cbhandle:NativeNuiCallbackHandle = new NativeNuiCallbackHandle()
g_asrinstance:NativeNui = new NativeNui(Constants.ModeType.MODE_DIALOG, "asr")
let ret:number = this.g_asrinstance.initialize(this.cbhandle, this.genInitParams(asset_path,debug_path), Constants.LogLevel.LOG_LEVEL_VERBOSE, false);
console.info("result = " + ret);
if (ret == Constants.NuiResultCode.SUCCESS) {
  console.error(`call g_asrinstance.initialize() return success`);
} else {
  //抛出错误异常信息。
  console.error(`call g_asrinstance.initialize() return error:${ret}`);
}

其中,genInitParams生成为String JSON字符串,包含资源目录和用户信息。其中用户信息包含如下字段。

genInitParams(workpath:string, debugpath:string):string {
  let str:string = "";

  let object:Map<string, string|number|boolean|object> = new Map();

  //账号和项目创建
  //  ak_id ak_secret app_key如何获得,请查看https://help.aliyun.com/document_detail/72138.html
  object.set("app_key","default"); // 必填
  object.set("token", "default"); // 必填

  object.set("device_id", "meta60protestdevice"/*Utils.getDeviceId()*/); // 必填, 推荐填入具有唯一性的id, 方便定位问题
  // url中填入生成的MeetingJoinUrl。
  // 由于MeetingJoinUrl生成过程涉及ak/sk,移动端不可存储账号信息,故需要在服务端生成,并下发给移动端。
  // 详细请看: https://help.aliyun.com/zh/tingwu/api-tingwu-2023-09-30-createtask?spm=a2c4g.11186623.0.i32
  object.set("url", "wss://tingwu-realtime-cn-hangzhou-pre.aliyuncs.com/api/ws/v1?XXXX"); // 必填

  object.set("workspace", workpath); // 必填, 且需要有读写权限

  //当初始化SDK时的save_log参数取值为true时,该参数生效。表示是否保存音频debug,该数据保存在debug目录中,需要确保debug_path有效可写。
  //            object.put("save_wav", "true");
  //debug目录,当初始化SDK时的save_log参数取值为true时,该目录用于保存中间音频文件。
  object.set("debug_path", debugpath);

  object.set("service_mode", Constants.ModeFullCloud); // 必填
  str = MapToJson(object)

  console.info("configinfo genInitParams:" + str);
  return str;
}

参数设置

以JSON字符串形式进行设置。

import {MapToJson} from 'neonui'

//设置相关识别参数,具体参考API文档
//  initialize()之后startDialog之前调用
this.g_asrinstance.setParams(this.genParams());

genParams():string {
  let params:string = "";
  let nls_config:Map<string, string|number|boolean|object> = new Map();

  nls_config.set("sample_rate", 16000);
  nls_config.set("sr_format", "pcm");

  let parameters:Map<string, string|number|boolean|object> = new Map();
  parameters.set("nls_config", Object( JSON.parse(MapToJson(nls_config)) ) );
  parameters.set("service_type", Constants.kServiceTypeSpeechTranscriber); // 必填

  params = MapToJson(parameters);
  console.log("configinfo genParams" + params)
  return params;
}

开始识别

通过startDialog接口开启监听。

import {MapToJson} from 'neonui'

let ret:number = this.g_asrinstance.startDialog(Constants.VadMode.TYPE_P2T,
  this.genDialogParams());
console.info("start done . ret = ", ret);
if (ret != 0) {
  console.info("call startDialog return error. ", ret);
}
nui_instance.startDialog(Constants.VadMode.TYPE_P2T, genDialogParams());

genDialogParams():string {
  let params:string = "";
  let dialog_param:Map<string, string|number|boolean|object> = new Map();
  params = MapToJson(dialog_param);
  console.info("configinfo dialog params: " + params);
  return params;
}

推送录音数据

  • updateAudio:在AudioCapturer的on('readData',)注册的回调函数中,直接调用updateAudio接口把录音数据送入SDK内部。

    //g_asrinstance.updateAudio(buffer,false)
    /*AudioCapturer中注册的'readData'接口是AudioCapturer.readDataCallback
     *AudioCapturer.audioCapturer.on('readData', AudioCapturer.readDataCallback);
    */
    class AudioCapturer{
      static  readDataCallback = (buffer: ArrayBuffer) => {
        console.log(`${TAG} read data bytelength is ${buffer.byteLength}. uid[${process.uid}] pid[${process.pid}] tid[${process.tid}]`);
        AudioCapturer.g_asrinstance.updateAudio(buffer,false)
      }
    }

回调处理

  • onNuiAudioStateChanged:录音状态回调,SDK内部维护录音状态,根据该状态的回调进行录音机的开关操作。

    /*
    对于鸿蒙开发环境IDE版本5.0.3.403 以前的版本,AudioCapturer模块如果使用注册回调[on("readData",)]的方式读取录音数据,
    存在AudioCapturer.stop后直接start不会触发回调的情况。此时必须按照 (stop,realease)再(createAudioCapturer,start)的流程才能正常工作。
    升级为 IDE版本5.0.3.403版本后,以上问题已经解决。所以以下示例代码中注释掉了create/release相关接口的调用。
    */
    onNuiAudioStateChanged(state:Constants.AudioState):void {
        console.info(`womx onUsrNuiAudioStateChanged(${state})`)
        if (state === Constants.AudioState.STATE_OPEN){
          console.info(`womx onUsrNuiAudioStateChanged(${state}) audio recorder start`)
          //AudioCapturer.init(g_asrinstance)
          AudioCapturer.start()
          console.info(`womx onUsrNuiAudioStateChanged(${state}) audio recorder start done`)
        } else if (state === Constants.AudioState.STATE_CLOSE){
          console.info(`womx onUsrNuiAudioStateChanged(${state}) audio recorder close`)
          AudioCapturer.stop()
          //AudioCapturer.release()
          console.info(`womx onUsrNuiAudioStateChanged(${state}) audio recorder close done`)
        } else if (state === Constants.AudioState.STATE_PAUSE){
          console.info(`womx onUsrNuiAudioStateChanged(${state}) audio recorder pause`)
          AudioCapturer.stop()
          //AudioCapturer.release()
          console.info(`womx onUsrNuiAudioStateChanged(${state}) audio recorder pause done`)
        }
      }
  • onNuiNeedAudioData:录音数据回调,在该回调中填充录音数据。

    public int onNuiNeedAudioData(byte[] buffer, int len) {
      console.info(`warning,this callback should not be called in HarmonyOS Next`)
      return 0;
    }
  • onNuiEventCallback:NUI SDK事件回调,请勿在事件回调中调用SDK的接口,可能引起死锁。

    //以下几个变量,用于在event回调过程中,进行实时识别结果、历史识别结果、实时翻译结果、历史翻译结果的汇总相关工作。
    asrspeechrealtimeResultOld:string=""
    asrmessage:string="";
    message:string="";    //保存当前回调的事件信息
    asr_message_id:string="";
    asrspeechrealtimeResultTranslatedOld:string=""
    asrmessageTranslated:string="";
    
    onNuiEventCallback(event:Constants.NuiEvent, resultCode:number, arg2:number, kwsResult:KwsResult,
      asrResult:AsrResult):void {
      let asrinfo:string = ""
      console.log("onUsrNuiEventCallback last this callback handle.asrmessage is " + this.asrmessage)
      console.log("onUsrNuiEventCallback new this callback handle is " + JSON.stringify(this))
      console.log("onUsrNuiEventCallback event is " + event)
    
      if (event === Constants.NuiEvent.EVENT_TRANSCRIBER_COMPLETE){
        // 实时识别结束
        this.message = "EVENT_TRANSCRIBER_COMPLETE"
        this.asrspeechrealtimeResultOld=""
        this.asrspeechrealtimeResultTranslatedOld=""
      } else if (event === Constants.NuiEvent.EVENT_SENTENCE_START){
        this.message = "EVENT_SENTENCE_START"
      } else if (event === Constants.NuiEvent.EVENT_ASR_PARTIAL_RESULT || event === Constants.NuiEvent.EVENT_SENTENCE_END){
        if (event === Constants.NuiEvent.EVENT_ASR_PARTIAL_RESULT ) {
          // 例如展示当前句子的识别中间结果
          this.message = "EVENT_ASR_PARTIAL_RESULT"
        } else if(event === Constants.NuiEvent.EVENT_SENTENCE_END){
          // 例如展示当前句子的完整识别结果
          this.message = "EVENT_SENTENCE_END"
        }
      } else if (event === Constants.NuiEvent.EVENT_RESULT_TRANSLATED){
        // 例如展示当前句子的翻译结果
        this.message = "EVENT_RESULT_TRANSLATED"
      } else if (event === Constants.NuiEvent.EVENT_ASR_ERROR){
        // asrResult在EVENT_ASR_ERROR中为错误信息,搭配错误码resultCode和其中的task_id更易排查问题,请用户进行记录保存。
        this.message = "EVENT_ASR_ERROR"
      } else if (event === Constants.NuiEvent.EVENT_MIC_ERROR){
        // EVENT_MIC_ERROR表示2s未传入音频数据,请检查录音相关代码、权限或录音模块是否被其他应用占用。
        this.message = "EVENT_MIC_ERROR"
      } else if (event === Constants.NuiEvent.EVENT_DIALOG_EX){ /* unused */
        // 此事件可不用关注
        this.message = "EVENT_DIALOG_EX"
      }
      if (asrResult) {
        asrinfo = asrResult.asrResult
        console.log(`asrinfo asrResult string is ${asrinfo}. all response is [${asrResult.allResponse}]`)
        if (asrinfo) {
          try {
            let asrresult_json:object|null = JSON.parse(asrinfo)
            //console.log(JSON.stringify(asrresult_json))
            if (asrresult_json) {
              let payload:object|null = asrresult_json["payload"];
              if (payload) {
                //console.log(JSON.stringify(payload))
    
                if (event === Constants.NuiEvent.EVENT_RESULT_TRANSLATED) {
                  //出现翻译结果事件时,进行翻译结果的实时更新。
                  
                  if (payload["translate_result"]) {
                    //实时更新当前的翻译结果。 
                    console.log(`translate_result is [${payload["translate_result"][0]["text"]}], with oldinfo [${this.asrspeechrealtimeResultTranslatedOld}]`)
                    
                    //this.asrmessageTranslated中保存的是start以后所有句子的翻译结果。包括历史句子的翻译汇总结果 + 当前句子的实时翻译结果。
                    this.asrmessageTranslated = this.asrspeechrealtimeResultTranslatedOld + payload["translate_result"][0]["text"];
                    
                  }
                  if (this.asr_message_id == asrresult_json["header"]["source_message_id"]){
                    //当翻译结果对应的source_message_id 是 EVENT_SENTENCE_END对应的message_id时,说明EVENT_SENTENCE_END对应语句的翻译结束。此时可以更新历史句子的总翻译结果。
                    this.asrspeechrealtimeResultTranslatedOld = this.asrmessageTranslated
                  }
                } else {
                  //出现识别结果事件时,进行识别结果的实时更新。
                  if (payload["result"]) {
                    //this.asrmessage中保存的是start以后所有句子的识别结果。包括历史句子的识别汇总结果 + 当前句子的实时识别结果。
                    this.asrmessage = this.asrspeechrealtimeResultOld + payload["result"];
                  }
                }
    
                if(event === Constants.NuiEvent.EVENT_SENTENCE_END){
                  //出现EVENT_SENTENCE_END事件事,进行历史识别结果的汇总、暂存最后一句识别结果的message_id作为该句识别结果的翻译结果过滤依据
                  
                  //进行历史识别结果的汇总
                  this.asrspeechrealtimeResultOld = this.asrmessage
    
                  //暂存最后一句识别结果的message_id作为该句识别结果的翻译结果过滤依据
                  this.asr_message_id = asrresult_json["header"]["message_id"]
                }
              }
            }
          } catch (e){
            console.error("got asrinfo asrResult not json, so donot fresh asrinfo." + JSON.stringify(e))
          }
        }
      }
      console.info(`womx onUsrNuiEventCallback(${event}, ${resultCode},${arg2}, kwsResult:${kwsResult},asrResult:"${this.asrmessage}") done`)
    }

结束识别

nui_instance.stopDialog();

释放SDK

nui_instance.release();