Disable captions automatically (after the user hits play) and make the video control bar transparent on Adobe Learn videos.
// ==UserScript==
// @name Adobe Learn Enhancer
// @namespace karim.adobe.learn
// @version 3.0.0
// @description Disable captions automatically (after the user hits play) and make the video control bar transparent on Adobe Learn videos.
// @author Karim Elgazar
// @match *://*.adobe.com/*
// @match *://video.tv.adobe.com/*
// @match *://*.tv.adobe.com/*
// @run-at document-start
// @icon https://www.google.com/s2/favicons?sz=64&domain_url=adobe.com
// @grant none
// @license Apache License, Version 2.0
// ==/UserScript==
(function () {
'use strict';
const TAG = '[Adobe Learn Enhancer]';
const log = (...a) => console.debug(TAG, ...a);
// ---------- 1. Transparent control bar ----------
// The player markup (inside the video iframe):
// <div class="controls pointer-events-auto" data-v-aa047c22 style="--controls-scale: 1;">
// .controls has background-color: var(--bg-color) which is what's covering the video.
const css = `
.controls,
.controls.pointer-events-auto,
div.controls[class*="pointer-events"] {
background-color: transparent !important;
background-image: linear-gradient(
to top,
rgba(0, 0, 0, 0.55) 0%,
rgba(0, 0, 0, 0.25) 60%,
rgba(0, 0, 0, 0) 100%
) !important;
}
.controls .v-button,
.controls .controls__timestamp {
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.9));
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
opacity: 1 !important;
}
/* Hide the top-left chapter-marker bubble. */
.chapter-marker {
display: none !important;
}
`;
function injectStyle() {
if (document.getElementById('adobe-learn-enhancer-style')) return;
const style = document.createElement('style');
style.id = 'adobe-learn-enhancer-style';
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
}
injectStyle();
document.addEventListener('DOMContentLoaded', injectStyle, { once: true });
// ---------- 2. Disable captions when they appear ----------
// Goal: whenever the closed-captions button is reachable AND reports it's ON
// (aria-pressed="true"), click it once to turn captions OFF. We don't know
// exactly when the player will mount its controls — it might be on hydrate,
// it might be only after the user presses play — so we use three triggers:
// (a) a MutationObserver on the whole document
// (b) a listener on every <video>'s "play" event
// (c) a slow polling interval as a final safety net
const CC_SELECTOR =
'button[aria-label="closed captions" i], button[aria-label*="caption" i]';
const handled = new WeakSet();
function disableCaptionsOnce(btn) {
if (handled.has(btn)) return false;
const pressed = btn.getAttribute('aria-pressed');
if (pressed === 'true') {
try {
btn.click();
handled.add(btn);
log('Clicked CC button to turn captions OFF.');
return true;
} catch (e) {
console.warn(TAG, 'CC click failed:', e);
}
} else if (pressed === 'false') {
// Already off — remember it so we don't fight the user if they
// turn captions back on manually.
handled.add(btn);
}
return false;
}
function forceTextTracksOff() {
document.querySelectorAll('video').forEach((v) => {
if (!v.textTracks || !v.textTracks.length) return;
for (const t of v.textTracks) {
if (t.mode === 'showing') {
t.mode = 'disabled';
log('Forced textTrack OFF:', t.label || t.language || '(no label)');
}
}
});
}
function scanAndDisable() {
let acted = false;
document.querySelectorAll(CC_SELECTOR).forEach((btn) => {
if (disableCaptionsOnce(btn)) acted = true;
});
forceTextTracksOff();
return acted;
}
// (a) Watch the DOM for the CC button being added or its aria-pressed flipping to "true".
const mo = new MutationObserver((mutations) => {
let shouldScan = false;
for (const m of mutations) {
if (m.type === 'attributes' && m.attributeName === 'aria-pressed') {
shouldScan = true;
break;
}
if (m.type === 'childList' && m.addedNodes.length) {
shouldScan = true;
break;
}
}
if (shouldScan) scanAndDisable();
});
function startObserver() {
mo.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['aria-pressed'],
});
}
startObserver();
// (b) Hook every <video> we ever see, so we can react the moment the user hits play.
const wiredVideos = new WeakSet();
function wireVideos() {
document.querySelectorAll('video').forEach((v) => {
if (wiredVideos.has(v)) return;
wiredVideos.add(v);
const onPlay = () => {
log('Video play event — scanning for CC button.');
// Run a few times because the controls bar may animate in.
scanAndDisable();
setTimeout(scanAndDisable, 150);
setTimeout(scanAndDisable, 600);
setTimeout(scanAndDisable, 1500);
};
v.addEventListener('play', onPlay);
v.addEventListener('playing', onPlay);
log('Wired play listener on a <video>.');
});
}
// Re-run wiring whenever the DOM changes.
new MutationObserver(wireVideos).observe(document.documentElement, {
childList: true,
subtree: true,
});
document.addEventListener('DOMContentLoaded', wireVideos, { once: true });
// (c) Safety-net poll (cheap, every 1s for up to 60s).
let tries = 0;
const poll = setInterval(() => {
tries++;
wireVideos();
scanAndDisable();
if (tries >= 60) clearInterval(poll);
}, 1000);
console.info(TAG, 'v3.0.0 active in', location.host, location.pathname);
})();