【新規事業】SkyWay1：1通信でスピーカーON/OFFとマイクミュート【プロト開発】 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/b6d31427-3ef7-8a1a-d68e-896254c9be43.png)

# はじめに
株式会社マイスター・ギルド新規事業部のウサギーです。
弊社新規事業部では、新規サービスの立ち上げを目指して
日々、アイディアの検証やプロトタイプの作成を行っています。

今回は通信アプリに必須の機能、「スピーカーON/OFF」と「マイクON/OFF」を作ります！
このプログラムでは通信にSkyWayを使っていますが、記事で紹介していることは汎用的なものです。

SkyWayでの１：１通信が気になる！って方は過去記事もご参照ください。

https://qiita.com/Shinkijigyo_no_Usagi/items/1d4e1cccbb6de9aa4526


# 前提
「スピーカーON/OFF」と「マイクON/OFF」と言っていますが
正確には「音声データの出力のON/OFF」と「音声データの入力のON/OFF」をする機能を作ります。
そのため、ハードデバイスの制御ではなくソフト部分の制御になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/efaad33d-3c78-586d-b140-6177dfc3bd7f.png)

# スピーカーON/OFF
「音を出す/音を出さない」をユーザーが任意で切り替えられるつくりにしようと思います。
前述の通り、ハードデバイスではなく、ソフト側で「音声データの出力」を制御して実現します。

音声情報を持っているHTML要素は＜video＞要素です。

やることは３つです。
1．UIの作成
2．状態管理変数の切り替え
3．状態管理変数に合わせたaudioの制御

#### 1．UIの作成
スピーカーボタンを作ります。

```diff_HTML:index.hmtl
<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>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/c3ae30c9-ae9e-daae-8477-7705aaff21b8.png)


#### 2．状態管理変数の切り替え
スピーカーの状態を管理する変数を用意します。
（初期値はON＝trueにしました）

```diff_JavaScript:main.js
let isSpeakerEnabled = true;
```

1.で作ったボタンを使って、状態を切り替えます。
スピーカーボタンのonclickプロパティに、アロー関数を使ってイベントハンドラを登録します。
スピーカーボタンに表示される文字列もついでに変更します。


```diff_JavaScript:main.js
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}` );
}
```
![Zdz2YwAei82iDsnL6FV21659406120-1659406181.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/096a916e-be92-7aa1-1152-9b5abd02ea9f.gif)　　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/1f25149d-0137-5d0e-91a0-0875f9388930.png)
切り替えできました！


#### 3．状態管理変数に合わせたaudioの制御
＜video＞要素の音声を制御します。音をなくすには、
・volumeを0にする
・mutedを有効にする
の2パターンが考えられますが、今回はmutedを使います。
mutedを選んだ理由はtrue/falseで状態管理できるからです！

```diff_JavaScript:main.js
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を解除しておきます。
```diff_HTML:index.hmtl
<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の作成
マイクボタンを作ります。

```diff_HTML:index.hmtl
<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>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/189b7070-f497-b9c8-40f7-c3b3f3b5fa90.png)


#### 2．状態管理変数の切り替え
こちらもスピーカーと同様に作ります。
マイクの方は初期値はOFF＝falseにしました。

```diff_JavaScript:main.js
let isMikeEnabled = false;
```

```diff_JavaScript:main.js
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}` );
}
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/08129a75-ff51-6a5e-3996-31873fedf5ba.png)
コンソールログで、狙った挙動になっていることが確認できました。

#### 3．状態管理変数に合わせたaudioの制御

送信前の音声情報を持っているのはstreamです。
先日の記事 ↓ で使った、getAudioTracks()を使います。


https://qiita.com/Shinkijigyo_no_Usagi/items/b2c9ae811cee6a68ac81

```diff_JavaScript:main.js
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機能もできました！

# コード全体
<details><summary>index.html</summary><div>

```HTML: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>
```
</div></details>
<details><summary>main.js</summary><div>

```js: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;
}
```
</div></details>

# おわりに
今回でSkyWayを使った1:1通信のアプリについては一旦おしまいです。
お付き合いいただきありがとうございました:rabbit: