// ==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 = ""; }) }; } })();