• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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