agora.io 



先日agora.ioの勉強会があった。
agora.ioのSDKはちょっと高レベルな実装となっており、素のWebRTC APIの知見を持っていると、当然ながら素のWebRTC API オブジェクトも扱いたいという場面も出てくる。
軽くのぞいてみての雑感。

# 1. navigator.mediaDevices.getUserMediaは使われない
SDKを素直に使うと、navigator.mediaDevices.getUserMediaは使われず、navigator.webkitGetUserMeidia/navigator.mozGetUserMediaが使用される。なんだかなぁ。。。
# 2. SDK経由でgUM()のconstraintsは渡せる
一応、完全ではないがSDK経由でgUM()にconstraintsを渡すことは可能。
ドキュメントにおいてはAgoraRTC.createStream()にわたすオブジェクトの、audio/videoはbool型と書かれているがオブジェクトを設定することができる。

```js
localStream = AgoraRTC.createStream({
    streamID: uid,
    audio: true,
    video: {
        width:{
            min:300, 
            max:600, 
            exact:500,
            ideal:600,
        }, 
        height:{
            min:200, 
            max:600
        }
    },
    attributes: {
        minFrameRate: 30,
        maxFrameRate: 60
    }
});
```
これを実行すると旧仕様のconstraintsに変換されてgUM()が実行される
(実際にはChromeの場合はwebkitGetUserMedia()、Firefoxの場合はmozGetUserMedia())

```js:変換されたconstraints
{
    video: {
        mandatory: {
            minWidth: 300,
            maxWidth: 600,
            minHeight: 200,
            maxHeight: 600,
            maxFrameRate: 60
        },
        optional: [
            {
                minFrameRate: 30
            },
            {
                maxFrameRate: 30
            }
        ]
    },
    audio: true
}
```
# 3. 生のMediaStreamを使う
余計にgUM()を実行することにはなるが、localStreamオブジェクト(agoraのストリームオブジェクト)を生成してinit()をした後に、streamプロパティの中身を入れ替えることによりそのMediaStreamを送信することができる。

```js
async getLocalStream() {
    const stream = await navigator.mediaDevices.getUserMedia({ video: true });
    localStream = AgoraRTC.createStream({
        streamID: uid,
        audio: true,
        video: true
    });
    localStream.init(_ => {
        // SDK内で生成されたstreamを解放
        localStream.stream.getTracks(track => track.stop());
        delete localStream.stream;
        // streamプロパティにMediaStreamオブジェクトをセット
        localStream.stream = stream;
    });
}
```

# 4. マスク映像を送信
勉強会で顔にマスクするサンプルが紹介された。
紹介された方法は、<video>に<canvas>をオーバレイする方法となっているため<canvas>を非表示にするとマスクが外れてしまう。
また、送信する映像はマスク前の映像である。
3.により生のMediaStreamが扱えるので、これを<canvas>に映像とマスクともに描画し、<canvas>をキャプチャーしたストリームを
送信することで、送信する映像もマスクされた映像にするように改造してみた。

```html:html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>agora</title>
</head>
<body>
    <script src="model_pca_20_svm.js"></script>
    <script src="clmtrackr.min.js"></script>
    <script src="AgoraRTCSDK-2.4.1.js"></script>
    <script src="main.js"></script>
</body>
</html>
```

```js:js
const appId = 'agora.ioのAPP ID';
let tracker = new clm.tracker();
let localStream = null;
let roomId = 'hogefugapiyo';
let userId = null;
const cnv = document.createElement('canvas');
let ctx = null;
let maskedStream = null;
let client = null;

// MediaStreamを取得
navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
    myVideo = document.createElement('video');
    myVideo.id = 'myVideo';
    myVideo.onloadedmetadata = evt => {
        initFaceTracker();
    };
    myVideo.srcObject = stream;
    myVideo.play();
    document.body.appendChild(myVideo);
});

// トラッカー初期化
function initFaceTracker() {
    cnv.style.width = `320px`;
    cnv.style.height = `${320 * myVideo.videoHeight / myVideo.videoWidth}px`;
    cnv.width = myVideo.width = myVideo.videoWidth;
    cnv.height = myVideo.height = myVideo.videoHeight;
    ctx = cnv.getContext('2d');
    // <canvas>をキャプチャーしてマスクされた映像(MediaStream)を取得
    maskedStream = cnv.captureStream(30);
    tracker.init(pModel);
    tracker.start(myVideo);
    faceTrack();
    initClient();
}

// agora.ioのclient初期化
function initClient() {
    client = AgoraRTC.createClient({ mode: 'live', codec: 'h264' });

    client.init(appId, _ => {
        client.join('', roomId, userId, uid => {
            console.log(`room joined. roomId:${roomId}, userId:${uid}`);
            localStream = AgoraRTC.createStream({
                streamID: uid,
                audio: true,
                video: true,
            });
            localStream.init(_ => {
                // SDK内で取得したストリームを解放
                localStream.stream.getTracks().forEach(track => track.stop());
                delete localStream.stream;
                // マスクストリームをセット
                localStream.stream = maskedStream;
                client.publish(localStream, err => { });
            }, err => { });
        });
    });

    client.on('peer-leave', evt => {
        evt.stream && evt.stream.stop();
    });

    client.on('stream-added', evt => {
        client.subscribe(evt.stream, err => { });
    });

    client.on('stream-subscribed', evt => {
        const vid = document.createElement('video');
        vid.srcObject = evt.stream.stream;
        document.body.appendChild(vid);
        vid.play();
    });
}

// Faceトラッキングのフレーム処理
function faceTrack() {
    requestAnimationFrame(faceTrack);
    const data = tracker.getCurrentPosition();
    if (data) {
        // console.dirxml(data)
        const x = data[1][0];
        const y = data[20][1];
        const w = data[13][0] - data[1][0];
        const h = data[7][1] - data[20][1];
        // canvasにマスクした映像を描画
        ctx.drawImage(myVideo, 0, 0);
        ctx.fillRect(x, y, w, h);
    }
}

```
マスク座標が取得された場合のみ描画を行うため、少々フレームレートが落ちるという欠点がある。

# あとがき
これ以外にも、client.subscribe()というのがあり、単純に送受信ではなく送られてくるストリームの取捨選択が行える機能があり、これも調べようと思ったが、めんどくさそうなのであきらめた。
どうやら、改めてSFUサーバーとのネゴシエーションを行うことでサブスクライブするようである。