Force Video Fullscreen for Bonga

Smart fullscreen with auto-fit for BongaCams and mirrors. Detects black borders in video frames and switches between contain/cover modes. Supports manual override.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Force Video Fullscreen for Bonga
// @name:ru      Полноэкранный режим для Bonga
// @namespace    qwen-helper
// @version      3.1
// @description  Smart fullscreen with auto-fit for BongaCams and mirrors. Detects black borders in video frames and switches between contain/cover modes. Supports manual override.
// @description:ru  Умный полноэкранный режим для BongaCams и зеркал. Определяет чёрные бордюры в кадре и автоматически переключает режимы contain/cover. Поддерживает ручное переключение.
// @author       QwenWhipped
// @include      /^https?:\/\/.*bonga.*/
// @run-at       document-idle
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  // ===== НАСТРОЙКИ =====
  const CONFIG = {
    activateDomain: 'bonga',  // подстрока в домене для активации
    debug: false,              // true — логи в консоль, false — тихо
    toastDuration: 1800,      // длительность toast-сообщений (мс)
  };

  const log = CONFIG.debug ? (...args) => console.log('[FS Script]', ...args) : () => {};

  // Скрипт активируется только если в домене есть подстрока из CONFIG
  if (!location.hostname.includes(CONFIG.activateDomain)) return;

  log('Запуск скрипта на', location.hostname);

  let manualMode = null;
  const detectCache = new WeakMap();

  function injectCss() {
    if (document.getElementById('__fsSmartFix__')) return;
    const style = document.createElement('style');
    style.id = '__fsSmartFix__';
    style.textContent = `
      video:fullscreen,
      video:-webkit-full-screen {
        background: #000 !important;
        width: 100% !important;
        height: 100% !important;
        margin: 0 !important;
        padding: 0 !important;
        border: none !important;
        max-width: 100vw !important;
        max-height: 100vh !important;
      }

      #__fsBtn__ {
        transition: background-color 0.3s ease, transform 0.1s ease;
      }

      #__fsBtn__:hover {
        background: rgba(0, 0, 0, 0.9) !important;
        transform: scale(1.05);
      }

      #__fsBtn__.flash {
        background: #4CAF50 !important;
      }
    `;
    (document.head || document.documentElement).appendChild(style);
  }

  function analyzeFrame(video) {
    try {
      const w = 160, h = 90;
      const canvas = document.createElement('canvas');
      canvas.width = w;
      canvas.height = h;
      const ctx = canvas.getContext('2d', { willReadFrequently: true });
      ctx.drawImage(video, 0, 0, w, h);
      const data = ctx.getImageData(0, 0, w, h).data;

      const colSharp = [];
      for (let x = 1; x < w; x++) {
        let sum = 0, cnt = 0;
        for (let y = 0; y < h; y += 2) {
          const i = (y * w + x) * 4;
          const p = (y * w + x - 1) * 4;
          sum += Math.abs(data[i] - data[p]) +
                 Math.abs(data[i + 1] - data[p + 1]) +
                 Math.abs(data[i + 2] - data[p + 2]);
          cnt++;
        }
        colSharp.push(sum / cnt);
      }

      const avg = (arr) => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
      const left   = avg(colSharp.slice(0, Math.floor(w * 0.20)));
      const right  = avg(colSharp.slice(Math.floor(w * 0.80)));
      const center = avg(colSharp.slice(Math.floor(w * 0.35), Math.floor(w * 0.65)));

      if (center <= 3) return 'dark';
      if (center > left * 2.2 && center > right * 2.2) return 'borders';
      return 'clean';
    } catch (e) {
      return 'cors_blocked';
    }
  }

  function autoDetectMode(video) {
    if (!video) return 'contain';
    if (detectCache.has(video)) return detectCache.get(video);

    const frame = analyzeFrame(video);
    const vw = video.videoWidth || 0;
    const vh = video.videoHeight || 0;
    const streamAR = vw && vh ? vw / vh : 0;

    let mode = 'contain';
    let reason = streamAR && streamAR < 0.8 ? 'вертикальное видео' : 'горизонтальное видео';

    if (frame === 'borders') {
      mode = 'cover';
      reason = 'найдены бордюры';
    } else if (frame === 'cors_blocked') {
      reason = 'CORS блок (fallback)';
    }

    detectCache.set(video, mode);
    video.__fsReason = reason;

    if (!video.__fsResizeAttached) {
      let resizeTimer = null;
      video.addEventListener('resize', () => {
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(() => {
          detectCache.delete(video);
          const active = document.fullscreenElement || document.webkitFullscreenElement;
          if (active === video || (active && active.contains && active.contains(video))) {
            applyFitMode(video, getMode(video));
          }
        }, 200);
      });
      video.__fsResizeAttached = true;
    }

    return mode;
  }

  function getMode(video) {
    return manualMode || autoDetectMode(video);
  }

  function applyFitMode(video, mode) {
    if (!video) return;
    if (!video.__fsOrigSaved) {
      video.__fsOrigFit = video.style.objectFit || '';
      video.__fsOrigPos = video.style.objectPosition || '';
      video.__fsOrigSaved = true;
    }
    video.style.setProperty('object-fit', mode, 'important');
    video.style.setProperty('object-position', 'center center', 'important');
  }

  function resetVideoStyles(video) {
    if (!video) return;
    if (video.__fsOrigSaved) {
      if (video.__fsOrigFit) {
        video.style.setProperty('object-fit', video.__fsOrigFit, 'important');
      } else {
        video.style.removeProperty('object-fit');
      }
      if (video.__fsOrigPos) {
        video.style.setProperty('object-position', video.__fsOrigPos, 'important');
      } else {
        video.style.removeProperty('object-position');
      }
      video.__fsOrigSaved = false;
    } else {
      video.style.removeProperty('object-fit');
      video.style.removeProperty('object-position');
    }
  }

  function showToast(text) {
    let toast = document.getElementById('__fsToast__');
    if (!toast) {
      toast = document.createElement('div');
      toast.id = '__fsToast__';
      toast.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:2147483647;background:rgba(0,0,0,.8);color:#fff;padding:8px 16px;border-radius:8px;font-size:13px;pointer-events:none;opacity:0;transition:opacity .3s;';
      (document.body || document.documentElement).appendChild(toast);
    }
    toast.textContent = text;
    toast.style.opacity = '1';
    clearTimeout(toast.__hideTimer);
    toast.__hideTimer = setTimeout(() => { toast.style.opacity = '0'; }, CONFIG.toastDuration);
  }

  function flashButton() {
    const btn = document.getElementById('__fsBtn__');
    if (!btn) return;
    btn.classList.add('flash');
    setTimeout(() => btn.classList.remove('flash'), 300);
  }

  function getAllVideos() {
    try { return Array.from(document.querySelectorAll('video')); }
    catch (e) { return []; }
  }

  function getLargestVideo() {
    const videos = getAllVideos();
    if (!videos.length) return null;
    return videos.sort((a, b) => {
      const areaA = (a.videoWidth || a.clientWidth || 0) * (a.videoHeight || a.clientHeight || 0);
      const areaB = (b.videoWidth || b.clientWidth || 0) * (b.videoHeight || b.clientHeight || 0);
      return areaB - areaA;
    })[0];
  }

  function requestNativeFullscreen(video) {
    if (!video) return;
    video.controls = true;
    applyFitMode(video, getMode(video));

    try {
      const fn = video.requestFullscreen || video.webkitRequestFullscreen;
      if (fn) {
        const p = fn.call(video);
        if (p && p.catch) p.catch(() => {
          if (video.parentElement) {
            const pFn = video.parentElement.requestFullscreen || video.parentElement.webkitRequestFullscreen;
            if (pFn) pFn.call(video.parentElement);
          }
        });
      }
    } catch (err) {
      log('Fullscreen error:', err);
    }
  }

  function activateFullscreen() {
    const video = getLargestVideo();
    if (!video) return;
    requestNativeFullscreen(video);
    const mode = getMode(video);
    showToast('FS: ' + mode + (manualMode ? ' (ручной)' : ' (авто: ' + (video.__fsReason || '') + ')'));
  }

  function updateBtnText() {
    const btn = document.getElementById('__fsBtn__');
    if (!btn) return;
    const modeLabel = manualMode ? manualMode : 'авто';
    btn.textContent = `FS [${modeLabel}]`;
  }

  function toggleManualMode() {
    const active = document.fullscreenElement || document.webkitFullscreenElement;
    const video = (active && active.tagName === 'VIDEO') ? active : getLargestVideo();

    if (manualMode === null) {
      manualMode = 'cover';
    } else if (manualMode === 'cover') {
      manualMode = 'contain';
    } else {
      manualMode = null;
    }

    if (video) {
      if (manualMode === null) detectCache.delete(video);
      applyFitMode(video, getMode(video));
    }

    updateBtnText();
    flashButton();
    showToast('Режим: ' + (manualMode ? manualMode + ' (ручной)' : 'авто'));
  }

  window.addEventListener('keydown', (e) => {
    if (e.altKey && e.shiftKey && e.code === 'KeyF') {
      e.preventDefault();
      e.stopPropagation();
      activateFullscreen();
    }

    if (e.altKey && e.shiftKey && e.code === 'KeyC') {
      e.preventDefault();
      e.stopPropagation();
      toggleManualMode();
    }

    if (e.altKey && e.shiftKey && e.code === 'KeyA') {
      e.preventDefault();
      e.stopPropagation();
      manualMode = null;
      const active = document.fullscreenElement || document.webkitFullscreenElement;
      const video = (active && active.tagName === 'VIDEO') ? active : getLargestVideo();
      if (video) {
        detectCache.delete(video);
        applyFitMode(video, autoDetectMode(video));
      }
      updateBtnText();
      flashButton();
      showToast('Режим: авто');
    }
  }, true);

  ['fullscreenchange', 'webkitfullscreenchange'].forEach(evt => {
    document.addEventListener(evt, () => {
      const el = document.fullscreenElement || document.webkitFullscreenElement;

      if (el) {
        let video = null;
        if (el.tagName === 'VIDEO') {
          video = el;
        } else if (el.querySelector) {
          video = el.querySelector('video');
        }
        if (video) {
          applyFitMode(video, getMode(video));
        }
      } else {
        getAllVideos().forEach(video => {
          resetVideoStyles(video);
          detectCache.delete(video);
        });
      }
    });
  });

  function addButton() {
    if (document.getElementById('__fsBtn__')) return;
    const btn = document.createElement('button');
    btn.id = '__fsBtn__';
    btn.title = 'ЛКМ: полноэкранный режим | ПКМ: смена режима\nХоткеи: Alt+Shift+F (FS), Alt+Shift+C (Смена), Alt+Shift+A (Авто)';
    btn.style.cssText = 'position:fixed;right:14px;bottom:14px;z-index:2147483647;background:rgba(0,0,0,.75);color:#fff;border:none;border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer;user-select:none;font-weight:bold;box-shadow:0 2px 8px rgba(0,0,0,0.5);';

    btn.addEventListener('click', activateFullscreen);

    btn.addEventListener('contextmenu', (e) => {
      e.preventDefault();
      e.stopPropagation();
      toggleManualMode();
    });

    (document.body || document.documentElement).appendChild(btn);
    updateBtnText();
  }

  function checkForVideo() {
    if (getLargestVideo()) {
      addButton();
    }
  }

  injectCss();

  let debouncedTimer = null;
  const obs = new MutationObserver(() => {
    if (document.getElementById('__fsBtn__')) return;
    clearTimeout(debouncedTimer);
    debouncedTimer = setTimeout(checkForVideo, 300);
  });

  obs.observe(document.documentElement, { childList: true, subtree: true });
  checkForVideo();

  log('Скрипт инициализирован успешно');
})();