Crunchyroll HTML5

Replaced Crunchyroll's Flash player with an HTML5 equivalent

目前为 2017-08-17 提交的版本。查看 最新版本

// ==UserScript==
// @name        Crunchyroll HTML5
// @namespace   DoomTay
// @description Replaced Crunchyroll's Flash player with an HTML5 equivalent
// @include     http://www.crunchyroll.com/*
// @include     https://www.crunchyroll.com/*
// @require     https://cdn.rawgit.com/peterolson/BigInteger.js/441ca6ed02655abc778beb0baf07259f6912018e/BigInteger.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/pako/1.0.5/pako.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/aes-js/3.1.0/index.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/video.js/5.20.1/video.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-hls/5.8.3/videojs-contrib-hls.min.js
// @require     https://cdn.rawgit.com/Arnavion/libjass/b13173112df83073e03fdd209b87de61f7eb7726/demo/libjass.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/videojs-vast-vpaid/2.0.2/videojs_5.vast.vpaid.min.js
// @resource    vpaidCSS https://cdnjs.cloudflare.com/ajax/libs/videojs-vast-vpaid/2.0.2/videojs.vast.vpaid.min.css
// @resource    libjassCSS https://cdn.rawgit.com/Arnavion/libjass/b13173112df83073e03fdd209b87de61f7eb7726/demo/libjass.css
// @resource    vjsASSCSS https://cdn.rawgit.com/SunnyLi/videojs-ass/4548378156565f230f6f1c64fa7b20a65baa363d/src/videojs.ass.css
// @resource    vjsASSJS https://cdn.rawgit.com/SunnyLi/videojs-ass/4548378156565f230f6f1c64fa7b20a65baa363d/src/videojs.ass.js
// @version     0.9.2
// @grant       none
// @run-at      document-start
// @no-frames
// ==/UserScript==

//As we're loading from document-start, it will be much harder to get access to the page's "built in" libjass variable, so we'll set up our own.
if(!window.libjass) window.libjass = libjass;

//Since the videojs ASS plugin relies on libjass, loading it with @require won't really work, so instead we'll load it in the page.
function loadPlugin()
{
	return new Promise(function(resolve,reject) {
		var newScript = document.createElement("script");
		newScript.type = "text/javascript";
		newScript.src = GM_getResourceURL("vjsASSJS");
		newScript.onload = resolve;
		document.head.appendChild(newScript);
	});
}

//Find the script that powers the embedSWF function so we can overwrite. This is why the script is set to load at document-start. This way, we have access to the function parameters, and more importantly, the function can be overwritten before the Flash plugin has a chance to load.
function findScript()
{
	var observer = new MutationObserver(function(mutations) {
		mutations.forEach(function(mutation) {
			for(var i = 0; i < mutation.addedNodes.length; i++)
			{
				findSWFScript(mutation.addedNodes[i]);
			}
		});
	});
	
	var config = { childList: true, subtree: true };
	observer.observe(document, config);
	
	for(var i = 0; i < document.scripts.length; i++)
	{
		findSWFScript(document.scripts[i]);
	}

	function findSWFScript(start)
	{
		if(start.nodeName == "SCRIPT" && start.src.includes("www/view/showmedia"))
		{
			observer.disconnect();
			
			swfobject.embedSWF = function(swf,id,width,height,version,downloadURL,params)
			{
				var placeholder = document.getElementById(id);
				
				var newVideo = document.createElement("video");
				newVideo.id = id;
				newVideo.className = "video-js vjs-default-skin";
				newVideo.controls = true;
				newVideo.width = width;
				newVideo.height = height;
				
				placeholder.parentNode.replaceChild(newVideo,placeholder);
				
				var configURL = decodeURIComponent(params.config_url);
				getConfig(configURL).then(function(config)
				{
					newVideo.poster = config.getElementsByTagName("default:backgroundUrl")[0].textContent;
					
					var autoplay = config.getElementsByTagName("default:isAutoPlay")[0].textContent == 1;
					var streamFile = config.querySelector("stream_info").querySelector("file").textContent;
					var subtitleTag = config.querySelector("subtitle:not([link])");
					var scriptObject = subtitleTag ? parseSubtitles(subtitleTag) : null;
					
					var adSlots = config.getElementsByTagName("adSlots")[0];
					
					loadPlugin().then(() =>
					{
						window.videojs(id, {
							sources: [
								{src: streamFile,type: 'application/x-mpegURL'}
							],
							controlBar: {
								children: [
								'playToggle',
								'progressControl',
								'currentTimeDisplay',
								'timeDivider',
								'durationDisplay',
								'liveDisplay',
								'customControlSpacer',
								'playbackRateMenuButton',
								'chaptersButton',
								'subtitlesButton',
								'captionsButton',
								'fullscreenToggle',
								'volumeMenuButton'
							]
						}}, function(){
							var player = this;
							
							//Load needed CSS.
							createCSS(GM_getResourceURL("vpaidCSS"));
							createCSS(GM_getResourceURL("libjassCSS"));
							createCSS(GM_getResourceURL("vjsASSCSS"));
							//Adding custom stylesheet after video is initialized so that the "default" stylesheet doesn't override it
							var newStyleSheet = document.createElement("style");
							newStyleSheet.rel = "stylesheet";
							newStyleSheet.innerHTML = ".vjs-volume-menu-button.vjs-menu-button-inline\n\
							{\n\
							  width: 12em;\n\
							}\n\
							.vjs-volume-menu-button.vjs-menu-button-inline .vjs-menu\n\
							{\n\
							  opacity: 1;\n\
							}\n\
							.video-js .vjs-control-bar\n\
							{\n\
							  background-color:#333;\n\
							}\n\
							.video-js .vjs-play-progress, .video-js .vjs-volume-level, .video-js .vjs-progress-holder, .video-js .vjs-load-progress div\n\
							{\n\
							  background-color:#f7931e;\n\
							}\n\
							.video-js .vjs-current-time\n\
							{\n\
								display:block;\n\
								padding-right: 0;\n\
							}\n\
							.video-js .vjs-time-divider\n\
							{\n\
								display:block;\n\
							}\n\
							.video-js .vjs-duration\n\
							{\n\
								display:block;\n\
								padding-left: 0;\n\
							}";
							document.head.appendChild(newStyleSheet);
							
							if(adSlots)
							{
								var slots = adSlots.getElementsByTagName("adSlot");
								var adTags = Array.from(slots[0].getElementsByTagName("vastAd"),ad => ad.getAttribute("url"));
								//At the moment, the VAST plugin can only handle one ad.
								var adUrl = adTags.find(ad => ad.includes("backup3"));
								
								if(adUrl)
								{
									var vastAd = player.vastClient({
										"adTagUrl": adUrl,
										"adsCancelTimeout": 5000,
										"adsEnabled": true
									});
								}
							}
							
							if(scriptObject)
							{
								var ASSFile = new ASSObject(scriptObject);
								var subtitleBlob = URL.createObjectURL(new Blob([ASSFile.toString()], {type : "text/plain"}));
								
								var vjs_ass = player.ass({
									"src": [subtitleBlob],
									"enableSvg": false,
									"delay": 0
								});
							}
							
							if(autoplay) player.play();
						});
					});
				});
			};
		}
	}
}

findScript();

function createCSS(css)
{
	var newStyleSheet = document.createElement("link");
	newStyleSheet.rel = "stylesheet";
	newStyleSheet.href = css;
	document.head.appendChild(newStyleSheet);
}

function parseSubtitles(subtitles)
{
	var iv = bytesToNumbers(atob(subtitles.getElementsByTagName("iv")[0].textContent));
	var subData = bytesToNumbers(atob(subtitles.getElementsByTagName("data")[0].textContent));
	var id = parseInt(subtitles.getAttribute("id"));
	
	var key = createKey(id);
	
	//CryptoJS's AES decrypting cuts off the resulting string sometimes, so we're using something else instead.
	var aesCbc = new aesjs.ModeOfOperation.cbc(bytesToNumbers(key.toString(CryptoJS.enc.Latin1)), iv);
	var decrypted = aesCbc.decrypt(subData);
	
	var deflated = pako.inflate(decrypted, {to: "string"});
	
	var script = new DOMParser().parseFromString(deflated,"text/xml").querySelector("subtitle_script");
		
	return script;
	
	function bytesToNumbers(bytes)
	{
		return Uint8Array.from(bytes,(letter,i) => bytes.charCodeAt(i));
	}
	
	function createKey(id)
	{
		function magic()
		{
			var hash = bigInt(88140282).xor(id).toJSNumber();
			var multipliedHash = bigInt(hash).multiply(32).toJSNumber();
			return bigInt(hash).xor(hash >> 3).xor(multipliedHash).toJSNumber();
		}
		
		var hash = "$&).6CXzPHw=2N_+isZK" + magic();
		var shaHashed = CryptoJS.SHA1(hash);
		
		var keyString = shaHashed.toString(CryptoJS.enc.Latin1);
		var paddedKey = keyString + "\u0000".repeat(32 - keyString.length);
		var recodedKey = CryptoJS.enc.Latin1.parse(paddedKey);
		
		return recodedKey;
	}
}

function getConfig(configURL)
{
	return new Promise(function(resolve,reject)
	{
		var config = new XMLHttpRequest();
		config.onload = function()
		{
			resolve(this.response);
		};
		config.onerror = reject;
		config.open("POST", configURL, true);
		config.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
		config.responseType = "document";
		config.send("current_page=" + window.location.href);
	});
}

function ASSObject(scriptSource)
{
	this.events = [];
	
	var styles = scriptSource.querySelector("styles").children;
	var events = scriptSource.querySelector("events").children;
	
	this.id = scriptSource.getAttribute("id");
	this.title = scriptSource.getAttribute("title");
	this.langCode = scriptSource.getAttribute("lang_code");
	this.langString = scriptSource.getAttribute("lang_string");
	this.playResX = scriptSource.getAttribute("play_res_x");
	this.playResY = scriptSource.getAttribute("play_res_y");
	this.created = scriptSource.getAttribute("created");
	this.wrapStyle = scriptSource.getAttribute("wrap_style");
	
	this.styles = {};
	
	var newStyle = document.createElement("style");
	document.head.appendChild(newStyle);
	
	for(var s = 0; s < styles.length; s++)
	{
		var styleObject = {};
		styleObject.name = this.formatClass(styles[s].getAttribute("name"));
		styleObject.fontName = styles[s].getAttribute("font_name");
		styleObject.fontSize = styles[s].getAttribute("font_size");
		styleObject.italic = styles[s].getAttribute("italic");
		styleObject.bold = styles[s].getAttribute("bold");
		styleObject.underline = styles[s].getAttribute("underline");
		styleObject.strikeout = styles[s].getAttribute("strikeout");
		styleObject.primaryColor = styles[s].getAttribute("primary_colour");
		styleObject.secondaryColor = styles[s].getAttribute("secondary_colour");
		styleObject.outlineColor = styles[s].getAttribute("outline_colour");
		styleObject.backColor = styles[s].getAttribute("back_colour");
		styleObject.scaleX = styles[s].getAttribute("scale_x");
		styleObject.scaleY = styles[s].getAttribute("scale_y");
		styleObject.spacing = styles[s].getAttribute("spacing");
		styleObject.angle = styles[s].getAttribute("angle");
		styleObject.borderStyle = styles[s].getAttribute("border_style");
		styleObject.outline = styles[s].getAttribute("outline");
		styleObject.shadow = styles[s].getAttribute("shadow");
		styleObject.alignment = styles[s].getAttribute("alignment");
		styleObject.marginL = styles[s].getAttribute("margin_l");
		styleObject.marginR = styles[s].getAttribute("margin_r");
		styleObject.marginV = styles[s].getAttribute("margin_v");
		styleObject.encoding = styles[s].getAttribute("encoding");
		
		this.styles[styleObject.name] = styleObject;
	}
	
	for(var e = 0; e < events.length; e++)
	{
		var parsedEvent = {};
	
		parsedEvent.id = parseInt(events[e].getAttribute("id"));
		parsedEvent.start = events[e].getAttribute("start");
		parsedEvent.end = events[e].getAttribute("end");
		parsedEvent.style = events[e].getAttribute("style");
		parsedEvent.name = events[e].getAttribute("name");
		parsedEvent.marginL = events[e].getAttribute("margin_l");
		parsedEvent.marginR = events[e].getAttribute("margin_r");
		parsedEvent.marginV = events[e].getAttribute("margin_v");
		parsedEvent.text = "{\shad3}" + events[e].getAttribute("text");
		parsedEvent.effect = events[e].getAttribute("effect");
		
		this.events.push(parsedEvent);
	}
	
}
ASSObject.prototype.constructor = ASSObject;
ASSObject.prototype.toString = function()
{
	var string = "[Script Info]\n";
	string += "Title: " + this.title + "\n";
	string += "ScriptType: v4.00+\n";
	string += "WrapStyle: " + this.wrapStyle + "\n";
	string += "PlayResX: " + this.playResX + "\n";
	string += "PlayResY: " + this.playResY + "\n";
	string += "Subtitle ID: " + this.id + "\n";
	string += "Language: " + this.langString + "\n";
	string += "Created: " + this.created + "\n\n";
	
	string += "[V4+ Styles]\n";
	string += "Format: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,MarginL,MarginR,MarginV,Encoding\n";
	
	for(var style in this.styles)
	{
		var currentStyle = this.styles[style];
		string += "Style: ";
		string += currentStyle.name + ",";
		string += currentStyle.fontName + ",";
		string += currentStyle.fontSize + ",";
		string += currentStyle.primaryColor + ",";
		string += currentStyle.secondaryColor + ",";
		string += currentStyle.outlineColor + ",";
		string += currentStyle.backColor + ",";
		string += currentStyle.bold + ",";
		string += currentStyle.italic + ",";
		string += currentStyle.underline + ",";
		string += currentStyle.strikeout + ",";
		string += currentStyle.scaleX + ",";
		string += currentStyle.scaleY + ",";
		string += currentStyle.spacing + ",";
		string += currentStyle.angle + ",";
		string += currentStyle.borderStyle + ",";
		string += currentStyle.outline + ",";
		string += currentStyle.shadow + ",";
		string += currentStyle.alignment + ",";
		string += currentStyle.marginL + ",";
		string += currentStyle.marginR + ",";
		string += currentStyle.marginV + ",";
		string += currentStyle.encoding;
		string += "\n";
	}
	
	string += "\n";
	
	string += "[Events]\n";
	string += "Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text\n";
	
	for(var e = 0; e < this.events.length; e++)
	{
		string += "Dialogue: 0,";
		string += this.events[e].start + ",";
		string += this.events[e].end + ",";
		string += this.formatClass(this.events[e].style) + ",";
		string += this.events[e].name + ",";
		string += this.events[e].marginL + ",";
		string += this.events[e].marginR + ",";
		string += this.events[e].marginV + ",";
		string += this.events[e].effect + ",";
		string += this.events[e].text;
		string += "\n";
	}
	
	return string;
};
ASSObject.prototype.formatClass = function(className)
{
	return className.replace(/ /g,"_");
};

function GM_getResourceURL(resourceName)
{
	if(GM_info.script.resources[resourceName]) return GM_info.script.resources[resourceName].url;
	else
	{
		//The "built in" mimetype tends to be inaccurate, so we're doing something simpler to determine the mimetype of the resource
		var mimetype = resourceName.endsWith(".js") ? "text/javascript" : "text/css";
        var resourceObject = GM_info.script.resources.find(resource => resource.name == resourceName);
		var dataURL = "data:" + mimetype + "," + encodeURIComponent(resourceObject.content);
		return dataURL;
	}
}

QingJ © 2025

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