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