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