はじめに
株式会社マイスター・ギルド新規事業部のウサギーです。
弊社新規事業部では、新規サービスの立ち上げを目指して
日々、アイディアの検証やプロトタイプの作成を行っています。
今回は通信アプリに必須の機能、「スピーカーON/OFF」と「マイクON/OFF」を作ります!
このプログラムでは通信にSkyWayを使っていますが、記事で紹介していることは汎用的なものです。
SkyWayでの1:1通信が気になる!って方は過去記事もご参照ください。
前提
「スピーカーON/OFF」と「マイクON/OFF」と言っていますが
正確には「音声データの出力のON/OFF」と「音声データの入力のON/OFF」をする機能を作ります。
そのため、ハードデバイスの制御ではなくソフト部分の制御になります。
スピーカーON/OFF
「音を出す/音を出さない」をユーザーが任意で切り替えられるつくりにしようと思います。
前述の通り、ハードデバイスではなく、ソフト側で「音声データの出力」を制御して実現します。
音声情報を持っているHTML要素は<video>要素です。
やることは3つです。
1.UIの作成
2.状態管理変数の切り替え
3.状態管理変数に合わせたaudioの制御
1.UIの作成
スピーカーボタンを作ります。
<div id="my-setting" >
<input type="button" id="mosaic-btn" value="モザイク">
<span id="mosaic-enabled">OFF</span>
+ <input type="button" id="speaker-btn" value="○スピーカー" >
</div>
2.状態管理変数の切り替え
スピーカーの状態を管理する変数を用意します。
(初期値はON=trueにしました)
let isSpeakerEnabled = true;
1.で作ったボタンを使って、状態を切り替えます。
スピーカーボタンのonclickプロパティに、アロー関数を使ってイベントハンドラを登録します。
スピーカーボタンに表示される文字列もついでに変更します。
document.getElementById('speaker-btn').onclick = () => {
if (isSpeakerEnabled) {
isSpeakerEnabled = false;
document.getElementById('speaker-btn').value = "×スピーカー";
} else {
isSpeakerEnabled = true;
document.getElementById('speaker-btn').value = "○スピーカー";
}
console.log(`押したから変わったよ:isSpeakerEnabled=${isSpeakerEnabled}` );
}
3.状態管理変数に合わせたaudioの制御
<video>要素の音声を制御します。音をなくすには、
・volumeを0にする
・mutedを有効にする
の2パターンが考えられますが、今回はmutedを使います。
mutedを選んだ理由はtrue/falseで状態管理できるからです!
const theirVideo = document.getElementById('their-video');
:
:
document.getElementById('speaker-btn').onclick = () => {
if (isSpeakerEnabled) {
isSpeakerEnabled = false;
document.getElementById('speaker-btn').value = "×スピーカー";
} else {
isSpeakerEnabled = true;
document.getElementById('speaker-btn').value = "○スピーカー";
}
console.log(`押したから変わったよ:isSpeakerEnabled=${isSpeakerEnabled}` );
+ theirVideo.muted = !(isSpeakerEnabled);
}
2.で作成したイベントハンドラの中で、<video>要素のmutedを制御します。
isSpeakerEnabledがtrue、すなわち音声を出力するときmutedをfalseに、
isSpeakerEnabledがfalse、すなわちミュートにするときmutedをtrueにします。
2.でスピーカーの初期状態はON(isSpeakerEnabled = true)にしたので、HTMLの方でもmutedを解除しておきます。
<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>
+ <video id="their-video" width="360px" height="270px" autoplay playsinline></video>
</div>
これで音声出力のON/OFFが切り替えられるようになりました。
マイクON/OFF
マイクの方も同様に作っていきます!
1.UIの作成
2.状態管理変数の切り替え
3.状態管理変数に合わせたaudioの制御
1.UIの作成
マイクボタンを作ります。
<div id="my-setting" >
<input type="button" id="mosaic-btn" value="モザイク">
<span id="mosaic-enabled">OFF</span>
<input type="button" id="speaker-btn" value="○スピーカー" >
+ <input type="button" id="mike-btn" value="×マイク" >
</div>
2.状態管理変数の切り替え
こちらもスピーカーと同様に作ります。
マイクの方は初期値はOFF=falseにしました。
let isMikeEnabled = false;
document.getElementById('mike-btn').onclick = () => {
if (isMikeEnabled) {
isMikeEnabled = false;
document.getElementById('mike-btn').value = "×マイク";
} else {
isMikeEnabled = true;
document.getElementById('mike-btn').value = "○マイク";
}
console.log(`押したから変わったよ:isMikeEnabled=${isMikeEnabled}` );
}
}
コンソールログで、狙った挙動になっていることが確認できました。
3.状態管理変数に合わせたaudioの制御
送信前の音声情報を持っているのはstreamです。
先日の記事 ↓ で使った、getAudioTracks()を使います。
let localStream;
:
:
document.getElementById('mike-btn').onclick = () => {
if (isMikeEnabled) {
isMikeEnabled = false;
document.getElementById('mike-btn').value = "×マイク";
} else {
isMikeEnabled = true;
document.getElementById('mike-btn').value = "○マイク";
}
console.log(`押したから変わったよ:isMikeEnabled=${isMikeEnabled}` );
+ localStream.getAudioTracks()[0].enabled = isMikeEnabled;
}
2.で作成したイベントハンドラの中で、audioTrackの有効/無効を切り替えています。
これでマイクON/OFF機能もできました!
コード全体
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:2;
}
</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>
<input type="button" id="speaker-btn" value="○スピーカー" >
<input type="button" id="mike-btn" value="×マイク" >
</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 playsinline></video>
</div>
</div>
<script src="//cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
<script src="./main.js"></script>
</body>
</html>
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);
localStream.addTrack(stream.getAudioTracks()[0]);
}).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);
});
let isSpeakerEnabled = true;
document.getElementById('speaker-btn').onclick = () => {
if (isSpeakerEnabled) {
isSpeakerEnabled = false;
document.getElementById('speaker-btn').value = "×スピーカー";
} else {
isSpeakerEnabled = true;
document.getElementById('speaker-btn').value = "○スピーカー";
}
console.log(`押したから変わったよ:isSpeakerEnabled=${isSpeakerEnabled}` );
theirVideo.muted = !(isSpeakerEnabled);
}
let isMikeEnabled = false;
document.getElementById('mike-btn').onclick = () => {
if (isMikeEnabled) {
isMikeEnabled = false;
document.getElementById('mike-btn').value = "×マイク";
} else {
isMikeEnabled = true;
document.getElementById('mike-btn').value = "○マイク";
}
console.log(`押したから変わったよ:isMikeEnabled=${isMikeEnabled}` );
localStream.getAudioTracks()[0].enabled = isMikeEnabled;
}
おわりに
今回でSkyWayを使った1:1通信のアプリについては一旦おしまいです。
お付き合いいただきありがとうございました