Chrome & Edge 拡張機能:GitHub へ動画貼り付け時に <video> タグに自動変換する

Chrome & Edge 拡張機能:GitHub へ動画貼り付け時に <video> タグに自動変換する

説明 #

GitHub に動画をアップロードした後に生成されるリンクを Video タグ形式に変換するブラウザ拡張機能です。

GitHub にリンクされた動画ファイルは通常であればプレビュー表示されますが、テーブル内にリンクを貼る場合などは <video> タグとして挿入しないとプレビュー表示されません。この拡張機能を利用することで、動画リンクを Video タグ形式に変換する手間を省くことができます。

拡張機能

対応ブラウザ #

Chrome と Edge にて動作確認済みです。

仕組み #

GitHub のテキストエリアからフォーカスが外れたタイミングで、テキストエリアに含まれる URL を抽出し、fetch API でリクエストを送ります。レスポンスの Content-Type ヘッダを確認し、動画ファイル(video/ で始まる MIME Type)であれば Video タグ形式に置換する形でテキストエリアの内容を更新します。

インストール方法 #

ローカルに以下のような拡張機能用ディレクトリ(ディレクトリ名はなんでも可)を作成してブラウザで読み込むことで利用できます。

└ my-extension/
  ├ manifest.json
  ├ background.js
  └ content.js

例えば Google Chrome の場合は chrome://extensions を開き、Developer mode をオンにして、Load unpacked から拡張機能用ディレクトリを選択してください。

拡張機能

拡張機能

コード #

manifest.json #

{
  "manifest_version": 3,
  "name": "GitHub Video Tag Converter",
  "version": "1.0.0",
  "content_scripts": [
    {
      "matches": ["https://github.com/*"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ],
  "background": {
    "service_worker": "background.js"
  },
  "host_permissions": [
    "https://github.com/user-attachments/assets/*",
    "https://*.s3.amazonaws.com/*"
  ]
}

background.js #

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (message.type !== "isVideoUrl") {
    return false;
  }

  (async () => {
    try {
      const response = await fetch(message.url, {
        headers: {
          Range: "bytes=0-0",
        },
        credentials: "include",
      });
      await response.arrayBuffer();
      const contentType = response.headers.get("content-type");
      const isVideo = contentType?.toLowerCase().startsWith("video/");
      sendResponse(response.ok && isVideo);
    } catch (_error) {
      sendResponse(false);
    }
  })();

  return true;
});

content.js #

(function () {
  "use strict";

  const GITHUB_ATTACHMENT_LINK_PATTERN =
    /!?\[[^\]\n]*\]\((https:\/\/github\.com\/user-attachments\/assets\/[0-9a-f-]+)\)/g;
  const GITHUB_ATTACHMENT_BARE_URL_PATTERN =
    /(?<!["(])(https:\/\/github\.com\/user-attachments\/assets\/[0-9a-f-]+)/g;

  const isVideoUrl = async (url) =>
    await chrome.runtime.sendMessage({ type: "isVideoUrl", url });

  const toVideoTag = async (markdown) => {
    const urls = [
      ...new Set(
        [
          ...markdown.matchAll(GITHUB_ATTACHMENT_LINK_PATTERN),
          ...markdown.matchAll(GITHUB_ATTACHMENT_BARE_URL_PATTERN),
        ].map(([, url]) => url),
      ),
    ];

    const videoUrls = new Set(
      (
        await Promise.all(
          urls.map(async (url) => ((await isVideoUrl(url)) ? url : null)),
        )
      ).filter(Boolean),
    );

    const replaceWithVideoTag = (matchedText, url) =>
      videoUrls.has(url)
        ? `<video src="${url}" controls></video>`
        : matchedText;

    return markdown
      .replace(GITHUB_ATTACHMENT_LINK_PATTERN, replaceWithVideoTag)
      .replace(GITHUB_ATTACHMENT_BARE_URL_PATTERN, replaceWithVideoTag);
  };

  const handleFocusout = async (event) => {
    const textarea = event.target;
    if (!(textarea instanceof HTMLTextAreaElement)) {
      return;
    }
    const currentValue = textarea.value;
    const nextValue = await toVideoTag(currentValue);
    if (textarea.value !== currentValue || textarea.value === nextValue) {
      return;
    }
    textarea.value = nextValue;
    textarea.dispatchEvent(new InputEvent("input", { bubbles: true }));
  };

  const addFocusoutEventListener = (listener) => {
    document.addEventListener("focusout", listener, true);
  };

  addFocusoutEventListener(handleFocusout);
})();