Edited at
SkyWayDay 21

SkyWayでデスクトップの映像とカメラの映像をクロマキー合成して配信する


はじめに

この記事はSkyWay Advent Calendar 2018の21日目(大遅刻)の記事です。

20日目はShirataHikaruさんの「SkyWayとVue.jsでリアルタイムに共同編集可能なマークダウンエディタっぽいもの」でした。


WebRTCの"W"の字もわからない

まず「WebRTC」という言葉を知っているかどうかも怪しい状態からのスタートです。

全くどうしたらいいかわからなかったので、SkyWayのアドベントカレンダーを書いている方達の記事を読みながら使い方を学ぼうと思いました。

まずはアドベントカレンダー8日目の、「【爆速!】5分でビデオチャットを構築する」by kolifeさんの記事を参考にWebRTCとSkyWayに触れてみます。

はい、これでWebRTCとSkyWayは完全に理解しました。


今回作るもの

完全に理解したので、早速サンプルコードなどを改造して自分でも何か作ってみようと思いました。

そこで参考にさせていただいたのがアドベントカレンダー16日目の「SkyWay のビデオチャットに雑なクロマキー合成してみた」byhorihiroさんの記事です。

カメラ映像の背景を任意の画像とクロマキー合成するというものですが、このアイデアはそのまま、置き換える背景画像をデスクトップキャプチャーの映像にすれば即席顔出しゲーム実況配信みたいなのが実現できるのではないかと思ったので作ってみました。horihiroさん良記事ありがとうございます。


下準備

SkyWayのスクリーンシェアサンプルコードを改造して作っていきます。


https://github.com/skyway/skyway-screenshare


サンプルのREADMEにしたがって、画面共有ができることを確認します。


作る

以下の記事が大変参考になりました。


canvasでクロマキー合成っぽいことしてみる


この記事では、カメラから取り込んだvideo要素をクロマキー加工して、結果をCanvasに描画しているようです。

これを使えばカメラの背景透過はできそう。

で、スクリーンキャプチャと合成して送信しなきゃいけないんですが、スクリーンキャプチャもCanvasに描画して、カメラCanvasスクリーンキャプチャCanvasをいい感じに合成して相手に送れば実現できそう。

概要図で表すとこんな感じ。


キャンバスを作る

まずは「いい感じに合成」する手前までを実装して、自分で確認できるようにしましょう。

クロマキー調節のInputと、Canvas2つを作ります。


index.html

      <p>My Video</p>

<div>
<input id="color" type="color" value="#00ff00" />
<input id="distance" type="number" value="30" />
</div>

<div id="canvasArea">
<canvas id="myCanvas1" width="320" height="240"></canvas>
<canvas id="myCanvas0" width="320" height="240"></canvas>
</div>


CSSで以下のように書くと、<div id="canvasArea">に囲まれたキャンバスが重なります。


style.css

#canvasArea {

position: relative;
width: 320px;
height: 240px;
margin: 0 auto;
border: 1px black solid;
}

canvas{
position: absolute;
}



クロマキーして描画

canvasでクロマキー合成っぽいことしてみるのコードを使ってWebカメラをクロマキー処理します。

クロマキー処理はapp.jsCameraボタンを押された時の動作が記述されているところに追記します。


app.js

  // Camera

$('#start-camera').on('click', () => {
navigator.mediaDevices.getUserMedia({audio: true, video: true})
.then(stream => {
$('#my-video')[0].srcObject = stream;
localStream = stream;

if (existingCall !== null) {
const peerid = existingCall.peer;
existingCall.close();
const call = peer.call(peerid, localStream);
step3(call);
}
})
.catch(err => {
$('#step1-error').show();
});

$('#my-video')[0].addEventListener("loadedmetadata",function(e) {
ctx.translate(canvas.width,0);
ctx.scale(-1,1);

setInterval(function(e) {
//videoタグの描画をコンテキストに描画
ctx.drawImage($('#my-video')[0],0,0,canvas.width,canvas.height);
chromaKey();
},33);
});
});


ポイントは、

$('#my-video')[0].addEventListener("loadedmetadata",function(e) {});

で、streamが取得されるのを待っていること。

これがないとタイミングが合わず、Canvasにうまく描画されないことがあります。

あと、Webカメラの映像はデフォルトで左右反転するらしいので、以下のコードでキャンバスを反転させて映像を正しい方向に直しています。

ctx.translate(canvas.width,0);

ctx.scale(-1,1);


スクリーンキャプチャも描画

スクリーンキャプチャの映像も同じ要領でCanvasに描画します。

スクリーンキャプチャで得られる映像は左右反転しないですし、クロマキー処理もいらないので、Screenボタンが押された時の動作に以下を追記するだけでいいです。


app.js

      $('#my-video2')[0].addEventListener("loadedmetadata",function(e) {

setInterval(function(e) {
ctx1.drawImage($('#my-video2')[0],0,0,canvas1.width,canvas1.height);
},33);
});

この段階でWebページの表示テストをしてみて、Cameraボタン Screenボタンをそれぞれ押してから、カラークロマキーを調節してうまく合成されればひとまず「いい感じに合成」の手前までの処理は完成です。



・・・怖い。


canvasをいい感じに合成

ここから2つのキャンバスをいい感じに1つにまとめて、相手に送ります。

別に一つにまとめなくても、SkyWayのMediaStreamはTrackを追加することで複数のメディア(Webカメラとスクリーンキャプチャ同時とか)を送信することができるようです。


Qiita記事

WebRTCで快適な画面共有を実現する


しかし、これがまたなかなか理解できず今回のアドベントカレンダー大遅刻の原因となりました。


力技で解決

しょうがないので今回は、もう一つキャンバスを作って、そこに必ずスクリーンキャプチャクロマキー処理されたカメラ映像の順番に描画を上書きすることで合成する方法で解決しました。

3つ目のキャンバスを作ります。

    var canvas2 = document.createElement("canvas");

var ctx2 = canvas2.getContext("2d");
canvas2.width = 320;
canvas2.height = 240;

3つ目のキャンバスは結合して送信するようで、別にWebページに表示する必要はないのでHTMLには記載せず.createElement("canvas")で作ってJS内だけで使います。

で、色々と悩んだ結果クロマキー処理の末尾にcanvas結合の処理を書きました。

  // クロマキー処理

var chromaKey = function () {

//...省略

ctx.putImageData(imageData, 0, 0);
//ここでcanvas3を上書きしていく
ctx2.drawImage(canvas1,0,0,canvas.width,canvas.height);
ctx2.drawImage(canvas,0,0,canvas.width,canvas.height);
};


相手に送信する

合成できたcanvas3を相手に送信するようにします。

ここもまた悩みましたが、アドベントカレンダー 9日目「カメラ映像を分割してSkyWayで配信するアプリケーションを作ってみた」bysublimerさんの記事を参考にしたら解決しました。

canvas.captureStream(フレームレート);

で、canvasをMediaStreamとして取得できるようです。

Camera処理ScreenShare処理それぞれの、

localStream = stream;

となっているところを、

localStream = canvas2.captureStream($('#FrameRate').val());

に書き換えます。

また、その下のif文内にある

const call = peer.call(peerid, stream);

の.call( )の引数も、

const call = peer.call(peerid, localStream);

と書き換えます。

これで通信相手にはcanvas3のMediaStreamのみが送信されるようになりました。


完成!


送信側

映像を送信する側はこんな感じに表示確認できます。

下段左側がWebカメラの映像。

下段右側がスクリーンキャプチャの映像。

上段がクロマキー合成された映像です。


受信側

受信側もきちんとクロマキー合成された映像が表示されます。


まとめ

これがあれば、即席で顔出しのゲーム実況配信動画講座の配信ができそうですね。

今回作成したコードを以下のリポジトリにまとめました。


GitHub

Video-and-Screen_ChromakeySharing


最初はわからないことだらけでしたが、SkyWayアドベントカレンダーの投稿記事のおかげでだいぶ助けられました。先人たちに感謝です。