Last active
November 30, 2023 00:47
-
-
Save vogler/b7c66088cd22f899496f353d5295b7ad to your computer and use it in GitHub Desktop.
Tampermonkey: video: scroll=seek
This file contains hidden or 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 Video: scroll=seek | |
// @namespace https://gist.github.com/vogler | |
// @downloadURL https://gist.github.com/vogler/b7c66088cd22f899496f353d5295b7ad/raw/video-scroll-seek.tamper.js | |
// @version 0.26 | |
// @description Scrolling a video seeks back/forward | |
// @author Ralf Vogler | |
// @match *://*/* | |
// @grant none | |
// ==/UserScript== | |
const parseDuration = str => str.split(':').toReversed().reduce((a,x,i) => a + parseInt(x) * 60**i, 0); // to seconds | |
(function() { | |
'use strict'; | |
// seek back/forward `step` seconds | |
const step = 3; | |
// If an element over the video snags scroll events (e.g. a div for video controls), we have to listen on it instead. | |
// If there is no manual rule for which element that is, we try to be smart and find something that works better than `v => v`. | |
// Unused: here we get the topmost element at the top-left position of the video element -> did not work for fm4.orf.at | |
const eventElementFromPosition = v => { | |
const b = v.getBoundingClientRect(); | |
// const es = document.elementsFromPoint(b.x+b.width/2, b.y+b.height/2); | |
const es = document.elementsFromPoint(b.x+10, b.y+10); | |
console.log(es); | |
return es[0]; | |
}; | |
// Used alternative: as long as the parent element still has the same dimensions, it will likely be fine to use for events. | |
const eventElementParent = e => { | |
const p = e.parentElement; | |
const be = e.getBoundingClientRect(); | |
const bp = p.getBoundingClientRect(); | |
// console.log(be, bp); | |
return (bp.width == be.width && bp.height == be.height) ? eventElementParent(p) : e; | |
}; | |
// manual override | |
const eventElement = [ | |
// ['coursera.org', v => v.parentElement], | |
['ted.com', v => v.closest('#ted-player')], | |
['netflix.com', v => document.querySelector('.watch-video--player-view')], // setting video.currentTime results in error "Sorry, we're having trouble with your request." | |
['waipu.tv', v => v.closest('.player-wrapper')], | |
['joyn.de', v => document.querySelector('main')], | |
['video.sat1.de', v => document.querySelector('.video-container')], | |
['account.ring.com/account/activity-history', v => v.closest('div')], | |
['store.epicgames.com', v => v.parentElement], | |
// ['fm4.orf.at', v => v.parentElement], | |
].reduce((af, [u,f]) => location.href.includes(u) ? f : af, eventElementParent); // v => v | |
const f = el => { | |
// console.log('f on', el); | |
if (!el.getElementsByTagName) return; // Angular adds some weird #comment and #text elements which don't have this function... | |
const vs = el.tagName == 'VIDEO' ? [el] : [...el.getElementsByTagName('video')]; // find videos in element | |
if (vs.length < 1) return; | |
// console.log('video-scroll-seek: found', vs.length, 'video(s)', vs, 'in', el); | |
vs.forEach(v => { | |
const e = eventElement(v); // element that gets the event | |
// console.log(v, e); | |
if (e.added) return; // avoid adding multiple listeners to the same video! YouTube added 7 b/c of mutations... | |
e.added = true; | |
// ring.com uses WebRTC where the video element does not have a .src but .srcObject with a MediaStream which does not allow to change .currentTime or .playbackRate (.duration is Inf). | |
// Disabling WebRTC (media.peerconnection.enabled in Firefox) disables ring.com video playback. | |
// However, their JS adds some streamedPlaybackControls with .play(), .pause(), .seek() etc. | |
let controls; | |
if (location.href.includes('ring.com')) { | |
const el = document.querySelector('main > div > div'); // react element with the JS object we want, e.g. div.styled__VideoContainer-sc-ff7bebee-15 | |
const reactPropsField = Object.keys(el).find(x => x.includes('reactProps')); // e.g. __reactProps$sjvdkfurj7s | |
controls = el[reactPropsField].children.props.streamedPlaybackControls; | |
console.log('WebRTC controls:', controls); | |
controls.time = 0; | |
v.muted = false; // also unmute video | |
} | |
e.addEventListener('wheel', ev => { // add listener for each video | |
// console.log('wheel', ev.wheelDeltaY > 0 ? 'up' : 'down'); | |
const scale = ev.altKey ? 10 : 1; | |
// on the trackpad we can scroll vertically or horizontally -> use the direction with the biggest delta | |
const scroll = Math.abs(ev.wheelDeltaY) > Math.abs(ev.wheelDeltaX) ? ev.wheelDeltaY : ev.wheelDeltaX; | |
const jump = step * scale * -1 * scroll/120; | |
// * -1 means back = left = up; forward = right = down | |
// had Math.sign(scroll) instead of scroll/120 before, but then scroll acceleration is ignored - min. scroll with wheel is 120, but may go up to 600 when scrolling faster | |
// on trackpad values are generally smaller, but fired more often, so scrolling speed is similar | |
if (location.href.includes('netflix.com')) { // does not allow setting video.currentTime, so we have to click their backward/forward buttons... | |
document.querySelector(scroll > 0 ? 'button[aria-label="Seek Forward"]' : 'button[aria-label="Seek Back"]').click(); | |
} else if (location.href.includes('ring.com')) { | |
const duration = parseDuration(document.querySelector('[role="progressbar"] + div').innerText); // since v.duration is Inf | |
// seek to `v.currentTime + ..` did not work since it did not update fast enough | |
controls.time = Math.abs(controls.time + jump) % duration; | |
controls.seek(controls.time, true); // true to not pause | |
} else { | |
v.currentTime += jump; | |
} | |
ev.preventDefault(); | |
}); | |
console.log('video-scroll-seek: added handler for video', v, 'to element', e); // v.baseURI | |
}); | |
}; | |
f(document.body); // handle all videos | |
// setTimeout(() => f(document.body), 3000); // if our wheel event listener gets removed, we add it again after 3s | |
// we also want to react to dynamically added videos, i.e., call f on any nodes added to document.body | |
(new MutationObserver((ms, ob) => ms.forEach(m => m.addedNodes.forEach(f)))).observe(document.body, { subtree: true , childList: true }); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment