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.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==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('Скрипт инициализирован успешно');
})();