LoginSignup
6

More than 3 years have passed since last update.

posted at

updated at

tensorflow.jsとWebRTCを組み合わせて、プライバシー保護のビデオチャットを作ってみた(後編)

はじめに

これはInfocom Advent Calendar 2019 20日目の記事です。

過去に社内でWebRTCを使ったビデオ会議のデモをした際に、「在宅勤務で使うときに、自分の顔よりも部屋の様子(背景)を隠したい」というリクエストを受けたことがありました。

それを実現すべく次の要素を組み合わせたサンプルを作ったので、前回に引き続き紹介します。

おさらい

前回はtensorflow.js+body-pixを使って、カメラで取得した映像から人物 or 周囲(背景)をマスクする部分を実装しました。

Canvasでマスクした映像を取得

Canvasは画面に見えるだけですが、これを今後の通信で使うために、captureStream()を使って映像ストリーム(MediaStream)として取得します。引数として、fps(フレームレート)を指定することもできます。

      let canvasStream = canvas.captureStream();

音声のピッチシフト加工

人体をマスクする場合に、声も変えたいことがあると思います。そのため、WebAudioを使ったピッチシフトで声も加工できうようにしました。

処理は基本的に Pitch shifter (MIT ライセンス) を踏襲しています。そのままでは動かなかったので、必要な部分を抜き出して利用させてもらいました。

カメラ映像と同様にマイク音声をgetUserMedia()で取得し、それをWebAudioのScriptProcessorで加工しています。元の音声と加工音声を切り替えられるように、gainNodeでミックスしています。

[構成図]

webaudio_pitch.png

    function initSoundProcesser(stream) {
      clearAudioNode();

      audioSrcNode = audioContext.createMediaStreamSource(stream);
      mediaDestNode = audioContext.createMediaStreamDestination();
      voiceChangeStream = mediaDestNode.stream;

      normalGainNode = audioContext.createGain();
      normalGainNode.gain.value = 0;
      audioSrcNode.connect(normalGainNode);
      //normalGainNode.connect(audioContext.destination);
      normalGainNode.connect(mediaDestNode);

      // ---- pitch shifter ---
      pitchShifterProcessor = audioContext.createScriptProcessor(grainSize, 1, 1);
      pitchShifterProcessor.buffer = new Float32Array(grainSize * 2);
      pitchShifterProcessor.grainWindow = hannWindow(grainSize);
      pitchShifterProcessor.onaudioprocess = function (event) {
        const inputData = event.inputBuffer.getChannelData(0);
        const outputData = event.outputBuffer.getChannelData(0);

        // ... 詳細は https://github.com/urtzurd/html-audio を参照 ...

      };
      audioSrcNode.connect(pitchShifterProcessor);

      shiftGainNode = audioContext.createGain();
      shiftGainNode.gain.value = 1.0;
      pitchShifterProcessor.connect(shiftGainNode);
      //shiftGainNode.connect(audioContext.destination);
      shiftGainNode.connect(mediaDestNode);

    }

映像との連結

Canvasから取得した映像と、WebAudioで加工した音声を、1つのメディアストリームにまとめます。新しくMediaStreamのインスタンスを作り、そこに映像トラックと音声トラックを追加します。

concat_video_audio.png

      // 新しいメディアストリーム
      modifiedStream = new MediaStream();

      // 映像加工を開始
      const maskStream = startCanvasVideo();
      modifiedStream.addTrack(maskStream.getVideoTracks()[0]); // 映像トラックを追加

      // 音声加工を開始
      const ctx = initWebAudio();
      pitchShifter = new PitchShifter(0.7);
      pitchStream = pitchShifter.initSoundProcesser(ctx, localStream);
      modifiedStream.addTrack(pitchStream.getAudioTracks()[0]); // 音声トラックを追加

WebRTC で通信

映像と音声の準備ができたので、WebRTCを使って通信します。今回は手軽に1対1通信ができる、Ayame Lite (株式会社時雨堂 運営)を使ってみました。

  • 未登録の場合 ... 認証なしのシンプルな通信
  • 登録ありの場合 ... 認証あり、STUN/TURNも利用可能

となっています。無償で無保証のサービスです。

実際の利用には各種SDKとサンプルがOSSとして用意されているで、そちら利用するのが便利です。

今回の利用例はこちら。

// -------- ayame -----------
    // --- 接続処理 ---
    async function connect() {
      let roomId = roomIdInput.value;
      if ((!roomId) || (roomId === '')) {
        roomId = defaultRoomId
      }

      if (!ayameConn) {
        ayameConn = Ayame.connection(signalingUrl, roomId);
      }

      await ayameConn.connect(modifiedStream);

      ayameConn.on('connect', () => {
        console.log('ayame connected. ayame state=', ayameConn.connectionState);
      });
      ayameConn.on('disconnect', (e) => {
        if (remoteVideo.srcObject) {
          remoteVideo.pause();
          remoteVideo.srcObject = null;
        }

        ayameConn.disconnect();
      });
      ayameConn.on('addstream', async (e) => {
        remoteVideo.srcObject = e.stream;
        await remoteVideo.play().catch(err => console.warn('remote play ERROR:', err));
        remoteVideo.volume = 0.5;
      });
      ayameConn.on('removestream', () => {
        remoteVideo.pause();
        remoteVideo.srcObject = null;
      });
    }

    // --- 切断処理 ---
    function disconnect() {
      if (!ayameConn) {
        return;
      }
      ayameConn.disconnect();

      if (remoteVideo.srcObject) {
        remoteVideo.pause();
        remoteVideo.srcObject = null;
      }
    }

ここまでのデモ(映像マスク+音声変換の通信)

ここまでのデモがこちらです。Webカメラが必要です。

  • ソースコード(GitHub) video_mask_voice_ayame.html
  • デモページ(GitHub Pages) video_mask_voice_ayame.html

  • 準備 ... 2台のPCで、それぞれブラウザを開く(Chrome 78で動作確認)

    • 1台のPCで、2つのウィンドウでも可
    • ハウリング防止のため、ヘッドフォンの利用を推奨
  • 開始

    • それぞれのブラウザで video_mask_voice_ayame.html を開く
    • それぞれのブラウザで[Start Video] ボタンをクリック
      • 自分の映像が、左上にビデオ要素に表示
      • 数秒待つとBody-Pixが初期化され、右上のビデオ要素にマスクされた映像が表示
    • マスクの種類は選択可能
      • mask background ... 人物の周囲の部屋の様子をマスク
      • mask person ... 人物をマスク
      • no mask ... マスク無し
    • 音声の種類を選択可能
      • mute ... 音声ミュート
      • low pitch ... 低いピッチの声
      • high pitch ... 高いピッチの声
      • normal ... 通常の声
    • Ayame Lite サービスに接続する room-id を指定
    • それぞれのブラウザで [connect] ボタンをクリック
  • 停止

    • [disconnect] ボタンをクリック
    • [Stop Video] ボタンをクリック

bodypix_capture.png

おわりに

WebRTCの通信には今回 Ayame Lite を利用しましたが、他にも Skyway や最近WebRTCをサポートした Amazon Kinesis Video Streams などを使うこともできるはずです。お試しください。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
6