SkywayのJavaScript SDKの中身を読んでみよう

こんにちは pco2699というものです。
普段は自分のはてなブログに記事を書くのですが、たまにはQiitaに書いてみます。

この記事は Skyway Advent Calendar 2018の15日目の記事です。

# やること
他のSkywayアドベントカレンダーだと、「つくってみた」系が多かったので
違う観点でSDKのコードリーディングとかしてみるのはどうだろう、と思いました。

以前、Skywayつかったことあるんですか結構SDKの中身を見てデバッグすることが多かったので
役立つと思います。

願わくば、将来的にはプルリクとか出したい...!!!

# 早速見てみよう
リポジトリはこちらです。
https://github.com/skyway/skyway-js-sdk

全部見るのは、流石に無理なので
今回はとりあえず、接続を確立する部分を見ていきたいと思います。

個人的には、WebRTCのAPIを呼びまくってる部分を見てみて
**「ほぉ...WebRTCってやっぱよくわからんな...、Skyway偉大...!」**という結論を出すまで読んでいきたいと思います。

## リポジトリの構成
ざっとリポジトリは以下のような構成になってます。

```
.
├── CHANGELOG.md
├── examples // サンプルが入っている
├── karma.conf.js
├── LICENSE
├── package.json
├── package-lock.json
├── README.md
├── scripts // CI用のコード
├── src // SDKのソースコード
├── tests // テストコード
└── webpack.config.js
```

まずリポジトリ構成をみて面白かったのは

- CI用のスクリプトがかなり手厚い
- CI用のスクリプトもJavaScriptで書かれてる

という点でしょうか。
自分だったらCI用のスクリプトはbashとかで書いてしまうので、のっぴきならぬJS好きか、なにか環境制約があったのか、気になるところです。

## package.jsonもみてみよう
せっかくなんで、依存しているライブラリがまとまっているpackage.jsonも見てみます。

```json
  "devDependencies": {
    "babel-loader": "^7.1.4",
    "babel-plugin-espower": "^2.4.0",
    "babel-plugin-istanbul": "^4.1.6",
    "babel-preset-es2015": "^6.24.1",
    "del-cli": "^1.1.0",
    "eslint": "^5.0.0",
    "eslint-config-prettier": "^2.9.0",
    "eslint-plugin-prettier": "^2.4.0",
    "inject-loader": "^4.0.1",
    "karma": "^2.0.0",
    "karma-chrome-launcher": "^2.2.0",
    "karma-coverage": "^1.1.2",
    "karma-mocha": "^1.3.0",
    "karma-mocha-reporter": "^2.2.5",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-webpack": "^3.0.0",
    "mocha": "^5.2.0",
    "power-assert": "^1.4.4",
    "prettier": "^1.9.2",
    "sinon": "^6.0.1",
    "webpack": "^4.12.1",
    "webpack-cli": "^3.0.8"
  },
  "dependencies": {
    "detect-browser": "^4.2.0",
    "enum": "git+https://github.com/eastandwest/enum.git#react-native",
    "events": "^3.0.0",
    "js-binarypack": "0.0.9",
    "object-sizeof": "^1.3.0",
    "query-string": "^6.4.0",
    "sdp-interop": "^0.1.13",
    "sdp-transform": "^2.7.0",
    "socket.io-client": "^2.2.0"
  }
```

気になるパッケージは`inject-loader`と`power-assert`ですね。
調べてみたところ`inject-loader`はモックテスト用にimportするモジュールを差し替えられるライブラリ。
`power-assert`はテスト時のassertがFailしたときの内容をきれいに出してくれるライブラリ、ということで両方ともテスト用のライブラリでした。

さすがに、企業のSDKだけあってテストはしっかりしてそうです。（当たり前

## いよいよ srcの中へ
というわけでソースコードが格納されているsrcの中を見ていきます。

```
.
├── peer
│   ├── connection.js
│   ├── dataConnection.js
│   ├── mediaConnection.js
│   ├── meshRoom.js
│   ├── negotiator.js
│   ├── room.js
│   ├── sfuRoom.js
│   └── socket.js
├── peer.js
└── shared
    ├── config.js
    ├── logger.js
    ├── sdpUtil.js
    └── util.js
```
srcの中はこんな感じになってます。

早速、最初のスタートポイントである`peer.js`を見てみましょう。

```javascript:peer.js
import EventEmitter from 'events';
import Enum from 'enum';

// (省略)
const PeerEvents = new Enum([
  'open',
  'error',
  'call',
  'connection',
  'expiresin',
  'close',
  'disconnected',
]);

/**
 * Class that manages all p2p connections and rooms.
 * This class contains socket.io message handlers.
 * @extends EventEmitter
 */
class Peer extends EventEmitter {
  /**
   * Create new Peer instance. This is called by user application.
   * @param {string} [id] - User's peerId.
   * (省略)
   */
  constructor(id, options) {
    super();

    this.connections = {};
    this.rooms = {};

    // messages received before connection is ready
    this._queuedMessages = {};

    if (id && id.constructor === Object) {
      options = id;
      id = undefined;
    } else if (id) {
      id = id.toString();
    }

    const defaultOptions = {
      debug: logger.LOG_LEVELS.NONE,
      secure: true,
      token: util.randomToken(),
      config: config.defaultConfig,
      // (省略)
    };

    this.options = Object.assign({}, defaultOptions, options);

    logger.setLogLevel(this.options.debug);
    // (省略)
  }
```

ES2015のクラス構文でめちゃくちゃキレイに書かれてますね。読みやすい！（素人の感想）
ここから、どこを見れば、最初の接続部分の処理になるのか簡単にわかりそうです。

ここで、APIのリファレンスを見てみて、どのAPIを呼べば接続開始するのか見てみましょう。
[SkywayのAPIのリファレンス](https://webrtc.ecl.ntt.com/skyway-js-sdk-doc/ja/peer/#callpeerid-stream-options)

![Peer - JS SDK API Reference - Google Chrome 2019-03-30 00.12.28.png](https://qiita-image-store.s3.amazonaws.com/0/125528/a649ea32-acab-3e84-a5d2-83e6fb1664c2.png)

peerのcallメソッドで、指定したpeerとメディアチャンネルをつくるようです。
APIの通り実際にコードの中でも`MediaConnection`オブジェクトが作られていますね。
そして、作られた`MediaConnection`オブジェクトの`startConnection()`メソッドを呼んで
接続を開始しているようです。

```javascript:peer.js
  /**
   * Creates new MediaConnection.
   * (省略)
   */
  call(peerId, stream, options = {}) {
    if (!this._checkOpenStatus()) {
      return;
    }

    options.originator = true;
    options.stream = stream;
    options.pcConfig = this._pcConfig;
    const mc = new MediaConnection(peerId, options);
    mc.startConnection();
    logger.log('MediaConnection created in call method');
    this._addConnection(peerId, mc);
    return mc;
  }
```

次はMediaConnectionの`startConnection()`部のソースコードを見てましょう。

```javascript:MediaConnection.js
  /**
   * Start connection via negotiator and handle queued messages.
   * @return {Promise<void>} Promise that resolves when starting is done.
   */
  async startConnection() {
    if (!this._options.originator) {
      return;
    }

    await this._negotiator.startConnection({
      type: 'media',
      stream: this.localStream,
      // 省略
    });

    this._pcAvailable = true;
    this._handleQueuedMessages();
  }

```

ふむふむ、どうやら`negotiator`の`startConnection`を呼んでるみたいです。
次は`negotiatator`内を見てみましょう。ここらへんからはAPIのリファレンスには説明があまり載ってこない世界になってきます。

```javascript:negotiator.js
  async startConnection(options = {}) {
    this._pc = this._createPeerConnection(options.pcConfig);
    // 省略

    if (this._type === 'media') {
      if (options.stream) {
        if (this._isAddTrackAvailable && !this._isForceUseStreamMethods) {
          options.stream.getTracks().forEach(track => {
            this._pc.addTrack(track, options.stream);
          });
        } else {
          this._pc.addStream(options.stream);
        }
      } else if (this.originator) {
        // This means the peer wants to create offer SDP with `recvonly`
        const offer = await this._makeOfferSdp();
        await this._setLocalDescription(offer);
      }
    }
  /**
   * Create new RTCPeerConnection.
   * (省略)
   */
  _createPeerConnection(pcConfig = {}) {
    logger.log('Creating RTCPeerConnection');

    const browserInfo = util.detectBrowser();

    this._isAddTrackAvailable =
      typeof RTCPeerConnection.prototype.addTrack === 'function';
    this._isOnTrackAvailable = 'ontrack' in RTCPeerConnection.prototype;
    this._isRtpSenderAvailable =
      typeof RTCPeerConnection.prototype.getSenders === 'function';

    // 省略
    return new RTCPeerConnection(pcConfig);
  }

```

ここでようやく`RTCPeerConnection`が出てきました！RTCPeerConnectionはWebRTCのAPIです。
WebRTCのAPIはMozillaのホームページなどに内容の説明が出てきます。
[MozillaのWebRTC APIリファレンス](https://developer.mozilla.org/ja/docs/Web/API/RTCPeerConnection)

この`negotiator`クラスが主にWebRTC周りのAPIを呼んでいるクラスでした。

ここまで読んで、なんとなく、各ファイルのうっすら責務がわかってきました。
![Untitled Diagram.png](https://qiita-image-store.s3.amazonaws.com/0/125528/f6308d45-25cd-bc4a-6dad-0b32f9544956.png)
 


## まとめ
という感じでざっくり、SkywayのJavaScript SDKの中身で、接続開始のWebRTC APIを叩くところまで呼んでみました。
WebRTCを用いたチャットアプリ、ビデオチャットを作りたい場合には、
WebRTCの実装をうまいこと抽象化してくれているSkywayは非常に便利です。

**「ほぉ...WebRTCってやっぱよくわからんな...、Skyway偉大...!」**という当初の結論を変えず、本記事を締めたいと思います。
どうもありがとうございました。