Wayne's Video Controller & Snapshot

A video speed controller, frame-by-frame navigation, web fullscreen, and video snapshot tool.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Wayne's Video Controller & Snapshot
// @namespace    http://tampermonkey.net/
// @version      1.1.2
// @description  A video speed controller, frame-by-frame navigation, web fullscreen, and video snapshot tool.
// @author       Wayne
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // Inject the styling rules for the overlay, modal, toasts, and custom controls
  const CSS_STYLE = `
/* waynes-video-controller styles - Glassmorphic Video Controller & Snapshots */

@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap');

/* Namespace everything to avoid conflict with hosting page styles */
.aura-controller-overlay {
  display: flex;
  align-items: center;
  gap: 8px;
  position: absolute;
  top: 15px;
  left: 15px;
  z-index: 2147483647; /* Ensure it is on top of full-screen video */
  background: rgba(15, 15, 24, 0.65);
  backdrop-filter: blur(12px) saturate(180%);
  -webkit-backdrop-filter: blur(12px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  padding: 6px 12px;
  box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37),
              0 0 0 1px rgba(255, 255, 255, 0.05) inset;
  font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 14px;
  color: #ffffff;
  user-select: none;
  pointer-events: auto;
  opacity: 0;
  transition: opacity 1s cubic-bezier(0.4, 0, 0.2, 1), 
              transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
              box-shadow 0.3s ease;
  transform: translateY(0);
}

/* Hovering over the overlay makes it visible */
.aura-controller-overlay:hover {
  opacity: 0.98;
  box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.5),
              0 0 10px 0 rgba(99, 102, 241, 0.2);
  transform: translateY(-2px);
  transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1), 
              transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
              box-shadow 0.3s ease;
}

/* Show when video is paused or when hovered */
.aura-controller-overlay.aura-visible {
  opacity: 0.98;
  transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1), 
              transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
              box-shadow 0.3s ease;
}

/* Elements */
.aura-drag-handle {
  cursor: grab;
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255, 255, 255, 0.35);
  padding: 2px;
  margin-right: 2px;
  transition: color 0.2s ease;
}
.aura-drag-handle:hover {
  color: rgba(255, 255, 255, 0.8);
}
.aura-drag-handle:active {
  cursor: grabbing;
}

.aura-speed-display {
  font-weight: 600;
  min-width: 52px;
  text-align: center;
  color: #a5b4fc; /* soft indigo-purple */
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.5px;
  background: rgba(99, 102, 241, 0.12);
  padding: 4px 8px;
  border-radius: 6px;
  border: 1px solid rgba(99, 102, 241, 0.2);
  transition: all 0.2s ease;
  cursor: pointer;
}
.aura-speed-display:hover {
  background: rgba(99, 102, 241, 0.25);
  border-color: rgba(99, 102, 241, 0.4);
  color: #c7d2fe;
}

.aura-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.05);
  color: rgba(255, 255, 255, 0.8);
  cursor: pointer;
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  padding: 0;
}

.aura-btn:hover {
  background: rgba(255, 255, 255, 0.15);
  border-color: rgba(255, 255, 255, 0.2);
  color: #ffffff;
  transform: scale(1.08);
}

.aura-btn:active {
  transform: scale(0.92);
  background: rgba(99, 102, 241, 0.4);
  border-color: #6366f1;
}

/* Specific button glows */
.aura-btn-copy-snap {
  background: rgba(16, 185, 129, 0.1);
  border-color: rgba(16, 185, 129, 0.2);
  color: #a7f3d0;
}
.aura-btn-copy-snap:hover {
  background: rgba(16, 185, 129, 0.25);
  border-color: rgba(16, 185, 129, 0.4);
  color: #ffffff;
  box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
}

.aura-btn-download-snap {
  background: rgba(59, 130, 246, 0.1);
  border-color: rgba(59, 130, 246, 0.2);
  color: #bfdbfe;
}
.aura-btn-download-snap:hover {
  background: rgba(59, 130, 246, 0.25);
  border-color: rgba(59, 130, 246, 0.4);
  color: #ffffff;
  box-shadow: 0 0 10px rgba(59, 130, 246, 0.3);
}

.aura-btn-web-fs {
  background: rgba(236, 72, 153, 0.1);
  border-color: rgba(236, 72, 153, 0.2);
  color: #fbcfe8;
}
.aura-btn-web-fs:hover {
  background: rgba(236, 72, 153, 0.25);
  border-color: rgba(236, 72, 153, 0.4);
  color: #ffffff;
  box-shadow: 0 0 10px rgba(236, 72, 153, 0.3);
}

.aura-btn-close {
  background: rgba(239, 68, 68, 0.1);
  border-color: rgba(239, 68, 68, 0.2);
  color: #fca5a5;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  margin-left: 4px;
}
.aura-btn-close:hover {
  background: rgba(239, 68, 68, 0.3);
  border-color: rgba(239, 68, 68, 0.5);
  color: #ffffff;
}

/* Divider inside controller */
.aura-divider {
  width: 1px;
  height: 18px;
  background: rgba(255, 255, 255, 0.12);
  margin: 0 2px;
}

/* Tooltip styles */
.aura-btn[data-tooltip] {
  position: relative;
}
.aura-btn[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: -32px;
  left: 50%;
  transform: translateX(-50%) scale(0.8);
  background: rgba(10, 10, 15, 0.9);
  border: 1px solid rgba(255, 255, 255, 0.1);
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 10px;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: all 0.15s ease;
  z-index: 10000;
  color: #ffffff;
}
.aura-btn[data-tooltip]:hover::after {
  opacity: 1;
  transform: translateX(-50%) scale(1);
}

/* Toast Notifications */
.aura-toast-container {
  position: absolute;
  top: 15px;
  right: 15px;
  z-index: 2147483647;
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
  font-family: 'Outfit', sans-serif;
}

.aura-toast {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px;
  background: rgba(20, 20, 30, 0.85);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border-left: 4px solid #6366f1;
  border-radius: 8px;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
  color: #ffffff;
  font-size: 13px;
  pointer-events: auto;
  animation: auraSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  max-width: 320px;
  transition: all 0.3s ease;
}

.aura-toast-success {
  border-left-color: #10b981;
}

.aura-toast-error {
  border-left-color: #ef4444;
}

.aura-toast-info {
  border-left-color: #3b82f6;
}

@keyframes auraSlideIn {
  from {
    opacity: 0;
    transform: translateX(30px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

.aura-toast.aura-fade-out {
  opacity: 0;
  transform: translateY(-10px) scale(0.9);
}

/* Modal - Screenshot Preview */
.aura-modal-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: rgba(7, 8, 14, 0.75);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  z-index: 2147483646;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1);
  font-family: 'Outfit', sans-serif;
}

.aura-modal-backdrop.aura-show {
  opacity: 1;
}

.aura-modal-content {
  background: rgba(22, 22, 33, 0.85);
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5),
              0 0 40px 0 rgba(99, 102, 241, 0.15);
  border-radius: 16px;
  width: 90%;
  max-width: 720px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transform: scale(0.92);
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

.aura-modal-backdrop.aura-show .aura-modal-content {
  transform: scale(1);
}

.aura-modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 24px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  background: rgba(15, 15, 24, 0.5);
}

.aura-modal-title {
  font-size: 18px;
  font-weight: 600;
  color: #ffffff;
  background: linear-gradient(135deg, #a5b4fc, #818cf8);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.aura-modal-preview-area {
  padding: 24px;
  display: flex;
  justify-content: center;
  align-items: center;
  background: rgba(10, 10, 15, 0.4);
  max-height: 400px;
  overflow: auto;
}

.aura-modal-img {
  max-width: 100%;
  max-height: 350px;
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  border: 1px solid rgba(255, 255, 255, 0.1);
  object-fit: contain;
}

.aura-modal-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 24px;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  background: rgba(15, 15, 24, 0.5);
}

.aura-modal-info {
  font-size: 12px;
  color: rgba(255, 255, 255, 0.45);
}

.aura-modal-actions {
  display: flex;
  gap: 10px;
}

.aura-btn-primary, .aura-btn-secondary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  font-size: 13px;
  font-weight: 500;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s ease;
  font-family: inherit;
}

.aura-btn-primary {
  background: linear-gradient(135deg, #6366f1, #4f46e5);
  color: #ffffff;
  border: none;
  box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}

.aura-btn-primary:hover {
  background: linear-gradient(135deg, #818cf8, #6366f1);
  box-shadow: 0 6px 16px rgba(79, 70, 229, 0.4);
  transform: translateY(-1px);
}

.aura-btn-primary:active {
  transform: translateY(1px);
}

.aura-btn-secondary {
  background: rgba(255, 255, 255, 0.06);
  color: rgba(255, 255, 255, 0.85);
  border: 1px solid rgba(255, 255, 255, 0.1);
}

.aura-btn-secondary:hover {
  background: rgba(255, 255, 255, 0.12);
  color: #ffffff;
  border-color: rgba(255, 255, 255, 0.2);
}

.aura-btn-secondary:active {
  background: rgba(255, 255, 255, 0.03);
}

/* Custom scrollbars for options/modal components */
.aura-modal-preview-area::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
.aura-modal-preview-area::-webkit-scrollbar-track {
  background: rgba(0,0,0,0.1);
}
.aura-modal-preview-area::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,0.1);
  border-radius: 4px;
}
.aura-modal-preview-area::-webkit-scrollbar-thumb:hover {
  background: rgba(255,255,255,0.25);
}

/* Web Fullscreen modes */
body.aura-web-fullscreen-active {
  overflow: hidden !important;
}

.aura-web-fullscreen-parent {
  /* Bypass stacking context limitations of overflow and transforms */
  overflow: visible !important;
  transform: none !important;
  webkit-transform: none !important;
  perspective: none !important;
  contain: none !important;
  filter: none !important;
  position: relative !important;
  z-index: 2147483646 !important;
}

.aura-web-fullscreen-element {
  position: fixed !important;
  top: 0 !important;
  left: 0 !important;
  width: 100vw !important;
  height: 100vh !important;
  z-index: 2147483647 !important;
  background: #000000 !important;
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
}

.aura-web-fullscreen-element video {
  width: 100% !important;
  height: 100% !important;
  max-width: 100vw !important;
  max-height: 100vh !important;
  object-fit: contain !important;
}

/* active Web Fullscreen button styling */
.aura-web-fullscreen-element .aura-btn-web-fs {
  background: rgba(236, 72, 153, 0.35) !important;
  border-color: #ec4899 !important;
  color: #ffffff !important;
  box-shadow: 0 0 10px rgba(236, 72, 153, 0.4);
}
`;

  // Inject styling into page
  if (typeof GM_addStyle !== 'undefined') {
    GM_addStyle(CSS_STYLE);
  } else {
    const style = document.createElement('style');
    style.textContent = CSS_STYLE;
    document.head.appendChild(style);
  }



  // --- Browser Extension Mocking for UserScript context ---
  const storageMock = {
    sync: {
      get: (keys, callback) => {
        let result = {};
        const keyList = Array.isArray(keys) ? keys : [keys];
        keyList.forEach(k => {
          try {
            const val = GM_getValue(k);
            if (val !== undefined && val !== null) {
              result[k] = JSON.parse(val);
            }
          } catch (e) {
            console.error('Error getting storage key', k, e);
          }
        });
        callback(result);
      },
      set: (items, callback) => {
        Object.keys(items).forEach(k => {
          GM_setValue(k, JSON.stringify(items[k]));
        });
        if (callback) callback();

        // Trigger changes manually for the script's storage listener
        if (storageMock.onChanged._listeners) {
          const changes = {};
          Object.keys(items).forEach(k => {
            changes[k] = { newValue: items[k] };
          });
          storageMock.onChanged._listeners.forEach(cb => {
            try { cb(changes, 'sync'); } catch (e) { }
          });
        }
      }
    },
    onChanged: {
      _listeners: [],
      addListener: (callback) => {
        storageMock.onChanged._listeners.push(callback);
      }
    }
  };

  const browserAPI = {
    storage: storageMock,
    runtime: {
      sendMessage: (message, callback) => {
        if (message.action === 'downloadScreenshot') {
          // Trigger the fallback direct download in content script context since we don't have a background script
          if (callback) callback({ success: false, error: 'UserScript Environment Direct Download Fallback' });
        }
      },
      onMessage: {
        addListener: (callback) => {
          // Expose a function on window for easy external triggering/testing if necessary
          window.__aura_takeTabScreenshot = () => {
            callback({ action: 'takeTabScreenshot' }, {}, (response) => {
              console.log('UserScript screenshot response:', response);
            });
          };
        }
      }
    }
  };

  // --- Original Content Script Logic ---

  // Default settings (updated with user's customized hotkeys)
  let settings = {
    speedStep: 0.25,
    rewindStep: 5,        // Fast Rewind 5s
    advanceStep: 5,       // Fast Forward 5s
    rewindStepLong: 30,   // Long Rewind 30s
    advanceStepLong: 30,  // Long Forward 30s
    snapshotFormat: 'png',
    autoShowOverlay: true,
    rememberSpeed: false,
    lastSpeed: 1.0,
    hotkeys: {
      decreaseSpeed: '-',
      increaseSpeed: '+',
      resetSpeed: '0',
      rewind5s: '4',
      advance5s: '6',
      rewind30s: '7',
      advance30s: '9',
      prevFrame: '/',
      nextFrame: '*',
      copySnapshot: '5',       // Map to 5 key
      downloadSnapshot: '',   // Map to empty string
      toggleOverlay: '1',      // Toggle UI overlay
      toggleWebFullscreen: '3' // Web Fullscreen
    }
  };

  // State management
  let activeVideo = null;
  let hoveredVideo = null;
  let overlayHiddenGlobally = false;
  let preResetSpeed = 1.0;

  // Load settings from storage
  function loadSettings() {
    browserAPI.storage.sync.get(['settings'], (result) => {
      if (result.settings) {
        // Deep merge hotkeys and load others
        settings = {
          ...settings,
          ...result.settings,
          hotkeys: { ...settings.hotkeys, ...(result.settings.hotkeys || {}) }
        };
      }

      // Initialize or update existing controllers
      initializeAllVideos();
    });
  }

  // Monitor storage changes to dynamically apply settings
  browserAPI.storage.onChanged.addListener((changes, namespace) => {
    if (changes.settings) {
      settings = {
        ...settings,
        ...changes.settings.newValue,
        hotkeys: { ...settings.hotkeys, ...(changes.settings.newValue.hotkeys || {}) }
      };
      // Update UI of all active controllers
      document.querySelectorAll('.aura-controller-overlay').forEach(overlay => {
        const video = overlay.videoElement;
        if (video) {
          updateOverlaySpeed(overlay, video.playbackRate);
          // Apply overlay visibility settings
          if (!settings.autoShowOverlay) {
            overlay.style.display = 'none';
          } else if (!overlayHiddenGlobally) {
            overlay.style.display = 'flex';
          }
        }
      });
    }
  });

  // SVG Icons
  const ICONS = {
    drag: `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="currentColor"><path d="M4 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM4 6a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM4 10a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/></svg>`,
    rewindLong: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 17l-5-5 5-5M18 17l-5-5 5-5"/></svg>`,
    rewind: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 19l-7-7 7-7"/></svg>`,
    minus: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
    plus: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
    forward: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5l7 7-7 7"/></svg>`,
    forwardLong: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 17l5-5-5-5M6 17l5-5-5-5"/></svg>`,
    prevFrame: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 20L9 12l10-8v16zM5 19V5"/></svg>`,
    nextFrame: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4l10 8-10 8V4zM19 5v14"/></svg>`,
    camera: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>`,
    download: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>`,
    webFullscreen: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>`,
    close: `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`
  };



  // Initialize controllers for videos in the document
  function initializeAllVideos() {
    const videos = document.getElementsByTagName('video');
    for (let video of videos) {
      setupVideo(video);
    }
  }

  // Setup single video controller
  function setupVideo(video) {
    if (video.dataset.auraInitialized) return;
    video.dataset.auraInitialized = "true";

    // Set initial custom speed if settings specify
    if (settings.rememberSpeed) {
      video.playbackRate = settings.lastSpeed || 1.0;
    }

    // Try to enable CORS on videos if they are cross-origin, so screenshot works
    if (video.src && !video.src.startsWith('blob:') && !video.src.startsWith(window.location.origin)) {
      if (!video.hasAttribute('crossorigin')) {
        // Only set crossorigin anonymous. Warning: some servers might block if they don't return CORS headers.
      }
    }

    // Create the overlay DOM
    const overlay = document.createElement('div');
    overlay.className = 'aura-controller-overlay';

    // Bind video element to overlay for simple references
    overlay.videoElement = video;

    // Apply visibility setting
    if (!settings.autoShowOverlay || overlayHiddenGlobally) {
      overlay.style.display = 'none';
    }

    // HTML Structure
    overlay.innerHTML = `
      <div class="aura-drag-handle" title="Drag to move">${ICONS.drag}</div>
      <button class="aura-btn aura-btn-rewind-long" data-tooltip="Rewind 30s (7)">${ICONS.rewindLong}</button>
      <button class="aura-btn aura-btn-rewind" data-tooltip="Rewind 5s (4)">${ICONS.rewind}</button>
      <button class="aura-btn aura-btn-prev-frame" data-tooltip="Prev Frame (/)">${ICONS.prevFrame}</button>
      <button class="aura-btn aura-btn-minus" data-tooltip="Slower (-)">${ICONS.minus}</button>
      <div class="aura-speed-display" data-tooltip="Reset Speed (0)">${video.playbackRate.toFixed(2)}x</div>
      <button class="aura-btn aura-btn-plus" data-tooltip="Faster (+)">${ICONS.plus}</button>
      <button class="aura-btn aura-btn-next-frame" data-tooltip="Next Frame (*)">${ICONS.nextFrame}</button>
      <button class="aura-btn aura-btn-forward" data-tooltip="Forward 5s (6)">${ICONS.forward}</button>
      <button class="aura-btn aura-btn-forward-long" data-tooltip="Forward 30s (9)">${ICONS.forwardLong}</button>
      <div class="aura-divider"></div>
      <button class="aura-btn aura-btn-copy-snap" data-tooltip="Copy Frame (5)">${ICONS.camera}</button>
      <button class="aura-btn aura-btn-download-snap" data-tooltip="Save Frame">${ICONS.download}</button>
      <button class="aura-btn aura-btn-web-fs" data-tooltip="Web Fullscreen (3)">${ICONS.webFullscreen}</button>
      <button class="aura-btn aura-btn-close" title="Hide Controller (1)">${ICONS.close}</button>
    `;

    // Position overlay inside video container
    let container = video.parentElement;
    if (!container) return;

    // Make parent relative if it is static, to keep overlay scoped
    const containerStyle = window.getComputedStyle(container);
    if (containerStyle.position === 'static') {
      container.style.position = 'relative';
    }

    container.appendChild(overlay);

    // Event listeners for UI updates
    video.addEventListener('ratechange', () => {
      updateOverlaySpeed(overlay, video.playbackRate);
      showOverlayTemporarily();
      if (settings.rememberSpeed) {
        // Save current speed to settings
        settings.lastSpeed = video.playbackRate;
        browserAPI.storage.sync.set({ settings });
      }
    });

    // Make the overlay float visibly when video state changes
    let fadeOutTimeout;
    const showOverlayTemporarily = () => {
      if (overlayHiddenGlobally || !settings.autoShowOverlay) return;
      overlay.classList.add('aura-visible');
      clearTimeout(fadeOutTimeout);
      fadeOutTimeout = setTimeout(() => {
        overlay.classList.remove('aura-visible');
      }, 2000);
    };

    video.addEventListener('play', showOverlayTemporarily);
    video.addEventListener('pause', () => {
      if (!overlayHiddenGlobally && settings.autoShowOverlay) {
        overlay.classList.add('aura-visible');
      }
    });

    // Hover trackers to set active video
    container.addEventListener('mouseenter', () => {
      hoveredVideo = video;
      activeVideo = video;
      if (!overlayHiddenGlobally && settings.autoShowOverlay) {
        overlay.classList.add('aura-visible');
      }
    });

    container.addEventListener('mouseleave', () => {
      hoveredVideo = null;
      if (video.paused) return; // Keep visible if paused
      overlay.classList.remove('aura-visible');
    });

    // Wire up buttons logic
    const btnRewindLong = overlay.querySelector('.aura-btn-rewind-long');
    const btnRewind = overlay.querySelector('.aura-btn-rewind');
    const btnPrevFrame = overlay.querySelector('.aura-btn-prev-frame');
    const btnMinus = overlay.querySelector('.aura-btn-minus');
    const btnPlus = overlay.querySelector('.aura-btn-plus');
    const btnNextFrame = overlay.querySelector('.aura-btn-next-frame');
    const btnForward = overlay.querySelector('.aura-btn-forward');
    const btnForwardLong = overlay.querySelector('.aura-btn-forward-long');

    const btnCopySnap = overlay.querySelector('.aura-btn-copy-snap');
    const btnDownloadSnap = overlay.querySelector('.aura-btn-download-snap');
    const btnWebFs = overlay.querySelector('.aura-btn-web-fs');
    const btnClose = overlay.querySelector('.aura-btn-close');
    const speedDisplay = overlay.querySelector('.aura-speed-display');

    btnRewindLong.addEventListener('click', (e) => { e.stopPropagation(); adjustTime(video, -settings.rewindStepLong); });
    btnRewind.addEventListener('click', (e) => { e.stopPropagation(); adjustTime(video, -settings.rewindStep); });
    btnPrevFrame.addEventListener('click', (e) => { e.stopPropagation(); stepFrame(video, -1); });
    btnMinus.addEventListener('click', (e) => { e.stopPropagation(); adjustSpeed(video, -settings.speedStep); });
    btnPlus.addEventListener('click', (e) => { e.stopPropagation(); adjustSpeed(video, settings.speedStep); });
    btnNextFrame.addEventListener('click', (e) => { e.stopPropagation(); stepFrame(video, 1); });
    btnForward.addEventListener('click', (e) => { e.stopPropagation(); adjustTime(video, settings.advanceStep); });
    btnForwardLong.addEventListener('click', (e) => { e.stopPropagation(); adjustTime(video, settings.advanceStepLong); });

    speedDisplay.addEventListener('click', (e) => {
      e.stopPropagation();
      toggleResetSpeed(video);
    });

    btnCopySnap.addEventListener('click', (e) => {
      e.stopPropagation();
      captureScreenshot(video, 'copy');
    });

    btnDownloadSnap.addEventListener('click', (e) => {
      e.stopPropagation();
      captureScreenshot(video, 'download');
    });

    btnWebFs.addEventListener('click', (e) => {
      e.stopPropagation();
      toggleWebFullscreen(video);
    });

    btnClose.addEventListener('click', (e) => {
      e.stopPropagation();
      overlay.style.display = 'none';
      showToast('Controller hidden. Press "' + settings.hotkeys.toggleOverlay.toUpperCase() + '" to show again.', 'info');
    });

    // Dragging logic
    const dragHandle = overlay.querySelector('.aura-drag-handle');
    setupDragging(overlay, dragHandle, container);
  }

  // Update speed text display
  function updateOverlaySpeed(overlay, rate) {
    const display = overlay.querySelector('.aura-speed-display');
    if (display) {
      display.textContent = rate.toFixed(2) + 'x';
    }
  }

  // Change video time
  function adjustTime(video, amount) {
    video.currentTime = Math.max(0, Math.min(video.duration || 0, video.currentTime + amount));
    showToast(`${amount > 0 ? '+' : ''}${amount}s (${formatTime(video.currentTime)})`, 'info');
  }

  // Frame stepping (Prev / Next frame)
  function stepFrame(video, direction) {
    if (!video) return;
    if (!video.paused) {
      video.pause();
    }
    const fps = 30; // Fallback frame rate
    const frameTime = 1 / fps;
    video.currentTime = Math.max(0, Math.min(video.duration || 0, video.currentTime + (direction * frameTime)));
    showToast(`${direction > 0 ? 'Next' : 'Prev'} frame (${formatTime(video.currentTime)})`, 'info');
  }

  // Change video speed
  function adjustSpeed(video, amount) {
    if (!video) return;
    let rate = parseFloat((video.playbackRate + amount).toFixed(2));
    // Firefox/Chrome limits playback speed between 0.0625 and 16
    rate = Math.max(0.0625, Math.min(16, rate));
    video.playbackRate = rate;

    // Find overlay and update directly to be safe
    const overlay = video.parentElement ? video.parentElement.querySelector('.aura-controller-overlay') : null;
    if (overlay) {
      updateOverlaySpeed(overlay, rate);
    }
    showToast(`Speed: ${rate.toFixed(2)}x`, 'success');
  }

  // Toggle speed reset
  function toggleResetSpeed(video) {
    if (!video) return;
    let targetSpeed = 1.0;
    if (Math.abs(video.playbackRate - 1.0) > 0.01) {
      preResetSpeed = video.playbackRate;
      targetSpeed = 1.0;
    } else {
      targetSpeed = preResetSpeed;
    }
    video.playbackRate = targetSpeed;

    // Find overlay and update directly to be safe
    const overlay = video.parentElement ? video.parentElement.querySelector('.aura-controller-overlay') : null;
    if (overlay) {
      updateOverlaySpeed(overlay, targetSpeed);
    }
    showToast(`Speed: ${targetSpeed.toFixed(2)}x`, 'success');
  }

  // Web Fullscreen toggle
  function toggleWebFullscreen(video) {
    if (!video) return;
    const container = video.parentElement;
    if (!container) return;

    if (container.classList.contains('aura-web-fullscreen-element')) {
      // Exit web fullscreen
      container.classList.remove('aura-web-fullscreen-element');
      document.body.classList.remove('aura-web-fullscreen-active');

      // Remove class from all parent elements
      let parent = container.parentElement;
      while (parent && parent !== document.documentElement) {
        parent.classList.remove('aura-web-fullscreen-parent');
        parent = parent.parentElement;
      }
      showToast('Exit Web Fullscreen', 'info');
    } else {
      // Enter web fullscreen
      container.classList.add('aura-web-fullscreen-element');
      document.body.classList.add('aura-web-fullscreen-active');

      // Add class to all parent elements to override transforms and clipping
      let parent = container.parentElement;
      while (parent && parent !== document.documentElement) {
        parent.classList.add('aura-web-fullscreen-parent');
        parent = parent.parentElement;
      }
      showToast('Enter Web Fullscreen', 'info');
    }
  }

  // Draggable overlay utility
  function setupDragging(overlay, dragHandle, container) {
    let isDragging = false;
    let startX, startY;
    let initialLeft, initialTop;

    dragHandle.addEventListener('mousedown', (e) => {
      e.preventDefault();
      e.stopPropagation();
      isDragging = true;
      startX = e.clientX;
      startY = e.clientY;

      // Read current offsets
      const rect = overlay.getBoundingClientRect();
      const parentRect = container.getBoundingClientRect();
      initialLeft = rect.left - parentRect.left;
      initialTop = rect.top - parentRect.top;

      overlay.style.transition = 'none'; // Disable transition while dragging
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('mouseup', onMouseUp);
    });

    function onMouseMove(e) {
      if (!isDragging) return;

      const dx = e.clientX - startX;
      const dy = e.clientY - startY;

      let newLeft = initialLeft + dx;
      let newTop = initialTop + dy;

      // Keep inside container boundaries
      const containerRect = container.getBoundingClientRect();
      const overlayRect = overlay.getBoundingClientRect();

      const maxLeft = containerRect.width - overlayRect.width;
      const maxTop = containerRect.height - overlayRect.height;

      newLeft = Math.max(0, Math.min(maxLeft, newLeft));
      newTop = Math.max(0, Math.min(maxTop, newTop));

      overlay.style.left = newLeft + 'px';
      overlay.style.top = newTop + 'px';
    }

    function onMouseUp() {
      isDragging = false;
      overlay.style.transition = ''; // Restore transitions
      document.removeEventListener('mousemove', onMouseMove);
      document.removeEventListener('mouseup', onMouseUp);
    }
  }

  // Hotkey handlers
  window.addEventListener('keydown', (e) => {
    // Ignore keyboard shortcuts if writing in text fields
    const activeEl = document.activeElement;
    if (
      activeEl && (
        activeEl.tagName === 'INPUT' ||
        activeEl.tagName === 'TEXTAREA' ||
        activeEl.isContentEditable ||
        activeEl.getAttribute('role') === 'textbox'
      )
    ) {
      return;
    }

    // Get key in lowercase
    const key = e.key.toLowerCase();

    // Prevent non-numpad numeric/math keys from triggering (location 3 is Numpad)
    const isNumpadCandidate = /^[0-9+\-*/=]$/.test(key);
    if (isNumpadCandidate && e.location !== 3) {
      return;
    }

    // Find matching video to control
    const targetVideo = getActiveVideo();
    if (!targetVideo) return;

    // Check if the user is actively watching/interacting with the target video
    const isPlaying = !targetVideo.paused && !targetVideo.ended;
    const isHovered = (hoveredVideo === targetVideo);
    const isWebFullscreen = targetVideo.parentElement && targetVideo.parentElement.classList.contains('aura-web-fullscreen-element');
    const isBrowserFullscreen = document.fullscreenElement === targetVideo || (targetVideo.parentElement && document.fullscreenElement === targetVideo.parentElement);

    if (!isPlaying && !isHovered && !isWebFullscreen && !isBrowserFullscreen) {
      return; // Ignore if not actively watching or interacting
    }

    const keys = settings.hotkeys;

    // Ignore repeating keys for non-incremental hotkeys (toggles, snapshots)
    if (e.repeat && (key === keys.resetSpeed || key === keys.toggleOverlay || key === keys.toggleWebFullscreen || key === keys.copySnapshot || key === '5')) {
      return;
    }

    if (key === keys.decreaseSpeed) {
      e.preventDefault();
      adjustSpeed(targetVideo, -settings.speedStep);
    } else if (key === keys.increaseSpeed || (keys.increaseSpeed === '+' && key === '=')) {
      e.preventDefault();
      adjustSpeed(targetVideo, settings.speedStep);
    } else if (key === keys.resetSpeed) {
      e.preventDefault();
      toggleResetSpeed(targetVideo);
    } else if (key === keys.rewind5s) {
      e.preventDefault();
      adjustTime(targetVideo, -settings.rewindStep);
    } else if (key === keys.advance5s) {
      e.preventDefault();
      adjustTime(targetVideo, settings.advanceStep);
    } else if (key === keys.rewind30s) {
      e.preventDefault();
      adjustTime(targetVideo, -settings.rewindStepLong);
    } else if (key === keys.advance30s) {
      e.preventDefault();
      adjustTime(targetVideo, settings.advanceStepLong);
    } else if (key === keys.prevFrame) {
      e.preventDefault();
      stepFrame(targetVideo, -1);
    } else if (key === keys.nextFrame) {
      e.preventDefault();
      stepFrame(targetVideo, 1);
    } else if (key === keys.copySnapshot || key === '5') { // Support '/' and fallback '5'
      e.preventDefault();
      captureScreenshot(targetVideo, 'copy');
    } else if (key === keys.downloadSnapshot) {
      e.preventDefault();
      captureScreenshot(targetVideo, 'download');
    } else if (key === keys.toggleOverlay) {
      e.preventDefault();
      toggleOverlayVisibility();
    } else if (key === keys.toggleWebFullscreen) {
      e.preventDefault();
      toggleWebFullscreen(targetVideo);
    }
  }, true); // capture phase to override page level video shortcuts when possible

  // Determine which video to control
  function getActiveVideo() {
    if (hoveredVideo && hoveredVideo.offsetWidth > 100) return hoveredVideo;
    if (activeVideo && activeVideo.offsetWidth > 100) return activeVideo;

    // Get all video elements and filter by visibility/size
    const videos = Array.from(document.getElementsByTagName('video')).filter(v => {
      return v.offsetWidth > 100 && v.offsetHeight > 100;
    });

    if (videos.length === 0) {
      // Fallback to any video if none are large enough
      return document.querySelector('video') || null;
    }

    // Prefers the playing one
    const playing = videos.find(v => !v.paused && !v.ended);
    if (playing) return playing;

    // If none are playing, find the one with the largest area (main player)
    return videos.reduce((largest, current) => {
      const largestArea = largest.offsetWidth * largest.offsetHeight;
      const currentArea = current.offsetWidth * current.offsetHeight;
      return currentArea > largestArea ? current : largest;
    }, videos[0]);
  }

  // Global toggle for overlays
  function toggleOverlayVisibility() {
    overlayHiddenGlobally = !overlayHiddenGlobally;
    document.querySelectorAll('.aura-controller-overlay').forEach(overlay => {
      overlay.style.display = overlayHiddenGlobally ? 'none' : (settings.autoShowOverlay ? 'flex' : 'none');
    });
    showToast(overlayHiddenGlobally ? 'Controller overlays hidden' : 'Controller overlays visible', 'info');
  }

  // Format seconds to MM:SS or HH:MM:SS
  function formatTime(secs) {
    const h = Math.floor(secs / 3600);
    const m = Math.floor((secs % 3600) / 60);
    const s = Math.floor(secs % 60);
    const mStr = m.toString().padStart(2, '0');
    const sStr = s.toString().padStart(2, '0');

    if (h > 0) {
      return `${h}:${mStr}:${sStr}`;
    }
    return `${mStr}:${sStr}`;
  }

  // Generate safe filename for screenshot
  function generateFilename(video) {
    const domain = window.location.hostname.replace('www.', '').split('.')[0];
    const pageTitle = document.title
      .replace(/[\\/:*?"<>|]/g, '') // remove illegal filename chars
      .substring(0, 40)             // truncate
      .trim();

    const time = formatTime(video.currentTime).replace(/:/g, '-');
    const ext = settings.snapshotFormat === 'jpeg' ? 'jpg' : 'png';

    return `Snapshot_${domain}_${pageTitle}_[${time}].${ext}`;
  }

  // Core Screenshot Engine
  function captureScreenshot(video, mode = 'copy') {
    const canvas = document.createElement('canvas');
    canvas.width = video.videoWidth || video.clientWidth;
    canvas.height = video.videoHeight || video.clientHeight;

    const ctx = canvas.getContext('2d');

    try {
      // Draw video frame to canvas
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

      const filename = generateFilename(video);

      if (mode === 'copy') {
        // Instantly copy to clipboard
        canvas.toBlob((blob) => {
          try {
            const item = new ClipboardItem({ [blob.type]: blob });
            navigator.clipboard.write([item]).then(() => {
              showToast('Snapshot copied to clipboard!', 'success');
            }).catch(err => {
              console.error('Clipboard copy error:', err);
              showToast('Clipboard block. Downloading instead...', 'error');
              const mimeType = settings.snapshotFormat === 'jpeg' ? 'image/jpeg' : 'image/png';
              const dataUrl = canvas.toDataURL(mimeType, 0.95);
              downloadDataUrl(dataUrl, filename);
            });
          } catch (err) {
            console.error('Clipboard item construction failed:', err);
            showToast('Clipboard write failed. Downloading instead...', 'error');
            const mimeType = settings.snapshotFormat === 'jpeg' ? 'image/jpeg' : 'image/png';
            const dataUrl = canvas.toDataURL(mimeType, 0.95);
            downloadDataUrl(dataUrl, filename);
          }
        }, 'image/png');
      } else if (mode === 'download') {
        // Instantly download
        const mimeType = settings.snapshotFormat === 'jpeg' ? 'image/jpeg' : 'image/png';
        const dataUrl = canvas.toDataURL(mimeType, 0.95);
        downloadDataUrl(dataUrl, filename);
        showToast('Snapshot downloaded: ' + filename.substring(0, 22) + '...', 'success');
      } else {
        // Mode 'preview'
        const mimeType = settings.snapshotFormat === 'jpeg' ? 'image/jpeg' : 'image/png';
        const dataUrl = canvas.toDataURL(mimeType, 0.95);
        showPreviewModal(dataUrl, filename, canvas);
      }
    } catch (error) {
      console.error('Snapshot capture failed:', error);

      // Handle CORS issue
      if (error.name === 'SecurityError') {
        showCORSWarning(video, mode);
      } else {
        showToast('Screenshot capture failed: ' + error.message, 'error');
      }
    }
  }

  // Helper to send data URL to background for downloading
  function downloadDataUrl(dataUrl, filename) {
    browserAPI.runtime.sendMessage({
      action: 'downloadScreenshot',
      url: dataUrl,
      filename: filename
    }, (response) => {
      if (response && !response.success) {
        // fallback download in tab context if background downloads api fails
        const link = document.createElement('a');
        link.href = dataUrl;
        link.download = filename;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
      }
    });
  }

  // Show Toast Notifications
  function showToast(message, type = 'info') {
    let container = document.querySelector('.aura-toast-container-global');
    if (!container) {
      container = document.createElement('div');
      container.className = 'aura-toast-container-global aura-toast-container';

      // Inject to parent of active video or body
      const activeVid = getActiveVideo();
      const parent = activeVid ? activeVid.parentElement : document.body;
      parent.appendChild(container);
    }

    // Attempt to merge matching toasts to prevent spam (speed changes, rewinds/forwards, frame steps)
    let existingToast = null;
    if (message.startsWith('Speed:')) {
      existingToast = Array.from(container.querySelectorAll('.aura-toast')).find(t =>
        t.textContent.includes('Speed:')
      );
    } else if (message.includes('s (') || message.includes('s)')) {
      existingToast = Array.from(container.querySelectorAll('.aura-toast')).find(t =>
        t.textContent.includes('s (') || t.textContent.includes('s)')
      );
    } else if (message.toLowerCase().includes('frame')) {
      existingToast = Array.from(container.querySelectorAll('.aura-toast')).find(t =>
        t.textContent.toLowerCase().includes('frame')
      );
    }

    const setToastTimeout = (el) => {
      el.timeoutId = setTimeout(() => {
        el.classList.add('aura-fade-out');
        el.fadeOutTimeoutId = setTimeout(() => {
          el.remove();
          if (container.children.length === 0) {
            container.remove();
          }
        }, 300);
      }, 2800);
    };

    if (existingToast) {
      const textDiv = existingToast.querySelector('div:last-child');
      if (textDiv) {
        textDiv.textContent = message;
      }

      // Reset fade-out animations and timeouts
      if (existingToast.timeoutId) clearTimeout(existingToast.timeoutId);
      if (existingToast.fadeOutTimeoutId) clearTimeout(existingToast.fadeOutTimeoutId);
      existingToast.classList.remove('aura-fade-out');

      setToastTimeout(existingToast);
      return;
    }

    const toast = document.createElement('div');
    toast.className = `aura-toast aura-toast-${type}`;

    let iconColor = '#a5b4fc';
    if (type === 'success') iconColor = '#10b981';
    if (type === 'error') iconColor = '#ef4444';

    toast.innerHTML = `
      <div style="color: ${iconColor}; display: flex; align-items:center;">
        ${ICONS.camera}
      </div>
      <div>${message}</div>
    `;

    container.appendChild(toast);
    setToastTimeout(toast);
  }

  // Handle CORS Security Error
  function showCORSWarning(video, mode) {
    // Check if crossorigin attribute is not set
    if (!video.hasAttribute('crossorigin')) {
      showToast('Protected source. Attempting bypass...', 'info');

      // Capture current stats to reload
      const currentSrc = video.src;
      const currentTime = video.currentTime;
      const wasPaused = video.paused;

      // Set crossorigin attribute
      video.setAttribute('crossorigin', 'anonymous');

      // Re-trigger load
      if (currentSrc) {
        video.load();

        // Wait for video metadata to reload, restore playtime
        const onLoaded = () => {
          video.currentTime = currentTime;
          if (!wasPaused) {
            video.play().catch(() => { });
          }
          video.removeEventListener('loadedmetadata', onLoaded);

          // Re-attempt snapshot after a short timeout to let stream settle
          setTimeout(() => {
            try {
              captureScreenshot(video, mode);
            } catch (err) {
              showToast('CORS restriction blocks frame capture on this domain.', 'error');
            }
          }, 300);
        };
        video.addEventListener('loadedmetadata', onLoaded);
      }
    } else {
      showToast('CORS security restriction. Frame capture is disabled by server.', 'error');
    }
  }

  // Show Premium Preview Modal
  function showPreviewModal(dataUrl, filename, canvas) {
    // Remove existing if any
    const existing = document.querySelector('.aura-modal-backdrop');
    if (existing) existing.remove();

    const backdrop = document.createElement('div');
    backdrop.className = 'aura-modal-backdrop';

    backdrop.innerHTML = `
      <div class="aura-modal-content">
        <div class="aura-modal-header">
          <div class="aura-modal-title">Snapshot Preview</div>
          <button class="aura-btn aura-btn-close aura-close-modal-btn" title="Close modal">${ICONS.close}</button>
        </div>
        <div class="aura-modal-preview-area">
          <img class="aura-modal-img" src="${dataUrl}" alt="Video snapshot" />
        </div>
        <div class="aura-modal-footer">
          <div class="aura-modal-info">
            Resolution: ${canvas.width}x${canvas.height}
          </div>
          <div class="aura-modal-actions">
            <button class="aura-btn-secondary aura-copy-btn">Copy to Clipboard</button>
            <button class="aura-btn-primary aura-download-btn">Save Snapshot</button>
          </div>
        </div>
      </div>
    `;

    document.body.appendChild(backdrop);

    // Trigger open animation
    setTimeout(() => {
      backdrop.classList.add('aura-show');
    }, 10);

    // Copy to clipboard function
    const copyBtn = backdrop.querySelector('.aura-copy-btn');
    copyBtn.addEventListener('click', () => {
      canvas.toBlob((blob) => {
        try {
          const item = new ClipboardItem({ [blob.type]: blob });
          navigator.clipboard.write([item]).then(() => {
            copyBtn.textContent = 'Copied!';
            copyBtn.style.backgroundColor = 'rgba(16, 185, 129, 0.2)';
            copyBtn.style.borderColor = '#10b981';
            showToast('Snapshot copied to clipboard!', 'success');
            setTimeout(() => {
              copyBtn.textContent = 'Copy to Clipboard';
              copyBtn.style.backgroundColor = '';
              copyBtn.style.borderColor = '';
            }, 2000);
          }).catch(err => {
            console.error('Clipboard copy error:', err);
            showToast('Clipboard copy failed. Direct downloading instead.', 'error');
            downloadDataUrl(dataUrl, filename);
          });
        } catch (err) {
          console.error('Clipboard item construction failed:', err);
          showToast('Clipboard write not supported in context. Download instead.', 'error');
        }
      }, 'image/png');
    });

    // Download action
    const downloadBtn = backdrop.querySelector('.aura-download-btn');
    downloadBtn.addEventListener('click', () => {
      downloadDataUrl(dataUrl, filename);
      closeModal();
      showToast('Snapshot downloaded!', 'success');
    });

    // Close actions
    const closeBtn = backdrop.querySelector('.aura-close-modal-btn');
    closeBtn.addEventListener('click', closeModal);
    backdrop.addEventListener('click', (e) => {
      if (e.target === backdrop) closeModal();
    });

    function closeModal() {
      backdrop.classList.remove('aura-show');
      setTimeout(() => {
        backdrop.remove();
      }, 300);
    }
  }

  // Observe page mutations to discover dynamically loaded video nodes
  function initObserver() {
    // Initial scan
    loadSettings();

    const observer = new MutationObserver((mutations) => {
      let needsScan = false;
      for (let mutation of mutations) {
        if (mutation.addedNodes.length > 0) {
          for (let node of mutation.addedNodes) {
            if (node.nodeType === Node.ELEMENT_NODE) {
              if (node.tagName === 'VIDEO' || node.querySelector('video')) {
                needsScan = true;
                break;
              }
            }
          }
        }
        if (needsScan) break;
      }
      if (needsScan) {
        initializeAllVideos();
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });

    // Register UserScript command menus if Tampermonkey supports it
    if (typeof GM_registerMenuCommand !== 'undefined') {
      GM_registerMenuCommand("📸 Copy Video Snapshot to Clipboard (5)", () => {
        const targetVideo = getActiveVideo();
        if (targetVideo) {
          captureScreenshot(targetVideo, 'copy');
        } else {
          showToast('No active video found on page.', 'error');
        }
      });
      GM_registerMenuCommand("💾 Save Video Snapshot to Disk", () => {
        const targetVideo = getActiveVideo();
        if (targetVideo) {
          captureScreenshot(targetVideo, 'download');
        } else {
          showToast('No active video found on page.', 'error');
        }
      });
      GM_registerMenuCommand("🖥️ Toggle Web Fullscreen (3)", () => {
        const targetVideo = getActiveVideo();
        if (targetVideo) {
          toggleWebFullscreen(targetVideo);
        } else {
          showToast('No active video found on page.', 'error');
        }
      });
      GM_registerMenuCommand("👁️ Toggle Controller Overlays (1)", () => {
        toggleOverlayVisibility();
      });
    }
  }

  // Run extension script when document is ready
  if (document.readyState === 'complete' || document.readyState === 'interactive') {
    initObserver();
  } else {
    document.addEventListener('DOMContentLoaded', initObserver);
  }
})();