在线视频外挂字幕

目前支持B站,爱奇艺,优酷,百度网盘(百度网盘由于chrome有closed shadow dom暂时只能在firefox里使用),按Q键+100ms,按W键-100ms,按E键显示/隐藏字幕,console可作为transcript使用

  1. // ==UserScript==
  2. // @name 在线视频外挂字幕
  3. // @namespace https://truework.top
  4. // @version 0.44
  5. // @description 目前支持B站,爱奇艺,优酷,百度网盘(百度网盘由于chrome有closed shadow dom暂时只能在firefox里使用),按Q键+100ms,按W键-100ms,按E键显示/隐藏字幕,console可作为transcript使用
  6. // @author cyj98
  7. // @match https://www.bilibili.com/bangumi/*
  8. // @match https://www.iqiyi.com/*
  9. // @match https://www.ixigua.com/cinema/album/*
  10. // @match https://v.youku.com/*
  11. // @match https://pan.baidu.com/play/*
  12. // @require https://gf.qytechs.cn/scripts/373379-subtitle-utils-module/code/subtitle%20utils%20module.js?version=637875
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17. let fileInput = document.createElement("input")
  18. fileInput.type = 'file'
  19. document.body.prepend(fileInput)
  20.  
  21. // after first file, this variable always true
  22. let firstFile = false
  23. let newFile = false
  24.  
  25. const hostname = window.location.hostname
  26. let curTimeElem, subtitlePosElem, subtitleElem, snackbarElem
  27. let observer, reader
  28.  
  29. // keep settings after reload
  30. let isShowSubtitle = true
  31. let delayTime = 0
  32.  
  33. fileInput.onchange = (e) => {
  34. const file = e.target.files[0];
  35. if (!file) {
  36. return;
  37. }
  38.  
  39. if (firstFile === true) {
  40. newFile = true
  41. observer.disconnect()
  42. reader.abort()
  43. } else {
  44. const subtitlePosObj = { 'www.bilibili.com': 'subtitle-position', 'www.iqiyi.com': 'iqp-subtitle', 'www.ixigua.com': 'teleplay__playerContainer', 'v.youku.com': 'subtitle-container', 'pan.baidu.com': 'vjs-text-track-display' }
  45. const curTimeObj = { 'www.bilibili.com': 'bilibili-player-video-time-now', 'www.iqiyi.com': 'iqp-time-cur', 'www.ixigua.com': 'xgplayer-time', 'v.youku.com': 'control-time-current', 'pan.baidu.com': 'vjs-current-time' }
  46.  
  47. subtitlePosElem = document.getElementsByClassName(subtitlePosObj[hostname])[0]
  48. curTimeElem = document.getElementsByClassName(curTimeObj[hostname])[0]
  49.  
  50. subtitleElem = document.createElement('div')
  51. subtitleElem.id = 'custom-subtitle'
  52. subtitleElem.style.cssText = 'text-align:center; font-size: 32px; font-weight: bold; text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;'
  53. snackbarElem = document.createElement('div')
  54. snackbarElem.id = 'snackbar'
  55. snackbarElem.style.cssText = 'font-size: 16px; position: absolute; text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;'
  56.  
  57. if (hostname === 'www.ixigua.com' || hostname === 'pan.baidu.com') {
  58. snackbarElem.style.cssText += "position: absolute; z-index: 1; color: white; bottom: 56px;"
  59. subtitleElem.style.cssText += "position: absolute; z-index: 1; color: white; bottom: 0; left: 50%; -webkit-transform: translateX(-50%); transform: translateX(-50%);"
  60. document.addEventListener('fullscreenchange', () => {
  61. subtitlePosElem.prepend(subtitleElem)
  62. subtitlePosElem.prepend(snackbarElem)
  63. }, false)
  64. }
  65.  
  66. // console.log(subtitlePosElem, curTimeElem)
  67. let isShowDelay = false
  68. const logKey = (e) => {
  69. const key = e.key.toUpperCase()
  70. if (key === 'Q' || key === 'W') {
  71. if (key === 'Q') delayTime += 100
  72. if (key === 'W') delayTime -= 100
  73.  
  74. snackbarElem.innerHTML = 'delaytime: ' + delayTime
  75. if (isShowDelay) {
  76. return
  77. }
  78. snackbarElem.style.visibility = 'visible'
  79. isShowDelay = true
  80. setTimeout(() => {
  81. snackbarElem.style.visibility = 'hidden'
  82. isShowDelay = false
  83. }, 2000);
  84. } else if (key === 'E') {
  85. isShowSubtitle = !isShowSubtitle
  86. if (!isShowSubtitle) {
  87. subtitleElem.style.visibility = 'hidden'
  88. console.log("hide subtitle")
  89. } else {
  90. subtitleElem.style.visibility = 'visible'
  91. console.log("show subtitle")
  92. }
  93. }
  94. };
  95. document.addEventListener('keypress', logKey);
  96. }
  97. firstFile = true
  98.  
  99. subtitlePosElem.prepend(subtitleElem)
  100. subtitlePosElem.prepend(snackbarElem)
  101.  
  102. if (hostname === 'www.iqiyi.com') {
  103. subtitlePosElem.style.cssText += "display: block; height: 50px;"
  104. }
  105.  
  106. reader = new FileReader();
  107. reader.readAsText(file)
  108. reader.onload = (e) => {
  109. let subtitles
  110. try {
  111. subtitles = window.Subtitle.parse(e.target.result)
  112. } catch (e) {
  113. alert("字幕解析出现问题");
  114. }
  115.  
  116. let prevPos = -2
  117. const binarySearch = (target, arr) => {
  118. let start = 0;
  119. let end = arr.length - 1;
  120. while (start <= end) {
  121. const mid = parseInt(start + (end - start) / 2);
  122. if (target >= arr[mid].start && target <= arr[mid].end) {
  123. return mid;
  124. } else if (target > arr[mid].end) {
  125. start = mid + 1;
  126. } else {
  127. end = mid - 1;
  128. }
  129. }
  130. return -1;
  131. }
  132. const callback = (_, observer) => {
  133. if (newFile === true) {
  134. newFile = false
  135. subtitleElem.innerHTML = ''
  136. observer.disconnect()
  137. return
  138. }
  139. let strTime
  140. if (hostname === 'www.ixigua.com') {
  141. strTime = curTimeElem.firstElementChild.firstElementChild.innerHTML;
  142. } else if (hostname === 'pan.baidu.com') {
  143. strTime = curTimeElem.firstElementChild.innerHTML;
  144. strTime = strTime.replace(/<span[^>]*>([^<]+)<\/span>/g, '$1');
  145. strTime = strTime.replace("Current Time ", "")
  146. // console.log(strTime)
  147. } else {
  148. strTime = curTimeElem.innerHTML
  149. }
  150. console.log(strTime)
  151. if (strTime.length <= 5) {
  152. strTime = "00:" + strTime + ",000"
  153. } else {
  154. strTime = strTime + ",000"
  155. }
  156. const time = window.Subtitle.toMS(strTime);
  157.  
  158. const pos = binarySearch(time + delayTime, subtitles)
  159. if (pos === -1) {
  160. prevPos = -2
  161. subtitleElem.style.visibility = 'hidden'
  162. return;
  163. }
  164. if (pos === prevPos) {
  165. prevPos = pos
  166. return
  167. }
  168. console.log(subtitles[pos].text);
  169. if (isShowSubtitle) {
  170. subtitleElem.style.visibility = 'visible'
  171. }
  172. subtitleElem.innerHTML = subtitles[pos].text
  173. window.subtitleCount += 1
  174. prevPos = pos
  175. };
  176.  
  177. observer = new MutationObserver(callback);
  178. const config = {
  179. attributes: true,
  180. childList: true,
  181. subtree: true
  182. };
  183. observer.observe(curTimeElem, config);
  184. }
  185. }
  186. })();

QingJ © 2025

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