WebdriverIO で動画を撮影しアニメ GIF にする

WebdriverIO で動画を撮影しアニメ GIF にする

WebdriverIO v5 には Video Reporter があるので動画の撮影が可能です。

まず MP4 動画を生成してアニメ GIF を作成することになりますが、その際 MP4 そのものではなく MP4 を生成するのに使用された PNG を使ってアニメ GIF を作成したいと思います。

これによりある程度 GIF アニメのサイズを削減できます。

生成例

これは前回の WebdriverIO でファイルの添付・ペースト・ドロップをテストする - Qiita を撮影したものです。

paste-file--paste--CHROME--2019-08-17--10-56-22-850.gif

時間での区切りではなく、各 setValue などのイベントでのコマ送りになります。

Video Reporter のインストール

module のインストール

Video Reporter ページ上の指示に従いインストールします。

yarn add wdio-video-reporter
# or
npm install wdio-video-reporter

wdio.conf.js 編集

成否に関わらず動画を残したいので saveAllVideos: true とし、普通のレポートも出しておきたいので spec は残しておきます。

// 追加
const video = require('wdio-video-reporter');

exports.config = {
 // 略
    reporters: [
        'spec',
        // 追加
        [video, {
            saveAllVideos: true,
            videoSlowdownMultiplier: 3,
        }]
    ],
 // 略
}

これでテストごとに動画が撮影されるようになりました。

アニメ GIF 生成

スクリプト全景

const fs = require('fs');
const path = require('path');
const glob = require('glob');
const execSync = require('child_process').execSync;

const movies = path.join(__dirname, './_results_');
const raws = path.join(movies, 'rawSeleniumVideoGrabs');
const animations = path.join(__dirname, './animations');

function fromMp4() {
  prepare();

  glob.sync(path.join(movies, '*.mp4')).forEach(function (mp4) {
    const name = path.basename(mp4, '.mp4');
    const gif = path.join(animations, `${name}.gif`);
    execSync(`ffmpeg -i ${mp4} -filter_complex "[0:v] fps=10,setpts=PTS/0.25,scale=640:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" ${gif}`)
  });
}

function fromPng() {
  prepare();

  glob.sync(path.join(raws, '*')).forEach(function (dir) {
    const name = path.basename(dir);
    const gif = path.join(animations, `${name}.gif`);
    execSync(`ffmpeg  -r 1 -i ${dir}/%04d.png -filter_complex "[0:v] scale=640:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" ${gif}`)
  });
}

function prepare() {
  clearAnimations();
  fs.existsSync(animations) || fs.mkdirSync(animations);
}

function clearAnimations() {
  glob.sync(path.join(animations, '**/*.gif')).forEach(fs.unlinkSync);
}

function clearMovies() {
  glob.sync(path.join(raws, '**/*.png')).forEach(fs.unlinkSync);
  glob.sync(path.join(raws, '*')).forEach(fs.rmdirSync);
  glob.sync(path.join(movies, '**/*.mp4')).forEach(fs.unlinkSync);
}

function clear() {
  clearAnimations();
  clearMovies();
}

switch (process.argv[2]) {
  case '-p':
    return fromPng();
  case '-m':
    return fromMp4();
  case '-c':
    return clear();
  default:
    console.error('require -p to encode from png, -m to encode from mp4 or -c to clear movies');
}

Video Reporter の生成するファイル

デフォルトでは _results_ というディレクトリに MP4 が生成されます。そしてそのディレクトリの中の rawSeleniumVideoGrabs に元となった PMG が残されています。

const movies = path.join(__dirname, './_results_');
const raws = path.join(movies, 'rawSeleniumVideoGrabs');

PNG -> GIF

execSync(`ffmpeg  -r 1 -i ${dir}/%04d.png -filter_complex "[0:v] scale=640:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" ${gif}`);

ffmpeg には PNG からアニメ GIF を作成する機能があるのでそれを使用しています。

MP4 -> GIF

execSync(`ffmpeg -i ${mp4} -filter_complex "[0:v] fps=10,setpts=PTS/0.25,scale=640:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" ${gif}`);

同じく ffmpeg の機能そのままですが、パレット抽出のオプションには ffmpegでとにかく綺麗なGIFを作りたい - Qiita を参考にさせていただきました。

その他

Video Reporter はファイルをどんどん積んでいくので、消去用のスクリプトを用意しました。

package.json 編集

各動作を yarn gif:foo で呼べるようにしておきます。

{
  // 略
  "scripts": {
    "gif:clear": "node ./encode.js -c",
    "gif:png": "node ./encode.js -p",
    "gif:mp4": "node ./encode.js -m",
    "test": "wdio wdio.conf.js"
  },
  // 略
}

まとめ

これで動画は GIF のみを許可している場所でも操作状況をシェアできるようになりました。