// ==UserScript==
// @name Netflix Plus
// @name:ja Netflix Plus
// @name:zh-CN Netflix Plus
// @name:zh-TW Netflix Plus
// @namespace http://tampermonkey.net/
// @version 2.15
// @description Enable best audio and video and more features on Netflix
// @description:ja Netflixで最高の音質と画質、そしてもっと多くの機能を体験しましょう
// @description:zh-CN 在 Netflix 上开启最佳音视频质量和更多功能
// @description:zh-TW 在 Netflix 上啓用最佳影音品質和更多功能
// @author TGSAN
// @match https://www.netflix.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=netflix.com
// @run-at document-start
// @sandbox raw
// @grant unsafeWindow
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// ==/UserScript==
(async () => {
"use strict";
let windowCtx = self.window;
if (self.unsafeWindow) {
console.log("[Netflix Plus] use unsafeWindow mode");
windowCtx = self.unsafeWindow;
} else {
console.log("[Netflix Plus] use window mode (your userscript extensions not support unsafeWindow)");
}
// Disable Cache
{
const meta = document.createElement('meta');
meta.httpEquiv = "Cache-Control";
meta.content = "no-cache";
windowCtx.document.head.appendChild(meta);
}
{
const meta = document.createElement('meta');
meta.httpEquiv = "Pragma";
meta.content = "no-cache";
windowCtx.document.head.appendChild(meta);
}
{
const meta = document.createElement('meta');
meta.httpEquiv = "Expires";
meta.content = "-1";
windowCtx.document.head.appendChild(meta);
}
function createToast() {
let toast = document.createElement("div");
toast.style.position = "fixed";
toast.style.top = "20px";
toast.style.left = "50%";
toast.style.transform = "translateX(-50%)";
toast.style.padding = "10px 20px";
toast.style.backgroundColor = "rgba(250, 250, 250, 1.0)";
toast.style.color = "rgba(32, 32, 32, 1.0)";
toast.style.fontSize = "12px";
toast.style.textAlign = "center";
toast.style.fontWeight = "600";
toast.style.zIndex = "9999";
toast.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.25)";
toast.style.borderRadius = "30px";
toast.style.opacity = "0.0";
toast.style.transition = "opacity 0.5s";
document.body.appendChild(toast);
return toast;
}
function showToast(message, time = 1500) {
let toast = createToast();
toast.innerText = message;
setTimeout(function () {
toast.style.opacity = "1.0";
setTimeout(function () {
toast.style.opacity = "0.0";
setTimeout(function () {
document.body.removeChild(toast);
}, 500);
}, time);
}, 1);
}
let playercoreDom = undefined;
let startCaptureFunctionExec = true;
windowCtx.Function.prototype.callNetflixPlusOriginal = windowCtx.Function.prototype.call;
windowCtx.Function.prototype.call = function (...args) {
if (startCaptureFunctionExec) {
let funcStr = this.toString();
let funcLen = funcStr.length;
if (funcLen > 1000000) {
// find original playercore not netflix plus playercore
if (funcStr.indexOf("h264mpl") > -1 && funcStr.indexOf("videoElementNetflixPlus") < 0) {
console.log("PlayerCore found len: " + funcStr.length);
loadCustomPlayerCore();
return undefined;
}
}
}
return this.callNetflixPlusOriginal(...args);
}
if (windowCtx.netflix !== undefined && windowCtx.netflix.player !== undefined) {
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);
console.warn("The user script is executing too late and is being forced to run, which may cause issues.");
loadCustomPlayerCore();
}
function loadCustomPlayerCore() {
startCaptureFunctionExec = false;
if (playercoreDom == undefined) {
for (let element of windowCtx.document.getElementsByTagName("script")) {
if (element.src && element.src.indexOf("cadmium-playercore") > -1) {
playercoreDom = element;
break;
}
}
}
let playercore = document.createElement('script');
playercore.src = "https://www.cloudmoe.com/static/userscript/netflix-plus/cadmium-playercore.js";
// playercore.crossOrigin = playercoreDom.crossOrigin;
playercore.async = playercoreDom.async;
playercore.id = playercoreDom.id;
playercoreDom.replaceWith(playercore);
}
// Register Netflix Plus Functions
windowCtx._videoElementNetflixPlus;
Object.defineProperty(windowCtx, "videoElementNetflixPlus", {
get: function () { return windowCtx._videoElementNetflixPlus; },
set: function (element) {
let backup = windowCtx._videoElementNetflixPlus;
windowCtx._videoElementNetflixPlus = element;
element.addEventListener('playing', function () {
if (backup === element) {
return;
}
if (!windowCtx.globalOptions.setMaxBitrate) {
return;
}
let getElementByXPath = function (xpath) {
return document.evaluate(
xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
).singleNodeValue;
};
let selectFun = function () {
windowCtx.dispatchEvent(new KeyboardEvent('keydown', {
keyCode: 83, // S (Old)
ctrlKey: true,
altKey: true,
shiftKey: true,
}));
windowCtx.dispatchEvent(new KeyboardEvent('keydown', {
keyCode: 66, // B
ctrlKey: true,
altKey: true,
shiftKey: true,
}));
const VIDEO_SELECT = getElementByXPath("//div[text()='Video Bitrate / VMAF']");
const AUDIO_SELECT = getElementByXPath("//div[text()='Audio Bitrate']");
const BUTTON = getElementByXPath("//button[text()='Override']");
if (VIDEO_SELECT && AUDIO_SELECT && BUTTON) {
[VIDEO_SELECT, AUDIO_SELECT].forEach(function (el) {
let parent = el.parentElement;
let selects = parent.querySelectorAll('select');
selects.forEach(function (select) {
select.removeAttribute("disabled");
});
let options = parent.querySelectorAll('select > option');
for (var i = 0; i < options.length - 1; i++) {
options[i].removeAttribute('selected');
}
options[options.length - 1].setAttribute('selected', 'selected');
});
setTimeout(function() { BUTTON.click(); }, 100);
backup = element;
} else {
setTimeout(selectFun, 100);
}
}
selectFun();
});
}
});
windowCtx.modifyFilterNetflixPlus = function (ModList, ModConfig, DRMType) {
let DrmVersion = "playready" === DRMType ? 30 : 0;
if (windowCtx.globalOptions.useprk) {
ModList.push("h264mpl30-dash-playready-prk-qc");
ModList.push("h264mpl31-dash-playready-prk-qc");
ModList.push("h264mpl40-dash-playready-prk-qc");
}
if (DrmVersion == 30) {
if (windowCtx.globalOptions.useddplus) {
ModList.push("ddplus-2.0-dash");
ModList.push("ddplus-5.1-dash");
ModList.push("ddplus-5.1hq-dash");
ModList.push("ddplus-atmos-dash");
// ModList = ModList.filter(item => { if (!new RegExp(/heaac/g).test(JSON.stringify(item))) return item; });
}
if (windowCtx.globalOptions.usehevc) {
ModList = ModList.filter(item => { if (!new RegExp(/main10-L5/g).test(JSON.stringify(item))) return item; });
}
if (windowCtx.globalOptions.usef12k) {
ModList = ModList.filter(item => { if (!new RegExp(/hevc-main10-L.*-dash-cenc-prk-do/g).test(JSON.stringify(item))) return item; });
}
if (windowCtx.globalOptions.usef4k) {
ModList.push("hevc-main10-L30-dash-cenc");
ModList.push("hevc-main10-L31-dash-cenc");
ModList.push("hevc-main10-L40-dash-cenc");
ModList.push("hevc-main10-L41-dash-cenc");
}
} else {
if (windowCtx.globalOptions.useFHD) {
ModList.push("playready-h264mpl40-dash");
ModList.push("playready-h264hpl40-dash");
ModList.push("vp9-profile0-L40-dash-cenc");
ModList.push("av1-main-L50-dash-cbcs-prk");
ModList.push("av1-main-L51-dash-cbcs-prk");
}
if (windowCtx.globalOptions.useHA) {
ModList.push("heaac-5.1-dash");
}
if (!windowCtx.globalOptions.usedef) {
if (windowCtx.globalOptions.useav1) {
ModList.push("av1-main-L20-dash-cbcs-prk");
ModList.push("av1-main-L21-dash-cbcs-prk");
ModList = ModList.filter(item => { if (!new RegExp(/h264/g).test(JSON.stringify(item))) return item; });
ModList = ModList.filter(item => { if (!new RegExp(/vp9-profile/g).test(JSON.stringify(item))) return item; });
}
if (windowCtx.globalOptions.usevp9) {
ModList.push("vp9-profile0-L21-dash-cenc");
ModList = ModList.filter(item => { if (!new RegExp(/h264/g).test(JSON.stringify(item))) return item; });
ModList = ModList.filter(item => { if (!new RegExp(/av1-main/g).test(JSON.stringify(item))) return item; });
}
if (windowCtx.globalOptions.useAVCH) {
ModList = ModList.filter(item => { if (!new RegExp(/vp9-profile/g).test(JSON.stringify(item))) return item; });
ModList = ModList.filter(item => { if (!new RegExp(/h264mp/g).test(JSON.stringify(item))) return item; });
ModList = ModList.filter(item => { if (!new RegExp(/av1-main/g).test(JSON.stringify(item))) return item; });
}
if (windowCtx.globalOptions.useAVC) {
ModList = ModList.filter(item => { if (!new RegExp(/vp9-profile/g).test(JSON.stringify(item))) return item; });
ModList = ModList.filter(item => { if (!new RegExp(/h264hp/g).test(JSON.stringify(item))) return item; });
ModList = ModList.filter(item => { if (!new RegExp(/av1-main/g).test(JSON.stringify(item))) return item; });
}
}
}
if (windowCtx.globalOptions.useallSub) {
ModConfig.showAllSubDubTracks = 1
}
if (windowCtx.globalOptions.closeimsc) {
ModList = ModList.filter(item => { if (!new RegExp(/imsc1.1/g).test(JSON.stringify(item))) return item; });
}
return [ModList, ModConfig, DRMType];
};
// Main Logic
const Event = class {
constructor(script, target) {
this.script = script;
this.target = target;
this._cancel = false;
this._replace = null;
this._stop = false;
}
preventDefault() {
this._cancel = true;
}
stopPropagation() {
this._stop = true;
}
replacePayload(payload) {
this._replace = payload;
}
};
let callbacks = [];
windowCtx.addBeforeScriptExecuteListener = (f) => {
if (typeof f !== "function") {
throw new Error("Event handler must be a function.");
}
callbacks.push(f);
};
windowCtx.removeBeforeScriptExecuteListener = (f) => {
let i = callbacks.length;
while (i--) {
if (callbacks[i] === f) {
callbacks.splice(i, 1);
}
}
};
const dispatch = (script, target) => {
if (script.tagName !== "SCRIPT") {
return;
}
const e = new Event(script, target);
if (typeof windowCtx.onbeforescriptexecute === "function") {
try {
windowCtx.onbeforescriptexecute(e);
} catch (err) {
console.error(err);
}
}
for (const func of callbacks) {
if (e._stop) {
break;
}
try {
func(e);
} catch (err) {
console.error(err);
}
}
if (e._cancel) {
script.textContent = "";
script.remove();
} else if (typeof e._replace === "string") {
script.textContent = e._replace;
}
};
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes) {
dispatch(n, m.target);
}
}
});
observer.observe(document, {
childList: true,
subtree: true,
});
const menuItems = [
["setMaxBitrate", "Automatically select best bitrate available"],
["useallSub", "Show all audio-tracks and subs"],
["closeimsc", "Use SUP subtitle replace IMSC subtitle"],
["useDDPandHA", "Enable Dolby and HE-AAC 5.1 Audio"],
["useFHDAndAVCH", "Focus 1080P and High-AVC"],
];
let menuCommandList = [];
windowCtx.globalOptions = {
useFHDAndAVCH: false,
useDDPandHA: true,
setMaxBitrate: true,
useallSub: true,
get ["useddplus"]() {
return windowCtx.globalOptions.useDDPandHA;
},
useAVC: false,
get ["useFHD"]() {
return windowCtx.globalOptions.useFHDAndAVCH;
},
usedef: false,
get ["useHA"]() {
return windowCtx.globalOptions.useDDPandHA;
},
get ["useAVCH"]() {
return windowCtx.globalOptions.useFHDAndAVCH;
},
usevp9: false,
useav1: false,
useprk: true,
usehevc: false,
usef4k: true,
usef12k: false,
closeimsc: true
};
windowCtx.globalOptions.useFHDAndAVCH = !await checkAdvancedDrm();
windowCtx.onbeforescriptexecute = function (e) {
let scripts = document.getElementsByTagName("script");
if (scripts.length === 0) return;
for (let i = 0; scripts.length > i; i++) {
let dom = scripts[i];
if (dom.src.includes("cadmium-playercore")) {
// firefox cannot reload src after change src url
// dom.src = "https://static.cloudmoe.com/res/userscript/netflix-plus/cadmium-playercore.js";
playercoreDom = dom;
console.warn("parsing playercore dom");
windowCtx.onbeforescriptexecute = null;
break;
}
}
};
async function checkAdvancedDrm() {
let supported = false;
if (windowCtx.MSMediaKeys) {
supported = true;
}
if (windowCtx.WebKitMediaKeys) {
supported = true;
}
// Check L1
let options = [
{
"videoCapabilities": [
{
"contentType": "video/mp4;codecs=avc1.42E01E",
"robustness": "HW_SECURE_ALL"
}
]
}
];
try {
await navigator.requestMediaKeySystemAccess("com.widevine.alpha.experiment", options);
supported = true;
} catch { }
console.log("Supported advanced DRM: " + supported);
return supported;
}
async function checkSelected(type) {
let selected = await GM_getValue("NETFLIX_PLUS_" + type);
if (typeof selected == "boolean") {
return selected;
} else {
return windowCtx.globalOptions[type];
}
}
async function registerSelectableVideoProcessingMenuCommand(name, type) {
let selected = await checkSelected(type);
windowCtx.globalOptions[type] = selected;
return await GM_registerMenuCommand((await checkSelected(type) ? "✅" : "🔲") + " " + name, async function () {
await GM_setValue("NETFLIX_PLUS_" + type, !selected);
windowCtx.globalOptions[type] = !selected;
updateMenuCommand();
});
}
async function updateMenuCommand() {
for (let command of menuCommandList) {
await GM_unregisterMenuCommand(command);
}
menuCommandList = [];
for (let menuItem of menuItems) {
menuCommandList.push(await registerSelectableVideoProcessingMenuCommand(menuItem[1], menuItem[0]));
}
}
updateMenuCommand();
})();