2
2

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 1 year has passed since last update.

【新規事業】SkyWayでモザイク加工したカメラ映像を送ってみるよ(後編)【プロト開発】

Last updated at Posted at 2022-07-25

image.png

はじめに

株式会社マイスター・ギルド新規事業部のウサギーです。
弊社新規事業部では、新規サービスの立ち上げを目指して
日々、アイディアの検証やプロトタイプの作成を行っています。

アイディアの検証のために、先日作成したSkyWay*の1対1通信アプリに自分たちの機能を載せていこう!となる中で

①カメラ映像に加工すること
②加工した映像をSkyWayを通じて送ること

が必要になりました。

*SkyWay…NTTコミュニケーションズが提供する、ビデオ・音声通話を簡単に実装できるSDKとAPIです

この記事の位置づけ

今回は上記のうち「②加工した映像をSkyWayを通じて送ること」についての記事になります。

あらすじ

前回、カメラ映像にモザイク加工を実装しました。

こんなかんじ
image.png
次は、このモザイク加工をかけた映像を相手側に送ります。

そもそもSkyWayってCanvas送れるの?

イメージはあった

どうやってSkyWay越しに映像を送るのか分からないけれど、
過去の経験から「カメラの映像にあれやこれやするのはCanvasを使うだろう」と考えていたので、
こんな実装イメージ(仮説)を持っていました。

  • カメラ映像を使って加工したCanvasを作って
  • そのCanvas映像をSkyWayで送る

ですので「CanvasをSkyWayで送れるのか?」と言う部分さえ解決できたら
この手順で機能を実現できます。

SkyWayでやり取りする情報を確認する

「SkyWayで送れる形」についてまず調べてみます。
現状、カメラ映像を送っている部分を確認することにしました。

SkyWayのAPI仕様を確認

SkyWayで「映像を送る」処理の入り口はここ

main.js
document.getElementById('call-btn').onclick = () => {
    const theirID = document.getElementById('their-id').value;
    const mediaConnection = peer.call(theirID, localStream);
    setEventListener(mediaConnection);
};

peer.call()メソッドの第2引数にlocalStreamを渡しているだけです。(あら簡単)

peer.call()メソッドのAPI仕様を確認してみます。

image.png
APIリファレンス:SkyWayより引用

第2引数はMediaStreamオブジェクトと定義されていることがわかりました。
つまり、MediaStreamオブジェクトの映像の部分にオリジナルの加工が出来たらOK!

CanvasからMediaStreamをつくる方法を探す

SkyWayで送るのに必要なのは「MediaStream」だと分かりました。
ということは CanvasからMediaStreamを作る方法 があれば送れるはず…!
早速、サラサラッとググります。

ググった結果

canvas の前面をリアルタイムにキャプチャした動画を CanvasCaptureMediaStream (en-US) として返すメソッド
captureStream:MDN Web Docs

を見つけました。

image.png
captureStream:MDN Web Docsより引用

って書いてあるし。

探してたのこれっぽい!
いける!いけるぞ~~~!

CanvasをMediaStreamへ

早速、試してみます。

main.js
navigator.mediaDevices.getUserMedia({ video:{ width: CVS_WIDTH , height: CVS_HEIGHT }, audio: true })
    .then(stream => {
        myVideo.srcObject = stream;
        myVideo.play();

-       localStream = stream;
+       localStream = myCvs.captureStream(30);   
        
        myVideo.onloadeddata = () => {
            setInterval(() => {
                if(isMosaicEnabled){
                    mosaicMyCvs();
                }else{
                    copyToMyCvs();
                }
            }, 1000 / 30);
        }
    }).catch(error => {
        console.error('mediaDevice.getUserMedia() error:', error);
});

書き替えたのは1行だけです。

peer.call()メソッドの第2引数渡しているlocalStreamを、
getUserMedia()で取得したMediaStreamではなく、
モザイクを描画しているmyCvsをキャプチャしたCanvasCaptureMediaStreamに。

流れはこんな感じです。
image.png

動かしてみると…
image.png
モザイクがかかったカメラ映像が送れました~!やった~~~!

コード全体

既存コードからすこしリファクタリングしてますが、機能の変更部分は上記の1文だけです。

index.html
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>1対1ビデオ通話</title>
    <style type="text/css">
        .container{
            width: 900px;
        }
        .main-wrapper{
            height: 600px;
        }
        .my-section{
            float: left;
            width: 50%;
            height: 100%;
        }
        .their-section{
            float: right;
            width: 50%;
            height: 100%;
        }
        #my-video{
            position:absolute;
            top:140px;
            left:10px;
            z-index:1;
        }
        #my-canvas{
            position:absolute;
            top:140px;
            left:10px;
            z-index:2;
        }
        #my-setting{
            position:absolute;
            top:420px;
            left:10px;
            z-index:3;
        }
    </style>
</head>
<body>
    <h1>1対1ビデオ通話のサンプル</h1>
    <div class="container">
        <div class="my-section">
            <p class="my-id-label">自分のPeerID: <span id="my-id"></span></p>
            <video id="my-video" width="360px" height="270px" autoplay muted playsinline></video>
            <canvas id="my-canvas"></canvas>
            <div id="my-setting" >
                <input type="button" id="mosaic-btn" value="モザイク">
                <span id="mosaic-enabled">OFF</span>
            </div>
        </div>
        <div class="their-section">
            <p>
                <label class="call-id-form-label" for="their-id">相手のPeerID: </label>
                <input id="their-id" class="call-id-form">
                <button type="button" id="call-btn">発信</button>
            </p>
            <video id="their-video"  width="360px" height="270px" autoplay muted playsinline></video>    
        </div>
    </div>
    <script src="//cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
    <script src="./main.js"></script>
</body>
</html>
main.js
main.js
// window.__SKYWAY_KEY__ = 'SkyWayのAPI Keyを入れてね';

let localStream;
const myVideo = document.getElementById('my-video');
myVideo.onloadeddata = () => {
    setInterval(() => {
        if(isMosaicEnabled){
            mosaicMyCvs();
        }else{
            copyToMyCvs();
        }
    }, 1000 / 30);
}

const theirVideo = document.getElementById('their-video');

const CVS_WIDTH = 360
const CVS_HEIGHT = 270

const myCvs = document.getElementById('my-canvas');
const myCtx = myCvs.getContext('2d');
myCvs.width = CVS_WIDTH;
myCvs.height = CVS_HEIGHT;
myCvs.style.width = `${CVS_WIDTH}px`;
myCvs.style.height = `${CVS_HEIGHT}px`;

navigator.mediaDevices.getUserMedia({ video:{ width: CVS_WIDTH , height: CVS_HEIGHT }, audio: true })
    .then(stream => {
        myVideo.srcObject = stream;
        myVideo.play();
        localStream = myCvs.captureStream(30);
    }).catch(error => {
        console.error('mediaDevice.getUserMedia() error:', error);
});

const MOSAIC_SIZE = 20;
const mosaicMyCvs = () => {
    myCtx.clearRect(0, 0, CVS_WIDTH, CVS_HEIGHT);
    const mosaicCvs = document.createElement('canvas');
    const mosaicCtx = mosaicCvs.getContext('2d');
    mosaicCvs.width = CVS_WIDTH ;
    mosaicCvs.height = CVS_HEIGHT ;
    mosaicCvs.style.width = `${CVS_WIDTH}px`;
    mosaicCvs.style.height = `${CVS_HEIGHT}px`;
    mosaicCtx.drawImage(myVideo, 0, 0);
    const imageData = mosaicCtx.getImageData(0, 0, CVS_WIDTH, CVS_HEIGHT);
    //モザイクサイズ単位でループ
    for (let y = 0; y < myCvs.height; y = y + MOSAIC_SIZE) {
        for (let x = 0; x < myCvs.width; x = x + MOSAIC_SIZE) {
            // 該当ピクセルの色情報を取得
            const cR = imageData.data[(y * myCvs.width + x) * 4];
            const cG = imageData.data[(y * myCvs.width + x) * 4 + 1];
            const cB = imageData.data[(y * myCvs.width + x) * 4 + 2];
            // モザイクサイズの正方形を描画
            myCtx.fillStyle = `rgb(${cR},${cG},${cB})`;
            myCtx.fillRect(x, y, x + MOSAIC_SIZE, y + MOSAIC_SIZE);
        }
    }
}

const copyToMyCvs = () => {
    myCtx.drawImage(myVideo, 0, 0, CVS_WIDTH, CVS_HEIGHT);
}

let isMosaicEnabled = false;
document.getElementById('mosaic-btn').onclick = () => {
    if (isMosaicEnabled) {
        isMosaicEnabled = false;
        document.getElementById('mosaic-enabled').textContent = "OFF";
    } else {
        isMosaicEnabled = true;
        document.getElementById('mosaic-enabled').textContent = "ON";
    }
}

const peer = new Peer({
    key: window.__SKYWAY_KEY__,
    debug: 3
});

peer.on('open', () => {
    document.getElementById('my-id').textContent = peer.id;
});

document.getElementById('call-btn').onclick = () => {
    const theirID = document.getElementById('their-id').value;
    const mediaConnection = peer.call(theirID, localStream);
    setEventListener(mediaConnection);
};

const setEventListener = mediaConnection => {
    mediaConnection.on('stream', stream => {
        theirVideo.srcObject = stream;
        theirVideo.play();
    });
}

peer.on('call', mediaConnection => {
    mediaConnection.answer(localStream);
    setEventListener(mediaConnection);
});

実は…

加工した映像をSkyWayを通じて送れた!
と喜んでいたものの、後日、ある事実に気付いて悩むことになります。

ウサギーは何を見落としていたのでしょうか?

おわりに

なにやら不穏な終わり方をしてしまいましたが、
カメラ映像を送る」と言う観点であれば、上記のコードは決して間違いではありません。
ヒントは”audio”です。

こちらも次の記事にしようと思いますので、お付き合いいただけると嬉しいです。

2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?