見出し画像

FirebaseRTCサンプルコードを動かしてみた③

前回の記事

でGoogle様の提供してくれているFirebase + node.jsで動くWebRTCサンプルコードを修正し、1対1の通信だけでなく複数人でのビデオ通信ができるようにしました。

こちら、さらに機能を追加し

・マイクのON/OFF機能

画面共有機能

room参加者に応じてビデオ画面を動的に増減

といったことをできるようにしました。ソースコードは以下のブランチにあります。


アプリケーションは相変わらずFirebaseにデプロイしております。試しに触ってみたい方はどうぞ!

ちなみに、今回追加した機能を使ってビデオチャットする時の様子は以下のようになります。

「OPEN CAMERA & MICROPHONE」からカメラと音声入力を許可。

画像2


「CREATE ROOM」を押下するとROOM IDが表示されるので、別端末から(今回は手持ちのipadから)同様にカメラを有効化し、「JOIN ROOM」ボタンを押下してROOM IDを入力します。ちなみに、ROOM IDはfirebaseのドキュメント作成時に生成されるランダムな文字列を利用しています。

画像4-1


そうすると、両端末でビデオ通話が開始されます。(3台目、4台目と「JOIN ROOM」していくとビデオ画面が自動で追加されていきます)

画像5-1


画面共有する場合は「SHARE SCREEN」ボタンを押下します。下の画像では、PC側のvscode画面をiPad側へ共有しています。iPad側のカメラ画像は見えたままです。

※ipadのsafari側からの画面共有機能はうまく動作しませんでした。

画像6-1


最後に、「HANGUP」で通話を終了します。複数人通信してる場合、通話を終了した端末のビデオ画面が、他の端末のブラウザから消えるようになっています。

いかがでしたでしょうか?フロントエンドを全く触ったことがない私でしたが、WebRTC関連の記事はネットに充実してますので、自力でサンプルコードに機能追加することができました。かつ、ブラウザで実際に動くものがつくれるので楽しみながら学ぶことが出来ました!

今回追加した機能は、仕様さえわかれば簡単なコード修正で機能追加できました。しかしながら、正しい仕様をネットで発見するまでに苦労したため、該当部分のコードを解説します。

画面共有

async function shareScreen(e) {
	document.querySelector('#shareScreenBtn').disabled = true;

このshareScreen関数は「SHARE SCREEN」ボタンが押下された時に呼ばれるため、まず「SHARE SCREEN」ボタンを無効化します。


	await navigator.mediaDevices.getDisplayMedia(
		{video: true})
	.then(async stream => {
		document.querySelector('#localVideo').srcObject = stream;

		// スクリーンストリームからビデオトラックを取り出す
		let screenTrack = stream.getVideoTracks()[0];

その後、navigator.mediaDevices.getDisplayMedia( )で取得した画面のストリーム情報を自身のビデオ画面に表示できるようにします。

この画面情報を通信相手全員に送る必要があるため、ストリーム配列からビデオトラックを取り出します。



		// 画面共有からの入力に切り替える
		Object.keys(peerConnections).forEach(id => {
			// peer connectionから送信側のRTCRtpTransceiverを取り出す
			let sender = peerConnections[id].getSenders().find(sdr => {
			  return sdr.track.kind == screenTrack.kind;
			});
			sender.replaceTrack(screenTrack);
		});

本コードでは全ての通信相手の情報はpeerConnections[]という連想配列でもっているため、forEach文のなかで各コネクションからRTCRtpTransceiver情報を取得し、replaceTrackを呼び出すことで、相手に送信しているトラック情報をカメラの情報から画面共有の情報に入れ替えます。

ここまでで、相手のビデオ画面にこちらの画面共有が反映されます。


		// 画面共有を終了したらカメラからの入力に戻す
		stream.getVideoTracks()[0].addEventListener('ended', () => {
			document.querySelector('#localVideo').srcObject = localStream;
			let localStreamTrack = localStream.getVideoTracks()[0];
			Object.keys(peerConnections).forEach(id => {
				let sender = peerConnections[id].getSenders().find(sdr => {
					return sdr.track.kind == localStreamTrack.kind;
				});
				sender.replaceTrack(localStreamTrack);
			});
			document.querySelector('#shareScreenBtn').disabled = false;
		});

画面共有を終了したらカメラからの入力情報に再び切り替えるため、ビデオトラックの'ended'イベントをリッスンします。

以上です。はじめはこのreplaceTrack()機能があるとは知らず、一度コネクションを切ってから再接続するようなフローを考えていたのですが、実装がややこしくなって諦めました。。


room参加者に応じてビデオ画面を動的に増減

続いてこちらです。

async function onRemoteStream(userId) {
	// ストリーミングが発生した場合、videoタグを追加する
	peerConnections[userId].addEventListener('track', event => {
		if (!document.querySelector(`#remoteVideo-${userId}`)) {
			remoteStream = new MediaStream();
			event.streams[0].getTracks().forEach(track => {
			  remoteStream.addTrack(track);
			});

このonRemoteStream()関数では、roomへの入退室の際にほかのメンバと通信を確立した後のtrackイベントを監視します。

相手のトラックを検知した場合、remoteStreamオブジェクトにトラックを追加します。


			let remoteVideo = document.createElement('video');
			remoteVideo.autoplay = true;
			remoteVideo.setAttribute('playsinline', '');
			remoteVideo.id = `remoteVideo-${userId}`;
			remoteVideo.srcObject = remoteStream;
			document.getElementById('remoteVideos').appendChild(remoteVideo);
		}
 });

その後、createElementでvideoタグを新規作成し、htmlファイル内のremoteVideosタグの子タグとしてvideoタグを追加することで動的にブラウザにビデオ画面を追加しています。

※ビデオ画面のサイズや配置は今のところテキトーです。そのうち綺麗に配置できるよう修正したいです。


	// ストリーミングが停止した場合、videoタグを削除する
	peerConnections[userId].oniceconnectionstatechange = function() {
		if (peerConnections[userId].iceConnectionState == 'disconnected') {
			const video = document.querySelector(`#remoteVideo-${userId}`);
			video.remove();
		}
 }
}

最後に、通信相手の誰かがHANGUPした場合、そのビデオ画面をブラウザから削除する処理です。

この相手側の通信断の検知方法がググっても最初はうまく見つからなかったのですが、結果的にoniceconnecitonstatechangeでコネクションを監視し、disconnected状態を検知した際にvideoタグを削除すればうまくいきました。

通信を切断する相手側は、peerConnection.close()を呼ぶだけでOKです。


よろしければサポートお願いします!頂いたサポート費は、執筆活動に使わせて頂きます。