はじめに
これはInfocom Advent Calendar 2019 20日目の記事です。
過去に社内でWebRTCを使ったビデオ会議のデモをした際に、「在宅勤務で使うときに、自分の顔よりも部屋の様子(背景)を隠したい」というリクエストを受けたことがありました。
それを実現すべく次の要素を組み合わせたサンプルを作ったので、前回に引き続き紹介します。
- tensorflow.js の人体検出モデル tfjs-models/body-pix (Apache 2.0 ライセンス)
- WebAudioを使ったピッチシフト Pitch shifter (MIT ライセンス)
- WebRTC シグナリングサービス Ayame Lite(株式会社時雨堂)
- そのSDK ayame-web-sdk(Apache 2.0 ライセンス)
おさらい
前回はtensorflow.js+body-pixを使って、カメラで取得した映像から人物 or 周囲(背景)をマスクする部分を実装しました。
Canvasでマスクした映像を取得
Canvasは画面に見えるだけですが、これを今後の通信で使うために、captureStream()を使って映像ストリーム(MediaStream)として取得します。引数として、fps(フレームレート)を指定することもできます。
let canvasStream = canvas.captureStream();
音声のピッチシフト加工
人体をマスクする場合に、声も変えたいことがあると思います。そのため、WebAudioを使ったピッチシフトで声も加工できうようにしました。
処理は基本的に Pitch shifter (MIT ライセンス) を踏襲しています。そのままでは動かなかったので、必要な部分を抜き出して利用させてもらいました。
カメラ映像と同様にマイク音声をgetUserMedia()で取得し、それをWebAudioのScriptProcessorで加工しています。元の音声と加工音声を切り替えられるように、gainNodeでミックスしています。
[構成図]
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のインスタンスを作り、そこに映像トラックと音声トラックを追加します。
// 新しいメディアストリーム
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-web-sdk (Apache 2.0 ライセンス)
- ayame-web-sdk-samples(Apache 2.0 ライセンス)
今回の利用例はこちら。
// -------- 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] ボタンをクリック
おわりに
WebRTCの通信には今回 Ayame Lite を利用しましたが、他にも Skyway や最近WebRTCをサポートした Amazon Kinesis Video Streams などを使うこともできるはずです。お試しください。