🧨

Web会議で自分の映像を派手派手にできるChrome拡張を作ってみた

2021/12/14に公開

はじめに

この記事は、 NTT Communications Advent Calendar 2021 14日目の記事です

どうもこんにちは
昨年はこんな記事を書いたtetrapod117です。

https://qiita.com/tetrapod117/items/648b05d250b436977f14

今年もアドベントカレンダーのネタを色々考えていった結果、またChrome拡張を作ることになりました。

何作ったの?

某アニメ「キル◯キル」風の文字を自分のカメラに出すChrome拡張です。
これを使えばWeb会議などで自分の映像が派手派手になります。

こんな感じです

今回はNeworkを使ってますが、他のWebブラウザを使ったビデオ会議サービス(Google Meetとか)でも普通に使えると思います。

https://nework.app/top

出てくる文字は設定できて、文字の色は赤と黄色から選べます
(ちなみに黄色だとキルミ◯ベイベーっぽくなります)

完成品のリポジトリはこちらになります
https://github.com/tetrapod117/draw-text-camera

なぜ作ろうと思ったか?

先日、Web会議の時にバーチャル名刺背景を使ってるチームメンバーの見て、なるほど。
色々文字が出てくる感じはなんかキル◯キルっぽいなぁと思いました。
たしかに、名刺で名前出してたらこの人誰かわかりやすいし便利です。

でも、ドンとでっかいテロップで名前とか色々出てくるのはやっぱりキル◯キルしか出てこないので、それならもっとキル◯キルっぽくなるやつを作ってみよう!という感じです。

ちなみに名刺背景で使ってたのはこちらのサービスです。
https://zoom.social-business-card.com/

実装

ということで実装方法について書いていきます。

今回はChrome拡張でCanvas APIを利用するのでJavaScriptを使います。

大きく分けて以下の2つのやることがあります。

  1. 自分のカメラの映像に大きく文字を出す
  2. その映像をWeb会議サービスで使えるようにする

getUserMediaで取得した映像をcanvasに映す

まずはCanvas APIを使ってカメラの映像をCanvasに描画します

適当にHTMLファイルを用意して、videoタグとcanvasタグを作ります。

<!DOCTYPE html>
<html lang="en">
    
<body>
    <p>localVideo</p>
    <video id="localVideo" width="360px" height="240px" autoplay muted playsinline></video>

    <p>canvas</p>
    <canvas id="canvas" width="360px" height="240px"></canvas>
    <script type="module"  src="./index.js"></script>
</body>
</html>

次にJavaScript側です
Elementを取得してgetUserMediaでvideo要素に流し込み、
canvasではgetContext()で映像が画像などのグラフィックを描画するためのメソッドやプロパティをもつオブジェクトを取得し、drawImage()でvideoの要素を描画、カメラの映像は動画になるのでrequestAnimationFrame()でループさせ更新させます。

const videoElm = document.getElementById('localVideo');
const canvas = document.getElementById("canvas");

// getUserMediaでカメラの映像を取得
navigator.mediaDevices.getUserMedia({video: {
    width: "360px",
    height: "240px"
  }, audio: false})
    .then( stream => {
    // videoエレメントに流す
    videoElm.srcObject = stream;
  }).catch( error => {
    console.error('mediaDevice.getUserMedia() error:', error);
    return;
  });

  const ctx = canvas.getContext('2d');
  // わかりやすいようにcanvas側は反転させておく
  ctx.scale(-1,1);
  ctx.translate(-canvas.width, 0);

  _drawCanvas();

  function _drawCanvas() {
    ctx.drawImage(videoElm, 0, 0, canvas.width, canvas.height);
    requestAnimationFrame(_drawCanvas);
  };

こんな感じでカメラの映像を取得できると思います

canvasに文字を描画する

次にcanvasに文字を描画していきます
canvas上に文字を描画するにはfillText()を使用します。

気をつける所としては、一行ごとに描画が重ねられるのでdrawImageの前にfillTextを呼ぶと隠れてしまうという所ですかね

https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/fillText

  function _drawCanvas() {
    ctx.drawImage(videoElm, 0, 0, canvas.width, canvas.height);
    // 文字を描画する関数を呼ぶ
    _drawText();
    requestAnimationFrame(_drawCanvas);
  };

  function _drawText(){
    // 文字のスタイルを指定
    ctx.font = "50px serif";
    // 文字の色を指定
    ctx.fillStyle = '#ff0000';
    // 文字の配置の指定
    ctx.textBaseline = 'center';
    ctx.textAlign = 'center';
    // 座標の指定(今回は中央やや下)
    const x = (canvas.width / 2);
    const y = (canvas.height / 1.2);
    // 文字の描画
    ctx.fillText("こんにちは", x, y);
  }

こんな感じでcanvasの方の映像に文字が追加されたことがわかります

とりあえずこれでcanvas上に映像と文字を描画することはできるようになりました。

フォントの読み込みについて

さて、ここからさらにキル◯キル雰囲気を出すために必要なものがあります。

あの独特なフォントです。

今回は風ということで雰囲気の近いフリーフォントのキルゴUを利用します
こちらのサイトからダウンロードできます。

http://getsuren.com/killgoU.html

ttfファイルを適当な場所に配置してcssのfont-faceで以下のように指定します。
FILENAMEは適宜置き換えてください。

@font-face {
	font-family: 'kirugo';
	src: url(/font/FILENAME.ttf);
}

そして先程の文字のfontを指定する所でfont-familyの名前を入力します。

// 文字のスタイルを指定
ctx.font = "50px kirugo";

読み込み直すとこんな感じ。結構雰囲気出てきました。

ちなみに、本家はラグランパンチというフォントを使ってるらしいです。
https://fontworks.co.jp/case/3486/

Chrome拡張にする

今のままではローカルの映像に文字を映しただけなので、これをWeb会議サービスでも使えるようにしていきます。

やり方的にはChrome拡張のcontent_scriptを利用して、利用したいWeb会議サービスのページにJSを挿入させ、getUserMediaの処理を上書きしていきます。

まずはmanifest.jsonはこんな感じ

{
    "manifest_version": 2,
    "name": "kirura-video",
    "version": "1.0.0",
    "description": "",
    "content_scripts": [
      {
        "matches": ["https://nework.app/*", "https://meet.google.com/*"],
        "js": ["src/loader.js"],
        "css":["css/style.css"],
        "run_at": "document_start",
        "all_frames": true
      }
    ],
    "web_accessible_resources": [
        "src/index.js",
        "font/FILENAME.ttf"
    ]
  }

重要なのはcontent_scriptsのmatchesに利用したいWeb会議サービスを入れていきます。
今回はNeworkとGoogle Meetの2つを入れました。
あとは、web_accessible_resourcesにページに挿入するJSと利用したいフォントのファイルパスを書くようにしましょう。ここに書いておかないとアクセスができなくてエラーになります。

次にloader.jsです。以下のような感じで<script>要素を作成し、htmlのbodyに書き込むようにしています。

loader.js
//  <script type="module" src="src/index.js"></script>を作成
const script = document.createElement("script");
script.setAttribute("type", "module");
script.setAttribute("src", chrome.extension.getURL("src/index.js"));

// head要素の取得
const head =
  document.head ||
  document.getElementsByTagName("head")[0]
  document.documentElement;

// headにscript を挿入する
head.insertBefore(script, head.lastChild);

次にhtmlに挿入するindex.jsを見ていきます

index.js

// もろもろの要素の設定
const canvas = document.createElement('canvas');
const video  = document.createElement('video');
video.width    = 640
video.height   = 480
video.autoplay = true
canvas.width = 640;
canvas.height = 480;
const ctx = canvas.getContext('2d');
let text = "スパイダーマン";
let color = '#ff0000'
	
// 元々のgetUserMedia()を持っておく
const _getUserMedia = navigator.mediaDevices.getUserMedia.bind(
    navigator.mediaDevices
);

// getUserMedia()の上書きして、カメラのstreamではなくcanvasのstreamを取得する
navigator.mediaDevices.getUserMedia = async function (constraints) {
    // canvasのStream情報を取得する
    const stream = await getCaptureCanvasStream();
    return stream;
};

async function getCaptureCanvasStream(){
    // 所持しているgetUserMediaでカメラの映像を取得
    _getUserMedia({video: {
      width: "640px",
      height: "480px"
    }, audio: false}).then(function(stream) {
	// 非表示のvideoエレメントにWebカメラの映像を表示させる
        video.srcObject = stream;
    });

    // video要素の映像を非表示のcanvasに描画する
    _drawCanvas();
    
    // canvasのStreamを取得
    const stream = canvas.captureStream(10);
    return stream;
}


function _drawCanvas() {
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  // 文字の描画
  _drawText();
  requestAnimationFrame(_drawCanvas);
};

function _drawText(){
  //文字のスタイルを指定
  ctx.font = "70px kirugo";
  ctx.fillStyle = color;
  //文字の配置の指定
  ctx.textBaseline = 'center';
  ctx.textAlign = 'center';
  //座標を指定して文字を描く(座標は画像の中心に)
  var x = (canvas.width / 2);
  var y = (canvas.height / 1.2);
  ctx.fillText(text, x, y);
}

ここでgetUserMediaの処理を上書きしてcanvasのstreamを返すようにしています。

// getUserMedia()の上書きして、カメラのstreamではなくcanvasのstreamを取得する
navigator.mediaDevices.getUserMedia = async function (constraints) {
    // canvasのStream情報を取得する
    const stream = await getCaptureCanvasStream();
    return stream;
};

getCaptureCanvasStreamでは事前に保持したgetUserMediaを使ってカメラ映像を取得しcanvasへの描画をしています。
ここで注意するのが、この際に保持したgetUserMediaではなく、navigator.mediaDevices.getUserMediaを使った場合、getUserMediaがループしめちゃくちゃPCが重たくなります。

async function getCaptureCanvasStream(){
    // 所持しているgetUserMediaでカメラの映像を取得
    _getUserMedia({video: {
      width: "640px",
      height: "480px"
    }, audio: false}).then(function(stream) {
	// 非表示のvideoエレメントにWebカメラの映像を表示させる
        video.srcObject = stream;
    });

    // video要素の映像を非表示のcanvasに描画する
    _drawCanvas();
    
    // canvasのStreamを取得
    const stream = canvas.captureStream(10);
    return stream;
}

chrome拡張でのフォントの利用方法

次にフォントを読み込めるようにします。
ローカルのフォントファイルをCSSで指定しても拡張では読むことができません

拡張上でフォントファイルを読むときはパスにchrome-extension://__MSG_@@extension_id__を入れましょう

https://developer.chrome.com/docs/extensions/reference/i18n/#overview-predefined

@font-face {
	font-family: 'kirugo';
	src: url(chrome-extension://__MSG_@@extension_id__/font/FILENAME.ttf);
}

NeWorkやGoogleMeetで使えるようにする

これで大体できたので、いざNeworkなどのWeb会議サービスで使ってみましょう!

そのまま使ったらエラーになります...

これはWeb会議サービスなどでは最初にマイクだけをgetUserMediaして、マイクが使えるかの検証や音声の通話だけを始めるみたいなのが多いので、Audioをfalseの状態にしていると初期化などに通らなくなります。

getUserMedia({video: {
      width: "640px",
      height: "480px"
    }, audio: false})

ということでこれをなんとかするためにgetUserMediaを上書きしている所の最初にAudioのみ取得したい場合は元のgetUserMediaを返すコードを入れましょう

navigator.mediaDevices.getUserMedia = async function (constraints) {
    // 音声のみが取得された場合は保持しているgetUserMediaを返す
    if (constraints.audio || !constraints.video) {
      return _getUserMedia(constraints);
    }

    // 画面キャプチャのStream情報を取得する
    const stream = await getCaptureCanvasStream();

    return stream;
  };

完成形はこんな感じです
(自分1人しかいなくて反転しています)

参考にしたサイト

作成の際に参考にしたサイトです。めちゃくちゃお世話になりました🙇‍♂️

https://techblog.securesky-tech.com/entry/2020/04/30/
https://qiita.com/massie_g/items/667627b6d12acc0163af
https://zenn.dev/uttk/articles/emoji-live-chrome-extensions

まとめ

という感じで、今回は自分の映像を派手派手にする拡張を作ってみました。
結構勢いで作ったので、streamを切る処理とか細かい部分の実装はまだできていませんのでおいおい追加していこうかと思います。
所管としてはCanvas使えばStreamの加工など色々できるので仮想背景を作ってる方などもいて色々できてもっと面白そうなこと色々できそうだなぁと思います。
作っててSnapCameraとかでもいけるんじゃねとか思いましたが、 こういうのは自分で作って遊ぶのに価値があるのですよね

という感じで終わろうと思います。

明日もお楽しみに!

Discussion