この記事は「マイスター・ギルド:暑中見舞!夏のアドベントカレンダー2020」3日目の記事です。
初めに...
コロナの流行が始まったとき、「Stay Home」対策で自宅に閉じ込められたとき、ITワーカーの私たちは特に自宅からリモートで仕事をすることが可能でラッキーでした。
残念ながら、私たちのほとんどはリモートでの作業に慣れておらず、最初は上司、同僚、お客さん等とリモート通信が特に困難でした。
特殊なツールを使用しても改善されましたが、同じオフィスで作業するほど自然ではありません。
弊社のMeister Guildでもその新しい作業環境に答えるツールを探して色々なツールを使ってみた:
- Zoom:ヴァーチャル背景を使える
- Discord:完全に無料
- Remo:ヴァーチャルルームに入れる、共有ホワイトボードもある
- Spatial Chat:距離によると声の高さが変わる
ビデオ会議ができるツールはほかにもあります:
各ツールが得点と弱点を持つけど「これ!」ってなるツールがなかったので「私たちの理想なビデオ会議のツール作れるかな?」と思って調査することになりました。
ビデオ会議のツール作りの調査
そのようなツールを一から開発するのはとても大変な仕事になるので、開発をスピードアップするWebRTCフレームワークを探しました。
日本製で無料プランあり、NTTコミュニケーションズが作成したWebRTCフレームワークを見つけました:
ユーザー認証をテストするために、認証付きのLaravelアプリケーションを作成し、ユーザーのメールをbase64でエンコーディングしてPeerIDとして使用しました。
SkyWayとは
ホームページによるとSkyWayは:
ビデオ・音声通話の機能をアプリケーションに簡単に実装できる、
マルチプラットフォームなSDK & フルマネージドなAPIサービスです。
無料プランで下記のSDKを使える:
- Javascript SDK
- iOS SDK
- Android SDK
- WebRTC Gateway
- APIキー認証
有料プランで録音SDKと管理APIも使えるんですが例えば録音も録画も普通のMedia Capture and Streams API (Media Streams)でできる。
Javascriptサンプル:
ビデオ会議
P2Pビデオ通話
P2Pテキスト通話
録音と録画
P2Pビデオ通話
通話の相手は一人です。
基本
CDNからSDKをインポートする:
<script src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
カメラ映像を表示するvideo要素を追加する:
<video id="my-video" width="400px" autoplay muted playsinline></video>
カメラ映像・マイク音声を取得する:
let localStream;
// カメラ映像取得
navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then( stream => {
// 成功時にvideo要素にカメラ映像をセットし、再生
const videoElm = document.getElementById('my-video')
videoElm.srcObject = stream;
videoElm.play();
// 着信時に相手にカメラ映像を返せるように、グローバル変数に保存しておく
localStream = stream;
}).catch( error => {
// 失敗時にはエラーログを出力
console.error('mediaDevice.getUserMedia() error:', error);
return;
});
通話の相手のは「peer」と呼ばれてる。
通話出来るように自分のPeerオブジェクトを作成して相手のPeerオブジェクトと繋がる。
Peerオブジェクトの作成
Peerオブジェクトを作成するときに引数のIDを渡さない場合はランダムなIDが生成される:
const peer = new Peer({
key: '<SkyWayのAPIキー>',
debug: 3
});
PeerオブジェクトのIDはpeer.id
で取得できる。
またはメールアドレスなどからIDを生成できる。
例えばLaravelのコントローラーでbase64
にエンコード:
public function index()
{
$user = Auth::user();
return view('videochat',['user'=>['email'=>rtrim(base64_encode($user->email),"=")]]);
}
ページでPeerオブジェクトに渡す:
const peer = new Peer('{{$user['email']}}',{
key: '<SkyWayのAPIキー>',
debug: 3
});
発信
相手のカメラ映像を表示するvideo要素を追加する:
<video id="their-video" width="400px" autoplay muted playsinline></video>
相手へ発信してリスナーで接続することを待つ:
// 発信処理
const mediaConnection = peer.call('<相手のPeerID>', localStream);
setEventListener(mediaConnection);
接続ができたときにビデオ要素を設定する:
let remoteStream;
// イベントリスナを設置する関数
const setEventListener = mediaConnection => {
mediaConnection.on('stream', stream => {
// video要素にカメラ映像をセットして再生
const videoElm = document.getElementById('their-video')
videoElm.srcObject = stream;
remoteStream = stream;
videoElm.play();
});
}
着信
相手側はPeerオブジェクトのcallイベントを待って着信の時ビデオ要素を設定する:
//着信処理
peer.on('call', mediaConnection => {
mediaConnection.answer(localStream);
setEventListener(mediaConnection);
});
映像・音声はオン・オフ等
マイク音声オフ
ミュートする:
localStream.getAudioTracks().forEach(track => track.enabled = false);
音声オフ
相手の音声を削音する:
remoteStream.getAudioTracks().forEach(track => track.enabled = false);
※ 音全部消したいときマイク音声もオフしなければならない。
カメラ映像オフ
カメラの映像を消す:
localStream.getVideoTracks().forEach(track => track.enabled = false);
反響キャンセリング
反響を消す:
localStream.getAudioTracks().forEach(track => {
let constraints = track.getConstraints();
constraints.echoCancellation = true;
track.applyConstraints(constraints);
});
ビデオ会議
ビデオ会議はビデオ通話との違いは2つ:
- 相手の数は一人以上になる
- roomオブジェクトで他のユーザーの存在(presence)が確認できる
基本
CDNからSDKをインポートする:
<script src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
カメラ映像を表示するvideo要素を追加する:
<video id="js-local-stream"></video>
カメラ映像・マイク音声を取得する:
// カメラ映像取得
const localStream = await navigator.mediaDevices
.getUserMedia({
audio: true,
video: true,
})
.catch(console.error);
// Render local stream
const localVideo = document.getElementById('js-local-stream');
localVideo.muted = true;
localVideo.srcObject = localStream;
localVideo.playsInline = true;
await localVideo.play().catch(console.error);
相手達のカメラ映像を表示する要素を追加する:
<div class="remote-streams" id="js-remote-streams"></div>
Peerオブジェクトの作成
PeerオブジェクトのIDを生成する。
例えばLaravelのコントローラーでbase64
にエンコード:
public function index()
{
$user = Auth::user();
return view('videochat',['user'=>['email'=>rtrim(base64_encode($user->email),"=")]]);
}
ページでPeerオブジェクトに渡す:
const peer = new Peer('{{$user['email']}}',{
key: '<SkyWayのAPIキー>',
debug: 3
});
roomオブジェクト
Peerオブジェクトが生成された後でroomに参加する:
peer.on('open', () => {
const room = peer.joinRoom('test', {
mode: 'sfu',
stream: localStream,
});
});
※ roomは2つのタイプがある:'sfu'
(通信がサーバーを通す)と'mesh'
(通信が直接にPeerへ発信する)。
roomのイベント
open
roomに入ったとき:
room.once('open', () => {
...
});
close
roomを出たとき:
room.once('close', () => {
// テキスト通信を止める
sendTrigger.removeEventListener('click', onClickSend);
// 相手達のビデオストリームを止める
Array.from(remoteVideos.children).forEach(remoteVideo => {
remoteVideo.srcObject.getTracks().forEach(track => track.stop());
remoteVideo.srcObject = null;
remoteVideo.remove();
});
});
peerJoin
一人がroomに入ったとき:
room.on('peerJoin', peerId => {
...
});
peerLeave
一人がroomを出たとき:
room.on('peerLeave', peerId => {
// ストリームを閉じてvideo要素を消す
const remoteVideo = remoteVideos.querySelector(
`[data-peer-id=${peerId}]`
);
remoteVideo.srcObject.getTracks().forEach(track => track.stop());
remoteVideo.srcObject = null;
remoteVideo.remove();
});
stream
roomに入った一人のストリームを表示:
room.on('stream', async stream => {
// video要素を生成
const newVideo = document.createElement('video');
newVideo.srcObject = stream;
newVideo.playsInline = true;
// peerLeaveイベントのときにストリームを見つけるためにpeerIdを付ける
newVideo.setAttribute('data-peer-id', stream.peerId);
remoteVideos.append(newVideo);
await newVideo.play().catch(console.error);
});
data
メッセージを着信したとき:
room.on('data', ({ data, src }) => {
// メッセージと発信者を表示
messages.textContent += `${src}: ${data}\n`;
});
テキスト発信
メッセージを発信するとき:
sendTrigger.addEventListener('click', onClickSend);
function onClickSend() {
// websocketでroomの皆さんにメッセージを起こる
room.send(localText.value);
// メッセージと発信者を表示
messages.textContent += `${peer.id}: ${localText.value}\n`;
// インプットを消す
localText.value = '';
}
});
映像・音声はオン・オフ等
マイク音声オフ
ミュートする:
localStream.getAudioTracks().forEach(track => track.enabled = !audioInStatus);
音声オフ
相手の音声を削音する:
remoteVideos.forEach(video => {
video.srcObject.getAudioTracks().forEach(track => track.enabled = !audioOutStatus);
});
※ 音全部消したいときマイク音声もオフしなければならない。
カメラ映像オフ
カメラの映像を消す:
localStream.getVideoTracks().forEach(track => track.enabled = false);
反響キャンセリング
反響を消す:
localStream.getAudioTracks().forEach(track => {
let constraints = track.getConstraints();
constraints.echoCancellation = true;
track.applyConstraints(constraints);
});
音声通話
navigator.mediaDevices.getUserMedia()
の引数でメディアのタイプ(画像・音声・両方)等を選択できる:
// カメラ映像取得
const localStream = await navigator.mediaDevices
.getUserMedia({
audio: true,
video: false,
})
画面共有
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
このメディアストリームを使って画面共有機能が実現できる。
終わりに...
WebRTCを使用できるのでウェブアプリケーションの元に使用できるフレームワークと思いました。
その調査をして私の理想なリモートワークのツールについていっぱいなアイデアが生まれて社長が本気で開発始めようのは本気になって欲しいです。