LoginSignup
13
9

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-09-01

image.png

はじめに

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

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

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

前提

「スピーカーON/OFF」と「マイクON/OFF」と言っていますが
正確には「音声データの出力のON/OFF」と「音声データの入力のON/OFF」をする機能を作ります。
そのため、ハードデバイスの制御ではなくソフト部分の制御になります。
image.png

スピーカーON/OFF

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

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

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

1.UIの作成

スピーカーボタンを作ります。

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

2.状態管理変数の切り替え

スピーカーの状態を管理する変数を用意します。
(初期値はON=trueにしました)

main.js
let isSpeakerEnabled = true;

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

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  image.png
切り替えできました!

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

<video>要素の音声を制御します。音をなくすには、
・volumeを0にする
・mutedを有効にする
の2パターンが考えられますが、今回はmutedを使います。
mutedを選んだ理由はtrue/falseで状態管理できるからです!

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を解除しておきます。

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の作成

マイクボタンを作ります。

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

2.状態管理変数の切り替え

こちらもスピーカーと同様に作ります。
マイクの方は初期値はOFF=falseにしました。

main.js
let isMikeEnabled = false;
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
コンソールログで、狙った挙動になっていることが確認できました。

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

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

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機能もできました!

コード全体

index.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>
main.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;
}

おわりに

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

13
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
9