🏠 Home 

Bilibili Video CDN Switcher

Modify Bilibili's CDN during playback to speed up video loading, supporting Animes & Videos


Install this script?
  1. // ==UserScript==
  2. // @name Bilibili Video CDN Switcher
  3. // @name:zh-CN Bilibili CDN切换
  4. // @name:zh-TW Bilibili CDN切換
  5. // @name:ja BilibiliビデオCDNスイッチャー
  6. // @name:en Bilibili Video CDN Switcher
  7. // @namespace mailto:1332019995@qq.com
  8. // @copyright Free For Personal Use
  9. // @license No License
  10. // @version 0.1.2
  11. // @description 修改哔哩哔哩播放时的所用CDN 加快视频加载 番剧加速 视频加速
  12. // @description:zh-CN 修改哔哩哔哩播放时的所用CDN 加快视频加载 番剧加速 视频加速
  13. // @description:en Modify Bilibili's CDN during playback to speed up video loading, supporting Animes & Videos
  14. // @description:zh-TW 修改 Bilibili 播放時的所用CDN 加快影片載入 番劇加速 影片加速
  15. // @description:ja ビリビリ動画(Bilibili)の動画再生時のCDNを変更して、動画読み込み速度の向上、アニメとビデオ読込高速化
  16. // @author 1332019995@qq.com
  17. // @run-at document-start
  18. // @match https://www.bilibili.com/video/*
  19. // @match https://www.bilibili.com/bangumi/play/*
  20. // @match https://www.bilibili.com/blackboard/*
  21. // @match https://live.bilibili.com/blanc/*
  22. // @match https://www.bilibili.com/?*
  23. // @match https://www.bilibili.com/
  24. // @match https://www.bilibili.com/mooc/*
  25. // @match https://www.bilibili.com/v/*
  26. // @match https://www.bilibili.com/documentary/*
  27. // @match https://www.bilibili.com/variety/*
  28. // @match https://www.bilibili.com/tv/*
  29. // @match https://www.bilibili.com/guochuang/*
  30. // @match https://www.bilibili.com/movie/*
  31. // @match https://www.bilibili.com/anime/*
  32. // @match https://www.bilibili.com/match/*
  33. // @match https://www.bilibili.com/cheese/*
  34. // @match https://music.bilibili.com/pc/music-center/*
  35. // @match https://search.bilibili.com/*
  36. // @match https://m.bilibili.com/video/*
  37. // @match https://m.bilibili.com/bangumi/play/*
  38. // @match https://m.bilibili.com/?*
  39. // @match https://m.bilibili.com/
  40. // @icon https://i0.hdslb.com/bfs/static/jinkela/long/images/512.png
  41. // @grant GM_getValue
  42. // @grant GM_setValue
  43. // @grant unsafeWindow
  44. // ==/UserScript==
  45. // 在这里的引号内输入自定义的CDN网址,设置为null可以禁用此配置 (Enter your custom CDN URL in quotes here. Setting this to null will disable this configuration)
  46. var CustomCDN = ''
  47. // 例如将上一行修改为如下,可以将CDN强制设置为 'upos-sz-mirrorali.bilivideo.com' (e.g. Modify the previous line as follows to force CDN to be set to 'upos-sz-mirrorali.bilivideo.com')
  48. // var CustomCDN = 'upos-sz-mirrorali.bilivideo.com'
  49. const PluginName = 'BiliCDNSwitcher'
  50. const log = console.log.bind(console, `[${PluginName}]:`)
  51. const Language = (() => {
  52. const lang = (navigator.language || navigator.browserLanguage || (navigator.languages || ["en"])[0]).substring(0, 2)
  53. return (lang === 'zh' || lang === 'ja') ? lang : 'en'
  54. })()
  55. let disabled = !!GM_getValue('disabled')
  56. const Replacement = (() => {
  57. const toURL = ((url) => { if (url.indexOf('://') === -1) url = 'https://' + url; return url.endsWith('/') ? url : `${url}/` })
  58. const stored = GM_getValue('CustomCDN')
  59. CustomCDN = CustomCDN === 'null' ? null : CustomCDN
  60. let domain
  61. if (CustomCDN && CustomCDN !== '') {
  62. domain = CustomCDN
  63. // Prevent custom CDNs from being disabled by update scripts
  64. if (CustomCDN !== stored) {
  65. GM_setValue('CustomCDN', domain)
  66. log('CustomCDN was saved to GM storage')
  67. }
  68. } else if (CustomCDN === null && stored !== null) {
  69. GM_setValue('CustomCDN', null)
  70. log('CustomCDN was deleted from GM storage')
  71. } else if (stored) {
  72. domain = stored
  73. }
  74. // Default Servers
  75. if (!domain) {domain = {
  76. 'zh': 'cn-jxnc-cmcc-bcache-06.bilivideo.com',
  77. 'en': 'upos-sz-mirroraliov.bilivideo.com',
  78. 'ja': 'upos-sz-mirroralib.bilivideo.com'
  79. }[Language]}
  80. log(`CDN=${domain}`)
  81. return toURL(domain)
  82. })()
  83. const SettingsBarTitle = {
  84. 'zh': '拦截修改视频CDN',
  85. 'en': 'CDN Switcher',
  86. 'ja': 'CDNスイッチャー'
  87. }[Language]
  88. const playInfoTransformer = playInfo => {
  89. const urlTransformer = i => {
  90. const newUrl = i.base_url.replace(
  91. /https:\/\/.*?\//,
  92. Replacement
  93. )
  94. i.baseUrl = newUrl; i.base_url = newUrl
  95. };
  96. const durlTransformer = i => { i.url = i.url.replace(/https:\/\/.*?\//, Replacement) };
  97. if (playInfo.code !== (void 0) && playInfo.code !== 0) {
  98. log('Failed to get playInfo, message:', playInfo.message)
  99. return
  100. }
  101. let video_info
  102. if (playInfo.r###lt) { // bangumi pages'
  103. video_info = playInfo.r###lt.dash === (void 0) ? playInfo.r###lt.video_info : playInfo.r###lt
  104. if (!video_info?.dash) {
  105. if (playInfo.r###lt.durl && playInfo.r###lt.durls) {
  106. video_info = playInfo.r###lt // documentary trail viewing, m.bilibili.com/bangumi/play/* trail or non-trail viewing
  107. } else {
  108. log('Failed to get video_info, limit_play_reason:', playInfo.r###lt.play_check?.limit_play_reason)
  109. }
  110. // durl & durls are for trial viewing, and they usually exist when limit_play_reason=PAY
  111. video_info?.durl?.forEach(durlTransformer)
  112. video_info?.durls?.forEach(durl => { durl.durl?.forEach(durlTransformer) })
  113. return
  114. }
  115. } else { // video pages'
  116. video_info = playInfo.data
  117. }
  118. try {
  119. video_info.dash.video.forEach(urlTransformer)
  120. video_info.dash.audio.forEach(urlTransformer)
  121. } catch (err) {
  122. if (video_info.durl) { // 充电专属视频、m.bilibili.com/video/*
  123. log('accept_description:', video_info.accept_description?.join(', '))
  124. video_info.durl.forEach(durlTransformer)
  125. } else {
  126. log('ERR:', err)
  127. }
  128. }
  129. return
  130. }
  131. // Network Request Interceptor
  132. const interceptNetResponse = (theWindow => {
  133. const interceptors = []
  134. const interceptNetResponse = (handler) => interceptors.push(handler)
  135. // when response === null && url is String, it's checking if the url is handleable
  136. const handleInterceptedResponse = (response, url) => interceptors.reduce((modified, handler) => {
  137. const ret = handler(modified, url)
  138. return ret ? ret : modified
  139. }, response)
  140. const OriginalXMLHttpRequest = theWindow.XMLHttpRequest
  141. class XMLHttpRequest extends OriginalXMLHttpRequest {
  142. get responseText() {
  143. if (this.readyState !== this.DONE) return super.responseText
  144. return handleInterceptedResponse(super.responseText, this.responseURL)
  145. }
  146. get response() {
  147. if (this.readyState !== this.DONE) return super.response
  148. return handleInterceptedResponse(super.response, this.responseURL)
  149. }
  150. }
  151. theWindow.XMLHttpRequest = XMLHttpRequest
  152. const OriginalFetch = fetch
  153. theWindow.fetch = (input, init) => (!handleInterceptedResponse(null, input) ? OriginalFetch(input, init) :
  154. OriginalFetch(input, init).then(response =>
  155. new Promise((resolve) => response.text()
  156. .then(text => resolve(new Response(handleInterceptedResponse(text, input), {
  157. status: response.status,
  158. statusText: response.statusText,
  159. headers: response.headers
  160. })))
  161. )
  162. )
  163. );
  164. return interceptNetResponse
  165. })(unsafeWindow)
  166. const waitForElm = (selector) => new Promise(resolve => {
  167. let ele = document.querySelector(selector)
  168. if (ele) return resolve(ele)
  169. const observer = new MutationObserver(mutations => {
  170. let ele = document.querySelector(selector)
  171. if (ele) {
  172. observer.disconnect()
  173. resolve(ele)
  174. }
  175. })
  176. observer.observe(document.documentElement, {
  177. childList: true,
  178. subtree: true
  179. })
  180. log('waitForElm, MutationObserver started.')
  181. })
  182. // Parse HTML string to DOM Element
  183. function fromHTML(html) {
  184. if (!html) throw Error('html cannot be null or undefined', html)
  185. const template = document.createElement('template')
  186. template.innerHTML = html
  187. const r###lt = template.content.children
  188. return r###lt.length === 1 ? r###lt[0] : r###lt
  189. }
  190. (function () {
  191. 'use strict';
  192. if (disabled) log('Plugin is Disabled');
  193. // Hook Bilibili PlayUrl Api
  194. interceptNetResponse((response, url) => {
  195. if (disabled) return
  196. if (url.startsWith('https://api.bilibili.com/x/player/wbi/playurl') ||
  197. url.startsWith('https://api.bilibili.com/pgc/player/web/v2/playurl') ||
  198. url.startsWith('https://api.bilibili.com/x/player/playurl') ||
  199. url.startsWith('https://api.bilibili.com/pgc/player/web/playurl') ||
  200. url.startsWith('https://api.bilibili.com/pugv/player/web/playurl') // at /cheese/
  201. ) {
  202. if (response === null) return true // the url is handleable
  203. log('(Intercepted) playurl api response.')
  204. const responseText = response
  205. const playInfo = JSON.parse(responseText)
  206. playInfoTransformer(playInfo)
  207. return JSON.stringify(playInfo)
  208. }
  209. });
  210. // Modify Pages playinfo
  211. if (location.host === 'm.bilibili.com') {
  212. const optionsTransformer = (opts) => (opts.readyVideoUrl = opts.readyVideoUrl?.replace(/https:\/\/.*?\//, Replacement))
  213. if (!disabled && unsafeWindow.options) { // Modify unsafeWindow.options
  214. log('Directly modify the window.options')
  215. optionsTransformer(unsafeWindow.options)
  216. } else {
  217. let internalOptions = unsafeWindow.options
  218. Object.defineProperty(unsafeWindow, 'options', {
  219. get: () => internalOptions,
  220. set: v => {
  221. if (!disabled) optionsTransformer(v);
  222. internalOptions = v
  223. }
  224. })
  225. }
  226. } else {
  227. if (!disabled && unsafeWindow.__playinfo__) { // Modify unsafeWindow.__playinfo__
  228. log('Directly modify the window.__playinfo__')
  229. playInfoTransformer(unsafeWindow.__playinfo__)
  230. } else {
  231. let internalPlayInfo = unsafeWindow.__playinfo__
  232. Object.defineProperty(unsafeWindow, '__playinfo__', {
  233. get: () => internalPlayInfo,
  234. set: v => {
  235. if (!disabled) playInfoTransformer(v);
  236. internalPlayInfo = v
  237. }
  238. })
  239. }
  240. }
  241. // Add setting checkbox
  242. if (location.href.startsWith('https://www.bilibili.com/video/') || location.href.startsWith('https://www.bilibili.com/bangumi/play/')) {
  243. waitForElm('#bilibili-player > div > div > div.bpx-player-primary-area > div.bpx-player-video-area > div.bpx-player-control-wrap > div.bpx-player-control-entity > div.bpx-player-control-bottom > div.bpx-player-control-bottom-right > div.bpx-player-ctrl-btn.bpx-player-ctrl-setting > div.bpx-player-ctrl-setting-box > div > div > div > div > div > div > div.bpx-player-ctrl-setting-others')
  244. .then(settingsBar => {
  245. settingsBar.appendChild(fromHTML(`<div class="bpx-player-ctrl-setting-others-title">${SettingsBarTitle}</div>`))
  246. const checkBoxWrapper = fromHTML(`<div class="bpx-player-ctrl-setting-checkbox bpx-player-ctrl-setting-blackgap bui bui-checkbox bui-dark"><div class="bui-area"><input class="bui-checkbox-input" type="checkbox" checked="" aria-label="自定义视频CDN">
  247. <label class="bui-checkbox-label">
  248. <span class="bui-checkbox-icon bui-checkbox-icon-default"><svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 32 32"><path d="M8 6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2H8zm0-2h16c2.21 0 4 1.79 4 4v16c0 2.21-1.79 4-4 4H8c-2.21 0-4-1.79-4-4V8c0-2.21 1.79-4 4-4z"></path></svg></span>
  249. <span class="bui-checkbox-icon bui-checkbox-icon-selected"><svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 32 32"><path d="m13 18.25-1.8-1.8c-.6-.6-1.65-.6-2.25 0s-.6 1.5 0 2.25l2.85 2.85c.318.318.762.468 1.2.448.438.02.882-.13 1.2-.448l8.85-8.85c.6-.6.6-1.65 0-2.25s-1.65-.6-2.25 0l-7.8 7.8zM8 4h16c2.21 0 4 1.79 4 4v16c0 2.21-1.79 4-4 4H8c-2.21 0-4-1.79-4-4V8c0-2.21 1.79-4 4-4z"></path></svg></span>
  250. <span class="bui-checkbox-name">${SettingsBarTitle}</span>
  251. </label></div></div>`)
  252. const checkBox = checkBoxWrapper.getElementsByTagName('input')[0]
  253. checkBox.checked = !disabled
  254. checkBoxWrapper.onclick = () => {
  255. if (checkBox.checked) {
  256. disabled = false
  257. GM_setValue('disabled', false)
  258. log(`已启用 ${SettingsBarTitle}`)
  259. } else {
  260. disabled = true
  261. GM_setValue('disabled', true)
  262. log(`已禁用 ${SettingsBarTitle}`)
  263. }
  264. }
  265. settingsBar.appendChild(checkBoxWrapper)
  266. log('checkbox added, MutationObserver disconnected.')
  267. });
  268. }
  269. })();