Bilibili 直播截图助手

B站直播截图工具,支援快捷键截图、连拍模式、支援自定义快捷键、连拍间隔设定、中英菜单切换

目前为 2025-04-12 提交的版本。查看 最新版本

// ==UserScript==
// @name         Bilibili Live Screenshot Helper
// @name:zh-TW   Bilibili 直播截圖助手
// @name:zh-CN   Bilibili 直播截图助手
// @namespace    https://www.tampermonkey.net/
// @version      1.14
// @description  Bilibili Live Screenshot Tool – supports hotkey capture, burst mode, customizable hotkeys, burst interval settings, and menu language switch between Chinese and English.
// @description:zh-TW B站直播截圖工具,支援快捷鍵截圖、連拍模式、支援自定義快捷鍵、連拍間隔設定、中英菜單切換
// @description:zh-CN B站直播截图工具,支援快捷键截图、连拍模式、支援自定义快捷键、连拍间隔设定、中英菜单切换
// @author       ChatGPT
// @match        https://live.bilibili.com/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  const DEFAULT_KEY = 'S';
  const DEFAULT_INTERVAL = 1000;
  const MIN_INTERVAL = 100;
  const SETTINGS_LOCK_KEY = 'screenshotHelperSettingsLock';

  const LANGS = {
    EN: {
      screenshot: 'Screenshot',
      keySetting: key => `Set Screenshot Key (Current: ${key})`,
      intervalSetting: val => `Set Burst Interval (Current: ${val}ms)`,
      langToggle: 'Language: EN',
      keyPrompt: 'Enter new key (A-Z)',
      intervalPrompt: 'Enter new interval in ms (>= 100)',
    },
    ZH: {
      screenshot: '截圖',
      keySetting: key => `設定截圖快捷鍵(目前:${key})`,
      intervalSetting: val => `設定連拍間隔(目前:${val} 毫秒)`,
      langToggle: '語言:ZH',
      keyPrompt: '輸入新快捷鍵(A-Z)',
      intervalPrompt: '輸入新的連拍間隔(最小 100ms)',
    }
  };

  let lang = GM_getValue('lang', 'EN');
  let currentKey = GM_getValue('hotkey', DEFAULT_KEY);
  let interval = GM_getValue('interval', DEFAULT_INTERVAL);
  const t = () => LANGS[lang];

  async function promptWithLock(action) {
    if (localStorage.getItem(SETTINGS_LOCK_KEY) === '1') return;
    localStorage.setItem(SETTINGS_LOCK_KEY, '1');
    await new Promise(resolve => requestAnimationFrame(resolve));
    localStorage.removeItem(SETTINGS_LOCK_KEY);
    action();
  }

  GM_registerMenuCommand(t().keySetting(currentKey), () => {
    promptWithLock(() => {
      const input = prompt(t().keyPrompt);
      if (input && /^[a-zA-Z]$/.test(input)) {
        const newKey = input.toUpperCase();
        if (newKey !== currentKey) {
          GM_setValue('hotkey', newKey);
          location.reload();
        }
      }
    });
  });

  GM_registerMenuCommand(t().intervalSetting(interval), () => {
    promptWithLock(() => {
      const input = prompt(t().intervalPrompt);
      const val = parseInt(input);
      if (!isNaN(val) && val >= MIN_INTERVAL && val !== interval) {
        GM_setValue('interval', val);
        location.reload();
      }
    });
  });

  GM_registerMenuCommand(t().langToggle, () => {
    promptWithLock(() => {
      GM_setValue('lang', lang === 'EN' ? 'ZH' : 'EN');
      location.reload();
    });
  });

  function takeScreenshot() {
    const video = document.querySelector('video');
    if (!video || video.readyState < 2 || video.videoWidth === 0) return;

    const canvas = document.createElement('canvas');
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

    const now = new Date();
    const pad = n => n.toString().padStart(2, '0');
    const padMs = n => n.toString().padStart(3, '0');

    const videoTime = video.currentTime;
    const h = pad(Math.floor(videoTime / 3600));
    const m = pad(Math.floor((videoTime % 3600) / 60));
    const s = pad(Math.floor(videoTime % 60));
    const ms = padMs(Math.floor((videoTime * 1000) % 1000));

    const roomIdMatch = location.pathname.match(/^\/(\d+)/);
    const roomId = roomIdMatch ? roomIdMatch[1] : 'UnknownRoom';

    // 使用當前瀏覽器的日期時間
    const dateStr = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
    const filename = `${dateStr}_${pad(now.getHours())}_${pad(now.getMinutes())}_${pad(now.getSeconds())}_${padMs(now.getMilliseconds())}_${roomId}_${canvas.width}x${canvas.height}.png`;

    canvas.toBlob(blob => {
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      a.click();
      URL.revokeObjectURL(url);
    }, 'image/png');
  }

  let holdTimer = null;
  document.addEventListener('keydown', e => {
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
    if (e.key.toUpperCase() === currentKey && !holdTimer) {
      takeScreenshot();
      holdTimer = setInterval(takeScreenshot, interval);
    }
  });

  document.addEventListener('keyup', e => {
    if (e.key.toUpperCase() === currentKey && holdTimer) {
      clearInterval(holdTimer);
      holdTimer = null;
    }
  });
})();