您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add NBA-style shortcuts (J/L/U/O/Space + S/D) to the MagentaSport player. Works with Mozilla Firefox.
// ==UserScript== // @name MagentaSport Player Keyboard Shortcuts // @namespace https://github.com/dusanvin/MagentaSport-Videoplayer-Shortcuts // @version 1.0 // @description Add NBA-style shortcuts (J/L/U/O/Space + S/D) to the MagentaSport player. Works with Mozilla Firefox. // @author Vincent Dusanek // @license CC-BY-NC-SA-4.0 // @match https://www.magentasport.de/* // @run-at document-idle // @grant none // ==/UserScript== (function () { 'use strict'; // Anpassbare Schrittweiten (Sekunden) const STEP_SMALL = 10; // J / L const STEP_LARGE = 60; // U / O // Diskrete Geschwindigkeitsstufen (min 0.1x, max 16x) const RATE_STEPS = [0.1, 0.25, 0.5, 1, 2, 4, 8, 16]; // Kleines Overlay für Feedback let toast; function ensureToast() { if (toast) return toast; toast = document.createElement('div'); toast.id = 'ms-shortcuts-toast'; Object.assign(toast.style, { position: 'fixed', left: '50%', bottom: '12%', transform: 'translateX(-50%)', padding: '8px 12px', borderRadius: '10px', background: 'rgba(0,0,0,0.65)', color: '#fff', fontSize: '14px', fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif', zIndex: 999999, opacity: '0', transition: 'opacity .15s ease', pointerEvents: 'none', userSelect: 'none' }); document.documentElement.appendChild(toast); return toast; } let toastTimer; function showToast(text) { const el = ensureToast(); el.textContent = text; el.style.opacity = '1'; clearTimeout(toastTimer); toastTimer = setTimeout(() => (el.style.opacity = '0'), 650); } // Aktives Video finden (robust bei SPA/Reload des Players) function findActiveVideo() { // Bevorzugt das bekannte Magenta-Video const byId = document.querySelector('#sravvpl_video-element--0'); if (byId && isUsableVideo(byId)) return byId; // Fallback: sichtbares, initialisiertes <video> const vids = Array.from(document.querySelectorAll('video')); const candidates = vids .filter(isUsableVideo) .sort((a, b) => (scoreVideo(b) - scoreVideo(a))); // priorisieren return candidates[0] || null; } function isUsableVideo(v) { if (!(v instanceof HTMLVideoElement)) return false; const rect = v.getBoundingClientRect(); const visible = rect.width > 0 && rect.height > 0; return visible; } function scoreVideo(v) { let s = 0; if (v.currentSrc || v.src) s += 2; if (isFinite(v.duration) && v.duration > 0) s += 2; if (!v.paused) s += 1; return s; } // Zielzeit in erlaubten Bereich klemmen (auch für Live/DVR) function clampTime(video, target) { try { const seekable = video.seekable; if (seekable && seekable.length) { const start = seekable.start(0); const end = seekable.end(seekable.length - 1); const epsilon = 0.25; // kleine Marge am Ende return Math.min(Math.max(target, start), end - epsilon); } } catch (e) {} if (isFinite(video.duration) && video.duration > 0) { const epsilon = 0.25; return Math.min(Math.max(target, 0), video.duration - epsilon); } return Math.max(target, 0); } function seekBy(video, delta) { const target = clampTime(video, (video.currentTime || 0) + delta); video.currentTime = target; const sign = delta > 0 ? '+' : '–'; const secs = Math.abs(delta); showToast(`${sign}${secs}s`); } function togglePlay(video) { if (video.paused) { const p = video.play(); if (p && typeof p.catch === 'function') p.catch(() => {}); showToast('▶︎'); } else { video.pause(); showToast('⏸'); } } // Geschwindigkeit anpassen (dir: +1 schneller, -1 langsamer) function bumpRate(video, dir) { const cur = video.playbackRate || 1; const eps = 1e-6; let next = cur; if (dir > 0) { // nächstgrößere Stufe finden, sonst Maximum next = RATE_STEPS.find(r => r > cur + eps) ?? RATE_STEPS[RATE_STEPS.length - 1]; } else { // nächstkleinere Stufe finden, sonst Minimum for (let i = RATE_STEPS.length - 1; i >= 0; i--) { if (RATE_STEPS[i] < cur - eps) { next = RATE_STEPS[i]; break; } } if (next === cur) next = RATE_STEPS[0]; } video.playbackRate = next; showToast(`${formatRate(next)}×`); } function formatRate(r) { // hübsche Ausgabe, max. 2 Nachkommastellen, ohne überflüssige Nullen const s = (Math.round(r * 100) / 100).toString(); return s.replace(/(\.\d*[1-9])0+$|\.0+$/, '$1'); } // Nur auslösen, wenn nicht gerade in einem Eingabefeld getippt wird function isTypingInField(e) { const t = e.target; return ( t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/i.test(t.tagName)) ); } // Einmal registrieren – auch bei SPA-Navigation ausreichend window.addEventListener('keydown', (e) => { if (isTypingInField(e)) return; const key = e.key.toLowerCase(); if (!['j', 'l', 'u', 'o', ' ', 's', 'd'].includes(key)) return; const video = findActiveVideo(); if (!video) return; // optional: Wiederholungen (Taste gehalten) ignorieren if (e.repeat) return; if (key === ' ') { e.preventDefault(); // Scrollen der Seite verhindern togglePlay(video); return; } switch (key) { case 'j': seekBy(video, -STEP_SMALL); break; case 'l': seekBy(video, +STEP_SMALL); break; case 'u': seekBy(video, -STEP_LARGE); break; case 'o': seekBy(video, +STEP_LARGE); break; case 's': // langsamer bumpRate(video, -1); break; case 'd': // schneller bumpRate(video, +1); break; } }); // Bonus: Reagiere auf Player-Wechsel (SPA) und räume altes Toast auf const mo = new MutationObserver(() => { if (toast && !document.documentElement.contains(toast)) { toast = null; ensureToast(); } }); mo.observe(document.documentElement, { childList: true, subtree: true }); })();