sciptprocessor 通过更改输入流来增加 Angular 10 中的 CPU 使用率还有怎么销毁

问题描述

希望从我在设备访问组件路由上实现的音量计释放 cpu 资源。此音量计组件使用 WebRTC navigator.mediaDevices.getUserMedia 调用获取在父组件(设备访问)中设置的现有流。父组件允许用户切换音频输入设备并让音量计反映输入的音量/增益/反馈在音量计中反映。

目前存在一个问题,在多个设备切换时,cpu 级别逐渐上升。此外,此设备访问页面是通往视频会议组件的门户。一个常见的场景是让用户返回到这个设备访问页面。当用户返回时,onaudioprocess 仍在运行并增加 cpu 使用率。

以下是代码。我在子组件(音量计)中实现了 ngOnDestroy,但它似乎不会影响仍在运行的进程。当我切换输入音频设备(在订阅中)时,我想杀死脚本处理器并重新启动它。我该怎么做?

export class VolumeMeterComponent implements OnInit,AfterViewInit,OnDestroy {

  private stream: MediaStream = null;
  private audioContext: AudioContext = null;
  private meter: any = null;
  private canvasContext: any = null;
  // height and width of the volume meter
  private WIDTH: number = 146;
  private HEIGHT: number = 9;
  private rafID: number = null;
  private mediastreamsource: any = null;
  private clipping: boolean = null;
  private lastClip: number = null;
  private volume:number = null;
  // averaging: how "smoothed" you would like the meter to be over time.  
  // Should be between 0 and less than 1.
  private averaging: number = .95;
  // the level (0 to 1) that you would consider "clipping"
  private clipLevel: number = .98;
  // clipLag: how long you would like the "clipping" indicator to show after clipping has occurred,in milliseconds.
  private clipLag: number = 750;
  private loopInstance: any = null;
  private processHandle: any = null;
  // @ts-ignore
  @ViewChild('meterElement',{read: ElementRef,static: false}) meterElement: ElementRef;

  constructor(private streamService: StreamService) {}

  ngOnInit(): void {
    // nothing here for Now
  }

  ngAfterViewInit(): void {
    this.streamService.stream$.subscribe(stream =>{
      this.stream = stream;
      if (this.loopInstance) {
        this.processHandle.stop();
        this.cleanupMeterOnChange();
      }
      this.initializeMeter();
    });
  }

  ngOnDestroy(): void {
    this.cleanupMeterOnChange();
  }

  cleanupMeterOnChange():void {
    // cleanup the canvasContext and meter onDestroy
    // best practice is to cleanup the the canvasContext as it has processes
    this.meter = null;
    this.canvasContext = null;
    this.drawLoop = null;
  }

  initializeMeter():void {
    this.canvasContext = this.meterElement.nativeElement.getContext('2d');
    // update audioContext to whatever is available from browser
    try {
      (window as any).AudioContext = (window as any).AudioContext || (window as any).webkitaudiocontext;
      this.audioContext = new AudioContext();
      this.mediastreamsource = this.audioContext.createmediastreamsource(this.stream);
      // Create a new volume meter and connect it.
      this.meter = this.createAudioMeter(this.audioContext);
      this.mediastreamsource.connect(this.meter);
      this.loopInstance = this.drawLoop();
    } catch(error) {
      console.log('Error setting up the volume meter. ' + error);
    }
  }

  drawLoop = () => {
    // clear the background
    this.canvasContext.clearRect(0,this.WIDTH,this.HEIGHT);
    // check if we're currently clipping
    if (this.meter.checkClipping()) {
      this.canvasContext.fillStyle = 'red';
    } else {
      this.canvasContext.fillStyle = 'green';
    }
    // draw a bar based on the current volume
    this.canvasContext.fillRect(0,this.meter.volume * this.WIDTH * 1.4,this.HEIGHT);

    // set up the next visual callback
    this.rafID = window.requestAnimationFrame( this.drawLoop );
  }

  createAudioMeter = audioContext => {
    const processor = audioContext.createscriptprocessor(2048,1,1);
    processor.onaudioprocess = this.volumeAudioProcess;
    this.processHandle = processor;
    processor.clipping = false;
    processor.lastClip = 0;
    processor.volume = 0;
    processor.clipLevel = this.clipLevel;
    processor.averaging = this.averaging;
    processor.clipLag = this.clipLag;

    // this will have no effect,since we don't copy the input to the output,// but works around a current Chrome bug.
    processor.connect(audioContext.destination);
    processor.checkClipping =
      checkClipping;

    // tslint:disable-next-line:typedef
    function checkClipping() {
      const that = this;
      if (!that.clipping) {
        return false;
      }

      if ((that.lastClip + that.clipLag) < window.performance.Now()) {
        that.clipping = false;
      }
      return that.clipping;
    }

    processor.shutdown =
      function(): void {
        this.disconnect();
        this.onaudioprocess = null;
      };

    return processor;
  }

  volumeAudioProcess( event ): void {
    this.clipping = false;
    const buf = event.inputBuffer.getChannelData(0);
    const bufLength = buf.length;
    let sum = 0;
    let x;

    // Do a root-mean-square on the samples: sum up the squares...
    for (let i = 0; i < bufLength; i++) {
      x = buf[i];
      if (Math.abs(x) >= this.clipLevel) {
        this.clipping = true;
        this.lastClip = window.performance.Now();
      }
      sum += x * x;
    }

    // ... then take the square root of the sum.
    const rms =  Math.sqrt(sum / bufLength);

    // Now smooth this out with the averaging factor applied
    // to the prevIoUs sample - take the max here because we
    // want "fast attack,slow release."
    this.volume = Math.max(rms,this.volume * this.averaging);
  }
}

组件标记

<canvas id="meterElement" #meterElement width="146" height="8"></canvas>
<p class="level-label">Microphone volume level</p>

我曾尝试使用 ViewChild 订阅画布并取消订阅,但运气不佳。任何人都对更有效地运行此策略的策略有任何见解。订阅 drawLoop(并将其提取到服务)是最好的答案吗?

我知道 WebRTC 推荐 audioWorklets: https://alvestrand.github.io/audio-worklet/

  • 这是一个草稿,没有采用 Safari。从长远来看,这似乎是一个更好的解决方案。

解决方法

您可以在 AudioContext 上调用 close() 以停止其所有节点并使其释放执行其操作所需的任何系统资源。我认为将此添加到您的 cleanupMeterOnChange() 方法应该可以提高性能。

cleanupMeterOnChange():void {
    // ...
    this.audioContext.close();
}