您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Simpler keyboard shorcuts with visual indicators for Microsoft Stream/SharePoint videos
// ==UserScript== // @name Better Keyboard Shortcuts for Microsoft Stream/SharePoint // @namespace http://tampermonkey.net/ // @version 1.2.0 // @description Simpler keyboard shorcuts with visual indicators for Microsoft Stream/SharePoint videos // @author kazcfz // @include https://*.sharepoint.com/* // @icon https://res-1.cdn.office.net/shellux/stream_24x.12dba766a9c30382b781c971070dc87c.svg // @grant none // @license MIT // ==/UserScript== /* eslint curly: "off" */ (function () { 'use strict'; // Wait until .oneplayer-root exists in the DOM, then run callback with it function waitForOnePlayerRoot(callback) { const root = document.querySelector('.oneplayer-root') || document.querySelector('.OnePlayer-container'); if (root) { callback(root); return; } const docObserver = new MutationObserver((mutations, obs) => { const el = document.querySelector('.oneplayer-root') || document.querySelector('.OnePlayer-container'); if (el) { obs.disconnect(); callback(el); } }); docObserver.observe(document.body || document.documentElement, { childList: true, subtree: true }); } waitForOnePlayerRoot(videoRoot => { if (getComputedStyle(videoRoot).position === 'static') videoRoot.style.position = 'relative'; // Overlay to indicate Skip const skipOverlay = document.createElement('div'); skipOverlay.style.position = 'absolute'; skipOverlay.style.top = '50%'; skipOverlay.style.transform = 'translateY(-50%)'; skipOverlay.style.padding = '15px 15px'; skipOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.9)'; skipOverlay.style.borderRadius = '50%'; skipOverlay.style.pointerEvents = 'none'; skipOverlay.style.opacity = '0'; skipOverlay.style.transition = 'opacity 0.1s ease'; skipOverlay.style.zIndex = '9999'; skipOverlay.style.userSelect = 'none'; skipOverlay.style.display = 'flex'; skipOverlay.style.flexDirection = 'column'; skipOverlay.style.alignItems = 'center'; skipOverlay.style.justifyContent = 'center'; skipOverlay.style.gap = '0.2em'; skipOverlay.style.minWidth = '75px'; skipOverlay.style.minHeight = '75px'; skipOverlay.style.textAlign = 'center'; videoRoot.appendChild(skipOverlay); // Overlay to indicate Volume const volumeOverlay = document.createElement('div'); volumeOverlay.style.position = 'absolute'; volumeOverlay.style.top = '10%'; volumeOverlay.style.left = '0'; volumeOverlay.style.right = '0'; volumeOverlay.style.margin = 'auto'; volumeOverlay.style.padding = '7px 11px'; volumeOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; volumeOverlay.style.borderRadius = '5px'; volumeOverlay.style.pointerEvents = 'none'; volumeOverlay.style.opacity = '0'; volumeOverlay.style.transition = 'opacity 0.015s ease'; volumeOverlay.style.zIndex = '9999'; volumeOverlay.style.userSelect = 'none'; volumeOverlay.style.display = 'flex'; volumeOverlay.style.flexDirection = 'column'; volumeOverlay.style.alignItems = 'center'; volumeOverlay.style.justifyContent = 'center'; volumeOverlay.style.gap = '0.2em'; volumeOverlay.style.width = '50px'; volumeOverlay.style.minHeight = '30px'; volumeOverlay.style.textAlign = 'center'; videoRoot.appendChild(volumeOverlay); // Overlay to indicate Play/Pause const centerOverlay = document.createElement('div'); centerOverlay.style.position = 'absolute'; centerOverlay.style.top = '50%'; centerOverlay.style.left = '50%'; centerOverlay.style.right = 'auto'; centerOverlay.style.margin = '0'; centerOverlay.style.transform = 'translate(-50%, -50%)'; centerOverlay.style.padding = '15px'; centerOverlay.style.fontVariantEmoji = 'text'; centerOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; centerOverlay.style.borderRadius = '50%'; centerOverlay.style.pointerEvents = 'none'; centerOverlay.style.opacity = '0'; centerOverlay.style.zIndex = '9999'; centerOverlay.style.userSelect = 'none'; centerOverlay.style.display = 'flex'; centerOverlay.style.flexDirection = 'column'; centerOverlay.style.alignItems = 'center'; centerOverlay.style.justifyContent = 'center'; centerOverlay.style.textAlign = 'center'; videoRoot.appendChild(centerOverlay); const centerSymbol = document.createElement('span'); centerSymbol.style.opacity = '1'; centerOverlay.appendChild(centerSymbol); // Overlay text for current volume const volumeText = document.createElement('div'); volumeText.style.fontSize = '17px'; volumeText.style.fontWeight = '500'; volumeText.style.color = 'rgba(255,255,255,0.85)'; volumeText.style.userSelect = 'none'; volumeOverlay.appendChild(volumeText); // Overlay container for left/right triangles const trianglesContainer = document.createElement('div'); trianglesContainer.style.display = 'flex'; trianglesContainer.style.gap = '10px'; skipOverlay.appendChild(trianglesContainer); // Add triangles into its container const triangles = []; for (let i = 0; i < 3; i++) { const tri = document.createElement('span'); tri.textContent = '▶'; tri.style.fontSize = '13px'; tri.style.fontWeight = 'bold'; trianglesContainer.appendChild(tri); triangles.push(tri); } // Overlay text for seconds skipped const secondsText = document.createElement('div'); secondsText.style.fontSize = '14px'; secondsText.style.fontWeight = 'normal'; secondsText.style.color = 'rgba(255,255,255,0.85)'; secondsText.style.userSelect = 'none'; skipOverlay.appendChild(secondsText); let hideTimeoutSkipOverlay; let hideTimeoutVolumeOverlay; let hideTimeoutCenterOverlay; let animTimeouts = []; // Displays overlay triangle animation and seconds skipped const secondsToSkip = 5; function showAnimatedTriangles(side) { // Clear animation timers clearTimeout(hideTimeoutSkipOverlay); animTimeouts.forEach(t => clearTimeout(t)); animTimeouts = []; const isLeft = side === 'left'; const char = isLeft ? '◀' : '▶'; const order = isLeft ? [2, 1, 0] : [0, 1, 2]; triangles.forEach(t => { t.textContent = char; t.style.transition = 'none'; t.style.color = 'rgba(255,255,255,0.3)'; }); // Force style flush to apply the color immediately before re-enabling transitions void skipOverlay.offsetHeight; triangles.forEach(t => { t.style.transition = 'color 0.3s ease'; }); secondsText.textContent = `${secondsToSkip} seconds`; skipOverlay.style.opacity = '1'; skipOverlay.style.left = isLeft ? '10%' : 'auto'; skipOverlay.style.right = isLeft ? 'auto' : '10%'; skipOverlay.style.textAlign = 'center'; triangles[order[0]].style.color = 'rgba(255,255,255,0.75)'; const interval = 200; for (let step = 2; step <= 3; step++) { animTimeouts.push(setTimeout(() => { if (step === 2) { triangles[order[0]].style.color = 'rgba(255,255,255,0.5)'; triangles[order[1]].style.color = 'rgba(255,255,255,0.75)'; } else if (step === 3) { triangles[order[0]].style.color = 'rgba(255,255,255,0.3)'; triangles[order[1]].style.color = 'rgba(255,255,255,0.5)'; triangles[order[2]].style.color = 'rgba(255,255,255,0.75)'; } }, (step - 1) * interval)); } hideTimeoutSkipOverlay = setTimeout(() => { skipOverlay.style.opacity = '0'; }, 3.3 * interval); } // Checks that the video player is focused function isInOnePlayerRoot(element) { while (element) { if (element === videoRoot) return true; element = element.parentElement; } return false; } let video = null; function setupVideo(v) { if (!v || video === v) return; video = v; } // Initial attempt to find video setupVideo(videoRoot.querySelector('video')); // Observe videoRoot subtree for added/removed video const observer = new MutationObserver(mutations => { for (const mutation of mutations) for (const node of mutation.addedNodes) if (node.nodeType === 1) if (node.tagName === 'VIDEO') setupVideo(node); else { const v = node.querySelector && node.querySelector('video'); if (v) setupVideo(v); } }); observer.observe(videoRoot, { childList: true, subtree: true }); function triggerCenterSymbol(symbol) { clearTimeout(hideTimeoutCenterOverlay); centerOverlay.style.width = '75px'; centerOverlay.style.height = '75px'; centerOverlay.style.transition = 'opacity 0.1s ease'; centerOverlay.style.opacity = '1'; hideTimeoutCenterOverlay = setTimeout(() => { centerOverlay.style.transition = 'width 0.6s ease, height 0.6s ease, font-size 0.6s ease, opacity 0.6s ease'; centerOverlay.style.width = `${parseFloat(getComputedStyle(centerOverlay).width) + 25}px`; centerOverlay.style.height = `${parseFloat(getComputedStyle(centerOverlay).height) + 25}px`; centerOverlay.style.fontSize = `${parseFloat(getComputedStyle(centerOverlay).fontSize) + 22.5}px`; centerOverlay.style.opacity = '0'; }, 60); centerSymbol.textContent = symbol; if (symbol === '🔊' || symbol === '🔇') { centerSymbol.style.marginBottom = '9px'; centerSymbol.style.marginLeft = '5px'; centerOverlay.style.fontSize = '75px'; } else if (symbol === '🔉') { centerSymbol.style.marginBottom = '9px'; centerSymbol.style.marginLeft = '0px'; centerOverlay.style.fontSize = '75px'; } else if (symbol === '▶') { centerSymbol.style.marginBottom = '7px'; centerSymbol.style.marginLeft = '12px'; centerOverlay.style.fontSize = '55px'; } else if (symbol === '⏸') { centerSymbol.style.marginBottom = '17px'; centerSymbol.style.marginLeft = '4px'; centerOverlay.style.fontSize = '68px'; } } // Math time: MS Stream/SharePoint handles volume steps exponentially (cubically) rather than linearly // The sequence is (steps of 0.1)^3, counting down from 1 to 0: // volume (base 𝑛) = (1 − 0.1 × 𝑛)^3, where 𝑛 = 0, 1, 2, ..., 10. // Alternatively, could just keep it to linear volume steps let currentVolumeStepCount = 0; function displayVolume() { const volumePercent = video.volume * 100; if (volumePercent >= 1 || video.volume == 0) volumeText.textContent = `${Math.round(volumePercent)}%`; else volumeText.textContent = `${volumePercent.toFixed(1)}%`; volumeOverlay.style.opacity = '1'; clearTimeout(hideTimeoutVolumeOverlay); hideTimeoutVolumeOverlay = setTimeout(() => { volumeOverlay.style.opacity = '0'; }, 600); } document.addEventListener('keydown', e => { if (!video) return; if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return; if (document.activeElement && !isInOnePlayerRoot(document.activeElement)) return; e.preventDefault(); // → to skip forward if (e.code === 'ArrowRight') { video.currentTime = Math.min(video.duration, video.currentTime + secondsToSkip); showAnimatedTriangles('right'); // ← to skip backward } else if (e.code === 'ArrowLeft') { video.currentTime = Math.max(0, video.currentTime - secondsToSkip); showAnimatedTriangles('left'); // ↑ to increase volume } else if (e.code === 'ArrowUp') { if (currentVolumeStepCount > 0) { currentVolumeStepCount--; video.volume = Math.pow(1 - 0.1 * currentVolumeStepCount, 3); //video.volume = Math.max(0, Math.min(1, Math.round((video.volume + 0.1) * 100) / 100)); } displayVolume(); triggerCenterSymbol('🔊'); // ↓ to decrease volume } else if (e.code === 'ArrowDown') { if (currentVolumeStepCount < 10) { currentVolumeStepCount++; video.volume = Math.pow(1 - 0.1 * currentVolumeStepCount, 3); //video.volume = Math.max(0, Math.min(1, Math.round((video.volume - 0.1) * 100) / 100)); } displayVolume(); if (video.volume == 0) triggerCenterSymbol('🔇'); else triggerCenterSymbol('🔉'); } else { // Adapted from [Sharepoint Keyboard Shortcuts] by [CyrilSLi], MIT License // https://greatest.deepsurf.us/en/scripts/524190-sharepoint-keyboard-shortcuts const keys = { Space: document.querySelector('[aria-description*="Alt + K"]'), KeyF: document.querySelector('[aria-description*="Alt + Enter"]'), KeyM: document.querySelector('[aria-description*="Alt + M"]'), KeyC: document.querySelector('[aria-description*="Alt + C"]'), KeyA: document.querySelector('[aria-description*="Alt + A"]'), } if (e.code === 'Space') { if (video.paused) triggerCenterSymbol('▶'); else triggerCenterSymbol('⏸'); } else if (e.code === 'KeyM') if (video.muted) triggerCenterSymbol('🔊'); else triggerCenterSymbol('🔇'); if (keys.hasOwnProperty(e.code)) keys[e.code].click(); } }); }); })();