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.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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