Last active
July 3, 2018 12:12
-
-
Save olmokramer/96a2b4daac2d1e6c2120352f0baaf653 to your computer and use it in GitHub Desktop.
Video controls for qutebrowser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name VideoControls | |
// @namespace https://github.com/olmokramer | |
// @description Add mouse and keyboard controls to video elements | |
// @include * | |
// @version 8 | |
// @author Olmo Kramer | |
// ==/UserScript== | |
// Controls: | |
// | |
// click toggle play/pause | |
// double click toggle fullscreen | |
// | |
// shift + scroll down/up seek forward/backward (handled by chromium) | |
// alt + scroll down/up change volume down/up | |
// | |
// arrow left/right seek backward/forward (handled by chromium) | |
// arrow down/up change volume down/up (handled by chromium) | |
// [ decrease playback rate | |
// ] increase playback rate | |
// m toggle mute | |
// Space toggle play/pause (handled by chromium) | |
// p toggle play/pause | |
// r toggle repeat | |
// Escape quit fullscreen | |
// f toggle fullscreen | |
// | |
// To get hints on videos, use the url group. The 'normal' hint target toggles | |
// the paused state on the video, while other targets act on the video url. | |
// | |
// :hint url [target] | |
(function IIFE() { | |
'use strict'; | |
const controlHideTimeout = 1000; | |
const doubleClickTimeout = 300; | |
const volumeStep = .05; | |
const seekStep = 5; | |
const rateStep = .25; | |
let focusedVideo = null; | |
function clamp(value, min, max) { | |
return Math.max(min, Math.min(max, value)); | |
} | |
function togglePlayPause(video) { | |
video.focus(); | |
if (video.paused) { | |
video.play(); | |
} else { | |
video.pause(); | |
} | |
} | |
function toggleFullscreen(video) { | |
if (document.fullscreenElement || document.webkitFullscreenElement) { | |
if (document.exitFullscreen) { | |
document.exitFullscreen(); | |
} else { | |
document.webkitExitFullscreen(); | |
} | |
} else { | |
if (video.requestFullScreen) { | |
video.requestFullScreen(); | |
} else { | |
video.webkitRequestFullScreen(); | |
} | |
} | |
} | |
function seekVideo(video, delta) { | |
video.currentTime = clamp(video.currentTime + delta, 0, video.duration); | |
} | |
function changeVolume(video, delta) { | |
video.volume = clamp(video.volume + delta, 0, 1); | |
} | |
function toggleMute(video) { | |
video.muted = !video.muted; | |
} | |
function toggleLoop(video) { | |
video.loop = !video.loop | |
} | |
function changePlaybackRate(video, delta) { | |
video.playbackRate = clamp(video.playbackRate + delta, 0, 5); | |
} | |
function addClickControls(video) { | |
let timeout = null; | |
video.addEventListener('click', function onClick(event) { | |
event.preventDefault(); | |
if (timeout == null) { | |
timeout = setTimeout(function onTimeout() { | |
timeout = null; | |
togglePlayPause(video); | |
}, doubleClickTimeout); | |
} else { | |
clearTimeout(timeout); | |
timeout = null; | |
toggleFullscreen(video); | |
} | |
}, true); | |
} | |
function addWheelControls(video) { | |
video.addEventListener('wheel', function onScroll(event) { | |
if (event.altKey) { | |
event.preventDefault(); | |
if (event.deltaX < 0) { | |
changeVolume(video, volumeStep); | |
} else if (event.deltaX > 0) { | |
changeVolume(video, -volumeStep); | |
} | |
} | |
}, true); | |
} | |
function addKeyControls(video) { | |
document.addEventListener('keyup', function onKeyUp(event) { | |
if (focusedVideo != video) { | |
return; | |
} | |
// The cases that check the document.activeElement are | |
// already handled by chrome *if* the video is the | |
// activeElement, so we don't need to handle them. It | |
// is a bit unclear when chromium considers the video | |
// to be focused, and it appears to unfocus videos | |
// whenever the controls are hidden. | |
switch (event.key) { | |
case 'ArrowLeft': | |
if (document.activeElement != video) { | |
return; | |
} | |
seekVideo(video, -seekStep); | |
break; | |
case 'ArrowRight': | |
if (document.activeElement == video) { | |
return; | |
} | |
seekVideo(video, seekStep); | |
break; | |
case 'ArrowUp': | |
if (document.activeElement == video) { | |
return; | |
} | |
changeVolume(video, volumeStep); | |
break; | |
case 'ArrowDown': | |
if (document.activeElement == video) { | |
return; | |
} | |
changeVolume(video, -volumeStep); | |
break; | |
case '[': | |
changePlaybackRate(video, -rateStep); | |
break; | |
case ']': | |
changePlaybackRate(video, rateStep); | |
break; | |
case 'm': | |
toggleMute(video); | |
break; | |
case ' ': | |
if (document.activeElement == video) { | |
return; | |
} | |
// no break | |
case 'p': | |
togglePlayPause(video); | |
break; | |
case 'r': | |
toggleLoop(video); | |
break; | |
case 'Escape': | |
if (!document.fullscreenElement && !document.webkitFullscreenElement) { | |
return; | |
} | |
// no break | |
case 'f': | |
toggleFullscreen(video); | |
break; | |
default: | |
return; | |
} | |
event.preventDefault(); | |
}, true); | |
} | |
function autoHideControls(video) { | |
if (video.id == '') { | |
const r = Math.round(Math.random() * Number.MAX_SAFE_INTEGER); | |
video.id = `__qb_video__${r}`; | |
} | |
let style = document.createElement('style'); | |
style.innerHTML = ` | |
video#${video.id} { | |
cursor: pointer; | |
} | |
video#${video.id}:not([controls]) { | |
cursor: none; | |
} | |
video#${video.id}:not([controls])::-webkit-media-controls { | |
display: none !important; | |
} | |
`; | |
document.head.appendChild(style); | |
let timeout = null; | |
const origControls = video.getAttribute('controls'); | |
video.addEventListener('mousemove', function() { | |
if (timeout != null) { | |
clearTimeout(timeout); | |
timeout = null; | |
} | |
video.setAttribute('controls', origControls); | |
timeout = setTimeout(function onTimeout() { | |
timeout = null; | |
video.removeAttribute('controls'); | |
video.focus(); | |
}, controlHideTimeout); | |
}); | |
} | |
function addControls(video) { | |
if (video.__qb_controls__ || !video.controls) { | |
return; | |
} | |
video.__qb_controls__ = true; | |
addClickControls(video); | |
addWheelControls(video); | |
addKeyControls(video); | |
autoHideControls(video); | |
} | |
new MutationObserver(function onMutate(mutations) { | |
for (let mutation of mutations) { | |
for (let node of mutation.addedNodes) { | |
if (node instanceof HTMLVideoElement) { | |
addControls(node); | |
} | |
} | |
} | |
}).observe(document.body, { | |
childList: true, | |
subtree: true, | |
}); | |
document.addEventListener('focus', function onFocus(event) { | |
focusedVideo = event.target.closest('video'); | |
}, true); | |
for (let video of Array.from(document.querySelectorAll('video'))) { | |
addControls(video); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment