iOSでNode.jsを動かしたい（ついでにWebRTCシグナリングサーバーも）

# はじめに

これは [Node.jsアドベントカレンダー2020](https://qiita.com/advent-calendar/2020/nodejs)の記事です。実用性皆無のネタ記事ですが、リラックスしてお楽しみください。

twitterで「iPadでLinuxが動く」というツイートを見たことがきっかけで、iPad/iPhone上でNode.jsを動かしてみました。内容は次の通りです。

- iSH Shell で Node.jsを動かす
- Node.jsでWebサーバーを動かし、Safariでアクセスする
- オマケ：WebRTCのシグナリングサーバーを動かして、iPadとiPhoneで通信する


# iSH Shell

# iSH Shellとは

利用するアプリは、「iSH Shell」[（App Store)](https://apps.apple.com/jp/app/ish-shell/id1436902243)というアプリです。私は知りませんでいたが、以前からあるアプリのようです。
名前は「Shell」とありますが、実際にはアプリケーションレイヤーでlinuxをエミュレーションしています。利用されているディストリビューションは、Alpine Linuxです。

ちなみに使用するには、Bluetoothキーボードがあった方が便利です。

## iSH Shellのインストール

普通にApp Storeからインストールします。


### 中身の確認

- デフォルトのユーザーは root
- /etc/issue をみると、Alpine Linux 3.12 
- CPUアーキテクチャーは、i686（インテル系32ビット）

```terminal:シェルで操作
~# whoami
root

~# cat /etc/issue
Welcome to Alpine Linux 3.12
Kernel \r on an \m (\l)

~# uname -m
i686
```

[iPadの画面キャプチャー]
![iSH_info.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/788cea6f-8b22-7dbb-3ffd-ec6224a92286.png)



### パッケージマネージャーの追加

アプリをインストールした状態では、Alpineのパッケージマネージャー apk はインストールされていません。どうやらApp Storeの規約上、取り除く必要があったようです。
（※2020.12.15現在、App Store版でもapkがインストール済みになったようです）

こちらの手順に従い、apkをインストールします。

- GitHub ... https://github.com/ish-app/ish/
- Wiki ... https://github.com/ish-app/ish/wiki
- Tutorial:Installing apk ... https://github.com/ish-app/ish/wiki/Installing-apk
    - ※手順はこのページに記載

```terminal:シェルで操作
~# wget -qO- http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86/apk-tools-static-2.10.5-r1.apk | tar -xz sbin/apk.static && ./sbin/apk.static add apk-tools && rm sbin/apk.static && rmdir sbin 2> /dev/null

~# apk --version
apk-tools 2.10.5, compiled for x86.
```

[apkインストールまでのキャプチャー]
![iSH_apk.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/4e5af3b1-0099-3d06-1f25-9be1ad450358.png)


# Node.js を動かす

## Node.jsのインストール

iSHとapkが用意できたら、念願のNode.jsをインストールします。

```terminal
~# apk add nodejs
~# apk add npm

~# node -v
v12.18.4
~# npm -v
6.14.6
```

2020/12/5現在、node v12.18と、npm 6.14がインストールされました。(※npmは応答が返ってくるまで、時間がかかります。何かのタイムアウトが発生している？）

[node, npmインストールまで]
![iSH_node_npm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/6aa2c3e1-53b1-4909-9e5e-c442c4a22640.png)


## REPLの利用

さっそくREPL（対話モード）を動かしてみます。

```terminal
~# node
Welcome to Node.js v12.18.4.
Type ".help" for more information.
> 1+2+3
6
> console.log('Hello Node.js')
Hello Node.js
undefined
> .exit
~#
```

[Node.js REPLの様子]
![iSH_node_repl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/5092d5de-6d41-e0f2-e5cb-6826cb93d6ae.png)


## Webサーバーを動かす

次はNode.jsを使って、簡単なWebサーバーを動かしてみます。

```js:server.js
const http = require('http');

const server = http.createServer((req, res) => {
  res.end('<html><h1>Hello World!</h1></html>');
});
server.listen(8000);
```

シェルから起動します。

```terminal
~# node server.js
```

### iPadの場合

iSHがバックグラウンドに回ってしまうと、内部のプロセスの実行が停止してしまいます。そのためiPadの場合はSplit View([Appleの説明](https://support.apple.com/ja-jp/HT207582))を使って、iSHとSafariを横に並べて使います。

- iSH上で、サーバーを起動
- Split Viewで、Safariを同時に表示
- Safariで、http://localhost:8000/にアクセス
    - 「Hello World!」と表示されればOK

iPadなら、そこそこ実用的にNode.jsを動かすことができます。

[iSHでWeサーバーを起動、Safariでアクセス]
![iSH_web_server.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/5a11aae3-2125-90f6-a130-bcdbaa927643.png)


### iPhoneの場合

iPhoneでは、Split Viewが使えず、同時に2つのアプリを動かすことができません。そこで次の手順で無理やり実行します。

- iSH上で、サーバーを起動
- Safarを起動し、http://localhost:8000/にアクセス
    - ロード待ちになる
- iSHに切り替えると、サーバーがリクエストを処理する
- 再びSafariに切り替えると、Safari上でページが表示される

リクエストが飛ぶたびに、iSHとSafariを切り替えてやる必要があります。iPhoneではかなり厳しいです。

## iSH Shellの制約

iSH Shellではx86をエミュレーとしてLinuxが動いていますが、一部デバイスに近い部分では使えない機能があるようです。具体的には、ネットワーク周りの機能では動かないものがありました。
例えば、Node.jsで次のコードを実行しようとすると、エラーが発生します。

```js:コードの抜粋
  const os = require('os');
  const interfaces = os.networkInterfaces();
```

```:エラー
Uncaught:
SystemError [ERR_SYSTEM_ERROR]: A system error occurred: uv_interface_addresses returned Unknown system error 22 (Unknown system error 22)
```

ネットワークスインターフェイスの情報が取れていないようです。

# オマケ：WebRTCのシグナリングサーバーを動かす

ここからは、完全に個人の興味によるオマケです。

## WebRTC とシグナリング

Node.js が動くなら、WebRTCのシグナリングを動かしたくなるのが性分です。

- WebRTC ... Web Realtime Communitaion
    - Web上で、音声/映像/データをやり取りするための仕組み
    - P2Pでのユースケースを想定して規格が作られたが、いまはサーバー経由の利用が増えている
- シグナリング
    - これから通信を始める2者が、通信開始に必要な情報を交換するための手続き
    - WebRTCではやり方は規定されていない
        - サーバー経由で、WebSocket等の双方向通信の仕組みでやり取りすることが多い

ここでは説明は省略します。興味がある方は（ちょっと古いですが）こちらをご参照ください。

- WebRTCハンズオン 概要編 ... https://qiita.com/massie_g/items/916694413353a3293f73

## シグナリングサーバーとWebサーバー

### よくあるケース

通常はシグナリングのサーバーはローカルネットワーク内のPC上か、インターネット上で動かし、通信したい2つの端末で接続します。

[通常の構成図]
![webrtc_signaling.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/cbb5b7fd-5c5b-116d-a9c3-69fe28bb0af4.png)

### iOSデバイスだけで動かしたい

今回はPC(Mac)やインターネット上のサーバーを利用せず、2台のiOSデバイスだけでシグナリングサーバーを動かし、WebRTC通信を行ってみます。

- iPad と iPad
- または、iPad と iPhone

（不自然ではありますが、そういう縛りのある状況でデモしなければならない、という設定をご想像ください汗）

また、ブラウザでメディア通信で使うカメラ/マイクにアクセスするには、セキュリティ上の制限により次の場所にあるWebページの必要があります。

- https://～
- または、 http://localhost:ポート番号/～

そのため、2台のデバイスそれぞれでWebサーバーは動かします。

[今回の構成図]
![webrtc_signaling_deviceonly.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/dcdc1710-7807-34db-f7e5-60b7c2ba1d0b.png)

## iSH でWebサーバー/シグナリングサーバーを動かしてみる

Webサーバー/シグナリングサーバは、Node.js  + express + ws(WebSocket) で実装しています。

### 準備（セットアップ）

- 2台のiOSデバイスを用意 (iPad + iPad, or iPad + iPhone)
- それぞれに、iSH Shell をインストール、apk, node.js もインストール
- それぞれに、gitもインストール
    - apk add git 
- それぞれに、コードを取得（ブランチを指定してください）

```terminal:コードを取得
~# apk add git

~# git clone https://github.com/mganeko/webrtc_1to1.git
~# cd webrtc_1to1
~# npm install

```

### 準備（情報収集）

今回シグナリングサーバー役のデバイス1は、iPadを利用する必要があります。（iSHをバックグラウンドに回さないように）。一方、デバイス2側からは、デバイス1のIPアドレスを知っておく必要があります。


- (A) 両方が、同じWiFi環境にいる場合
    - デバイス1側で、割り振られているIPアドレスを確認
    - 「設定」-「Wi-Fi」- 詳細情報(iボタン）で、ネットワークの情報を確認
        - IPV4アドレスの表から、自分のIPアドレスを取得
        - 192.168.0.xxx など

![iOS_addr.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/bc29026f-031e-fd7b-5886-bef8a5ad5982.jpeg)

- (B) それ以外の場合
    - デバイス1側で、「インターネット共有」（テザリング）をオンに
    - デバイス2側で、デバイス1で共有したWiFiに接続
    - デバイス2側で、接続情報を確認
        - IPV4アドレスの表から、「ルーター」のアドレスを確認
        - 172.20.xxx.1 など
        - ※テザリングしているデバイス1でなく、そこのぶら下がるデバイス2側で確認します

![iOS_router_addr.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/5142b8ba-49b3-f6b6-b18b-bd02827288d4.jpeg)


(A),(B)それぞれに応じた方法で取得したシグナリングサーバー（デバイス1）のIPアドレスを、次のステップで利用します。

### 実行

(1) サーバーの起動

デバイス1、2の両方で、サーバーを起動します。

```terminal:起動
~# npm start

> webrtc_1to1@1.0.0 start /root/work/webrtc_1to1
> node server_1to1.js
websocket server start. port=8080
Web server start. http://localhost:8080/

```

(2) SafariでWebサーバーにアクセス

起動したらそれぞれのデバイスでWebサーバーにつなぎますが、デバイスごとにURLが異なります。

- デバイス1（ローカルのシグナリングサーバーに接続する）
    - http://localhost:8080/
- デバイス2（シグナリングサーバーのURLを別途指定する）
    - http://localhost:8080/sub.html
    - ※デバイス2がiPhoneの場合は、SahariとiSHを交互に切り替えながら、Webページをロードしてください

[デバイス1の画面(iPad)] 
![ipad_webrtc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/7220ba61-c8be-412f-064f-81438d2d6434.png)

[デバイス2の画面(iPhone)] 
![iphone_webrtc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/24e0c716-3a49-fb7e-5e47-dabf38bae520.png)


※iOSデバイスがスリープしてしまうと、サーバーの実行が止まります。再度起動し直してください。


(3) カメラ映像の取得

- デバイス1で、[Start Video]ボタンをタップ
    - アクセス許可を求められたら、[許可]をタップしてください
- 同様にデバイス2でも、[Start Video]ボタンをタップ
    - アクセス許可を求められたら、[許可]をタップしてください

(4) 接続

接続はデバイス2 (sub.html)側から行います

- デバイス2側で、テキストエリアにシグナリングサーバーのURLを入力
    - 例えば、「ws://192.168.0.10:8080」
    - ※ポート番号は 8080 です
- デバイス2側で[Connect]ボタンをタップ
    - 接続情報が交換され、通信が開始します。

![ios_webrtc_connected2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/7fbca0b2-f65a-b58a-f157-29a43ccffed5.jpeg)


(5) 切断

- デバイス1,2 どちらでも良いので、[Hang Up]ボタンをタップします
    - ※この状態で[Connect]ボタンをタップすると、再接続できます



### コード

https://github.com/mganeko/webrtc_1to1.git にあります。

# おわりに

「iOSでNode.jsを動かしたい」という何の実用性もない記事でした。もし不幸にも(?)そういう状況に陥ったら思い出してみてください。