YouTube - Miniplayer

Shows a mini player when you scroll past the video.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name                    YouTube - Miniplayer
// @name:fr                 YouTube - Lecteur réduit
// @name:es                 YouTube - Minirreproductor
// @name:de                 YouTube - Miniplayer
// @name:it                 YouTube - Miniplayer
// @name:zh-CN              YouTube - Miniplayer
// @namespace               https://gist.github.com/4lrick/5a54e121bdc9056a7551529669d65ae6
// @version                 1.1
// @description             Shows a mini player when you scroll past the video.
// @description:fr          Affiche un lecteur réduit quand vous faites défiler sous la vidéo.
// @description:es          Muestra un minirreproductor al desplazarte más allá del video.
// @description:de          Zeigt einen Miniplayer, sobald Sie am Video vorbeiscrollen.
// @description:it          Mostra un miniplayer quando scorri oltre il video.
// @description:zh-CN       在滚动越过视频时显示迷你播放器。
// @author                  4lrick
// @match                   https://www.youtube.com/*
// @icon                    https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant                   none
// @license                 GPL-3.0-only
// ==/UserScript==

(function () {
    'use strict';

    const CFG = {
        rawVideoSelector: '.html5-video-container',
        mainPlayerSelector: '#movie_player',
        miniPlayerClass: 'mpu-mini-player',
        widthPx: 480,
        scrollThreshold: 300,
        autohideDelayMs: 2000,
        transitionDurationMs: 300,
    };

    function injectStyle() {
        if (document.getElementById('mpu-style')) return;
        const style = document.createElement('style');
        style.id = 'mpu-style';
        style.textContent = `
        /* Mini player container */
        .${CFG.miniPlayerClass} {
            position: fixed !important;
            width: ${CFG.widthPx}px !important;
            height: ${Math.round(CFG.widthPx * 9 / 16)}px !important;
            right: 16px !important;
            bottom: 16px !important;
            background: #000;
            overflow: hidden !important;
            border-radius: 12px !important;
            opacity: 0;
            transform: scale(0.8) !important;
            transition: opacity ${CFG.transitionDurationMs}ms ease, transform ${CFG.transitionDurationMs}ms ease !important;
            user-select: none;
        }
        .${CFG.miniPlayerClass}.visible {
            opacity: 1;
            transform: scale(1) !important;
        }

        /* Ensure the video fills the mini player */
        .${CFG.miniPlayerClass} ${CFG.rawVideoSelector},
        .${CFG.miniPlayerClass} video.html5-main-video {
            width: 100% !important;
            height: 100% !important;
            left: 0 !important;
        }
        .${CFG.miniPlayerClass} ${CFG.rawVideoSelector} {
            position: static !important;
        }

        /* Controls bar */
        .${CFG.miniPlayerClass} .mpu-controls {
            position: absolute !important;
            left: 0; right: 0; bottom: 0;
            display: flex; align-items: center; gap: 8px;
            color: #fff; font: 12px/1.2 system-ui, sans-serif;
            user-select: none; pointer-events: auto;
            padding: 4px;
        }

        /* Bottom buttons */
        .${CFG.miniPlayerClass} .mpu-btn {
            cursor: pointer;
        }
        .${CFG.miniPlayerClass} .mpu-btn-play,
        .${CFG.miniPlayerClass} .mpu-btn-fullscreen {
            background: transparent; border: none;
            display: flex;
        }
        .${CFG.miniPlayerClass} .mpu-btn-play::before {
            content: '';
            width: 28px; height: 28px;
            background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><path d="M12,26 L18.5,22 L18.5,14 L12,10 Z M18.5,22 L25,18 L25,18 L18.5,14 Z" fill="%23fff"/></svg>');
            background-size: contain;
            background-repeat: no-repeat;
        }
        .${CFG.miniPlayerClass} .mpu-btn-play.playing::before {
            background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><path d="M12,26 L16,26 L16,10 L12,10 Z M21,26 L25,26 L25,10 L21,10 Z" fill="%23fff"/></svg>');
        }
        .${CFG.miniPlayerClass} .mpu-btn-fullscreen { margin-left: auto; }
        .${CFG.miniPlayerClass} .mpu-btn-fullscreen::before {
            content: '';
            width: 28px; height: 28px;
            background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><path d="M10,16 h2 v-4 h4 v-2 h-6 v6 z" fill="%23fff"/><path d="M20,10 v2 h4 v4 h2 v-6 h-6 z" fill="%23fff"/><path d="M24,24 h-4 v2 h6 v-6 h-2 v4 z" fill="%23fff"/><path d="M12,20 h-2 v6 h6 v-2 h-4 v-4 z" fill="%23fff"/></svg>');
            background-size: contain;
            background-repeat: no-repeat;
        }
        .${CFG.miniPlayerClass} .mpu-btn-fullscreen.fullscreen::before {
            background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><path d="M14,14 h-4 v2 h6 v-6 h-2 v4 z" fill="%23fff"/><path d="M22,14 v-4 h-2 v6 h6 v-2 h-4 z" fill="%23fff"/><path d="M20,26 h2 v-4 h4 v-2 h-6 v6 z" fill="%23fff"/><path d="M10,22 h4 v4 h2 v-6 h-6 v2 z" fill="%23fff"/></svg>');
        }

        /* Top buttons */
        .${CFG.miniPlayerClass} .mpu-btn-top {
            position: absolute; top: 0 !important;
            background: transparent; border: none;
            display: flex;
            padding: 8px;
        }
        .${CFG.miniPlayerClass} .mpu-btn-close { right: 0; }
        .${CFG.miniPlayerClass} .mpu-btn-scroll-up::before {
            content: '';
            width: 24px; height: 24px;
            background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g transform="translate(12,12) scale(-1,1) translate(-12,-12)"><path d="M19,19 L5,19 L5,5 L12,5 L12,3 L5,3 C3.89,3 3,3.9 3,5 L3,19 C3,20.1 3.89,21 5,21 L19,21 C20.1,21 21,20.1 21,19 L21,12 L19,12 L19,19 Z M14,3 L14,5 L17.59,5 L7.76,14.83 L9.17,16.24 L19,6.41 L19,10 L21,10 L21,3 L14,3 Z" fill="%23fff"/></g></svg>');
            background-size: contain;
            background-repeat: no-repeat;
        }
        .${CFG.miniPlayerClass} .mpu-btn-close::before {
            content: '';
            width: 24px; height: 24px;
            background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="%23fff"/></svg>');
            background-size: contain;
            background-repeat: no-repeat;
        }

        /* Autohide behavior */
        .${CFG.miniPlayerClass} .mpu-controls,
        .${CFG.miniPlayerClass} .mpu-btn-top,
        .${CFG.miniPlayerClass} .mpu-progress-container,
        .${CFG.miniPlayerClass} .ytp-gradient { transition: opacity 150ms ease; }

        .${CFG.miniPlayerClass}.ytp-autohide .mpu-controls,
        .${CFG.miniPlayerClass}.ytp-autohide .mpu-btn-top,
        .${CFG.miniPlayerClass}.ytp-autohide .mpu-progress-container,
        .${CFG.miniPlayerClass}.ytp-autohide .ytp-gradient {
            opacity: 0; pointer-events: none;
        }

        /* Progress bar */
        .${CFG.miniPlayerClass} .mpu-progress-container {
        position: absolute; left: 8px; right: 8px; bottom: 40px;
        }
        @-moz-document url-prefix() {
            .${CFG.miniPlayerClass} .mpu-progress-container {
            bottom: 30px;
            }
        }

        .${CFG.miniPlayerClass} .mpu-slider {
            --bar-h: 4px;
            --thumb: 13px;
            --fill: 0%;

            --accent: var(--yt-spec-static-brand-red, #f03);
            --accent2: #ff2791;
            --track-bg: rgba(255,255,255,0.2);
            --fill-gradient: linear-gradient(to right, var(--accent) 80%, var(--accent2) 100%);

            -webkit-appearance: none;
            width: -webkit-fill-available;
            width: -moz-available;
            cursor: pointer;
            background: none;
        }
        .${CFG.miniPlayerClass} .mpu-slider:hover { --bar-h: 6px; }

        .${CFG.miniPlayerClass} .mpu-slider::-webkit-slider-runnable-track {
            height: var(--bar-h);
            background:
            var(--fill-gradient) 0 50% / var(--fill) var(--bar-h) no-repeat,
            linear-gradient(var(--track-bg), var(--track-bg)) 0 50% / 100% var(--bar-h) no-repeat;
        }
        .${CFG.miniPlayerClass} .mpu-slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: var(--thumb); height: var(--thumb);
            border-radius: 50%;
            background: var(--accent);
            margin-top: calc((var(--bar-h) - var(--thumb)) / 2);
        }

        .${CFG.miniPlayerClass} .mpu-slider::-moz-range-track {
            height: var(--bar-h);
            background: var(--track-bg);
        }
        .${CFG.miniPlayerClass} .mpu-slider::-moz-range-progress {
            height: var(--bar-h);
            background: var(--fill-gradient);
        }
        .${CFG.miniPlayerClass} .mpu-slider::-moz-range-thumb {
            width: var(--thumb); height: var(--thumb);
            border: 0; border-radius: 50%;
            background: var(--accent);
        }

        /* Gradient behind controls */
        .${CFG.miniPlayerClass} .ytp-gradient {
            position: absolute; left: 0; right: 0;
            height: 64px; pointer-events: none;
            background: linear-gradient(to top, rgba(0,0,0,0.9), rgba(0,0,0,0));
        }

    `;
        document.head.appendChild(style);
    }

    let rawVideo;
    let miniPlayer;
    let playerInitialLocation = null;
    let controls = null;
    let isPlayerHidden = false;
    let autohideTimer = null;
    let observer = null;
    let loopStarted = false;
    let isDragging = false;
    let dragStartX = 0;
    let dragStartY = 0;
    let dragStartLeft = 0;
    let dragStartTop = 0;

    function restoreFromMiniPlayer() {
        if (!rawVideo) return;
        if (!playerInitialLocation) return;
        const parent = playerInitialLocation.parentNode;
        if (!parent) return;
        parent.insertBefore(rawVideo, playerInitialLocation);
        if (miniPlayer) {
            miniPlayer.classList.remove('visible');
            setTimeout(() => {
                miniPlayer.remove();
                miniPlayer = null;
                controls = null;
            }, CFG.transitionDurationMs);
        }
        playerInitialLocation = null;
    }

    function attachAutohideListeners() {
        miniPlayer.addEventListener('mousemove', showControlsNow);
        miniPlayer.addEventListener('mouseenter', showControlsNow);
        miniPlayer.addEventListener('touchstart', showControlsNow, { passive: true });
        miniPlayer.addEventListener('focusin', showControlsNow);
        miniPlayer.addEventListener('mouseleave', scheduleAutohide);
    }

    function attachFullscreenListener() {
        if (!controls) return;
        const { fullscreenBtn } = controls;
        function onFullscreenChange() {
            if (document.fullscreenElement) {
                fullscreenBtn.classList.add('fullscreen');
                fullscreenBtn.title = 'Exit full screen';
            } else {
                fullscreenBtn.classList.remove('fullscreen');
                fullscreenBtn.title = 'Full screen';
            }
        }
        document.addEventListener('fullscreenchange', onFullscreenChange);
    }

    function updatePlayIcon() {
        if (!controls) return;
        const video = getVideo();
        const isPlaying = video && !video.paused;
        if (isPlaying) {
            controls.playBtn.classList.add('playing');
            controls.playBtn.title = 'Pause';
        } else {
            controls.playBtn.classList.remove('playing');
            controls.playBtn.title = 'Play';
        }
    }

    function attachVideoListeners() {
        const video = getVideo();
        if (!video) return;
        video.addEventListener('play', updatePlayIcon);
        video.addEventListener('pause', updatePlayIcon);
        video.addEventListener('timeupdate', () => { updateBars(); updateTime(); });
        updatePlayIcon();
        updateBars();
        updateTime();
    }

    function formatTime(t) {
        if (!isFinite(t) || t < 0) return '--:--';
        const s = Math.floor(t % 60).toString().padStart(2, '0');
        const m = Math.floor((t / 60) % 60).toString();
        const h = Math.floor(t / 3600);
        return h > 0 ? `${h}:${m.padStart(2, '0')}:${s}` : `${m}:${s}`;
    }

    function updateTime() {
        if (!controls) return;
        const { time } = controls;
        const video = getVideo();
        if (video) {
            time.textContent = `${formatTime(video.currentTime)} / ${formatTime(video.duration)}`;
        } else {
            time.textContent = '--:-- / --:--';
        }
    }

    function isLive() {
        const mainPlayer = document.querySelector(CFG.mainPlayerSelector);
        if (!mainPlayer) return false;
        const timeDisplay = mainPlayer.querySelector('.ytp-time-display');
        return !!(timeDisplay && timeDisplay.classList.contains('ytp-live'));
    }

    function updateBars() {
        if (!controls) return;
        const { progressBar } = controls;
        const video = getVideo();
        if (!video) return;
        let progressFraction = Math.max(0, Math.min(1, video.currentTime / video.duration));
        progressBar.slider.value = progressFraction;
        progressBar.slider.style.setProperty('--fill', `${progressFraction * 100}%`);
    }

    function attachControlListeners() {
        if (!controls) return;
        const { playBtn, fullscreenBtn, scrollUpBtn, closeBtn, progressBar } = controls;

        function onPlayClick() {
            const video = getVideo();
            if (!video) return;
            if (video.paused) video.play(); else video.pause();
            showControlsNow();
        }

        function onFullscreenClick() {
            if (document.fullscreenElement) document.exitFullscreen();
            else displayMiniPlayer().requestFullscreen();
            showControlsNow();
        }

        function onUpClick() {
            window.scrollTo({ top: 0, behavior: 'smooth' });
            showControlsNow();
        }

        function onCloseClick() {
            isPlayerHidden = true;
            toggleMiniPlayer(false);
        }

        function onProgressInput(event) {
            const newProgressFraction = parseFloat(event.target.value);
            const video = getVideo();
            if (!video) return;
            video.currentTime = newProgressFraction * video.duration;
            updateBars();
            updateTime();
            showControlsNow();
        }

        playBtn.addEventListener('click', onPlayClick);
        fullscreenBtn.addEventListener('click', onFullscreenClick);
        scrollUpBtn.addEventListener('click', onUpClick);
        closeBtn.addEventListener('click', onCloseClick);
        progressBar.slider.addEventListener('input', onProgressInput);
    }

    function createGradient(position = 'bottom') {
        const gradient = document.createElement('div');
        gradient.className = 'ytp-gradient';
        if (position === 'top') {
            gradient.style.top = '-20px';
            gradient.style.transform = 'rotate(180deg)';
        } else {
            gradient.style.bottom = '-5px';
        }
        return gradient;
    }

    function createProgressBar() {
        const progressContainer = document.createElement('div');
        progressContainer.className = 'mpu-progress-container';

        const slider = document.createElement('input');
        slider.className = 'mpu-slider';
        slider.type = 'range';
        slider.min = '0';
        slider.max = '1';
        slider.step = '0.001';
        slider.value = '0';

        progressContainer.appendChild(slider);

        return {
            progressContainer,
            slider
        };
    }

    function createButton(type, text) {
        const btn = document.createElement('button');
        let className = `mpu-btn mpu-btn-${type}`;

        if (type === 'scroll-up' || type === 'close') {
            className += ' mpu-btn-top';
        }

        btn.className = className;
        btn.title = text;
        return btn;
    }

    function displayControls() {
        if (controls && controls.root && miniPlayer && miniPlayer.contains(controls.root)) return controls;

        const mainPlayer = document.querySelector(CFG.mainPlayerSelector);
        if (!mainPlayer) return null;

        const bottomGradient = createGradient('bottom');
        const topGradient = createGradient('top');
        miniPlayer.appendChild(bottomGradient);
        miniPlayer.appendChild(topGradient);

        const controlsRoot = document.createElement('div');
        controlsRoot.className = 'mpu-controls';

        const playBtn = createButton('play', 'Play/Pause');
        const fullscreenBtn = createButton('fullscreen', 'Full screen');
        const scrollUpBtn = createButton('scroll-up', 'Scroll to top');
        const closeBtn = createButton('close', 'Close');

        const time = document.createElement('span');
        time.textContent = '--:-- / --:--';

        controlsRoot.append(playBtn, time, fullscreenBtn);
        miniPlayer.appendChild(controlsRoot);

        const progressBar = createProgressBar();
        miniPlayer.appendChild(progressBar.progressContainer);

        miniPlayer.appendChild(scrollUpBtn);
        miniPlayer.appendChild(closeBtn);
        controls = { root: controlsRoot, playBtn, fullscreenBtn, time, scrollUpBtn, closeBtn, progressBar, bottomGradient, topGradient };
        return controls;
    }

    function onTimestampClickPreserveScroll(event) {
        if (isPlayerHidden) return;
        const anchor = event.target?.closest('a[href]');
        if (!anchor?.closest('#comments, ytd-comments')) return;

        const text = anchor.textContent?.trim();
        if (!text) return;

        const parts = text.split(':').map(p => parseInt(p, 10));
        if (parts.some(n => isNaN(n))) return;

        let seconds;
        if (parts.length === 2) {
            seconds = parts[0] * 60 + parts[1];
        } else if (parts.length === 3) {
            seconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
        } else {
            return;
        }

        const videoElement = rawVideo?.querySelector('video');
        if (!videoElement?.duration) return;

        event.preventDefault();
        event.stopPropagation();

        videoElement.currentTime = Math.max(0, Math.min(videoElement.duration, seconds));
        showControlsNow();
    }

    function clearAutohideTimer() {
        if (autohideTimer) {
            clearTimeout(autohideTimer); autohideTimer = null;
        }
    }

    function scheduleAutohide() {
        clearAutohideTimer();
        autohideTimer = setTimeout(() => {
            if (miniPlayer) miniPlayer.classList.add('ytp-autohide');
        }, CFG.autohideDelayMs);
    }

    function showControlsNow() {
        miniPlayer.classList.remove('ytp-autohide');
        scheduleAutohide();
    }

    function getVideo() {
        return rawVideo ? rawVideo.querySelector('video') : null;
    }

    function onMiniPlayerBackgroundClick(event) {
        if (event.button !== 0) return;

        const mouseMovement = Math.abs(event.clientX - dragStartX) + Math.abs(event.clientY - dragStartY);
        if (mouseMovement > 1) return;

        const target = event.target;
        if (!target) return;
        if (isTargetInteractive(target)) return;
        const videoElement = getVideo();
        if (!videoElement) return;
        if (videoElement.paused) videoElement.play(); else videoElement.pause();
        showControlsNow();
    }

    function handleDragEnd() {
        isDragging = false;
        document.removeEventListener('mousemove', handleDragMove);
        document.removeEventListener('mouseup', handleDragEnd);
    }

    function handleDragMove(event) {
        if (!isDragging) return;

        const deltaX = event.clientX - dragStartX;
        const deltaY = event.clientY - dragStartY;

        const newLeft = Math.max(0, Math.min(window.innerWidth - CFG.widthPx, dragStartLeft + deltaX));
        const newTop = Math.max(0, Math.min(window.innerHeight - Math.round(CFG.widthPx * 9 / 16), dragStartTop + deltaY));

        miniPlayer.style.left = newLeft + 'px';
        miniPlayer.style.top = newTop + 'px';
    }

    function isTargetInteractive(target) {
        if (!target || !target.closest) return false;
        return !!(
            target.closest('.mpu-progress-container') ||
            target.closest('button')
        );
    }

    function handleDragStart(event) {
        if (event.button !== 0 || isTargetInteractive(event.target) || document.fullscreenElement === miniPlayer) return;

        isDragging = true;
        dragStartX = event.clientX;
        dragStartY = event.clientY;

        const rect = miniPlayer.getBoundingClientRect();
        dragStartLeft = rect.left;
        dragStartTop = rect.top;

        document.addEventListener('mousemove', handleDragMove);
        document.addEventListener('mouseup', handleDragEnd);

        event.preventDefault();
    }

    function displayMiniPlayer() {
        if (miniPlayer && document.body.contains(miniPlayer)) return miniPlayer;
        miniPlayer = document.createElement('div');
        miniPlayer.className = CFG.miniPlayerClass;
        document.body.appendChild(miniPlayer);
        miniPlayer.addEventListener('mousedown', handleDragStart);
        miniPlayer.addEventListener('click', onMiniPlayerBackgroundClick);
        document.addEventListener('click', onTimestampClickPreserveScroll, true);

        requestAnimationFrame(() => {
            miniPlayer.classList.add('visible');
        });

        return miniPlayer;
    }

    function moveToMiniPlayer() {
        if (!rawVideo || !window.location.pathname.includes('watch')) return;

        const mainPlayer = document.querySelector(CFG.mainPlayerSelector);
        const activeVideo = mainPlayer ? mainPlayer.querySelector(CFG.rawVideoSelector) : null;
        const offlineSlate = mainPlayer ? mainPlayer.querySelector('.ytp-offline-slate') : null;
        if (!activeVideo || (offlineSlate && offlineSlate.style.display !== 'none')) return;
        if (isLive()) return;

        rawVideo = activeVideo;

        playerInitialLocation = rawVideo.nextSibling;
        displayMiniPlayer().appendChild(rawVideo);
        displayControls();
        attachControlListeners();
        attachVideoListeners();
        attachFullscreenListener();
        attachAutohideListeners();
        showControlsNow();
    }

    function toggleMiniPlayer(enabled) {
        if (!rawVideo) return;

        if (enabled) {
            moveToMiniPlayer();
        } else {
            restoreFromMiniPlayer();
        }
    }

    function handleVisibility(inView) {
        if (!inView) isPlayerHidden = false;
        if (isPlayerHidden && inView) return;
        toggleMiniPlayer(inView);
    }

    function tick() {
        if (!rawVideo) return;

        handleVisibility(window.scrollY > CFG.scrollThreshold);
        requestAnimationFrame(tick);
    }

    function checkVideoExists() {
        const mainPlayer = document.querySelector(CFG.mainPlayerSelector);
        const inWatchPage = window.location.pathname.includes('watch');
        const foundVideo = mainPlayer ? mainPlayer.querySelector(CFG.rawVideoSelector) : null;
        const newVideo = (inWatchPage && foundVideo) ? foundVideo : null;
        if (rawVideo !== newVideo) {
            rawVideo = newVideo;
        }
        if (!rawVideo) return;
        if (!loopStarted) {
            loopStarted = true;
            if (observer) observer.disconnect();
            tick();
        }
    }

    function init() {
        injectStyle();
        if (observer) observer.disconnect();
        observer = new MutationObserver(checkVideoExists);
        observer.observe(document.body, { childList: true, subtree: true });
    }

    init();
})();