1/* 2* Copyright (C) 2025 Huawei Device Co., Ltd. 3* Licensed under the Apache License, Version 2.0 (the "License"); 4* you may not use this file except in compliance with the License. 5* You may obtain a copy of the License at 6* 7* http://www.apache.org/licenses/LICENSE-2.0 8* 9* Unless required by applicable law or agreed to in writing, software 10* distributed under the License is distributed on an "AS IS" BASIS, 11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12* See the License for the specific language governing permissions and 13* limitations under the License. 14*/ 15import audio from '@ohos.multimedia.audio'; 16import fs from '@ohos.file.fs'; 17import router from '@ohos.router'; 18import { BusinessError } from '@ohos.base'; 19import Logger from '../../../ohosTest/ets/utils/Logger'; 20 21const MIN_RECORD_SECOND = 5; 22const TOTAL_SECOND = 30; 23const RANDOM_NUM = 10000; 24const INTERVAL_TIME = 1000; 25const READ_TIME_OUT = 0; 26class Options { 27 offset: number = 0; 28 length: number = 0; 29} 30 31@Entry 32@Component 33struct LiveCapturer { 34 @State fontColor: string = '#182431'; 35 @State selectedFontColor: string = '#007DFF'; 36 @State currentIndex: number = 2; 37 private audioCapturer?: audio.AudioCapturer; 38 private audioRenderer?: audio.AudioRenderer; 39 @State recordState: string = 'init'; // [init,started,continued,paused,stoped]; 40 private audioCapturerOptions: audio.AudioCapturerOptions = { 41 streamInfo: { 42 samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, 43 channels: audio.AudioChannel.CHANNEL_2, 44 sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, 45 encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW 46 }, 47 capturerInfo: { 48 source: audio.SourceType.SOURCE_TYPE_LIVE, 49 capturerFlags: 0 50 } 51 }; 52 private audioRendererOptions: audio.AudioRendererOptions = { 53 streamInfo: { 54 samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, 55 channels: audio.AudioChannel.CHANNEL_2, 56 sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, 57 encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW 58 }, 59 rendererInfo: { 60 content: audio.ContentType.CONTENT_TYPE_MUSIC, 61 usage: audio.StreamUsage.STREAM_USAGE_MEDIA, 62 rendererFlags: 0 63 } 64 }; 65 @State title: string = ''; 66 @State date: string = ''; 67 @State playSec: number = 0; 68 @State renderState: number = 0; 69 @State recordSec: number = 0; 70 @State showTime: string = '00:00:00'; 71 private interval: number = 0; 72 private bufferSize = 0; 73 private path = ``; 74 private fd = 0; 75 @State isRecordOver: boolean = false; 76 @State start: number = 0; 77 @State capturerOffsetStart: number = 0; 78 79 @Builder TabBuilder(index: number, btnId: string) { 80 Column() { 81 Text(index === 0 ? $r('app.string.NORMAL_CAPTURER') : 82 index === 1 ? $r('app.string.PARALLEL_CAPTURER') : 83 $r('app.string.LIVE_CAPTURER')) 84 .fontColor(this.currentIndex === index ? this.selectedFontColor : this.fontColor) 85 .opacity(this.currentIndex === index ? 1 : 0.6) 86 .fontSize(16) 87 .fontWeight(this.currentIndex === index ? 500 : 400) 88 .lineHeight(22) 89 .margin({ top: 17, bottom: 7 }); 90 Divider() 91 .strokeWidth(2) 92 .color('#007DFF') 93 .opacity(this.currentIndex === index ? 1 : 0); 94 }.width(78).id('btn_' + btnId); 95 } 96 97 async aboutToAppear(): Promise<void> { 98 Logger.info('LiveCapturer aboutToAppear'); 99 await this.initResource(); 100 } 101 102 async initResource(): Promise<void> { 103 try { 104 let isSupportAec = audio.getAudioManager().getStreamManager() 105 .isAcousticEchoCancelerSupported(audio.SourceType.SOURCE_TYPE_LIVE); 106 if (!isSupportAec) { //not support echo cancellation, requiring implementation at the application layer 107 this.audioCapturerOptions.capturerInfo.source = audio.SourceType.SOURCE_TYPE_MIC; 108 } 109 this.audioCapturer = await audio.createAudioCapturer(this.audioCapturerOptions); 110 this.bufferSize = await this.audioCapturer.getBufferSize(); 111 this.recordState = 'init'; 112 this.title = `${this.getDate(2)}_${Math.floor(Math.random() * RANDOM_NUM)}`; 113 this.path = `/data/storage/el2/base/haps/entry/files/live_capturer_${this.title}.pcm`; 114 this.date = this.getDate(1); 115 await this.openFile(this.path); 116 } catch (err) { 117 let error = err as BusinessError; 118 Logger.error(`LiveCapturer:createAudioCapturer err=${JSON.stringify(error)}`); 119 } 120 } 121 122 async releseResource(): Promise<void> { 123 if (this.fd > 0) { 124 this.closeFile(); 125 this.fd = 0; 126 } 127 if (this.interval) { 128 clearInterval(this.interval); 129 } 130 if (this.audioCapturer) { 131 Logger.info('LiveCapturer,audioCapturer released'); 132 await this.audioCapturer.release(); 133 this.audioCapturer = undefined; 134 this.recordState = 'init'; 135 clearInterval(this.interval); 136 } 137 if (this.audioRenderer) { 138 Logger.info('LiveCapturer,audioRenderer released'); 139 await this.audioRenderer.release(); 140 this.audioRenderer = undefined; 141 } 142 } 143 144 async aboutToDisappear(): Promise<void> { 145 Logger.info('LiveCapturer,aboutToDisappear is called'); 146 await this.releseResource(); 147 } 148 149 async openFile(path: string): Promise<void> { 150 Logger.info(path); 151 try { 152 await fs.open(path, 0o100); 153 Logger.info('file created success'); 154 } catch (err) { 155 let error = err as BusinessError; 156 Logger.error('file created err:' + JSON.stringify(error)); 157 return; 158 } 159 160 try { 161 let file = await fs.open(path, 0o2); 162 this.fd = file.fd; 163 Logger.info(`file open success for read and write mode,fd=${file.fd}`); 164 } catch (err) { 165 let error = err as BusinessError; 166 Logger.error('file open err:' + JSON.stringify(error)); 167 return; 168 } 169 } 170 171 async closeFile(): Promise<void> { 172 try { 173 await fs.close(this.fd); 174 Logger.info('file close success'); 175 } catch (err) { 176 let error = err as BusinessError; 177 Logger.error('file close err:' + JSON.stringify(error)); 178 return; 179 } 180 } 181 182 async capturerStart(): Promise<void> { 183 if (!this.audioCapturer) { 184 Logger.error(`LiveCapturer,capturerStart:audioCapturer is null`); 185 return; 186 } 187 188 try { 189 await this.audioCapturer.start(); 190 // when start,init recordSec 191 this.recordSec = 0; 192 this.recordState = 'started'; 193 Logger.info('audioCapturer start ok'); 194 clearInterval(this.interval); 195 this.interval = setInterval(async () => { 196 if (this.recordSec >= TOTAL_SECOND) { 197 // over TOTAL_SECOND,need to stop auto 198 clearInterval(this.interval); 199 if (this.audioCapturer && this.audioCapturer.state === audio.AudioState.STATE_RUNNING) { 200 await this.capturerStop(); 201 } 202 return; 203 } 204 this.recordSec++; 205 this.showTime = this.getTimesBySecond(this.recordSec); 206 207 }, INTERVAL_TIME); 208 setTimeout(async () => { 209 await this.readCapturer(); 210 }, READ_TIME_OUT); 211 } catch (err) { 212 let error = err as BusinessError; 213 Logger.error(`LiveCapturer:audioCapturer start err=${JSON.stringify(error)}`); 214 } 215 } 216 217 async renderCreate(): Promise<void> { 218 try { 219 this.audioRenderer = await audio.createAudioRenderer(this.audioRendererOptions); 220 this.renderState = this.audioRenderer.state; 221 this.audioRenderer.on('stateChange', (state) => { 222 this.renderState = state; 223 }); 224 } catch (err) { 225 let error = err as BusinessError; 226 Logger.error(`createAudioRenderer err=${JSON.stringify(error)}`); 227 } 228 } 229 230 async renderStart(): Promise<void> { 231 if (!this.audioRenderer) { 232 return; 233 } 234 let bufferSize = 0; 235 try { 236 bufferSize = await this.audioRenderer.getBufferSize(); 237 await this.audioRenderer.start(); 238 } catch (err) { 239 let error = err as BusinessError; 240 Logger.error(`start err:${JSON.stringify(error)}`); 241 } 242 243 try { 244 let stat = await fs.stat(this.path); 245 let buf = new ArrayBuffer(bufferSize); 246 Logger.info(`audioRenderer write start..........`); 247 let startOffset = this.start; 248 while (startOffset <= stat.size) { 249 if (this.audioRenderer.state === audio.AudioState.STATE_PAUSED) { 250 break; 251 } 252 // change tag,to stop 253 if (this.audioRenderer.state === audio.AudioState.STATE_STOPPED) { 254 break; 255 } 256 if (this.audioRenderer.state === audio.AudioState.STATE_RELEASED) { 257 return; 258 } 259 let options: Options = { 260 offset: startOffset, 261 length: bufferSize 262 }; 263 Logger.info('renderStart,options=' + JSON.stringify(options)); 264 265 await fs.read(this.fd, buf, options); 266 await this.audioRenderer.write(buf); 267 this.playSec = Math.round(startOffset / stat.size * this.recordSec); 268 startOffset = startOffset + bufferSize; 269 this.start = startOffset; 270 } 271 Logger.info(`audioRenderer write end..........`) 272 if (this.audioRenderer.state === audio.AudioState.STATE_RUNNING) { 273 this.start = 0; 274 await this.renderStop(); 275 } 276 } catch (err) { 277 let error = err as BusinessError; 278 Logger.error(`write err:${JSON.stringify(error)}`); 279 } 280 } 281 282 async renderPause(): Promise<void> { 283 if (!this.audioRenderer) { 284 return; 285 } 286 try { 287 await this.audioRenderer.pause(); 288 } catch (err) { 289 let error = err as BusinessError; 290 Logger.error(`pause err:${JSON.stringify(error)}`); 291 } 292 } 293 294 async renderStop(): Promise<void> { 295 if (!this.audioRenderer) { 296 return; 297 } 298 try { 299 await this.audioRenderer.stop(); 300 this.start = 0; 301 } catch (err) { 302 let error = err as BusinessError; 303 Logger.error(`stop err:${JSON.stringify(error)}`); 304 } 305 } 306 307 async releaseStop(): Promise<void> { 308 if (!this.audioRenderer) { 309 return; 310 } 311 try { 312 await this.audioRenderer.release(); 313 } catch (err) { 314 let error = err as BusinessError; 315 Logger.error(`release err:${JSON.stringify(error)}`); 316 } 317 } 318 319 async capturerContinue(): Promise<void> { 320 if (!this.audioCapturer) { 321 Logger.error(`LiveCapturer,capturerContinue:audioCapturer is null`); 322 return; 323 } 324 325 try { 326 await this.audioCapturer.start() 327 this.recordState = 'continued'; 328 Logger.info('audioCapturer start ok'); 329 this.interval = setInterval(async () => { 330 if (this.recordSec >= TOTAL_SECOND) { 331 // over TOTAL_SECOND,need to stop auto 332 clearInterval(this.interval); 333 if (this.audioCapturer && this.audioCapturer.state === audio.AudioState.STATE_RUNNING) { 334 await this.capturerStop(); 335 } 336 return; 337 } 338 this.recordSec++; 339 this.showTime = this.getTimesBySecond(this.recordSec); 340 }, INTERVAL_TIME); 341 setTimeout(async () => { 342 await this.readCapturer(); 343 }, READ_TIME_OUT); 344 } catch (err) { 345 let error = err as BusinessError; 346 Logger.error(`LiveCapturer:audioCapturer start err=${JSON.stringify(error)}`); 347 } 348 } 349 350 async capturerStop(): Promise<void> { 351 if (!this.audioCapturer) { 352 Logger.error(`LiveCapturer,capturerStop:audioCapturer is null`); 353 return; 354 } 355 if (this.recordSec < MIN_RECORD_SECOND) { 356 return; 357 } 358 359 try { 360 await this.audioCapturer.stop(); 361 // when recordState is started or continued 362 this.recordState = 'stopped'; 363 clearInterval(this.interval); 364 } catch (err) { 365 let error = err as BusinessError; 366 // when recordState is paused 367 this.recordState = 'stopped'; 368 Logger.info(`LiveCapturer:audioCapturer stop err=${JSON.stringify(error)}`); 369 } 370 this.isRecordOver = true; 371 await this.renderCreate(); 372 } 373 374 async capturerPause(): Promise<void> { 375 if (!this.audioCapturer) { 376 Logger.error(`LiveCapturer,capturerPause:audioCapturer is null`); 377 return; 378 } 379 380 try { 381 await this.audioCapturer.stop(); 382 this.recordState = 'paused'; 383 384 clearInterval(this.interval); 385 } catch (err) { 386 let error = err as BusinessError; 387 Logger.error(`LiveCapturer:audioCapturer stop err=${JSON.stringify(error)}`); 388 return; 389 } 390 } 391 392 async readCapturer(): Promise<void> { 393 Logger.info('LiveCapturer:readCapturer enter'); 394 if (!this.audioCapturer) { 395 Logger.error(`LiveCapturer,readCapturer:audioCapturer is null`); 396 return; 397 } 398 399 try { 400 let startOffset = this.capturerOffsetStart; 401 while (true) { 402 if (this.audioCapturer.state === audio.AudioState.STATE_STOPPED) { 403 Logger.info('state is changed to be stopped'); 404 break; 405 } 406 let buffer = await this.audioCapturer.read(this.bufferSize, true); 407 Logger.info('LiveCapturer:readCapturer read success'); 408 let options: Options = { 409 offset: startOffset, 410 length: this.bufferSize 411 }; 412 let writen = await fs.write(this.fd, buffer, options); 413 Logger.info(`LiveCapturer:readCapturer,startOffset=${startOffset},writen=${writen}`); 414 startOffset += this.bufferSize; 415 this.capturerOffsetStart = startOffset; 416 } 417 } catch (err) { 418 let error = err as BusinessError; 419 Logger.error(`readCapturer err=${JSON.stringify(error)}`); 420 } 421 } 422 423 formatNumber(num: number): string { 424 if (num <= 9) { 425 return '0' + num; 426 } else { 427 return '' + num; 428 } 429 } 430 431 getDate(mode: number): string { 432 let date = new Date(); 433 if (mode === 1) { 434 return `${date.getFullYear()}/${this.formatNumber(date.getMonth() + 1)}/${this.formatNumber(date.getDate())}`; 435 } else { 436 return `${date.getFullYear()}${this.formatNumber(date.getMonth() + 1)}${this.formatNumber(date.getDate())}`; 437 } 438 } 439 440 getTimesBySecond(t: number): string { 441 let h = Math.floor(t / 60 / 60 % 24); 442 let m = Math.floor(t / 60 % 60); 443 let s = Math.floor(t % 60); 444 let hs = h < 10 ? '0' + h : h; 445 let ms = m < 10 ? '0' + m : m; 446 let ss = s < 10 ? '0' + s : s; 447 return `${hs}:${ms}:${ss}`; 448 } 449 450 @Builder InitRecord() { 451 Column() { 452 Image($r('app.media.ic_record')).width(56).height(56); 453 } 454 .width('100%') 455 .height(56) 456 .position({ y: 60 }) 457 .id('live_start_record_btn') 458 .onClick(() => { 459 this.capturerStart(); 460 }); 461 } 462 463 @Builder StartedRecord() { 464 Column() { 465 Text(this.showTime).fontSize(21).fontWeight(500).margin({ bottom: 8 }) 466 }.width('100%').height(66).position({ y: 30 }).id('live_show_time_txt'); 467 468 469 Column() { 470 Image($r('app.media.ic_recording')).width(56).height(56); 471 } 472 .width('100%') 473 .height(56) 474 .position({ y: 60 }) 475 .id('live_stop_record_btn') 476 .onClick(() => { 477 this.capturerStop(); 478 }); 479 480 Column() { 481 Image($r('app.media.ic_record_pause')).width(24).height(24); 482 Text($r('app.string.PAUSE')).fontSize(12).fontWeight(400).id('live_pause_record_btn').margin({ top: 2 }); 483 } 484 .height(56) 485 .width(56) 486 .position({ x: '80%', y: 60 }) 487 .alignItems(HorizontalAlign.Center) 488 .justifyContent(FlexAlign.Center) 489 .onClick(() => { 490 this.capturerPause(); 491 }); 492 } 493 494 @Builder PausedRecord() { 495 Column() { 496 Text(this.showTime).fontSize(21).fontWeight(500).margin({ bottom: 8 }); 497 }.width('100%').height(66).position({ y: 30 }); 498 499 Column() { 500 Image($r('app.media.ic_recording')).width(56).height(56); 501 }.width('100%').height(56).position({ y: 60 }) 502 .onClick(() => { 503 this.capturerStop(); 504 }); 505 506 Column() { 507 Image($r('app.media.ic_record_continue')).width(24).height(24); 508 Text($r('app.string.CONTINUE')).fontSize(12).fontWeight(400).margin({ top: 2 }); 509 } 510 .height(56) 511 .width(56) 512 .position({ x: '80%', y: 60 }) 513 .alignItems(HorizontalAlign.Center) 514 .justifyContent(FlexAlign.Center) 515 .id('live_continue_record_btn') 516 .onClick(() => { 517 this.capturerContinue(); 518 }); 519 } 520 521 @Builder FinishedRecord() { 522 Column() { 523 Image($r('app.media.ic_record')).width(56).height(56); 524 } 525 .width('100%') 526 .height(56) 527 .position({ y: 60 }) 528 .opacity(0.4) 529 .id('disalbe_btn'); 530 } 531 532 build() { 533 Column() { 534 Column() { 535 Navigation() { 536 } 537 .width('100%') 538 .height('100%') 539 .hideBackButton(false) 540 .titleMode(NavigationTitleMode.Mini) 541 .title($r('app.string.AUDIO_CAPTURER')) 542 .mode(NavigationMode.Stack) 543 .backgroundColor('#F1F3F5'); 544 } 545 .id('live_capturer_back_btn') 546 .width('100%') 547 .height(56) 548 .onClick(async () => { 549 await router.replaceUrl({ url: 'pages/Index' }); 550 }); 551 552 Column() { 553 Tabs({ barPosition: BarPosition.Start, index: 2 }) { 554 TabContent().tabBar(this.TabBuilder(0, 'normal_capturer')); 555 TabContent().tabBar(this.TabBuilder(1, 'parallel_capturer')); 556 TabContent().tabBar(this.TabBuilder(2, 'live_capturer')); 557 } 558 .vertical(false) 559 .barMode(BarMode.Fixed) 560 .barWidth(360) 561 .barHeight(56) 562 .animationDuration(400) 563 .onChange((index: number) => { 564 this.currentIndex = index; 565 if (this.currentIndex === 0) { 566 router.replaceUrl({ url: 'pages/NormalCapturer' }); 567 } else if (this.currentIndex === 1) { 568 router.replaceUrl({ url: 'pages/ParallelCapturer' }); 569 } 570 }) 571 .width('100%') 572 .height(56) 573 }.padding({ left: 12, right: 12 }); 574 575 576 if (this.isRecordOver === true) { 577 Column() { 578 Row() { 579 Text(this.title) 580 .fontSize(16) 581 .fontWeight(500) 582 .fontColor('#182431') 583 .fontFamily($r('sys.string.ohos_id_text_font_family_medium')); 584 if (this.renderState === audio.AudioState.STATE_RUNNING) { 585 Image($r('app.media.ic_record_playing')).width(24).height(24).id('playing_state'); 586 } else { 587 Image($r('app.media.ic_record_paused')).width(24).height(24).id('paused_state'); 588 } 589 590 }.width('100%').height(24).justifyContent(FlexAlign.SpaceBetween).margin({ top: 16 }); 591 592 Row() { 593 Text(this.date) 594 .fontSize(16) 595 .fontWeight(400) 596 .fontColor('#182431') 597 .opacity(0.6) 598 .fontFamily($r('sys.string.ohos_id_text_font_family_medium')); 599 Text(this.getTimesBySecond(this.recordSec) + '') 600 .fontSize(16) 601 .fontWeight(400) 602 .fontColor('#182431') 603 .opacity(0.6) 604 .fontFamily($r('sys.string.ohos_id_text_font_family_medium')); 605 }.width('100%').height(24).justifyContent(FlexAlign.SpaceBetween).margin({ top: 4 }); 606 607 Row() { 608 Progress({ value: this.playSec, total: this.recordSec, type: ProgressType.Linear }) 609 .color('#007DFF') 610 .value(this.playSec) 611 .width('100%') 612 .height(4) 613 }.margin({ top: 23, bottom: 3 }); 614 615 Row() { 616 Text(this.getTimesBySecond(this.playSec) + '') 617 .fontSize(12) 618 .fontFamily($r('sys.string.ohos_id_text_font_family_medium')) 619 .fontColor('#182431') 620 .opacity(0.6) 621 .fontWeight(400); 622 Text(this.getTimesBySecond(this.recordSec) + '') 623 .fontSize(12) 624 .fontFamily($r('sys.string.ohos_id_text_font_family_medium')) 625 .fontColor('#182431') 626 .opacity(0.6) 627 .fontWeight(400); 628 }.justifyContent(FlexAlign.SpaceBetween).width('100%'); 629 } 630 .width('100%') 631 .height(126) 632 .backgroundColor(Color.White) 633 .borderRadius(24) 634 .margin({ top: 12 }) 635 .padding({ left: 12, right: 12 }) 636 .id('live_player') 637 .onClick(() => { 638 if (this.renderState === audio.AudioState.STATE_PREPARED) { 639 this.renderStart(); 640 } 641 if (this.renderState === audio.AudioState.STATE_RUNNING) { 642 this.renderPause(); 643 } 644 if (this.renderState === audio.AudioState.STATE_PAUSED) { 645 this.renderStart(); 646 } 647 if (this.renderState === audio.AudioState.STATE_STOPPED) { 648 this.renderStart(); 649 } 650 }); 651 } 652 Row() { 653 if (this.recordState === 'init') { 654 this.InitRecord(); 655 } else if (this.recordState === 'started') { 656 this.StartedRecord(); 657 } else if (this.recordState === 'paused') { 658 this.PausedRecord(); 659 } else if (this.recordState === 'continued') { 660 this.StartedRecord(); 661 } else if (this.recordState === 'stopped') { 662 this.FinishedRecord(); 663 } 664 } 665 .width('100%') 666 .alignItems(VerticalAlign.Center) 667 .height(116) 668 .position({ y: '82%' }); 669 } 670 .width('100%') 671 .height('100%') 672 .justifyContent(FlexAlign.Start) 673 .backgroundColor('#F1F3F5') 674 .padding({ left: 12, right: 12 }); 675 } 676} 677