Greasy Fork

Greasy Fork is available in English.

SOOP (숲) - 목록 탐색 자동 PIP

재생중인 화면을 PIP로 전환하고 탐색

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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;
}
    `);
})();