Smart fullscreen with auto-fit for BongaCams and mirrors. Detects black borders in video frames and switches between contain/cover modes. Supports manual override.
// ==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('Скрипт инициализирован успешно');
})();