Skip to content

Commit

Permalink
improve default playback quality of YouTube videos to 1440p (or vq=
Browse files Browse the repository at this point in the history
…, `quality=` query-string parameter) (fixes issue #1051) (#1052)

* tidy up YouTube WebCompat WebExtension (follow up to PR #1047)

* automatically upgrade default YouTube video playback quality to HD 1440p (fixes issue #1051); hide `Full screen is unavailable` message (fixes issue #1056)

- or via `vq=`/`query=` query-string parameter in URLs for youtube.com and youtube-nocookie.com

* Fix SyntaxError

* Update to wait until player has resolutions available
  • Loading branch information
cvan authored and MortimerGoro committed Apr 11, 2019
1 parent 863be49 commit a125258
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 18 deletions.
Expand Up @@ -59,7 +59,7 @@
import static org.mozilla.vrbrowser.utils.ServoUtils.isServoAvailable;

public class SessionStore implements ContentBlocking.Delegate, GeckoSession.NavigationDelegate,
GeckoSession.ProgressDelegate, GeckoSession.ContentDelegate, GeckoSession.TextInputDelegate,
GeckoSession.ProgressDelegate, GeckoSession.ContentDelegate, GeckoSession.TextInputDelegate,
GeckoSession.PromptDelegate, GeckoSession.MediaDelegate, SharedPreferences.OnSharedPreferenceChangeListener {

private static SessionStore mInstance;
Expand Down Expand Up @@ -1057,7 +1057,9 @@ public void onCanGoForward(@NonNull GeckoSession aSession, boolean aCanGoForward
private String checkYoutubeOverride(String aUri) {
try {
Uri uri = Uri.parse(aUri);
if (!uri.getHost().toLowerCase().contains("www.youtube.")) {
String hostLower = uri.getHost().toLowerCase();
if (!hostLower.endsWith(".youtube.com") &&
!hostLower.endsWith(".youtube-nocookie.com")) {
return null;
}
String query = uri.getQueryParameter("disable_polymer");
Expand Down
7 changes: 5 additions & 2 deletions app/src/main/assets/web_extensions/youtube_webcompat/main.css
@@ -1,3 +1,6 @@
div.alert-with-button-renderer {
display:none;
/* To hide harmless warnings. */
.alert-with-button-renderer, .ytm-alert-with-button-renderer, ytm-alert-with-button-renderer,
.ytp-ad-module, ytp-ad-module
.ytp-generic-popup, ytp-generic-popup {
display: none;
}
205 changes: 197 additions & 8 deletions app/src/main/assets/web_extensions/youtube_webcompat/main.js
@@ -1,8 +1,197 @@
// Add meta-viewport
let viewport = document.head.querySelector("meta[name='viewport']");
if (!viewport) {
viewport = document.createElement("meta");
viewport.name = "viewport";
viewport.content = "width=user-width, initial-scale=1";
document.head.appendChild(viewport);
}
(function () {
// If missing, inject a `<meta name="viewport">` tag to trigger YouTube's mobile layout.
window.addEventListener('load', () => {
let viewport = document.head.querySelector('meta[name="viewport"]');
if (!viewport) {
viewport = document.createElement('meta');
viewport.name = 'viewport';
viewport.content = 'width=device-width, initial-scale=1';
document.head.appendChild(viewport);
}
});

const LOGTAG = '[firefoxreality:webcompat]'
const qs = new URLSearchParams(window.location.search);
let retryTimeout = null;

function getTruthyQS (key) {
if (!qs || !qs.has(key)) {
return false;
}
const valueLower = (qs.get('key') || '').trim().toLowerCase();
return valueLower === '' || valueLower === '1' || valueLower === 'true' || valueLower === 'yes' || valueLower === 'on';
}

const prefs = {
hd: false,
quality: 1440,
log: qs.get('mozDebug') ? getTruthyQS('mozDebug') : true,
retryAttempts: parseInt(qs.get('retryAttempts') || qs.get('retryattempts') || '10', 10),
retryTimeout: parseInt(qs.get('retryTimeout') || qs.get('retrytimeout') || '500', 10)
};

const printLog = String(prefs.log) === 'true';

const log = (...args) => printLog && console.log(LOGTAG, ...args);
const logError = (...args) => printLog && console.error(LOGTAG, ...args);
const logWarn = (...args) => printLog && console.warn(LOGTAG, ...args);

const ytImprover = window.ytImprover = (state, attempts) => {
if (ytImprover.completed) {
return;
}

if (typeof attempts === 'undefined') {
attempts = 1;
}
if (attempts >= prefs.retryAttempts) {
logError(`Giving up trying to increase resolution after ${prefs.retryAttempts} attempts.`);
return;
}

let player = document.getElementById('movie_player');
let reason = 'unknown';
if (state !== 1) {
reason = 'invalid state';
} else if (!player) {
reason = 'player not found';
} else if (!player.wrappedJSObject) {
reason = 'player.wrappedJSObject not found';
player = null;
} else if (!player.wrappedJSObject.getAvailableQualityLevels) {
reason = 'player.wrappedJSObject.getAvailableQualityLevels not found';
player = null;
}

if (!player) {
logWarn(`Cannot find player because ${reason}. attempts: ${attempts}`);
attempts++;
retryTimeout = setTimeout(() => {
ytImprover(state, attempts);
}, prefs.retryTimeout);
return;
}

player = player.wrappedJSObject;

const levels = player.getAvailableQualityLevels();
if (!levels || !levels.length) {
logWarn(`Cannot read 'player.getAvailableQualityLevels()' attempts: ${attempts}`);
attempts++;
retryTimeout = setTimeout(() => {
ytImprover(state, attempts);
}, prefs.retryTimeout);
return;
}

clearTimeout(retryTimeout);
ytImprover.completed = true;

prefs.qualities = [
'highres', 'h2880', 'hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'auto'
];
prefs.qualityLabels = {
'4320': 'highres', // 8K / 4320p / QUHD
'2880': 'hd2880', // 5K / 2880p / UHD+
'2160': 'hd2160', // 4K / 2160p / UHD
'1440': 'hd1440', // 1440p / QHD
'1080': 'hd1080', // 1080p / FHD
'720': 'hd720', // 720p / HD
'480': 'large', // 480p
'360': 'medium', // 360p
'240': 'small', // 240p
'144': 'tiny', // 144p
'0': 'auto'
};

const getDesiredQuality = () => {
const qsQuality = (qs.get('vq') || qs.get('quality') || '').trim().toLowerCase();
if (qsQuality) {
if (qsQuality in prefs.qualityLabels) {
prefs.quality = prefs.qualityLabels[qsQuality];
} else {
const qsQualityNumber = parseInt(qsQuality, 10);
if (Number.isInteger(qsQualityNumber)) {
prefs.quality = qsQualityNumber;
} else {
prefs.quality = qsQuality;
}
}
}
prefs.quality = String(prefs.quality).toLowerCase();
if (qsQuality === 'auto' || qsQuality === 'default') {
prefs.quality = 'auto';
}
if (prefs.quality in prefs.qualityLabels) {
prefs.quality = prefs.qualityLabels[prefs.quality];
}
return prefs.quality;
};

prefs.quality = getDesiredQuality();
if (prefs.quality === 'auto') {
return log(`Desired quality is fine (${prefs.quality})`);
}

const currentQuality = player.getPlaybackQuality();
if (prefs.quality === currentQuality) {
return log(`Current quality is desired quality (${currentQuality})`);
}

const findBestQuality = increase => {
if (prefs.quality === 'highest' || prefs.quality === 'best' || prefs.quality === 'max' || prefs.quality === 'maximum') {
return levels[0];
}
if (prefs.quality === 'lowest' || prefs.quality === 'worst' || prefs.quality === 'min' || prefs.quality === 'minimum') {
return levels[levels.length - 1];
}
if (increase) {
prefs.quality = prefs.qualities[prefs.qualities.indexOf(prefs.quality) - 1] || levels[0];
}
const index = levels.indexOf(prefs.quality);
if (index !== -1) {
return prefs.quality;
}
return findBestQuality(true);
};
const newBestQuality = findBestQuality();
if (currentQuality === newBestQuality) {
return log(`Current quality "${currentQuality}" is the best available quality`);
}

if (!player.setPlaybackQuality) {
return logError('`player.setPlaybackQuality` not available');
}
player.setPlaybackQuality(newBestQuality);

if (!player.setPlaybackQualityRange) {
return logError('`player.setPlaybackQualityRange` not available');
}
try {
player.setPlaybackQualityRange(newBestQuality, newBestQuality);
} catch (e) {
logError(`Failed to call 'player.setPlaybackQualityRange(${newBestQuality}, ${newBestQuality})' with exception: `, e);
return;
}

log(`Changed quality from "${currentQuality}" to "${newBestQuality}"`);
};

if (window.location.pathname.startsWith('/watch')) {
const onYouTubePlayerReady = window.onYouTubePlayerReady = evt => {
log('`onYouTubePlayerReady` called');
window.ytImprover(1);
evt.addEventListener('onStateChange', 'ytImprover');
};

window.addEventListener('spfready', () => {
log('`spfready` event fired');
if (typeof window.ytplayer === 'object' && window.ytplayer.config) {
log('`window.ytplayer.config.args.jsapicallback` set');
window.ytplayer.config.args.jsapicallback = 'onYouTubePlayerReady';
}
});

ytImprover(1);
}
})();
20 changes: 14 additions & 6 deletions app/src/main/assets/web_extensions/youtube_webcompat/manifest.json
@@ -1,14 +1,22 @@
{
"manifest_version": 2,
"name": "YoutubeWebCompat",
"name": "FirefoxRealityYouTubeWebCompat",
"version": "1.0",
"description": "Youtube web compat fixes for Firefox Reality",
"description": "YouTube web-site compatability fixes for Firefox Reality.",
"content_scripts": [
{
"matches": ["*://*.youtube.com/*"],
"css": ["main.css"],
"js": ["main.js"],
"run_at": "document_end"
"matches": [
"*://*.youtube.com/*",
"*://*.youtube-nocookie.com/*"
],
"css": [
"main.css"
],
"js": [
"main.js"
],
"run_at": "document_start",
"all_frames": true
}
]
}

0 comments on commit a125258

Please sign in to comment.