先日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型と書かれているがオブジェクトを設定することができる。
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())
{
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を送信することができる。
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. マスク映像を送信
勉強会で顔にマスクするサンプルが紹介された。
紹介された方法は、にをオーバレイする方法となっているためを非表示にするとマスクが外れてしまう。
また、送信する映像はマスク前の映像である。
3.により生のMediaStreamが扱えるので、これをに映像とマスクともに描画し、をキャプチャーしたストリームを
送信することで、送信する映像もマスクされた映像にするように改造してみた。
<!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>
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サーバーとのネゴシエーションを行うことでサブスクライブするようである。