概要
弊社のインターンのお手伝いで、たまたまWebRTCを触る機会があったので、
気軽に加工した映像を配信したりできるのかなと思って試したので記します
※ 本来なら、MCU ( Multipoint Control Unit )とかで映像加工するのかなと思うんですが、
今回は、インターン期間中に気軽に実装できそうなものくらいの感覚なので
配信者側(クライアント側)で映像を加工して配信する想定です。
本題
今回は、SkyWay さんのサービスを利用します。(簡単にビデオ通話が実装できます)
結論から言うと、 カメラから取得した映像ストリームをCanvasに描画し、
Canvasで加工したものを再び映像ストリームとして取得したものを配信すれば可能 です。
なので、👇のように、白黒モザイク加工を施した上で、Websocketで受け取ったコメントを映像に載せて配信なんかもできます
簡易版として、チュートリアル 1:1ビデオ通話のサンプルコードを元に、
カメラから取得した映像を 白黒加工して配信 していきます
カメラから映像を取得したら発火する処理に追記してく感じです
<script>
let localStream;
navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then( stream => {
/**
* ここに映像を加工する処理を書く
*/
// 着信時に相手にカメラ映像を返せるように、グローバル変数に保存しておく
localStream = stream;
最初にカメラから取得した映像を流すためのVideoタグに加えて、加工用のCanvasタグを追加しておきます
<canvas id="my-canvas" width="640" height="360"></canvas>
<video id="my-video" width="640" height="360" autoplay></video>
追加したCanvasに Videoに流しているカメラから取得した映像を描画しながら、ピクセル単位で白黒にします
navigator.mediaDevices.getUserMedia(
..
)
.then( stream => {
const videoElm = document.getElementById('my-video')
const canvasElm = document.getElementById('my-canvas')
const ctx = canvasElm.getContext('2d'); // CanvasRenderingContext2D インスタンス
setInterval(() => {
ctx.drawImage(videoElm, 0, 0, canvasElm.width, canvasElm.height); // Videoに描画されている内容をCanvasにも描画する
var frame = ctx.getImageData(0, 0, canvasElm.width, canvasElm.height); // ImageData インスタンス
var pixels = frame.data.length / 4; // ピクセルサイズ長 (今回の場合 640 x 360)
for (var i = 0; i < pixels; i++) {
const grey = (frame.data[i * 4 + 0] + frame.data[i * 4 + 1] + frame.data[i * 4 + 2]) / 3;
frame.data[i * 4 + 0] = grey // Red
frame.data[i * 4 + 1] = grey // Green
frame.data[i * 4 + 2] = grey // Blue
}
ctx.putImageData(frame, 0, 0);
}, 10000/120);
videoElm.srcObject = stream;
videoElm.play();
// Canvasから映像ストリームを取得する
localStream = canvasElm.captureStream(120); // 120fps
順に見ていきます
カメラから映像を取得できたら、Canvasから CanvasRenderingContext2D
という2次元のレンダリングコンテキストを取得してきます。
これに手を加えていくことでCanvasを加工していくことが可能です。
const ctx = canvasElm.getContext('2d');
次に、 setInterval
で定期的に、さきほどの ctx
に対して加工を加えていきます
(今回の場合、120fps で加工を加えていきます)
setInterval(() => {
/**
* ここに ctx に対して加工を加えていく処理を書く
*/
}, 10000/120);
setInterval
内での処理に入り、 最初に Videoに描画されている内容をCanvasにも描画します
ctx.drawImage(videoElm, 0, 0, canvasElm.width, canvasElm.height);
Videoの映像が描画された ctx
を加工していきたいので、 getImageData
を実行し ImageData
を取得してきます
ImageData
の data
を実行すると、 1ピクセルごとのRGBAの値が格納された一次元配列(正確には Uint8ClampedArray
)を取得できます。
RGBA: 赤・緑・青・アルファの値が 0 ~ 255
で格納されてます
なので配列の中身としては以下のようなものになります
[
1ピクセル目の赤, 1ピクセル目の緑, 1ピクセル目の青, 1ピクセル目のアルファ,
2ピクセル目の赤, 2ピクセル目の緑, 2ピクセル目の青, 2ピクセル目のアルファ,
..
]
4
で割ってCanvasのピクセルサイズ長を算出しているのはそのためです
var frame = ctx.getImageData(0, 0, canvasElm.width, canvasElm.height);
var pixels = frame.data.length / 4;
後は、ピクセルサイズ長だけ繰り返しで、RGBの平均値を算出し、値を代入していきます
for (var i = 0; i < pixels; i++) {
const grey = (frame.data[i * 4 + 0] + frame.data[i * 4 + 1] + frame.data[i * 4 + 2]) / 3;
frame.data[i * 4 + 0] = grey // Red
frame.data[i * 4 + 1] = grey // Green
frame.data[i * 4 + 2] = grey // Blue
}
setInterval
の最後に ctx
に putImageData
を実行すれば、Canvasに白黒が反映されます
ctx.putImageData(frame, 0, 0);
加工したCanvasから映像ストリームを取得するには captureStream
を利用します
(setInterval
に合わせて 120fps にしてあります)
localStream = canvasElm.captureStream(120);
これで、加工した映像ストリームを取得できたので、カメラから取得した映像ストリームをP2P通信に流すのと同じようにすればできます。
const mediaConnection = peer.call(theirID, localStream);
最後に、今回はSkyWayを利用して、さくっとクライアント側で映像を加工して配信する方法を試してみました。
おそらく本来はこういう感じにはしないのでは(?)と思ったりしているので、詳しい方がいらっしゃれば教えていただきたいところです
あと、 setInterval
を利用しているので、ChromeやFirefoxなどのブラウザを利用していて、別タブに移動したりすると、非アクティブと認識され、映像がカクカクするようになるので注意です
MDNの setInterval
にも以下のような記載があります
Starting in Gecko 5.0 (Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2), intervals are clamped to fire no more often than once per second in inactive tabs.