Greasy Fork is available in English.
재생중인 화면을 PIP로 전환하고 탐색
// ==UserScript==
// @name SOOP (숲) - 목록 탐색 자동 PIP
// @namespace http://tampermonkey.net/
// @version 40.3
// @description 재생중인 화면을 PIP로 전환하고 탐색
// @author tamszero1, Gemini, Claude
// @license MIT
// @match https://play.sooplive.com/*
// @match https://vod.sooplive.com/*
// @match https://www.sooplive.com/*
// @grant GM_addStyle
// @grant unsafeWindow
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const EXIT_REDIRECT_KEY = 'soop_exit_redirect_url';
const EXIT_REDIRECT_ARMED = 'soop_exit_redirect_armed';
const KEEP_MS = 30000;
const CLICK_TTL = 1500;
const VOD_RE = /^\/(player|vod|catchstory|catch|view)\//i;
const PIP_PATHS = new Set(['/', '/live/all', '/my/favorite', '/search', '/directory/category']);
const DROPDOWN_SEL = '#areaSuggest li, #areaHistory li, #areaRealtime li, #areaRecommend li';
const P_ID = '#player_area,#playerArea,#playerWrap,#player_wrap,#vodPlayer,#webPlayer,#player,#afreecaPlayer,#ap_player,#vodWrap,#vod_player';
const P_CLS = '.player_area,.webplayer_area,.vod_player,.player_wrap,.player-wrap,.player_box,.video_box,.vod_area,.catch_player';
const MENU_IGNORE_SEL = '#userArea, #logArea, #soop-gnb, .loginUserMenu, .profileWrap, .serviceUtil';
const originalOpen = window.open;
const uWin = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const originalTargetOpen = uWin.open;
const isPlayerDomain = location.hostname === 'play.sooplive.com' || location.hostname === 'vod.sooplive.com';
const FAKE_WIN = Object.freeze({
closed: false, close() {}, focus() {}, blur() {},
postMessage() {}, document: { write() {}, close() {} },
location: { href: '' }
});
function isCatchVodPlayer(el) {
return !!(el && el.matches?.('.vod_player') && el.closest('section.catch_webplayer_wrap'));
}
function rememberCatchPlayerSize(player) {
if (!isCatchVodPlayer(player)) return;
if (player.dataset.origWidth == null) player.dataset.origWidth = player.style.width || '';
if (player.dataset.origHeight == null) player.dataset.origHeight = player.style.height || '';
}
function restoreCatchPlayerSize(player) {
if (!isCatchVodPlayer(player)) return;
player.style.width = player.dataset.origWidth || '';
player.style.height = player.dataset.origHeight || '';
delete player.dataset.origWidth;
delete player.dataset.origHeight;
}
function refreshPipPlayerSize() {
const player = document.querySelector('.pip-player');
if (!player || !pipActive || pageOverlayHidden) return;
const oldRect = player.getBoundingClientRect();
applyPipPlayerSize(player);
const { width, height } = getScaledPipSize();
if (savedTop == null || savedLeft == null) {
player.style.top = Math.max(0, window.innerHeight - height - 30) + 'px';
player.style.left = Math.max(0, window.innerWidth - width - 20) + 'px';
return;
}
const maxLeft = Math.max(0, window.innerWidth - width);
const maxTop = Math.max(0, window.innerHeight - height);
player.style.left = Math.min(Math.max(0, oldRect.left), maxLeft) + 'px';
player.style.top = Math.min(Math.max(0, oldRect.top), maxTop) + 'px';
}
function norm(url) {
try { return new URL(url, location.href).href; } catch { return url || ''; }
}
function same(a, b) {
return !!(a && b) && norm(a) === norm(b);
}
function isPlayerUrl(url) {
try {
const u = new URL(url, location.href);
if (u.protocol !== 'https:') return false;
if (u.hostname === 'play.sooplive.com' || u.hostname === 'vod.sooplive.com') return true;
return u.hostname === 'www.sooplive.com' && VOD_RE.test(u.pathname);
} catch { return false; }
}
function shouldPipUrl(url) {
try {
const u = new URL(url, location.href);
if (u.protocol !== 'https:' || u.hostname !== 'www.sooplive.com') return false;
if (isPlayerUrl(u.href)) return false;
if (u.hash && (u.hash === '#' || u.hash.startsWith('#javascript'))) return false;
return PIP_PATHS.has(u.pathname) || u.pathname.startsWith('/directory/category/');
} catch { return false; }
}
function canReflectPanelUrl(url) {
try {
const u = new URL(url, location.href);
if (u.protocol !== 'https:') return false;
if (u.hostname !== 'www.sooplive.com') return false;
if (isPlayerUrl(u.href)) return false;
if (u.hash && (u.hash === '#' || u.hash.startsWith('#javascript'))) return false;
return true;
} catch {
return false;
}
}
function withReload(url) {
try {
const u = new URL(url, location.href);
u.searchParams.set('_r', Date.now().toString(36));
return u.href;
} catch { return url; }
}
function isMenuActionAnchor(a) {
return !!a?.closest?.(MENU_IGNORE_SEL);
}
function getPlainLeftClickAnchor(e) {
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return null;
return e.target.closest?.('a[href]') || null;
}
function isHandledPlayerAnchor(a) {
return !!a &&
isPlayerUrl(a.href) &&
!a.hasAttribute('download') &&
!isMenuActionAnchor(a);
}
function saveExitRedirectUrl(url) {
try {
if (url && canReflectPanelUrl(url)) {
sessionStorage.setItem(EXIT_REDIRECT_KEY, url);
}
} catch {}
}
function loadExitRedirectUrl() {
try {
const url = sessionStorage.getItem(EXIT_REDIRECT_KEY);
return (url && canReflectPanelUrl(url)) ? url : null;
} catch {
return null;
}
}
function armExitRedirect() {
try {
sessionStorage.setItem(EXIT_REDIRECT_ARMED, '1');
} catch {}
}
function disarmExitRedirect() {
try {
sessionStorage.removeItem(EXIT_REDIRECT_ARMED);
} catch {}
}
function isExitRedirectArmed() {
try {
return sessionStorage.getItem(EXIT_REDIRECT_ARMED) === '1';
} catch {
return false;
}
}
function clearExitRedirectState() {
try {
sessionStorage.removeItem(EXIT_REDIRECT_KEY);
sessionStorage.removeItem(EXIT_REDIRECT_ARMED);
} catch {}
}
function nudgeBodyClick() {
try { document.body?.click(); } catch {}
setTimeout(() => { try { document.body?.click(); } catch {} }, 60);
}
function createPlayerGuardState() {
let pendingClick = null;
let pendingBlank = null;
function markClick(anchor) {
if (!anchor?.href || !isPlayerUrl(anchor.href)) return;
if (isMenuActionAnchor(anchor)) return;
pendingClick = {
url: norm(anchor.href),
ts: Date.now(),
targetBlank: anchor.target === '_blank'
};
}
function clearClick() {
pendingClick = null;
}
function getClick() {
if (!pendingClick) return null;
if (Date.now() - pendingClick.ts > CLICK_TTL) {
pendingClick = null;
return null;
}
return pendingClick;
}
function armBlank(anchor) {
if (!anchor?.href || !isPlayerUrl(anchor.href) || anchor.target !== '_blank') return;
if (isMenuActionAnchor(anchor)) return;
pendingBlank = {
url: norm(anchor.href),
ts: Date.now(),
handled: false
};
}
function getBlank() {
if (!pendingBlank) return null;
if (Date.now() - pendingBlank.ts > CLICK_TTL) {
pendingBlank = null;
return null;
}
return pendingBlank;
}
function clearBlank() {
pendingBlank = null;
}
return {
markClick,
clearClick,
getClick,
armBlank,
getBlank,
clearBlank
};
}
let externalClickWatch = null;
function beginExternalClickWatch(e, href, fallback, opts = {}) {
const waitMs = opts.waitMs ?? 90;
if (externalClickWatch) {
clearTimeout(externalClickWatch.timer);
externalClickWatch = null;
}
const startHref = location.href;
const watch = {
href: norm(href),
startHref,
handled: false,
preventedInitially: !!e?.defaultPrevented,
timer: null
};
watch.timer = setTimeout(() => {
if (externalClickWatch !== watch) return;
externalClickWatch = null;
if (watch.handled) return;
if (e?.defaultPrevented && !watch.preventedInitially) return;
if (location.href !== startHref) return;
fallback();
}, waitMs);
externalClickWatch = watch;
}
function markExternalClickHandled() {
if (!externalClickWatch) return;
externalClickWatch.handled = true;
}
function bindPlayerClickMarker(state, onAfterMark) {
document.addEventListener('click', function (e) {
const a = getPlainLeftClickAnchor(e);
if (!a || !isPlayerUrl(a.href) || isMenuActionAnchor(a)) return;
state.markClick(a);
setTimeout(() => {
const p = state.getClick();
if (p && same(p.url, a.href)) state.clearClick();
}, CLICK_TTL + 50);
onAfterMark?.(a);
}, true);
}
function bindBlankGuard(state, navigateFn) {
document.addEventListener('click', function (e) {
const a = getPlainLeftClickAnchor(e);
if (!a || !isHandledPlayerAnchor(a) || a.target !== '_blank') return;
state.armBlank(a);
e.preventDefault();
setTimeout(() => {
const g = state.getBlank();
if (!g) return;
if (!same(g.url, a.href)) return;
if (g.handled) {
state.clearBlank();
return;
}
state.clearBlank();
state.clearClick();
navigateFn(a.href);
}, 0);
}, true);
}
function installOpenHook(w, realOpen, consumeFn, key = '__soopOpenHookInstalled') {
if (!w || w[key]) return;
w[key] = true;
let _real = realOpen || w.open;
function hooked(url, name, specs) {
if (typeof url === 'string') {
try {
const consumed = consumeFn(url, name, specs);
if (consumed) return FAKE_WIN;
} catch {}
}
return _real.call(w, url, name, specs);
}
try {
Object.defineProperty(w, 'open', {
get() { return hooked; },
set(v) { _real = v; },
configurable: true,
enumerable: true
});
} catch {
w.open = hooked;
}
}
function installTopWwwPlayerGuards() {
if (window.__soopTopWwwGuardsInstalled) return;
window.__soopTopWwwGuardsInstalled = true;
const state = createPlayerGuardState();
installOpenHook(window, originalOpen, (url) => {
const pending = state.getClick();
if (!pending || !isPlayerUrl(url) || !same(pending.url, url)) return false;
state.clearClick();
const g = state.getBlank();
if (g && same(g.url, url)) g.handled = true;
location.href = norm(url);
return true;
}, '__soopTopWwwOpen_window');
if (uWin !== window) {
installOpenHook(uWin, originalTargetOpen, (url) => {
const pending = state.getClick();
if (!pending || !isPlayerUrl(url) || !same(pending.url, url)) return false;
state.clearClick();
const g = state.getBlank();
if (g && same(g.url, url)) g.handled = true;
location.href = norm(url);
return true;
}, '__soopTopWwwOpen_uwin');
}
bindPlayerClickMarker(state);
bindBlankGuard(state, (url) => {
location.href = norm(url);
});
}
function installIframePlayerGuards() {
if (window.__soopIframePlayerGuardsInstalled) return;
window.__soopIframePlayerGuardsInstalled = true;
const state = createPlayerGuardState();
let searchBodyClickTimer = null;
let lastSent = '';
installOpenHook(window, window.open, (url) => {
const pending = state.getClick();
if (!pending || !isPlayerUrl(url) || !same(pending.url, url)) return false;
state.clearClick();
const g = state.getBlank();
if (g && same(g.url, url)) g.handled = true;
window.parent.postMessage({ type: 'SOOP_NAV_PLAYER', url }, '*');
return true;
}, '__soopIframeOpen_window');
if (uWin !== window) {
installOpenHook(uWin, uWin.open, (url) => {
const pending = state.getClick();
if (!pending || !isPlayerUrl(url) || !same(pending.url, url)) return false;
state.clearClick();
const g = state.getBlank();
if (g && same(g.url, url)) g.handled = true;
window.parent.postMessage({ type: 'SOOP_NAV_PLAYER', url }, '*');
return true;
}, '__soopIframeOpen_uwin');
}
bindPlayerClickMarker(state);
bindBlankGuard(state, (url) => {
window.parent.postMessage({ type: 'SOOP_NAV_PLAYER', url }, '*');
});
function sendInfo() {
const url = location.href;
const dark =
document.documentElement.classList.contains('dark') ||
document.documentElement.dataset.theme === 'dark' ||
document.body?.classList.contains('dark');
window.parent.postMessage({ type: 'SOOP_THEME', dark }, '*');
if (url !== lastSent && canReflectPanelUrl(url)) {
lastSent = url;
window.parent.postMessage({ type: 'SOOP_PANEL_URL', url }, '*');
try {
const u = new URL(url);
if (u.hostname === 'www.sooplive.com' && u.pathname === '/search') {
clearTimeout(searchBodyClickTimer);
searchBodyClickTimer = setTimeout(() => {
window.parent.postMessage({ type: 'SOOP_PARENT_BODY_CLICK' }, '*');
}, 220);
}
} catch {}
}
try {
window.parent.postMessage({ type: 'SOOP_IFRAME_URL', url }, '*');
} catch {}
}
window.addEventListener('load', sendInfo);
window.addEventListener('popstate', () => setTimeout(sendInfo, 0));
['pushState', 'replaceState'].forEach(method => {
const orig = history[method];
history[method] = function () {
const r = orig.apply(this, arguments);
setTimeout(sendInfo, 0);
return r;
};
});
if (window.navigation && !window.__soopIframeNavHooked) {
window.__soopIframeNavHooked = true;
window.navigation.addEventListener('navigate', function (e) {
const url = e.destination?.url;
if (url && isPlayerUrl(url)) {
e.preventDefault();
window.parent.postMessage({ type: 'SOOP_NAV_PLAYER', url }, '*');
}
});
}
document.addEventListener('mousedown', function (e) {
if (!e.target.closest('a, button, input, textarea, select, [role="button"]')) {
setTimeout(() => window.parent.postMessage({ type: 'SOOP_PARENT_BODY_CLICK' }, '*'), 0);
}
}, true);
}
if (!isPlayerDomain) {
if (location.hostname !== 'www.sooplive.com') return;
if (window.self === window.top) {
installTopWwwPlayerGuards();
return;
}
{
const style = document.createElement('style');
let css = `
*{text-rendering:optimizeSpeed!important}
video:not([controls]):not([src^="blob:"]),.thumbs_box .thumb,.broad_thumb,iframe[title*="광고"]{display:none!important}
[class*="preview"] video,[class*="Preview"] video,[class*="modal"] video{display:block!important}
.thumbs_box{contain:layout paint}
body{overflow-x:hidden}
`;
if (location.href.includes('/my/favorite')) {
css += `
div[class*="list_wrap"],ul{
content-visibility:visible!important;
contain:none!important
}
.thumbs_box li,.list_wrap li,.cBox{
content-visibility:auto;
contain-intrinsic-size:300px;
contain:layout paint style
}
`;
}
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
}
(function installIframeNetworkThrottler() {
const Q = [];
const D = 80;
let busy = false;
async function run() {
if (busy || !Q.length) return;
busy = true;
while (Q.length) {
await Q.shift()();
await new Promise(r => setTimeout(r, D));
}
busy = false;
}
const isImage = u => /\.(jpg|jpeg|png|gif|webp|svg)/i.test(u);
const isTargetApi = u => /\/api\/|station|list/.test(u);
const origFetch = window.fetch;
window.fetch = async function (...args) {
const url = args[0]?.toString() || '';
if (isImage(url) || !isTargetApi(url)) return origFetch(...args);
return new Promise((resolve, reject) => {
Q.push(async () => {
try { resolve(await origFetch(...args)); }
catch (err) { reject(err); }
});
run();
});
};
const origOpen = XMLHttpRequest.prototype.open;
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
this._url = url;
return origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
const url = this._url || '';
if (isImage(url) || !isTargetApi(url)) return origSend.call(this, body);
Q.push(() => new Promise(resolve => {
this.addEventListener('loadend', resolve, { once: true });
origSend.call(this, body);
setTimeout(resolve, 1200);
}));
run();
};
})();
installIframePlayerGuards();
return;
}
if (window.self !== window.top) {
let parentIsSoop = false;
try {
parentIsSoop = window.parent.location.hostname.endsWith('sooplive.com');
} catch {
parentIsSoop = false;
}
if (!parentIsSoop) return;
}
const bootExitUrl = loadExitRedirectUrl();
const bootExitArmed = isExitRedirectArmed();
let isRealReload = false;
try {
const navEntry = performance.getEntriesByType('navigation')[0];
isRealReload = navEntry?.type === 'reload';
} catch {}
if (!isRealReload) {
try {
isRealReload = performance.navigation?.type === 1;
} catch {}
}
if (bootExitUrl && bootExitArmed && isRealReload) {
clearExitRedirectState();
location.replace(bootExitUrl);
return;
}
clearExitRedirectState();
let panelUrl = '';
let lastPipUrl = null;
let pipActive = false;
let pipClosedMode = false;
let pageOverlayHidden = false;
let bypass = false;
let savedTop = null;
let savedLeft = null;
let _pa = null, _paT = 0;
let resumeTimer = null;
let keepTimer = null;
let initObserver = null;
let initScheduled = false;
let pendingRouteTimer = null;
const PIP_PLAYER_RATIO = 0.46;
const PIP_MIN_W = 300;
const PIP_MAX_W = 520;
function getPipLayoutWidth() {
const frame = getFrame();
if (frame) {
const r = frame.getBoundingClientRect();
if (r.width > 100) return r.width;
}
return document.documentElement.clientWidth || window.innerWidth || 1280;
}
function getScaledPipSize() {
const baseWidth = getPipLayoutWidth();
let width = Math.round(baseWidth * PIP_PLAYER_RATIO);
width = Math.max(PIP_MIN_W, Math.min(PIP_MAX_W, width));
const height = Math.round(width * 9 / 16);
return { width, height };
}
function applyPipPlayerSize(player) {
if (!player) return;
const { width, height } = getScaledPipSize();
player.style.width = width + 'px';
player.style.height = height + 'px';
}
const playerPageState = createPlayerGuardState();
function getPlayer() {
const now = Date.now();
if (_pa && now - _paT < 2000 && _pa.isConnected) return _pa;
_paT = now;
_pa = document.querySelector(P_ID) || document.querySelector(P_CLS);
if (_pa) return _pa;
const v = document.querySelector('video');
if (!v) return null;
_pa = v.closest('[id*="player" i],[class*="player" i],[id*="vod" i],[class*="vod_" i]');
if (_pa) return _pa;
let p = v.parentElement;
while (p && p !== document.body) {
const s = getComputedStyle(p).position;
if (s === 'relative' || s === 'absolute' || s === 'fixed') { _pa = p; return _pa; }
p = p.parentElement;
}
_pa = v.parentElement;
return _pa;
}
function findVideo() {
const p = getPlayer();
return p ? p.querySelector('video') : document.querySelector('video');
}
function setLastUrl(url) {
if (!canReflectPanelUrl(url)) return;
lastPipUrl = url;
}
function getResumeUrl() {
if (lastPipUrl && canReflectPanelUrl(lastPipUrl)) return lastPipUrl;
return null;
}
function reflectPanelUrlToHash(url) {
try {
if (!url || !canReflectPanelUrl(url)) return;
const currentBase = location.pathname + location.search;
history.replaceState({ soopPanelUrl: url }, '', currentBase + '#pipurl=' + url);
} catch {}
}
function nav(url, skipSameCheck) {
if (!url) return;
const t = norm(url);
if (!skipSameCheck && t === norm(location.href)) return;
disarmExitRedirect();
bypass = true;
location.href = t;
}
function getFrame(create) {
let f = document.getElementById('soop-pip-frame');
if (f || !create) return f;
f = document.createElement('iframe');
f.id = 'soop-pip-frame';
f.loading = 'eager';
f.referrerPolicy = 'strict-origin-when-cross-origin';
document.body.appendChild(f);
return f;
}
function setFramePointer(on) {
const f = getFrame();
if (f) f.style.pointerEvents = on ? 'auto' : 'none';
}
function destroyFrame() {
const f = getFrame();
if (f) f.remove();
}
function cancelKeep() {
if (keepTimer) {
clearTimeout(keepTimer);
keepTimer = null;
}
}
function scheduleKeep() {
cancelKeep();
keepTimer = setTimeout(() => {
if (!pipActive && !pipClosedMode) destroyFrame();
keepTimer = null;
}, KEEP_MS);
}
function navPanel(url, reload) {
if (!canReflectPanelUrl(url)) return;
const f = getFrame();
if (!f) return;
const isSame = same(panelUrl, url);
panelUrl = url;
setLastUrl(url);
reflectPanelUrlToHash(url);
f.src = (isSame || reload) ? withReload(url) : url;
}
function cleanPipDom(player) {
if (!player) return;
player.querySelector('#pip-bar')?.remove();
player.onmousedown = null;
}
function applyOverlayHidden() {
document.documentElement.classList.toggle('soop-pip-overlay-hidden', !!pageOverlayHidden);
document.body?.classList.toggle('soop-pip-overlay-hidden', !!pageOverlayHidden);
const player = getPlayer();
if (!player) return;
if (pageOverlayHidden) {
player.classList.remove('pip-player');
player.classList.add('pip-player-hidden');
cleanPipDom(player);
player.style.top = '';
player.style.left = '';
} else if (document.body?.classList.contains('pip-mode')) {
player.classList.remove('pip-player-hidden');
player.classList.add('pip-player');
buildControls(player);
makeDrag(player);
}
}
function mkBtn(html, title, color, fn) {
const b = document.createElement('button');
b.className = 'pip-btn';
b.innerHTML = html;
b.title = title;
if (color) b.style.color = color;
b.addEventListener('click', e => {
e.stopPropagation();
e.preventDefault();
fn();
});
return b;
}
function buildControls(player) {
if (player.querySelector('#pip-bar')) return;
const bar = document.createElement('div');
bar.id = 'pip-bar';
bar.append(
mkBtn('⤢', '복귀', null, stopPip),
mkBtn('✖', '종료', null, exitPip)
);
player.appendChild(bar);
}
function makeDrag(el) {
let sx, sy, il, it, raf;
el.onmousedown = e => {
if (!pipActive || pageOverlayHidden) return;
if (e.target.closest('button,[role="button"],input,.play_control_box')) return;
e.preventDefault();
el.classList.add('dragging');
sx = e.clientX;
sy = e.clientY;
il = el.offsetLeft;
it = el.offsetTop;
document.onmouseup = () => {
document.onmouseup = document.onmousemove = null;
cancelAnimationFrame(raf);
el.classList.remove('dragging');
};
document.onmousemove = de => {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
el.style.top = (it + de.clientY - sy) + 'px';
el.style.left = (il + de.clientX - sx) + 'px';
});
};
};
}
function stopPlayerMedia() {
try {
if (location.hostname === 'vod.sooplive.com') {
const ctrlBox = document.querySelector('.ctrlBox');
if (ctrlBox) {
const ctrlBtn = ctrlBox.querySelector('button.pause');
if (ctrlBtn) { ctrlBtn.click(); return true; }
if (ctrlBox.querySelector('button.play')) return false;
}
const buttons = [...document.querySelectorAll('button.play, button.pause')];
const btn = buttons.find(b => {
if (b.classList.contains('prev') || b.classList.contains('next')) return false;
const text = b.querySelector('.tooltip span')?.textContent?.trim() || '';
return text === '재생' || text === '일시정지';
});
if (!btn) return false;
if (btn.classList.contains('play')) return false;
if (btn.classList.contains('pause')) {
btn.click();
return true;
}
return false;
}
if (location.hostname === 'play.sooplive.com') {
const btn = document.querySelector('#play');
if (!btn) return false;
if (btn.classList.contains('play')) return false;
if (btn.classList.contains('stop')) {
btn.click();
return true;
}
return false;
}
} catch {}
return false;
}
function startPip(url, reload) {
pageOverlayHidden = false;
pipClosedMode = false;
clearExitRedirectState();
const player = getPlayer();
if (!player) {
if (url) nav(url, true);
return;
}
cancelKeep();
pipActive = true;
bypass = false;
const f = getFrame(true);
const target = url || getResumeUrl();
if (target) {
const currentSrc = f.src || '';
if (!same(currentSrc, target) || reload) {
navPanel(target, reload || same(panelUrl, target));
} else {
panelUrl = target;
setLastUrl(target);
reflectPanelUrlToHash(target);
}
}
document.body.classList.add('pip-mode');
player.classList.remove('pip-player-hidden');
player.classList.add('pip-player');
rememberCatchPlayerSize(player);
applyPipPlayerSize(player);
const { width, height } = getScaledPipSize();
if (savedTop !== null && savedLeft !== null) {
player.style.top = savedTop;
player.style.left = savedLeft;
} else {
player.style.top = Math.max(0, window.innerHeight - height - 30) + 'px';
player.style.left = Math.max(0, window.innerWidth - width - 20) + 'px';
}
cleanPipDom(player);
buildControls(player);
makeDrag(player);
setFramePointer(true);
applyOverlayHidden();
updateResume();
}
function stopPip() {
pageOverlayHidden = false;
clearExitRedirectState();
const player = document.querySelector('.pip-player, .pip-player-hidden');
if (player) {
savedTop = player.style.top;
savedLeft = player.style.left;
player.classList.remove('pip-player');
player.classList.remove('pip-player-hidden');
player.style.top = '';
player.style.left = '';
if (isCatchVodPlayer(player)) {
restoreCatchPlayerSize(player);
} else {
player.style.width = '';
player.style.height = '';
}
cleanPipDom(player);
}
pipActive = false;
document.body.classList.remove('pip-mode');
document.body.classList.remove('soop-pip-overlay-hidden');
document.documentElement.classList.remove('soop-pip-overlay-hidden');
setFramePointer(false);
scheduleKeep();
updateResume();
}
function exitPip() {
if (!pipActive) return;
pipActive = false;
pipClosedMode = true;
pageOverlayHidden = true;
setTimeout(() => stopPlayerMedia(), 180);
if (panelUrl) {
saveExitRedirectUrl(panelUrl);
armExitRedirect();
reflectPanelUrlToHash(panelUrl);
}
applyOverlayHidden();
const f = getFrame();
if (f) {
f.style.visibility = 'visible';
f.style.pointerEvents = 'auto';
f.style.zIndex = '100';
}
updateResume();
}
function route(url, opts = {}) {
if (!url || bypass || !shouldPipUrl(url)) return false;
if (pipClosedMode) {
nav(url, true);
return true;
}
setLastUrl(url);
const doPanelRoute = () => {
if (pipActive) {
pageOverlayHidden = false;
applyOverlayHidden();
navPanel(url, opts.reload || same(panelUrl, url));
} else {
startPip(url, opts.reload);
}
};
if (getPlayer()) {
doPanelRoute();
return true;
}
if (pendingRouteTimer) {
clearTimeout(pendingRouteTimer);
pendingRouteTimer = null;
}
let tries = 0;
const retry = () => {
if (getPlayer()) {
pendingRouteTimer = null;
doPanelRoute();
return;
}
tries += 1;
if (tries < 4) {
pendingRouteTimer = setTimeout(retry, 80);
return;
}
pendingRouteTimer = null;
nav(url, true);
};
pendingRouteTimer = setTimeout(retry, 60);
return true;
}
function getDropdownUrl(li) {
const id = li.querySelector('button.thumb img')?.alt?.trim();
if (!id) return null;
return li.classList.contains('live')
? 'https://play.sooplive.com/' + id
: 'https://www.sooplive.com/station/' + id;
}
function getDropdownKw(li) {
if (li.classList.contains('tag_result')) {
const t = li.querySelector('.hash_result')?.textContent?.trim();
if (t) return t;
}
const span = li.querySelector('span:not(.certify):not(.live_cnt):not([class*="ic"])');
if (span?.textContent?.trim()) return span.textContent.trim();
const a = li.querySelector('a');
if (a) {
const c = a.cloneNode(true);
c.querySelectorAll('.num,i,.btn_delete,.ic_chain,.related_search,em').forEach(x => x.remove());
if (c.textContent?.trim()) return c.textContent.trim();
}
return null;
}
function searchUrl(kw) {
return 'https://www.sooplive.com/search?szLocation=total_search&szSearchType=total'
+ '&szKeyword=' + encodeURIComponent(kw)
+ '&szStype=di&szActype=input_field';
}
function installPlayerOpenHook(w, realOpen) {
installOpenHook(w, realOpen, (url) => {
if (externalClickWatch) {
markExternalClickHandled();
}
if (shouldPipUrl(url)) {
if (pipClosedMode) nav(url, true);
else route(url, { reload: same(panelUrl, url) });
return true;
}
const pending = playerPageState.getClick();
if (!pending || !isPlayerUrl(url) || !same(pending.url, url)) return false;
playerPageState.clearClick();
const g = playerPageState.getBlank();
if (g && same(g.url, url)) g.handled = true;
nav(url, true);
return true;
});
}
installPlayerOpenHook(window, originalOpen);
if (uWin !== window) installPlayerOpenHook(uWin, originalTargetOpen);
function toggleResume(show) {
const b = document.getElementById('pip-resume');
if (!b) return;
clearTimeout(resumeTimer);
if (show && b.classList.contains('can') && !pipActive) {
b.classList.add('show');
resumeTimer = setTimeout(() => b?.classList.remove('show'), 3000);
} else {
b.classList.remove('show');
}
}
function updateResume() {
const b = document.getElementById('pip-resume');
if (!b) return;
const hasFrame = !!getFrame();
const ok = (hasFrame || !!getResumeUrl() || !!panelUrl) && !pipActive && !pipClosedMode;
b.classList.toggle('can', ok);
if (!ok) b.classList.remove('show');
}
function doResume() {
const url = getResumeUrl() || panelUrl;
if (!url) return;
toggleResume(false);
if (document.fullscreenElement) {
document.exitFullscreen?.();
setTimeout(() => startPip(url, false), 350);
} else {
startPip(url, false);
}
}
function overVideo(e) {
const el = document.elementFromPoint(e.clientX, e.clientY);
if (el?.id === 'pip-resume' || el?.closest('#pip-resume')) return true;
const v = findVideo();
if (!v) return false;
const r = v.getBoundingClientRect();
return r.width > 10 &&
e.clientX >= r.left && e.clientX <= r.right &&
e.clientY >= r.top && e.clientY <= r.bottom;
}
function ensureResume() {
if (document.getElementById('pip-resume')) return;
const player = getPlayer();
if (!player) return;
const pos = getComputedStyle(player).position;
if (pos === 'static' || !pos) player.style.position = 'relative';
const b = document.createElement('button');
b.id = 'pip-resume';
b.className = 'pip-btn';
b.innerHTML = '↩';
b.title = '현재 세션의 이전 탐색 페이지로 복귀 (PIP)';
b.addEventListener('click', e => {
e.stopPropagation();
e.preventDefault();
doResume();
});
b.addEventListener('mouseenter', () => clearTimeout(resumeTimer));
b.addEventListener('mouseleave', () => {
if (b.classList.contains('show')) {
resumeTimer = setTimeout(() => b.classList.remove('show'), 3000);
}
});
player.appendChild(b);
updateResume();
}
function scheduleEnsureResume() {
if (initScheduled) return;
initScheduled = true;
requestAnimationFrame(() => {
initScheduled = false;
ensureResume();
if (pipActive || pageOverlayHidden) applyOverlayHidden();
});
}
bindPlayerClickMarker(playerPageState, () => {
disarmExitRedirect();
});
bindBlankGuard(playerPageState, (url) => {
nav(url, true);
});
document.addEventListener('click', function (e) {
if (e.defaultPrevented || bypass) return;
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
const li = e.target.closest(DROPDOWN_SEL);
if (li && e.target.closest('button.thumb')) {
disarmExitRedirect();
const url = getDropdownUrl(li);
if (!url) return;
if (li.classList.contains('live') || isPlayerUrl(url)) {
e.preventDefault();
e.stopPropagation();
nav(url, true);
return;
}
return;
}
const a = e.target.closest('a[href]');
if (!a || a.hasAttribute('download')) return;
disarmExitRedirect();
const href = a.href;
if (!href) return;
if (isPlayerUrl(href)) {
if (isMenuActionAnchor(a)) return;
beginExternalClickWatch(e, href, () => {
nav(href, true);
});
return;
}
if (!shouldPipUrl(href)) return;
if (a.closest('#hashtag') && a.target === '_blank') {
e.preventDefault();
e.stopPropagation();
if (pipClosedMode) {
nav(href, true);
} else {
route(href, { reload: same(panelUrl, href) });
}
return;
}
if (a.closest('#pip-bar,.play_control_box,#hashtag')) return;
e.preventDefault();
e.stopPropagation();
if (pipClosedMode) {
nav(href, true);
return;
}
route(href, { reload: same(panelUrl, href) });
route(href, { reload: same(panelUrl, href) });
}, true);
document.addEventListener('auxclick', function (e) {
if (e.button !== 1) return;
const li = e.target.closest(DROPDOWN_SEL);
if (!li) return;
disarmExitRedirect();
e.preventDefault();
e.stopPropagation();
const u = e.target.closest('button.thumb')
? getDropdownUrl(li)
: (() => {
const kw = getDropdownKw(li);
return kw ? searchUrl(kw) : null;
})();
if (u) originalOpen.call(window, u, '_blank');
}, true);
document.addEventListener('dblclick', function (e) {
if (!pipActive || pageOverlayHidden) return;
const player = document.querySelector('.pip-player');
if (!player?.contains(e.target)) return;
if (e.target.closest('#pip-bar,.play_control_box')) return;
e.preventDefault();
e.stopPropagation();
stopPip();
setTimeout(() => {
const t = player.querySelector('video') || player;
(t.requestFullscreen || t.webkitRequestFullscreen)?.call(t);
}, 150);
}, true);
window.addEventListener('message', e => {
if (!e.data) return;
const { type, dark, url } = e.data;
switch (type) {
case 'SOOP_THEME':
document.body.classList.toggle('iframe-dark', dark);
break;
case 'SOOP_PANEL_URL':
if (canReflectPanelUrl(url)) {
panelUrl = url;
setLastUrl(url);
reflectPanelUrlToHash(url);
}
break;
case 'SOOP_IFRAME_URL':
if (pipClosedMode && url && canReflectPanelUrl(url) && !same(location.href, url)) {
nav(url, true);
}
break;
case 'SOOP_NAV_PLAYER':
markExternalClickHandled();
if (url) nav(url, true);
break;
case 'SOOP_PARENT_BODY_CLICK':
nudgeBodyClick();
break;
}
}, { passive: true });
const init = () => {
ensureResume();
applyOverlayHidden();
setTimeout(scheduleEnsureResume, 500);
setTimeout(scheduleEnsureResume, 1500);
if (!document.__soopHover) {
document.__soopHover = true;
document.addEventListener('mousemove', function (e) {
if (!pipActive) overVideo(e) ? toggleResume(true) : toggleResume(false);
}, { passive: true });
}
if (!window.__soopPipResizeBound) {
window.__soopPipResizeBound = true;
window.addEventListener('resize', refreshPipPlayerSize, { passive: true });
}
if (!initObserver && document.body) {
initObserver = new MutationObserver(() => {
const resume = document.getElementById('pip-resume');
const player = getPlayer();
if (resume && player && player.contains(resume)) {
initObserver.disconnect();
initObserver = null;
return;
}
if (!resume && player) {
scheduleEnsureResume();
}
});
initObserver.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
initObserver?.disconnect();
initObserver = null;
}, 3000);
}
};
if (document.body) init();
else document.addEventListener('DOMContentLoaded', init);
GM_addStyle(`
body.pip-mode .pip-player{
position:fixed!important;
z-index:999999!important;
bottom:auto!important;
right:auto!important;
border:none!important;
box-shadow:0 4px 20px rgba(0,0,0,.7);
background:#000;
border-radius:8px;
overflow:hidden;
cursor:move!important;
contain:strict!important;
}
body.pip-mode .pip-player.dragging{
transition:none!important;
box-shadow:0 2px 10px rgba(0,0,0,.5);
}
body.pip-mode .pip-player video{
pointer-events:none!important;
object-fit:contain!important;
width:100%!important;
height:100%!important;
position:relative!important;
z-index:5!important;
}
body.pip-mode .pip-player .player_cover{
pointer-events:none!important;
width:100%!important;
height:100%!important;
position:relative!important;
z-index:4!important;
}
body.pip-mode .pip-player .play_control_box{
display:block!important;
pointer-events:auto!important;
bottom:0!important;
position:absolute!important;
width:100%!important;
background:linear-gradient(to top,rgba(0,0,0,.7),transparent);
z-index:20!important;
cursor:default!important;
}
body.pip-mode .pip-player .play_control_box *{
pointer-events:auto!important;
}
body.pip-mode #web_chatting,
body.pip-mode .header_area,
body.pip-mode .sidebar_area,
body.pip-mode .start_ad_area,
body.pip-mode #action_bar,
body.pip-mode .btn_chat_open,
body.pip-mode .btn_chat_fold,
body.pip-mode .btn_expand,
body.pip-mode button[class*="chat"],
body.pip-mode .chat_layer,
body.pip-mode .btn_sidebar,
body.pip-mode .chat-icon.trash-icon.trash,
body.pip-mode .chat-icon.highlight-icon.highlight{
display:none!important;
}
#soop-pip-frame{
position:fixed;
top:0;
left:0;
width:100%;
height:100%;
border:none;
background:#fff;
visibility:hidden;
pointer-events:none;
z-index:-1;
transform:translateZ(0);
}
body.pip-mode #soop-pip-frame{
visibility:visible;
pointer-events:auto;
z-index:100;
}
body.pip-mode.iframe-dark #soop-pip-frame{
background:#141517;
}
body:not(.pip-mode) #soop-pip-frame{
visibility:hidden;
pointer-events:none;
z-index:-1;
}
#pip-bar{
display:none;
position:absolute;
top:0;
left:0;
width:100%;
height:48px;
background:linear-gradient(to bottom,rgba(0,0,0,.5),transparent);
z-index:1000000;
justify-content:space-between;
align-items:flex-start;
padding:5px 8px;
opacity:0;
transition:opacity .15s;
pointer-events:auto!important;
cursor:default;
}
body.pip-mode .pip-player:hover #pip-bar{
opacity:1;
}
body.pip-mode #pip-bar{
display:flex;
}
.pip-btn{
background:rgba(0,0,0,.45);
border:1px solid rgba(255,255,255,.2);
color:#fff;
width:40px;
height:40px;
border-radius:50%;
cursor:pointer;
font-size:25px;
line-height:34px;
text-align:center;
padding:0;
margin:0;
flex-shrink:0;
}
.pip-btn:hover{
background:rgba(255,255,255,.25);
}
#pip-resume{
position:absolute;
top:10px;
left:10px;
z-index:999999;
opacity:0;
pointer-events:none;
transition:opacity .2s;
width:47px;
height:47px;
font-size:23px;
line-height:45px;
}
#pip-resume.can.show{
opacity:.75;
pointer-events:auto;
}
#pip-resume.can.show:hover{
opacity:1;
}
body.pip-mode #pip-resume{
display:none!important;
}
body.soop-pip-overlay-hidden .pip-player,
body.soop-pip-overlay-hidden #pip-bar{
display:none!important;
}
.pip-player-hidden{
display:none!important;
}
`);
})();