// ==UserScript==
// @name YouTube Embed Enhancer
// @namespace https://github.com/jmpatag
// @version 2.3.0
// @description Enhances YouTube Embeds with custom volume controls, hotkeys, and some optimizations.
// @author jmpatag
// @license GPL-3.0
// @match *://www.youtube.com/embed/*
// @match *://www.youtube-nocookie.com/embed/*
// @run-at document-idle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addValueChangeListener
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @updateURL https://raw.githubusercontent.com/jmpatag/YouTube-Embed-Enhancer/main/youtube-embed-enhancer.user.js
// @downloadURL https://raw.githubusercontent.com/jmpatag/YouTube-Embed-Enhancer/main/youtube-embed-enhancer.user.js
// ==/UserScript==
(() => {
"use strict";
const isControlsDisabled = new URLSearchParams(window.location.search).get("controls") === "0";
const isPlayButtonMissing = !document.querySelector(".ytp-play-button");
if (!isControlsDisabled && !isPlayButtonMissing) {
console.log('YTEE: Normal controls detected, enhancing anyway.');
}
document.head.appendChild(
Object.assign(document.createElement("style"), {
textContent: `
:root {
/* Styles */
--ytee-ew: 800px;
/* Sizing */
--ytee-btn-size: clamp(25.5px, calc(var(--ytee-ew) * 0.04), 37.5px);
--ytee-icon-size: clamp(15px, calc(var(--ytee-ew) * 0.0225), 21px);
--ytee-font-size: clamp(11px, calc(var(--ytee-ew) * 0.0175), 15px);
--ytee-gap: clamp(2.5px, calc(var(--ytee-ew) * 0.00375), 5px);
--ytee-bg-dark: rgba(15, 15, 15, 0.75);
--ytee-bg-medium: rgba(25, 25, 25, 0.65);
--ytee-btn-bg: rgba(255,255,255,0.08);
--ytee-btn-border: rgba(255,255,255,0.13);
--ytee-btn-hover: rgba(255,255,255,0.19);
--ytee-text: rgba(255,255,255,0.92);
--ytee-stats-color: #ffd700;
--ytee-stats-bg: rgba(255,215,0,0.14);
--ytee-stats-border: rgba(255,215,0,0.55);
--ytee-speed-color: #7ddeff;
--ytee-speed-bg: rgba(119,221,255,0.12);
--ytee-speed-border: rgba(119,221,255,0.55);
--ytee-rec-bg: rgba(255,68,68,0.15);
--ytee-rec-border: rgba(255,68,68,0.65);
--ytee-anim-fast: 0.08s cubic-bezier(0.4,0,0.2,1);
--ytee-anim-normal: 0.15s cubic-bezier(0.4,0,0.2,1);
--ytee-anim-slow: 0.25s cubic-bezier(0.4,0,0.2,1);
--ytee-radius: 5px;
}
[data-ytee-high-contrast="1"] {
--ytee-btn-bg: rgba(10, 10, 10, 0.82) !important;
--ytee-btn-border: rgba(255, 255, 255, 0.28) !important;
--ytee-btn-hover: rgba(30, 30, 30, 0.95) !important;
--ytee-text: #ffffff !important;
}
player-fullscreen-action-menu { display: none !important; }
/* Overlays */
#custom-vol-overlay,
#custom-speed-overlay {
position: fixed;
top: clamp(60px,10vh,140px);
left: 50%;
transform: translateX(-50%);
background: var(--ytee-bg-dark);
color: var(--ytee-text);
padding: clamp(5px,calc(var(--ytee-ew) * 0.008),10px) clamp(10px,calc(var(--ytee-ew) * 0.016),20px);
border-radius: 6px;
font-size: clamp(13px,calc(var(--ytee-ew) * 0.018),20px);
font-family: sans-serif;
font-weight: bold;
z-index: 9999;
opacity: 0;
pointer-events: none;
transition: opacity var(--ytee-anim-slow);
will-change: opacity;
}
#custom-vol-overlay.show,
#custom-speed-overlay.show { opacity: 1; transition: opacity var(--ytee-anim-fast); }
#custom-speed-overlay { top: clamp(95px,16vh,190px); }
#custom-clip-overlay {
position: fixed;
top: clamp(125px,22vh,240px);
left: 50%;
transform: translateX(-50%);
background: rgba(180,0,0,0.78);
color: white;
padding: clamp(4px,calc(var(--ytee-ew) * 0.005),7px) clamp(10px,calc(var(--ytee-ew) * 0.014),18px);
border-radius: 6px;
font-size: clamp(11px,calc(var(--ytee-ew) * 0.014),16px);
font-family: monospace;
font-weight: bold;
z-index: 9999;
opacity: 0;
pointer-events: none;
transition: opacity var(--ytee-anim-slow);
will-change: opacity;
}
#custom-clip-overlay.show { opacity: 1; transition: opacity var(--ytee-anim-fast); }
#custom-mini-stats {
position: fixed;
bottom: 45px;
left: 10px;
background: rgba(10, 10, 10, 0.7);
backdrop-filter: blur(4px);
color: rgba(255, 255, 255, 0.6);
padding: 4px 10px;
border-radius: 6px;
font-family: ui-monospace, 'Cascadia Code', monospace;
font-size: 10.5px;
z-index: 9999;
border: 1px solid rgba(255, 255, 255, 0.08);
opacity: 0;
pointer-events: none;
transition: opacity var(--ytee-anim-slow);
white-space: nowrap;
display: flex;
gap: 12px;
}
#custom-mini-stats.show { opacity: 1; }
#custom-mini-stats span { color: var(--ytee-speed-color); font-weight: bold; }
#custom-mini-stats b { color: rgba(255, 255, 255, 0.3); font-weight: normal; margin-right: 4px; }
/* Mute button */
#custom-mute-btn {
position: fixed;
bottom: clamp(6px,1vh,18px);
left: clamp(6px,calc(var(--ytee-ew) * 0.008),14px);
width: var(--ytee-btn-size);
height: var(--ytee-btn-size);
z-index: 9999;
cursor: pointer;
border: none;
background-color: transparent;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
background-image: url('data:image/svg+xml;utf8,');
opacity: 0;
pointer-events: none;
transition: opacity var(--ytee-anim-slow), transform var(--ytee-anim-fast);
will-change: opacity;
}
#custom-mute-btn.show { opacity: 1; pointer-events: auto; }
#custom-mute-btn.show:hover { transform: scale(1.1); }
#custom-mute-btn.muted {
background-image: url('data:image/svg+xml;utf8,');
opacity: 0.5 !important;
}
#custom-mute-btn.show.muted { opacity: 0.5; }
/* Volume slider */
#custom-vol-slider {
position: fixed;
bottom: calc(clamp(6px,1vh,18px) + var(--ytee-btn-size)/2 - 6px);
left: calc(clamp(6px,calc(var(--ytee-ew) * 0.008),14px) + var(--ytee-btn-size) + clamp(3px,calc(var(--ytee-ew) * 0.004),7px));
z-index: 9999;
width: clamp(52px,calc(var(--ytee-ew) * 0.072),90px);
height: 12px;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
background: transparent !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
outline: none;
opacity: 0;
pointer-events: none;
transition: opacity var(--ytee-anim-slow), width var(--ytee-anim-normal);
will-change: opacity;
}
#custom-vol-slider.show { opacity: 0.75; pointer-events: auto; }
#custom-vol-slider.show:hover { opacity: 1; width: clamp(68px,calc(var(--ytee-ew) * 0.092),110px); }
#custom-vol-slider::-webkit-slider-runnable-track { -webkit-appearance: none; height: 3px; border-radius: 2px; background: rgba(255,255,255,0.35); border: none; }
#custom-vol-slider::-moz-range-track { height: 3px; border-radius: 2px; background: rgba(255,255,255,0.35); border: none; }
#custom-vol-slider::-webkit-slider-thumb {
width: clamp(10px,calc(var(--ytee-ew) * 0.013),14px); height: clamp(10px,calc(var(--ytee-ew) * 0.013),14px);
margin-top: calc(-1*(clamp(10px,calc(var(--ytee-ew) * 0.013),14px)/2 - 1.5px));
appearance: none; border-radius: 50%; background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
#custom-vol-slider::-moz-range-thumb {
width: clamp(10px,calc(var(--ytee-ew) * 0.013),14px); height: clamp(10px,calc(var(--ytee-ew) * 0.013),14px);
border-radius: 50%; background: white; border: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
/* Button group */
#custom-btn-group {
position: fixed;
bottom: clamp(6px,1vh,18px);
right: clamp(6px,calc(var(--ytee-ew) * 0.008),14px);
display: flex;
align-items: center;
gap: var(--ytee-gap);
z-index: 9999;
opacity: 0;
pointer-events: none;
transition: opacity var(--ytee-anim-slow);
will-change: opacity;
}
#custom-btn-group.show { opacity: 1; pointer-events: auto; }
#custom-btn-group.collapsed #custom-wl-btn,
#custom-btn-group.collapsed #custom-url-btn,
#custom-btn-group.collapsed #custom-screenshot-btn,
#custom-btn-group.collapsed #custom-clip-btn,
#custom-btn-group.collapsed #custom-pip-btn,
#custom-btn-group.collapsed #custom-speed-btn,
#custom-btn-group.collapsed #custom-stats-btn {
display: none !important;
}
#custom-btn-group.collapsed { gap: 4px; }
/* Shared button base */
#custom-wl-btn,
#custom-url-btn,
#custom-screenshot-btn,
#custom-clip-btn,
#custom-pip-btn,
#custom-speed-btn,
#custom-stats-btn,
#custom-toggle-btn,
#custom-settings-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
cursor: pointer;
color: var(--ytee-text);
background: var(--ytee-btn-bg) !important;
border: 1px solid var(--ytee-btn-border) !important;
border-radius: var(--ytee-radius) !important;
font-size: var(--ytee-font-size);
font-family: ui-monospace, 'Cascadia Code', monospace;
font-weight: 700;
letter-spacing: 0.03em;
line-height: 1;
text-shadow: 0 1px 2px rgba(0,0,0,0.6);
opacity: 0;
pointer-events: none;
position: relative;
overflow: visible;
white-space: nowrap;
transition: opacity var(--ytee-anim-slow), background var(--ytee-anim-fast), transform var(--ytee-anim-fast);
}
#custom-btn-group.show #custom-wl-btn,
#custom-btn-group.show #custom-url-btn,
#custom-btn-group.show #custom-screenshot-btn,
#custom-btn-group.show #custom-clip-btn,
#custom-btn-group.show #custom-pip-btn,
#custom-btn-group.show #custom-speed-btn,
#custom-btn-group.show #custom-stats-btn,
#custom-toggle-btn,
#custom-settings-btn {
opacity: 1; pointer-events: auto;
}
#custom-btn-group.show #custom-wl-btn:hover,
#custom-btn-group.show #custom-url-btn:hover,
#custom-btn-group.show #custom-screenshot-btn:hover,
#custom-btn-group.show #custom-clip-btn:hover,
#custom-btn-group.show #custom-pip-btn:hover,
#custom-btn-group.show #custom-speed-btn:hover,
#custom-btn-group.show #custom-stats-btn:hover,
#custom-btn-group.show #custom-settings-btn:hover {
background: var(--ytee-btn-hover) !important;
transform: scale(1.08);
z-index: 10;
}
/* Icon mode */
#custom-wl-btn,
#custom-url-btn,
#custom-screenshot-btn,
#custom-clip-btn,
#custom-pip-btn,
#custom-stats-btn,
#custom-toggle-btn,
#custom-settings-btn {
width: var(--ytee-btn-size);
height: var(--ytee-btn-size);
padding: 0;
}
/* Speed keeps auto width for the rate text */
#custom-speed-btn {
height: var(--ytee-btn-size);
min-width: var(--ytee-btn-size);
padding: 0 clamp(3px,calc(var(--ytee-ew) * 0.004),7px);
}
/* SVG icons inside each button */
#custom-wl-btn .ytee-icon,
#custom-url-btn .ytee-icon,
#custom-screenshot-btn .ytee-icon,
#custom-clip-btn .ytee-icon,
#custom-pip-btn .ytee-icon,
#custom-stats-btn .ytee-icon,
#custom-toggle-btn .ytee-icon,
#custom-settings-btn .ytee-icon,
#custom-speed-btn .ytee-icon { display: flex; }
/* Label spans — hidden in icon mode */
#custom-wl-btn .ytee-label,
#custom-url-btn .ytee-label,
#custom-screenshot-btn .ytee-label,
#custom-clip-btn .ytee-label,
#custom-pip-btn .ytee-label,
#custom-speed-btn .ytee-label,
#custom-stats-btn .ytee-label,
#custom-toggle-btn .ytee-label,
#custom-settings-btn .ytee-label { display: none; }
/* Tooltips — shown in icon mode only */
#custom-wl-btn::after,
#custom-url-btn::after,
#custom-screenshot-btn::after,
#custom-clip-btn::after,
#custom-pip-btn::after,
#custom-speed-btn::after,
#custom-stats-btn::after,
#custom-toggle-btn::after,
#custom-settings-btn::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 7.5px);
right: 0;
background: rgba(10,10,10,0.92);
color: rgba(255,255,255,0.92);
font-size: 12.5px;
font-family: system-ui, sans-serif;
font-weight: 500;
white-space: nowrap;
padding: 3.75px 10px;
border-radius: 5px;
border: 1.25px solid rgba(255,255,255,0.1);
pointer-events: none;
opacity: 0;
transition: opacity 0.12s;
letter-spacing: 0;
}
#custom-wl-btn:hover::after,
#custom-url-btn:hover::after,
#custom-screenshot-btn:hover::after,
#custom-clip-btn:hover::after,
#custom-pip-btn:hover::after,
#custom-speed-btn:hover::after,
#custom-stats-btn:hover::after,
#custom-settings-btn:hover::after { opacity: 1; }
/* Label mode */
[data-ytee-labels="1"] #custom-wl-btn,
[data-ytee-labels="1"] #custom-url-btn,
[data-ytee-labels="1"] #custom-screenshot-btn,
[data-ytee-labels="1"] #custom-clip-btn,
[data-ytee-labels="1"] #custom-pip-btn,
[data-ytee-labels="1"] #custom-stats-btn {
width: auto;
padding: 0 clamp(6px,calc(var(--ytee-ew) * 0.009),13px);
}
[data-ytee-labels="1"] #custom-speed-btn {
padding: 0 clamp(6px,calc(var(--ytee-ew) * 0.009),13px);
}
[data-ytee-labels="1"] #custom-wl-btn .ytee-label,
[data-ytee-labels="1"] #custom-url-btn .ytee-label,
[data-ytee-labels="1"] #custom-screenshot-btn .ytee-label,
[data-ytee-labels="1"] #custom-clip-btn .ytee-label,
[data-ytee-labels="1"] #custom-pip-btn .ytee-label,
[data-ytee-labels="1"] #custom-speed-btn .ytee-label,
[data-ytee-labels="1"] #custom-stats-btn .ytee-label { display: inline; }
/* In label mode, always show icons inside buttons (icon+label together) */
[data-ytee-labels="1"] #custom-wl-btn .ytee-icon,
[data-ytee-labels="1"] #custom-url-btn .ytee-icon,
[data-ytee-labels="1"] #custom-screenshot-btn .ytee-icon,
[data-ytee-labels="1"] #custom-clip-btn .ytee-icon,
[data-ytee-labels="1"] #custom-pip-btn .ytee-icon,
[data-ytee-labels="1"] #custom-speed-btn .ytee-icon,
[data-ytee-labels="1"] #custom-stats-btn .ytee-icon { display: flex; }
/* State colors */
#custom-stats-btn.active {
color: var(--ytee-stats-color) !important;
background: var(--ytee-stats-bg) !important;
border-color: var(--ytee-stats-border) !important;
}
#custom-stats-btn.active svg { fill: var(--ytee-stats-color); }
#custom-speed-btn.modified {
color: var(--ytee-speed-color) !important;
background: var(--ytee-speed-bg) !important;
border-color: var(--ytee-speed-border) !important;
}
#custom-speed-btn.modified svg { fill: var(--ytee-speed-color); }
@keyframes ytee-rec-pulse {
0%,100% { box-shadow: 0 0 0 0 rgba(255,60,60,0.7); }
50% { box-shadow: 0 0 0 5px rgba(255,60,60,0); }
}
#custom-clip-btn.recording {
color: #ff4444 !important;
background: var(--ytee-rec-bg) !important;
border-color: var(--ytee-rec-border) !important;
animation: ytee-rec-pulse 1.1s ease-in-out infinite;
}
#custom-clip-btn.recording svg { fill: #ff4444; }
/* Feedback States */
@keyframes ytee-btn-bounce {
0%, 100% { transform: scale(1); }
40% { transform: scale(1.25); }
60% { transform: scale(0.95); }
}
.ytee-btn.success {
color: #2ecc71 !important;
background: rgba(46, 204, 113, 0.2) !important;
border-color: rgba(46, 204, 113, 0.6) !important;
animation: ytee-btn-bounce 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 20;
}
.ytee-btn.success svg { fill: #2ecc71; }
.ytee-btn.error {
color: #ff4444 !important;
background: rgba(255, 68, 68, 0.2) !important;
border-color: rgba(255, 68, 68, 0.6) !important;
animation: ytee-btn-bounce 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 20;
}
.ytee-btn.error svg { fill: #ff4444; }
/* Settings Modal */
@keyframes ytee-modal-in {
from { opacity:0; transform: scale(0.96) translateY(8px); }
to { opacity:1; transform: scale(1) translateY(0); }
}
#custom-settings-modal {
position: fixed; inset: 0;
background: rgba(0,0,0,0.78);
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
z-index: 10000; display: none;
align-items: center; justify-content: center;
}
#custom-settings-modal.show { display: flex; }
#custom-settings-content {
background: linear-gradient(160deg,rgba(22,22,28,0.97) 0%,rgba(14,14,18,0.97) 100%);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 14px;
padding: 16px 18px 14px;
width: min(480px,96vw);
max-height: 90vh;
overflow-y: auto; overflow-x: hidden;
color: white;
font-family: system-ui,-apple-system,sans-serif;
box-shadow: 0 24px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04) inset;
animation: ytee-modal-in 0.2s cubic-bezier(0.34,1.56,0.64,1) both;
scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.15) transparent;
}
#custom-settings-content::-webkit-scrollbar { width: 4px; }
#custom-settings-content::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.15); border-radius:99px; }
#custom-settings-content h2 { margin:0 0 2px; font-size:15px; font-weight:700; letter-spacing:-0.01em; color:rgba(255,255,255,0.95); }
#ytee-settings-subtitle { font-size:11px; color:rgba(255,255,255,0.35); margin:0 0 12px; letter-spacing:0.01em; }
#custom-settings-content h3.setting-section-title {
display:flex; align-items:center; gap:8px;
margin:14px 0 2px; font-size:10px; font-weight:700;
color:rgba(255,255,255,0.38); text-transform:uppercase; letter-spacing:0.15em;
}
#custom-settings-content h3.setting-section-title::before {
content:''; display:block; width:3px; height:14px; border-radius:99px; flex-shrink:0;
background:linear-gradient(to bottom,rgba(119,221,255,0.9),rgba(119,221,255,0.2));
}
#custom-settings-content h3.setting-section-title::after { content:''; flex:1; height:1px; background:rgba(255,255,255,0.06); }
#custom-settings-content .setting-item {
display:flex; align-items:center; justify-content:space-between; gap:10px;
padding:7px 10px; margin:2px 0; border-radius:8px; border:1px solid transparent;
transition: background 0.15s, border-color 0.15s;
}
#custom-settings-content .setting-item:hover { background:rgba(255,255,255,0.04); border-color:rgba(255,255,255,0.07); }
#custom-settings-content .setting-item label:not(.ytee-toggle-track) {
flex:1; font-size:13.5px; font-weight:500; color:rgba(255,255,255,0.85); cursor:pointer; user-select:none;
}
.ytee-toggle-wrap { position:relative; width:38px; height:22px; flex:0 0 38px; flex-shrink:0; }
.ytee-toggle-wrap input[type="checkbox"] { opacity:0; width:0; height:0; position:absolute; }
.ytee-toggle-track {
position:absolute; inset:0; border-radius:99px;
background:rgba(255,255,255,0.12); border:1px solid rgba(255,255,255,0.12);
transition: background 0.2s, border-color 0.2s; cursor:pointer;
}
.ytee-toggle-thumb {
position:absolute; top:3px; left:3px; width:14px; height:14px; border-radius:50%;
background:rgba(255,255,255,0.5);
transition: transform 0.2s cubic-bezier(0.34,1.56,0.64,1), background 0.2s;
pointer-events:none;
}
.ytee-toggle-wrap input:checked ~ .ytee-toggle-track { background:rgba(119,221,255,0.25); border-color:rgba(119,221,255,0.6); }
.ytee-toggle-wrap input:checked ~ .ytee-toggle-track .ytee-toggle-thumb { transform:translateX(16px); background:#7ddeff; }
#custom-settings-content .hk-input {
width:108px; padding:7px 11px;
background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1);
border-radius:8px; color:rgba(255,255,255,0.9);
font-size:12px; font-family:ui-monospace,'Cascadia Code',monospace;
font-weight:600; letter-spacing:0.04em; text-transform:lowercase;
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s; flex-shrink:0;
}
#custom-settings-content .hk-input:focus { outline:none; background:rgba(119,221,255,0.06); border-color:rgba(119,221,255,0.5); box-shadow:0 0 0 3px rgba(119,221,255,0.1); }
#custom-settings-content input[type="range"] { -webkit-appearance:none; appearance:none; height:4px; border-radius:99px; background:rgba(255,255,255,0.12); outline:none; cursor:pointer; flex-shrink:0; }
#custom-settings-content input[type="range"]::-webkit-slider-thumb { -webkit-appearance:none; width:16px; height:16px; border-radius:50%; background:#fff; box-shadow:0 1px 4px rgba(0,0,0,0.4); transition:transform 0.12s,box-shadow 0.12s; }
#custom-settings-content input[type="range"]:hover::-webkit-slider-thumb { transform:scale(1.15); box-shadow:0 2px 8px rgba(0,0,0,0.5); }
#custom-settings-content input[type="range"]::-moz-range-thumb { width:16px; height:16px; border-radius:50%; background:#fff; border:none; box-shadow:0 1px 4px rgba(0,0,0,0.4); }
#custom-settings-content input[type="range"]::-moz-range-track { height:4px; border-radius:99px; background:rgba(255,255,255,0.12); }
.ytee-slider-value { font-size:12px; font-weight:700; font-family:ui-monospace,monospace; color:rgba(255,255,255,0.5); min-width:40px; text-align:right; flex-shrink:0; }
.setting-note { display:block; font-size:11px; color:rgba(255,255,255,0.3); margin:4px 14px 12px; line-height:1.4; font-style:italic; }
#custom-settings-buttons { display:flex; align-items:center; justify-content:flex-end; gap:8px; margin-top:14px; padding-top:10px; border-top:1px solid rgba(255,255,255,0.07); }
#custom-settings-restore { margin-right:auto; padding:8px 14px; background:transparent; border:1px solid rgba(255,80,80,0.3); border-radius:9px; color:rgba(255,110,110,0.8); cursor:pointer; font-size:12.5px; font-weight:600; transition:background 0.15s,border-color 0.15s,color 0.15s; }
#custom-settings-restore:hover { background:rgba(255,60,60,0.1); border-color:rgba(255,80,80,0.6); color:#ff8080; }
#custom-settings-cancel { padding:8px 16px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:9px; color:rgba(255,255,255,0.6); cursor:pointer; font-size:13px; font-weight:600; transition:background 0.15s,color 0.15s; }
#custom-settings-cancel:hover { background:rgba(255,255,255,0.1); color:rgba(255,255,255,0.9); }
#custom-settings-save { padding:8px 20px; background:rgba(119,221,255,0.15); border:1px solid rgba(119,221,255,0.45); border-radius:9px; color:#7ddeff; cursor:pointer; font-size:13px; font-weight:700; letter-spacing:0.01em; transition:background 0.15s,border-color 0.15s,box-shadow 0.15s; }
#custom-settings-save:hover { background:rgba(119,221,255,0.25); border-color:rgba(119,221,255,0.7); box-shadow:0 0 14px rgba(119,221,255,0.15); }
.ytee-quality-select {
padding:7px 11px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1);
border-radius:8px; color:rgba(255,255,255,0.9); font-size:12px;
font-family:ui-monospace,'Cascadia Code',monospace; font-weight:600; letter-spacing:0.04em;
cursor:pointer; flex-shrink:0; transition:border-color 0.15s,box-shadow 0.15s,background 0.15s;
appearance:none; -webkit-appearance:none;
background-image:url("data:image/svg+xml;utf8,");
background-repeat:no-repeat; background-position:right 10px center; padding-right:28px;
}
.ytee-quality-select:focus { outline:none; background-color:rgba(119,221,255,0.06); border-color:rgba(119,221,255,0.5); box-shadow:0 0 0 3px rgba(119,221,255,0.1); }
.ytee-quality-select option { background:#1a1a22; color:white; }
.ytee-quality-note { display:block; font-size:11px; color:rgba(255,255,255,0.3); margin:4px 14px 12px; line-height:1.4; font-style:italic; }
/* Info Button and Box */
.ytee-info-btn {
display: flex; align-items: center; justify-content: center;
width: 18px; height: 18px; border-radius: 50%;
background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.12);
color: rgba(255,255,255,0.4); cursor: pointer; transition: all 0.15s;
flex-shrink: 0;
}
.ytee-info-btn:hover { background: rgba(119,221,255,0.12); border-color: rgba(119,221,255,0.35); color: #7ddeff; }
.ytee-info-box {
margin: 4px 10px 14px; padding: 12px 14px;
background: rgba(15, 15, 20, 0.45); border: 1px solid rgba(255,255,255,0.06);
border-radius: 10px; font-size: 11.5px; line-height: 1.6; color: rgba(255,255,255,0.55);
display: none; animation: ytee-modal-in 0.2s ease-out;
}
.ytee-info-box.show { display: block; }
.ytee-info-box strong { color: rgba(119,221,255,0.9); font-weight: 700; margin-right: 4px; font-family: ui-monospace, monospace; }
.ytee-info-box p { margin: 0 0 10px; }
.ytee-info-box p:last-child { margin-bottom: 4px; }
.ytee-info-link { display: inline-block; color: #7ddeff; text-decoration: none; font-size: 10px; opacity: 0.5; transition: opacity 0.2s; margin-top: 4px; }
.ytee-info-link:hover { opacity: 0.9; text-decoration: underline; }
#ytee-settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2px;
}
`
})
);
const SVG_NS = 'http://www.w3.org/2000/svg';
const mkSvgEl = (...pathDefs) => {
const svg = document.createElementNS(SVG_NS, 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'currentColor');
svg.style.cssText = 'width:var(--ytee-icon-size,13px);height:var(--ytee-icon-size,13px);flex-shrink:0;';
pathDefs.forEach(def => {
const el = document.createElementNS(SVG_NS, def.tag || 'path');
Object.entries(def.attrs).forEach(([k, v]) => el.setAttribute(k, v));
svg.appendChild(el);
});
return svg;
};
const ICON_DEFS = {
wl: () => mkSvgEl({ tag: 'path', attrs: { d: 'M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z' } }),
url: () => mkSvgEl({ tag: 'path', attrs: { d: 'M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z' } }),
screenshot: () => mkSvgEl({ tag: 'path', attrs: { d: 'M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z' } }),
clip: () => mkSvgEl({ tag: 'circle', attrs: { cx: '12', cy: '12', r: '7' } }),
pip: () => mkSvgEl(
{ tag: 'path', attrs: { d: 'M19 7H5c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zm-9 5v-1.5l4 2-4 2V12z' } },
{ tag: 'path', attrs: { d: 'M23 5h-2v14h2V5zM1 5v14h2V5H1z' } }
),
stats: () => mkSvgEl({ tag: 'path', attrs: { d: 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 3c1.93 0 3.5 1.57 3.5 3.5S13.93 13 12 13s-3.5-1.57-3.5-3.5S10.07 6 12 12 6zm7 13H5v-.23c0-.62.28-1.2.76-1.58C7.47 15.82 9.64 15 12 15s4.53.82 6.24 2.19c.48.38.76.97.76 1.58V19z' } }),
settings: () => mkSvgEl({ tag: 'path', attrs: { d: 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z' } }),
speed: () => mkSvgEl({ tag: 'path', attrs: { d: 'M10 8v8l6-4-6-4zm6.5-4.5l-1.5 1.5C16.78 6.76 18 9.24 18 12s-1.22 5.24-3 6.99l1.5 1.5C18.77 18.12 20 15.2 20 12s-1.23-6.12-3.5-8.5zM7.5 5.5L6 4C3.23 6.38 2 9.3 2 12s1.23 5.62 4 8l1.5-1.5C5.22 16.76 4 14.29 4 12s1.22-5.24 3.5-6.5z' } }),
hide: () => mkSvgEl({ tag: 'path', attrs: { d: 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z' } }),
expand: () => mkSvgEl({ tag: 'path', attrs: { d: 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z' } }),
info: () => mkSvgEl({ tag: 'path', attrs: { d: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z' } }),
};
const mkBtn = (id, iconKey, labelText, tipText, titleText) => {
const btn = document.createElement('button');
btn.id = id;
btn.className = 'ytee-btn';
btn.title = titleText || labelText;
btn.dataset.tip = tipText || labelText;
btn.setAttribute('aria-label', labelText);
const iconSpan = document.createElement('span');
iconSpan.className = 'ytee-icon';
iconSpan.appendChild(ICON_DEFS[iconKey]());
const labelSpan = document.createElement('span');
labelSpan.className = 'ytee-label';
labelSpan.textContent = labelText;
btn.appendChild(iconSpan);
btn.appendChild(labelSpan);
return btn;
};
const waitForVideo = (callback, timeoutMs = 15000) => {
const existing = document.querySelector("video");
if (existing) { callback(existing); return; }
const observer = new MutationObserver(() => {
const v = document.querySelector("video");
if (v) { observer.disconnect(); clearTimeout(timer); callback(v); }
});
const timer = setTimeout(() => { observer.disconnect(); console.warn('YTEE: video element never appeared'); }, timeoutMs);
observer.observe(document.documentElement, { childList: true, subtree: true });
};
// Settings
const defaultSettings = {
buttons: { wl: true, url: true, screenshot: true, clip: true, pip: true, speed: true, stats: true, vol: true },
hotkeys: {
toggleMute: 'm',
toggleStats: 'shift+s',
increaseSpeed: '.',
decreaseSpeed: ',',
increaseSpeedFine: 'shift+.',
decreaseSpeedFine: 'shift+,',
volumeUp: 'arrowup',
volumeDown: 'arrowdown',
},
volumeBoostLevel: 1,
enableVolumeBoost: true,
clipDuration: 5,
clipDurationCtrl: 300,
preferredQuality: 'auto',
compactMode: false,
isCollapsed: false,
highContrastUI: false,
playbackSpeed: 1,
volumeCache: {},
};
const loadStoredSettings = () => {
try {
if (typeof GM_getValue === 'function') {
const v = GM_getValue('ytee-settings', null);
if (v) return typeof v === 'string' ? JSON.parse(v) : v;
}
} catch (e) { console.warn('GM_getValue failed', e); }
try { const r = localStorage.getItem('ytee-settings'); if (r) return JSON.parse(r); } catch (e) { }
return null;
};
const saveStoredSettings = (s) => {
try { if (typeof GM_setValue === 'function') GM_setValue('ytee-settings', JSON.stringify(s)); } catch (e) { }
try { localStorage.setItem('ytee-settings', JSON.stringify(s)); } catch (e) { }
};
let currentSettings = loadStoredSettings() || defaultSettings;
const normalizeSettings = (s) => {
if (!s || typeof s !== 'object') return JSON.parse(JSON.stringify(defaultSettings));
const rs = {
buttons: Object.assign({}, defaultSettings.buttons, s.buttons),
hotkeys: Object.assign({}, defaultSettings.hotkeys, s.hotkeys),
volumeBoostLevel: typeof s.volumeBoostLevel === 'number' ? s.volumeBoostLevel
: s.volumeBoost === true ? 1.5 : defaultSettings.volumeBoostLevel,
enableVolumeBoost: typeof s.enableVolumeBoost === 'boolean' ? s.enableVolumeBoost : defaultSettings.enableVolumeBoost,
clipDuration: typeof s.clipDuration === 'number' ? Math.min(300, Math.max(1, s.clipDuration)) : defaultSettings.clipDuration,
clipDurationCtrl: typeof s.clipDurationCtrl === 'number' ? Math.min(300, Math.max(1, s.clipDurationCtrl)) : defaultSettings.clipDurationCtrl,
preferredQuality: typeof s.preferredQuality === 'string' ? s.preferredQuality : defaultSettings.preferredQuality,
compactMode: typeof s.compactMode === 'boolean' ? s.compactMode : (typeof s.labelMode === 'boolean' ? !s.labelMode : defaultSettings.compactMode),
isCollapsed: typeof s.isCollapsed === 'boolean' ? s.isCollapsed : defaultSettings.isCollapsed,
highContrastUI: typeof s.highContrastUI === 'boolean' ? s.highContrastUI : defaultSettings.highContrastUI,
playbackSpeed: typeof s.playbackSpeed === 'number' ? Math.min(16, Math.max(0.1, s.playbackSpeed)) : defaultSettings.playbackSpeed,
volumeCache: typeof s.volumeCache === 'object' ? s.volumeCache : defaultSettings.volumeCache,
};
// memory remembers 10
const keys = Object.keys(rs.volumeCache);
if (keys.length > 10) {
keys.slice(0, keys.length - 10).forEach(k => delete rs.volumeCache[k]);
}
return rs;
};
const applyUIStates = (settings) => {
document.documentElement.dataset.yteeLabels = settings.compactMode ? '0' : '1';
document.documentElement.dataset.yteeHighContrast = settings.highContrastUI ? '1' : '0';
const group = document.getElementById('custom-btn-group');
if (group) {
group.classList.toggle('collapsed', !!settings.isCollapsed);
const tBtn = document.getElementById('custom-toggle-btn');
if (tBtn) {
const iconSpan = tBtn.querySelector('.ytee-icon');
if (iconSpan) {
while (iconSpan.firstChild) iconSpan.removeChild(iconSpan.firstChild);
iconSpan.appendChild(ICON_DEFS[settings.isCollapsed ? 'expand' : 'hide']());
}
setBtnLabel(tBtn, settings.isCollapsed ? 'Expand' : 'Hide');
}
}
};
currentSettings = normalizeSettings(currentSettings);
applyUIStates(currentSettings);
const getVideoAuthor = (player) => {
if (player && typeof player.getVideoData === 'function') {
const data = player.getVideoData();
if (data?.author) return data.author.replace(/[<>:"/\\|?*\x00-\x1F]/g, '').trim();
}
return 'YouTube';
};
const formatTimestamp = (currentTime) => {
const timeMs = Math.floor(currentTime * 1000);
const mins = Math.floor(timeMs / 60000).toString().padStart(2, '0');
const secs = Math.floor((timeMs % 60000) / 1000).toString().padStart(2, '0');
const ms = (timeMs % 1000).toString().padStart(3, '0');
return `${mins}-${secs}-${ms}`;
};
const setBtnLabel = (btn, text, tipText) => {
const label = btn.querySelector('.ytee-label');
if (label) label.textContent = text;
btn.dataset.tip = tipText ?? text;
};
const flashBtnState = (btn, state, duration = 1500) => {
btn.classList.add(state);
setTimeout(() => btn.classList.remove(state), duration);
};
// Main
waitForVideo((video) => {
let targetVolume = video.volume;
let targetMuted = video.muted;
const uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
let cachedPlayer = null;
const getPlayer = () => {
if (cachedPlayer && cachedPlayer.isConnected) return cachedPlayer;
cachedPlayer = uw.document.getElementById("movie_player") || uw.document.querySelector(".html5-video-player");
return cachedPlayer;
};
const QUALITY_LABELS = {
auto: 'Auto (YouTube decides)', hd2160: '4K (2160p)', hd1440: '1440p',
hd1080: '1080p', hd720: '720p', large: '480p', medium: '360p', small: '240p', tiny: '144p',
};
const QUALITY_ORDER = ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny'];
const applyQuality = () => {
const pref = currentSettings.preferredQuality;
if (!pref || pref === 'auto') return;
const p = getPlayer();
if (!p) return;
try {
if (typeof p.setPlaybackQualityRange === 'function') p.setPlaybackQualityRange(pref, pref);
if (typeof p.setPlaybackQuality === 'function') p.setPlaybackQuality(pref);
} catch (e) { console.warn('YTEE: applyQuality failed', e); }
};
const hookPlayerQuality = () => {
const p = getPlayer();
if (!p || typeof p.addEventListener !== 'function') return;
p.addEventListener('onStateChange', (state) => { if (state === 1 || state === 3) applyQuality(); });
};
let qualityHookAttempts = 0;
const tryHookQuality = () => {
const p = getPlayer();
if (p && typeof p.addEventListener === 'function') { hookPlayerQuality(); applyQuality(); }
else if (qualityHookAttempts < 20) { qualityHookAttempts++; setTimeout(tryHookQuality, 500); }
};
tryHookQuality();
let scriptChangeDepth = 0;
let audioContext = null, gainNode = null, mediaSource = null, virtualMuted = video.muted, audioSetupFailed = false;
let activeStream = null, rafPending = 0;
let volTimeout, speedTimeout, controlsTimeout, clipRafId = null;
const getBoostLevel = () => currentSettings.enableVolumeBoost ? Math.max(1, Number(currentSettings.volumeBoostLevel) || 1) : 1;
// Volume
const setupWebAudio = () => {
if (gainNode || audioSetupFailed || !(window.AudioContext || window.webkitAudioContext)) return;
try {
const AudioCtor = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioCtor();
mediaSource = audioContext.createMediaElementSource(video);
gainNode = audioContext.createGain();
mediaSource.connect(gainNode);
gainNode.connect(audioContext.destination);
video.volume = 1;
if (audioContext.state === 'suspended') audioContext.resume().catch(e => console.warn('AudioContext resume failed', e));
} catch (e) {
console.warn('Web Audio setup failed', e);
audioContext = null; gainNode = null; mediaSource = null; audioSetupFailed = true;
}
};
const setGain = (linearVolume) => {
if (!gainNode || !audioContext) return;
if (audioContext.state === 'suspended') audioContext.resume().catch(e => console.warn('AudioContext resume failed', e));
gainNode.gain.setTargetAtTime(linearVolume * getBoostLevel(), audioContext.currentTime, 0.01);
};
const applyAudioState = (volume, muted) => {
scriptChangeDepth++;
try {
targetVolume = Math.min(1, Math.max(0, volume));
targetMuted = muted;
virtualMuted = muted;
if (currentSettings.enableVolumeBoost && !gainNode && getBoostLevel() > 1) setupWebAudio();
if (currentSettings.enableVolumeBoost && gainNode) {
setGain(targetMuted ? 0 : targetVolume);
} else {
const p = getPlayer();
if (targetMuted) { if (p && typeof p.mute === 'function') p.mute(); else video.muted = true; }
else {
if (p && typeof p.unMute === 'function') p.unMute(); else video.muted = false;
if (p && typeof p.setVolume === 'function') p.setVolume(Math.round(targetVolume * 100)); else video.volume = targetVolume;
}
}
} finally { scriptChangeDepth--; }
};
const applyVolume = (newVol) => {
const clamped = Math.min(1, Math.max(0, Math.round(newVol * 100) / 100));
applyAudioState(clamped, clamped === 0);
vol.value = clamped;
showVolumePercent(clamped === 0 ? 0 : clamped);
//memory
const p = getPlayer();
const vid = p?.getVideoData?.().video_id;
if (vid) {
currentSettings.volumeCache[vid] = clamped;
saveStoredSettings(currentSettings);
}
};
const toggleMute = () => {
const newMuted = !targetMuted;
applyAudioState(targetVolume, newMuted);
muteBtn.classList.toggle("muted", newMuted);
showVolumePercent(newMuted ? 0 : targetVolume);
};
// Overlays
const volPct = Object.assign(document.createElement("div"), { id: "custom-vol-overlay" });
const showVolumePercent = (volume) => {
const text = Math.round(volume * 100) + "%";
if (volPct.textContent !== text) volPct.textContent = text;
volPct.classList.add("show");
clearTimeout(volTimeout);
volTimeout = setTimeout(() => volPct.classList.remove("show"), 1500);
};
const speedOverlay = Object.assign(document.createElement("div"), { id: "custom-speed-overlay" });
const showSpeedOverlay = (rate) => {
const text = rate + "x";
if (speedOverlay.textContent !== text) speedOverlay.textContent = text;
speedOverlay.classList.add("show");
clearTimeout(speedTimeout);
speedTimeout = setTimeout(() => speedOverlay.classList.remove("show"), 1500);
};
// Mute button
const muteBtn = Object.assign(document.createElement("button"), { id: "custom-mute-btn" });
muteBtn.addEventListener("click", toggleMute);
// Volume slider
const vol = Object.assign(document.createElement("input"), {
id: "custom-vol-slider", type: "range", min: 0, max: 1, step: 0.01, value: video.volume,
});
vol.addEventListener("input", () => applyVolume(Number(vol.value)));
video.addEventListener("volumechange", () => {
if (scriptChangeDepth > 0) return;
if (gainNode) { muteBtn.classList.toggle("muted", video.muted); return; }
targetMuted = video.muted;
targetVolume = video.muted ? targetVolume : video.volume;
muteBtn.classList.toggle("muted", targetMuted);
const displayVol = targetMuted ? 0 : targetVolume;
if (Number(vol.value) !== displayVol) vol.value = displayVol;
});
let isHoveringSpeedBtn = false;
let pendingWheelDelta = 0, wheelRafId = 0;
window.addEventListener("wheel", (e) => {
if (isSettingsOpen) {
if (settingsContent.contains(e.target)) return;
e.stopImmediatePropagation(); e.preventDefault(); return;
}
e.preventDefault();
pendingWheelDelta += e.deltaY;
if (wheelRafId) return;
wheelRafId = requestAnimationFrame(() => {
const delta = pendingWheelDelta;
pendingWheelDelta = 0;
wheelRafId = 0;
if (isHoveringSpeedBtn) {
applySpeed(targetSpeed + (delta > 0 ? -SPEED_STEP : SPEED_STEP));
} else {
applyVolume(targetVolume + (delta > 0 ? -0.05 : 0.05));
}
});
}, { passive: false });
// Stats
let isStatsOpen = false, isMiniStatsOpen = false, miniStatsTimer = null;
const miniStats = Object.assign(document.createElement("div"), { id: "custom-mini-stats" });
const statsBtn = mkBtn('custom-stats-btn', 'stats', 'Stats', 'Stats (Ctrl = Mini)', 'Stats for Nerds (Shift+S)');
const miniStatsParts = {};
(() => {
const mkPart = (key, label) => {
const wrap = document.createElement('div');
const b = document.createElement('b'); b.textContent = label;
const s = document.createElement('span'); s.textContent = '–';
wrap.append(b, s);
miniStats.appendChild(wrap);
miniStatsParts[key] = s;
};
mkPart('buffer', 'Buffer');
mkPart('latency', 'Latency');
mkPart('dropped', 'Drop');
})();
const updateMiniStats = () => {
if (!isMiniStatsOpen) return;
const p = getPlayer();
if (!p) return;
const stats = (typeof p.getStatsForNerdsData === 'function') ? p.getStatsForNerdsData() : null;
const quality = (typeof video.getVideoPlaybackQuality === 'function') ? video.getVideoPlaybackQuality() : null;
const buffer = stats ? stats.buffer_health : (video.buffered.length > 0 ? (video.buffered.end(video.buffered.length - 1) - video.currentTime).toFixed(2) : '0.00');
let latency = null;
if (stats) {
if (typeof stats.latency === 'number') latency = stats.latency;
else if (typeof stats.latency_ms === 'number') latency = stats.latency_ms / 1000;
else if (typeof stats.live_latency === 'number') latency = stats.live_latency;
}
if (latency == null && p.getVideoData?.().isLive) {
const liveEdge = p.getDuration?.();
const current = video.currentTime;
if (liveEdge > 0) latency = Math.max(0, liveEdge - current);
}
const formattedLatency = latency != null ? Number(latency).toFixed(2) : 'N/A';
const dropped = quality ? quality.droppedVideoFrames : 0;
const dCount = Number(dropped);
let dColor = '';
if (dCount > 0 && dCount <= 100) dColor = '#3498db';
else if (dCount > 100 && dCount <= 250) dColor = '#f1c40f';
else if (dCount > 250 && dCount <= 350) dColor = '#e67e22';
else if (dCount > 350) dColor = '#e74c3c';
miniStatsParts.buffer.textContent = buffer != null ? buffer + 's' : 'N/A';
miniStatsParts.latency.textContent = formattedLatency !== 'N/A' ? formattedLatency + 's' : 'N/A';
miniStatsParts.dropped.textContent = dropped;
miniStatsParts.dropped.style.color = dColor;
miniStatsTimer = setTimeout(updateMiniStats, 1000);
};
const toggleMiniStats = () => {
isMiniStatsOpen = !isMiniStatsOpen;
miniStats.classList.toggle("show", isMiniStatsOpen);
clearTimeout(miniStatsTimer);
if (isMiniStatsOpen) updateMiniStats();
};
const toggleStats = (e) => {
if (e && e.ctrlKey) { toggleMiniStats(); return; }
const p = getPlayer();
if (!p) return;
if (isStatsOpen && p.hideVideoInfo) { p.hideVideoInfo(); isStatsOpen = false; }
else if (p.showVideoInfo) { p.showVideoInfo(); isStatsOpen = true; }
statsBtn.classList.toggle("active", isStatsOpen);
};
statsBtn.addEventListener("click", toggleStats);
// Speed
const SPEED_MIN = 0.1, SPEED_MAX = 16, SPEED_STEP = 0.05, SPEED_STEP_FINE = 0.01, SPEED_DEFAULT = 1;
let targetSpeed = Math.round((video.playbackRate || SPEED_DEFAULT) * 100) / 100;
const speedBtn = mkBtn('custom-speed-btn', 'speed', targetSpeed + 'x', 'Speed', 'Playback Speed');
const updateSpeedBtnText = (rate) => {
setBtnLabel(speedBtn, rate + 'x', `Speed: ${rate}x`);
};
const applySpeed = (rate) => {
targetSpeed = Math.round(Math.min(SPEED_MAX, Math.max(SPEED_MIN, rate)) * 100) / 100;
if (video.playbackRate !== targetSpeed) video.playbackRate = targetSpeed;
updateSpeedBtnText(targetSpeed);
speedBtn.classList.toggle("modified", targetSpeed !== 1);
showSpeedOverlay(targetSpeed);
const speedInput = document.getElementById('ytee-precise-speed');
if (speedInput && parseFloat(speedInput.value) !== targetSpeed) speedInput.value = targetSpeed;
};
speedBtn.addEventListener("click", (e) => {
const step = e.shiftKey ? SPEED_STEP_FINE : SPEED_STEP;
const next = targetSpeed + step;
applySpeed(next > SPEED_MAX ? SPEED_MIN : next);
});
speedBtn.addEventListener("contextmenu", (e) => { e.preventDefault(); applySpeed(SPEED_DEFAULT); });
speedBtn.addEventListener("mouseenter", () => { isHoveringSpeedBtn = true; });
speedBtn.addEventListener("mouseleave", () => { isHoveringSpeedBtn = false; });
// Snap
const screenshotBtn = mkBtn('custom-screenshot-btn', 'screenshot', 'Snap', 'Snap (Ctrl+Click = save to storage)', 'Take Screenshot');
screenshotBtn.addEventListener("click", (e) => {
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth; canvas.height = video.videoHeight;
canvas.getContext("2d", { alpha: false }).drawImage(video, 0, 0);
const author = getVideoAuthor(getPlayer());
const timestamp = formatTimestamp(video.currentTime);
canvas.toBlob((blob) => {
if (!blob) return;
if (e.ctrlKey) {
const objUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = objUrl; a.download = `${author}_${timestamp}.png`; a.click();
URL.revokeObjectURL(objUrl);
}
if (navigator.clipboard) {
if (typeof ClipboardItem !== "undefined") {
navigator.clipboard.write([new ClipboardItem({ "image/png": blob })])
.then(() => {
setBtnLabel(screenshotBtn, e.ctrlKey ? '✓ Saved!' : '✓ Copied!');
flashBtnState(screenshotBtn, 'success');
setTimeout(() => setBtnLabel(screenshotBtn, 'Snap'), 1500);
})
.catch(() => {
setBtnLabel(screenshotBtn, '✗ Error');
flashBtnState(screenshotBtn, 'error');
setTimeout(() => setBtnLabel(screenshotBtn, 'Snap'), 1500);
});
} else {
if (!e.ctrlKey) { setBtnLabel(screenshotBtn, '✗ N/A'); setTimeout(() => setBtnLabel(screenshotBtn, 'Snap'), 1500); }
}
}
}, "image/png");
});
// Clip
const clipOverlay = Object.assign(document.createElement('div'), { id: 'custom-clip-overlay' });
const clipBtn = mkBtn('custom-clip-btn', 'clip', 'Clip', 'Clip (Ctrl = long)', 'Record WebM Clip');
let clipRecorder = null;
const stopClip = (cancelled) => {
if (!clipRecorder) return;
if (clipRecorder.state !== 'inactive') clipRecorder.stop();
clipRecorder = null;
if (activeStream) { activeStream.getTracks().forEach(t => t.stop()); activeStream = null; }
cancelAnimationFrame(clipRafId); clipRafId = null;
clipBtn.classList.remove('recording');
setBtnLabel(clipBtn, cancelled ? '✗ Cancelled' : 'Processing…');
clipOverlay.classList.remove('show');
if (cancelled) setTimeout(() => setBtnLabel(clipBtn, 'Clip'), 1500);
};
const startClip = (durationSec) => {
if (clipRecorder) { stopClip(true); return; }
if (!video.captureStream) { setBtnLabel(clipBtn, '✗ N/A'); setTimeout(() => setBtnLabel(clipBtn, 'Clip'), 2000); return; }
const mimeType = MediaRecorder.isTypeSupported('video/webm; codecs=vp9,opus')
? 'video/webm; codecs=vp9,opus'
: MediaRecorder.isTypeSupported('video/webm; codecs=vp8,opus')
? 'video/webm; codecs=vp8,opus' : 'video/webm';
// Clip Recording Setup
const width = video.videoWidth || 1920;
const height = video.videoHeight || 1080;
const px = width * height;
const isVP9 = mimeType.includes('vp9');
let fps = 30;
const p = getPlayer();
if (p && typeof p.getStatsForNerdsData === 'function') {
const s = p.getStatsForNerdsData();
if (s && s.resolution) {
const m = s.resolution.match(/@(\d+)/);
if (m) fps = parseInt(m[1]);
}
}
const BPP_MAP = [
[426 * 240, 0.250, 0.350],
[640 * 360, 0.200, 0.300],
[854 * 480, 0.170, 0.250],
[1280 * 720, 0.140, 0.200],
[1920 * 1080, 0.110, 0.160],
[2560 * 1440, 0.080, 0.120],
[3840 * 2160, 0.060, 0.090],
];
const [, vp9bpp, vp8bpp] = BPP_MAP.find(([maxPx]) => px <= maxPx) ?? BPP_MAP.at(-1);
const videoBitsPerSecond = Math.round(width * height * fps * (isVP9 ? vp9bpp : vp8bpp));
try {
activeStream = video.captureStream();
} catch (e) {
console.warn('YTEE: captureStream failed', e);
setBtnLabel(clipBtn, '✗ Error'); setTimeout(() => setBtnLabel(clipBtn, 'Clip'), 2000); return;
}
const chunks = [];
let recorder;
try {
recorder = new MediaRecorder(activeStream, {
mimeType,
videoBitsPerSecond,
audioBitsPerSecond: 192_000
});
} catch (e) {
console.warn('YTEE: MediaRecorder init failed', e);
activeStream.getTracks().forEach(t => t.stop()); activeStream = null;
setBtnLabel(clipBtn, '✗ Error'); setTimeout(() => setBtnLabel(clipBtn, 'Clip'), 2000); return;
}
recorder.ondataavailable = (ev) => { if (ev.data && ev.data.size > 0) chunks.push(ev.data); };
recorder.onstop = () => {
if (chunks.length === 0) { setBtnLabel(clipBtn, '✗ Empty'); setTimeout(() => setBtnLabel(clipBtn, 'Clip'), 1500); return; }
const blob = new Blob(chunks, { type: mimeType });
const author = getVideoAuthor(getPlayer());
const timestamp = formatTimestamp(video.currentTime);
const objUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = objUrl; a.download = `${author}_${timestamp}.webm`; a.click();
URL.revokeObjectURL(objUrl);
setBtnLabel(clipBtn, '✓ Saved!');
setTimeout(() => setBtnLabel(clipBtn, 'Clip'), 2000);
};
recorder.onerror = (ev) => {
console.warn('YTEE: MediaRecorder error', ev);
stopClip(true); setBtnLabel(clipBtn, '✗ Error'); setTimeout(() => setBtnLabel(clipBtn, 'Clip'), 2000);
};
clipRecorder = recorder;
recorder.start(200);
clipBtn.classList.add('recording');
setBtnLabel(clipBtn, 'Stop');
const endTime = performance.now() + durationSec * 1000;
const tick = () => {
const remaining = endTime - performance.now();
if (remaining <= 0) { stopClip(false); return; }
const text = `REC ${(remaining / 1000).toFixed(1)}s`;
if (clipOverlay.textContent !== text) clipOverlay.textContent = text;
clipOverlay.classList.add('show');
clipRafId = requestAnimationFrame(tick);
};
clipRafId = requestAnimationFrame(tick);
};
clipBtn.addEventListener('click', (e) => {
if (clipRecorder) { stopClip(true); return; }
const dur = e.ctrlKey ? (Number(currentSettings.clipDurationCtrl) || 300) : (Number(currentSettings.clipDuration) || 5);
startClip(dur);
});
clipBtn.addEventListener('contextmenu', (e) => { e.preventDefault(); stopClip(true); });
// PiP
const pipSupported = document.pictureInPictureEnabled && typeof video.requestPictureInPicture === "function";
const pipBtn = mkBtn('custom-pip-btn', 'pip', 'PiP', 'Picture-in-Picture', 'Picture-in-Picture');
if (pipSupported) {
pipBtn.addEventListener("click", async () => {
try {
if (document.pictureInPictureElement) await document.exitPictureInPicture();
else await video.requestPictureInPicture();
} catch (err) { console.error("PiP failed:", err); }
});
} else {
pipBtn.style.display = "none";
}
// URL
const urlBtn = mkBtn('custom-url-btn', 'url', 'URL', 'Copy URL (Ctrl+Click = with timestamp)', 'Copy Video URL');
urlBtn.addEventListener("click", async (e) => {
try {
let videoId = "";
const p = getPlayer();
if (p && typeof p.getVideoData === 'function') { const data = p.getVideoData(); if (data && data.video_id) videoId = data.video_id; }
if (!videoId) videoId = window.location.pathname.split('/').pop();
let url = `https://youtu.be/${videoId}`;
if (e.ctrlKey) url += `?t=${Math.floor(video.currentTime)}`;
if (navigator.clipboard) {
await navigator.clipboard.writeText(url);
setBtnLabel(urlBtn, '✓ Copied!');
flashBtnState(urlBtn, 'success');
setTimeout(() => setBtnLabel(urlBtn, 'URL', 'Copy URL (Ctrl+Click = with timestamp)'), 1500);
}
} catch (err) { console.error("Copy URL failed:", err); flashBtnState(urlBtn, 'error'); }
});
// Watch Later
const wlBtn = mkBtn('custom-wl-btn', 'wl', 'WL', 'Watch Later', 'Save to Watch Later');
let cachedApiKey = null, cachedContext = null;
const INNERTUBE_CACHE_TTL = 24 * 60 * 60 * 1000;
try {
if (typeof GM_getValue === 'function') {
const stored = GM_getValue('ytee-innertube', null);
if (stored) {
const parsed = JSON.parse(stored);
const age = Date.now() - (parsed.savedAt || 0);
if (age < INNERTUBE_CACHE_TTL && parsed.apiKey && parsed.context) { cachedApiKey = parsed.apiKey; cachedContext = parsed.context; }
}
}
} catch (e) { console.warn('InnerTube cache read failed', e); }
const sha1 = async (str) => {
const buf = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(str));
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, "0")).join("");
};
const getSapisid = () => { const m = document.cookie.match(/(?:^|;\s*)(?:__Secure-3PAPISID|SAPISID)=([^;]+)/); return m ? m[1] : null; };
const getInnertubeConfig = async (videoId) => {
if (cachedApiKey && cachedContext) return { apiKey: cachedApiKey, context: cachedContext };
const localYtcfg = uw.ytcfg || (uw.yt && uw.yt.config_);
if (localYtcfg && localYtcfg.get) {
const key = localYtcfg.get("INNERTUBE_API_KEY"), ctx = localYtcfg.get("INNERTUBE_CONTEXT");
if (key && ctx) {
cachedApiKey = key; cachedContext = ctx;
try { if (typeof GM_setValue === 'function') GM_setValue('ytee-innertube', JSON.stringify({ apiKey: key, context: ctx, savedAt: Date.now() })); } catch (e) { }
return { apiKey: key, context: ctx };
}
}
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest === "undefined") return reject(new Error("GM_xmlhttpRequest unavailable"));
GM_xmlhttpRequest({
method: "GET", url: `https://www.youtube.com/watch?v=${videoId}`,
headers: { "Accept-Language": navigator.language || "en-US,en;q=0.9" },
onload: (res) => {
const m = res.responseText.match(/ytcfg\.set\s*\(({[\s\S]+?})\s*\)\s*;/);
if (!m) return reject(new Error("ytcfg block not found"));
try {
const cfg = JSON.parse(m[1]);
if (!cfg.INNERTUBE_API_KEY) return reject(new Error("INNERTUBE_API_KEY missing"));
cachedApiKey = cfg.INNERTUBE_API_KEY; cachedContext = cfg.INNERTUBE_CONTEXT;
try { if (typeof GM_setValue === 'function') GM_setValue('ytee-innertube', JSON.stringify({ apiKey: cachedApiKey, context: cachedContext, savedAt: Date.now() })); } catch (e) { }
resolve({ apiKey: cachedApiKey, context: cachedContext });
} catch (e) { reject(e); }
},
onerror: () => reject(new Error("Network error")),
});
});
};
const clearInnertubeCache = () => {
cachedApiKey = null; cachedContext = null;
try { if (typeof GM_setValue === 'function') GM_setValue('ytee-innertube', null); } catch (e) { }
};
wlBtn.addEventListener("click", async () => {
try {
setBtnLabel(wlBtn, '…');
let videoId = "";
const p = getPlayer();
if (p && typeof p.getVideoData === 'function') { const data = p.getVideoData(); if (data && data.video_id) videoId = data.video_id; }
if (!videoId) videoId = window.location.pathname.split('/').pop();
if (!videoId) { setBtnLabel(wlBtn, '✗ Err'); setTimeout(() => setBtnLabel(wlBtn, 'WL'), 1500); return; }
const sapisid = getSapisid();
if (!sapisid) { setBtnLabel(wlBtn, '✗ Login'); setTimeout(() => setBtnLabel(wlBtn, 'WL'), 1500); return; }
const attemptRequest = async () => {
const { apiKey, context } = await getInnertubeConfig(videoId);
const ts = Math.floor(Date.now() / 1000);
const hashStr = await sha1(`${ts} ${sapisid} https://www.youtube.com`);
const sapisidHash = `${ts}_${hashStr}`;
const payload = { context, playlistId: "WL", actions: [{ addedVideoId: videoId, action: "ACTION_ADD_VIDEO" }] };
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== "undefined") {
GM_xmlhttpRequest({
method: "POST",
url: `https://www.youtube.com/youtubei/v1/browse/edit_playlist?key=${apiKey}&prettyPrint=false`,
headers: { "Content-Type": "application/json", "X-Origin": "https://www.youtube.com", "X-Goog-AuthUser": "0", "Authorization": `SAPISIDHASH ${sapisidHash}` },
data: JSON.stringify(payload),
onload: (res) => resolve(res.status),
onerror: () => reject(new Error("Network error")),
});
} else { reject(new Error("GM_xmlhttpRequest required")); }
});
};
let status = await attemptRequest();
if (status === 401 || status === 403) { clearInnertubeCache(); status = await attemptRequest(); }
if (status === 200) { setBtnLabel(wlBtn, '✓ Saved'); flashBtnState(wlBtn, 'success'); } else { throw new Error(`HTTP ${status}`); }
setTimeout(() => setBtnLabel(wlBtn, 'WL'), 1500);
} catch (err) { console.error("Watch Later failed:", err); setBtnLabel(wlBtn, '✗ Err'); flashBtnState(wlBtn, 'error'); setTimeout(() => setBtnLabel(wlBtn, 'WL'), 1500); }
});
const toggleBtn = mkBtn('custom-toggle-btn', currentSettings.isCollapsed ? 'expand' : 'hide', currentSettings.isCollapsed ? 'Expand' : 'Hide', 'Hide/Show controls', 'Toggle UI visibility');
toggleBtn.addEventListener('click', () => {
currentSettings.isCollapsed = !currentSettings.isCollapsed;
saveStoredSettings(currentSettings);
applyUIStates(currentSettings);
});
// Settings Modal
const settingsBtn = mkBtn('custom-settings-btn', 'settings', 'Settings', 'Settings', 'Settings');
const settingsModal = document.createElement("div");
settingsModal.id = "custom-settings-modal";
let isSettingsOpen = false;
const settingsContent = document.createElement("div");
settingsContent.id = "custom-settings-content";
const settingsHeader = Object.assign(document.createElement("div"), { id: "ytee-settings-header" });
const settingsTitle = Object.assign(document.createElement("h2"), { textContent: "YouTube Embed Enhancer" });
const infoBtn = Object.assign(document.createElement('div'), { className: 'ytee-info-btn', title: 'What do these stats mean?' });
infoBtn.appendChild(ICON_DEFS.info());
settingsHeader.append(settingsTitle, infoBtn);
const infoBox = Object.assign(document.createElement('div'), { className: 'ytee-info-box' });
const mkInfoRow = (title, text) => {
const p = document.createElement('p');
const s = document.createElement('strong'); s.textContent = title;
p.append(s, document.createTextNode(' ' + text));
return p;
};
infoBox.append(
mkInfoRow('Buffer', 'Seconds of video data pre-downloaded. Acts as a safety cushion; higher is more stable.'),
mkInfoRow('Latency', "Measures the network delay between your player and YouTube's servers. Note: YouTube's 'Live Latency' (Stats for Nerds) is the total delay from the streamer (e.g. mikochi did a pon if live latency is 6 secs then it will take 6 secs for the viewers to see it) to your screen."),
mkInfoRow('Drop', 'Frames that failed to display. Ideally zero; if rising, your device is struggling to keep up with the resolution.'),
Object.assign(document.createElement('a'), {
href: 'https://github.com/jmpatag/YouTube-Embed-Enhancer',
target: '_blank',
className: 'ytee-info-link',
textContent: 'Source: YouTube Embed Enhancer (GitHub)'
})
);
infoBtn.addEventListener('click', () => infoBox.classList.toggle('show'));
const settingsSubtitle = Object.assign(document.createElement("p"), { id: "ytee-settings-subtitle", textContent: "Customize your embed experience" });
const settingsItems = Object.assign(document.createElement("div"), { id: "custom-settings-items" });
const settingsButtons = Object.assign(document.createElement("div"), { id: "custom-settings-buttons" });
const restoreBtn = Object.assign(document.createElement("button"), { id: "custom-settings-restore", textContent: "Restore defaults" });
const cancelBtn = Object.assign(document.createElement("button"), { id: "custom-settings-cancel", textContent: "Cancel" });
const saveBtn = Object.assign(document.createElement("button"), { id: "custom-settings-save", textContent: "Save" });
settingsButtons.append(restoreBtn, cancelBtn, saveBtn);
settingsContent.append(settingsHeader, settingsSubtitle, infoBox, settingsItems, settingsButtons);
settingsModal.appendChild(settingsContent);
document.body.appendChild(settingsModal);
const mkToggleRow = (id, labelText, checked) => {
const div = Object.assign(document.createElement('div'), { className: 'setting-item' });
const wrap = Object.assign(document.createElement('div'), { className: 'ytee-toggle-wrap' });
const cb = Object.assign(document.createElement('input'), { type: 'checkbox', id, checked });
const track = Object.assign(document.createElement('label'), { className: 'ytee-toggle-track', htmlFor: id });
track.appendChild(Object.assign(document.createElement('span'), { className: 'ytee-toggle-thumb' }));
wrap.append(cb, track);
const label = Object.assign(document.createElement('label'), { htmlFor: id, textContent: labelText });
div.append(label, wrap);
return div;
};
const showSettingsModal = () => {
const items = settingsItems;
while (items.firstChild) items.removeChild(items.firstChild);
// Playback Quality
const sectionQ = Object.assign(document.createElement('h3'), { className: 'setting-section-title', textContent: 'Playback Quality' });
items.appendChild(sectionQ);
const qualityDiv = Object.assign(document.createElement('div'), { className: 'setting-item' });
const qualityLabel = Object.assign(document.createElement('label'), { htmlFor: 'preferred-quality', textContent: 'Preferred quality' });
const qualitySelect = Object.assign(document.createElement('select'), { id: 'preferred-quality', className: 'ytee-quality-select' });
const p = getPlayer();
let availableLevels = [];
if (p && typeof p.getAvailableQualityLevels === 'function') availableLevels = p.getAvailableQualityLevels().filter(q => q !== 'auto' && q !== 'unknown');
if (availableLevels.length === 0) availableLevels = QUALITY_ORDER.slice();
qualitySelect.appendChild(Object.assign(document.createElement('option'), { value: 'auto', textContent: QUALITY_LABELS['auto'] }));
availableLevels.forEach(level => qualitySelect.appendChild(Object.assign(document.createElement('option'), { value: level, textContent: QUALITY_LABELS[level] || level })));
qualitySelect.value = currentSettings.preferredQuality || 'auto';
if (!qualitySelect.value) qualitySelect.value = 'auto';
qualityDiv.append(qualityLabel, qualitySelect);
items.appendChild(qualityDiv);
items.appendChild(Object.assign(document.createElement('div'), { className: 'ytee-quality-note', textContent: 'Applies to all embeds instantly. Falls back to closest available level.' }));
// Volume
const sectionVol = Object.assign(document.createElement('h3'), { className: 'setting-section-title', textContent: 'Volume' });
items.appendChild(sectionVol);
const boostToggleRow = mkToggleRow('ytee-enable-volume-boost', 'Enable volume boost', currentSettings.enableVolumeBoost);
items.appendChild(boostToggleRow);
const boostToggleInput = boostToggleRow.querySelector('input');
const volBoostDiv = Object.assign(document.createElement('div'), { className: 'setting-item' });
const volBoostLabel = Object.assign(document.createElement('label'), { htmlFor: 'volume-boost-level', textContent: 'Boost level' });
const volBoostInput = Object.assign(document.createElement('input'), { type: 'range', id: 'volume-boost-level', min: '1.0', max: '3.0', step: '0.1', value: currentSettings.volumeBoostLevel });
volBoostInput.style.width = '140px';
const volBoostValue = Object.assign(document.createElement('span'), { className: 'ytee-slider-value', textContent: `${Number(currentSettings.volumeBoostLevel).toFixed(1)}x` });
volBoostInput.addEventListener('input', () => { volBoostValue.textContent = `${Number(volBoostInput.value).toFixed(1)}x`; });
const updateBoostState = () => {
const enabled = boostToggleInput.checked;
volBoostInput.disabled = !enabled;
volBoostDiv.style.opacity = enabled ? '1' : '0.4';
volBoostDiv.style.filter = enabled ? '' : 'grayscale(1)';
volBoostDiv.style.pointerEvents = enabled ? 'auto' : 'none';
};
boostToggleInput.addEventListener('change', updateBoostState);
volBoostDiv.append(volBoostLabel, volBoostInput, volBoostValue);
items.appendChild(volBoostDiv);
updateBoostState();
// Clip Recording
const sectionClip = Object.assign(document.createElement('h3'), { className: 'setting-section-title', textContent: 'Clip Recording' });
items.appendChild(sectionClip);
const clipDurDiv = Object.assign(document.createElement('div'), { className: 'setting-item' });
const clipDurLabel = Object.assign(document.createElement('label'), { htmlFor: 'clip-duration', textContent: 'Duration (seconds)' });
const clipDurInput = Object.assign(document.createElement('input'), { type: 'number', id: 'clip-duration', min: '1', max: '300', step: '1', value: currentSettings.clipDuration, className: 'hk-input' });
clipDurInput.style.width = '80px';
clipDurDiv.append(clipDurLabel, clipDurInput);
items.appendChild(clipDurDiv);
const clipCtrlDiv = Object.assign(document.createElement('div'), { className: 'setting-item' });
const clipCtrlLabel = Object.assign(document.createElement('label'), { htmlFor: 'clip-duration-ctrl', textContent: 'Ctrl+Click duration (seconds)' });
const clipCtrlInput = Object.assign(document.createElement('input'), { type: 'number', id: 'clip-duration-ctrl', min: '1', max: '300', step: '1', value: currentSettings.clipDurationCtrl, className: 'hk-input' });
clipCtrlInput.style.width = '80px';
clipCtrlDiv.append(clipCtrlLabel, clipCtrlInput);
items.appendChild(clipCtrlDiv);
items.appendChild(Object.assign(document.createElement('div'), { className: 'setting-note', textContent: 'Higher resolutions require more system resources.' }));
// Hotkeys
const hotkeyNames = {
toggleMute: 'Toggle Mute', toggleStats: 'Toggle Stats',
increaseSpeed: 'Increase Speed', decreaseSpeed: 'Decrease Speed',
increaseSpeedFine: 'Increase Speed (fine)', decreaseSpeedFine: 'Decrease Speed (fine)',
volumeUp: 'Volume Up', volumeDown: 'Volume Down',
};
const sectionHK = Object.assign(document.createElement('h3'), { className: 'setting-section-title', textContent: 'Hotkeys' });
items.appendChild(sectionHK);
Object.keys(hotkeyNames).forEach(key => {
const div = Object.assign(document.createElement('div'), { className: 'setting-item' });
const input = Object.assign(document.createElement('input'), { type: 'text', id: `hk-${key}`, value: currentSettings.hotkeys[key], className: 'hk-input' });
input.addEventListener('blur', () => { input.value = sanitizeHotkeyInput(input.value); });
const label = Object.assign(document.createElement('label'), { htmlFor: `hk-${key}`, textContent: hotkeyNames[key] });
div.append(label, input);
items.appendChild(div);
});
// Appearance
const sectionUI = Object.assign(document.createElement('h3'), { className: 'setting-section-title', textContent: 'Appearance' });
items.appendChild(sectionUI);
items.appendChild(mkToggleRow('ytee-compact-mode', 'Compact icon mode (hides text labels)', currentSettings.compactMode));
items.appendChild(Object.assign(document.createElement('div'), { className: 'setting-note', textContent: 'Compact mode saves space in multi-stream layouts by hiding text labels.' }));
items.appendChild(mkToggleRow('ytee-high-contrast', 'High Contrast Mode', currentSettings.highContrastUI));
items.appendChild(Object.assign(document.createElement('div'), { className: 'setting-note', textContent: 'Uses solid backgrounds for buttons.' }));
const buttonNames = {
vol: 'Volume Controls', wl: 'Watch Later', url: 'Copy URL', screenshot: 'Screenshot',
clip: 'Clip', pip: 'Picture-in-Picture (Firefox unsupported)',
speed: 'Playback Speed', stats: 'Stats for Nerds',
};
Object.keys(buttonNames).forEach(key => items.appendChild(mkToggleRow(`btn-${key}`, buttonNames[key], currentSettings.buttons[key])));
settingsModal.classList.add('show');
isSettingsOpen = true;
};
const hideSettingsModal = () => { settingsModal.classList.remove('show'); isSettingsOpen = false; };
settingsBtn.addEventListener('click', showSettingsModal);
cancelBtn.addEventListener('click', hideSettingsModal);
restoreBtn.addEventListener('click', () => {
currentSettings = JSON.parse(JSON.stringify(defaultSettings));
applyUIStates(currentSettings);
if (settingsModal.classList.contains('show')) showSettingsModal();
restoreBtn.textContent = 'Restored!'; restoreBtn.disabled = true;
setTimeout(() => { restoreBtn.textContent = 'Restore defaults'; restoreBtn.disabled = false; }, 1000);
});
saveBtn.addEventListener('click', () => {
const newSettings = { buttons: {}, hotkeys: {} };
Object.keys(defaultSettings.buttons).forEach(key => {
const el = document.getElementById(`btn-${key}`);
if (el) newSettings.buttons[key] = el.checked;
});
Object.keys(defaultSettings.hotkeys).forEach(key => {
const el = document.getElementById(`hk-${key}`);
if (el) newSettings.hotkeys[key] = sanitizeHotkeyInput(el.value);
});
newSettings.volumeBoostLevel = Number(document.getElementById('volume-boost-level').value) || 1;
newSettings.enableVolumeBoost = document.getElementById('ytee-enable-volume-boost').checked;
newSettings.clipDuration = Math.min(300, Math.max(1, Number(document.getElementById('clip-duration').value) || 5));
newSettings.clipDurationCtrl = Math.min(300, Math.max(1, Number(document.getElementById('clip-duration-ctrl').value) || 300));
newSettings.preferredQuality = document.getElementById('preferred-quality').value || 'auto';
newSettings.compactMode = document.getElementById('ytee-compact-mode').checked;
newSettings.highContrastUI = document.getElementById('ytee-high-contrast').checked;
newSettings.volumeCache = currentSettings.volumeCache || {};
newSettings.isCollapsed = currentSettings.isCollapsed;
currentSettings = newSettings;
saveStoredSettings(currentSettings);
buildHotkeyMap();
applyVolume(targetVolume);
applyQuality();
applyUIStates(currentSettings);
updateButtonVisibility();
saveBtn.textContent = 'Saved'; saveBtn.disabled = true;
setTimeout(() => { hideSettingsModal(); saveBtn.textContent = 'Save'; saveBtn.disabled = false; }, 350);
});
const updateButtonVisibility = () => {
const buttonMap = { wl: wlBtn, url: urlBtn, screenshot: screenshotBtn, clip: clipBtn, pip: pipBtn, speed: speedBtn, stats: statsBtn };
let anyCollapsibleVisible = false;
Object.keys(buttonMap).forEach(key => {
const isVisible = key === 'pip'
? currentSettings.buttons[key] && pipSupported
: currentSettings.buttons[key];
buttonMap[key].style.display = isVisible ? '' : 'none';
if (isVisible) anyCollapsibleVisible = true;
});
const volDisplay = currentSettings.buttons.vol ? '' : 'none';
if (muteBtn) muteBtn.style.display = volDisplay;
if (vol) vol.style.display = volDisplay;
if (toggleBtn) toggleBtn.style.display = anyCollapsibleVisible ? '' : 'none';
};
// Hotkeys
const SHIFTED_SYMBOL_MAP = {
'!': '1', '@': '2', '#': '3', '$': '4', '%': '5', '^': '6', '&': '7', '*': '8', '(': '9', ')': '0',
'~': '`', '_': '-', '+': '=', '{': '[', '}': ']', '|': '\\', ':': ';', '"': "'", '<': ',', '>': '.', '?': '/'
};
const sanitizeHotkeyInput = (value) => {
if (!value || typeof value !== 'string') return '';
let cleaned = value.trim().toLowerCase().replace(/\s*\+\s*/g, '+');
const parts = cleaned.split('+').filter(Boolean);
let modifiers = []; let key = '';
parts.forEach(part => {
if (['shift', 'ctrl', 'alt'].includes(part)) {
if (!modifiers.includes(part)) modifiers.push(part);
} else if (!key) key = part;
});
if (SHIFTED_SYMBOL_MAP[key]) {
key = SHIFTED_SYMBOL_MAP[key];
if (!modifiers.includes('shift')) modifiers.push('shift');
}
if (!key) return '';
return [...modifiers.sort(), key].join('+');
};
const normalizeHotkey = (hk) => {
const sanitized = sanitizeHotkeyInput(hk);
if (!sanitized) return { key: '', modifiers: { shift: false, ctrl: false, alt: false } };
const parts = sanitized.split('+');
const key = parts.pop();
const modifiers = {
shift: parts.includes('shift'),
ctrl: parts.includes('ctrl'),
alt: parts.includes('alt')
};
return { key, modifiers };
};
const getHotkeyCombos = ({ key, modifiers }) => {
const mods = (modifiers.ctrl ? 'ctrl+' : '') + (modifiers.alt ? 'alt+' : '') + (modifiers.shift ? 'shift+' : '');
const combos = [mods + key];
if (modifiers.shift) {
const shiftedKey = Object.keys(SHIFTED_SYMBOL_MAP).find(k => SHIFTED_SYMBOL_MAP[k] === key);
if (shiftedKey) {
const baseMods = (modifiers.ctrl ? 'ctrl+' : '') + (modifiers.alt ? 'alt+' : '');
combos.push(baseMods + shiftedKey);
}
}
return combos;
};
const hotkeyMap = {};
const buildHotkeyMap = () => {
Object.keys(hotkeyMap).forEach(k => delete hotkeyMap[k]);
Object.keys(currentSettings.hotkeys).forEach(action => {
getHotkeyCombos(normalizeHotkey(currentSettings.hotkeys[action])).forEach(combo => {
hotkeyMap[combo] = action;
});
});
};
buildHotkeyMap();
window.addEventListener("keydown", (e) => {
if (isSettingsOpen) {
if (e.key === "Escape") { hideSettingsModal(); e.preventDefault(); e.stopImmediatePropagation(); }
return;
}
const isShiftedSym = e.shiftKey && (e.key in SHIFTED_SYMBOL_MAP);
const combo = [
e.altKey ? 'alt+' : '',
e.ctrlKey ? 'ctrl+' : '',
e.shiftKey && !isShiftedSym ? 'shift+' : '',
e.key.toLowerCase()
].join('');
const action = hotkeyMap[combo];
if (action) {
e.stopImmediatePropagation(); e.preventDefault();
switch (action) {
case 'toggleMute': toggleMute(); break;
case 'toggleStats': toggleStats(); break;
case 'increaseSpeed': applySpeed(targetSpeed + SPEED_STEP); break;
case 'decreaseSpeed': applySpeed(targetSpeed - SPEED_STEP); break;
case 'increaseSpeedFine': applySpeed(targetSpeed + SPEED_STEP_FINE); break;
case 'decreaseSpeedFine': applySpeed(targetSpeed - SPEED_STEP_FINE); break;
case 'volumeUp': applyVolume(targetVolume + 0.05); break;
case 'volumeDown': applyVolume(targetVolume - 0.05); break;
}
showControls();
}
}, true);
const btnGroup = document.createElement("div");
btnGroup.id = "custom-btn-group";
btnGroup.append(toggleBtn, wlBtn, urlBtn, screenshotBtn, clipBtn, pipBtn, speedBtn, statsBtn, settingsBtn);
updateButtonVisibility();
let isHoveringBtnGroup = false;
btnGroup.addEventListener("mouseenter", () => { isHoveringBtnGroup = true; });
btnGroup.addEventListener("mouseleave", () => { isHoveringBtnGroup = false; });
const ALL_CONTROLS = [muteBtn, vol, btnGroup];
let controlsVisible = false, lastInteractionTime = 0;
const checkHideControls = () => {
if (Date.now() - lastInteractionTime >= 2000 && !isHoveringBtnGroup) {
controlsVisible = false;
ALL_CONTROLS.forEach(el => el.classList.remove("show"));
} else {
controlsTimeout = setTimeout(checkHideControls, 2000);
}
};
const showControls = () => {
lastInteractionTime = Date.now();
if (!controlsVisible) {
controlsVisible = true;
ALL_CONTROLS.forEach(el => el.classList.add("show"));
clearTimeout(controlsTimeout);
controlsTimeout = setTimeout(checkHideControls, 2000);
}
};
window.addEventListener("mousemove", () => {
if (rafPending) return;
rafPending = requestAnimationFrame(() => { showControls(); rafPending = 0; });
});
// memory 2
const initialVid = getPlayer()?.getVideoData?.().video_id;
if (initialVid && currentSettings.volumeCache[initialVid] !== undefined) {
applyVolume(currentSettings.volumeCache[initialVid]);
}
applySpeed(targetSpeed);
showControls();
window.addEventListener("dblclick", (e) => {
if (isSettingsOpen) return;
if (e.target.closest("#custom-btn-group, #custom-mute-btn, #custom-vol-slider")) return;
if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(() => { });
else if (document.exitFullscreen) document.exitFullscreen();
});
const initUI = () => {
document.body.prepend(volPct, speedOverlay, clipOverlay, miniStats, vol, muteBtn, btnGroup);
};
initUI();
// Cleanup
const updateEmbedWidth = () => {
const w = document.documentElement.clientWidth || window.innerWidth;
document.documentElement.style.setProperty('--ytee-ew', w + 'px');
};
updateEmbedWidth();
const resizeObs = new ResizeObserver(updateEmbedWidth);
resizeObs.observe(document.documentElement);
if (typeof GM_addValueChangeListener === 'function') {
GM_addValueChangeListener('ytee-settings', (name, oldVal, newVal, remote) => {
if (remote) {
try {
currentSettings = normalizeSettings(typeof newVal === 'string' ? JSON.parse(newVal) : newVal);
updateButtonVisibility();
buildHotkeyMap();
applyQuality();
applyUIStates(currentSettings);
} catch (e) { console.warn('Failed to sync settings', e); }
}
});
}
const cleanup = () => {
clearTimeout(volTimeout); clearTimeout(speedTimeout); clearTimeout(controlsTimeout); clearTimeout(miniStatsTimer);
if (clipRafId) cancelAnimationFrame(clipRafId);
if (rafPending) cancelAnimationFrame(rafPending);
if (wheelRafId) cancelAnimationFrame(wheelRafId);
rafPending = 0; clipRafId = null; wheelRafId = 0;
if (clipRecorder) { try { if (clipRecorder.state !== 'inactive') clipRecorder.stop(); } catch (e) { } clipRecorder = null; }
if (activeStream) { activeStream.getTracks().forEach(t => t.stop()); activeStream = null; }
if (audioContext) { audioContext.close().catch(() => { }); audioContext = null; gainNode = null; mediaSource = null; }
cachedPlayer = null;
resizeObs.disconnect();
};
window.addEventListener('pagehide', cleanup);
});
})();