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