Last active
September 1, 2025 20:41
-
-
Save sharethewisdom/43ca7b86e5d40289632ae5c8812260f5 to your computer and use it in GitHub Desktop.
improved accessibility and user experience of the Theoplayer on VRTMAX
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 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