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.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

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