WebRTCでも負荷試験がしたい!

# Puppeteer + Fargate で負荷テストするお話
今回は他のWebRTC負荷テストツールやサービスについては扱いません。
それなりにコストがかかるのと、やっぱりブラウザでの動作するか検証したいからです。


# 「テストするのでこのURL開いてください。ひとり10タブで」
一般的なWebサイトと違い、WebRTCや動画配信サービスで負荷テストをするのはなかなか大変です。
人数が多い会社では、上記のようにアナウンスして協力して貰えば100接続くらいはなんとかなります。
ただし、社内LANに負荷をかけても大丈夫か、あるいはいろんな経路で試したい、など痒いところに手が届きません。

# AWSBatch + Fargateでお手軽に300接続したい!
経路はアマゾンからになってしまいますが、Batch + Fargateを使えばお手軽にヘッドレスChromeで接続することができます。
EC2インスタンスだと100台制限の緩和申請など必要ですが、Fargateだともっとたくさん起動できます。

## dockerファイルが書きたい!

puppeteer docker で検索すると大体同じです。

```dockerfile:Dockerfile
FROM alpine:edge

RUN apk update

# japanese font
RUN apk add --no-cache curl fontconfig font-noto-cjk \
  && fc-cache -fv

# Installs latest Chromium (76) package.
RUN apk add --no-cache \
      chromium \
      nss \
      freetype \
      freetype-dev \
      harfbuzz \
      ca-certificates \
      ttf-freefont \
      nodejs \
      yarn 

# timezone
RUN apk add --update --no-cache tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    echo "Asia/Tokyo" > /etc/timezone && \
    apk del tzdata

# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true

WORKDIR /app
COPY ./package.json /app/package.json
COPY ./yarn.lock /app/yarn.lock
RUN yarn


COPY main.js /app

CMD ["yarn", "start"]
```

あとは ECRにpushしておきます。


## Puppeteerで接続したい!

ちなみに、AWSBatchでは引数を渡すことはできず、他プロセスと識別するにはインデックスを使います。

```js
const uid = process.env.AWS_BATCH_JOB_ARRAY_INDEX
  ? "test_"+process.env.AWS_BATCH_JOB_ARRAY_INDEX
  : "local";
```

puppeteerで接続する処理は基本的なものと同じです。

```js
const puppeteer = require('puppeteer'),
  WebinarURL = "動作確認したいページのURL"

... なんか初期化とか

(async() => {
try{
  const browser = await puppeteer.launch({
    executablePath: "/usr/bin/chromium-browser",
    headless: true,
    ignoreHTTPSErrors: true,
    defaultViewport: null,
    dumpio: false,
    args: ['--disable-setuid-sandbox', '--no-sandbox'] // ←これ大事
  });

  const page = await browser.newPage();

  // 必要ならログインとかする
  // WebRTCのページにアクセスする
  await page.goto(WebinarURL);
  await sleep(500);
  ...

  while(true) {
    // 動画が動いているか確認
    // ログ出力
    sleep(60); // 一分毎にチェックする
  }
  await browser.close();
} catch (err) {
  // catchしておかないとプロセスが終了しない
  console.log(err);
}
})();
```

package.jsonはとてもシンプル

```json:package.json
{
  "name": "client_sama_sama",
  "version": "1.0.0",
  "main": "main.js",
  "dependencies": {
    "puppeteer": "5.5.0",
    "aws-sdk": "2.824.0"
  },
  "scripts": {
    "start": "node main.js"
  },
  "license": "UNLICENSED"
}
```

## 音声が出ているか確認したい!
無理でした。。
いや、音声データが流れていることを確認すればそれでも良いのですが
本当に音が出てるの? というのを確認したかったのです。
ところが、headless chromeだとオーディオキャプチャの実装がないとかで難しそうでした。

```js
      /* デスクトップオーディオを取得して、音がととしるかチェックする。だが、DOMException: Not supported
       * issue : https://github.com/puppeteer/puppeteer/issues/4404
       * やり方 : https://paul.kinlan.me/ja/screen-recorderrecording-microphone-and-the-desktop-audio-at-the-same-time/
      if (!window.analyser) {
        const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        // ノード作成
        const desktopStream = await navigator.mediaDevices.getDisplayMedia({ video:true, audio: true });
        const audioSource = audioCtx.createMediaStreamSource(desktopStream);
        const desktopGain = context.createGain();
        desktopGain.gain.value = 0.7;
        const analyser = audioCtx.createAnalyser();
        // 接続
        audioSource.connect(desktopGain).connect(analyser).connect(audioCtx.destination);
        window.analyser = analyser;
      }
      const analyser = window.analyser;
      // データから音声レベルを取得
      const frequencies = new Uint8Array(analyser.frequencyBinCount);
      analyser.getByteFrequencyData(frequencies);
      const audioLevel = frequencies.reduce(function(previous, current) {
        return previous + current;
      }) / analyser.frequencyBinCount;

      // analyser.frequencyBinCount
      */
```

## 動画が止まっていないか確認したい!

キャンパスにコピーして、0(真っ暗でないか)、前回と同じでないか確認します。
最初の一回、固まった後の復帰、などなど考えると意外と面倒ですがそこは割愛します。

```js
    // ブラウザ上でjavascriptを実行する
    const {ログ出力したい戻り値をここで受け取る...} = await page.evaluate(async() => {
      // ここはブラウザ上

      // 0出ないピクセルがあればなにか写ってる
      function isCanvasSome(ctx) {
        return canvas.getContext("2d")
          .getImageData(0, 0, canvas.width, canvas.height).data
          .some(channel => channel !== 0);
      }
      const video = document.getElementById("remotevideo-Camera");
      if (!video || video.videoWidth === 0) {
        return ...;
      }
      if (!window.tempCanvas) {
        window.tempCanvas = document.createElement("canvas");
      }
      const canvas = window.tempCanvas;
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      const videoCtx = canvas.getContext("2d").drawImage(video, 0, 0, video.videoWidth, video.videoWidth);

       // 音声チェックしたかったけど断念

      return {videoWidthとかgetStats()した戻り値とか}
    }); 

    // 戻り値をログ出力する
```

## CloudWatchでグラフ化がしたい!

cloudwatchに投げてやればいい感じでグラフにできます。
60秒単位より細かく撮ろうとするとコストが上がるので、とりあえず60秒で良いと思います。

- 動画の横幅 width
- ビットレート
- 動画が動いているかフラグ
- その他 RTCPeerConnection.getStats() で取れるパケロスとかジッターとかお好きなもの

すみません。
あまりディメンジョンやら概念が分かっておらず、
とりあえず以下の設定で、動いているか? とwidthがインスタンス毎にグラフ化されます。
大体、配信サーバーを再起動したり帯域上限に引っかかるとwidthが安定しないのが見てとれます。

```js
    const cw = new AWS.CloudWatch({apiVersion: '2010-08-01'});

   cw.putMetricData({
      MetricData: [{
        MetricName: 'VideoOkCount',
        Dimensions: [
          {
            Name: 'name',
            Value: uid,
          },
        ],
        Timestamp: new Date(),
        Unit: 'Count',
        Value: (videoOK && isMoving ? 1 : 0),
      },{
        MetricName: 'VideoWidth',
        Dimensions: [
          {
            Name: 'name',
            Value: uid,
          },
        ],
        Timestamp: new Date(),
        Unit: 'Count',
        Value: videoWidth,
      }],
      Namespace: 'ClinetSamaSama/LoadTest'
    }, (err, data) => { if (err) { console.log(err); } });
```



## AWSの環境構築がしたい!
長くなるので要点だけ記載します。

- API用のアクセスキーを作る
- Batch用のロールを作る
- VPCを作る。
- ステージング環境のアクセス制限がかかっている場合は、VPCエンドポイントで繋ぐなりピアで繋ぐなりセキュリティグループ当てるなりして疎通させる
- Batchのコンピューティング環境を作る
    - vCPU : インスタンスの上限数にする (1インスタンス1vCPUで良い)
- ジョブ定義を作る
    - ロールを作る AmazonECSTaskExecutionRolePolicyをアタッチ
    - パブリックIPを有効にしておく
    - イメージ : pushしたdocker
    - mem : 2GBは欲しいところ
- ジョブキューを作る
- タスク数をお好きなものにして実行する
- CloudWatchにアクセスし、グラフを見て思いを巡らせる。
- 注意 : タスクが終了していても課金され続けることがあるので、コンピューティング環境も削除すると安心です。(使い方を間違っているだけかもしれません)





# 独り言
2,3年のブランクを経て再びWebRTCをやる機会をいただきました。
手が掛かる子ほど可愛いといいますか、やっぱWebRTCいいよなーと思います。
Unix Network Programing と TCP Illustrated と マスタリングTCP/IP RTP編　を読みつつ (全て絶版。。)
そろそろRustでなんか書こうかなー
音声や映像をリアルタイムにサーバー側でAIと繋ぎ込む需要ありそうだなー
クラウドネイティブでスケールするミドルウェアも書きたいなー
と思う今日この頃です。