YouTube PIP Button (Safari Desktop Mode + Auto PIP)

Generate PIP button on YouTube Player Desktop Mode + auto PIP when Safari tab goes background

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         YouTube PIP Button (Safari Desktop Mode + Auto PIP)
// @version      1.1
// @description  Generate PIP button on YouTube Player Desktop Mode + auto PIP when Safari tab goes background
// @match        https://www.youtube.com/*
// @grant        none
// @namespace    colodes
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const controlSelector = '.ytp-left-controls';
    const AUTO_PIP_ON_HIDE = true; // 页面被隐藏(回桌面/切换标签)时自动尝试进入 PIP

    let lastVideo = null;

    function canUsePip(video) {
        if (!video) return false;
        // 老 API:webkitSupportsPresentationMode / webkitSetPresentationMode
        if (typeof video.webkitSetPresentationMode === 'function') {
            if (typeof video.webkitSupportsPresentationMode === 'function') {
                try {
                    if (!video.webkitSupportsPresentationMode('picture-in-picture')) {
                        return false;
                    }
                } catch (_) {}
            }
            return true;
        }
        return false;
    }

    function enterPip(video) {
        if (!canUsePip(video)) return;
        try {
            if (video.webkitPresentationMode !== 'picture-in-picture') {
                video.webkitSetPresentationMode('picture-in-picture');
            }
        } catch (_) {}
    }

    function exitPip(video) {
        if (!canUsePip(video)) return;
        try {
            if (video.webkitPresentationMode === 'picture-in-picture') {
                video.webkitSetPresentationMode('inline');
            }
        } catch (_) {}
    }

    function togglePip(video) {
        if (!canUsePip(video)) return;
        try {
            if (video.webkitPresentationMode === 'picture-in-picture') {
                video.webkitSetPresentationMode('inline');
            } else {
                video.webkitSetPresentationMode('picture-in-picture');
            }
        } catch (_) {}
    }

    function createPipButton(video) {
        const btn = document.createElement('button');
        const icon = document.createElement('span');
        icon.textContent = '◲';

        Object.assign(btn.style, {
            background: 'rgba(0, 0, 0, 0.3)',
            color: '#fff',
            border: 'none',
            borderRadius: '50%',
            width: '36px',
            height: '36px',
            fontSize: '30px',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            margin: '10px',
            userSelect: 'none',
            textShadow: '0 0px 3px rgba(0, 0, 0, 0.5)',
            cursor: 'pointer'
        });
        Object.assign(icon.style, {
            transform: 'translateY(-2px)',
            display: 'inline-block',
        });

        btn.appendChild(icon);
        btn.classList.add('my-pip-btn');
        btn.onclick = e => {
            e.stopPropagation();
            if (!video) return;
            togglePip(video);
        };
        return btn;
    }

    function injectButtons() {
        const video = document.querySelector('video');
        if (!video) return;
        lastVideo = video;

        const roots = [document];
        const host = document.querySelector('ytp-player');
        if (host?.shadowRoot) roots.push(host.shadowRoot);

        roots.forEach(root => {
            root.querySelectorAll(controlSelector).forEach(container => {
                if (!container.querySelector('.my-pip-btn')) {
                    container.appendChild(createPipButton(video));
                }
            });
        });
    }

    // 当页面进入后台 / 被隐藏时,自动尝试进入 PIP
    function handleAutoPip() {
        if (!AUTO_PIP_ON_HIDE) return;

        // 优先用 lastVideo,退而求其次重新 query
        const video = lastVideo || document.querySelector('video');
        if (!video) return;

        // 只对正在播放的视频自动 PIP,避免误触
        if (video.paused || video.ended) return;

        enterPip(video);
    }

    // 1. Safari 切换标签页 / 回桌面时,一般会触发 visibilitychange 为 hidden
    document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'hidden') {
            handleAutoPip();
        }
    }, true);

    // 2. 作为补充,一些情况下会触发 pagehide
    window.addEventListener('pagehide', () => {
        handleAutoPip();
    }, true);

    // 观察页面变化,注入按钮
    new MutationObserver(injectButtons).observe(document.body, { childList: true, subtree: true });
    injectButtons();
})();