Better YouTube Video Controls

Enhanced YouTube playback controls with privacy-focused features. Hold right arrow for fast playback, left for slow-mo, and track your last 5 videos' timestamps locally so you can resume watching where you left off. (no data sent to servers).

  1. // ==UserScript==
  2. // @name Better YouTube Video Controls
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.5.3
  5. // @description Enhanced YouTube playback controls with privacy-focused features. Hold right arrow for fast playback, left for slow-mo, and track your last 5 videos' timestamps locally so you can resume watching where you left off. (no data sent to servers).
  6. // @author Henry Suen
  7. // @match *://*.youtube.com/*
  8. // @license MIT
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_deleteValue
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // --- USER CONFIGURABLE SETTINGS ---
  19.  
  20. // Get user settings or use defaults
  21. let HOLD_RIGHT_PLAYBACK_SPEED = GM_getValue('holdRightPlaybackSpeed', 2);
  22. let HOLD_LEFT_PLAYBACK_SPEED = GM_getValue('holdLeftPlaybackSpeed', 0.25);
  23. let LONG_PRESS_THRESHOLD = GM_getValue('longPressThreshold', 250); // Default 250ms
  24. let SKIP_SECONDS = GM_getValue('skipSeconds', 5); // Default 5 seconds
  25. let RESUME_WATCHING_ENABLED = GM_getValue('resumeWatchingEnabled', true); // Default to on
  26. const MAX_HISTORY_SIZE = 5; // Number of videos to remember
  27.  
  28. // Register menu commands for user configuration
  29. GM_registerMenuCommand('Set Right Arrow Hold Speed (Fast)', function() {
  30. const newSpeed = parseFloat(prompt('Enter playback speed when holding right arrow e.g.,1.5, 2.0(max):', HOLD_RIGHT_PLAYBACK_SPEED));
  31. if (!isNaN(newSpeed) && newSpeed >= 1 && newSpeed <= 2) {
  32. HOLD_RIGHT_PLAYBACK_SPEED = newSpeed;
  33. GM_setValue('holdRightPlaybackSpeed', newSpeed);
  34. alert(`Right arrow hold speed set to ${newSpeed}x`);
  35. } else {
  36. alert('Invalid value. Please enter a number between 1 and 2.');
  37. }
  38. });
  39.  
  40. GM_registerMenuCommand('Set Left Arrow Hold Speed (Slow)', function() {
  41. const newSpeed = parseFloat(prompt('Enter playback speed when holding left arrow. e.g.,0.25(min), 0.5:', HOLD_LEFT_PLAYBACK_SPEED));
  42. if (!isNaN(newSpeed) && newSpeed >= 0.25 && newSpeed <= 1) {
  43. HOLD_LEFT_PLAYBACK_SPEED = newSpeed;
  44. GM_setValue('holdLeftPlaybackSpeed', newSpeed);
  45. alert(`Left arrow hold speed set to ${newSpeed}x`);
  46. } else {
  47. alert('Invalid value. Please enter a number between 0.2 and 1.');
  48. }
  49. });
  50.  
  51. GM_registerMenuCommand('Set Skip Seconds', function() {
  52. const newSkip = parseInt(prompt('Enter seconds to skip on right/left arrow tap:', SKIP_SECONDS));
  53. if (!isNaN(newSkip) && newSkip > 0) {
  54. SKIP_SECONDS = newSkip;
  55. GM_setValue('skipSeconds', newSkip);
  56. alert(`Skip seconds set to ${newSkip}`);
  57. } else {
  58. alert('Invalid value. Please enter a positive number.');
  59. }
  60. });
  61.  
  62. GM_registerMenuCommand('Toggle Resume Watching: ' + (RESUME_WATCHING_ENABLED ? 'ON' : 'OFF'), function() {
  63. RESUME_WATCHING_ENABLED = !RESUME_WATCHING_ENABLED;
  64. GM_setValue('resumeWatchingEnabled', RESUME_WATCHING_ENABLED);
  65. alert(`Resume Watching: ${RESUME_WATCHING_ENABLED ? 'Enabled ✓' : 'Disabled ✗'}`);
  66. // Refresh menu command label
  67. GM_registerMenuCommand('Toggle Resume Watching: ' + (RESUME_WATCHING_ENABLED ? 'ON' : 'OFF'), arguments.callee);
  68. });
  69.  
  70. GM_registerMenuCommand('Clear Saved Video History', function() {
  71. clearVideoHistory();
  72. alert('Video history has been cleared.');
  73. });
  74.  
  75. // --- END OF USER CONFIGURABLE SETTINGS ---
  76.  
  77. // Store the original playback rate
  78. let originalPlaybackRate = 1.0;
  79. // Flag to track if we're handling a long press
  80. let isLongPress = false;
  81. // Track when the key was pressed
  82. let keyDownTime = 0;
  83. // Flag to ensure we only process one keydown at a time
  84. let keyAlreadyDown = false;
  85. // Track which key is being held
  86. let activeKey = null;
  87. // Reference to our speed indicator element
  88. let speedIndicator = null;
  89. // Reference to our timeout for detecting long press
  90. let longPressTimeout = null;
  91. // Our own UI indicator for actions
  92. let actionIndicator = null;
  93. // Timeout for hiding the action indicator
  94. let hideActionTimeout = null;
  95. // Variables for tracking video position
  96. let currentVideoId = null;
  97. let positionSaveInterval = null;
  98. let lastSavedPosition = 0;
  99. // Store the element that had focus before we blurred the progress bar
  100. let lastActiveElement = null;
  101. // Flag to track if resume was skipped due to timestamp in URL
  102. let resumeSkippedDueToTimestamp = false;
  103. // Debug mode for troubleshooting
  104. const DEBUG = false;
  105.  
  106. // --- Utility Functions ---
  107.  
  108. // Debug logging function
  109. function debugLog(...args) {
  110. if (DEBUG) {
  111. console.log('[YT Controls]', ...args);
  112. }
  113. }
  114.  
  115. // Get video history array
  116. function getVideoHistory() {
  117. const history = GM_getValue('videoHistory', []);
  118. return Array.isArray(history) ? history : [];
  119. }
  120.  
  121. // Save video history array
  122. function saveVideoHistory(history) {
  123. GM_setValue('videoHistory', history);
  124. }
  125.  
  126. // Add a video to the history
  127. function addToVideoHistory(videoId) {
  128. if (!videoId) return;
  129.  
  130. let history = getVideoHistory();
  131.  
  132. // Remove the video ID if it's already in the history to avoid duplicates
  133. history = history.filter(item => item !== videoId);
  134.  
  135. // Add the video ID to the beginning of the array
  136. history.unshift(videoId);
  137.  
  138. // Limit the history size
  139. if (history.length > MAX_HISTORY_SIZE) {
  140. history = history.slice(0, MAX_HISTORY_SIZE);
  141. }
  142.  
  143. // Save the updated history
  144. saveVideoHistory(history);
  145. debugLog('Updated video history:', history);
  146. }
  147.  
  148. // Clear all saved video history
  149. function clearVideoHistory() {
  150. GM_setValue('videoHistory', []);
  151.  
  152. // Also clear all position values
  153. const allKeys = GM_listValues ? GM_listValues() : [];
  154. for (const key of allKeys) {
  155. if (key.startsWith('video_pos_')) {
  156. GM_deleteValue(key);
  157. }
  158. }
  159.  
  160. debugLog('Video history cleared');
  161. }
  162.  
  163. // Extract video ID from YouTube URL using multiple methods
  164. function getVideoId() {
  165. try {
  166. // Method 1: Using URLSearchParams (most reliable)
  167. const urlParams = new URLSearchParams(window.location.search);
  168. const vParam = urlParams.get('v');
  169. if (vParam) {
  170. debugLog('Video ID from URL params:', vParam);
  171. return vParam;
  172. }
  173.  
  174. // Method 2: Try regex on full URL
  175. const url = window.location.href;
  176. const regex1 = /(?:v=|\/v\/|youtu\.be\/|\/embed\/)([a-zA-Z0-9_-]{11})/;
  177. const match1 = url.match(regex1);
  178. if (match1 && match1[1]) {
  179. debugLog('Video ID from URL regex:', match1[1]);
  180. return match1[1];
  181. }
  182.  
  183. // Method 3: Look for it in page metadata
  184. const metaTag = document.querySelector('meta[property="og:video:url"], meta[itemprop="videoId"]');
  185. if (metaTag && metaTag.content) {
  186. const metaUrl = metaTag.content;
  187. const metaMatch = metaUrl.match(/([a-zA-Z0-9_-]{11})/);
  188. if (metaMatch && metaMatch[1]) {
  189. debugLog('Video ID from meta tag:', metaMatch[1]);
  190. return metaMatch[1];
  191. }
  192. }
  193.  
  194. // Method 4: Look for video ID in the page content
  195. const pageContent = document.documentElement.innerHTML;
  196. const videoIdMatch = pageContent.match(/"videoId"\s*:\s*"([a-zA-Z0-9_-]{11})"/);
  197. if (videoIdMatch && videoIdMatch[1]) {
  198. debugLog('Video ID from page content:', videoIdMatch[1]);
  199. return videoIdMatch[1];
  200. }
  201.  
  202. debugLog('Could not find video ID');
  203. return null;
  204. } catch (e) {
  205. console.error('Error in getVideoId:', e);
  206. return null;
  207. }
  208. }
  209.  
  210. // Make video ID safe for storage as a key
  211. function getSafeVideoKey(videoId) {
  212. if (!videoId) return null;
  213.  
  214. // Encode the video ID for safety as a key
  215. return 'video_pos_' + encodeURIComponent(videoId);
  216. }
  217.  
  218. // Check if URL has a timestamp parameter
  219. function hasTimestampInUrl() {
  220. const url = window.location.href;
  221. return url.includes("&t=") || url.includes("?t=");
  222. }
  223.  
  224. // Extract timestamp value from URL
  225. function getTimestampFromUrl() {
  226. const url = window.location.href;
  227. const regex = /[?&]t=([0-9hms]+)/;
  228. const match = url.match(regex);
  229.  
  230. if (!match) return 0;
  231.  
  232. const value = match[1];
  233.  
  234. // Handle numeric seconds format (e.g., t=120)
  235. if (/^\d+$/.test(value)) {
  236. return parseInt(value);
  237. }
  238.  
  239. // Handle YouTube's time format (e.g., 1h2m3s)
  240. let seconds = 0;
  241. const hours = value.match(/(\d+)h/);
  242. const minutes = value.match(/(\d+)m/);
  243. const secs = value.match(/(\d+)s/);
  244.  
  245. if (hours) seconds += parseInt(hours[1]) * 3600;
  246. if (minutes) seconds += parseInt(minutes[1]) * 60;
  247. if (secs) seconds += parseInt(secs[1]);
  248.  
  249. return seconds;
  250. }
  251.  
  252. // Save current video position
  253. function saveVideoPosition() {
  254. // Skip if resume watching is disabled
  255. if (!RESUME_WATCHING_ENABLED) return;
  256.  
  257. const video = findYouTubeVideo();
  258. if (!video) return;
  259.  
  260. const videoId = getVideoId();
  261. if (!videoId) return;
  262.  
  263. const safeKey = getSafeVideoKey(videoId);
  264. if (!safeKey) return;
  265.  
  266. // Only update if position changed significantly (more than 1 second)
  267. if (Math.abs(video.currentTime - lastSavedPosition) > 1) {
  268. lastSavedPosition = video.currentTime;
  269.  
  270. // Add to video history
  271. addToVideoHistory(videoId);
  272.  
  273. // Save position
  274. GM_setValue(safeKey, video.currentTime);
  275. debugLog('Saved position', videoId, video.currentTime);
  276. }
  277. }
  278.  
  279. // Start tracking video position
  280. function startPositionTracking() {
  281. // Skip if resume watching is disabled
  282. if (!RESUME_WATCHING_ENABLED) return;
  283.  
  284. // Clear any existing interval
  285. if (positionSaveInterval) {
  286. clearInterval(positionSaveInterval);
  287. }
  288.  
  289. // Set up the new interval
  290. positionSaveInterval = setInterval(saveVideoPosition, 5000);
  291.  
  292. // Update the current video ID
  293. currentVideoId = getVideoId();
  294.  
  295. // Add to history immediately
  296. if (currentVideoId) {
  297. addToVideoHistory(currentVideoId);
  298. debugLog('Started tracking', currentVideoId);
  299. }
  300. }
  301.  
  302. // Restore video position
  303. function restoreVideoPosition() {
  304. // Skip if resume watching is disabled
  305. if (!RESUME_WATCHING_ENABLED) return;
  306.  
  307. const videoId = getVideoId();
  308. if (!videoId) return;
  309.  
  310. debugLog('Attempting to restore position for', videoId);
  311.  
  312. // Check if URL has a timestamp
  313. if (hasTimestampInUrl()) {
  314. resumeSkippedDueToTimestamp = true;
  315. // Show notification that resume was skipped
  316. showActionIndicator("Resume skipped: Timestamp in URL", 3000);
  317. debugLog('Resume skipped due to timestamp in URL');
  318. return;
  319. }
  320.  
  321. // Reset the flag since there's no timestamp
  322. resumeSkippedDueToTimestamp = false;
  323.  
  324. const safeKey = getSafeVideoKey(videoId);
  325. if (!safeKey) return;
  326.  
  327. // Get the saved position
  328. const savedPosition = GM_getValue(safeKey, 0);
  329.  
  330. debugLog('Retrieved saved position:', savedPosition);
  331.  
  332. if (savedPosition > 0) {
  333. const video = findYouTubeVideo();
  334. if (video) {
  335. // Don't resume if we're near the start or very close to where we left off
  336. if (video.currentTime < 3 && savedPosition > 5) {
  337. video.currentTime = savedPosition;
  338.  
  339. // Update YouTube's internal state to be aware of our time change
  340. const ytplayer = findYouTubePlayer();
  341. if (ytplayer && typeof ytplayer.seekTo === 'function') {
  342. try {
  343. ytplayer.seekTo(savedPosition, true);
  344. } catch(e) {
  345. debugLog('Error in ytplayer.seekTo', e);
  346. }
  347. }
  348.  
  349. // Show a notification that we've resumed
  350. showActionIndicator(`Resumed at ${formatTime(savedPosition)}`, 3000);
  351. debugLog('Resumed to', savedPosition);
  352. }
  353. }
  354. }
  355. }
  356.  
  357. // Format time in MM:SS format
  358. function formatTime(seconds) {
  359. const minutes = Math.floor(seconds / 60);
  360. const remainingSeconds = Math.floor(seconds % 60);
  361. return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
  362. }
  363.  
  364. // Find video element on YouTube
  365. function findYouTubeVideo() {
  366. return document.querySelector('#movie_player video');
  367. }
  368.  
  369. // Find YouTube player element
  370. function findYouTubePlayer() {
  371. return document.querySelector('#movie_player');
  372. }
  373.  
  374. // Utility function to check if an element is an input field
  375. function isInputField(element) {
  376. if (!element) return false;
  377. const tagName = element.tagName.toLowerCase();
  378. const type = (element.type || '').toLowerCase();
  379.  
  380. return (tagName === 'input' &&
  381. ['text', 'password', 'email', 'number', 'search', 'tel', 'url'].includes(type)) ||
  382. tagName === 'textarea' ||
  383. element.isContentEditable;
  384. }
  385.  
  386. // Check if focus is on progress bar
  387. function isProgressBarFocused() {
  388. // There are multiple elements that make up the progress bar
  389. const progressBar = document.querySelector('.ytp-progress-bar');
  390. const scrubber = document.querySelector('.ytp-scrubber-container');
  391. const progressList = document.querySelectorAll('.ytp-progress-list');
  392.  
  393. if (document.activeElement === progressBar ||
  394. document.activeElement === scrubber ||
  395. (progressList && Array.from(progressList).includes(document.activeElement))) {
  396. return true;
  397. }
  398.  
  399. // Check for aria attributes that might indicate focus on progress controls
  400. const activeElement = document.activeElement;
  401. if (activeElement && (
  402. activeElement.getAttribute('aria-valuemin') !== null ||
  403. activeElement.getAttribute('aria-valuemax') !== null ||
  404. activeElement.getAttribute('role') === 'slider'
  405. )) {
  406. // Check if it's in the player controls
  407. const isInControls = activeElement.closest('.ytp-chrome-bottom');
  408. return isInControls !== null;
  409. }
  410.  
  411. return false;
  412. }
  413.  
  414. // Remove focus from progress bar and trigger volume control
  415. function handleVolumeKeyOnProgressBar(isVolumeUp) {
  416. if (isProgressBarFocused()) {
  417. // Store the active element so we can restore it later
  418. lastActiveElement = document.activeElement;
  419.  
  420. // Blur the progress bar
  421. if (lastActiveElement && lastActiveElement.blur) {
  422. lastActiveElement.blur();
  423. }
  424.  
  425. // Move focus to the player itself
  426. const player = findYouTubePlayer();
  427. if (player && player.focus) {
  428. player.focus();
  429. }
  430.  
  431. // Create and dispatch a synthetic key event to trigger YouTube's volume control
  432. // We do this after ensuring focus is off the progress bar
  433. setTimeout(() => {
  434. const event = new KeyboardEvent('keydown', {
  435. key: isVolumeUp ? 'ArrowUp' : 'ArrowDown',
  436. code: isVolumeUp ? 'ArrowUp' : 'ArrowDown',
  437. keyCode: isVolumeUp ? 38 : 40,
  438. which: isVolumeUp ? 38 : 40,
  439. bubbles: true,
  440. cancelable: true,
  441. composed: true
  442. });
  443.  
  444. // Dispatch to player element to ensure YouTube's volume control is triggered
  445. if (player) {
  446. player.dispatchEvent(event);
  447. } else {
  448. // Fallback to document if player can't be found
  449. document.dispatchEvent(event);
  450. }
  451. }, 10);
  452.  
  453. return true;
  454. }
  455. return false;
  456. }
  457.  
  458. // --- UI Elements ---
  459.  
  460. // Create the speed indicator UI element
  461. function createSpeedIndicator() {
  462. // Remove any existing indicator first
  463. removeSpeedIndicator();
  464.  
  465. // Create a new indicator
  466. speedIndicator = document.createElement('div');
  467. speedIndicator.id = 'speed-indicator';
  468. speedIndicator.textContent = '1x'; // Default text - will be updated when shown
  469.  
  470. // Style the indicator with larger text
  471. const style = speedIndicator.style;
  472. style.position = 'absolute';
  473. style.right = '20px';
  474. style.top = '20px';
  475. style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
  476. style.color = 'white';
  477. style.padding = '8px 16px'; // Larger padding
  478. style.borderRadius = '6px';
  479. style.fontSize = '28px'; // Large font size
  480. style.fontWeight = 'bold';
  481. style.zIndex = '9999';
  482. style.display = 'none'; // Hidden by default
  483. style.opacity = '0';
  484. style.transition = 'opacity 0.3s ease';
  485.  
  486. // Add it to the player
  487. const player = findYouTubePlayer();
  488. if (player) {
  489. player.appendChild(speedIndicator);
  490. } else {
  491. document.body.appendChild(speedIndicator); // Fallback
  492. }
  493.  
  494. return speedIndicator;
  495. }
  496.  
  497. // Create action indicator for volume and skip
  498. function createActionIndicator() {
  499. if (actionIndicator) {
  500. return actionIndicator;
  501. }
  502.  
  503. actionIndicator = document.createElement('div');
  504. actionIndicator.id = 'action-indicator';
  505.  
  506. // Style the action indicator
  507. const style = actionIndicator.style;
  508. style.position = 'absolute';
  509. style.left = '50%';
  510. style.top = '50%';
  511. style.transform = 'translate(-50%, -50%)';
  512. style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
  513. style.color = 'white';
  514. style.padding = '12px 20px';
  515. style.borderRadius = '8px';
  516. style.fontSize = '24px';
  517. style.fontWeight = 'bold';
  518. style.zIndex = '10000';
  519. style.display = 'none';
  520. style.opacity = '0';
  521. style.transition = 'opacity 0.3s ease';
  522.  
  523. // Add it to the player
  524. const player = findYouTubePlayer();
  525. if (player) {
  526. player.appendChild(actionIndicator);
  527. } else {
  528. document.body.appendChild(actionIndicator);
  529. }
  530.  
  531. return actionIndicator;
  532. }
  533.  
  534. // Show the speed indicator with a fade-in effect
  535. function showSpeedIndicator(speed) {
  536. if (!speedIndicator) {
  537. speedIndicator = createSpeedIndicator();
  538. }
  539. speedIndicator.textContent = speed + 'x'; // Update with current speed
  540. speedIndicator.style.display = 'block';
  541. setTimeout(() => {
  542. speedIndicator.style.opacity = '1';
  543. }, 10); // Small delay to ensure the transition works
  544. }
  545.  
  546. // Hide the speed indicator with a fade-out effect
  547. function hideSpeedIndicator() {
  548. if (speedIndicator) {
  549. speedIndicator.style.opacity = '0';
  550. setTimeout(() => {
  551. speedIndicator.style.display = 'none';
  552. }, 300); // Wait for the transition to complete
  553. }
  554. }
  555.  
  556. // Remove the speed indicator completely
  557. function removeSpeedIndicator() {
  558. if (speedIndicator && speedIndicator.parentNode) {
  559. speedIndicator.parentNode.removeChild(speedIndicator);
  560. speedIndicator = null;
  561. }
  562. }
  563.  
  564. // Show action indicator
  565. function showActionIndicator(text, duration = 1000) {
  566. // Clear any existing hide timeout
  567. if (hideActionTimeout) {
  568. clearTimeout(hideActionTimeout);
  569. hideActionTimeout = null;
  570. }
  571.  
  572. const indicator = createActionIndicator();
  573.  
  574. // If indicator is already visible, just update text without the fade-out/fade-in
  575. if (indicator.style.opacity === '1') {
  576. indicator.textContent = text;
  577. } else {
  578. indicator.textContent = text;
  579. indicator.style.display = 'block';
  580.  
  581. // Use a timeout to ensure the transition works
  582. setTimeout(() => {
  583. indicator.style.opacity = '1';
  584. }, 10);
  585. }
  586.  
  587. // Set a new timeout to hide the indicator
  588. hideActionTimeout = setTimeout(() => {
  589. indicator.style.opacity = '0';
  590. setTimeout(() => {
  591. indicator.style.display = 'none';
  592. }, 300);
  593. }, duration);
  594. }
  595.  
  596. // --- Action Functions ---
  597.  
  598. // Function to perform a seek forward or backward
  599. function performSeek(forward = true) {
  600. const video = findYouTubeVideo();
  601. if (!video) return false;
  602.  
  603. // Calculate new time
  604. const currentTime = video.currentTime;
  605. const newTime = currentTime + (forward ? SKIP_SECONDS : -SKIP_SECONDS);
  606. const finalTime = Math.max(0, newTime);
  607.  
  608. // Update both the HTML5 video element and YouTube's internal state
  609. video.currentTime = finalTime;
  610.  
  611. // Try to sync with YouTube's API
  612. const player = findYouTubePlayer();
  613. if (player && typeof player.seekTo === 'function') {
  614. try {
  615. player.seekTo(finalTime, true);
  616. } catch(e) {}
  617. }
  618.  
  619. // Show our custom UI indicator
  620. showActionIndicator(`${forward ? 'Forward' : 'Backward'} ${SKIP_SECONDS}s`);
  621. return true;
  622. }
  623.  
  624. // Function to change playback speed
  625. function changePlaybackSpeed(speed) {
  626. const video = findYouTubeVideo();
  627. if (!video) return false;
  628.  
  629. // Set new speed
  630. video.playbackRate = speed;
  631.  
  632. // Also try to set through YouTube's API if available
  633. const player = findYouTubePlayer();
  634. if (player && typeof player.setPlaybackRate === 'function') {
  635. try {
  636. player.setPlaybackRate(speed);
  637. } catch(e) {}
  638. }
  639.  
  640. // Show indicator
  641. showSpeedIndicator(speed);
  642.  
  643. return true;
  644. }
  645.  
  646. // Function to reset playback speed
  647. function resetPlaybackSpeed() {
  648. const video = findYouTubeVideo();
  649. if (!video) return false;
  650.  
  651. // Reset to original speed
  652. video.playbackRate = 1;
  653.  
  654. // Also try to reset through YouTube's API
  655. const player = findYouTubePlayer();
  656. if (player && typeof player.setPlaybackRate === 'function') {
  657. try {
  658. player.setPlaybackRate(1);
  659. } catch(e) {}
  660. }
  661.  
  662. // Hide speed indicator
  663. hideSpeedIndicator();
  664.  
  665. return true;
  666. }
  667.  
  668. // --- Key Event Handlers ---
  669.  
  670. // Main handler for key down events
  671. const handleKeyDown = function(event) {
  672. // Skip if we're in an input field
  673. if (isInputField(document.activeElement)) return;
  674.  
  675. // Handle arrow keys
  676. if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
  677. // Prevent default immediately
  678. event.preventDefault();
  679. event.stopPropagation();
  680.  
  681. // Store which key was pressed
  682. activeKey = event.key;
  683.  
  684. // Only proceed if we found a video and it's the first keydown
  685. if (findYouTubeVideo() && !keyAlreadyDown) {
  686. keyAlreadyDown = true;
  687. keyDownTime = Date.now();
  688.  
  689. // Set up a timeout to check for long press
  690. longPressTimeout = setTimeout(() => {
  691. // If key is still being pressed after threshold
  692. if (keyAlreadyDown) {
  693. isLongPress = true;
  694.  
  695. // Different behavior based on which arrow key
  696. if (activeKey === 'ArrowRight') {
  697. // Right arrow for fast playback
  698. changePlaybackSpeed(HOLD_RIGHT_PLAYBACK_SPEED);
  699. } else if (activeKey === 'ArrowLeft') {
  700. // Left arrow for slow playback
  701. changePlaybackSpeed(HOLD_LEFT_PLAYBACK_SPEED);
  702. }
  703. }
  704. }, LONG_PRESS_THRESHOLD);
  705. }
  706. }
  707. // Handle up/down arrow keys on progress bar
  708. else if ((event.key === 'ArrowUp' || event.key === 'ArrowDown') && isProgressBarFocused()) {
  709. // Prevent default to avoid any time change
  710. event.preventDefault();
  711. event.stopPropagation();
  712.  
  713. // Handle volume control properly by fixing focus and dispatching a new event
  714. handleVolumeKeyOnProgressBar(event.key === 'ArrowUp');
  715. }
  716. };
  717.  
  718. // Main handler for key up events
  719. const handleKeyUp = function(event) {
  720. // Skip if we're in an input field
  721. if (isInputField(document.activeElement)) return;
  722.  
  723. if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
  724. // Prevent default
  725. event.preventDefault();
  726. event.stopPropagation();
  727.  
  728. // Only process if this is the active key (prevents issues with multiple keys)
  729. if (event.key === activeKey) {
  730. // Clear the timeout to prevent speed change activation if key was released quickly
  731. if (longPressTimeout) {
  732. clearTimeout(longPressTimeout);
  733. longPressTimeout = null;
  734. }
  735.  
  736. // If this was a long press, reset playback speed
  737. if (isLongPress) {
  738. resetPlaybackSpeed();
  739. } else if (keyAlreadyDown) {
  740. // This was a quick tap, perform a seek
  741. performSeek(event.key === 'ArrowRight');
  742. }
  743.  
  744. // Reset tracking variables
  745. isLongPress = false;
  746. keyDownTime = 0;
  747. keyAlreadyDown = false;
  748. activeKey = null;
  749. }
  750. }
  751. };
  752.  
  753. // --- Setup Functions ---
  754.  
  755. // More comprehensive event handling
  756. function setupGlobalEventHandlers() {
  757. // Capture all keyboard events at the window level
  758. window.addEventListener('keydown', (e) => {
  759. // Skip if we're in an input field to allow normal typing
  760. if (isInputField(document.activeElement)) {
  761. return;
  762. }
  763.  
  764. if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
  765. // Always handle left/right arrows
  766. handleKeyDown(e);
  767. // Always prevent propagation
  768. e.stopPropagation();
  769. e.preventDefault();
  770. } else if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isProgressBarFocused()) {
  771. // For up/down, handle and prevent default when progress bar is focused
  772. handleKeyDown(e);
  773. e.stopPropagation();
  774. e.preventDefault();
  775. }
  776. }, true);
  777.  
  778. window.addEventListener('keyup', (e) => {
  779. // Skip if we're in an input field
  780. if (isInputField(document.activeElement)) {
  781. return;
  782. }
  783.  
  784. if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
  785. // Call our handler first
  786. handleKeyUp(e);
  787. // Always prevent propagation to YouTube
  788. e.stopPropagation();
  789. e.preventDefault();
  790. }
  791. }, true);
  792. }
  793.  
  794. // Additional handler for the YouTube player specifically
  795. function addYouTubePlayerHandlers() {
  796. const player = findYouTubePlayer();
  797. if (player) {
  798. // Create our indicators now that we have the player
  799. createSpeedIndicator();
  800. createActionIndicator();
  801.  
  802. // Additional direct event listeners for the player
  803. player.addEventListener('keydown', function(e) {
  804. if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
  805. e.preventDefault();
  806. e.stopPropagation();
  807. } else if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isProgressBarFocused()) {
  808. // For up/down on progress bar, handle and prevent
  809. e.preventDefault();
  810. e.stopPropagation();
  811. handleVolumeKeyOnProgressBar(e.key === 'ArrowUp');
  812. }
  813. }, true);
  814.  
  815. // Try to restore video position
  816. setTimeout(() => {
  817. restoreVideoPosition();
  818. startPositionTracking();
  819. }, 1500); // Give YouTube a moment to initialize the video
  820. }
  821. }
  822.  
  823. // Function to handle YouTube video element being added or replaced
  824. function handleVideoElementChange() {
  825. const video = findYouTubeVideo();
  826. if (video) {
  827. // Try to restore video position
  828. setTimeout(() => {
  829. restoreVideoPosition();
  830. startPositionTracking();
  831. }, 1500); // Give YouTube a moment to initialize the video
  832. }
  833. }
  834.  
  835. // Setup functions that will need to be called once the page is loaded
  836. function setupOnLoad() {
  837. // Set up the global event handlers
  838. setupGlobalEventHandlers();
  839.  
  840. // Add player-specific handlers
  841. addYouTubePlayerHandlers();
  842.  
  843. // Also try to intercept YouTube's internal keyboard event handling
  844. const originalDocKeyDown = document.onkeydown;
  845. const originalDocKeyUp = document.onkeyup;
  846.  
  847. document.onkeydown = function(e) {
  848. // Skip if we're in an input field
  849. if (isInputField(document.activeElement)) {
  850. return originalDocKeyDown ? originalDocKeyDown(e) : true;
  851. }
  852.  
  853. if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
  854. handleKeyDown(e);
  855. return false; // Prevent default
  856. } else if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isProgressBarFocused()) {
  857. // Completely prevent default when progress bar is focused
  858. handleKeyDown(e);
  859. return false;
  860. }
  861. return originalDocKeyDown ? originalDocKeyDown(e) : true;
  862. };
  863.  
  864. document.onkeyup = function(e) {
  865. // Skip if we're in an input field
  866. if (isInputField(document.activeElement)) {
  867. return originalDocKeyUp ? originalDocKeyUp(e) : true;
  868. }
  869.  
  870. if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
  871. handleKeyUp(e);
  872. return false; // Prevent default
  873. }
  874. return originalDocKeyUp ? originalDocKeyUp(e) : true;
  875. };
  876. }
  877.  
  878. // Watch for DOM changes to catch the player element if it loads after the script
  879. const observer = new MutationObserver(function(mutations) {
  880. // Look for the player
  881. if (!document.querySelector('#movie_player')) {
  882. return;
  883. }
  884.  
  885. // Check if we need to set up the player
  886. if (!speedIndicator) {
  887. addYouTubePlayerHandlers();
  888. }
  889.  
  890. // Also watch for video element changes
  891. const video = findYouTubeVideo();
  892. if (video && currentVideoId != getVideoId()) {
  893. handleVideoElementChange();
  894. }
  895. });
  896.  
  897. // Start observing the document
  898. observer.observe(document, { childList: true, subtree: true });
  899.  
  900. // Listen for navigation events (YouTube is a SPA)
  901. function handleNavigation() {
  902. // Check for video ID change
  903. const newVideoId = getVideoId();
  904. const currentId = currentVideoId;
  905.  
  906. if (newVideoId && newVideoId !== currentId) {
  907. debugLog('Video ID changed from', currentId, 'to', newVideoId);
  908. currentVideoId = newVideoId;
  909.  
  910. // Clear existing tracking
  911. if (positionSaveInterval) {
  912. clearInterval(positionSaveInterval);
  913. positionSaveInterval = null;
  914. }
  915.  
  916. // Add to history
  917. addToVideoHistory(newVideoId);
  918.  
  919. // Handle the video element for the new page
  920. handleVideoElementChange();
  921. }
  922. }
  923.  
  924. // YouTube uses History API for navigation
  925. const originalPushState = history.pushState;
  926. history.pushState = function() {
  927. originalPushState.apply(this, arguments);
  928. handleNavigation();
  929. };
  930.  
  931. window.addEventListener('popstate', handleNavigation);
  932.  
  933. // Call setup once the page is fully loaded
  934. if (document.readyState === 'complete') {
  935. setupOnLoad();
  936. } else {
  937. window.addEventListener('load', setupOnLoad);
  938. }
  939.  
  940. // Clean up when the page unloads
  941. window.addEventListener('unload', function() {
  942. // Save final position before unloading
  943. saveVideoPosition();
  944.  
  945. removeSpeedIndicator();
  946. if (actionIndicator && actionIndicator.parentNode) {
  947. actionIndicator.parentNode.removeChild(actionIndicator);
  948. }
  949. observer.disconnect();
  950. if (longPressTimeout) {
  951. clearTimeout(longPressTimeout);
  952. }
  953. if (hideActionTimeout) {
  954. clearTimeout(hideActionTimeout);
  955. }
  956. if (positionSaveInterval) {
  957. clearInterval(positionSaveInterval);
  958. }
  959. });
  960. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址