無料でインターネットペットカメラを作ろう

## 概要

仕事中に、家でお留守番しているペットを見たくなったことはありませんか？

この記事では、

使わなくなったスマホやRaspberry Piを自宅のペットカメラにして外出先からペットを見ることができるシステムを作ります。

このシステムの特徴は
- 無料
- 3分で利用開始できる
- 自宅ネットワークの設定変更不要で、インターネットから見られる

です。

完成すると、この図のように、いつでもペットが見られます！
![構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141013/1d5849c4-82e1-5104-725e-0bf955f2fb57.png)



## さぁ始めよう

この記事では

1. SkyWayのセットアップ
2. デモサーバーを使ってインターネットペットカメラを動かす
3. SkyWayの説明
4. ローカルでペットカメラを動かす
5. GitHub Pagesを使ってホスティングする
6. プログラムの解説


という流れで進めていきます。

1, 2だけでも、インターネットペットカメラとして使えます。
3以降は開発したい人向けです。


## 1. SkyWayのセットアップ

このシステムでは[SkyWay](https://skyway.ntt.com/ja/)のプラットフォームを利用します。
SkyWayでは無料でアカウントを作成することができ、メアドのみ（クレジットカードの登録なし）でFreeプランが利用できます。

Freeプランは利用上限がありますが、個人向けペットカメラ用途ではFreeで大丈夫そうです。

まず[ユーザー登録](https://console.skyway.ntt.com/signup/)をして[ログイン](https://console.skyway.ntt.com/login/)しましょう。

ログインしたら、`プロジェクトを作成`ボタンを押して、新しいプロジェクトを作成します。
![make project](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141013/1e3fb7c5-53a8-b02e-61eb-534c485d7a34.png)

プロジェクト名は自由で構いません。

次に`アプリケーションを作成`ボタンを押して、新しいアプリケーションを作ります。

![make app](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141013/a25249cd-1c41-9c0b-8fd2-aa39905d2d6e.png)

このアプリケーション名も自由で構いません。

アプリケーションを作成すると、
`アプリケーションID`と`シークレットキー`
が表示されますので、大事に保管しておきましょう。

![appid](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141013/eb9225f3-2b4b-f50f-1cc3-ed8cfafaf895.png)


## 2. デモサーバーを使ってインターネットペットカメラを動かす

早速ですが、インターネットペットカメラを動かしてみましょう。

デモサーバー(GitHub Pages)を用意しています。
動かすことができると、全体像がイメージがしやすくなりますよね！


### ペットカメラ側（自宅）

カメラが付いているスマートフォンはありますか？
iPhoneであればiPhone6以降(iOS 11以降)のSafariで利用できます。
なければ、カメラ付きのノートPCやRaspberry Piでも構いません。


ブラウザから、[カメラ用のURL](https://komasayuki.github.io/pet-cam/index.html?camera=true&audio=true)を開いてみましょう。

すると「Input application ID」や「Input application Secret」と聞かれます。

さきほどSkyWayで取得した、あなたの`アプリケーションID`と`シークレットキー`を入力してください。

（このプロンプトがでない場合はブラウザでページをリロードをしましょう。ページのタブが表示されていないときは、プロンプトが出ない仕様のブラウザがあります。）


入力後、下の画像のような表示になり、カメラのストリーミングが開始されます。

![IMG_1165.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141013/a3a64cd3-1bbe-f773-2fa5-2cbe7506ed43.png)

小さいですが、画面左上のボタンを押すとカメラを切り替えできます。


マイクをミュートしておきたい場合は、カメラ用のURLの`audio`パラメータを`false`にしましょう。


### ビューワー側（外出先）

スマホやパソコンのブラウザから、[ビューワー用のURL](https://komasayuki.github.io/pet-cam/index.html?camera=false)を開いてみましょう。

カメラ側のときと同様に、あなたの`アプリケーションID`と`シークレットキー`の入力が必要です。

このような感じで、カメラ側から映像がストリーミングされます。

![IMG_4861.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141013/b19111f3-84db-e6c1-facb-00a4e6bc72f5.png)



ペットに癒やされましょう！


### いちいちアプリケーションIDとシークレットキーを入力するのは面倒？

`https://komasayuki.github.io/pet-cam/index.html?camera=true&audio=true&appId=YOUR_APPLICATION_ID`

のように`appId`パラメータで`アプリケーションID`を渡すことができます。そうすると`アプリケーションID`の入力はスキップできます。

`シークレットキー`も`secret`パラメータで渡すことができますが、セキュリティ上推奨しません。ローカルのサーバーで試す場合のみ利用してください。


## 3. SkyWayの説明

このインターネットペットカメラを支えてくれているSkyWayの説明をします。

### SkyWay JavaScript SDK

SkyWayのSDKは、現在

- JavaScript
- iOS
- Android

の３種類が用意されています。
今回はブラウザ向けですので、[JavaScript SDK](https://skyway.ntt.com/ja/docs/user-guide/javascript-sdk/overview/)を使います。

SkyWayは2023年1月31日から新バージョンを公開していて、
本記事では新しいバージョンを使っています。


#### Room
JavaScript SDKでは2種類のRoomを作ることができ、ユーザーが選択できます。

- P2P Room （4人以下の少人数向け）
- SFU Room （多人数向け、SFUサーバーを経由して通信する）

の2種類があります。
個人用のペットカメラでは視聴者はわずかですから、P2P Roomが最適です。


#### Stream

Roomで送受信できるメディアはStreamと呼ばれます。
やり取りできる情報の種類のことです。

- VideoStream (ビデオ映像)
- AudioStream (オーディオ音声、マイク音声)
- DataStream （任意のメッセージ、P2P Roomでのみ使える）

本ペットカメラでは、VideoStreamとAudioStreamだけ使います。
チャットやコマンドなどを送りたい場合は、DataStreamが役に立ちます。



#### Channel

RoomにはChannelを作ることができます。Channelは通話しているユーザーグループの集まりです。ChannelはRoomの中に複数作ることができ、このChannelに参加することで実際に通話を開始できます。

Roomに動画などStreamを公開することをPublish、
Roomから受信することをSubscribeと言います。

本ペットカメラでは、Channelは1つでいいので`my-lovely-pets`という固定文字列をChannel名として使っています。


#### 認証

認証はJWT（ジョットと読みます）で行います。JSON Web Tokenのことです。

が、認証に関してはあまり気にしなくてもSkyWayのSDK任せで大丈夫です。


#### WebRTC

WebRTCはWeb Real-Time Communicationのことで、ブラウザを使って動画や音声をやり取りするのに使える技術です。

[各ブラウザのWebRTCサポート状況](https://caniuse.com/?search=webrtc)でブラウザがどのバージョンからWebRTCがサポートしているか確認できます。
ここ5年ぐらいのブラウザバージョンなら、サポートされている感じですね。

iOSだとiOS 11以降(iPhone6以降)、AndroidだとAndroid 5以降から使えるようです。




### 4. ローカルでペットカメラを動かす

[SkyWay JavaScript SDKリポジトリ](https://github.com/skyway/js-sdk)にはサンプルコードが含まれており、[p2p room example](https://github.com/skyway/js-sdk/tree/main/examples/p2p-room)という、本件に最適なサンプルがあります。

サンプルをベースにインターネットペットカメラ用途に書き換えたソフトをGitHubで公開しました。

https://github.com/komasayuki/pet-cam

#### ビルド

以下の方法でコードを取得してビルドできます。
[nodeのインストール](https://nodejs.org/ja/download)が必要です。

```bash

# clone pet-cam repository from GitHub
% git clone https://github.com/komasayuki/pet-cam

% cd pet-cam

# first time only
% npm install

# ビルド ./distに生成される
% npm run build
```

※SkyWayのサンプルでは`npm run build`にBabelとParcelを併用していたので、[Parcelのみで動くように変更](https://github.com/komasayuki/pet-cam/commit/a4d5f06242eeb51d56d6e509ae0ecf5bc37326d1)しています。


#### 実行

実行するためにはWebサーバーが必要です。
chromeではhttpでもWebRTCが使えるので、パソコンで実行してみましょう。
（iOSではWebRTCの利用はhttps必須です）

[http-server](https://github.com/http-party/http-server)と[npx](https://github.com/npm/npx)を使ってWebサーバーを立ち上げます。
静的ファイルをホストするだけなので、Webサーバーになら何でもいいです。

```bash
% npx http-server ./dist
Available on:
  http://127.0.0.1:8080
```

ポート8080でWebサーバーが立ち上がりますので、

カメラ側: [http://127.0.0.1:8080/index.html?camera=true](http://127.0.0.1:8080/index.html?camera=true)

ビューワー側: [http://127.0.0.1:8080/index.html?camera=false](http://127.0.0.1:8080/index.html?camera=false)

をPCのブラウザで開いてください。
プロンプトが出たら、SkyWayで取得したApplication IDとSecretを入力しましょう。

うまく動きましたか？

この方法では、distフォルダのファイルを
インターネットに公開しなくても、
それぞれのPC環境でWebサーバーを立ち上げて、
それぞれのPC環境で`127.0.0.1`にアクセスしても、
インターネットを超えてカメラに繋がります。


## 5. GitHub Pagesを使ってホスティングする

GitHubは、GitHub Pagesという無料でhttpsでホスティングできる仕組みを提供しています。

さきほどビルドした際にdistフォルダに生成されたファイルを公開するだけなのですが、今回はGitHubリポジトリからGitHub Actionsを使ってWebに公開されるようにしました。

まず、GitHubリポジトリのSettingsからGitHub Pagesの `Source`の設定を
`GitHub Actions`に変更します。

![Screenshot 2023-07-05 at 14.49.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141013/2a4c970a-55e4-1206-6df0-d7fa00f759b3.png)


そしてリポジトリのActionsを開いて、左側の`Deploy to GitHub Pages`を開き、右の`Run workflow`から`Run workflow`を実行します。

そうすると、ローカルPCで実行した`npm run build`で作られたファイルをGitHub Pagesとして公開してくれます。2分ぐらいかかります。

![Screenshot 2023-07-05 at 17.29.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141013/634ecdeb-beb4-9fb4-3beb-ef3b8eeb845d.png)

公開されるURLはActionsの結果の画面で取得できます。
![Screenshot 2023-07-05 at 17.31.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141013/f7e412e4-043b-9e6b-5e4b-dee02a8c5a74.png)


あなた専用のインターネットペットカメラができました！




## 6. プログラムの解説

では最後に、プログラムの解説をしていきます。

### ローカルでAuthトークン(JWT)の作成

SkyWayサーバーの認証はJWTで行います。
これは以下のように、SkyWayのJavaScript SDKで簡単に作ることができます。

```typescript
const token = new SkyWayAuthToken({
  jti: uuidV4(),
  iat: nowInSec(),
  exp: nowInSec() + 60 * 60 * 24,
  scope: {
    app: {
      id: appId,
      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(secret);
```

これはサンプルから変更ありません。
SkyWayのWebサイトで作成したappIdとsecretを渡すことでAuthトークンが生成されます。

このAuthトークンはJWTなので、[jwt.io](https://jwt.io/)などでデコードできます。私が試したところでは有効期間が24時間のJWTになっていました。JWTをデコードすると、`exp`という値に失効日時が含まれています。

また、このサンプルでは認可の設定が`*`だらけですが、特定のチャンネルだけ操作できる、など制限をかけたAuthトークンを生成することもできます。



### SkyWayで認証、コンテキストの作成

作成したAuthトークンを使って、SkyWayのContextを作成します。
Contextは認証ができたら取得できるものぐらいの認識でOKです。

```typescript
const context = await SkyWayContext.Create(token);
```

認証情報が間違っている場合などは、この関数コールで例外がThrowされます。try-catchしましょう。


### Roomを作成して、ジョイン

contextが手に入ったら、p2pルームの`my-lovely-pets`というチャンネルに参加します。

```typescript
const channel = await SkyWayRoom.FindOrCreate(context, {
    type: 'p2p',
    name: 'my-lovely-pets',
});
const me = await channel.join();
```

ペットカメラなのでチャンネルは1種類でいいです。
ペットがたくさんいる幸せな方は、うまく切り替えしましょう。


これ以降の手順は、カメラ側とビューワー側で異なります。

### カメラ側から動画/音声StreamをPublishする

ペットカメラは1方通行のストリーミングです。
自宅カメラ側から動画と音声を送信します。

まず、`SkyWayStreamFactory.enumerateInputVideoDevices()`でカメラ一覧を取得します。
取得したカメラ一覧の配列には`MediaDevice`が含まれており、`id`の他に人間がカメラを認識できる`label`という属性もあります。


```typescript
const cameraDevices = await SkyWayStreamFactory.enumerateInputVideoDevices();
```

取得したカメラのidをdeviceIdとして`SkyWayStreamFactory.createCameraVideoStream()`
に渡すと、videoのStreamが取得できます。

audioのStreamは`SkyWayStreamFactory.createMicrophoneAudioStream()`
で取得できます。

```typescript
const video = await SkyWayStreamFactory.createCameraVideoStream({deviceId: cameraDevices[0].id});
const audio = await SkyWayStreamFactory.createMicrophoneAudioStream();
```


HTMLでvideoというIDのvideoタグを用意してあります。
以下のコードを実行することで、videoタグではカメラのプレビューが見られるようになります。

```typescript
const elm = document.getElementById('video') as HTMLVideoElement;
video.attach(elm);
await elm.play();
```

あとは、さきほど取得したme:LocalP2PRoomMemberに対して、publishするだけです。
これで動画/音声が送信が開始されます。
（実際には送信相手がいない場合はネットワークには流れません）

```typescript
await me.publish(audio);
await me.publish(video);
```

iPhoneではデフォルトが前面カメラになったので、カメラの切り替え機能がないと使いにくいです。そのため、サンプルコードにはないカメラの切り替えを追加しています。

送信を開始してからカメラを切り替えるために

```typescript
const publication = await me.publish(video);
```
のようにpublishの戻り値を保管しておき、新しく作ったvideoStreamを`publication.replaceStream()`します。

```typescript
const anotherCamera = await SkyWayStreamFactory.createCameraVideoStream({ deviceId: devices[1].id });
publication.replaceStream(anotherCamera);
```



### ビューワー側から動画/音声StreamをSubscribeする

以下のコードでは、現在publishされているStreamと、これからPublishされるStreamでSubscribeを実行します。

これはサンプルコードのままです。
VideoStreamが来たら、videoタグで表示するようにして
AudioStreamが来たら、audioタグで再生できるようにします。

ペットカメラでは、一番最後にPublishされたStreamを無条件に最新のものとみなしています。


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

  const { stream } = await me.subscribe(publication.id);

  switch (stream.contentType) {
    case 'video':
      {
        const elm = document.getElementById('video') as HTMLVideoElement;
        stream.attach(elm);
      }
      break;
    case 'audio':
      {
        const elm = document.getElementById('audio') as HTMLAudioElement;
        stream.attach(elm);
      }
      break;
  }
};

channel.publications.forEach(subscribeAndAttach);
channel.onStreamPublished.add((e) => subscribeAndAttach(e.publication));
```



### 再接続

短時間の切断などはSkyWayのSDKによって自動的に再接続されますが、長時間の場合は手動での再接続が必要となります。

```typescript
context.onFatalError.add(async () => {
  context.dispose();

  const newContext = await createContext(token, true);
  //context作成以降のフローをやり直し
});
```

contextに対してErrorハンドラーを追加できますので、この中でcontextを破棄して、contextの作成からやり直しが必要です。

ペット愛に溢れているなら、再接続の実装は欠かせませんね！


## セキュリティについて

ブラウザのクライアントサイドのみで動くシステムとして実装しています。

もし、appIdやsecretを入力するのが面倒で、main.tsなどにハードコードしてしまうとアクセスした人からsecretが見えてしまいます。

そのような改造を行う場合は、自分専用のサービス利用に限定し、Webページ自体への認証を確実に行いましょう。

[SkyWayのドキュメントではバックエンドでのAuthトークン生成を推奨しています。](https://skyway.ntt.com/ja/docs/user-guide/javascript-sdk/quickstart/#170)

URLパラメータで認証情報（シークレット）を渡すのは、セキュリティ上危険とされています。
通信経路上では漏れないものの、Webサーバーのログに残ったり、ブックマークを誰かに見られたり、ショルダーハックされるリスクがあるからです。

注意して設計しましょう。

## 補足

ペットカメラ側はデバイスが起動しっぱなしになると思いますがビューワーが接続されていない時間は、カメラからのビデオの送信は止まっていますのでネットワーク的な負荷は低いです。

## 最後に

SkyWayのプラットフォームを使うことで、とても簡単にインターネットペットカメラを作ることができました。とてもシンプルですね。

同様のプラットフォームにはAmazon Kinesis Video streamsなどありますがそれと比較しても簡単でした。

わずか100行ちょっとで実現できるなんて素敵ですね。

この記事を読んでくれた人が、これで何か作りたい！と思ってくれたら幸いです。