diff --git a/extension/content/inject.js b/extension/content/inject.js index ab6b0cf..3e6665f 100644 --- a/extension/content/inject.js +++ b/extension/content/inject.js @@ -98,6 +98,10 @@ var tc = { rememberSpeed: getSharedDefault("rememberSpeed", false), forceLastSavedSpeed: getSharedDefault("forceLastSavedSpeed", false), audioBoolean: getSharedDefault("audioBoolean", false), + showAmbientLoopControls: getSharedDefault( + "showAmbientLoopControls", + false + ), startHidden: getSharedDefault("startHidden", false), hideWithYouTubeControls: getSharedDefault( "hideWithYouTubeControls", @@ -488,6 +492,7 @@ function captureSiteRuleBase() { rememberSpeed: tc.settings.rememberSpeed, forceLastSavedSpeed: tc.settings.forceLastSavedSpeed, audioBoolean: tc.settings.audioBoolean, + showAmbientLoopControls: tc.settings.showAmbientLoopControls, controllerOpacity: tc.settings.controllerOpacity, controllerMarginTop: tc.settings.controllerMarginTop, controllerMarginBottom: tc.settings.controllerMarginBottom, @@ -516,6 +521,7 @@ function resetSettingsFromSiteRuleBase() { tc.settings.rememberSpeed = base.rememberSpeed; tc.settings.forceLastSavedSpeed = base.forceLastSavedSpeed; tc.settings.audioBoolean = base.audioBoolean; + tc.settings.showAmbientLoopControls = base.showAmbientLoopControls; tc.settings.controllerOpacity = base.controllerOpacity; tc.settings.controllerMarginTop = base.controllerMarginTop; tc.settings.controllerMarginBottom = base.controllerMarginBottom; @@ -1364,8 +1370,70 @@ function isMediaElement(node) { ); } +function hasInteractivePlayerChrome(node) { + if (!node || node.nodeName !== "VIDEO") return false; + if (node.controls === true) return true; + + if (typeof node.closest === "function") { + var knownPlayer = node.closest( + ".html5-video-player, .video-js, .jwplayer, .plyr, " + + "[class~='video-player'], [data-testid*='video-player'], " + + "media-player, amp-video" + ); + if (knownPlayer) return true; + } + + var ancestor = node.parentElement; + var videoRect = node.getBoundingClientRect(); + var depth = 0; + while (ancestor && depth < 4) { + var ancestorRect = ancestor.getBoundingClientRect(); + if ( + videoRect.width > 0 && + videoRect.height > 0 && + (ancestorRect.width > Math.max(videoRect.width * 1.75, videoRect.width + 160) || + ancestorRect.height > Math.max(videoRect.height * 1.75, videoRect.height + 160)) + ) { + break; + } + if ( + (videoRect.width <= 0 || videoRect.height <= 0) && + depth > 1 + ) { + break; + } + try { + var controls = ancestor.querySelectorAll( + "button, [role='button'], input[type='range'], [role='slider']" + ); + for (var i = 0; i < Math.min(controls.length, 40); i += 1) { + var control = controls[i]; + var description = [ + control.getAttribute("aria-label"), + control.getAttribute("title"), + control.getAttribute("data-title-no-tooltip"), + control.className + ] + .filter(Boolean) + .join(" "); + if ( + /(^|\b)(play|pause|mute|volume|seek|captions?|subtitles?|fullscreen|full screen)(\b|$)/i.test( + description + ) + ) { + return true; + } + } + } catch (_error) {} + ancestor = ancestor.parentElement; + depth += 1; + } + + return false; +} + function isAmbientLoopMedia(node) { - return Boolean( + var hasAmbientSignature = Boolean( node && node.nodeName === "VIDEO" && node.autoplay === true && @@ -1374,6 +1442,8 @@ function isAmbientLoopMedia(node) { node.playsInline === true && node.controls !== true ); + if (!hasAmbientSignature || tc.settings.showAmbientLoopControls) return false; + return !hasInteractivePlayerChrome(node); } function hasUsableMediaSource(node) { @@ -1424,10 +1494,6 @@ function ensureController(node, parent) { return null; } if (!isMediaElement(node)) return null; - if (isAmbientLoopMedia(node)) { - if (node.vsc) removeController(node); - return null; - } if (typeof tc.videoController === "undefined") defineVideoController(); // href selects site rules; re-run on every new/usable media so all runtime @@ -1437,6 +1503,10 @@ function ensureController(node, parent) { if (node.vsc) removeController(node); return null; } + if (isAmbientLoopMedia(node)) { + if (node.vsc) removeController(node); + return null; + } if ( node.vsc && @@ -1913,6 +1983,8 @@ function hydrateRuntimeSettings(rawStorage, options) { tc.settings.rememberSpeed = storage.rememberSpeed === true; tc.settings.forceLastSavedSpeed = storage.forceLastSavedSpeed === true; tc.settings.audioBoolean = storage.audioBoolean === true; + tc.settings.showAmbientLoopControls = + storage.showAmbientLoopControls === true; tc.settings.enabled = storage.enabled !== false; tc.settings.startHidden = storage.startHidden === true; tc.settings.hideWithControls = @@ -3573,9 +3645,13 @@ function defineVideoController() { ) { wrapper.classList.add("ytp-autohide"); - // Immediately end any temporary "vsc-show" state to hide with YouTube - // UNLESS it was forced by a shortcut (vsc-forced-show) - if (!wrapper.classList.contains("vsc-forced-show")) { + // Preserve an active pointer reveal or shortcut-forced reveal. The + // ytp-autohide class remains in place, so CSS hides the host as soon as + // vsc-show's bounded timer expires. + if ( + !wrapper.classList.contains("vsc-forced-show") && + !wrapper.classList.contains("vsc-show") + ) { wrapper.classList.remove("vsc-show"); if (wrapper.showTimeOut) { clearTimeout(wrapper.showTimeOut); @@ -3984,6 +4060,7 @@ function applySiteRuleOverrides() { "rememberSpeed", "forceLastSavedSpeed", "audioBoolean", + "showAmbientLoopControls", "controllerOpacity", "controllerMarginTop", "controllerMarginBottom", @@ -4033,9 +4110,12 @@ function applySiteRuleOverrides() { } function removeIneligibleMediaControllers() { - if (tc.settings.audioBoolean) return; tc.mediaElements.slice().forEach(function(media) { - if (media && media.nodeName === "AUDIO") { + if ( + media && + ((media.nodeName === "AUDIO" && !tc.settings.audioBoolean) || + isAmbientLoopMedia(media)) + ) { removeController(media); } }); diff --git a/extension/options/options.html b/extension/options/options.html index 5619026..64ba0a9 100644 --- a/extension/options/options.html +++ b/extension/options/options.html @@ -282,6 +282,17 @@ +
+ + +

Playback

@@ -803,8 +814,8 @@