SkyWayを使って爆速で見守りアプリを作ってみた

## 概要
SkyWayを用いて爆速で見守りアプリでを作ってみました。

![ 2023-07-20 8.07.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487820/a463e576-5b01-f70e-f97d-1d0cb2f66c47.png)

想定する対象は以下です。

:::note 
お家にいる子供やペットの様子を確認したい方。簡単にSkyWayを使ってみたい方。
:::

注意点として、本アプリはあくまで簡易版です。
見守りアプリの不備によって、万が一のことがあってはいけないのでその点についてはご留意くださいませ。

## 内容
### SkyWayとは
SkyWayとはビデオ・音声・データ通信機能をアプリケーションに簡単に実装できるSDK&APIです。

50万回までの接続および500GBまでのサーバー通信が無料で利用できます。また開発ドキュメントが豊富に用意されています。

### SkyWayを使うための準備
SkyWayを使うためには、アカウント作成->アプリケーション作成が必要です。

https://skyway.ntt.com/ja/

アプリケーション作成時に`アプリケーションID`と`シークレットキー`が作成されます。
プログラムを記載時に必要なので、メモしておいて下さい。

## プログラム
### チュートリアル
プログラムは簡単のためにSkyWayのGitHubのプログラムを用いました。

https://github.com/skyway/js-sdk/tree/main/examples/p2p-room

今回は爆速で作るために環境構築はせずにCDNを用いました。

`index.html`に以下を記述すればSkyWayを使えるようになります。

```html
<script src="https://cdn.jsdelivr.net/npm/@skyway-sdk/room/dist/skyway_room-latest.js"></script>
```

また`アプリケーションID`と`シークレットキー`を記載する項目があるので、メモしておいたものを記載してください。この時、`シークレットキー`が外部に漏れないように注意してください。

実際のチュートリアルのプログラムで作ったWebアプリのスクリーンショットはこんな感じです。

![skyway1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487820/e6c4d5c3-d5c2-714d-e066-0b6615cab326.png)


### チュートリアルの改良
このままでも要件は満たすのですが、少しだけ画面を見やすくしていきます。

今回は爆速で作るということで、ChatGPTを用います(筆者はChatGPT4を使っています)。具体的には以下のようにプロンプトを与えました。

```
以下のindex.htmlとmain.jsに対して、要素を真ん中揃えにしてテーマカラーを緑色にしてください。全体的にカッコよくしてください。
ビデオの画面はボタンで表示•非表示を切り替えられるようにして下さい。
見守りアプリというアプリ名を入れてください。


--以下ファイルの中身をペースト--
```

次に言葉を少しわかりやすくします。以下の表のように変更しました。

|変更前  |変更後  
|---|---
|channel name  |キーワード 
|write dataStream  |メッセージ
|join  |参加
|write |送信

最終的な見た目はこんな感じです。

![ 2023-07-20 23.26.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487820/c7101846-a7c4-95e5-201f-7cf59ce4b764.png)



スイッチボタンをオフにすればカメラもOFFになります。

まだまだ改善の余地だらけですが、少しは見やすくなったので一旦プログラム部分は終了とします。

<details><summary>index.html</summary>


```html
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>SkyWay Tutorial</title>
    <style>
        body {
            background-color: #f5f5f5;
            font-family: Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }

        .container {
            text-align: center;
        }

        p {
            color: #555;
        }

        #my-id {
            background-color: #8bc34a;
            color: white;
            padding: 5px 10px;
            border-radius: 5px;
            font-weight: bold;
        }

        input[type=text], button {
            padding: 10px;
            margin: 5px 0;
        }

        input[type=text] {
            width: 70%;
            border: none;
            border-radius: 5px;
            box-shadow: 0 0 15px -4px rgba(0, 0, 0, 0.1);
        }

        button {
            background-color: #8bc34a;
            border: none;
            color: white;
            border-radius: 5px;
            cursor: pointer;
        }

        button:hover {
            background-color: #689f38;
        }

        .video-container {
            display: flex;
            justify-content: center;
            gap: 20px;
        }

        #local-video {
            border: 8px solid #8bc34a;
            border-radius: 5px;
            width: 100%;
            max-width: 400px;
            margin-top: 20px;
        }

        .channel-group {
            display: flex;
            gap: 10px;
        }

        .switch {
        position: relative;
        display: inline-block;
        width: 60px;
        height: 34px;
        }

        .switch input { 
        opacity: 0;
        width: 0;
        height: 0;
        }

        .slider {
        position: absolute;
        cursor: pointer;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #ccc;
        -webkit-transition: .4s;
        transition: .4s;
        }

        .slider:before {
        position: absolute;
        content: "";
        height: 26px;
        width: 26px;
        left: 4px;
        bottom: 4px;
        background-color: white;
        -webkit-transition: .4s;
        transition: .4s;
        }

        input:checked + .slider {
        background-color:  #8bc34a;;
        }

        input:focus + .slider {
        box-shadow: 0 0 1px  #8bc34a;;
        }

        input:checked + .slider:before {
        -webkit-transform: translateX(26px);
        -ms-transform: translateX(26px);
        transform: translateX(26px);
        }

        .slider.round {
        border-radius: 34px;
        }

        .slider.round:before {
        border-radius: 50%;
        }
    </style>
</head>

<body>
    
    <div class="container">
        <h1>見守りアプリ</h1>
        <p>ID: <span id="my-id">Your ID</span></p>
        <div class="channel-group">
            <input id="channel-name" type="text" placeholder="キーワード" />
            <button id="join">参加</button>
        </div>

        <label class="switch">
            <input type="checkbox" id="videoToggle">
            <span class="slider round"></span>
        </label>
        
        <p id="status">カメラ OFF</p>
        <video id="local-video" muted playsinline style="display: none;"></video>

        <div id="remote-media-area"></div>

        <div>
            <input id="data-stream" type="text" placeholder="メッセージ" />
            <button id="write">送信</button>
        </div>

        <div id="button-area"></div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/@skyway-sdk/room/dist/skyway_room-latest.js"></script>
    <script type="module" src="main.js"></script>
</body>

</html>
```
</details>

<details><summary>main.js</summary>


```javascript
const { nowInSec, SkyWayAuthToken, SkyWayContext, SkyWayRoom, SkyWayStreamFactory, uuidV4 } = skyway_room;


const token = new SkyWayAuthToken({
jti: uuidV4(),
iat: nowInSec(),
exp: nowInSec() + 60 * 60 * 24,
scope: {
    app: {
    id: "アプリケーションID",
    turn: true,
    actions: ['read'],
    channels: [
        {
        id: '*',
        name: '*',
        actions: ['write'],
        members: [
            {
            id: '*',
            name: '*',
            actions: ['write'],
            publication: {
                actions: ['write'],
            },
            subscription: {
                actions: ['write'],
            },
            },
        ],

        sfuBots: [
            {
            actions: ['write'],
            forwardings: [
                {
                actions: ['write'],
                },
            ],
            },
        ],
        },
    ],
    },
},
}).encode("シークレットキー");

document.addEventListener('DOMContentLoaded', function() {
    const videoToggle = document.getElementById('videoToggle');
    const localVideo = document.getElementById('local-video');
    const status = document.getElementById('status');
    
    videoToggle.addEventListener('change', function() {
      if (this.checked) {
        localVideo.style.display = 'block';
        status.textContent = 'カメラ ON';
      } else {
        localVideo.style.display = 'none';
        status.textContent = 'カメラ OFF';
      }
    });
  });
 

(async () => {
const localVideo = document.getElementById('local-video');
const buttonArea = document.getElementById('button-area');
const remoteMediaArea = document.getElementById('remote-media-area');
const channelNameInput = document.getElementById('channel-name');

const dataStreamInput = document.getElementById('data-stream');

const myId = document.getElementById('my-id');
const joinButton = document.getElementById('join');
const writeButton = document.getElementById('write');

const { audio, video } =
    await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream();
video.attach(localVideo);
await localVideo.play();

const data = await SkyWayStreamFactory.createDataStream();
writeButton.onclick = () => {
    data.write(dataStreamInput.value);
    dataStreamInput.value = '';
};

joinButton.onclick = async () => {
    if (channelNameInput.value === '') return;

    const context = await SkyWayContext.Create(token);
    const channel = await SkyWayRoom.FindOrCreate(context, {
    type: 'p2p',
    name: channelNameInput.value,
    });
    const me = await channel.join();

    myId.textContent = me.id;

    await me.publish(audio);
    await me.publish(video);
    await me.publish(data);

    const subscribeAndAttach = (publication) => {
    if (publication.publisher.id === me.id) return;

    const subscribeButton = document.createElement('button');
    subscribeButton.textContent = `${publication.publisher.id}: ${publication.contentType}`;
    buttonArea.appendChild(subscribeButton);

    subscribeButton.onclick = async () => {
        const { stream } = await me.subscribe(publication.id);

        switch (stream.contentType) {
        case 'video':
            {
            const elm = document.createElement('video');
            elm.playsInline = true;
            elm.autoplay = true;
            stream.attach(elm);
            remoteMediaArea.appendChild(elm);
            }
            break;
        case 'audio':
            {
            const elm = document.createElement('audio');
            elm.controls = true;
            elm.autoplay = true;
            stream.attach(elm);
            remoteMediaArea.appendChild(elm);
            }
            break;
        case 'data': {
            const elm = document.createElement('div');
            remoteMediaArea.appendChild(elm);
            elm.innerText = 'data\n';
            stream.onData.add((data) => {
            elm.innerText += data + '\n';
            });
        }
        }
    };
    };

    channel.publications.forEach(subscribeAndAttach);
    channel.onStreamPublished.add((e) => subscribeAndAttach(e.publication));
};
})();
```
</details>

### デプロイ
今回は複数デバイスで接続することを想定しているため、デプロイする必要があります。
作成したプログラムをGitHubにpush->Vercelにデプロイすると一瞬で公開できます。(ここら辺はお好みで大丈夫です)

https://vercel.com/

この時シークレットキーは必ず外部に漏れないように注意してください。

### 使い方
それでは実際に使ってみます！

1. 2台のカメラがついているデバイス（携帯、iPad、パソコン、ラズパイでも可）を用意します。自分の場合は、使っていない昔の携帯と今の携帯を使います。

2. それぞれのデバイスで作成したWebアプリを起動します。(この時、見守る側のデバイスはカメラOFFでも構いません。)
3. 初回のみカメラを許可するかどうか聞かれるので、「許可」ボタンを押します。
4. 見守られる側のデバイスのカメラに、見守りたい対象がしっかり写っていることを確認します。
5. 両方のデバイスで共通のキーワード(厳密に言えばchannel name)を入力し、参加ボタンを押します。
6. 画面下に表示されている`{ユーザーIDの文字列}.video`を押します。
7. 見守りたい対象が写っていればバッチリです！！

![ 2023-07-20 23.01.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487820/a2579238-6706-16ff-d468-334aa241686d.png)
（↑本来ならばカメラ(携帯)をどこかに固定して撮影すべき）


もし通話したい場合は、画面下に表示される`{ユーザーIDの文字列}.audio`を押してください。

また今回はスコープ外ですが、チャットしたい場合は画面下に表示される`{ユーザーIDの文字列}.data`を押して下さい。


### 余談:通信データの確認
SkyWayは500GBまで無料で使えます。
アプリケーション詳細からデータの使用量を簡単に確認できます。

![ 2023-07-20 23.07.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487820/f66bd2e6-8aaf-f7ca-9a6a-57a3279179fe.png)


## 今後の展望

本アプリの今後の展望は以下です。

- CSSを修正する。
- Reactに書き換える。

https://qiita.com/akid3020/items/69752609714272fb8dec


- 入退出時の処理など、細かい処理を実装する。

以下の記事が参考になります。

https://qiita.com/akid3020/items/4898aebea14a565832cc


- 複数のデバイスに対応する。
カメラの数をたくさん増やす場合は`P2P Room`ではなく`SFU Room`を使用した方が良いと考えられます。(今回はビデオ通話しているわけではないので、問題ないかもしれません。)

- ラズパイと組み合わせてスマートカメラ化にする。(ex.カメラの向きを遠隔操作する)
面白そうなので後日やってみます！！！

## 最後に
SkyWayの本来の使い方ではない気がしますが、こんな感じの見守り系アプリにも使えるというアイデアでした。

もしペットを飼った時に、このアプリを改良して使ってみたいと思います！