10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-19

はじめに

これは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 などを使うこともできるはずです。お試しください。

10
6
0

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
  3. You can use dark theme
What you can do with signing up
10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?