// ==UserScript== // @name Video Quality Fixer for X (Twitter) // @name:zh X (Twitter) 视频画质修复 // @name:zh-CN X (Twitter) 视频画质修复 // @namespace https://github.com/yuhaofe // @version 0.3.0 // @description Force highest quality playback for X (Twitter) videos. // @description:zh 强制 X (Twitter) 播放最高画质的视频 // @description:zh-CN 强制 X (Twitter) 播放最高画质的视频 // @author yuhaofe // @match https://x.com/* // @match https://mobile.x.com/* // @match https://twitter.com/* // @match https://mobile.twitter.com/* // @match https://pro.x.com/* // @grant none // @downloadURL none // ==/UserScript== (function () { 'use strict'; var QUALITY_DEFINE = { 'Auto': {}, 'Best': { min: 1080 }, '1080P': { max: 1080, min: 720 }, '720P': { max: 720, min: 480 }, 'Worst': { max: 480 } }; function XVidHijacker(quality) { this.quality = quality; // Auto / Best / 1080P / 720P / Worst this.realOpen = window.XMLHttpRequest.prototype.open; } XVidHijacker.prototype.setQuality = function(quality) { this.quality = quality; } XVidHijacker.prototype.hijack = function() { var self = this; window.XMLHttpRequest.prototype.open = function() { var url = arguments['1']; if (self.isHLSUrl(url)) { this.addEventListener('readystatechange', function (e) { if (this.readyState === 4) { var originalText = e.target.responseText; if (!self.isAutoQuality() && self.isMasterPlaylist(originalText)) { var modifiedText = self.modifyMasterPlaylist(originalText); Object.defineProperty(this, 'response', { writable: true }); Object.defineProperty(this, 'responseText', { writable: true }); this.response = this.responseText = modifiedText; } } }); } return self.realOpen.apply(this, arguments); }; } XVidHijacker.prototype.isHLSUrl = function(url) { var reg = new RegExp(/^https:\/\/video\.twimg\.com\/.+m3u8?/, 'i'); return reg.test(url); } XVidHijacker.prototype.isMasterPlaylist = function(text) { return text.indexOf('#EXT-X-TARGETDURATION') === -1 && text.indexOf('#EXT-X-STREAM-INF') != -1; } XVidHijacker.prototype.isAutoQuality = function() { return this.quality === 'Auto'; } XVidHijacker.prototype.modifyMasterPlaylist = function(text) { var result = text; var reg = new RegExp(/^#EXT-X-STREAM-INF:(.*)\r?\n.*$/, 'gm'); var stream = reg.exec(text); if (stream) { var globalTags = text.substring(0, stream.index); var targetPlaylist = null; var hlsPlaylists = []; var hlsPlaylist = new HLSPlaylist(stream[0], stream[1]); hlsPlaylists.push(hlsPlaylist); while ((stream = reg.exec(text)) != null) { hlsPlaylist = new HLSPlaylist(stream[0], stream[1]); hlsPlaylists.push(hlsPlaylist); } hlsPlaylists.forEach(playlist => { if (playlist.isTargetQuality(this.quality)) { targetPlaylist = playlist; } }); if (!targetPlaylist) { targetPlaylist = hlsPlaylists[0]; } result = globalTags + targetPlaylist.streamStr; } return result; } function HLSPlaylist(streamStr, infStr) { var inf = this.parseInf(infStr); this.streamStr = streamStr; this.bandwidth = inf.bandwidth; this.resolution = inf.resolution; } HLSPlaylist.prototype.parseInf = function(infStr) { var result = {}; var resKeys = { 'BANDWIDTH': 'bandwidth', 'RESOLUTION': 'resolution', 'AVERAGE-BANDWIDTH': 'averageBandwidth', 'CODECS': 'codecs' } var infs = infStr.split(','); infs.forEach(inf => { var infKV = inf.split('='); var infKey = infKV[0]; var infValue = infKV[1]; var resKey = resKeys[infKey]; if (resKey) { result[resKey] = infValue; } }) return result; } HLSPlaylist.prototype.getShorterDimension = function() { if(this.resolution) { var dims = this.resolution.split('x'); if (parseInt(dims[0]) > parseInt(dims[1])) { return parseInt(dims[1]); } else { return parseInt(dims[0]); } } else { return 0; } } HLSPlaylist.prototype.isTargetQuality = function(quality) { var shorterDim = this.getShorterDimension(); var qualityReq = QUALITY_DEFINE[quality]; var passMin = qualityReq.min ? shorterDim > qualityReq.min : true; var passMax = qualityReq.max ? shorterDim <= qualityReq.max : true; return passMin && passMax; } function initUI(hijacker) { var htmlStr = '