Picture-in-Picture for MJPEG images
// ==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();
})();