Skip to content

Instantly share code, notes, and snippets.

@sharethewisdom
Last active September 1, 2025 20:41
Show Gist options
  • Save sharethewisdom/43ca7b86e5d40289632ae5c8812260f5 to your computer and use it in GitHub Desktop.
Save sharethewisdom/43ca7b86e5d40289632ae5c8812260f5 to your computer and use it in GitHub Desktop.
improved accessibility and user experience of the Theoplayer on VRTMAX
// ==UserScript==
// @name Fix vrtmax
// @namespace http://tampermonkey.net/
// @version 2025-07-23
// @description redirect watch pages to show episodes, focus the player when it appears or when exiting fullscreen
// @author You
// @match https://www.vrt.be/vrtmax/*
// @run-at document-start
// @icon https://www.google.com/s2/favicons?sz=64&domain=vrt.be
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
// https://www.vrt.be/vrtmax/artikels/2024/03/18/hoe-gebruik-ik-de-videospeler-op-vrt-max/
(function() {
'use strict';
const childObserver = new MutationObserver((mutationList) => {
let newpath = window.location.pathname
if (newpath.startsWith("/vrtmax/a-z/")) {
if (newpath.split("/").length < 7) { // if this is an overview, not an episode sub-page
const startButton = document.querySelector('header[aria-label="Paginahoofding"] a[href^="/vrtmax"]')
if (startButton !== null) { // fetch the episode path of the first anchor, which is "start/continue watching"
newpath = startButton.getAttribute('href')
if (newpath.startsWith("/vrtmax/a-z/")) {
window.stop()
window.location.pathname = newpath
return
}
}
} else { // if it's an episode sub-page
// console.log("old " + oldpath + "\tnew " + newpath + " l:" + newpath.split("/").length)
if (window.location.search != "?tab=episodes") { // select the episode tab rather than "continue watching" list
window.stop()
window.location.search = "?tab=episodes";
return
}
}
} else if (newpath.startsWith("/vrtmax/livestream/")) {
console.log("livestream")
} else {
return
}
for (const mutation of mutationList) {
for (const node of mutation.addedNodes.values()) {
//console.log(node)
if (node.tagName.toLowerCase() === "vrt-mediaplayer") {
new MutationObserver((attrMutations,o) => {
const player = attrMutations[0].target.shadowRoot.childNodes[0];
init(player)
o.disconnect();
}).observe(node,{ attributes: true });
}
}
}
}).observe(document.documentElement, { childList: true, subtree: true });
window.addEventListener('beforeunload', (event) => {
childObserver.disconnect()
});
function init(player) {
document.addEventListener('keydown', (e) => {
if (document.activeElement.tagName === "INPUT") return
const video = player.getElementsByTagName("video")[0]
const ui = player.getElementsByTagName("ui-container")[0]
const layout = player.getElementsByTagName("video-layout-regular")[0]
layout.firstChild.style.background = "none"; // background fade
layout.firstChild.style.fontSize = "xxx-large";
layout.firstChild.style.fontFamily = "mono";
layout.firstChild.style.justifyContent = "end";
if (e.key === "k") {
player.focus()
const event = (video.paused) ? new Event("doPlay") : new Event("doPause")
if (video.currentTime < 2 && video.paused) {
video.volume = 0; // start off quietly
video.style.filter = "brightness(0.1)";
video.playbackRate = GM_getValue("rate",1)
const poll = setInterval(() => {
const countdown = ui.querySelector('video-countdown-btn>button.countdown')
if (video.currentTime >= 2 && countdown === null) {
clearInterval(poll)
video.style.filter = "brightness(1)";
rampVolume(video)
}
}, 500);
}
ui.dispatchEvent(event)
} else if (e.key === "l" || e.key === "ArrowRight") {
video.currentTime += 10
} else if (e.key === "h" || e.key === "ArrowLeft") {
video.currentTime -= 12
} else if (e.key === "ArrowUp") {
e.preventDefault()
let volume = +((video.volume + 0.1).toFixed(1))
volume = (volume > 1) ? 1 : volume
video.volume = volume
GM_setValue("volume", volume)
osd(ui,layout,volume * 100 + "%")
setTimeout(() => ui.dispatchEvent(new Event("hideControlsForSettings")), 1000)
} else if (e.key === "ArrowDown") {
e.preventDefault()
let volume = +((video.volume - 0.1).toFixed(1))
volume = (volume < 0) ? 0 : volume
video.volume = volume
GM_setValue("volume", volume)
osd(ui,layout,volume * 100 + "%")
setTimeout(() => ui.dispatchEvent(new Event("hideControlsForSettings")), 1000)
} else if (e.key === "m") {
const event = (video.muted) ? new Event("doUnmute") : new Event("doMute")
ui.dispatchEvent(event)
} else if (e.key === "]") { // TODO: osd
let rate = +((video.playbackRate + 0.1).toFixed(1))
rate = (rate > 2) ? 2.0 : rate.toFixed(1)
GM_setValue("rate", rate)
video.playbackRate = rate
osd(ui,layout,rate)
setTimeout(() => ui.dispatchEvent(new Event("hideControlsForSettings")), 1000)
} else if (e.key === "[") { // TODO: osd
let rate = +((video.playbackRate - 0.1).toFixed(1))
rate = (rate < 0.5) ? 0.5 : rate.toFixed(1)
GM_setValue("rate", rate)
video.playbackRate = rate
osd(ui,layout,rate)
setTimeout(() => ui.dispatchEvent(new Event("hideControlsForSettings")), 1000)
} else if (e.code === "Enter") { // TODO: why does it sometimes pause the video?
const open = ui.querySelector('button.sc-common-settings-btn[aria-expanded="true"]')
const event = open ? new Event("closeSettings") : new Event("openSettings")
ui.dispatchEvent(event)
ui.dispatchEvent(new Event("showControlsForSettings"))
} else if (e.code === "Slash") {
e.preventDefault();
document.querySelector('input[type="search"]').focus();
} else if (e.key === "t") {
const grid = document.getElementById("main").parentElement;
const aside = document.querySelector('aside[aria-label="Gerelateerde inhoud"]');
if (grid.style.gridTemplateAreas.indexOf("aside") > 0) {
setTimeout(() => { aside.style.width = "0px"; grid.style.gridTemplateAreas = '"topbar topbar" "main main" "footer footer"' }, 200);
aside.style.transition = "left 0.2s ease-out";
aside.style.left = "100%";
} else {
aside.style.width = "auto";
grid.style.gridTemplateAreas = '"topbar topbar" "main aside" "footer footer"';
setTimeout(() => { aside.style.left = "0%" }, 100);
}
} else if (e.code === "Space" || e.key === "f") {
e.preventDefault()
player.focus()
}
})
document.addEventListener('fullscreenchange', () => {(!document.fullscreenElement) && player.focus()})
document.addEventListener("visibilitychange", () => {(!document.hidden) && player.focus()})
const rampVolume = (video) => {
const c = setInterval(() => {
if (video.volume === GM_getValue("volume", 1)) {
clearInterval(c)
return
}
let volume = +((video.volume + 0.05).toFixed(2))
volume = (volume < 0) ? 0 : volume
video.volume = volume
}, 200);
};
const osd = (ui,layout,text,timer) => {
ui.dispatchEvent(new Event("showControlsForSettings"))
const el = layout.firstChild
el.innerHTML = text;
//setTimeout((el) => { if (el.classList.contains("hidden")) alert("hidden")}, 2000, el);
el.addEventListener("transitionend", () => {
if (getComputedStyle(el).getPropertyValue("opacity") === "0") el.innerHTML = "";
})
};
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment