Netflix Plus

在 Netflix 上开启最佳音视频质量和更多功能

当前为 2024-07-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Netflix Plus
  3. // @name:ja Netflix Plus
  4. // @name:zh-CN Netflix Plus
  5. // @name:zh-TW Netflix Plus
  6. // @namespace http://tampermonkey.net/
  7. // @version 2.4
  8. // @description Enable best audio and video and more features on Netflix
  9. // @description:ja Netflixで最高の音質と画質、そしてもっと多くの機能を体験しましょう
  10. // @description:zh-CN 在 Netflix 上开启最佳音视频质量和更多功能
  11. // @description:zh-TW 在 Netflix 上啓用最佳影音品質和更多功能
  12. // @author TGSAN
  13. // @match https://www.netflix.com/*
  14. // @icon https://www.google.com/s2/favicons?sz=64&domain=netflix.com
  15. // @run-at document-start
  16. // @grant unsafeWindow
  17. // @grant GM_setValue
  18. // @grant GM_getValue
  19. // @grant GM_registerMenuCommand
  20. // @grant GM_unregisterMenuCommand
  21. // ==/UserScript==
  22.  
  23. (async () => {
  24. "use strict";
  25.  
  26. let windowCtx = self.window;
  27. if (self.unsafeWindow) {
  28. console.log("[Netflix Plus] use unsafeWindow mode");
  29. windowCtx = self.unsafeWindow;
  30. } else {
  31. console.log("[Netflix Plus] use window mode (your userscript extensions not support unsafeWindow)");
  32. }
  33.  
  34. // Register Netflix Plus Functions
  35.  
  36. windowCtx._videoElementNetflixPlus;
  37. Object.defineProperty(windowCtx, "videoElementNetflixPlus", {
  38. get: function () { return windowCtx._videoElementNetflixPlus; },
  39. set: function (element) {
  40. let backup = windowCtx._videoElementNetflixPlus;
  41. windowCtx._videoElementNetflixPlus = element;
  42. element.addEventListener('playing', function () {
  43. if (backup === element) {
  44. return;
  45. }
  46.  
  47. if (!windowCtx.globalOptions.setMaxBitrate) {
  48. return;
  49. }
  50.  
  51. let getElementByXPath = function (xpath) {
  52. return document.evaluate(
  53. xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
  54. ).singleNodeValue;
  55. };
  56.  
  57. let selectFun = function () {
  58. windowCtx.dispatchEvent(new KeyboardEvent('keydown', {
  59. keyCode: 83, // S (Old)
  60. ctrlKey: true,
  61. altKey: true,
  62. shiftKey: true,
  63. }));
  64.  
  65. windowCtx.dispatchEvent(new KeyboardEvent('keydown', {
  66. keyCode: 66, // B
  67. ctrlKey: true,
  68. altKey: true,
  69. shiftKey: true,
  70. }));
  71.  
  72. const VIDEO_SELECT = getElementByXPath("//div[text()='Video Bitrate / VMAF']");
  73. const AUDIO_SELECT = getElementByXPath("//div[text()='Audio Bitrate']");
  74. const BUTTON = getElementByXPath("//button[text()='Override']");
  75. if (VIDEO_SELECT && AUDIO_SELECT && BUTTON) {
  76. [VIDEO_SELECT, AUDIO_SELECT].forEach(function (el) {
  77. let parent = el.parentElement;
  78.  
  79. let selects = parent.querySelectorAll('select');
  80.  
  81. selects.forEach(function (select) {
  82. select.removeAttribute("disabled");
  83. });
  84.  
  85. let options = parent.querySelectorAll('select > option');
  86.  
  87. for (var i = 0; i < options.length - 1; i++) {
  88. options[i].removeAttribute('selected');
  89. }
  90.  
  91. options[options.length - 1].setAttribute('selected', 'selected');
  92. });
  93.  
  94. BUTTON.click();
  95.  
  96. backup = element;
  97. } else {
  98. setTimeout(selectFun, 100);
  99. }
  100. }
  101. selectFun();
  102. });
  103. }
  104. });
  105.  
  106. windowCtx.modifyFilterNetflixPlus = function (ModList, ModConfig, DRMType) {
  107. let DrmVersion = "playready" === DRMType ? 30 : 0;
  108. if (windowCtx.globalOptions.useprk) {
  109. ModList.push("h264mpl30-dash-playready-prk-qc");
  110. ModList.push("h264mpl31-dash-playready-prk-qc");
  111. ModList.push("h264mpl40-dash-playready-prk-qc");
  112. }
  113. if (DrmVersion == 30) {
  114. if (windowCtx.globalOptions.useddplus) {
  115. ModList.push("ddplus-2.0-dash");
  116. ModList.push("ddplus-5.1-dash");
  117. ModList.push("ddplus-5.1hq-dash");
  118. ModList.push("ddplus-atmos-dash");
  119. // ModList = ModList.filter(item => { if (!new RegExp(/heaac/g).test(JSON.stringify(item))) return item; });
  120. }
  121. if (windowCtx.globalOptions.usehevc) {
  122. ModList = ModList.filter(item => { if (!new RegExp(/main10-L5/g).test(JSON.stringify(item))) return item; });
  123. }
  124. if (windowCtx.globalOptions.usef12k) {
  125. ModList = ModList.filter(item => { if (!new RegExp(/hevc-main10-L.*-dash-cenc-prk-do/g).test(JSON.stringify(item))) return item; });
  126. }
  127. if (windowCtx.globalOptions.usef4k) {
  128. ModList.push("hevc-main10-L30-dash-cenc");
  129. ModList.push("hevc-main10-L31-dash-cenc");
  130. ModList.push("hevc-main10-L40-dash-cenc");
  131. ModList.push("hevc-main10-L41-dash-cenc");
  132. }
  133. } else {
  134. if (windowCtx.globalOptions.useFHD) {
  135. ModList.push("playready-h264mpl40-dash");
  136. ModList.push("playready-h264hpl40-dash");
  137. ModList.push("vp9-profile0-L40-dash-cenc");
  138. ModList.push("av1-main-L50-dash-cbcs-prk");
  139. ModList.push("av1-main-L51-dash-cbcs-prk");
  140. }
  141. if (windowCtx.globalOptions.useHA) {
  142. ModList.push("heaac-5.1-dash");
  143. }
  144. if (!windowCtx.globalOptions.usedef) {
  145. if (windowCtx.globalOptions.useav1) {
  146. ModList.push("av1-main-L20-dash-cbcs-prk");
  147. ModList.push("av1-main-L21-dash-cbcs-prk");
  148. ModList = ModList.filter(item => { if (!new RegExp(/h264/g).test(JSON.stringify(item))) return item; });
  149. ModList = ModList.filter(item => { if (!new RegExp(/vp9-profile/g).test(JSON.stringify(item))) return item; });
  150. }
  151. if (windowCtx.globalOptions.usevp9) {
  152. ModList.push("vp9-profile0-L21-dash-cenc");
  153. ModList = ModList.filter(item => { if (!new RegExp(/h264/g).test(JSON.stringify(item))) return item; });
  154. ModList = ModList.filter(item => { if (!new RegExp(/av1-main/g).test(JSON.stringify(item))) return item; });
  155. }
  156. if (windowCtx.globalOptions.useAVCH) {
  157. ModList = ModList.filter(item => { if (!new RegExp(/vp9-profile/g).test(JSON.stringify(item))) return item; });
  158. ModList = ModList.filter(item => { if (!new RegExp(/h264mp/g).test(JSON.stringify(item))) return item; });
  159. ModList = ModList.filter(item => { if (!new RegExp(/av1-main/g).test(JSON.stringify(item))) return item; });
  160. }
  161. if (windowCtx.globalOptions.useAVC) {
  162. ModList = ModList.filter(item => { if (!new RegExp(/vp9-profile/g).test(JSON.stringify(item))) return item; });
  163. ModList = ModList.filter(item => { if (!new RegExp(/h264hp/g).test(JSON.stringify(item))) return item; });
  164. ModList = ModList.filter(item => { if (!new RegExp(/av1-main/g).test(JSON.stringify(item))) return item; });
  165. }
  166. }
  167. }
  168. if (windowCtx.globalOptions.useallSub) {
  169. ModConfig.showAllSubDubTracks = 1
  170. }
  171. if (windowCtx.globalOptions.closeimsc) {
  172. ModList = ModList.filter(item => { if (!new RegExp(/imsc1.1/g).test(JSON.stringify(item))) return item; });
  173. }
  174. return [ModList, ModConfig, DRMType];
  175. };
  176.  
  177. // Main Logic
  178.  
  179. const Event = class {
  180. constructor(script, target) {
  181. this.script = script;
  182. this.target = target;
  183.  
  184. this._cancel = false;
  185. this._replace = null;
  186. this._stop = false;
  187. }
  188.  
  189. preventDefault() {
  190. this._cancel = true;
  191. }
  192. stopPropagation() {
  193. this._stop = true;
  194. }
  195. replacePayload(payload) {
  196. this._replace = payload;
  197. }
  198. };
  199.  
  200. let callbacks = [];
  201. windowCtx.addBeforeScriptExecuteListener = (f) => {
  202. if (typeof f !== "function") {
  203. throw new Error("Event handler must be a function.");
  204. }
  205. callbacks.push(f);
  206. };
  207. windowCtx.removeBeforeScriptExecuteListener = (f) => {
  208. let i = callbacks.length;
  209. while (i--) {
  210. if (callbacks[i] === f) {
  211. callbacks.splice(i, 1);
  212. }
  213. }
  214. };
  215.  
  216. const dispatch = (script, target) => {
  217. if (script.tagName !== "SCRIPT") {
  218. return;
  219. }
  220.  
  221. const e = new Event(script, target);
  222.  
  223. if (typeof windowCtx.onbeforescriptexecute === "function") {
  224. try {
  225. windowCtx.onbeforescriptexecute(e);
  226. } catch (err) {
  227. console.error(err);
  228. }
  229. }
  230.  
  231. for (const func of callbacks) {
  232. if (e._stop) {
  233. break;
  234. }
  235. try {
  236. func(e);
  237. } catch (err) {
  238. console.error(err);
  239. }
  240. }
  241.  
  242. if (e._cancel) {
  243. script.textContent = "";
  244. script.remove();
  245. } else if (typeof e._replace === "string") {
  246. script.textContent = e._replace;
  247. }
  248. };
  249. const observer = new MutationObserver((mutations) => {
  250. for (const m of mutations) {
  251. for (const n of m.addedNodes) {
  252. dispatch(n, m.target);
  253. }
  254. }
  255. });
  256. observer.observe(document, {
  257. childList: true,
  258. subtree: true,
  259. });
  260.  
  261. const menuItems = [
  262. ["setMaxBitrate", "Automatically select best bitrate available"],
  263. ["useallSub", "Show all audio-tracks and subs"],
  264. ["closeimsc", "Use SUP subtitle replace IMSC subtitle"],
  265. ["useDDPandHA", "Enable Dolby and HE-AAC 5.1 Audio"],
  266. ["useFHDAndAVCH", "Focus 1080P and High-AVC"],
  267. ];
  268. let menuCommandList = [];
  269.  
  270. windowCtx.globalOptions = {
  271. useFHDAndAVCH: !checkAdvancedDrm(),
  272. useDDPandHA: true,
  273. setMaxBitrate: true,
  274. useallSub: true,
  275. get ["useddplus"]() {
  276. return windowCtx.globalOptions.useDDPandHA;
  277. },
  278. useAVC: false,
  279. get ["useFHD"]() {
  280. return windowCtx.globalOptions.useFHDAndAVCH;
  281. },
  282. usedef: false,
  283. get ["useHA"]() {
  284. return windowCtx.globalOptions.useDDPandHA;
  285. },
  286. get ["useAVCH"]() {
  287. return windowCtx.globalOptions.useFHDAndAVCH;
  288. },
  289. usevp9: false,
  290. useav1: false,
  291. useprk: true,
  292. usehevc: false,
  293. usef4k: true,
  294. usef12k: false,
  295. closeimsc: true
  296. };
  297.  
  298. windowCtx.onbeforescriptexecute = function (e) {
  299. let scripts = document.getElementsByTagName("script");
  300. if (scripts.length === 0) return;
  301. for (let i = 0; scripts.length > i; i++) {
  302. let dom = scripts[i];
  303. if (dom.src.includes("cadmium-playercore")) {
  304. // firefox cannot reload src after change src url
  305. // dom.src = "https://static-os.kumo.moe/js/netflix/cadmium-playercore.js";
  306. let playercore = document.createElement('script');
  307. playercore.src = "https://static-os.kumo.moe/js/netflix/cadmium-playercore.js";
  308. playercore.crossOrigin = dom.crossOrigin;
  309. // playercore.async = dom.async;
  310. playercore.id = dom.id;
  311. dom.replaceWith(playercore);
  312. windowCtx.onbeforescriptexecute = null;
  313. break;
  314. }
  315. }
  316. };
  317.  
  318. async function checkAdvancedDrm() {
  319. let supported = false;
  320. if (windowCtx.MSMediaKeys) {
  321. supported = true;
  322. }
  323. if (windowCtx.WebKitMediaKeys) {
  324. supported = true;
  325. }
  326. // Check L1
  327. let options = [
  328. {
  329. "videoCapabilities": [
  330. {
  331. "contentType": "video/mp4;codecs=avc1.42E01E",
  332. "robustness": "HW_SECURE_ALL"
  333. }
  334. ]
  335. }
  336. ];
  337.  
  338. try {
  339. await navigator.requestMediaKeySystemAccess("com.widevine.alpha.experiment", options);
  340. supported = true;
  341. } catch { }
  342. console.log("Supported advanced DRM: " + supported);
  343. return supported;
  344. }
  345.  
  346. function checkSelected(type) {
  347. let selected = GM_getValue("NETFLIX_PLUS_" + type);
  348. if (typeof selected == "boolean") {
  349. return selected;
  350. } else {
  351. return windowCtx.globalOptions[type];
  352. }
  353. }
  354.  
  355. function registerSelectableVideoProcessingMenuCommand(name, type) {
  356. let selected = checkSelected(type);
  357. windowCtx.globalOptions[type] = selected;
  358. return GM_registerMenuCommand((checkSelected(type) ? "✅" : "🔲") + " " + name, function () {
  359. GM_setValue("NETFLIX_PLUS_" + type, !selected);
  360. windowCtx.globalOptions[type] = !selected;
  361. updateMenuCommand();
  362. });
  363. }
  364.  
  365. async function updateMenuCommand() {
  366. for (let command of menuCommandList) {
  367. await GM_unregisterMenuCommand(command);
  368. }
  369. menuCommandList = [];
  370. for (let menuItem of menuItems) {
  371. menuCommandList.push(await registerSelectableVideoProcessingMenuCommand(menuItem[1], menuItem[0]));
  372. }
  373. }
  374.  
  375. updateMenuCommand();
  376. })();