MJPEG PiP Support

Picture-in-Picture for MJPEG images

スクリプトをインストールするには、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         MJPEG PiP Support
// @namespace    http://tampermonkey.net/
// @version      1.4
// @author       pooiod7
// @description  Picture-in-Picture for MJPEG images
// @match        *://*/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let stream = null;
    let btn = null;
    let video = null;
    let canvas = null;
    let rafId = null;
    let captureStream = null;
    let lastW = 0;
    let lastH = 0;
    let polling = null;

    function findStream() {
        const imgs = document.getElementsByTagName('img');
        for (let i = 0; i < imgs.length; i++) {
            const src = imgs[i].src || '';
            if (src.includes('.mjp')) return imgs[i];
        }
        return null;
    }

    function updateSize(force) {
        const w = stream.naturalWidth || stream.width || 640;
        const h = stream.naturalHeight || stream.height || 360;
        if (force || Math.abs(w - lastW) > 50 || Math.abs(h - lastH) > 50) {
            canvas.width = w;
            canvas.height = h;
            lastW = w;
            lastH = h;
        }
    }

    function startCapture() {
        updateSize(true);
        const ctx = canvas.getContext('2d');

        function draw() {
            updateSize(false);
            try { ctx.drawImage(stream, 0, 0, canvas.width, canvas.height); } catch {}
            rafId = requestAnimationFrame(draw);
        }

        draw();
        captureStream = canvas.captureStream(15);
        video.srcObject = captureStream;
    }

    function stopCapture() {
        if (rafId) {
            cancelAnimationFrame(rafId);
            rafId = null;
        }
        if (captureStream) {
            captureStream.getTracks().forEach(t => t.stop());
            captureStream = null;
        }
        video.pause();
        video.srcObject = null;
        canvas.width = 0;
        canvas.height = 0;
        lastW = 0;
        lastH = 0;
    }

    function createButton() {
        if (btn) return;

        const style = document.createElement('style');
        style.textContent =
            'button#tm-pip-btn{position:fixed;top:10px;right:10px;z-index:2147483647;width:46px;height:46px;border-radius:10px;border:0;background:#111;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.4)}' +
            'button#tm-pip-btn svg{width:26px;height:26px;fill:#fff}' +
            'video#tm-hidden-video,canvas#tm-hidden-canvas{display:none;visibility:hidden;position:fixed;left:-9999px;top:-9999px}';
        document.head.appendChild(style);

        btn = document.createElement('button');
        btn.id = 'tm-pip-btn';
        btn.innerHTML =
            '<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">' +
            '<path d="M15.694 13.541l2.666 2.665 5.016-5.017 2.59 2.59 0.004-7.734-7.785-0.046 2.526 2.525-5.017 5.017z"></path>' +
            '<path d="M25.926 16.945l-1.92-1.947 0.035 9.007-16.015 0.009 0.016-15.973 8.958-0.040-2-2h-7c-1.104 0-2 0.896-2 2v16c0 1.104 0.896 2 2 2h16c1.104 0 2-0.896 2-2l-0.074-7.056z"></path>' +
            '</svg>';

        document.body.appendChild(btn);

        video = document.createElement('video');
        video.id = 'tm-hidden-video';
        video.muted = true;
        video.playsInline = true;
        document.body.appendChild(video);

        canvas = document.createElement('canvas');
        canvas.id = 'tm-hidden-canvas';
        document.body.appendChild(canvas);

        video.addEventListener('leavepictureinpicture', stopCapture);

        btn.addEventListener('click', async () => {
            if (document.pictureInPictureElement) {
                try { await document.exitPictureInPicture(); } catch {}
                return;
            }
            try {
                startCapture();
                await video.play();
                await video.requestPictureInPicture();
            } catch {
                stopCapture();
            }
        });
    }

    function poll() {
        if (!document.hasFocus()) return;

        if (!stream || !document.contains(stream)) {
            stream = findStream();
            if (stream) createButton();
        }
    }

    function startPolling() {
        if (polling) return;
        polling = setInterval(poll, 1000);
    }

    function stopPolling() {
        if (!polling) return;
        clearInterval(polling);
        polling = null;
    }

    window.addEventListener('focus', startPolling);
    window.addEventListener('blur', stopPolling);
    window.addEventListener('beforeunload', () => {
        try { if (document.pictureInPictureElement) document.exitPictureInPicture(); } catch {}
    });

    if (document.hasFocus()) startPolling();
})();