Skip to content

Instantly share code, notes, and snippets.

@gsuberland
Created November 7, 2021 14:45
Show Gist options
  • Save gsuberland/968ed4ed21a7f09425cac198a03c1b8b to your computer and use it in GitHub Desktop.
Save gsuberland/968ed4ed21a7f09425cac198a03c1b8b to your computer and use it in GitHub Desktop.
Tampermonkey / Greasemonkey script to force high quality video on Twitter
// ==UserScript==
// @name Load HQ Video on Twitter
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Forces Twitter to always load the highest bitrate video available.
// @author Graham Sutherland
// @match https://twitter.com/*
// @icon https://www.google.com/s2/favicons?domain=twitter.com
// @grant none
// ==/UserScript==
/*
This works by hooking XMLHttpRequest with a custom readystatechange event listener whenever a request is opened to an M3U8 playlist target.
The playlist being targeted contains a list of streams for various bandwidths. The event listener modifies the response to remove all but the highest bandwidth stream.
For safety, two M3U8 rewriting methods are defined. The primary one is a regex, which allows us to find the stream with the greatest bandwidth.
If the regex parsing fails, e.g. due to a format change, the fallback method simply removes all streams but the last one in the list, which is usually the highest bandwidth one.
*/
(function(open) {
XMLHttpRequest.prototype.open = function() {
if (arguments[1].match(/\/pl\/[a-zA-Z0-9_]+\.m3u8\?/))
{
console.log("Attaching readystatechange event for video playlist XHR.");
this.addEventListener("readystatechange", function() {
if (this.readyState == 4)
{
if (this.responseText.startsWith("#EXTM3U"))
{
console.log("Handling M3U response for video...");
var m3u8info = this.responseText;
var m3u8new = "";
// re-define the response properties as writable
Object.defineProperty(this, 'response', {writable: true});
Object.defineProperty(this, 'responseText', {writable: true});
// use regex to match stream info
var results = m3u8info.matchAll(/^#EXT-X-STREAM-INF:.*BANDWIDTH=(?<bandwidth>\d+),RESOLUTION=(?<resX>\d+)x(?<resY>\d+),CODECS="(?<codecs>[^"]+)"\r?\n(?<url>\/.*\.m3u8)/gm);
results = Array.from(results);
// attempt to extract stream info with regex to get the highest bandwidth option
var gotResultsFromRegex = false;
var indexOfFirstMatch = -1;
var maxBandwidth = -1;
var maxBandwidthIndex = -1;
var currentIndex = -1;
for (const match of results)
{
//console.log(match);
currentIndex++;
gotResultsFromRegex = true;
if (indexOfFirstMatch == -1)
{
indexOfFirstMatch = match.index;
}
var bw = parseInt(match.groups["bandwidth"], 10);
if (bw > maxBandwidth)
{
maxBandwidth = bw;
maxBandwidthIndex = currentIndex;
}
}
// if this succeeded, use the regex results.
if (gotResultsFromRegex)
{
console.log("Stream index " + maxBandwidthIndex + " has the highest bandwidth. BANDWIDTH=" + maxBandwidth + ", RESOLUTION=" + results[maxBandwidthIndex].groups.resX + "x" + results[maxBandwidthIndex].groups.resY + ", CODECS=" + results[maxBandwidthIndex].groups.codecs);
m3u8new = m3u8info.substring(0, indexOfFirstMatch) + results[maxBandwidthIndex][0];
}
else
{
console.log("Warning: failed to get results from regex. Falling back to line-splitting method.");
// split the lines around #EXT-X-STREAM-INF. the first element will be everything before the first stream info, the last element is usually the highest bandwidth
var lines = m3u8info.split(/\r?\n#EXT-X-STREAM-INF/);
m3u8new = lines[0] + "\n#EXT-X-STREAM-INF" + lines[lines.length - 1];
}
//console.log(m3u8new);
this.response = this.responseText = m3u8new;
console.log("Re-wrote M3U8 response to use highest bandwidth stream.");
}
}
});
}
open.apply(this, arguments);
};
})(XMLHttpRequest.prototype.open);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment