鸿蒙5:HarmonyOS应用开发-面试录音(上)

72 阅读4分钟

1. 面试录音

编辑

1.1. 应用权限

1.1.1. 应用权限概述

系统提供了一种允许应用访问系统资源(如:通讯录等)和系统能力(如:访问摄像头、麦克风等)的通用权限访问方式,来保护系统数据(包括用户个人数据)或功能,避免它们被不当或恶意使用。

应用申请敏感权限时,必须填写权限使用理由字段,敏感权限通常是指与用户隐私密切相关的权限,包括地理位置、相机、麦克风、日历、健身运动、身体传感器、音乐、文件、图片视频等权限。参考向用户申请授权。

  • system_grant

在配置文件中,声明应用需要请求的权限后,系统会在安装应用时自动为其进行权限预授予,开发者不需要做其他操作即可使用权限。

  • user_grant
    • 在配置文件中,声明应用需要请求的权限,且要设置需要使用的场景+使用原因
    • 调用 requestPermissionsFromUser() 方法后,应用程序将等待用户授权的结果。如果用户授权,则可以继续访问目标操作。如果用户拒绝授权,则需要提示用户必须授权才能访问当前页面的功能,并引导用户到系统应用“设置”中打开相应的权限。可参考二次向用户申请权限 requestPermissionOnSetting()

对所有应用开放权限列表

录音授权演示:

module.json5

"requestPermissions": [
  {
    "name": 'ohos.permission.INTERNET'
  },
  {
    "name": "ohos.permission.MICROPHONE",
    "reason": "$string:permission_microphone",
    "usedScene": {
      "abilities": ["EntryAbility"]
    }
  }
],

原因格式:用于xxx模块xxx功能

{
  "string": [
    ...
    {
      "name": "permission_microphone",
      "value": "录音功能申请麦克风权限"
    }
  ]
}

页面测试:

import { abilityAccessCtrl, Permissions } from "@kit.AbilityKit"
import { HcNavBar } from "../../commons/components/HcNavBar"

@Component
  struct AudioPage {
    aboutToAppear(): void {
      this.requestPermission()
    }

    async requestPermission() {
      // 1. 用户授权
      const permissionList: Permissions[] = ['ohos.permission.MICROPHONE']
      const atManager = abilityAccessCtrl.createAtManager()
      const ctx = getContext(this)
      const result = await atManager.requestPermissionsFromUser(ctx, permissionList)
      const flag = result.authResults.every(item => item === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
      // 2. 二次授权
      if (!flag) {
        const result2 = await atManager.requestPermissionOnSetting(ctx, permissionList)
        const flag2 = result2.every(item => item === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
        console.log(flag2.toString())
      }
    }

    build() {
      //必须用NavDestination包裹
      NavDestination() {
        HcNavBar({ title: '录音功能' })
      }
      .hideTitleBar(true)
    }
  }

// 跳转页面入口函数
@Builder
  export function AudioBuilder() {
    AudioPage()
  }

配置路由

{
  "routerMap": [
    ...
    {
      "name": "AudioPage",
      "pageSourceFile": "src/main/ets/pages/Audio/AudioPage.ets",
      "buildFunction": "AudioBuilder"
    }
  ]
}

跳转页面

this.toolsBuilder({
  icon: $r('app.media.ic_mine_invite'),
  name: '面试录音',
  onClick: () => {
    auth.checkAuth({
      name: 'AudioPage'
    })
  }
})

1.1.2. permission 工具

目标:封装权限工具,提供请求用户权限,拉起用户权限设置的能力

import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
import { AppStorageV2 } from '@kit.ArkUI';
import { SavedContext } from '../../models';

class Permission {
  // 请求用户授权
  async requestPermissions(permissions: Permissions[]) {
    const atManager = abilityAccessCtrl.createAtManager()
    const ctx = AppStorageV2.connect(SavedContext)!.context
    if (ctx) {
      const result = await atManager.requestPermissionsFromUser(ctx, permissions)
      return result.authResults.every(result => result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
    }
    return false
  }

  // 打开权限设置 beta3
  async openPermissionSetting(permissions: Permissions[]) {
    const atManager = abilityAccessCtrl.createAtManager()
    const ctx = AppStorageV2.connect(SavedContext)!.context
    if (ctx) {
      const authResults = await atManager.requestPermissionOnSetting(ctx, permissions)
      return authResults.every(result => result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
    }
    return false
  }
}

export const permission = new Permission()

1.1.3. 录音授权

目标: 使用权限请求工具,在录音页面实现请求权限,无权限不可进入

编辑

编辑

编辑

import { HcNavBar } from "../../commons/components/HcNavBar"
import { promptAction } from "@kit.ArkUI"
import { navPathStack } from "../Index"
import { permission } from "../../commons/utils/Permission"
import { Permissions } from "@kit.AbilityKit"

@ComponentV2
struct AudioPage {
  permissions: Permissions[] = ['ohos.permission.MICROPHONE']
  confirmConfig: promptAction.ShowDialogOptions = {
    title: "温馨提示",
    message: "未授权使用麦克风将无法使用该面试录音功能,是否前往设置进行授权?",
    buttons: [
      { text: '离开', color: $r('app.color.common_gray_01') },
      { text: '去授权', color: $r('app.color.black') }
    ]
  }

  aboutToAppear(): void {
    this.requestPermission()
  }

  async requestPermission() {
    try {
      // 第一请求授权
      const isOk = await permission.requestPermissions(this.permissions)
      if (isOk) {
        return
      }
      // 弹窗提示
      const confirm = await promptAction.showDialog(this.confirmConfig)
      if (confirm.index === 1) {
        const isOk2 = await permission.openPermissionSetting(this.permissions)
        if (isOk2) {
          return
        }
      }
      navPathStack.pop()
    } catch (e) {
      promptAction.showToast({ message: '未授权' })
      navPathStack.pop()
    }
  }

  build() {
    //必须用NavDestination包裹
    NavDestination() {
      HcNavBar({ title: '录音功能' })
    }
    .hideTitleBar(true)
  }
}

// 跳转页面入口函数
@Builder
export function AudioBuilder() {
  AudioPage()
}

1.2. 录音知识

1.2.1. 使用 AvRecorder 录音

目标:使用 AvRecorder 实现音频录制存储到应用沙箱

编辑

实现步骤:

  • 需要一个文件接收音频数据
  • 准备录音配置
  • 使用 AvRecorder 实现开始录音,结束录音

落地代码:

avRecorder?: media.AVRecorder // 音视频录制管理类
fd?: number  // 资源句柄(fd)
filePath?: string // 文件路径

async startRecord() {
  // 1. 准备一个文件接收录音
  const ctx = getContext(this)
  const filePath = ctx.filesDir + '/' + Date.now() + '.m4a'
  this.filePath = filePath
  const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
  this.fd = file.fd
  // 2. 准备路由配置对象
  const config: media.AVRecorderConfig = {
    audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
    profile: {
      audioBitrate: 100000, // 音频比特率
      audioChannels: 1, // 音频声道数
      audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac
      audioSampleRate: 48000, // 音频采样率
      fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
    },
    url: `fd://${file.fd}`
  }
  // 3. 开始录制
  const avRecorder = await media.createAVRecorder()
  await avRecorder.prepare(config)
  await avRecorder.start()
  this.avRecorder = avRecorder
}

async stopRecord() {
  if (this.avRecorder) {
    await this.avRecorder.stop()
    await this.avRecorder.release()
    fileIo.closeSync(this.fd)
  }
}

//必须用NavDestination包裹
NavDestination() {
  HcNavBar({ title: '录音功能' })
  Button('开始录音')
    .onClick(() => {
      this.startRecord()
    })
  Button('结束录音')
    .onClick(() => {
      this.stopRecord()
    })
}
.hideTitleBar(true)

1.2.2. 录音声音振动效果

目标:根据声音的大小实现声音振动特效

编辑

实现步骤:

  • 通过 getAudioCapturerMaxAmplitude 观察音频区间
  • 封装振动组件,通过声音振幅数据实现振动效果

落地代码:

1)获取振幅数据,出入振动组件 AudioPage.ets

timer?: number
@Local maxAmplitude: number = 0

// 每100ms获取一下声音振幅
this.timer = setInterval(async () => {
  this.maxAmplitude = await avRecorder.getAudioCapturerMaxAmplitude()
  logger.debug('startRecord', this.maxAmplitude.toString())
}, 100)

async stopRecord() {
  if (this.avRecorder) {
    await this.avRecorder.stop()
    await this.avRecorder.release()
    fileIo.closeSync(this.fd)
    // stopRecord 清理定时器
    clearInterval(this.timer)
  }
}

AudioBoComp({ maxAmplitude: this.maxAmplitude })

2)实现振动组件 Audio/components/AudioBoComp.ets

@ComponentV2
export struct AudioBoComp {
  @Local per: number = 0
  @Param maxAmplitude: number = 0
  @Monitor('maxAmplitude')
  onChange() {
    animateTo({ duration: 100 }, () => {
      if (this.maxAmplitude < 500) {
        this.per = 0
      } else if (this.maxAmplitude > 30000) {
        this.per = 1
      } else {
        this.per = this.maxAmplitude / 30000
      }
    })
  }

  build() {
    Row({ space: 5 }) {
      ForEach(Array.from({ length: 30 }), () => {
        Column()
          .layoutWeight(1)
          .height(this.per * 100 * Math.random())
          .backgroundColor($r('app.color.common_blue'))
      })
    }
    .width('100%')
    .height(100)
  }
}

1.2.3. 使用 AvPlayer 播放

目标:能够使用 AvPlayer 播放应用沙箱中的音频文件,且显示进度条

落地代码:

avPlayer?: media.AVPlayer
@Local total: number = 0
@Local value: number = 0

async startPlay() {
  try {
    const file = fileIo.openSync(this.filePath, fileIo.OpenMode.READ_ONLY)
    const avPlayer = await media.createAVPlayer()
    avPlayer.on('stateChange', state => {
      if (state === 'initialized') {
        avPlayer.prepare()
      } else if ( state === 'prepared') {
        avPlayer.loop = true
        this.total = avPlayer.duration
        avPlayer.play()
      }
    })
    // 当前播放时间改变
    avPlayer.on('timeUpdate', (time) => {
      this.value = time
    })
    avPlayer.url = `fd://${file.fd}`
    this.avPlayer = avPlayer
  } catch (e) {
    logger.error('startPlay', JSON.stringify(e))
  }
}

stopPlay() {
  if (this.avPlayer) {
    this.avPlayer.stop()
    this.avPlayer.release()
  }
}

Button('开始播放')
  .onClick(() => {
    this.startPlay()
  })
Button('停止播放')
  .onClick(() => {
    this.stopPlay()
  })
Progress({ total: this.total, value: this.value })
  .width('100%')

HarmonyOS赋能资源丰富度建设(第四期)-吴东林

developer.huawei.com/consumer/cn…