Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

こんにちは 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も見てみます。

  "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-loaderpower-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を見てみましょう。

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のリファレンス

Peer - JS SDK API Reference - Google Chrome 2019-03-30 00.12.28.png

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

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()部のソースコードを見てましょう。

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();
  }

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

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リファレンス

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

ここまで読んで、なんとなく、各ファイルのうっすら責務がわかってきました。
Untitled Diagram.png

まとめ

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

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

pco2699
最近の関心: アルゴリズム/TypeScript/Flutter/Go/enebular
https://twitter.com/pco2699/
iotlt
IoT縛りの勉強会です。 毎月イベントを実施しているので是非遊びに来てください! 登壇者を中心にQiitaでも情報発信していきます。 https://iotlt.connpass.com
https://iotlt.connpass.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした