1# 开发音频通话功能 2<!--Kit: Audio Kit--> 3<!--Subsystem: Multimedia--> 4<!--Owner: @songshenke--> 5<!--Designer: @caixuejiang; @hao-liangfei; @zhanganxiang--> 6<!--Tester: @Filger--> 7<!--Adviser: @zengyawen--> 8 9在音频通话场景下,音频输出(播放对端声音)和音频输入(录制本端声音)会同时进行,应用可以通过使用AudioRenderer来实现音频输出,通过使用AudioCapturer来实现音频输入,同时使用AudioRenderer和AudioCapturer即可实现音频通话功能。 10 11在音频通话开始和结束时,应用可以自行检查当前的[音频场景模式](audio-call-overview.md#音频场景模式)和[铃声模式](audio-call-overview.md#铃声模式),以便采取合适的音频管理及提示策略。 12 13以下代码示范了同时使用AudioRenderer和AudioCapturer实现音频通话功能的基本过程,其中未包含音频通话数据的传输过程,实际开发中,需要将网络传输来的对端通话数据解码播放,此处仅以读取音频文件的数据代替;同时需要将本端录制的通话数据编码打包,通过网络发送给对端,此处仅以将数据写入音频文件代替。 14 15## 使用AudioRenderer播放对端的通话声音 16 17 该过程与[使用AudioRenderer开发音频播放功能](using-audiorenderer-for-playback.md)过程相似,关键区别在于audioRendererInfo参数和音频数据来源。audioRendererInfo参数中,音频流使用类型usage需设置为VoIP通话:STREAM_USAGE_VOICE_COMMUNICATION。 18 19```ts 20import { audio } from '@kit.AudioKit'; 21import { BusinessError } from '@kit.BasicServicesKit'; 22import { fileIo as fs } from '@kit.CoreFileKit'; 23import { common } from '@kit.AbilityKit'; 24 25// 与使用AudioRenderer开发音频播放功能过程相似,关键区别在于audioRendererInfo参数和音频数据来源。 26const TAG = 'VoIPDemoForAudioRenderer'; 27 28class Options { 29 offset?: number; 30 length?: number; 31} 32 33let bufferSize: number = 0; 34let audioRenderer: audio.AudioRenderer | undefined = undefined; 35let audioStreamInfo: audio.AudioStreamInfo = { 36 samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率。 37 channels: audio.AudioChannel.CHANNEL_2, // 通道。 38 sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式。 39 encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式。 40}; 41let audioRendererInfo: audio.AudioRendererInfo = { 42 // 需使用通话场景相应的参数。 43 usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION, // 音频流使用类型:VoIP通话。 44 rendererFlags: 0 // 音频渲染器标志:默认为0即可。 45}; 46let audioRendererOptions: audio.AudioRendererOptions = { 47 streamInfo: audioStreamInfo, 48 rendererInfo: audioRendererInfo 49}; 50let file: fs.File; 51let writeDataCallback: audio.AudioRendererWriteDataCallback; 52 53async function initArguments(context: common.UIAbilityContext) { 54 let path = context.cacheDir; 55 // 确保该沙箱路径下存在该资源。 56 let filePath = path + '/StarWars10s-2C-48000-4SW.pcm'; 57 file = fs.openSync(filePath, fs.OpenMode.READ_ONLY); 58 writeDataCallback = (buffer: ArrayBuffer) => { 59 let options: Options = { 60 offset: bufferSize, 61 length: buffer.byteLength 62 }; 63 64 try { 65 let bufferLength = fs.readSync(file.fd, buffer, options); 66 bufferSize += buffer.byteLength; 67 // 如果当前回调传入的数据不足一帧,空白区域需要使用静音数据填充,否则会导致播放出现杂音。 68 if (bufferLength < buffer.byteLength) { 69 let view = new DataView(buffer); 70 for (let i = bufferLength; i < buffer.byteLength; i++) { 71 // 空白区域填充静音数据。当使用音频采样格式为SAMPLE_FORMAT_U8时0x7F为静音数据,使用其他采样格式时0为静音数据。 72 view.setUint8(i, 0); 73 } 74 } 75 // API version 11不支持返回回调结果,从API version 12开始支持返回回调结果。 76 // 如果开发者不希望播放某段buffer,返回audio.AudioDataCallbackResult.INVALID即可。 77 return audio.AudioDataCallbackResult.VALID; 78 } catch (error) { 79 console.error('Error reading file:', error); 80 // API version 11不支持返回回调结果,从API version 12开始支持返回回调结果。 81 return audio.AudioDataCallbackResult.INVALID; 82 } 83 }; 84} 85 86// 初始化,创建实例,设置监听事件。 87async function init() { 88 audio.createAudioRenderer(audioRendererOptions, (err, renderer) => { // 创建AudioRenderer实例。 89 if (!err) { 90 console.info(`${TAG}: creating AudioRenderer success`); 91 audioRenderer = renderer; 92 if (audioRenderer !== undefined) { 93 audioRenderer.on('writeData', writeDataCallback); 94 } 95 } else { 96 console.info(`${TAG}: creating AudioRenderer failed, error: ${err.message}`); 97 } 98 }); 99} 100 101// 开始一次音频渲染。 102async function start() { 103 if (audioRenderer !== undefined) { 104 let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED]; 105 if (stateGroup.indexOf(audioRenderer.state.valueOf()) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染。 106 console.error(TAG + 'start failed'); 107 return; 108 } 109 // 启动渲染。 110 audioRenderer.start((err: BusinessError) => { 111 if (err) { 112 console.error('Renderer start failed.'); 113 } else { 114 console.info('Renderer start success.'); 115 } 116 }); 117 } 118} 119 120// 暂停渲染。 121async function pause() { 122 if (audioRenderer !== undefined) { 123 // 只有渲染器状态为running的时候才能暂停。 124 if (audioRenderer.state.valueOf() !== audio.AudioState.STATE_RUNNING) { 125 console.info('Renderer is not running'); 126 return; 127 } 128 // 暂停渲染。 129 audioRenderer.pause((err: BusinessError) => { 130 if (err) { 131 console.error('Renderer pause failed.'); 132 } else { 133 console.info('Renderer pause success.'); 134 } 135 }); 136 } 137} 138 139// 停止渲染。 140async function stop() { 141 if (audioRenderer !== undefined) { 142 // 只有渲染器状态为running或paused的时候才可以停止。 143 if (audioRenderer.state.valueOf() !== audio.AudioState.STATE_RUNNING && audioRenderer.state.valueOf() !== audio.AudioState.STATE_PAUSED) { 144 console.info('Renderer is not running or paused.'); 145 return; 146 } 147 // 停止渲染。 148 audioRenderer.stop((err: BusinessError) => { 149 if (err) { 150 console.error('Renderer stop failed.'); 151 } else { 152 fs.close(file); 153 console.info('Renderer stop success.'); 154 } 155 }); 156 } 157} 158 159// 销毁实例,释放资源。 160async function release() { 161 if (audioRenderer !== undefined) { 162 // 渲染器状态不是released状态,才能release。 163 if (audioRenderer.state.valueOf() === audio.AudioState.STATE_RELEASED) { 164 console.info('Renderer already released'); 165 return; 166 } 167 // 释放资源。 168 audioRenderer.release((err: BusinessError) => { 169 if (err) { 170 console.error('Renderer release failed.'); 171 } else { 172 console.info('Renderer release success.'); 173 } 174 }); 175 } 176} 177 178@Entry 179@Component 180struct Index { 181 build() { 182 Scroll() { 183 Column() { 184 Row() { 185 Column() { 186 Text('初始化').fontColor(Color.Black).fontSize(16).margin({ top: 12 }); 187 } 188 .backgroundColor(Color.White) 189 .borderRadius(30) 190 .width('45%') 191 .height('25%') 192 .margin({ right: 12, bottom: 12 }) 193 .onClick(async () => { 194 let context = this.getUIContext().getHostContext() as common.UIAbilityContext; 195 initArguments(context); 196 init(); 197 }); 198 199 Column() { 200 Text('开始播放').fontColor(Color.Black).fontSize(16).margin({ top: 12 }); 201 } 202 .backgroundColor(Color.White) 203 .borderRadius(30) 204 .width('45%') 205 .height('25%') 206 .margin({ bottom: 12 }) 207 .onClick(async () => { 208 start(); 209 }); 210 } 211 212 Row() { 213 Column() { 214 Text('暂停播放').fontSize(16).margin({ top: 12 }); 215 } 216 .id('audio_effect_manager_card') 217 .backgroundColor(Color.White) 218 .borderRadius(30) 219 .width('45%') 220 .height('25%') 221 .margin({ right: 12, bottom: 12 }) 222 .onClick(async () => { 223 pause(); 224 }); 225 226 Column() { 227 Text('停止播放').fontColor(Color.Black).fontSize(16).margin({ top: 12 }); 228 } 229 .backgroundColor(Color.White) 230 .borderRadius(30) 231 .width('45%') 232 .height('25%') 233 .margin({ bottom: 12 }) 234 .onClick(async () => { 235 stop(); 236 }); 237 } 238 239 Row() { 240 Column() { 241 Text('释放资源').fontColor(Color.Black).fontSize(16).margin({ top: 12 }); 242 } 243 .id('audio_volume_card') 244 .backgroundColor(Color.White) 245 .borderRadius(30) 246 .width('45%') 247 .height('25%') 248 .margin({ right: 12, bottom: 12 }) 249 .onClick(async () => { 250 release(); 251 }); 252 } 253 .padding(12) 254 } 255 .height('100%') 256 .width('100%') 257 .backgroundColor('#F1F3F5'); 258 } 259 } 260} 261``` 262 263## 使用AudioCapturer录制本端的通话声音 264 265 该过程与[使用AudioCapturer开发音频录制功能](using-audiocapturer-for-recording.md)过程相似,关键区别在于audioCapturerInfo参数和音频数据流向。audioCapturerInfo参数中音源类型source需设置为语音通话:SOURCE_TYPE_VOICE_COMMUNICATION。 266 267 所有录制均需要申请麦克风权限:ohos.permission.MICROPHONE,申请方式请参考[向用户申请授权](../../security/AccessToken/request-user-authorization.md)。 268 269```ts 270import { audio } from '@kit.AudioKit'; 271import { BusinessError } from '@kit.BasicServicesKit'; 272import { fileIo as fs } from '@kit.CoreFileKit'; 273import { common } from '@kit.AbilityKit'; 274 275// 与使用AudioCapturer开发音频录制功能过程相似,关键区别在于audioCapturerInfo参数和音频数据流向。 276const TAG = 'VoIPDemoForAudioCapturer'; 277 278class Options { 279 offset?: number; 280 length?: number; 281} 282 283let bufferSize: number = 0; 284let audioCapturer: audio.AudioCapturer | undefined = undefined; 285let audioStreamInfo: audio.AudioStreamInfo = { 286 samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率。 287 channels: audio.AudioChannel.CHANNEL_2, // 通道。 288 sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式。 289 encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式。 290}; 291let audioCapturerInfo: audio.AudioCapturerInfo = { 292 // 需使用通话场景相应的参数。 293 source: audio.SourceType.SOURCE_TYPE_VOICE_COMMUNICATION, // 音源类型:语音通话。 294 capturerFlags: 0 // 音频采集器标志:默认为0即可。 295}; 296let audioCapturerOptions: audio.AudioCapturerOptions = { 297 streamInfo: audioStreamInfo, 298 capturerInfo: audioCapturerInfo 299}; 300let file: fs.File; 301let readDataCallback: Callback<ArrayBuffer>; 302 303async function initArguments(context: common.UIAbilityContext) { 304 let path = context.cacheDir; 305 // 确保该沙箱路径下存在该资源。 306 let filePath = path + '/StarWars10s-2C-48000-4SW.pcm'; 307 file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); 308 readDataCallback = (buffer: ArrayBuffer) => { 309 let options: Options = { 310 offset: bufferSize, 311 length: buffer.byteLength 312 } 313 fs.writeSync(file.fd, buffer, options); 314 bufferSize += buffer.byteLength; 315 }; 316} 317 318// 初始化,创建实例,设置监听事件。 319async function init() { 320 audio.createAudioCapturer(audioCapturerOptions, (err, capturer) => { // 创建AudioCapturer实例。 321 if (err) { 322 console.error(`Invoke createAudioCapturer failed, code is ${err.code}, message is ${err.message}`); 323 return; 324 } 325 console.info(`${TAG}: create AudioCapturer success`); 326 audioCapturer = capturer; 327 if (audioCapturer !== undefined) { 328 audioCapturer.on('readData', readDataCallback); 329 } 330 }); 331} 332 333// 开始一次音频采集。 334async function start() { 335 if (audioCapturer !== undefined) { 336 let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED]; 337 if (stateGroup.indexOf(audioCapturer.state.valueOf()) === -1) { // 当且仅当状态为STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一时才能启动采集。 338 console.error(`${TAG}: start failed`); 339 return; 340 } 341 342 // 启动采集。 343 audioCapturer.start((err: BusinessError) => { 344 if (err) { 345 console.error('Capturer start failed.'); 346 } else { 347 console.info('Capturer start success.'); 348 } 349 }); 350 } 351} 352 353// 停止采集。 354async function stop() { 355 if (audioCapturer !== undefined) { 356 // 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止。 357 if (audioCapturer.state.valueOf() !== audio.AudioState.STATE_RUNNING && audioCapturer.state.valueOf() !== audio.AudioState.STATE_PAUSED) { 358 console.info('Capturer is not running or paused'); 359 return; 360 } 361 362 // 停止采集。 363 audioCapturer.stop((err: BusinessError) => { 364 if (err) { 365 console.error('Capturer stop failed.'); 366 } else { 367 fs.close(file); 368 console.info('Capturer stop success.'); 369 } 370 }); 371 } 372} 373 374// 销毁实例,释放资源。 375async function release() { 376 if (audioCapturer !== undefined) { 377 // 采集器状态不是STATE_RELEASED或STATE_NEW状态,才能release。 378 if (audioCapturer.state.valueOf() === audio.AudioState.STATE_RELEASED || audioCapturer.state.valueOf() === audio.AudioState.STATE_NEW) { 379 console.info('Capturer already released'); 380 return; 381 } 382 383 // 释放资源。 384 audioCapturer.release((err: BusinessError) => { 385 if (err) { 386 console.error('Capturer release failed.'); 387 } else { 388 console.info('Capturer release success.'); 389 } 390 }); 391 } 392} 393 394@Entry 395@Component 396struct Index { 397 build() { 398 Scroll() { 399 Column() { 400 Row() { 401 Column() { 402 Text('初始化').fontColor(Color.Black).fontSize(16).margin({ top: 12 }); 403 } 404 .backgroundColor(Color.White) 405 .borderRadius(30) 406 .width('45%') 407 .height('25%') 408 .margin({ right: 12, bottom: 12 }) 409 .onClick(async () => { 410 let context = this.getUIContext().getHostContext() as common.UIAbilityContext; 411 initArguments(context); 412 init(); 413 }); 414 415 Column() { 416 Text('开始录制').fontColor(Color.Black).fontSize(16).margin({ top: 12 }); 417 } 418 .backgroundColor(Color.White) 419 .borderRadius(30) 420 .width('45%') 421 .height('25%') 422 .margin({ bottom: 12 }) 423 .onClick(async () => { 424 start(); 425 }); 426 } 427 428 Row() { 429 Column() { 430 Text('停止录制').fontSize(16).margin({ top: 12 }); 431 } 432 .id('audio_effect_manager_card') 433 .backgroundColor(Color.White) 434 .borderRadius(30) 435 .width('45%') 436 .height('25%') 437 .margin({ right: 12, bottom: 12 }) 438 .onClick(async () => { 439 stop(); 440 }); 441 442 Column() { 443 Text('释放资源').fontColor(Color.Black).fontSize(16).margin({ top: 12 }); 444 } 445 .backgroundColor(Color.White) 446 .borderRadius(30) 447 .width('45%') 448 .height('25%') 449 .margin({ bottom: 12 }) 450 .onClick(async () => { 451 release(); 452 }); 453 } 454 .padding(12) 455 } 456 .height('100%') 457 .width('100%') 458 .backgroundColor('#F1F3F5'); 459 } 460 } 461} 462``` 463