Programming Field

[Web] メディアのキャプチャーと阻止方法

※ このページではキャプチャー方法を紹介していますが、その方法の使用は自己責任とします。

HTMLの「audio」および「video」要素は、特定の手順を踏めばデータ(画面および音声)をキャプチャーすることができます。ここでは、その方法とそれを阻止することができるかについて紹介しています。

video 要素のキャプチャー

video 要素は、簡単には以下のように記述して生成します。

<video src="video-url"></video>

(また、document.createElement を使って動的に生成することもできます。)

この video 要素で再生される動画の画面をキャプチャーするには、canvas 要素を用います。canvas 要素の 2d コンテキストを取得し、drawImage に video 要素を与えることで、再生中の動画の画面をキャプチャーすることができます。

// video: video 要素のインスタンス
function startCapture(video) {
  // 要素の作成
  const canvas = document.createElement('canvas');
  canvas.width = video.width;
  canvas.height = video.height;

  // コンテキストを取得する
  const canvasCtx = canvas.getContext('2d');

  // video要素の映像をcanvasに描画する
  _canvasUpdate();

  function _canvasUpdate() {
    canvasCtx.drawImage(video, 0, 0, canvas.width, canvas.height);
    // 描画された内容を画像データとして取得
    // (または .toBlob を使う)
    console.log(canvas.toDataURL());

    // 毎フレーム描画する
    requestAnimationFrame(_canvasUpdate);
  }
}

※ 上記では毎フレーム描画していますが、特定のフレームだけほしい場合は video の再生を一時停止してシンプルに drawImage すれば問題ありません。

音声のキャプチャー(video および audio)

音声をキャプチャーする場合は、Web Audio APIの MediaElementAudioSourceNode を中心に、キャプチャー処理に AudioWorkletNode を用いて実装します。MediaElementAudioSourceNode はソースに video 要素・audio 要素どちらも使用できるため、それらの音声データを Web Audio API の中で扱えるようになります。

AudioWorklet は通常はスクリプトをロードして扱いますが、Blob を使って文字列からロードすることもできます。

// element: video または audio 要素のインスタンス
function captureAudio(element) {
  const ac = new AudioContext();
  const workletBlob = makeWorkletScript();
  const workletUrl = URL.createObjectURL(workletBlob);

  ac.audioWorklet.addModule(workletUrl).then(() => {
    const node = new AudioWorkletNode(ac, 'sampler', {
      channelCount: 2,
      numberOfInputs: 1,
      numberOfOutputs: 1,
    });
    // let confirmed = false;
    node.port.addEventListener('message', (e) => {
      // キャプチャーしたオーディオフレームを出力
      console.log(e.data.frames.map((b) => new Float32Array(b)));
    });
    node.port.start();
    // MediaElementAudioSourceNode を作成し AudioWorkletNode に接続
    const m = ac.createMediaElementSource(element);
    m.connect(node);
    node.connect(ac.destination);
    // あとは element を再生すればキャプチャー可能
  });
}

function makeWorkletScript() {
  // AudioWorklet で実行するソースコード
  const script = `class Processor extends AudioWorkletProcessor {
    constructor() {
      super();
      this.port.start();
    }

    process(inputs, outputs) {
      const frames = [];
      for (let i = 0; i < inputs[0].length; ++i) {
        const x = inputs[0][i];
        outputs[0][i].set(x);
        const f = new Float32Array(x.length);
        f.set(x);
        frames.push(f.buffer);
      }
      this.port.postMessage({ frames }, frames);
    }
  }

  registerProcessor('sampler', Processor);
  `;
  const blob = new Blob([script], { type: 'text/javascript' });
  return blob;
}

ブラウザー拡張経由でのキャプチャー

上記のコードはクライアントサイドでのスクリプトコードのため、Tampermonkey をはじめとしたブラウザー拡張を使用することで、任意のウェブサイトに注入することができます。任意のウェブサイトに注入することができるということは、後述の対応をとっていないウェブサイトでは容易に動画および音声データをキャプチャーできるということを意味します。

※ キャプチャー自体は再生と同じ時間必要としますが、現在の技術では WebDriver などを使ったブラウザー自動化技術が存在するため、複数タブを自動で開いて複数のデータを同時並行でキャプチャーする、ということが理論上可能です。

実際のところ、手元で確認(2024年6月時点)した限りでは、YouTubeやSpotifyで音声データをキャプチャーする実験を行ったところ、wavファイルに書き出すことに成功してしまいました。これらのサイトでは「Encrypted Media Extensions API」(暗号化技術)を使ってネットワークレイヤーでは暗号化したデータを使っていますが、上記の方法はデコード・復号化後のデータが扱われるため、暗号化技術を用いるだけでは効果が無いということになります。必要なJSコードも大した量にならないので、比較的カジュアルにキャプチャーできると言えます。

※ Firefox のみ、MediaElementAudioSourceNode で先に接続してからメディアの setMediaKeys が行われると例外が発生するようです(バグ?)。

キャプチャーをブロックする方法 (失敗例)

ではキャプチャーをブロックするにはどうすればよいか、ですが、単純に思い付くのは「canvas や Web Audio API をサイト内で無効化する」という手段です。例えば、

function blockCanvas() {
  const ce = document.createElement;
  document.createElement = (...args) => {
    // canvas 要素を作成しようとする場合のみブロック
    if (args[0].toLowerCase() === 'canvas') {
      throw new Error('Blocked!');
	}
    return ce.apply(document, args);
  };
  // これ以上書き換えを許さない
  Object.freeze(document);
}

function blockWebAudio() {
  // AudioContext を使わせない
  delete window.AudioContext;
  delete window.OfflineAudioContext;
}

という関数を書けば、スクリプトから canvas や Web Audio を使うことはできなくなるように見えるかもしれません。

しかし、ブラウザー拡張のスクリプトは設定によりページのスクリプトよりも前に実行できるため、

// ブラウザー拡張側がこのようなコードを書いたら阻止することができない
const savedCreateElement = document.createElement;
const savedAudioContext = AudioContext;

というコードが実行されたらこれを防止することができません。

HTMLCanvasElement.prototypeAudioContext.prototype などをブロックしたところで話は変わりません。

キャプチャーをブロックする方法 (成功例?)

ここまでだとキャプチャーをブロックする方法が無いように見えますが、やや裏技的にブロックする方法が存在します。

それは、「メディアをウェブサイトと異なるオリジンに置き、オリジン間リソース共有(CORS)を許可しない(サーバーが Access-Control-Allow-Origin を返さない)ようにする」という方法です。

video 要素や audio 要素は、既定では異なるオリジンのURLでも問題なく読み込み、再生することが可能です。ところが、そのようなケースにおいては、canvas の drawImage や Web Audio API の MediaElementAudioSourceNode を使おうとしたタイミングで制約に引っかかって使用できないという仕様があります。

この方法の場合、サーバー側で適切な設定をすればよいだけであるため、ハックされたブラウザーでもなければJSを使ったキャプチャーはできないということになります。

※ ハックされたブラウザーだと何でもできてしまうため、何も対策できませんが、カジュアルな方法ではないのでそこまでの考慮は不要かと思います。

※ CORS無効の状態で「Encrypted Media Extensions API」を使えるかは未確認ですが、「Encrypted Media Extensions API」でCORSが関与するのは鍵取得の部分のみのため、そこさえCORSが許可されていれば問題ないと考えられます。

まとめ

  • video 要素の画面キャプチャーは canvas 要素を使って容易にできます
  • video 要素・audio 要素の音声のキャプチャーは Web Audio API を使ってできます
  • ブラウザー拡張を使えばウェブサイトにスクリプトを自由に注入することができるので、スクリプトレベルではキャプチャーを防止することはできません
  • CORSの仕様を逆手にとってCORSを許可しないようにすれば、キャプチャーすることができなくなると考えられます