YouTube - 剩余时间指示器

在视频时长旁边显示YouTube视频的剩余时长,考虑播放速度。

当前为 2025-05-29 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube - Remaining Time Indicator
  3. // @name:fr YouTube - Indicateur du temps restant
  4. // @name:es YouTube - Indicador de tiempo restante
  5. // @name:de YouTube - Anzeige der verbleibenden Zeit
  6. // @name:it YouTube - Indicatore del tempo rimanente
  7. // @name:zh-CN YouTube - 剩余时间指示器
  8. // @namespace https://gist.github.com/4lrick/cf14cf267684f06c1b7bc559ddf2b943
  9. // @version 2.1
  10. // @description Displays the remaining duration of a YouTube video next to the video duration, taking into account the playback rate.
  11. // @description:fr Affiche la durée restante d'une vidéo YouTube à côté de la durée de la vidéo, en tenant compte de la vitesse de lecture.
  12. // @description:es Muestra la duración restante de un video de YouTube junto a la duración del video, teniendo en cuenta la velocidad de reproducción.
  13. // @description:de Zeigt die verbleibende Dauer eines YouTube-Videos neben der Videodauer an und berücksichtigt dabei die Wiedergabegeschwindigkeit.
  14. // @description:it Mostra la durata rimanente di un video di YouTube accanto alla durata del video, tenendo conto della velocità di riproduzione.
  15. // @description:zh-CN 在视频时长旁边显示YouTube视频的剩余时长,考虑播放速度。
  16. // @author 4lrick
  17. // @match https://www.youtube.com/*
  18. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  19. // @grant none
  20. // @license GPL-3.0-only
  21. // ==/UserScript==
  22.  
  23. (function() {
  24. 'use strict';
  25. let timeDisplay;
  26. let lastPlaybackRate = null;
  27. let lastRenderedText = '';
  28. let lastCurrentTime = null;
  29. let showEndTime = localStorage.getItem('yt-player-remaining-time-mode') === 'true';
  30.  
  31. function createTimeDisplayElement() {
  32. const timeDisplayElement = document.createElement('span');
  33.  
  34. timeDisplayElement.style.display = 'inline-block';
  35. timeDisplayElement.style.marginLeft = '10px';
  36. timeDisplayElement.style.color = '#ddd';
  37. timeDisplayElement.style.cursor = 'pointer';
  38. timeDisplayElement.title = 'Click to toggle between remaining time and end time';
  39.  
  40. timeDisplayElement.addEventListener('click', () => {
  41. showEndTime = !showEndTime;
  42. localStorage.setItem('yt-player-remaining-time-mode', showEndTime);
  43. });
  44.  
  45. return timeDisplayElement;
  46. }
  47.  
  48. function formatTimeDisplay(videoElement) {
  49. if (showEndTime) {
  50. const endTime = new Date(Date.now() + (videoElement.duration - videoElement.currentTime) * 1000 / videoElement.playbackRate);
  51. return `(${endTime.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })})`;
  52. } else {
  53. const timeRemaining = (videoElement.duration - videoElement.currentTime) / videoElement.playbackRate;
  54. const hours = Math.floor(timeRemaining / 3600);
  55. const minutes = Math.floor((timeRemaining % 3600) / 60);
  56. const seconds = Math.floor(timeRemaining % 60);
  57. return `(${hours > 0 ? `${hours}:` : ''}${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')})`;
  58. }
  59. }
  60.  
  61. function displayRemainingTime() {
  62. const videoElement = document.querySelector('video');
  63. const isLive = document.querySelector('.ytp-time-display')?.classList.contains('ytp-live');
  64. const miniplayerUI = document.querySelector('.ytp-miniplayer-ui');
  65. const isMiniplayerVisible = miniplayerUI && getComputedStyle(miniplayerUI).display !== 'none';
  66. const currentTime = videoElement?.currentTime;
  67. const timeContainer = document.querySelector(
  68. isMiniplayerVisible ? '.ytp-miniplayer-ui .ytp-time-contents' : '.ytp-chrome-controls .ytp-time-contents'
  69. );
  70. const shouldUpdate =
  71. videoElement &&
  72. !isLive &&
  73. (
  74. !videoElement.paused ||
  75. videoElement.playbackRate !== lastPlaybackRate ||
  76. currentTime !== lastCurrentTime ||
  77. showEndTime != !showEndTime
  78. );
  79.  
  80. if (!videoElement || isLive || !timeContainer) {
  81. if (timeDisplay) {
  82. timeDisplay.remove();
  83. timeDisplay = null;
  84. }
  85. requestAnimationFrame(displayRemainingTime);
  86. return;
  87. }
  88.  
  89. if (!timeDisplay) {
  90. timeDisplay = createTimeDisplayElement();
  91. timeContainer.appendChild(timeDisplay);
  92. }
  93.  
  94. if (!timeContainer.contains(timeDisplay)) {
  95. timeDisplay.remove();
  96. timeContainer.appendChild(timeDisplay);
  97. }
  98.  
  99. if (shouldUpdate) {
  100. const text = formatTimeDisplay(videoElement);
  101.  
  102. if (text !== lastRenderedText) {
  103. timeDisplay.textContent = text;
  104. lastRenderedText = text;
  105. }
  106.  
  107. lastPlaybackRate = videoElement.playbackRate;
  108. lastCurrentTime = currentTime;
  109. }
  110.  
  111. requestAnimationFrame(displayRemainingTime);
  112. }
  113.  
  114. function initRemainingCounter() {
  115. const timeContainer = document.querySelector('.ytp-time-contents');
  116.  
  117. if (timeContainer) {
  118. timeDisplay = createTimeDisplayElement();
  119. timeContainer.appendChild(timeDisplay);
  120. requestAnimationFrame(displayRemainingTime);
  121. observer.disconnect();
  122. }
  123. }
  124.  
  125. function checkVideoExists() {
  126. const videoElement = document.querySelector('video');
  127.  
  128. if (videoElement) {
  129. initRemainingCounter();
  130. }
  131. }
  132.  
  133. const observer = new MutationObserver(checkVideoExists);
  134. observer.observe(document.body, { childList: true, subtree: true });
  135. })();

QingJ © 2025

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