Netflix Plus

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

安装此脚本
作者推荐脚本

您可能也喜欢Netflix UHD

安装此脚本
  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 4.1
  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. // @sandbox raw
  17. // @grant unsafeWindow
  18. // @grant GM_setValue
  19. // @grant GM_getValue
  20. // @grant GM_registerMenuCommand
  21. // @grant GM_unregisterMenuCommand
  22. // ==/UserScript==
  23.  
  24. (async () => {
  25. "use strict";
  26.  
  27. let windowCtx = self.window;
  28. if (self.unsafeWindow) {
  29. console.log("[Netflix Plus] use unsafeWindow mode");
  30. windowCtx = self.unsafeWindow;
  31. } else {
  32. console.log("[Netflix Plus] use window mode (your userscript extensions not support unsafeWindow)");
  33. }
  34.  
  35. windowCtx.addEventListener("load", function(){
  36. // Edge fullscreen bug fix
  37. const overlay = windowCtx.document.createElement("div");
  38. windowCtx.document.body.appendChild(overlay);
  39. overlay.style.width = "100%";
  40. overlay.style.height = "100%";
  41. overlay.style.backgroundColor = "transparent";
  42. overlay.style.zIndex = 9999;
  43. overlay.style.pointerEvents = "none";
  44. overlay.style.position = "fixed";
  45. overlay.style.backdropFilter = "blur(0px)";
  46. });
  47.  
  48. // Disable Cache
  49. {
  50. const meta = document.createElement('meta');
  51. meta.httpEquiv = "Cache-Control";
  52. meta.content = "no-cache";
  53. windowCtx.document.head.appendChild(meta);
  54. }
  55. {
  56. const meta = document.createElement('meta');
  57. meta.httpEquiv = "Pragma";
  58. meta.content = "no-cache";
  59. windowCtx.document.head.appendChild(meta);
  60. }
  61. {
  62. const meta = document.createElement('meta');
  63. meta.httpEquiv = "Expires";
  64. meta.content = "-1";
  65. windowCtx.document.head.appendChild(meta);
  66. }
  67.  
  68. function createToast() {
  69. let toast = document.createElement("div");
  70. toast.style.position = "fixed";
  71. toast.style.top = "20px";
  72. toast.style.left = "50%";
  73. toast.style.transform = "translateX(-50%)";
  74. toast.style.padding = "10px 20px";
  75. toast.style.backgroundColor = "rgba(250, 250, 250, 1.0)";
  76. toast.style.color = "rgba(32, 32, 32, 1.0)";
  77. toast.style.fontSize = "12px";
  78. toast.style.textAlign = "center";
  79. toast.style.fontWeight = "600";
  80. toast.style.zIndex = "9999";
  81. toast.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.25)";
  82. toast.style.borderRadius = "30px";
  83. toast.style.opacity = "0.0";
  84. toast.style.transition = "opacity 0.5s";
  85. document.body.appendChild(toast);
  86. return toast;
  87. }
  88.  
  89. function showToast(message, time = 1500) {
  90. let toast = createToast();
  91. toast.innerText = message;
  92. setTimeout(function () {
  93. toast.style.opacity = "1.0";
  94. setTimeout(function () {
  95. toast.style.opacity = "0.0";
  96. setTimeout(function () {
  97. document.body.removeChild(toast);
  98. }, 500);
  99. }, time);
  100. }, 1);
  101. }
  102.  
  103. let playercoreDom = undefined;
  104. let startCaptureFunctionExec = true;
  105. windowCtx.Function.prototype.callNetflixPlusOriginal = windowCtx.Function.prototype.call;
  106. windowCtx.Function.prototype.call = function (...args) {
  107. if (startCaptureFunctionExec) {
  108. let funcStr = this.toString();
  109. let funcLen = funcStr.length;
  110. if (funcLen > 1000000) {
  111. // find original playercore not netflix plus playercore
  112. if (funcStr.indexOf("h264mpl") > -1 && funcStr.indexOf("videoElementNetflixPlus") < 0) {
  113. console.log("PlayerCore found len: " + funcStr.length);
  114. loadCustomPlayerCore();
  115. return undefined;
  116. }
  117. }
  118. }
  119. return this.callNetflixPlusOriginal(...args);
  120. }
  121.  
  122. if (windowCtx.netflix !== undefined && windowCtx.netflix.player !== undefined) {
  123. showToast("Netflix Plus is executing too late and is being forced to run, which may cause issues.\n\nRefresh the page while holding down the \"Shift\" key to resolve the issue.", 10000);
  124. console.warn("The user script is executing too late and is being forced to run, which may cause issues.");
  125. loadCustomPlayerCore();
  126. }
  127.  
  128. function loadCustomPlayerCore() {
  129. const setPlayerInitParams = () => {
  130. try {
  131. // windowCtx.netflix.reactContext.models.playerModel.data.config.core.initParams.enableHWDRMForHEVCAndQHDOnly = false;
  132. if (windowCtx.netflix.reactContext.models.playerModel.data.config.core.initParams.browserInfo.os.name == "linux") {
  133. windowCtx.netflix.reactContext.models.playerModel.data.config.core.initParams.browserInfo.os = {
  134. "name": "windows",
  135. "version": "10.0"
  136. };
  137. }
  138. if (windowCtx.netflix.reactContext.models.playerModel.data.config.core.initParams.browserInfo.hardware != "computer") {
  139. windowCtx.netflix.reactContext.models.playerModel.data.config.core.initParams.browserInfo.hardware = "computer";
  140. }
  141. } catch {
  142. setTimeout(setPlayerInitParams, 0)
  143. }
  144. };
  145. setPlayerInitParams();
  146. startCaptureFunctionExec = false;
  147. if (playercoreDom == undefined) {
  148. for (let element of windowCtx.document.getElementsByTagName("script")) {
  149. if (element.src && element.src.indexOf("cadmium-playercore") > -1) {
  150. playercoreDom = element;
  151. break;
  152. }
  153. }
  154. }
  155. let playercore = document.createElement('script');
  156. playercore.src = "https://www.cloudmoe.com/static/userscript/netflix-plus/cadmium-playercore.js";
  157. // playercore.crossOrigin = playercoreDom.crossOrigin;
  158. playercore.async = playercoreDom.async;
  159. playercore.id = playercoreDom.id;
  160. playercoreDom.replaceWith(playercore);
  161. }
  162.  
  163. // Register Netflix Plus Functions
  164.  
  165. windowCtx._videoElementNetflixPlus;
  166. Object.defineProperty(windowCtx, "videoElementNetflixPlus", {
  167. get: function () { return windowCtx._videoElementNetflixPlus; },
  168. set: function (element) {
  169. let backup = windowCtx._videoElementNetflixPlus;
  170. windowCtx._videoElementNetflixPlus = element;
  171. element.addEventListener('playing', function () {
  172. if (backup === element) {
  173. return;
  174. }
  175.  
  176. if (!windowCtx.globalOptions.setMaxBitrateOld) {
  177. return;
  178. }
  179.  
  180. let getElementByXPath = function (xpath) {
  181. return document.evaluate(
  182. xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
  183. ).singleNodeValue;
  184. };
  185.  
  186. let selectFun = function () {
  187. windowCtx.dispatchEvent(new KeyboardEvent('keydown', {
  188. keyCode: 83, // S (Old)
  189. ctrlKey: true,
  190. altKey: true,
  191. shiftKey: true,
  192. }));
  193.  
  194. windowCtx.dispatchEvent(new KeyboardEvent('keydown', {
  195. keyCode: 66, // B
  196. ctrlKey: true,
  197. altKey: true,
  198. shiftKey: true,
  199. }));
  200.  
  201. const VIDEO_SELECT = getElementByXPath("//div[text()='Video Bitrate / VMAF']");
  202. const AUDIO_SELECT = getElementByXPath("//div[text()='Audio Bitrate']");
  203. const BUTTON = getElementByXPath("//button[text()='Override']");
  204. if (VIDEO_SELECT && AUDIO_SELECT && BUTTON) {
  205. [VIDEO_SELECT, AUDIO_SELECT].forEach(function (el) {
  206. let parent = el.parentElement;
  207.  
  208. let selects = parent.querySelectorAll('select');
  209.  
  210. selects.forEach(function (select) {
  211. select.removeAttribute("disabled");
  212. });
  213.  
  214. let options = parent.querySelectorAll('select > option');
  215.  
  216. for (var i = 0; i < options.length - 1; i++) {
  217. options[i].removeAttribute('selected');
  218. }
  219.  
  220. options[options.length - 1].setAttribute('selected', 'selected');
  221. });
  222.  
  223. setTimeout(function () { BUTTON.click(); }, 100);
  224.  
  225. backup = element;
  226. } else {
  227. setTimeout(selectFun, 100);
  228. }
  229. }
  230. selectFun();
  231. });
  232. }
  233. });
  234.  
  235. windowCtx.modifyStreamInfoFilterNetflixPlus = function (Info) {
  236. if (windowCtx.globalOptions.onlyMaxBitrate) {
  237. console.debug(`[OnlyMaxBitrate] Dump Data`, Info);
  238. for (const InfoProperty in Info) {
  239. const InfoSub = Info[InfoProperty];
  240. // console.debug(`[OnlyMaxBitrate] ${InfoProperty}: ${InfoSub}`);
  241. const audio_tracks = InfoSub.audio_tracks;
  242. const video_tracks = InfoSub.video_tracks;
  243. if (audio_tracks && video_tracks) {
  244. const StreamInfo = InfoSub
  245. console.debug(`[OnlyMaxBitrate] Found StreamInfo in ${InfoProperty}`);
  246. for (const StreamInfoProperty in StreamInfo) {
  247. const StreamInfoSub = StreamInfo[StreamInfoProperty];
  248. if (Array.isArray(StreamInfoSub) && StreamInfoSub.length > 0 && StreamInfoSub[0].streams) {
  249. console.debug(`[OnlyMaxBitrate] Found CurrentSelectedStreamInfo in ${StreamInfoProperty}`);
  250. for (let i = 0; StreamInfoSub.length > i; i++) {
  251. StreamInfoSub[i].streams = [StreamInfoSub[i].streams.pop()];
  252. if (StreamInfoSub[i].bitrates) {
  253. StreamInfoSub[i].bitrates = [StreamInfoSub[i].bitrates.pop()];
  254. }
  255. }
  256. }
  257. }
  258. }
  259. }
  260. }
  261. return Info;
  262. }
  263.  
  264. windowCtx.modifyFilterNetflixPlus = function (ModList, ModConfig, DRMType) {
  265. let DrmVersion = "playready" === DRMType ? 30 : 0;
  266. if (windowCtx.globalOptions.useprk) {
  267. ModList.push("h264mpl30-dash-playready-prk-qc");
  268. ModList.push("h264mpl31-dash-playready-prk-qc");
  269. ModList.push("h264mpl40-dash-playready-prk-qc");
  270. }
  271. if (DrmVersion == 30) {
  272. if (windowCtx.globalOptions.useddplus) {
  273. ModList.push("ddplus-2.0-dash");
  274. ModList.push("ddplus-5.1-dash");
  275. ModList.push("ddplus-5.1hq-dash");
  276. ModList.push("ddplus-atmos-dash");
  277. // ModList = ModList.filter(item => { if (!new RegExp(/heaac/g).test(JSON.stringify(item))) return item; });
  278. }
  279. if (windowCtx.globalOptions.usehevc) {
  280. ModList = ModList.filter(item => { if (!new RegExp(/main10-L5/g).test(JSON.stringify(item))) return item; });
  281. }
  282. if (windowCtx.globalOptions.usef12k) {
  283. ModList = ModList.filter(item => { if (!new RegExp(/hevc-main10-L.*-dash-cenc-prk-do/g).test(JSON.stringify(item))) return item; });
  284. }
  285. if (windowCtx.globalOptions.usef4k) {
  286. ModList.push("hevc-main10-L30-dash-cenc");
  287. ModList.push("hevc-main10-L31-dash-cenc");
  288. ModList.push("hevc-main10-L40-dash-cenc");
  289. ModList.push("hevc-main10-L41-dash-cenc");
  290. }
  291. } else {
  292. if (windowCtx.globalOptions.useFHD) {
  293. ModList.push("playready-h264mpl40-dash");
  294. ModList.push("playready-h264hpl40-dash");
  295. ModList.push("vp9-profile0-L40-dash-cenc");
  296. ModList.push("av1-main-L50-dash-cbcs-prk");
  297. ModList.push("av1-main-L51-dash-cbcs-prk");
  298. ModList.push("av1-hdr10plus-main-L40-dash-cbcs-prk");
  299. ModList.push("av1-hdr10plus-main-L41-dash-cbcs-prk");
  300. ModList.push("av1-hdr10plus-main-L50-dash-cbcs-prk");
  301. ModList.push("av1-hdr10plus-main-L51-dash-cbcs-prk");
  302. }
  303. if (windowCtx.globalOptions.useHA) {
  304. ModList.push("heaac-5.1-dash");
  305. }
  306. if (!windowCtx.globalOptions.usedef) {
  307. if (windowCtx.globalOptions.useav1) {
  308. ModList.push("av1-main-L20-dash-cbcs-prk");
  309. ModList.push("av1-main-L21-dash-cbcs-prk");
  310. ModList = ModList.filter(item => { if (!new RegExp(/h264/g).test(JSON.stringify(item))) return item; });
  311. ModList = ModList.filter(item => { if (!new RegExp(/vp9-profile/g).test(JSON.stringify(item))) return item; });
  312. }
  313. if (windowCtx.globalOptions.usevp9) {
  314. ModList.push("vp9-profile0-L21-dash-cenc");
  315. ModList = ModList.filter(item => { if (!new RegExp(/h264/g).test(JSON.stringify(item))) return item; });
  316. ModList = ModList.filter(item => { if (!new RegExp(/av1-main/g).test(JSON.stringify(item))) return item; });
  317. }
  318. if (windowCtx.globalOptions.useAVCH) {
  319. ModList = ModList.filter(item => { if (!new RegExp(/vp9-profile/g).test(JSON.stringify(item))) return item; });
  320. // ModList = ModList.filter(item => { if (!new RegExp(/h264mp/g).test(JSON.stringify(item))) return item; });
  321. ModList = ModList.filter(item => { if (!new RegExp(/av1-main/g).test(JSON.stringify(item))) return item; });
  322. }
  323. if (windowCtx.globalOptions.useAVC) {
  324. ModList = ModList.filter(item => { if (!new RegExp(/vp9-profile/g).test(JSON.stringify(item))) return item; });
  325. ModList = ModList.filter(item => { if (!new RegExp(/h264hp/g).test(JSON.stringify(item))) return item; });
  326. ModList = ModList.filter(item => { if (!new RegExp(/av1-main/g).test(JSON.stringify(item))) return item; });
  327. }
  328. }
  329. }
  330. if (windowCtx.globalOptions.useallSub) {
  331. ModConfig.showAllSubDubTracks = 1
  332. }
  333. if (windowCtx.globalOptions.closeimsc) {
  334. ModList = ModList.filter(item => { if (!new RegExp(/imsc1.1/g).test(JSON.stringify(item))) return item; });
  335. }
  336. return [ModList, ModConfig, DRMType];
  337. };
  338.  
  339. // Main Logic
  340.  
  341. const originFetchNetflixPlus = windowCtx.fetch;
  342. windowCtx.fetch = (...arg) => {
  343. let url = "";
  344. let isRequest = false;
  345. switch (typeof arg[0]) {
  346. case "object":
  347. url = arg[0].url;
  348. isRequest = true;
  349. break;
  350. case "string":
  351. url = arg[0];
  352. break;
  353. default:
  354. break;
  355. }
  356.  
  357. if (url.indexOf('//web.prod.cloud.netflix.com/graphql') > -1) {
  358. if (typeof arg[1] == "object") {
  359. let options = arg[1];
  360. if (typeof options.body == "string" && options.body.startsWith("{") && options.body.endsWith("}")) {
  361. let body = JSON.parse(options.body);
  362. if (typeof body.operationName == "string") {
  363. if (windowCtx.globalOptions.disableHouseholdCheck && body.operationName.startsWith("CLCSInterstitial")) { // "CLCSInterstitialPlaybackAndPostPlayback or CLCSInterstitialLolomo
  364. console.debug("[DisableHouseholdCheck] Mocked: " + body.operationName);
  365. return new Promise((resolve) => {
  366. let fakeData = {
  367. data: {}
  368. };
  369. fakeData.data["body.operationName"] = null;
  370. resolve(new Response(JSON.stringify(fakeData)));
  371. });
  372. }
  373. }
  374. options.body = JSON.stringify(body);
  375. }
  376. arg[1] = options;
  377. }
  378. }
  379.  
  380. return originFetchNetflixPlus(...arg);
  381. }
  382.  
  383. const Event = class {
  384. constructor(script, target) {
  385. this.script = script;
  386. this.target = target;
  387.  
  388. this._cancel = false;
  389. this._replace = null;
  390. this._stop = false;
  391. }
  392.  
  393. preventDefault() {
  394. this._cancel = true;
  395. }
  396. stopPropagation() {
  397. this._stop = true;
  398. }
  399. replacePayload(payload) {
  400. this._replace = payload;
  401. }
  402. };
  403.  
  404. let callbacks = [];
  405. windowCtx.addBeforeScriptExecuteListener = (f) => {
  406. if (typeof f !== "function") {
  407. throw new Error("Event handler must be a function.");
  408. }
  409. callbacks.push(f);
  410. };
  411. windowCtx.removeBeforeScriptExecuteListener = (f) => {
  412. let i = callbacks.length;
  413. while (i--) {
  414. if (callbacks[i] === f) {
  415. callbacks.splice(i, 1);
  416. }
  417. }
  418. };
  419.  
  420. const dispatch = (script, target) => {
  421. if (script.tagName !== "SCRIPT") {
  422. return;
  423. }
  424.  
  425. const e = new Event(script, target);
  426.  
  427. if (typeof windowCtx.onbeforescriptexecute === "function") {
  428. try {
  429. windowCtx.onbeforescriptexecute(e);
  430. } catch (err) {
  431. console.error(err);
  432. }
  433. }
  434.  
  435. for (const func of callbacks) {
  436. if (e._stop) {
  437. break;
  438. }
  439. try {
  440. func(e);
  441. } catch (err) {
  442. console.error(err);
  443. }
  444. }
  445.  
  446. if (e._cancel) {
  447. script.textContent = "";
  448. script.remove();
  449. } else if (typeof e._replace === "string") {
  450. script.textContent = e._replace;
  451. }
  452. };
  453. const observer = new MutationObserver((mutations) => {
  454. for (const m of mutations) {
  455. for (const n of m.addedNodes) {
  456. dispatch(n, m.target);
  457. }
  458. }
  459. });
  460. observer.observe(document, {
  461. childList: true,
  462. subtree: true,
  463. });
  464.  
  465. const menuItems = [
  466. ["onlyMaxBitrate", "Only use best bitrate available"],
  467. // ["setMaxBitrateOld", "Automatically select best bitrate available"],
  468. ["useallSub", "Show all audio-tracks and subs"],
  469. ["closeimsc", "Use SUP subtitle replace IMSC subtitle"],
  470. ["useDDPandHA", "Enable Dolby and HE-AAC 5.1 Audio"],
  471. // ["useXHA", "Focus xHE-AAC Audio"],
  472. ["alwaysUseHDR", "Always use HDR or Dolby Vision when available"],
  473. ["useFHD", "Focus 1080P"],
  474. ["disableHouseholdCheck", "Disable checks for Netflix Household"],
  475. ];
  476. let menuCommandList = [];
  477.  
  478. windowCtx.globalOptions = {
  479. disableHouseholdCheck: true,
  480. useDDPandHA: true,
  481. useXHA: false,
  482. alwaysUseHDR: false,
  483. onlyMaxBitrate: true,
  484. get ["onlyVideoMaxBitrate"]() {
  485. return windowCtx.globalOptions.onlyMaxBitrate;
  486. },
  487. get ["onlyAudioMaxBitrate"]() {
  488. return windowCtx.globalOptions.onlyMaxBitrate;
  489. },
  490. setMaxBitrateOld: false,
  491. useallSub: true,
  492. get ["useddplus"]() {
  493. return windowCtx.globalOptions.useDDPandHA;
  494. },
  495. useAVC: false,
  496. usedef: false,
  497. get ["useHA"]() {
  498. return windowCtx.globalOptions.useDDPandHA;
  499. },
  500. useAVCH: true,
  501. usevp9: false,
  502. useav1: false,
  503. useprk: true,
  504. usehevc: false,
  505. usef4k: true,
  506. usef12k: false,
  507. closeimsc: true
  508. };
  509.  
  510. windowCtx.globalOptions.useFHD = !await checkAdvancedDrm();
  511.  
  512. windowCtx.onbeforescriptexecute = function (e) {
  513. let scripts = document.getElementsByTagName("script");
  514. if (scripts.length === 0) return;
  515. for (let i = 0; scripts.length > i; i++) {
  516. let dom = scripts[i];
  517. if (dom.src.includes("cadmium-playercore")) {
  518. // firefox cannot reload src after change src url
  519. // dom.src = "https://static.cloudmoe.com/res/userscript/netflix-plus/cadmium-playercore.js";
  520. playercoreDom = dom;
  521. console.warn("parsing playercore dom");
  522. windowCtx.onbeforescriptexecute = null;
  523. break;
  524. }
  525. }
  526. };
  527.  
  528. async function checkAdvancedDrm() {
  529. let supported = false;
  530. if (windowCtx.MSMediaKeys) {
  531. supported = true;
  532. }
  533. if (windowCtx.WebKitMediaKeys) {
  534. supported = true;
  535. }
  536. // Check L1
  537. let options = [
  538. {
  539. "videoCapabilities": [
  540. {
  541. "contentType": "video/mp4;codecs=avc1.42E01E",
  542. "robustness": "HW_SECURE_ALL"
  543. }
  544. ]
  545. }
  546. ];
  547.  
  548. try {
  549. await navigator.requestMediaKeySystemAccess("com.widevine.alpha.experiment", options);
  550. supported = true;
  551. } catch { }
  552. console.debug("Supported advanced DRM: " + supported);
  553. return supported;
  554. }
  555.  
  556. async function checkSelected(type) {
  557. let selected = await GM_getValue("NETFLIX_PLUS_" + type);
  558. if (typeof selected == "boolean") {
  559. return selected;
  560. } else {
  561. return windowCtx.globalOptions[type];
  562. }
  563. }
  564.  
  565. async function registerSelectableVideoProcessingMenuCommand(name, type) {
  566. let selected = await checkSelected(type);
  567. windowCtx.globalOptions[type] = selected;
  568. return await GM_registerMenuCommand((await checkSelected(type) ? "✅" : "🔲") + " " + name, async function () {
  569. await GM_setValue("NETFLIX_PLUS_" + type, !selected);
  570. windowCtx.globalOptions[type] = !selected;
  571. updateMenuCommand();
  572. });
  573. }
  574.  
  575. async function updateMenuCommand() {
  576. for (let command of menuCommandList) {
  577. await GM_unregisterMenuCommand(command);
  578. }
  579. menuCommandList = [];
  580. for (let menuItem of menuItems) {
  581. menuCommandList.push(await registerSelectableVideoProcessingMenuCommand(menuItem[1], menuItem[0]));
  582. }
  583. }
  584.  
  585. updateMenuCommand();
  586. })();