[WebRTC][Web Audio API] 발성 중인 사람 식별

40961 단어 typescriptwebrtcpion

소개





  • 특히 WebRTC로 비디오를 사용하지 않을 때는 누가 발성하는지 식별할 수 없습니다.
    그래서 이번에는 클라이언트의 볼륨에서 이를 확인하려고 합니다.


  • webappsample - GitHub

  • 연결된 클라이언트 이름 공유



    WebRTC에서 클라이언트 이름을 공유하기 위한 사양이 없기 때문에 SSE와 공유하겠습니다.

    sseClient.go



    ...
    type ClientName struct {
        Name string `json:"name"`
    }
    type ClientNames struct {
        Names []ClientName `json:"names"`
    }
    ...
    

    sseHub.go



    ...
    func (h *SSEHub) run() {
    ...
        for {
            select {
            case client := <-h.register:
                h.clients[client] = true
                signalPeerConnections(h)
                sendClientNames(h)
            case client := <-h.unregister:
                if _, ok := h.clients[client]; ok {
                    delete(h.clients, client)
                    signalPeerConnections(h)
                    sendClientNames(h)
                }
            case track := <-h.addTrack:
    ...
            }
        }
    }
    ...
    func sendClientNames(h *SSEHub) {
        names := ClientNames{
            Names: make([]ClientName, len(h.clients)),
        }
    
        i := 0
        for ps := range h.clients {
            names.Names[i] = ClientName{
                Name: ps.client.userName,
            }
            i += 1
        }
        message, err := NewClientNameMessageJSON(names)
        if err != nil {
            log.Printf("Error sendClientNames Message: %s", err.Error())
            return
        }
        for ps := range h.clients {
            flusher, _ := ps.client.w.(http.Flusher)
            fmt.Fprintf(ps.client.w, "data: %s\n\n", message)
            flusher.Flush()
        }
    }
    

    main.view.ts



    ...
    type ConnectedClient = {
        name: ClientName,
        element: HTMLElement,
    };
    export class MainView {
    ...
        private clientArea: HTMLElement;
        private connectedClients: ConnectedClient[];
        public constructor() {
    ...
            this.clientArea = document.getElementById("client_names") as HTMLElement;
            this.connectedClients = new Array<ConnectedClient>();
        }
    ...
        public updateClientNames(names: ClientNames): void {
            if(names == null) {
                console.warn("updateClientNames were null");
                return;
            }
            const newClients = new Array<ConnectedClient>();
            for(const c of this.connectedClients) {
                const clientName = c.name.name;
                if(names.names.some(n => n.name === clientName)) {
                    newClients.push(c);
                } else {
                    this.clientArea.removeChild(c.element);
                }
            }
            for(const n of names.names) {
                const clientName = n;
                if(this.connectedClients.some(c => c.name.name === clientName.name) === false) {
                    const newElement = document.createElement("div");
                    newElement.textContent = clientName.name;
                    this.clientArea.appendChild(newElement);
                    this.connectedClients.push({
                        name: clientName,
                        element: newElement,
                    });
                }
            }
        }
    ...
    

    오디오 레벨 얻기



    오디오 레벨은 여러 가지 방법으로 얻을 수 있습니다.
    또한 로컬 미디어 스트림 트랙이나 원격 미디어 스트림 트랙에서 가져올 수도 있습니다.

    원격 미디어 스트림 트랙에서 오디오 레벨을 검색하려면 연결 수와 동일한 횟수의 처리가 필요하기 때문에 이번에는 로컬 미디어 스트림 트랙에서 오디오 레벨을 검색하기로 결정했습니다.

    "RTCPeerConnection.getStats()"로 오디오 레벨 얻기



    RTCPeerConnection의 통계를 얻을 수 있습니다.
    오디오 미디어 스트림 트랙에서 오디오 레벨을 가져올 수 있습니다.



    0: "RTCAudioSource_1"
    1:
        audioLevel: 0.15381328775902586
        echoReturnLoss: -30
        echoReturnLossEnhancement: 0.17551203072071075
        id: "RTCAudioSource_1"
        kind: "audio"
        timestamp: 1659880489574
        totalAudioEnergy: 0.06016985176246171
        totalSamplesDuration: 2.1399999999999983
        trackIdentifier: "f987f34e-ef52-4a27-a73e-910f00bfd090"
        type: "media-source"
    


    webrtc.controller.ts




    ...
        public init(videoUsed: boolean) {
    ...
            let audioTrack: MediaStreamTrack|null = null;
            navigator.mediaDevices.getUserMedia({ video: videoUsed, audio: true })
                .then(stream => {
                    this.webcamStream = stream;
                    const audios = this.webcamStream.getAudioTracks();
                    for(const a of audios) {
                        audioTrack = a;
                    }
                });
            setInterval(() => {
                if(this.peerConnection == null ||
                    this.peerConnection.connectionState !== "connected") {
                    return;
                }
                this.peerConnection.getStats(audioTrack).then((stats) => {
                    for(const report of stats) {
                        for(const r of report) {
                            const audioLevel = this.getAudioLevel(r);
                            if(audioLevel != null &&
                                audioLevel > 0.0) {
                                // If the threshold established between 0 and 1 is exceeded,
                                // it is considered to be talking
                                console.log(audioLevel);
                            }
                        }
                    }
                });
            }, 500);
        }
    ...
        private getAudioLevel(stat: any): number|null {
            if(stat == null ||
                typeof stat !== "object") {
                return null;
            }
            if(!("kind" in stat) ||
                stat.kind !== "audio" ||
                !("audioLevel" in stat)) {
                return null;
            }
            if(typeof stat.audioLevel === "number") {
                return stat.audioLevel;
            }
            if(typeof stat.audioLevel === "string") {
                const parsedResult = parseFloat(stat.audioLevel);
                if(isNaN(parsedResult) === false) {
                    return parsedResult;
                }
            }
            return null;
        }
    }
    


  • Identifiers for WebRTC's Statistics API - W3C
  • WebRTC 1.0: Real-Time Communication Between Browsers - W3C
  • WebRTC Statistics API - MDN

  • 코드가 중복되고 기본 스레드에서 실행되기 때문에 다른 방법을 사용하기로 선택했습니다.

    AudioWorkletNode 및 AudioWorkletProcessor



    또한 "AudioWorkletNode"및 "AudioWorkletProcessor"로 오디오 레벨을 얻을 수 있습니다.
    "AudioWorkletGlobalScope"에서 작동하는 사용자 지정 노드를 제공합니다.

    이를 사용하려면 Main Global Scope에서 실행되는 것과는 별도로 JavaScript 파일을 추가해야 합니다.
    이를 구현하기 위해 "GoogleChromeLabs"의 샘플을 참조합니다.
  • volume-meter - web-audio-samples - GoogleChromeLabs - GitHub

  • 볼륨 측정기-processor.js




    // This code is based on GoogleChromeLabs/web-audio-samples(Copyright (c) 2022 The Chromium Authors) for reference
    // https://github.com/GoogleChromeLabs/web-audio-samples
    
    /* global currentTime */
    
    const FRAME_INTERVAL = 1 / 60;
    
    /**
     *  Measure microphone volume.
     *
     * @class VolumeMeter
     * @extends AudioWorkletProcessor
     */
    class VolumeMeasurer extends AudioWorkletProcessor {
    
      constructor() {
        super();
        this._lastUpdate = currentTime;
      }
    
      calculateRMS(inputChannelData) {
        // Calculate the squared-sum.
        let sum = 0;
        // the value of "inputChannelData.length" is 128 by default.
        for (let i = 0; i < inputChannelData.length; i++) {
          sum += inputChannelData[i] * inputChannelData[i];
        }
        // Calculate the RMS(Root Mean Square) level.
        return Math.sqrt(sum / inputChannelData.length);
      }
      // "output" and "parameters" can be omitted
      process(inputs) {
        // This example only handles mono channel.
        const inputChannelData = inputs[0][0];
        // Calculate and post the RMS level every 16ms.
        if (currentTime - this._lastUpdate > FRAME_INTERVAL) {
          const volume = this.calculateRMS(inputChannelData);
          this.port.postMessage(volume);
          this._lastUpdate = currentTime;
        }
        return true;
      }
    }
    
    registerProcessor("volume-measurer", VolumeMeasurer);
    


    webrtc.controller.ts




    ...
    export class WebRtcController {
        private webcamStream: MediaStream | null = null;
        private peerConnection: RTCPeerConnection | null = null;
    ...
        private localAudioContext: AudioContext;
        private localAudioNode: MediaStreamAudioSourceNode|null = null;
        public constructor() {
            this.localVideo = document.getElementById("local_video") as HTMLVideoElement;
            this.localAudioContext = new AudioContext();
        }
        public init(videoUsed: boolean) {
    ...
            navigator.mediaDevices.getUserMedia({ video: videoUsed, audio: true })
                .then(async stream => {
                    this.webcamStream = stream;
                    // the AudioWorkletProcessor sub classes must be added as modules before creating AudioWorkletNode.
                    await this.localAudioContext.audioWorklet.addModule("./js/volume-measurer-processor.js");
                    // Create a MediaStreamAudioSourceNode and connect AudioWorkletNode to use the AudioWorkletProcessor sub classes.
                    this.localAudioNode = this.localAudioContext.createMediaStreamSource(stream);
                    const volumeMeterNode = new AudioWorkletNode(this.localAudioContext, "volume-measurer");   
                    // MainGlobalScope and AudioWorkletGlobalScope are communicated by "postMessage" and "onmessage".
                    volumeMeterNode.port.onmessage = async ({data}) => {
                        if(this.peerConnection?.connectionState === "connected") {
                            // If the threshold established between 0 and 1 is exceeded,
                            // it is considered to be talking.
                            if(data > 0.05) {
                                console.log(`talking V:${data}`);
                            }
                        }
                    };
                    this.localAudioNode.connect(volumeMeterNode).connect(this.localAudioContext.destination);
                });
        }
    ...
        public connect() {
    ...
            this.peerConnection.onconnectionstatechange = () => {
                if(this.peerConnection?.connectionState === "connected") {
                    // start VolumeMeasurer 
                    this.localAudioContext.resume();
                } else {
                    // stop VolumeMeasurer 
                    this.localAudioContext.suspend();
                }
            };
    ...
        }
    }
    


  • Enter Audio Worklet - Chrome Developers
  • Web Audio API - W3C
  • web-audio-samples - GoogleChromeLabs - GitHub
  • AudioWorkletProcessor - MDN
  • アールエムエス : RMSとは - 偏ったDTM用語辞典 - DTM / MIDI 用語の意味・解説 - g200kg Music & Software
  • AudioWorklet - MDN
  • AudioParam - MDN
  • Audio worklet design pattern - Chrome Developers
  • 좋은 웹페이지 즐겨찾기