Youtube subtitles under video frame

Have you ever been annoyed by youtube subtitles covering some important part of the video? No more! The userscript moves subtitles under video frame (but you can still drag-move them horizontally). It works for default and theater modes.

安装此脚本?
作者推荐脚本

您可能也喜欢Youtube sticky Show Less button

安装此脚本
  1. /*
  2. Youtube subtitles under video frame: Move youtube subtitles under video frame.
  3. Copyright (C) 2023 T1mL3arn
  4.  
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13.  
  14. You should have received a copy of the GNU General Public License
  15. along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. */
  17.  
  18. // ==UserScript==
  19. // @name Youtube subtitles under video frame
  20. // @name:RU Субтитры Youtube под видео
  21. // @description Have you ever been annoyed by youtube subtitles covering some important part of the video? No more! The userscript moves subtitles under video frame (but you can still drag-move them horizontally). It works for default and theater modes.
  22. // @description:RU Вам когда-нибудь мешали субтитры Youtube, закрывыющие какую-то важную область видео? Пора это прекратить! Этот скрипт сдвигает субтитры под видео (вы все еще можете перетаскивать их по горизонтали). Работает в режимах "обычный" и "широкий экран".
  23. // @namespace https://github.com/t1ml3arn-userscript-js
  24. // @version 1.5.1
  25. // @match https://www.youtube.com/*
  26. // @match https://youtube.com/*
  27. // @grant none
  28. // @noframes
  29. // @author T1mL3arn
  30. // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGRlZnM+PHN5bWJvbCBpZD0iYSIgdmlld0JveD0iMCAwIDM0NCA1OSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHJlY3QgZmlsbD0icmdiYSgyNTUsIDI1NSwgMjU1LCAwKSIgaGVpZ2h0PSIxMDAlIiB3aWR0aD0iMTAwJSIvPjxwYXRoIGQ9Ik0tNTAtNTBINTBWNTBILTUwVi01MHoiIGZpbGw9IiNGRkYiIGZpbGwtb3BhY2l0eT0iMCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNTAgNTApIi8+PHBhdGggZD0iTS01MC01MEg1MFY1MEgtNTBWLTUweiIgZmlsbD0iI0ZGRiIgZmlsbC1vcGFjaXR5PSIwIiB0cmFuc2Zvcm09Im1hdHJpeCguNDUgMCAwIC43NiAxNTYuNDEgMzY5Ljc1KSIvPjxwYXRoIGQ9Ik0tNTAtNTBWNTBINTBWLTUwSC01MHoiIGZpbGw9IiNGRkYiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIwIiB0cmFuc2Zvcm09Im1hdHJpeCgyLjQxIDAgMCAuMiAxMjAuNSAxMCkiLz48cGF0aCBkPSJNLTUwLTUwVjUwSDUwVi01MEgtNTB6IiBmaWxsPSIjRkZGIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMCIgdHJhbnNmb3JtPSJtYXRyaXgoLjcgMCAwIC4yIDMxMC4xOCAxMCkiLz48cGF0aCBkPSJNLTUwLTUwVjUwSDUwVi01MEgtNTB6IiBmaWxsPSIjRkZGIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMCIgdHJhbnNmb3JtPSJtYXRyaXgoLTIuNDEgMCAwIC0uMiAyMjQuNjggNTApIi8+PHBhdGggZD0iTS01MC01MFY1MEg1MFYtNTBILTUweiIgZmlsbD0iI0ZGRiIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjAiIHRyYW5zZm9ybT0ibWF0cml4KC0uNyAwIDAgLS4yIDM1IDUwKSIvPjwvc3ltYm9sPjwvZGVmcz48ZyBjbGFzcz0ibGF5ZXIiPjxwYXRoIGQ9Ik00OTguMjk0IDU3LjI5OWMtNS44MjgtMjEuNzc2LTIyLjk0Mi0zOC44ODgtNDQuNzE4LTQ0LjcxN0M0MTQuMTUgMi4wMDYgMjU1Ljk3MiAyLjAwNiAyNTUuOTcyIDIuMDA2cy0xNTguMTc0IDAtMTk3LjYwMyAxMC41NzZDMzYuNTkzIDE4LjQxIDE5LjQ4IDM1LjUyMyAxMy42NTIgNTcuMjk5IDMuMDc2IDk2LjcyOCAzLjA3NiAxNzkuMDQyIDMuMDc2IDE3OS4wNDJzMCA4Mi4zMTUgMTAuNTc2IDEyMS43NDRjNS44MjkgMjEuNzc2IDIyLjk0MSAzOC44ODggNDQuNzE3IDQ0LjcxNiAzOS40MjkgMTAuNTc2IDE5Ny42MDMgMTAuNTc2IDE5Ny42MDMgMTAuNTc2czE1OC4xNzcgMCAxOTcuNjA0LTEwLjU3NmMyMS43NzYtNS44MjggMzguODktMjIuOTQgNDQuNzE4LTQ0LjcxNiAxMC41NzYtMzkuNDMgMTAuNTc2LTEyMS43NDQgMTAuNTc2LTEyMS43NDRzLS4wNDItODIuMzE0LTEwLjU3Ni0xMjEuNzQzeiIgZmlsbD0icmVkIi8+PHBhdGggZD0iTTAgLjVoNTEydjUxMkgwVi41eiIgZmlsbD0idHJhbnNwYXJlbnQiLz48cGF0aCBkPSJNNDMuNzg4IDM3MWg0MjQuNDI3djEzMS4zMkg0My43ODhWMzcxeiIvPjx1c2UgdHJhbnNmb3JtPSJtYXRyaXgoLjczNjA4IDAgMCAuOTYyMTcgLTc1Ljk5IC00ODUuMDY1KSIgeD0iMTk0LjE0NCIgeGxpbms6aHJlZj0iI2EiIHk9IjcwMiIvPjxwYXRoIGQ9Im0yMDUuMzQ1IDI1NS4wODYgMTMxLjQwNC03NS44Ni0xMzEuNDA0LTc1Ljg2djE1MS43MnoiIGZpbGw9IiNGRkYiLz48L2c+PC9zdmc+
  31. // @homepageURL https://github.com/t1ml3arn-userscript-js/Youtube-subtitles-under-video-frame
  32. // @supportURL https://github.com/t1ml3arn-userscript-js/Youtube-subtitles-under-video-frame/issues
  33. // @license GPL-3.0-or-later
  34. // ==/UserScript==
  35.  
  36. const SUBS_BUTTON_SELECTOR = '.ytp-subtitles-button'
  37. const USERJS_ELT_CLASS = 'yfms-userjs'
  38. const USERJS_STYLE_ID = 'youtube-subs-under-video-css'
  39. const PLAYER_ELT_SELECTOR = 'ytd-watch-flexy'
  40. const SUBS_GAP = 64;
  41. const SUBS_GAP_THEATER = 100;
  42. const KEY__PLAYER_CAPTION_DISPLAY_SETTINGS = 'yt-player-caption-display-settings'
  43. const KEY__PLAYER_STICKY_CAPTION = 'yt-player-sticky-caption'
  44.  
  45. const USERJS_STYLE_CONTENT = `
  46. .${USERJS_ELT_CLASS} {
  47. --subs-gap: ${SUBS_GAP}px;
  48. --subs-gap-theater: ${SUBS_GAP_THEATER}px;
  49. }
  50.  
  51. .${USERJS_ELT_CLASS}:not([fullscreen]) .caption-window.ytp-caption-window-bottom {
  52. margin-bottom: 0 !important;
  53. margin-top: 0 !important;
  54. position: absolute !important;
  55. bottom: 0 !important;
  56. top: calc(100% + 16px) !important;
  57. z-index: 9999 !important;
  58. }
  59.  
  60. ytd-player:not([fullscreen]):not([theater]) {
  61. /* in default mode "ytd-player" has "overflow: hidden" thus hiding the subs,
  62. this rule makes it visible again */
  63. overflow: visible !important;
  64. }
  65.  
  66. .${USERJS_ELT_CLASS} .html5-video-player {
  67. /* to make subs visible when they are outside player frame */
  68. overflow: visible;
  69. /* to make player to be on top
  70. (combined with captions z-index rule,
  71. it places captions over any element on the page) */
  72. z-index: 999;
  73. }
  74.  
  75. .${USERJS_ELT_CLASS} #movie_player.ended-mode .html5-video-container,
  76. .${USERJS_ELT_CLASS} #movie_player.unstarted-mode .html5-video-container {
  77. /* By default this container has no height,
  78. setting height explicitly prevents hiding of video.
  79. This actually not needed since "overflow: hidden" happens
  80. before start and after end, but I want to be sure. */
  81. height: 100%;
  82. /* video frame move above when video ends, without hiding
  83. a user can see part of the video above player */
  84. overflow: hidden;
  85. }
  86.  
  87. .${USERJS_ELT_CLASS} #below {
  88. margin-top: var(--subs-gap);
  89. transition: margin-top 0.25s;
  90. }
  91.  
  92. .${USERJS_ELT_CLASS}[theater]:not([fullscreen]) #below {
  93. margin-top: var(--subs-gap-theater);
  94. }
  95.  
  96. /* styling for "related videos" section */
  97. .${USERJS_ELT_CLASS}[theater]:not([fullscreen]) #secondary.ytd-watch-flexy {
  98. margin-top: var(--subs-gap-theater);
  99. transition: margin-top 0.25s;
  100. }
  101. `
  102.  
  103. let canToggleSubsWithKeyboard = true;
  104. const ccSizes = {
  105. 0: 64,
  106. 1: 80,
  107. }
  108.  
  109. function addStyles(css, id) {
  110. const style = document.head.appendChild(document.createElement('style'))
  111. style.textContent = css;
  112. style.id = id
  113. }
  114.  
  115. function displaceSubtitles(below = true) {
  116. // this elt gets special attribute by youtube when view mode changes,
  117. // so it also gets my marker class to apply my CSS
  118. const playerElt = document.querySelector(PLAYER_ELT_SELECTOR)
  119. if (below)
  120. playerElt.classList.add(USERJS_ELT_CLASS)
  121. else
  122. playerElt.classList.remove(USERJS_ELT_CLASS)
  123. }
  124.  
  125. function onSubsClick() {
  126. displaceSubtitles(areSubsEnabled())
  127. }
  128.  
  129. function getCaptionsButton() {
  130. return getVisibleElt(SUBS_BUTTON_SELECTOR)
  131. }
  132.  
  133. function getVisibleElt(selector) {
  134. return Array.from(document.querySelectorAll(selector)).find(e => e.offsetParent !==null)
  135. }
  136.  
  137. function isItVideoPage() {
  138. return window.location.search.includes('v=')
  139. }
  140.  
  141. function areSubsAvailable() {
  142. const subsButton = getCaptionsButton()
  143.  
  144. if (!subsButton) {
  145. console.debug(`Video ${window.location.href} has no subtitles button`);
  146. return false
  147. }
  148.  
  149. // Video may have no subs at all, to catch that case
  150. // I can only check button's opacity
  151. const btnIcon = subsButton.querySelector('svg')
  152. if (parseFloat(btnIcon.getAttribute("fill-opacity") || 1) != 1)
  153. return false
  154.  
  155. return true
  156. }
  157.  
  158. function toggleSubtitlesKeyDown(e) {
  159. if (e.code === 'KeyC' || e.keyCode === 67)
  160. if (isItVideoPage() && areSubsAvailable() && canToggleSubsWithKeyboard) {
  161. displaceSubtitles(areSubsEnabled())
  162. }
  163. }
  164.  
  165. function onFocusIn(e) {
  166.  
  167. // disable captions toggling
  168. // if user focused any input element (like search bar or comment textarea)
  169. if (e.target.tagName === 'INPUT' ||
  170. e.target.matches('div#contenteditable-root.style-scope.yt-formatted-string')) {
  171. canToggleSubsWithKeyboard = false
  172. } else {
  173. canToggleSubsWithKeyboard = true
  174. }
  175. }
  176.  
  177. function onFocusOut(e) {
  178.  
  179. // restoring captions toggling if user focused out
  180. // input elements
  181. if (e.target.tagName === 'INPUT' ||
  182. e.target.matches('div#contenteditable-root.style-scope.yt-formatted-string')) {
  183. canToggleSubsWithKeyboard = true
  184. }
  185. }
  186.  
  187. function enchanceSubsButton() {
  188. if (isItVideoPage()) {
  189.  
  190. if (!areSubsAvailable())
  191. return
  192.  
  193. const subsButton = getCaptionsButton()
  194.  
  195. let subsEnabled = areSubsEnabled()
  196. // sometimes I cannot rely on local storage setting
  197. // to get subtitles state but I still can get it
  198. // from the caption button ARIA attribute
  199. let subsButtonPressed = subsButton.getAttribute('aria-pressed') === 'true'
  200. displaceSubtitles(subsEnabled || subsButtonPressed)
  201. // forcing YT to enable subs
  202. if (subsEnabled && !subsButtonPressed) {
  203. subsButton.click()
  204. }
  205.  
  206. updateGapSize()
  207.  
  208. document.addEventListener('keydown', toggleSubtitlesKeyDown )
  209. subsButton.addEventListener('click', onSubsClick)
  210. }
  211. }
  212.  
  213. function localStorageHook() {
  214. let original = Storage.prototype.setItem;
  215. Storage.prototype.setItem = function() {
  216. const event = new CustomEvent('storageSetItem', {
  217. detail: {
  218. key: arguments[0],
  219. value: arguments[1]
  220. },
  221. });
  222. original.apply(this, arguments);
  223. window.dispatchEvent(event);
  224. }
  225. }
  226.  
  227. function getGapSize(f, initial) {
  228. // this formula is found in youtube js code
  229. return initial * (1 + 0.25 * Math.max(f || 0, 0));
  230. }
  231.  
  232. function updateGapSize() {
  233. // NOTE CC visible size remains FIXED even if a user changes
  234. // zoom level in his browser!
  235.  
  236. const raw = localStorage.getItem(KEY__PLAYER_CAPTION_DISPLAY_SETTINGS)
  237. let ccDisplaySettings;
  238. try {
  239. ccDisplaySettings = JSON.parse(JSON.parse(raw).data)
  240. } catch(e) {
  241. ccDisplaySettings = {}
  242. }
  243. const fontSizeIncrement = ccDisplaySettings.fontSizeIncrement || 0
  244. let newGap = getGapSize(fontSizeIncrement, SUBS_GAP);
  245. document.querySelector(PLAYER_ELT_SELECTOR).style.setProperty('--subs-gap', `${newGap}px`)
  246. newGap = getGapSize(fontSizeIncrement, SUBS_GAP_THEATER)
  247. document.querySelector(PLAYER_ELT_SELECTOR).style.setProperty('--subs-gap-theater', `${newGap}px`)
  248. }
  249.  
  250. /** Checks if local storage has a setting for
  251. * enabled/disabled subs.
  252. *
  253. * **NOTE**: This setting might not be in local storage even
  254. * if a registered YT user enabled subtitles globally
  255. * with YT settings.
  256. * @returns {Bool}
  257. */
  258. function areSubsEnabled() {
  259. const raw = localStorage.getItem(KEY__PLAYER_STICKY_CAPTION)
  260. try {
  261. return JSON.parse(JSON.parse(raw).data)
  262. } catch(e) {
  263. return false
  264. }
  265. }
  266.  
  267. function init() {
  268. addStyles(USERJS_STYLE_CONTENT, USERJS_STYLE_ID)
  269.  
  270. document.addEventListener('focusin', onFocusIn)
  271. document.addEventListener('focusout', onFocusOut)
  272.  
  273. // Hint about youtube-specific events was found there
  274. // https://stackoverflow.com/questions/34077641/how-to-detect-page-navigation-on-youtube-and-modify-its-appearance-seamlessly/34100952#34100952
  275. document.addEventListener('yt-navigate-finish', enchanceSubsButton)
  276. document.addEventListener('yt-page-data-updated', enchanceSubsButton)
  277.  
  278. localStorageHook();
  279.  
  280. window.addEventListener('storageSetItem', e => {
  281. const { key } = e.detail;
  282.  
  283. // console.log(`YT local storage "${key}" was updated`)
  284.  
  285. switch (key) {
  286. case KEY__PLAYER_CAPTION_DISPLAY_SETTINGS:
  287. updateGapSize()
  288. break;
  289.  
  290. case KEY__PLAYER_STICKY_CAPTION:
  291. // if sticky subs are enabled - displace subs
  292. displaceSubtitles(areSubsEnabled())
  293. break;
  294.  
  295. default:
  296. break;
  297. }
  298.  
  299. })
  300. }
  301.  
  302. init();

QingJ © 2025

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