説明 #
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);
})();