mirror of
https://github.com/goodtube4u/goodtube.git
synced 2025-11-22 10:16:11 +00:00
6747 lines
202 KiB
JavaScript
6747 lines
202 KiB
JavaScript
(function() {
|
|
'use strict';
|
|
|
|
|
|
// Bypass CSP restrictions, introduced by the latest Chrome updates
|
|
if (window.trustedTypes && window.trustedTypes.createPolicy && !window.trustedTypes.defaultPolicy) {
|
|
window.trustedTypes.createPolicy('default', {
|
|
createHTML: string => string,
|
|
createScriptURL: string => string,
|
|
createScript: string => string
|
|
});
|
|
}
|
|
|
|
|
|
/* General config
|
|
------------------------------------------------------------------------------------------ */
|
|
// Github location for loading assets
|
|
let goodTube_github = 'https://raw.githubusercontent.com/goodtube4u/GoodTube/main';
|
|
|
|
// How long to wait before trying to load something again (in milliseconds)
|
|
let goodTube_retryDelay = 500;
|
|
|
|
// How many times to try and load something again
|
|
let goodTube_retryAttempts = 3;
|
|
|
|
// Create a variable to hold retry timeouts
|
|
let goodTube_pendingRetry = [];
|
|
|
|
|
|
/* Video servers
|
|
------------------------------------------------------------------------------------------ */
|
|
let goodTube_videoServers = [
|
|
// AUTOMATIC OPTION
|
|
// --------------------------------------------------------------------------------
|
|
{
|
|
'name': 'Automatic',
|
|
'type': false,
|
|
'proxy': true,
|
|
'url': 'automatic'
|
|
},
|
|
|
|
// HD SERVERS
|
|
// --------------------------------------------------------------------------------
|
|
// FAST
|
|
{
|
|
'name': 'Acid (US)',
|
|
'type': 2,
|
|
'proxy': true,
|
|
'url': 'https://invidious.incogniweb.net'
|
|
},
|
|
// FAST
|
|
{
|
|
'name': 'Anubis (DE)',
|
|
'type': 3,
|
|
'proxy': true,
|
|
'url': 'https://pipedapi.r4fo.com'
|
|
},
|
|
// FAST
|
|
{
|
|
'name': 'Phoenix (US)',
|
|
'type': 3,
|
|
'proxy': true,
|
|
'url': 'https://pipedapi.drgns.space'
|
|
},
|
|
// // FAST
|
|
// {
|
|
// 'name': 'Ra (US)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://pipedapi.us.projectsegfau.lt'
|
|
// },
|
|
// // FAST
|
|
// {
|
|
// 'name': 'Hades (DE)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://pipedapi.dedyn.io'
|
|
// },
|
|
// // FAST
|
|
// {
|
|
// 'name': 'Rain (DE)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://pipedapi.andreafortuna.org'
|
|
// },
|
|
// FAST
|
|
{
|
|
'name': 'Nymph (AT)',
|
|
'type': 2,
|
|
'proxy': true,
|
|
'url': 'https://invidious.private.coffee'
|
|
},
|
|
// FAST
|
|
{
|
|
'name': 'Serpent (US)',
|
|
'type': 2,
|
|
'proxy': true,
|
|
'url': 'https://invidious.darkness.services'
|
|
},
|
|
// FAST
|
|
{
|
|
'name': 'Sphere (US)',
|
|
'type': 3,
|
|
'proxy': true,
|
|
'url': 'https://pipedapi.darkness.services'
|
|
},
|
|
// // FAST
|
|
// {
|
|
// 'name': 'Obsidian (AT)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://pipedapi.leptons.xyz'
|
|
// },
|
|
// // MEDIUM
|
|
// {
|
|
// 'name': 'Hunter (NL)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://pipedapi.ducks.party'
|
|
// },
|
|
// MEDIUM
|
|
{
|
|
'name': 'Sapphire (IN)',
|
|
'type': 3,
|
|
'proxy': true,
|
|
'url': 'https://pipedapi.in.projectsegfau.lt'
|
|
},
|
|
// FAST
|
|
{
|
|
'name': 'Sphynx (JP)',
|
|
'type': 2,
|
|
'proxy': true,
|
|
'url': 'https://invidious.jing.rocks'
|
|
},
|
|
// // MEDIUM
|
|
// {
|
|
// 'name': 'Space (DE)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://pipedapi.smnz.de'
|
|
// },
|
|
// MEDIUM
|
|
{
|
|
'name': 'Orchid (DE)',
|
|
'type': 3,
|
|
'proxy': true,
|
|
'url': 'https://api.piped.yt'
|
|
},
|
|
// MEDIUM
|
|
{
|
|
'name': 'Emerald (DE)',
|
|
'type': 3,
|
|
'proxy': true,
|
|
'url': 'https://pipedapi.phoenixthrush.com'
|
|
},
|
|
// MEDIUM
|
|
{
|
|
'name': '420 (FI)',
|
|
'type': 2,
|
|
'proxy': true,
|
|
'url': 'https://invidious.privacyredirect.com'
|
|
},
|
|
// MEDIUM
|
|
{
|
|
'name': 'Onyx (FR)',
|
|
'type': 2,
|
|
'proxy': true,
|
|
'url': 'https://invidious.fdn.fr'
|
|
},
|
|
// // MEDIUM
|
|
// {
|
|
// 'name': 'Indigo (FI)',
|
|
// 'type': 2,
|
|
// 'proxy': true,
|
|
// 'url': 'https://iv.datura.network'
|
|
// },
|
|
// // MEDIUM
|
|
// {
|
|
// 'name': 'Andromeda (FI)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://pipedapi-libre.kavin.rocks'
|
|
// },
|
|
// // MEDIUM
|
|
// {
|
|
// 'name': 'Lilith (INT)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://pipedapi.syncpundit.io'
|
|
// },
|
|
// // MEDIUM
|
|
// {
|
|
// 'name': 'Basilisk (DE)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://pipedapi.adminforge.de'
|
|
// },
|
|
// // MEDIUM
|
|
// {
|
|
// 'name': 'Golem (AT)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://schaunapi.ehwurscht.at'
|
|
// },
|
|
|
|
// // SLOW
|
|
// {
|
|
// 'name': 'Centaur (FR)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://api.piped.projectsegfau.lt'
|
|
// },
|
|
// // SLOW
|
|
// {
|
|
// 'name': 'Cypher (FR)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://api.piped.privacydev.net'
|
|
// },
|
|
// // SLOW
|
|
// {
|
|
// 'name': 'T800 (DE)',
|
|
// 'type': 2,
|
|
// 'proxy': true,
|
|
// 'url': 'https://invidious.protokolla.fi'
|
|
// },
|
|
// // SLOW
|
|
// {
|
|
// 'name': 'Wasp (DE)',
|
|
// 'type': 2,
|
|
// 'proxy': true,
|
|
// 'url': 'https://iv.melmac.space'
|
|
// },
|
|
// // SLOW
|
|
// {
|
|
// 'name': 'Platinum (TR)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://pipedapi.ngn.tf'
|
|
// },
|
|
// // SLOW
|
|
// {
|
|
// 'name': 'Minotaur (NL)',
|
|
// 'type': 3,
|
|
// 'proxy': true,
|
|
// 'url': 'https://pipedapi.astartes.nl'
|
|
// },
|
|
|
|
// 360p SERVERS
|
|
// --------------------------------------------------------------------------------
|
|
{
|
|
'name': '360p - Amethyst (DE)',
|
|
'type': 1,
|
|
'proxy': true,
|
|
'url': 'https://yt.artemislena.eu'
|
|
},
|
|
{
|
|
'name': '360p - Goblin (AU)',
|
|
'type': 1,
|
|
'proxy': false,
|
|
'url': 'https://invidious.perennialte.ch'
|
|
},
|
|
// {
|
|
// 'name': '360p - Jade (SG)',
|
|
// 'type': 1,
|
|
// 'proxy': true,
|
|
// 'url': 'https://vid.lilay.dev'
|
|
// },
|
|
{
|
|
'name': '360p - Raptor (US)',
|
|
'type': 1,
|
|
'proxy': true,
|
|
'url': 'https://invidious.drgns.space'
|
|
},
|
|
// {
|
|
// 'name': '360p - Velvet (CL)',
|
|
// 'type': 1,
|
|
// 'proxy': true,
|
|
// 'url': 'https://inv.nadeko.net'
|
|
// },
|
|
// {
|
|
// 'name': '360p - Druid (DE)',
|
|
// 'type': 1,
|
|
// 'proxy': true,
|
|
// 'url': 'https://invidious.projectsegfau.lt'
|
|
// }
|
|
];
|
|
|
|
// Set the starting server to automatic mode
|
|
let goodTube_videoServer_type = goodTube_videoServers[0]['type'];
|
|
let goodTube_videoServer_proxy = goodTube_videoServers[0]['proxy'];
|
|
let goodTube_videoServer_url = goodTube_videoServers[0]['url'];
|
|
let goodTube_videoServer_name = goodTube_videoServers[0]['name'];
|
|
|
|
// Set the automatic server index
|
|
let goodTube_videoServer_automaticIndex = 0;
|
|
|
|
|
|
/* Subtitle and storyboard servers
|
|
------------------------------------------------------------------------------------------ */
|
|
let goodTube_storyboardSubtitleServers_subtitleIndex = 0;
|
|
let goodTube_storyboardSubtitleServers_storyboardIndex = 0;
|
|
let goodTube_storyboardSubtitleServers = [
|
|
'https://invidious.perennialte.ch',
|
|
'https://yt.artemislena.eu',
|
|
'https://invidious.private.coffee',
|
|
'https://invidious.drgns.space',
|
|
'https://inv.nadeko.net',
|
|
'https://invidious.projectsegfau.lt',
|
|
'https://invidious.jing.rocks',
|
|
'https://invidious.incogniweb.net',
|
|
'https://invidious.privacyredirect.com',
|
|
'https://invidious.fdn.fr',
|
|
'https://iv.datura.network',
|
|
'https://pipedapi-libre.kavin.rocks',
|
|
'https://pipedapi.syncpundit.io',
|
|
'https://invidious.protokolla.fi',
|
|
'https://iv.melmac.space'
|
|
];
|
|
|
|
|
|
/* Download servers
|
|
------------------------------------------------------------------------------------------ */
|
|
// We first try these servers, recommended by "ihatespawn".
|
|
// As I understand it these are ok to use, not trying to step on anyone's toes here.
|
|
// Any issues with this implementation, please contact me. I am happy to work with you, so long as we let people download from somewhere.
|
|
let goodTube_downloadServers_default = [
|
|
// 'https://dl01.yt-dl.click',
|
|
// 'https://dl02.yt-dl.click',
|
|
// 'https://dl03.yt-dl.click',
|
|
// 'https://apicloud9.filsfkwtlfjas.xyz',
|
|
'https://apicloud3.filsfkwtlfjas.xyz',
|
|
'https://apicloud8.filsfkwtlfjas.xyz',
|
|
'https://apicloud4.filsfkwtlfjas.xyz',
|
|
'https://apicloud5.filsfkwtlfjas.xyz',
|
|
];
|
|
|
|
// Only if they all fail, will we then fallback to using community instances.
|
|
// This array is also shuffled to take the load off any single community instance.
|
|
let goodTube_downloadServers_community = [
|
|
'https://sea-downloadapi.stuff.solutions',
|
|
'https://ca.haloz.at',
|
|
'https://cobalt.wither.ing',
|
|
'https://capi.tieren.men',
|
|
'https://co.tskau.team',
|
|
'https://apicb.tigaultraman.com',
|
|
'https://api-cobalt.boykisser.systems',
|
|
'https://cobalt.decrystalfan.app',
|
|
'https://wukko.wolfdo.gg',
|
|
'https://capi.oak.li',
|
|
'https://cb.nyoom.fun',
|
|
'https://dl.khyernet.xyz',
|
|
'https://cobalt-api.alexagirl.studio',
|
|
'https://nyc1.coapi.ggtyler.dev',
|
|
'https://api.dl.ixhby.dev',
|
|
'https://co.eepy.today',
|
|
'https://downloadapi.stuff.solutions',
|
|
'https://cobalt-api.ayo.tf',
|
|
'https://api.sacreations.me',
|
|
'https://apicloud2.filsfkwtlfjas.xyz',
|
|
'https://dl01.yt-dl.click'
|
|
];
|
|
|
|
// Shuffle community instances
|
|
let currentIndex = goodTube_downloadServers_community.length;
|
|
while (currentIndex != 0) {
|
|
let randomIndex = Math.floor(Math.random() * currentIndex);
|
|
currentIndex--;
|
|
|
|
[goodTube_downloadServers_community[currentIndex], goodTube_downloadServers_community[randomIndex]] = [goodTube_downloadServers_community[randomIndex], goodTube_downloadServers_community[currentIndex]];
|
|
}
|
|
|
|
// Combine the default download servers with the shuffled community instances
|
|
let goodTube_downloadServers = goodTube_downloadServers_default.concat(goodTube_downloadServers_community);
|
|
|
|
|
|
/* Helper functions
|
|
------------------------------------------------------------------------------------------ */
|
|
// Are you on iOS?
|
|
function goodTube_helper_iOS() {
|
|
return [
|
|
'iPad Simulator',
|
|
'iPhone Simulator',
|
|
'iPod Simulator',
|
|
'iPad',
|
|
'iPhone',
|
|
'iPod'
|
|
].includes(navigator.platform)
|
|
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
|
}
|
|
|
|
// Pad a number with leading zeros
|
|
function goodTube_helper_padNumber(num, size) {
|
|
num = num.toString();
|
|
while (num.length < size) num = "0" + num;
|
|
return num;
|
|
}
|
|
|
|
// Setup GET parameters
|
|
function goodTube_helper_setupGetParams() {
|
|
let getParams = {};
|
|
|
|
document.location.search.replace(/\??(?:([^=]+)=([^&]*)&?)/g, function() {
|
|
function decode(s) {
|
|
return decodeURIComponent(s.split("+").join(" "));
|
|
}
|
|
|
|
getParams[decode(arguments[1])] = decode(arguments[2]);
|
|
});
|
|
|
|
// If we're on a playlist, but we don't have a video id in the URL - then get it from the frame API
|
|
if (typeof getParams['list'] !== 'undefined' && typeof getParams['v'] === 'undefined') {
|
|
let youtubeFrameAPI = document.getElementById('movie_player');
|
|
|
|
if (youtubeFrameAPI && typeof youtubeFrameAPI.getVideoData === 'function') {
|
|
let videoData = youtubeFrameAPI.getVideoData();
|
|
|
|
if (typeof videoData['video_id'] !== 'undefined' && videoData['video_id']) {
|
|
getParams['v'] = videoData['video_id'];
|
|
}
|
|
}
|
|
}
|
|
|
|
return getParams;
|
|
}
|
|
|
|
// Set a cookie
|
|
function goodTube_helper_setCookie(name, value) {
|
|
// 399 days
|
|
document.cookie = name + "=" + encodeURIComponent(value) + "; max-age=" + (399*24*60*60);
|
|
}
|
|
|
|
// Get a cookie
|
|
function goodTube_helper_getCookie(name) {
|
|
// Split the cookie string and get all individual name=value pairs in an array
|
|
let cookies = document.cookie.split(";");
|
|
|
|
// Loop through the array elements
|
|
for (let i = 0; i < cookies.length; i++) {
|
|
let cookie = cookies[i].split("=");
|
|
|
|
// Removing whitespace at the beginning of the cookie name and compare it with the given string
|
|
if (name == cookie[0].trim()) {
|
|
// Decode the cookie value and return
|
|
return decodeURIComponent(cookie[1]);
|
|
}
|
|
}
|
|
|
|
// Return null if not found
|
|
return null;
|
|
}
|
|
|
|
// Hide or show an element / youtube player
|
|
function goodTube_helper_hideElement_init() {
|
|
let style = document.createElement('style');
|
|
style.textContent = `
|
|
.goodTube_hidden {
|
|
position: fixed !important;
|
|
top: -9999px !important;
|
|
left: -9999px !important;
|
|
transform: scale(0) !important;
|
|
pointer-events: none !important;
|
|
}
|
|
|
|
.goodTube_hiddenPlayer {
|
|
position: relative;
|
|
overflow: hidden;
|
|
z-index: 1;
|
|
}
|
|
|
|
.goodTube_hiddenPlayer::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: #ffffff;
|
|
z-index: 998;
|
|
}
|
|
|
|
html[dark] .goodTube_hiddenPlayer::before {
|
|
background: #0f0f0f;
|
|
}
|
|
`;
|
|
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
function goodTube_helper_hideElement(element) {
|
|
if (element && !element.classList.contains('goodTube_hidden')) {
|
|
element.classList.add('goodTube_hidden');
|
|
}
|
|
}
|
|
|
|
function goodTube_helper_showElement(element) {
|
|
if (element && element.classList.contains('goodTube_hidden')) {
|
|
element.classList.remove('goodTube_hidden');
|
|
}
|
|
}
|
|
|
|
function goodTube_helper_hideYoutubePlayer(element) {
|
|
// Add a wrapping div to help avoid detection
|
|
if (!element.closest('.goodTube_hiddenPlayer')) {
|
|
let parent = element.parentNode;
|
|
let wrapper = document.createElement('div');
|
|
wrapper.classList.add('goodTube_hiddenPlayer');
|
|
parent.replaceChild(wrapper, element);
|
|
wrapper.appendChild(element);
|
|
}
|
|
}
|
|
|
|
|
|
/* Video server functions
|
|
------------------------------------------------------------------------------------------ */
|
|
// Check for custom video servers
|
|
function goodTube_server_custom(server) {
|
|
// Check setting
|
|
if (typeof goodTube_getParams['goodtube_customserver_'+server] !== 'undefined' && goodTube_getParams['goodtube_customserver_'+server] === 'false') {
|
|
goodTube_helper_setCookie('goodtube_customserver_'+server+'_name', 'false');
|
|
goodTube_helper_setCookie('goodtube_customserver_'+server+'_type', 'false');
|
|
goodTube_helper_setCookie('goodtube_customserver_'+server+'_proxy', 'false');
|
|
goodTube_helper_setCookie('goodtube_customserver_'+server+'_url', 'false');
|
|
}
|
|
|
|
if (typeof goodTube_getParams['goodtube_customserver_'+server+'_name'] !== 'undefined' && typeof goodTube_getParams['goodtube_customserver_'+server+'_type'] !== 'undefined' && typeof goodTube_getParams['goodtube_customserver_'+server+'_proxy'] !== 'undefined' && typeof goodTube_getParams['goodtube_customserver_'+server+'_url'] !== 'undefined') {
|
|
goodTube_helper_setCookie('goodtube_customserver_'+server+'_name', goodTube_getParams['goodtube_customserver_'+server+'_name']);
|
|
goodTube_helper_setCookie('goodtube_customserver_'+server+'_type', goodTube_getParams['goodtube_customserver_'+server+'_type']);
|
|
goodTube_helper_setCookie('goodtube_customserver_'+server+'_proxy', goodTube_getParams['goodtube_customserver_'+server+'_proxy']);
|
|
goodTube_helper_setCookie('goodtube_customserver_'+server+'_url', goodTube_getParams['goodtube_customserver_'+server+'_url']);
|
|
}
|
|
|
|
// If custom video server is enabled
|
|
if (goodTube_helper_getCookie('goodtube_customserver_'+server+'_name') && goodTube_helper_getCookie('goodtube_customserver_'+server+'_name') !== 'false') {
|
|
// Format the data (cookies are always strings)
|
|
let customServer_name = goodTube_helper_getCookie('goodtube_customserver_'+server+'_name');
|
|
let customServer_type = parseFloat(goodTube_helper_getCookie('goodtube_customserver_'+server+'_type'));
|
|
let customServer_url = goodTube_helper_getCookie('goodtube_customserver_'+server+'_url');
|
|
|
|
let customServer_proxy = goodTube_helper_getCookie('goodtube_customserver_'+server+'_proxy');
|
|
if (customServer_proxy === 'false') {
|
|
customServer_proxy = false;
|
|
}
|
|
else if (customServer_proxy === 'true') {
|
|
customServer_proxy = true;
|
|
}
|
|
|
|
// Add custom server to servers list
|
|
goodTube_videoServers.splice(1, 0, {
|
|
'name': customServer_name,
|
|
'type': customServer_type,
|
|
'proxy': customServer_proxy,
|
|
'url': customServer_url
|
|
});
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Custom video server '+server+' enabled ('+customServer_name+')');
|
|
}
|
|
|
|
// Do this for up to 10 custom servers
|
|
server++;
|
|
if (server < 10) {
|
|
goodTube_server_custom(server);
|
|
}
|
|
}
|
|
|
|
// Check for a local video server
|
|
function goodTube_server_local() {
|
|
// Check setting
|
|
if (typeof goodTube_getParams['goodtube_local'] !== 'undefined') {
|
|
if (goodTube_getParams['goodtube_local'] === 'true') {
|
|
goodTube_helper_setCookie('goodTube_local', 'true');
|
|
}
|
|
else if (goodTube_getParams['goodtube_local'] === 'false') {
|
|
goodTube_helper_setCookie('goodTube_local', 'false');
|
|
}
|
|
}
|
|
|
|
// If local video server is enabled
|
|
if (goodTube_helper_getCookie('goodTube_local') === 'true') {
|
|
// Add local video server to servers list
|
|
goodTube_videoServers.splice(1, 0, {
|
|
'name': 'LOCAL',
|
|
'type': 2,
|
|
'proxy': true,
|
|
'url': 'http://127.0.0.1:3000'
|
|
});
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Local video server enabled! 🚀');
|
|
}
|
|
}
|
|
|
|
|
|
/* Youtube functions
|
|
------------------------------------------------------------------------------------------ */
|
|
// Hide ads, shorts, etc - init
|
|
function goodTube_youtube_hideAdsShortsEtc() {
|
|
let style = document.createElement('style');
|
|
style.textContent = `
|
|
.ytd-search ytd-shelf-renderer,
|
|
ytd-reel-shelf-renderer,
|
|
ytd-merch-shelf-renderer,
|
|
ytd-action-companion-ad-renderer,
|
|
ytd-display-ad-renderer,
|
|
ytd-rich-section-renderer,
|
|
ytd-video-masthead-ad-advertiser-info-renderer,
|
|
ytd-video-masthead-ad-primary-video-renderer,
|
|
ytd-in-feed-ad-layout-renderer,
|
|
ytd-ad-slot-renderer,
|
|
ytd-statement-banner-renderer,
|
|
ytd-banner-promo-renderer-background
|
|
ytd-ad-slot-renderer,
|
|
ytd-in-feed-ad-layout-renderer,
|
|
ytd-engagement-panel-section-list-renderer:not(.ytd-popup-container):not([target-id='engagement-panel-clip-create']),
|
|
ytd-compact-video-renderer:has(.goodTube_hidden),
|
|
ytd-rich-item-renderer:has(> #content > ytd-ad-slot-renderer)
|
|
.ytd-video-masthead-ad-v3-renderer,
|
|
div#root.style-scope.ytd-display-ad-renderer.yt-simple-endpoint,
|
|
div#sparkles-container.style-scope.ytd-promoted-sparkles-web-renderer,
|
|
div#main-container.style-scope.ytd-promoted-video-renderer,
|
|
div#player-ads.style-scope.ytd-watch-flexy,
|
|
#clarify-box,
|
|
|
|
ytm-rich-shelf-renderer,
|
|
ytm-search ytm-shelf-renderer,
|
|
ytm-button-renderer.icon-avatar_logged_out,
|
|
ytm-companion-slot,
|
|
ytm-reel-shelf-renderer,
|
|
ytm-merch-shelf-renderer,
|
|
ytm-action-companion-ad-renderer,
|
|
ytm-display-ad-renderer,
|
|
ytm-rich-section-renderer,
|
|
ytm-video-masthead-ad-advertiser-info-renderer,
|
|
ytm-video-masthead-ad-primary-video-renderer,
|
|
ytm-in-feed-ad-layout-renderer,
|
|
ytm-ad-slot-renderer,
|
|
ytm-statement-banner-renderer,
|
|
ytm-banner-promo-renderer-background
|
|
ytm-ad-slot-renderer,
|
|
ytm-in-feed-ad-layout-renderer,
|
|
ytm-compact-video-renderer:has(.goodTube_hidden),
|
|
ytm-rich-item-renderer:has(> #content > ytm-ad-slot-renderer)
|
|
.ytm-video-masthead-ad-v3-renderer,
|
|
div#root.style-scope.ytm-display-ad-renderer.yt-simple-endpoint,
|
|
div#sparkles-container.style-scope.ytm-promoted-sparkles-web-renderer,
|
|
div#main-container.style-scope.ytm-promoted-video-renderer,
|
|
div#player-ads.style-scope.ytm-watch-flexy,
|
|
ytm-pivot-bar-item-renderer:has(> .pivot-shorts),
|
|
ytd-compact-movie-renderer,
|
|
|
|
yt-about-this-ad-renderer,
|
|
masthead-ad,
|
|
ad-slot-renderer,
|
|
yt-mealbar-promo-renderer,
|
|
statement-banner-style-type-compact,
|
|
ytm-promoted-sparkles-web-renderer,
|
|
tp-yt-iron-overlay-backdrop,
|
|
#masthead-ad
|
|
{
|
|
display: none !important;
|
|
}
|
|
|
|
.style-scope[page-subtype='channels'] ytd-shelf-renderer,
|
|
.style-scope[page-subtype='channels'] ytm-shelf-renderer {
|
|
display: block !important;
|
|
}
|
|
`;
|
|
|
|
document.head.appendChild(style);
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Ads removed');
|
|
}
|
|
|
|
// Hide shorts
|
|
function goodTube_youtube_hideShorts() {
|
|
// If we're on a channel page, don't hide shorts
|
|
if (window.location.href.indexOf('@') !== -1) {
|
|
return;
|
|
}
|
|
|
|
// Hide shorts links
|
|
let shortsLinks = document.querySelectorAll('a:not(.goodTube_hidden)');
|
|
shortsLinks.forEach((element) => {
|
|
if (element.href.indexOf('shorts/') !== -1) {
|
|
goodTube_helper_hideElement(element);
|
|
goodTube_helper_hideElement(element.closest('ytd-video-renderer'));
|
|
goodTube_helper_hideElement(element.closest('ytd-compact-video-renderer'));
|
|
goodTube_helper_hideElement(element.closest('ytd-rich-grid-media'));
|
|
}
|
|
});
|
|
}
|
|
|
|
// Support timestamp links in comments
|
|
function goodTube_youtube_timestampLinks() {
|
|
// Links in video description and comments
|
|
let timestampLinks = document.querySelectorAll('#description a, ytd-comments .yt-core-attributed-string a, ytm-expandable-video-description-body-renderer a, .comment-content a');
|
|
|
|
// For each link
|
|
timestampLinks.forEach((element) => {
|
|
// Make sure we've not touched it yet, this stops doubling up on event listeners
|
|
if (!element.classList.contains('goodTube_timestampLink') && element.getAttribute('href') && element.getAttribute('href').indexOf(goodTube_getParams['v']) !== -1 && element.getAttribute('href').indexOf('t=') !== -1) {
|
|
element.classList.add('goodTube_timestampLink');
|
|
|
|
// Add the event listener to send our player to the correct time
|
|
element.addEventListener('click', function() {
|
|
let bits = element.getAttribute('href').split('t=');
|
|
if (typeof bits[1] !== 'undefined') {
|
|
let time = bits[1].replace('s', '');
|
|
goodTube_player_skipTo(goodTube_player, time);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Make the youtube player the lowest quality to save on bandwidth (via the frame API)
|
|
function goodTube_youtube_lowestQuality() {
|
|
let youtubeFrameAPI = document.getElementById('movie_player');
|
|
|
|
if (youtubeFrameAPI && typeof youtubeFrameAPI.setPlaybackQualityRange === 'function' && typeof youtubeFrameAPI.getAvailableQualityData === 'function' && typeof youtubeFrameAPI.getPlaybackQuality === 'function') {
|
|
let qualities = youtubeFrameAPI.getAvailableQualityData();
|
|
let currentQuality = youtubeFrameAPI.getPlaybackQuality();
|
|
if (qualities.length && currentQuality) {
|
|
let lowestQuality = qualities[qualities.length-1]['quality'];
|
|
|
|
if (currentQuality != lowestQuality) {
|
|
youtubeFrameAPI.setPlaybackQualityRange(lowestQuality, lowestQuality);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hide all Youtube players
|
|
function goodTube_youtube_hidePlayers() {
|
|
// Hide the normal Youtube player
|
|
let regularPlayers = document.querySelectorAll('#player:not(.ytd-channel-video-player-renderer)');
|
|
regularPlayers.forEach((element) => {
|
|
goodTube_helper_hideYoutubePlayer(element);
|
|
});
|
|
|
|
// Remove the full screen and theater Youtube player
|
|
let fullscreenPlayers = document.querySelectorAll('#full-bleed-container');
|
|
fullscreenPlayers.forEach((element) => {
|
|
goodTube_helper_hideYoutubePlayer(element);
|
|
});
|
|
|
|
// Hide the mobile controls
|
|
let mobileControls = document.querySelectorAll('#player-control-container');
|
|
mobileControls.forEach((element) => {
|
|
goodTube_helper_hideElement(element);
|
|
});
|
|
|
|
// Hide the Youtube miniplayer
|
|
let miniPlayers = document.querySelectorAll('ytd-miniplayer');
|
|
miniPlayers.forEach((element) => {
|
|
goodTube_helper_hideElement(element);
|
|
});
|
|
}
|
|
|
|
// Turn off autoplay
|
|
let goodTube_turnedOffAutoplay = false;
|
|
function goodTube_youtube_turnOffAutoplay() {
|
|
// If we've already turned off autoplay, just return
|
|
if (goodTube_turnedOffAutoplay) {
|
|
return;
|
|
}
|
|
|
|
let autoplayButton = false;
|
|
|
|
// Desktop
|
|
if (!goodTube_mobile) {
|
|
// Target the autoplay button
|
|
autoplayButton = document.querySelector('.ytp-autonav-toggle-button');
|
|
|
|
// If we found it
|
|
if (autoplayButton) {
|
|
// Set a variable if autoplay has been turned off
|
|
if (autoplayButton.getAttribute('aria-checked') === 'false') {
|
|
goodTube_turnedOffAutoplay = true;
|
|
return;
|
|
}
|
|
// Otherwise click the button
|
|
else {
|
|
autoplayButton.click();
|
|
}
|
|
}
|
|
}
|
|
// Mobile
|
|
else {
|
|
// Autoplay is always on for mobile now, we can't control it sadly...
|
|
|
|
// // Target the autoplay button
|
|
// autoplayButton = document.querySelector('.ytm-autonav-toggle-button-container');
|
|
|
|
// // If we found it
|
|
// if (autoplayButton) {
|
|
// // Set a variable if autoplay has been turned off
|
|
// if (autoplayButton.getAttribute('aria-pressed') === 'false') {
|
|
// goodTube_turnedOffAutoplay = true;
|
|
// return;
|
|
// }
|
|
// // Otherwise click the button
|
|
// else {
|
|
// autoplayButton.click();
|
|
// }
|
|
// }
|
|
// // If we didn't find it - click the player a bit, this helps to actually make the autoplay button show (after ads)
|
|
// else {
|
|
// document.querySelector('#player .html5-video-player')?.click();
|
|
// document.querySelector('#player').click();
|
|
// document.querySelector('.ytp-unmute')?.click();
|
|
// }
|
|
}
|
|
}
|
|
|
|
// Sync players
|
|
let goodTube_youtube_syncing = false;
|
|
let goodTube_youtube_previousSyncTime = 0;
|
|
function goodTube_youtube_syncPlayers() {
|
|
let youtubeVideo = document.querySelector('#movie_player video');
|
|
|
|
// If the youtube player exists, our player is loaded and we're viewing a video
|
|
if (youtubeVideo && goodTube_videojs_player_loaded && typeof goodTube_getParams['v'] !== 'undefined') {
|
|
// Don't keep syncing the same time over and over unless it's the start of the video
|
|
let syncTime = goodTube_player.currentTime;
|
|
if (syncTime === goodTube_youtube_previousSyncTime && parseFloat(syncTime) > 0) {
|
|
return;
|
|
}
|
|
|
|
// Setup the previous sync time
|
|
goodTube_youtube_previousSyncTime = syncTime;
|
|
|
|
// Set the current time of the Youtube player to match ours (this makes history and watched time work correctly)
|
|
youtubeVideo.currentTime = syncTime;
|
|
|
|
// We're syncing (this turns off the pausing of the Youtube video in goodTube_youtube_mutePauseSkipAds)
|
|
goodTube_youtube_syncing = true;
|
|
|
|
// Play for 10ms to make history work via JS
|
|
youtubeVideo.play();
|
|
youtubeVideo.muted = true;
|
|
youtubeVideo.volume = 0;
|
|
|
|
// Play for 10ms to make history work via the frame API
|
|
let youtubeFrameApi = document.querySelector('#movie_player');
|
|
if (youtubeFrameApi) {
|
|
if (typeof youtubeFrameApi.playVideo === 'function') {
|
|
youtubeFrameApi.playVideo();
|
|
}
|
|
|
|
if (typeof youtubeFrameApi.mute === 'function') {
|
|
youtubeFrameApi.mute();
|
|
}
|
|
|
|
if (typeof youtubeFrameApi.setVolume === 'function') {
|
|
youtubeFrameApi.setVolume(0);
|
|
}
|
|
}
|
|
|
|
// Stop syncing after 10ms (this turns on the pausing of the Youtube video in goodTube_youtube_mutePauseSkipAds)
|
|
setTimeout(function() {
|
|
goodTube_youtube_syncing = false;
|
|
}, 10);
|
|
}
|
|
}
|
|
|
|
// Mute, pause and skip ads on all Youtube videos
|
|
function goodTube_youtube_mutePauseSkipAds() {
|
|
// // Always skip the ads as soon as possible by clicking the skip button
|
|
// let skipButton = document.querySelector('.ytp-skip-ad-button');
|
|
// if (skipButton) {
|
|
// skipButton.click();
|
|
// }
|
|
|
|
// Pause and mute all HTML videos on the page that are not GoodTube
|
|
let youtubeVideos = document.querySelectorAll('video:not(#goodTube_player):not(#goodTube_player_html5_api)');
|
|
youtubeVideos.forEach((element) => {
|
|
// Don't touch the thumbnail hover player
|
|
if (!element.closest('#inline-player')) {
|
|
element.muted = true;
|
|
element.volume = 0;
|
|
|
|
if (!goodTube_youtube_syncing) {
|
|
element.pause();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/* Video JS functions
|
|
------------------------------------------------------------------------------------------ */
|
|
let goodTube_videojs_player = false;
|
|
let goodTube_videojs_player_loaded = false;
|
|
let goodTube_videojs_tapTimer_backwards = false;
|
|
let goodTube_videojs_tapTimer_forwards = false;
|
|
let goodTube_videojs_fastForwardTimeout = false;
|
|
let goodTube_videojs_fastForward = false;
|
|
let goodTube_qualityApi = false;
|
|
let goodTube_bufferingTimeout = false;
|
|
let goodTube_bufferCountTimeout = false;
|
|
let goodTube_loadingTimeout = false;
|
|
let goodTube_seeking = false;
|
|
let goodTube_bufferCount = 0;
|
|
let goodTube_videojs_playbackRate = 1;
|
|
let goodTube_player_restoreTime = 0;
|
|
|
|
// Init video js
|
|
function goodTube_videojs_init() {
|
|
// Debug message
|
|
console.log('[GoodTube] Loading player...');
|
|
|
|
|
|
// Style video js
|
|
goodTube_videojs_style();
|
|
|
|
|
|
// Add custom menu buttons
|
|
const MenuItem = videojs.getComponent("MenuItem");
|
|
const MenuButton = videojs.getComponent("MenuButton");
|
|
|
|
class CustomMenuButton extends MenuButton {
|
|
createItems() {
|
|
const items = [];
|
|
const { myItems } = this.options_;
|
|
|
|
if (!Array.isArray(myItems)) items;
|
|
|
|
myItems.forEach(({ clickHandler, ...item }) => {
|
|
const menuItem = new MenuItem(this.player(), item);
|
|
|
|
if (clickHandler) {
|
|
menuItem.handleClick = clickHandler;
|
|
}
|
|
|
|
items.push(menuItem);
|
|
});
|
|
|
|
return items;
|
|
}
|
|
|
|
buildCSSClass() {
|
|
return `${super.buildCSSClass()}`;
|
|
}
|
|
}
|
|
|
|
videojs.registerComponent("DownloadButton", CustomMenuButton);
|
|
videojs.registerComponent("SourceButton", CustomMenuButton);
|
|
videojs.registerComponent("AutoplayButton", CustomMenuButton);
|
|
|
|
|
|
// Add custom normal buttons
|
|
const Button = videojs.getComponent("Button");
|
|
|
|
class PrevButton extends Button {
|
|
handleClick(event) {
|
|
event.stopImmediatePropagation();
|
|
goodTube_nav_prev();
|
|
}
|
|
}
|
|
videojs.registerComponent('PrevButton', PrevButton);
|
|
|
|
class NextButton extends Button {
|
|
handleClick(event) {
|
|
event.stopImmediatePropagation();
|
|
goodTube_nav_next(true);
|
|
}
|
|
}
|
|
videojs.registerComponent('NextButton', NextButton);
|
|
|
|
class MiniplayerButton extends Button {
|
|
handleClick(event) {
|
|
event.stopImmediatePropagation();
|
|
goodTube_miniplayer_showHide();
|
|
}
|
|
}
|
|
videojs.registerComponent('MiniplayerButton', MiniplayerButton);
|
|
|
|
class TheaterButton extends Button {
|
|
handleClick(event) {
|
|
event.stopImmediatePropagation();
|
|
goodTube_shortcuts_trigger('theater');
|
|
}
|
|
}
|
|
videojs.registerComponent('TheaterButton', TheaterButton);
|
|
|
|
|
|
// Setup the API selection
|
|
let apiList = [];
|
|
|
|
goodTube_videoServers.forEach((api) => {
|
|
apiList.push({
|
|
label: api['name'],
|
|
clickHandler(event) {
|
|
// Get the menu
|
|
let menu = event.target.closest('.vjs-menu');
|
|
|
|
// Deselect the currently selected menu items
|
|
let selectedMenuItems = menu.querySelectorAll('.vjs-selected');
|
|
selectedMenuItems.forEach((selectedMenuItem) => {
|
|
selectedMenuItem.classList.remove('vjs-selected');
|
|
});
|
|
|
|
// Select the clicked menu item
|
|
let menuItem = event.target.closest('.vjs-menu-item');
|
|
menuItem.classList.add('vjs-selected');
|
|
|
|
// If we selected automatic, reset the server index so it tries them all
|
|
if (menuItem.getAttribute('api') === 'automatic') {
|
|
goodTube_videoServer_automaticIndex = 0;
|
|
}
|
|
|
|
// Set the player time to be restored when the new server loads
|
|
if (goodTube_player.currentTime > 0) {
|
|
goodTube_player_restoreTime = goodTube_player.currentTime;
|
|
}
|
|
|
|
// Set the new API
|
|
goodTube_player_selectVideoServer(menuItem.getAttribute('api'), true);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Init the player
|
|
goodTube_videojs_player = videojs('goodTube_player', {
|
|
inactivityTimeout: 3000,
|
|
controls: true,
|
|
autoplay: false,
|
|
preload: 'auto',
|
|
width: '100%',
|
|
height: '100%',
|
|
playbackRates: [0.25, 0.5, 1, 1.25, 1.5, 1.75, 2],
|
|
userActions: {
|
|
doubleClick: false
|
|
},
|
|
// This fixes issues with the quality selector on iOS
|
|
html5: {
|
|
vhs: {
|
|
overrideNative: true
|
|
},
|
|
hls: {
|
|
overrideNative: true
|
|
}
|
|
},
|
|
controlBar: {
|
|
children: [
|
|
'playToggle',
|
|
'volumePanel',
|
|
'currentTimeDisplay',
|
|
'timeDivider',
|
|
'durationDisplay',
|
|
'progressControl',
|
|
'playbackRateMenuButton',
|
|
'subsCapsButton',
|
|
'qualitySelector',
|
|
'fullscreenToggle'
|
|
],
|
|
|
|
// Add next button
|
|
NextButton: {
|
|
className: "vjs-next-button"
|
|
},
|
|
|
|
// Add prev button
|
|
PrevButton: {
|
|
className: "vjs-prev-button"
|
|
},
|
|
|
|
// Add autoplay button
|
|
AutoplayButton: {
|
|
controlText: "Autoplay",
|
|
className: "vjs-autoplay-button",
|
|
myItems: [
|
|
{
|
|
label: "Autoplay off",
|
|
clickHandler() {
|
|
// Get the menu
|
|
let menu = event.target.closest('.vjs-menu');
|
|
|
|
// Deselect the currently selected menu item
|
|
menu.querySelector('.vjs-selected')?.classList.remove('vjs-selected');
|
|
|
|
// Select the clicked menu item
|
|
let menuItem = event.target.closest('.vjs-menu-item');
|
|
menuItem.classList.add('vjs-selected');
|
|
|
|
goodTube_helper_setCookie('goodTube_autoplay', 'off');
|
|
},
|
|
},
|
|
{
|
|
label: "Autoplay on",
|
|
clickHandler() {
|
|
// Get the menu
|
|
let menu = event.target.closest('.vjs-menu');
|
|
|
|
// Deselect the currently selected menu item
|
|
menu.querySelector('.vjs-selected')?.classList.remove('vjs-selected');
|
|
|
|
// Select the clicked menu item
|
|
let menuItem = event.target.closest('.vjs-menu-item');
|
|
menuItem.classList.add('vjs-selected');
|
|
|
|
goodTube_helper_setCookie('goodTube_autoplay', 'on');
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// Add source button
|
|
SourceButton: {
|
|
controlText: "Video source",
|
|
className: "vjs-source-button",
|
|
myItems: apiList,
|
|
},
|
|
|
|
// Add download button
|
|
DownloadButton: {
|
|
controlText: "Download",
|
|
className: "vjs-download-button",
|
|
myItems: [
|
|
{
|
|
className: 'goodTube_download_playlist_cancel',
|
|
label: "CANCEL ALL DOWNLOADS",
|
|
clickHandler() {
|
|
goodTube_download_cancelAll();
|
|
},
|
|
},
|
|
{
|
|
label: "Download video",
|
|
clickHandler() {
|
|
// Add to pending downloads
|
|
goodTube_pendingDownloads[goodTube_getParams['v']] = true;
|
|
|
|
// Download the video
|
|
goodTube_download_addToQue(0, 'video', goodTube_getParams['v']);
|
|
},
|
|
},
|
|
{
|
|
label: "Download audio",
|
|
clickHandler() {
|
|
// Add to pending downloads
|
|
goodTube_pendingDownloads[goodTube_getParams['v']] = true;
|
|
|
|
// Download the audio
|
|
goodTube_download_addToQue(0, 'audio', goodTube_getParams['v']);
|
|
},
|
|
},
|
|
{
|
|
className: 'goodTube_download_playlist_video',
|
|
label: "Download playlist (video)",
|
|
clickHandler() {
|
|
goodTube_download_playlist('video');
|
|
},
|
|
},
|
|
{
|
|
className: 'goodTube_download_playlist_audio',
|
|
label: "Download playlist (audio)",
|
|
clickHandler() {
|
|
goodTube_download_playlist('audio');
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// Add miniplayer button
|
|
MiniplayerButton: {
|
|
className: "vjs-miniplayer-button"
|
|
},
|
|
|
|
// Add theater button
|
|
TheaterButton: {
|
|
className: "vjs-theater-button"
|
|
},
|
|
}
|
|
});
|
|
|
|
// Disable console errors from video js
|
|
videojs.log.level('off');
|
|
|
|
// If for any reason the video failed to load, try reloading it again
|
|
videojs.hook('error', function(error) {
|
|
// Ensure we're viewing a video
|
|
if (!goodTube_player.getAttribute('src')) {
|
|
return;
|
|
}
|
|
|
|
if (typeof goodTube_pendingRetry['reloadVideo'] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['reloadVideo']);
|
|
}
|
|
|
|
goodTube_pendingRetry['reloadVideo'] = setTimeout(function() {
|
|
goodTube_video_reloadVideo(goodTube_player);
|
|
}, goodTube_retryDelay);
|
|
|
|
// Add the loading state
|
|
goodTube_player_addLoadingState();
|
|
|
|
// Update the video js player
|
|
goodTube_videojs_update();
|
|
});
|
|
|
|
// After video JS has loaded
|
|
goodTube_videojs_player.on('ready', function() {
|
|
goodTube_videojs_player_loaded = true;
|
|
|
|
// Add the playsinline attributes (this stops iOS from automatically going fullscreen)
|
|
let video = document.querySelector('#goodTube_player video');
|
|
if (video) {
|
|
video.setAttribute('playsinline', '');
|
|
video.setAttribute('webkit-playsinline', '');
|
|
}
|
|
|
|
// Sync the Youtube player for watch history
|
|
goodTube_youtube_syncPlayers();
|
|
|
|
// Enable the qualities API
|
|
goodTube_qualityApi = goodTube_videojs_player.hlsQualitySelector();
|
|
|
|
// Add expand and close miniplayer buttons
|
|
let goodTube_target = document.querySelector('#goodTube_player');
|
|
|
|
if (goodTube_target) {
|
|
let miniplayer_closeButton = document.createElement('div');
|
|
miniplayer_closeButton.id = 'goodTube_miniplayer_closeButton';
|
|
miniplayer_closeButton.onclick = function() {
|
|
goodTube_miniplayer_showHide();
|
|
};
|
|
goodTube_target.appendChild(miniplayer_closeButton);
|
|
|
|
let miniplayer_expandButton = document.createElement('div');
|
|
miniplayer_expandButton.id = 'goodTube_miniplayer_expandButton';
|
|
miniplayer_expandButton.onclick = function() {
|
|
if (goodTube_miniplayer_video !== goodTube_getParams['v']) {
|
|
window.location.href = '/watch?v='+goodTube_miniplayer_video+'&t='+parseFloat(goodTube_player.currentTime).toFixed(0)+'s';
|
|
}
|
|
else {
|
|
goodTube_miniplayer_showHide();
|
|
}
|
|
};
|
|
goodTube_target.appendChild(miniplayer_expandButton);
|
|
}
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Player loaded');
|
|
|
|
// Re-target the goodTube player globally (as video JS shifts this element)
|
|
goodTube_player = document.querySelector('#goodTube_player video');
|
|
|
|
// Attach mobile seeking events
|
|
if (goodTube_mobile) {
|
|
// Attach the backwards seek button
|
|
let goodTube_seekBackwards = document.createElement('div');
|
|
goodTube_seekBackwards.id = 'goodTube_seekBackwards';
|
|
goodTube_target.append(goodTube_seekBackwards);
|
|
|
|
// Double tap event to seek backwards
|
|
goodTube_seekBackwards.onclick = function() {
|
|
// Get the time
|
|
var now = new Date().getTime();
|
|
|
|
// Check how long since last tap
|
|
var timesince = now - goodTube_videojs_tapTimer_backwards;
|
|
|
|
// If it's less than 400ms
|
|
if ((timesince < 400) && (timesince > 0)) {
|
|
// Remove active state and hide overlays (so you can see the video properly)
|
|
goodTube_target.classList.remove('vjs-user-active');
|
|
goodTube_target.classList.add('vjs-user-inactive');
|
|
|
|
// Seek backwards 10 seconds
|
|
goodTube_player.currentTime -= 10;
|
|
}
|
|
// If it's just a normal tap
|
|
else {
|
|
// Swap to opposite state of active / inactive
|
|
if (goodTube_target.classList.contains('vjs-user-active')) {
|
|
goodTube_target.classList.remove('vjs-user-active');
|
|
goodTube_target.classList.add('vjs-user-inactive');
|
|
}
|
|
else {
|
|
goodTube_target.classList.add('vjs-user-active');
|
|
goodTube_target.classList.remove('vjs-user-inactive');
|
|
}
|
|
}
|
|
|
|
// Set the last tap time
|
|
goodTube_videojs_tapTimer_backwards = new Date().getTime();
|
|
}
|
|
|
|
|
|
// Attach the forwards seek button
|
|
let goodTube_seekForwards = document.createElement('div');
|
|
goodTube_seekForwards.id = 'goodTube_seekForwards';
|
|
goodTube_target.append(goodTube_seekForwards);
|
|
|
|
goodTube_seekForwards.onclick = function() {
|
|
// Get the time
|
|
var now = new Date().getTime();
|
|
|
|
// Check how long since last tap
|
|
var timesince = now - goodTube_videojs_tapTimer_forwards;
|
|
|
|
// If it's less than 400ms
|
|
if ((timesince < 400) && (timesince > 0)) {
|
|
// Remove active state and hide overlays (so you can see the video properly)
|
|
goodTube_target.classList.remove('vjs-user-active');
|
|
goodTube_target.classList.add('vjs-user-inactive');
|
|
|
|
// Seek forwards 5 seconds
|
|
goodTube_player.currentTime += 5;
|
|
}
|
|
// If it's just a normal tap
|
|
else {
|
|
// Swap to opposite state of active / inactive
|
|
if (goodTube_target.classList.contains('vjs-user-active')) {
|
|
goodTube_target.classList.remove('vjs-user-active');
|
|
goodTube_target.classList.add('vjs-user-inactive');
|
|
}
|
|
else {
|
|
goodTube_target.classList.add('vjs-user-active');
|
|
goodTube_target.classList.remove('vjs-user-inactive');
|
|
}
|
|
}
|
|
|
|
// Set the last tap time
|
|
goodTube_videojs_tapTimer_forwards = new Date().getTime();
|
|
}
|
|
|
|
|
|
// Long press to fast forward
|
|
|
|
// On touch start
|
|
goodTube_target.addEventListener('touchstart', function(e) {
|
|
// Start fast forward after 1 second
|
|
goodTube_videojs_fastForwardTimeout = setTimeout(function() {
|
|
// Remove active state and hide overlays (so you can see the video properly)
|
|
goodTube_target.classList.remove('vjs-user-active');
|
|
goodTube_target.classList.add('vjs-user-inactive');
|
|
|
|
// Save the current playback rate
|
|
goodTube_videojs_playbackRate = goodTube_player.playbackRate;
|
|
|
|
// Set playback rate to 2x (fast forward)
|
|
goodTube_player.playbackRate = 2;
|
|
|
|
// Set a variable to indicate that we're fast forwarding
|
|
goodTube_videojs_fastForward = true;
|
|
}, 1000);
|
|
});
|
|
|
|
// On touch move / touch end
|
|
['touchmove','touchend', 'touchcancel'].forEach(eventType => {
|
|
goodTube_target.addEventListener(eventType, function(e) {
|
|
// Remove any pending timeouts to fast forward
|
|
if (goodTube_videojs_fastForwardTimeout) {
|
|
clearTimeout(goodTube_videojs_fastForwardTimeout);
|
|
}
|
|
|
|
// If we are fast forwarding
|
|
if (goodTube_videojs_fastForward) {
|
|
// Restore the current playback rate
|
|
goodTube_player.playbackRate = goodTube_videojs_playbackRate;
|
|
|
|
// Set a variable to indicate that we're not fast forwarding anymore
|
|
goodTube_videojs_fastForward = false;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Double click to fullscreen (desktop only)
|
|
if (!goodTube_mobile) {
|
|
goodTube_target.addEventListener('dblclick', function(event) {
|
|
// Make sure we're not clicking a menu button or the seek bar
|
|
if (!event.target.closest('.vjs-progress-control') && !event.target.closest('.vjs-menu-button') && !event.target.closest('.vjs-control')) {
|
|
// Click the fullscreen button
|
|
document.querySelector('.vjs-fullscreen-control')?.click();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Active and inactive control based on mouse movement (desktop only)
|
|
if (!goodTube_mobile) {
|
|
// Mouse off make inactive
|
|
goodTube_target.addEventListener('mouseout', function(event) {
|
|
if (goodTube_target.classList.contains('vjs-user-active') && !goodTube_target.classList.contains('vjs-paused')) {
|
|
goodTube_target.classList.remove('vjs-user-active');
|
|
goodTube_target.classList.add('vjs-user-inactive');
|
|
}
|
|
});
|
|
|
|
// Mouse over make active
|
|
goodTube_target.addEventListener('mouseover', function(event) {
|
|
if (goodTube_target.classList.contains('vjs-user-inactive') && !goodTube_target.classList.contains('vjs-paused')) {
|
|
goodTube_target.classList.add('vjs-user-active');
|
|
goodTube_target.classList.remove('vjs-user-inactive');
|
|
}
|
|
});
|
|
|
|
// Click to play, don't make inactive (override video js default behavior)
|
|
goodTube_target.addEventListener('click', function(event) {
|
|
setTimeout(function() {
|
|
if (goodTube_target.classList.contains('vjs-user-inactive') && !goodTube_target.classList.contains('vjs-paused')) {
|
|
goodTube_target.classList.add('vjs-user-active');
|
|
goodTube_target.classList.remove('vjs-user-inactive');
|
|
|
|
// Set a timeout to make inactive (to replace video js default behavior)
|
|
window.goodTube_inactive_timeout = setTimeout(function() {
|
|
if (goodTube_target.classList.contains('vjs-user-active') && !goodTube_target.classList.contains('vjs-paused')) {
|
|
goodTube_target.classList.remove('vjs-user-active');
|
|
goodTube_target.classList.add('vjs-user-inactive');
|
|
}
|
|
}, 3000);
|
|
}
|
|
}, 1);
|
|
});
|
|
|
|
// If they move the mouse, remove our timeout to make inactive (return to video js default behavior)
|
|
goodTube_target.addEventListener('mousemove', function(event) {
|
|
if (typeof window.goodTube_inactive_timeout !== 'undefined') {
|
|
clearTimeout(window.goodTube_inactive_timeout);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Remove all title attributes from buttons, we don't want hover text
|
|
let buttons = document.querySelectorAll('#goodTube_player button');
|
|
buttons.forEach((element) => {
|
|
element.setAttribute('title', '');
|
|
});
|
|
|
|
|
|
// Set the default volume (if a cookie exists for it)
|
|
let volume = goodTube_helper_getCookie('goodTube_volume');
|
|
if (volume && volume == parseFloat(volume)) {
|
|
goodTube_player_volume(goodTube_player, volume);
|
|
}
|
|
|
|
|
|
// Autoplay
|
|
// If autoplay cookie doesn't exist, or we're on mobile, turn autoplay on (as it's forced for mobile now)
|
|
if (!goodTube_helper_getCookie('goodTube_autoplay') || goodTube_mobile) {
|
|
goodTube_helper_setCookie('goodTube_autoplay', 'on');
|
|
}
|
|
|
|
// Select the correct autoplay button
|
|
let autoplayButton = document.querySelector('.vjs-autoplay-button');
|
|
|
|
if (autoplayButton) {
|
|
// Deselect all our autoplay menu items
|
|
autoplayButton.querySelector('.vjs-menu .vjs-selected')?.classList.remove('vjs-selected');
|
|
|
|
// Select the correct autoplay menu item
|
|
let autoplay_menuItems = autoplayButton.querySelectorAll('.vjs-menu .vjs-menu-item');
|
|
|
|
if (goodTube_helper_getCookie('goodTube_autoplay') === 'on') {
|
|
autoplay_menuItems[autoplay_menuItems.length- 1].classList.add('vjs-selected');
|
|
}
|
|
else {
|
|
autoplay_menuItems[0].classList.add('vjs-selected');
|
|
}
|
|
}
|
|
|
|
// Make mute button work
|
|
let muteButton = document.querySelector('.vjs-mute-control');
|
|
if (muteButton) {
|
|
muteButton.onmousedown = function() {
|
|
if (goodTube_player.muted) {
|
|
goodTube_videojs_player.muted(false);
|
|
}
|
|
else {
|
|
goodTube_videojs_player.muted(true);
|
|
}
|
|
}
|
|
|
|
muteButton.ontouchstart = function() {
|
|
if (goodTube_player.muted) {
|
|
goodTube_videojs_player.muted(false);
|
|
}
|
|
else {
|
|
goodTube_videojs_player.muted(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make clicking the play / pause button work
|
|
let playPauseButton = document.querySelector('.vjs-play-control');
|
|
if (playPauseButton) {
|
|
playPauseButton.removeEventListener('click', goodTube_player_togglePlayPause, false);
|
|
playPauseButton.addEventListener('click', goodTube_player_togglePlayPause, false);
|
|
}
|
|
|
|
// Click off close menu
|
|
document.onmousedown = function() {
|
|
if (!event.target.closest('.vjs-menu') && !event.target.closest('.vjs-menu-button')) {
|
|
let openMenuButtons = document.querySelectorAll('.vjs-menuOpen');
|
|
|
|
openMenuButtons.forEach((openMenuButton) => {
|
|
openMenuButton.classList.remove('vjs-menuOpen');
|
|
});
|
|
}
|
|
}
|
|
|
|
document.ontouchstart = function() {
|
|
if (!event.target.closest('.vjs-menu') && !event.target.closest('.vjs-menu-button')) {
|
|
let openMenuButtons = document.querySelectorAll('.vjs-menuOpen');
|
|
|
|
openMenuButtons.forEach((openMenuButton) => {
|
|
openMenuButton.classList.remove('vjs-menuOpen');
|
|
});
|
|
}
|
|
}
|
|
|
|
// Make replay button work
|
|
let playButton = document.querySelector('.vjs-control-bar .vjs-play-control');
|
|
if (playButton) {
|
|
playButton.onclick = function() {
|
|
if (goodTube_player.currentTime === 0) {
|
|
goodTube_player.click();
|
|
}
|
|
}
|
|
|
|
playButton.ontouchstart = function() {
|
|
if (goodTube_player.currentTime === 0) {
|
|
goodTube_player.click();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add URL param to default video source menu items
|
|
let sourceMenuItems = document.querySelectorAll('.vjs-source-button .vjs-menu .vjs-menu-item');
|
|
if (sourceMenuItems) {
|
|
let i = 0;
|
|
|
|
sourceMenuItems.forEach((sourceMenuItem) => {
|
|
sourceMenuItem.setAttribute('api', goodTube_videoServers[i]['url']);
|
|
i++;
|
|
});
|
|
}
|
|
|
|
// If they're on iOS - hide the download button
|
|
if (goodTube_helper_iOS()) {
|
|
let downloadButton = document.querySelector('.vjs-download-button');
|
|
if (downloadButton) {
|
|
downloadButton.remove();
|
|
}
|
|
}
|
|
|
|
// Init the API selection
|
|
goodTube_player_selectVideoServer(goodTube_helper_getCookie('goodTube_videoServer_withauto'), false);
|
|
|
|
// Update the video js player
|
|
goodTube_videojs_update();
|
|
});
|
|
|
|
// Esc keypress close menus
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.keyCode == 27) {
|
|
let openMenuButtons = document.querySelectorAll('.vjs-menuOpen');
|
|
|
|
openMenuButtons.forEach((openMenuButton) => {
|
|
openMenuButton.classList.remove('vjs-menuOpen');
|
|
});
|
|
}
|
|
}, true);
|
|
|
|
// Seeking events
|
|
goodTube_videojs_player.on('seeking', function() {
|
|
goodTube_seeking = true;
|
|
});
|
|
|
|
goodTube_videojs_player.on('seeked', function() {
|
|
goodTube_seeking = false;
|
|
|
|
// Sync the Youtube player for watch history
|
|
goodTube_youtube_syncPlayers();
|
|
});
|
|
|
|
// On buffering / loading
|
|
goodTube_videojs_player.on('waiting', function() {
|
|
// Clear any buffering timeouts
|
|
if (goodTube_bufferingTimeout) {
|
|
clearTimeout(goodTube_bufferingTimeout);
|
|
}
|
|
if (goodTube_bufferCountTimeout) {
|
|
clearTimeout(goodTube_bufferCountTimeout);
|
|
}
|
|
|
|
// If we're at the start of the video, don't do anything
|
|
if (goodTube_player.currentTime <= 0) {
|
|
return;
|
|
}
|
|
|
|
// If we're not seeking
|
|
if (!goodTube_seeking) {
|
|
goodTube_bufferCountTimeout = setTimeout(function() {
|
|
// And we've had to wait for it to buffer for at least 1 second 3 times, select the next server
|
|
goodTube_bufferCount++;
|
|
|
|
if (goodTube_bufferCount >= 3) {
|
|
// Clear any buffering timeouts
|
|
if (goodTube_bufferingTimeout) {
|
|
clearTimeout(goodTube_bufferingTimeout);
|
|
}
|
|
if (goodTube_bufferCountTimeout) {
|
|
clearTimeout(goodTube_bufferCountTimeout);
|
|
}
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Video buffering too often - selecting next video source...');
|
|
|
|
// Reset the buffer count
|
|
goodTube_bufferCount = 0;
|
|
|
|
// Set the player time to be restored when the new server loads
|
|
goodTube_player_restoreTime = goodTube_player.currentTime;
|
|
|
|
// Select the next server
|
|
goodTube_player_selectVideoServer('automatic', true);
|
|
|
|
return;
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
// Only do this for HD servers (Invidious and Piped)
|
|
if ((goodTube_videoServer_type === 2 || goodTube_videoServer_type === 3)) {
|
|
// Save the time we started buffering
|
|
let bufferStartTime = goodTube_player.currentTime;
|
|
|
|
// If we've been waiting more than 15s, select the next server
|
|
goodTube_bufferingTimeout = setTimeout(function() {
|
|
if (goodTube_player.currentTime === bufferStartTime) {
|
|
// Clear any buffering timeouts
|
|
if (goodTube_bufferingTimeout) {
|
|
clearTimeout(goodTube_bufferingTimeout);
|
|
}
|
|
if (goodTube_bufferCountTimeout) {
|
|
clearTimeout(goodTube_bufferCountTimeout);
|
|
}
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Video not loading fast enough - selecting next video source...');
|
|
|
|
// Set the player time to be restored when the new server loads
|
|
goodTube_player_restoreTime = goodTube_player.currentTime;
|
|
|
|
// Select the next server
|
|
goodTube_player_selectVideoServer('automatic', true);
|
|
}
|
|
}, 15000);
|
|
}
|
|
});
|
|
|
|
// Once the metadata has loaded
|
|
goodTube_videojs_player.on('loadedmetadata', function() {
|
|
// Ensure we're viewing a video
|
|
if (!goodTube_player.getAttribute('src')) {
|
|
return;
|
|
}
|
|
|
|
// Clear any loading timeouts
|
|
if (goodTube_loadingTimeout) {
|
|
clearTimeout(goodTube_loadingTimeout);
|
|
}
|
|
|
|
// Skip to remembered time once loaded metadata (if there's a get param of 't')
|
|
if (typeof goodTube_getParams['t'] !== 'undefined') {
|
|
let time = goodTube_getParams['t'].replace('s', '');
|
|
goodTube_player_skipTo(goodTube_player, time);
|
|
}
|
|
|
|
// Skip to remembered time if we're changing server
|
|
if (goodTube_player_restoreTime > 0) {
|
|
goodTube_player_skipTo(goodTube_player, goodTube_player_restoreTime);
|
|
}
|
|
|
|
// Focus the video player once loaded metadata
|
|
goodTube_player.focus();
|
|
});
|
|
|
|
// Debug message to show the video is loading
|
|
goodTube_videojs_player.on('loadstart', function() {
|
|
// Ensure we're viewing a video
|
|
if (!goodTube_player.getAttribute('src')) {
|
|
return;
|
|
}
|
|
|
|
// Clear any loading timeouts
|
|
if (goodTube_loadingTimeout) {
|
|
clearTimeout(goodTube_loadingTimeout);
|
|
}
|
|
|
|
// If we've been waiting more than 15s, select the next server
|
|
goodTube_loadingTimeout = setTimeout(function() {
|
|
// Debug message
|
|
console.log('[GoodTube] Video not loading fast enough - selecting next video source...');
|
|
|
|
// Get the next server
|
|
goodTube_player_selectVideoServer('automatic', true);
|
|
}, 15000);
|
|
|
|
// Server 1 quality stuff
|
|
if (goodTube_videoServer_type === 1) {
|
|
let qualityLabel = '';
|
|
|
|
// Get the quality label from the quality select menu in the player
|
|
let qualityLabelMenuItem = document.querySelector('.vjs-quality-selector .vjs-menu .vjs-selected .vjs-menu-item-text');
|
|
if (qualityLabelMenuItem) {
|
|
qualityLabel = qualityLabelMenuItem.innerHTML;
|
|
}
|
|
// Otherwise that doesn't exist so get it from the selected source
|
|
else {
|
|
qualityLabel = goodTube_player.querySelector('source[selected=true]').getAttribute('label');
|
|
}
|
|
|
|
// If we've manually changed quality, remember it so the next video stays with the same quality
|
|
let newQuality = qualityLabel.replace('p', '').replace('hd', '').replace(' ', '').toLowerCase();
|
|
|
|
if (parseFloat(goodTube_player_selectedQuality) !== parseFloat(newQuality)) {
|
|
goodTube_player_manuallySelectedQuality = newQuality;
|
|
goodTube_player_selectedQuality = newQuality;
|
|
}
|
|
|
|
// Target the outer wrapper
|
|
let goodTube_target = document.querySelector('#goodTube_playerWrapper');
|
|
|
|
// If the quality is audio, add the audio style to the player
|
|
if (newQuality === 'audio') {
|
|
if (!goodTube_target.classList.contains('goodTube_audio')) {
|
|
goodTube_target.classList.add('goodTube_audio');
|
|
}
|
|
}
|
|
// Otherwise remove the audio style from the player
|
|
else if (goodTube_target.classList.contains('goodTube_audio')) {
|
|
goodTube_target.classList.remove('goodTube_audio');
|
|
}
|
|
|
|
// Debug message
|
|
if (goodTube_player_reloadVideoAttempts <= 1) {
|
|
console.log('[GoodTube] Loading quality '+qualityLabel+'...');
|
|
}
|
|
}
|
|
|
|
|
|
// Server type 2 (dash) quality stuff
|
|
else if (goodTube_videoServer_type === 2 || goodTube_videoServer_type === 3) {
|
|
// Target the outer wrapper
|
|
let goodTube_target = document.querySelector('#goodTube_playerWrapper');
|
|
|
|
// Remove any audio styles from the player
|
|
if (goodTube_target.classList.contains('goodTube_audio')) {
|
|
goodTube_target.classList.remove('goodTube_audio');
|
|
}
|
|
|
|
// Debug message
|
|
if (goodTube_player_reloadVideoAttempts <= 1) {
|
|
console.log('[GoodTube] Loading qualities...');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Once data had loaded
|
|
goodTube_videojs_player.on('loadeddata', function() {
|
|
// Reset the buffer count
|
|
goodTube_bufferCount = 0;
|
|
|
|
// Autoplay the video
|
|
// Only autoplay if the user hasn't paused the video prior to it loading
|
|
if (!goodTube_player.paused) {
|
|
goodTube_player_play(goodTube_player);
|
|
}
|
|
|
|
// The load worked so clear any pending reloads and allow more reload attempts for future loads
|
|
goodTube_player_reloadVideoAttempts = 1;
|
|
if (typeof goodTube_pendingRetry['reloadVideo'] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['reloadVideo']);
|
|
}
|
|
|
|
// Debug message
|
|
if (goodTube_videoServer_type === 1) {
|
|
console.log('[GoodTube] Quality loaded');
|
|
}
|
|
else if (goodTube_videoServer_type === 2 || goodTube_videoServer_type === 3) {
|
|
console.log('[GoodTube] Qualities loaded');
|
|
}
|
|
|
|
// Update the video js player
|
|
goodTube_videojs_update();
|
|
|
|
// Remove the loading state
|
|
goodTube_player_removeLoadingState();
|
|
});
|
|
|
|
// Play next video this video has ended
|
|
goodTube_videojs_player.on('ended', function() {
|
|
goodTube_youtube_syncPlayers();
|
|
goodTube_nav_next();
|
|
});
|
|
|
|
// Save the volume you were last at in a cookie
|
|
goodTube_videojs_player.on('volumechange', function() {
|
|
let volume = goodTube_player.volume;
|
|
if (goodTube_player.muted) {
|
|
volume = 0;
|
|
}
|
|
|
|
goodTube_helper_setCookie('goodTube_volume', volume);
|
|
});
|
|
}
|
|
|
|
// Style video js
|
|
function goodTube_videojs_style() {
|
|
let style = document.createElement('style');
|
|
style.textContent = `
|
|
.video-js {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.video-js *:focus {
|
|
outline-color: transparent;
|
|
outline-style: none;
|
|
}
|
|
|
|
.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar {
|
|
transition: visibility .25s, opacity .25s !important;
|
|
}
|
|
|
|
.vjs-menu .vjs-menu-item-text {
|
|
text-transform: none !important;
|
|
}
|
|
|
|
.vjs-menu .vjs-menu-item-text:first-letter {
|
|
text-transform: uppercase !important;
|
|
}
|
|
|
|
.video-js .vjs-download-button .vjs-icon-placeholder,
|
|
.video-js .vjs-source-button .vjs-icon-placeholder,
|
|
.video-js .vjs-autoplay-button .vjs-icon-placeholder,
|
|
.video-js .vjs-quality-selector .vjs-icon-placeholder,
|
|
.video-js .vjs-prev-button .vjs-icon-placeholder,
|
|
.video-js .vjs-next-button .vjs-icon-placeholder,
|
|
.video-js .vjs-miniplayer-button .vjs-icon-placeholder,
|
|
.video-js .vjs-theater-button .vjs-icon-placeholder {
|
|
font-family: VideoJS;
|
|
font-weight: 400;
|
|
font-style: normal;
|
|
}
|
|
|
|
.video-js .vjs-control-bar > button {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.video-js .vjs-prev-button .vjs-icon-placeholder:before {
|
|
content: "\\f124";
|
|
}
|
|
|
|
.video-js .vjs-next-button .vjs-icon-placeholder:before {
|
|
content: "\\f123";
|
|
}
|
|
|
|
.video-js .vjs-download-button .vjs-icon-placeholder:before {
|
|
content: "\\f110";
|
|
}
|
|
|
|
|
|
|
|
// Loading indicator for downloads
|
|
.video-js .vjs-download-button {
|
|
position: relative;
|
|
}
|
|
|
|
.video-js .vjs-download-button .goodTube_spinner {
|
|
opacity: 0;
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
transition: opacity .4s linear;
|
|
}
|
|
.video-js .vjs-download-button.goodTube_loading .goodTube_spinner {
|
|
opacity: 1;
|
|
transition: opacity .2s .2s linear;
|
|
}
|
|
|
|
.video-js .vjs-download-button .vjs-icon-placeholder:before {
|
|
opacity: 1;
|
|
transition: opacity .2s .2s linear;
|
|
}
|
|
.video-js .vjs-download-button.goodTube_loading .vjs-icon-placeholder:before {
|
|
opacity: 0;
|
|
transition: opacity .2s linear;
|
|
}
|
|
|
|
.goodTube_spinner {
|
|
color: #ffffff;
|
|
pointer-events: none;
|
|
}
|
|
.goodTube_spinner,
|
|
.goodTube_spinner div {
|
|
box-sizing: border-box;
|
|
}
|
|
.goodTube_spinner {
|
|
display: inline-block;
|
|
position: relative;
|
|
width: 36px;
|
|
height: 36px;
|
|
}
|
|
.goodTube_spinner div {
|
|
position: absolute;
|
|
border: 2px solid currentColor;
|
|
opacity: 1;
|
|
border-radius: 50%;
|
|
animation: goodTube_spinner 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
|
}
|
|
.goodTube_spinner div:nth-child(2) {
|
|
animation-delay: -0.5s;
|
|
}
|
|
@keyframes goodTube_spinner {
|
|
0% {
|
|
top: 16px;
|
|
left: 16px;
|
|
width: 4px;
|
|
height: 4px;
|
|
opacity: .5;
|
|
}
|
|
4.9% {
|
|
top: 16px;
|
|
left: 16px;
|
|
width: 4px;
|
|
height: 4px;
|
|
opacity: .5;
|
|
}
|
|
5% {
|
|
top: 16px;
|
|
left: 16px;
|
|
width: 4px;
|
|
height: 4px;
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
top: 0;
|
|
left: 0;
|
|
width: 36px;
|
|
height: 36px;
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.video-js .vjs-source-button .vjs-icon-placeholder:before {
|
|
content: "\\f10e";
|
|
}
|
|
|
|
.video-js .vjs-autoplay-button .vjs-icon-placeholder:before {
|
|
content: "\\f102";
|
|
}
|
|
|
|
.video-js .vjs-quality-selector .vjs-icon-placeholder:before {
|
|
content: "\\f114";
|
|
}
|
|
|
|
.video-js .vjs-source-button .vjs-icon-placeholder:before {
|
|
content: "\\f10e";
|
|
}
|
|
|
|
.video-js .vjs-miniplayer-button .vjs-icon-placeholder:before {
|
|
content: "\\f127";
|
|
}
|
|
|
|
.video-js .vjs-theater-button .vjs-icon-placeholder:before {
|
|
content: "\\f115";
|
|
}
|
|
|
|
/* Youtube player style */
|
|
.vjs-slider-horizontal .vjs-volume-level:before {
|
|
font-size: 14px !important;
|
|
}
|
|
|
|
.vjs-volume-control {
|
|
width: auto !important;
|
|
margin-right: 0 !important;
|
|
}
|
|
|
|
.video-js .vjs-volume-panel.vjs-volume-panel-horizontal {
|
|
transition: width .25s !important;
|
|
z-index: 999;
|
|
}
|
|
|
|
.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal {
|
|
transition: opacity .25s, width 1s !important;
|
|
min-width: 0 !important;
|
|
padding-right: 8px !important;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.video-js .vjs-volume-panel {
|
|
margin-right: 6px !important;
|
|
}
|
|
|
|
.video-js .vjs-volume-panel.vjs-hover,
|
|
.video-js .vjs-volume-panel.vjs-slider-active {
|
|
margin-right: 16px !important;
|
|
}
|
|
|
|
.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control.vjs-volume-horizontal {
|
|
pointer-events: all;
|
|
}
|
|
|
|
.vjs-volume-bar.vjs-slider-horizontal {
|
|
min-width: 52px !important;
|
|
}
|
|
|
|
.video-js.player-style-youtube .vjs-control-bar > .vjs-spacer {
|
|
flex: 1;
|
|
order: 2;
|
|
}
|
|
|
|
.video-js.player-style-youtube .vjs-play-progress .vjs-time-tooltip {
|
|
display: none;
|
|
}
|
|
|
|
.video-js.player-style-youtube .vjs-play-progress::before {
|
|
color: red;
|
|
font-size: 0.85em;
|
|
display: none;
|
|
}
|
|
|
|
.video-js.player-style-youtube .vjs-progress-holder:hover .vjs-play-progress::before {
|
|
display: unset;
|
|
}
|
|
|
|
.video-js.player-style-youtube .vjs-control-bar {
|
|
display: flex;
|
|
flex-direction: row;
|
|
}
|
|
|
|
.video-js.player-style-youtube .vjs-big-play-button {
|
|
top: 50%;
|
|
left: 50%;
|
|
margin-top: -0.81666em;
|
|
margin-left: -1.5em;
|
|
}
|
|
|
|
.video-js.player-style-youtube .vjs-menu-button-popup .vjs-menu {
|
|
margin-bottom: 2em;
|
|
}
|
|
|
|
.video-js ul.vjs-menu-content::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.video-js .vjs-user-inactive:not(.vjs-paused) {
|
|
cursor: none;
|
|
}
|
|
|
|
.video-js .vjs-text-track-display > div > div > div {
|
|
border-radius: 0 !important;
|
|
padding: 4px 8px !important;
|
|
line-height: calc(1.2em + 7px) !important;
|
|
white-space: break-spaces !important;
|
|
}
|
|
|
|
.video-js .vjs-play-control {
|
|
order: 0;
|
|
}
|
|
|
|
.video-js .vjs-prev-button {
|
|
order: 1;
|
|
}
|
|
|
|
.video-js .vjs-next-button {
|
|
order: 2;
|
|
}
|
|
|
|
.video-js .vjs-volume-panel {
|
|
order: 3;
|
|
}
|
|
|
|
/* Time control */
|
|
html body #goodTube_playerWrapper .video-js .vjs-time-control {
|
|
font-family: "YouTube Noto", Roboto, Arial, Helvetica, sans-serif !important;
|
|
order: 4;
|
|
font-size: 13.0691px !important;
|
|
padding-top: 4px !important;
|
|
color: rgb(221, 221, 221) !important;
|
|
text-shadow: 0 0 2px rgba(0, 0, 0, .5) !important;
|
|
min-width: 0 !important;
|
|
z-index: 1;
|
|
}
|
|
|
|
html body #goodTube_playerWrapper .video-js .vjs-time-control * {
|
|
min-width: 0 !important;
|
|
}
|
|
|
|
.video-js .vjs-current-time {
|
|
padding-right: 4px !important;
|
|
padding-left: 0 !important;
|
|
margin-left: 0 !important;
|
|
}
|
|
|
|
.video-js .vjs-duration {
|
|
padding-left: 4px !important;
|
|
padding-right: 5px !important;
|
|
margin-right: 0 !important;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_mobile .video-js .vjs-time-control {
|
|
position: absolute;
|
|
top: calc(100% - 98px);
|
|
font-weight: 500;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_mobile .video-js .vjs-current-time {
|
|
color: #ffffff !important;
|
|
}
|
|
|
|
.video-js .vjs-source-button {
|
|
margin-left: auto !important;
|
|
order: 5;
|
|
}
|
|
|
|
.video-js .vjs-download-button {
|
|
order: 6;
|
|
}
|
|
|
|
.video-js .vjs-autoplay-button {
|
|
order: 7;
|
|
}
|
|
|
|
.video-js .vjs-playback-rate {
|
|
order: 8;
|
|
}
|
|
|
|
.video-js .vjs-subs-caps-button {
|
|
order: 9;
|
|
}
|
|
|
|
.video-js .vjs-quality-selector {
|
|
order: 10;
|
|
}
|
|
|
|
.video-js .vjs-miniplayer-button {
|
|
order: 11;
|
|
}
|
|
|
|
.video-js .vjs-theater-button {
|
|
order: 12;
|
|
}
|
|
|
|
.video-js .vjs-fullscreen-control {
|
|
order: 13;
|
|
}
|
|
|
|
.video-js .vjs-control-bar {
|
|
display: flex;
|
|
flex-direction: row;
|
|
scrollbar-width: none;
|
|
height: 48px !important;
|
|
background: transparent !important;
|
|
z-index: 2 !important;
|
|
}
|
|
|
|
#goodTube_playerWrapper:not(.goodTube_mobile) .video-js::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
height: 33.33%;
|
|
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAD1CAYAAACRFp+GAAAAAXNSR0IArs4c6QAAASpJREFUOE9lyOlHGAAcxvHuY63Wta3WsdWqdaz7vtfduoyZSBLJmCSSSCaSSBJJJIkk0h+Z7/Pm59Hz4sP3SUh4tUSeIIkMkkmR4qSSIs1JJ4MMUmQ6b0iR5bwlg2xS5DjvSJHr5JFBPikKnEIyeE+KD85HUhQ5xWTwiRQlTikpypxyMvhMii9OBSkqna9kUEWKaqeGDL6RotapI0W900AG30nR6DSRotlpIYNWUrQ57aTocDrJoIsU3U4PKXqdPjLoJ8WAM0gGQ6QYdn6QYsQZJYMxUow7E6SYdKbIYJoUP50ZUsw6c2QwTy7AL/gNf2ARlmAZVmAV1mAd/sI/2IBN2IJt2IFd2IN9+A8HcAhHcAwncApncA4XcAlXcA03cAt3cA8P8AhP8PwCakcyvVVFagcAAAAASUVORK5CYII=");
|
|
background-size: cover;
|
|
background-repeat: repeat-x;
|
|
background-position: bottom;
|
|
background-size: contain;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity .1s linear;
|
|
z-index: 1;
|
|
}
|
|
|
|
#goodTube_playerWrapper:not(.goodTube_mobile) .video-js.vjs-paused::before,
|
|
#goodTube_playerWrapper:not(.goodTube_mobile) .video-js.vjs-user-active::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_mobile .video-js .vjs-control-bar::before {
|
|
display: none;
|
|
content: none;
|
|
}
|
|
|
|
.video-js .vjs-menu .vjs-icon-placeholder {
|
|
display: none !important;
|
|
}
|
|
|
|
.video-js .vjs-menu .vjs-menu-content > * {
|
|
padding-top: 8px !important;
|
|
padding-bottom: 8px !important;
|
|
padding-left: 12px !important;
|
|
padding-right: 12px !important;
|
|
}
|
|
|
|
.video-js .vjs-menu {
|
|
height: auto !important;
|
|
bottom: 48px !important;
|
|
padding-bottom: 0 !important;
|
|
margin-bottom: 0 !important;
|
|
width: auto !important;
|
|
transform: translateX(-50%) !important;
|
|
left: 50% !important;
|
|
}
|
|
|
|
.video-js .vjs-menu .vjs-menu-content {
|
|
position: static !important;
|
|
border-radius: 4px !important;
|
|
}
|
|
|
|
.video-js .vjs-volume-control {
|
|
height: 100% !important;
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
}
|
|
|
|
.video-js .vjs-vtt-thumbnail-display {
|
|
bottom: calc(100% + 35px) !important;
|
|
border-radius: 12px !important;
|
|
overflow: hidden !important;
|
|
border: 2px solid #ffffff !important;
|
|
background-color: #000000 !important;
|
|
}
|
|
|
|
.video-js .vjs-control-bar .vjs-icon-placeholder {
|
|
height: 100%;
|
|
}
|
|
|
|
.video-js .vjs-control {
|
|
min-width: 48px !important;
|
|
}
|
|
|
|
#goodTube_playerWrapper:not(goodTube_mobile) .video-js .vjs-control-bar > .vjs-play-control {
|
|
padding-left: 8px;
|
|
box-sizing: content-box;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_mobile .video-js .vjs-control:not(.vjs-progress-control) {
|
|
min-width: 0 !important;
|
|
flex-grow: 1 !important;
|
|
max-width: 9999px !important;
|
|
padding-left: 0 !important;
|
|
padding-right: 0 !important;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_mobile .video-js .vjs-control.vjs-volume-panel,
|
|
#goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-control.vjs-volume-panel {
|
|
display: none;
|
|
}
|
|
|
|
.video-js .vjs-control-bar .vjs-icon-placeholder::before {
|
|
height: auto;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
font-size: 24px;
|
|
line-height: 100%;
|
|
}
|
|
|
|
.video-js .vjs-control-bar *:not(.vjs-time-control) {
|
|
text-shadow: none !important;
|
|
}
|
|
|
|
.video-js .vjs-vtt-thumbnail-time {
|
|
display: none !important;
|
|
}
|
|
|
|
.video-js .vjs-playback-rate .vjs-playback-rate-value {
|
|
line-height: 48px;
|
|
font-size: 14px !important;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.video-js .vjs-play-progress .vjs-time-tooltip {
|
|
display: none !important;
|
|
}
|
|
|
|
.video-js .vjs-mouse-display .vjs-time-tooltip {
|
|
background: none !important;
|
|
font-size: 12px !important;
|
|
top: -50px !important;
|
|
text-shadow: 0 0 10px rgba(0, 0, 0, .5) !important;
|
|
font-family: "YouTube Noto", Roboto, Arial, Helvetica, sans-serif !important;
|
|
font-weight: 500 !important;
|
|
}
|
|
|
|
.video-js .vjs-control-bar::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.video-js .vjs-icon-cog {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.video-js .vjs-control-bar,
|
|
.video-js .vjs-menu-button-popup .vjs-menu .vjs-menu-content {
|
|
background-color: rgba(35, 35, 35, 0.75);
|
|
}
|
|
|
|
.video-js .vjs-menu li.vjs-menu-item:not(.vjs-selected) {
|
|
background-color: transparent !important;
|
|
color: #ffffff !important;
|
|
}
|
|
|
|
.video-js .vjs-menu li.vjs-menu-item:not(.vjs-selected):hover {
|
|
background-color: rgba(255, 255, 255, 0.75) !important;
|
|
color: rgba(49, 49, 51, 0.75) !important;
|
|
color: #ffffff !important;
|
|
}
|
|
|
|
.video-js .vjs-menu li.vjs-selected,
|
|
.video-js .vjs-menu li.vjs-selected:hover {
|
|
background-color: #ffffff !important;
|
|
color: #000000 !important;
|
|
}
|
|
|
|
.video-js .vjs-menu li {
|
|
white-space: nowrap !important;
|
|
font-size: 12px !important;
|
|
font-weight: 700 !important;
|
|
max-width: 9999px !important;
|
|
}
|
|
|
|
.video-js .vjs-subs-caps-button .vjs-menu li {
|
|
white-space: normal !important;
|
|
min-width: 128px !important;
|
|
}
|
|
|
|
/* Progress Bar */
|
|
.video-js .vjs-slider {
|
|
background-color: rgba(15, 15, 15, 0.5);
|
|
}
|
|
|
|
.video-js .vjs-load-progress,
|
|
.video-js .vjs-load-progress div {
|
|
background: rgba(87, 87, 88, 1);
|
|
}
|
|
|
|
.video-js .vjs-slider:hover,
|
|
.video-js button:hover {
|
|
color: #ffffff;
|
|
}
|
|
|
|
/* Overlay */
|
|
.video-js .vjs-overlay {
|
|
background-color: rgba(35, 35, 35, 0.75) !important;
|
|
}
|
|
.video-js .vjs-overlay * {
|
|
color: rgba(255, 255, 255, 1) !important;
|
|
text-align: center;
|
|
}
|
|
|
|
/* ProgressBar marker */
|
|
.video-js .vjs-marker {
|
|
background-color: rgba(255, 255, 255, 1);
|
|
z-index: 0;
|
|
}
|
|
|
|
/* Big "Play" Button */
|
|
.video-js .vjs-big-play-button {
|
|
background-color: rgba(35, 35, 35, 0.5);
|
|
}
|
|
|
|
.video-js:hover .vjs-big-play-button {
|
|
background-color: rgba(35, 35, 35, 0.75);
|
|
}
|
|
|
|
.video-js .vjs-current-time,
|
|
.video-js .vjs-time-divider,
|
|
.video-js .vjs-duration {
|
|
display: block;
|
|
}
|
|
|
|
.video-js .vjs-time-divider {
|
|
min-width: 0px;
|
|
padding-left: 0px;
|
|
padding-right: 0px;
|
|
}
|
|
|
|
.video-js .vjs-poster {
|
|
background-size: cover;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.video-js .player-dimensions.vjs-fluid {
|
|
padding-top: 82vh;
|
|
}
|
|
|
|
video.video-js {
|
|
position: absolute;
|
|
height: 100%;
|
|
}
|
|
|
|
.video-js .mobile-operations-bar {
|
|
display: flex;
|
|
position: absolute;
|
|
top: 0;
|
|
right: 1px !important;
|
|
left: initial !important;
|
|
width: initial !important;
|
|
}
|
|
|
|
.video-js .mobile-operations-bar ul {
|
|
position: absolute !important;
|
|
bottom: unset !important;
|
|
top: 1.5em;
|
|
}
|
|
|
|
.video-js .vjs-menu-button-popup .vjs-menu {
|
|
border: 0 !important;
|
|
padding-bottom: 12px !important;
|
|
}
|
|
|
|
.video-js .vjs-menu li.vjs-menu-item:not(.vjs-selected):hover,
|
|
.video-js .vjs-menu li.vjs-menu-item.vjs-auto-selected {
|
|
background-color: rgba(255, 255, 255, .2) !important;
|
|
color: #ffffff !important;
|
|
}
|
|
|
|
.video-js .vjs-menu * {
|
|
border: 0 !important;
|
|
}
|
|
|
|
/* Tooltips
|
|
------------------------------------------------------------------------------------------ */
|
|
.video-js .vjs-control-bar > .vjs-prev-button::before {
|
|
content: 'Previous video';
|
|
}
|
|
|
|
.video-js .vjs-control-bar > .vjs-next-button::before {
|
|
content: 'Next video';
|
|
}
|
|
|
|
.video-js .vjs-control-bar .vjs-mute-control:not(.vjs-vol-0)::before {
|
|
content: 'Mute (m)';
|
|
}
|
|
|
|
.video-js .vjs-control-bar .vjs-mute-control.vjs-vol-0::before {
|
|
content: 'Unmute (m)';
|
|
}
|
|
|
|
.video-js .vjs-control-bar > .vjs-playback-rate > .vjs-menu-button::before {
|
|
content: 'Playback speed';
|
|
}
|
|
|
|
.video-js .vjs-control-bar > .vjs-subs-caps-button > .vjs-menu-button::before {
|
|
content: 'Subtitles';
|
|
}
|
|
|
|
.video-js .vjs-control-bar > .vjs-quality-selector > .vjs-menu-button::before {
|
|
content: 'Quality';
|
|
}
|
|
|
|
.video-js .vjs-control-bar > .vjs-download-button > .vjs-menu-button::before {
|
|
content: 'Download';
|
|
}
|
|
|
|
.video-js .vjs-control-bar > .vjs-autoplay-button > .vjs-menu-button::before {
|
|
content: 'Autoplay';
|
|
}
|
|
|
|
.video-js .vjs-control-bar > .vjs-source-button > .vjs-menu-button::before {
|
|
content: 'Video source';
|
|
}
|
|
|
|
.video-js .vjs-control-bar > .vjs-miniplayer-button::before {
|
|
content: 'Miniplayer (i)';
|
|
}
|
|
|
|
.video-js .vjs-control-bar > .vjs-theater-button::before {
|
|
content: 'Theater mode (t)';
|
|
}
|
|
|
|
.video-js .vjs-control-bar > .vjs-fullscreen-control::before {
|
|
content: 'Fullscreen (f)';
|
|
left: auto !important;
|
|
right: 12px !important;
|
|
transform: none !important;
|
|
}
|
|
|
|
.video-js .vjs-control-bar button.vjs-menu-button::before,
|
|
.video-js .vjs-control-bar .vjs-button:not(.vjs-menu-button)::before {
|
|
position: absolute;
|
|
top: -40px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(0, 0, 0, .75);
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
padding: 8px;
|
|
white-space: nowrap;
|
|
opacity: 0;
|
|
transition: opacity .1s;
|
|
pointer-events: none;
|
|
text-shadow: none !important;
|
|
z-index: 1;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_mobile .video-js .vjs-control-bar button.vjs-menu-button::before,
|
|
#goodTube_playerWrapper.goodTube_mobile .video-js .vjs-control-bar .vjs-button:not(.vjs-menu-button)::before {
|
|
display: none !important;
|
|
content: none !important;
|
|
}
|
|
|
|
.video-js .vjs-control-bar div.vjs-menu-button:not(.vjs-menuOpen) button.vjs-menu-button:hover::before,
|
|
.video-js .vjs-control-bar .vjs-button:not(.vjs-menu-button):hover::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.video-js div.vjs-menu-button:not(.vjs-menuOpen) .vjs-menu {
|
|
display: none !important;
|
|
}
|
|
|
|
.video-js div.vjs-menu-button.vjs-menuOpen .vjs-menu {
|
|
display: block !important;
|
|
}
|
|
|
|
.video-js .vjs-menu {
|
|
z-index: 999 !important;
|
|
}
|
|
|
|
.video-js .vjs-big-play-button {
|
|
display: none !important;
|
|
}
|
|
|
|
.video-js .vjs-volume-panel,
|
|
.video-js .vjs-button {
|
|
z-index: 1;
|
|
}
|
|
|
|
.video-js .vjs-button.vjs-menuOpen {
|
|
z-index: 999;
|
|
}
|
|
|
|
.video-js .vjs-error-display .vjs-modal-dialog-content {
|
|
display: none;
|
|
}
|
|
|
|
.video-js:not(.vjs-has-started) .vjs-control-bar {
|
|
display: flex !important;
|
|
}
|
|
|
|
.vjs-track-settings-controls button:hover {
|
|
color: #000000 !important;
|
|
}
|
|
`;
|
|
|
|
document.body.appendChild(style);
|
|
}
|
|
|
|
// Update video js
|
|
function goodTube_videojs_update() {
|
|
// Make menus work
|
|
let menuButtons = document.querySelectorAll('.vjs-control-bar button');
|
|
menuButtons.forEach((button) => {
|
|
button.onclick = function() {
|
|
let openMenuButtons = document.querySelectorAll('.vjs-menuOpen');
|
|
openMenuButtons.forEach((openMenuButton) => {
|
|
if (openMenuButton != button.closest('div.vjs-menu-button')) {
|
|
openMenuButton.classList.remove('vjs-menuOpen');
|
|
}
|
|
});
|
|
|
|
let menu = button.closest('div.vjs-menu-button');
|
|
|
|
if (menu) {
|
|
if (menu.classList.contains('vjs-menuOpen')) {
|
|
menu.classList.remove('vjs-menuOpen');
|
|
}
|
|
else {
|
|
menu.classList.add('vjs-menuOpen');
|
|
}
|
|
}
|
|
}
|
|
|
|
button.ontouchstart = function() {
|
|
let openMenuButtons = document.querySelectorAll('.vjs-menuOpen');
|
|
openMenuButtons.forEach((openMenuButton) => {
|
|
if (openMenuButton != button.closest('div.vjs-menu-button')) {
|
|
openMenuButton.classList.remove('vjs-menuOpen');
|
|
}
|
|
});
|
|
|
|
let menu = button.closest('div.vjs-menu-button');
|
|
|
|
if (menu) {
|
|
if (menu.classList.contains('vjs-menuOpen')) {
|
|
menu.classList.remove('vjs-menuOpen');
|
|
}
|
|
else {
|
|
menu.classList.add('vjs-menuOpen');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const onClickOrTap = (element, handler) => {
|
|
let touchMoveHappened = false;
|
|
|
|
function touchstart() {
|
|
touchMoveHappened = false;
|
|
}
|
|
|
|
function touchmove() {
|
|
touchMoveHappened = true;
|
|
}
|
|
|
|
function touchend(e) {
|
|
if (touchMoveHappened) {
|
|
return;
|
|
}
|
|
|
|
handler(e);
|
|
}
|
|
|
|
function click(e) {
|
|
handler(e);
|
|
}
|
|
|
|
element.addEventListener('touchstart', touchstart);
|
|
element.addEventListener('touchmove', touchmove);
|
|
element.addEventListener('touchend', touchend);
|
|
element.addEventListener('click', click);
|
|
};
|
|
|
|
// Click menu item, close menu
|
|
let menuItems = document.querySelectorAll('.vjs-menu-item');
|
|
menuItems.forEach((item) => {
|
|
onClickOrTap(item, (e) => {
|
|
let delay = 0;
|
|
|
|
if (goodTube_mobile) {
|
|
delay = 400;
|
|
}
|
|
|
|
setTimeout(function() {
|
|
let openMenuButtons = document.querySelectorAll('.vjs-menuOpen');
|
|
openMenuButtons.forEach((openMenuButton) => {
|
|
openMenuButton.classList.remove('vjs-menuOpen');
|
|
});
|
|
}, delay);
|
|
});
|
|
});
|
|
|
|
// Add a hover bar to the DOM if we haven't already (desktop only)
|
|
if (!goodTube_mobile) {
|
|
if (!document.querySelector('.goodTube_hoverBar')) {
|
|
let hoverBar = document.createElement('div');
|
|
hoverBar.classList.add('goodTube_hoverBar');
|
|
document.querySelector('.video-js .vjs-progress-control').appendChild(hoverBar);
|
|
|
|
// Add actions to size the hover bar
|
|
document.querySelector('.video-js .vjs-progress-control').addEventListener('mousemove', function(event) {
|
|
window.requestAnimationFrame(function() {
|
|
hoverBar.style.width = document.querySelector('.video-js .vjs-progress-control .vjs-mouse-display').style.left;
|
|
});
|
|
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Usage stats
|
|
------------------------------------------------------------------------------------------ */
|
|
// Don't worry everyone - this is just a counter that totals unique users / how many videos were played with GoodTube.
|
|
// It's only in here so I can have some fun and see how many people use this thing I made - no private info is tracked.
|
|
|
|
// Count unique users
|
|
function goodTube_stats_user() {
|
|
if (!goodTube_helper_getCookie('goodTube_unique_new2')) {
|
|
fetch('https://api.counterapi.dev/v1/goodtube/users/up/');
|
|
|
|
// Set a cookie to only count users once
|
|
goodTube_helper_setCookie('goodTube_unique_new2', 'true');
|
|
}
|
|
}
|
|
|
|
// Count videos
|
|
function goodTube_stats_video() {
|
|
fetch('https://api.counterapi.dev/v1/goodtube/videos/up/');
|
|
}
|
|
|
|
|
|
/* Downloads
|
|
------------------------------------------------------------------------------------------ */
|
|
let goodTube_downloadTimeouts = [];
|
|
let goodTube_pendingDownloads = [];
|
|
|
|
// Que download video / audio for a specificed youtube ID
|
|
function goodTube_download_addToQue(serverIndex, type, youtubeId, fileName, codec) {
|
|
// Ensure filename as a value
|
|
if (typeof fileName === 'undefined') {
|
|
fileName = '';
|
|
}
|
|
|
|
// Stop if this is no longer a pending download
|
|
if (typeof goodTube_pendingDownloads[youtubeId] === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
// If we're out of download servers to try, show an error
|
|
if (typeof goodTube_downloadServers[serverIndex] === 'undefined') {
|
|
// Remove from pending downloads
|
|
if (typeof goodTube_pendingDownloads[youtubeId] !== 'undefined') {
|
|
delete goodTube_pendingDownloads[youtubeId];
|
|
}
|
|
|
|
// Debug message
|
|
if (typeof fileName !== 'undefined') {
|
|
alert('[GoodTube] '+type.charAt(0).toUpperCase()+type.slice(1)+' - '+fileName+' could not be downloaded. Please try again soon.');
|
|
console.log('[GoodTube] '+type.charAt(0).toUpperCase()+type.slice(1)+' - '+fileName+' could not be downloaded. Please try again soon.');
|
|
}
|
|
else {
|
|
alert('[GoodTube] '+type.charAt(0).toUpperCase()+type.slice(1)+' could not be downloaded. Please try again soon.');
|
|
console.log('[GoodTube] '+type.charAt(0).toUpperCase()+type.slice(1)+' could not be downloaded. Please try again soon.');
|
|
}
|
|
|
|
// Hide the downloading indicator
|
|
goodTube_download_hideDownloading();
|
|
|
|
return;
|
|
}
|
|
|
|
// Show the downloading indicator
|
|
goodTube_download_showDownloading();
|
|
|
|
// Delay calling the API 3s since it was last called
|
|
let delaySeconds = 0;
|
|
let currentTimeSeconds = new Date().getTime() / 1000;
|
|
let lastDownloadTimeSeconds = parseFloat(goodTube_helper_getCookie('goodTube_lastDownloadTimeSeconds'));
|
|
if (lastDownloadTimeSeconds) {
|
|
delaySeconds = (3 - (currentTimeSeconds - lastDownloadTimeSeconds));
|
|
|
|
if (delaySeconds < 0) {
|
|
delaySeconds = 0;
|
|
}
|
|
}
|
|
goodTube_helper_setCookie('goodTube_lastDownloadTimeSeconds', (currentTimeSeconds + delaySeconds));
|
|
|
|
goodTube_downloadTimeouts[youtubeId] = setTimeout(function() {
|
|
// Debug message
|
|
if (fileName !== '') {
|
|
console.log('[GoodTube] Downloading '+type+' - '+fileName+'...');
|
|
}
|
|
else {
|
|
console.log('[GoodTube] Downloading '+type+'...');
|
|
}
|
|
|
|
// CODEC:
|
|
// Desktop tries in this order: vp9, av1, h264
|
|
// Mobile tries in this order: h264, av1, vp9
|
|
|
|
// Set the default codec (first download call)
|
|
let vCodec = 'vp9';
|
|
if (goodTube_mobile) {
|
|
vCodec = 'h264';
|
|
}
|
|
|
|
// If a codec was passed to this function (cus it retried itself) - then use that
|
|
if (typeof codec !== 'undefined') {
|
|
vCodec = codec;
|
|
}
|
|
|
|
// Audio only option
|
|
let isAudioOnly = false;
|
|
if (type === 'audio') {
|
|
isAudioOnly = true;
|
|
}
|
|
|
|
// Setup options to call the API
|
|
let jsonData = JSON.stringify({
|
|
'url': 'https://www.youtube.com/watch?v='+youtubeId,
|
|
'vCodec': vCodec,
|
|
'vQuality': 'max',
|
|
'filenamePattern': 'basic',
|
|
'isAudioOnly': isAudioOnly
|
|
});
|
|
|
|
// Call the API (die after 10s)
|
|
fetch(goodTube_downloadServers[serverIndex]+'/api/json', {
|
|
signal: AbortSignal.timeout(10000),
|
|
method: 'POST',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: jsonData
|
|
})
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
// Stop if this is no longer a pending download
|
|
if (typeof goodTube_pendingDownloads[youtubeId] === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
// Turn data into JSON
|
|
data = JSON.parse(data);
|
|
|
|
// Try again if we've hit the API rate limit
|
|
if (typeof data['status'] !== 'undefined' && data['status'] === 'rate-limit') {
|
|
if (typeof goodTube_pendingRetry['download_'+youtubeId] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['download_'+youtubeId]);
|
|
}
|
|
|
|
goodTube_pendingRetry['download_'+youtubeId] = setTimeout(function() {
|
|
goodTube_download_addToQue(serverIndex, type, youtubeId, fileName);
|
|
}, goodTube_retryDelay);
|
|
|
|
return;
|
|
}
|
|
|
|
// If there was an error returned from the API
|
|
if (typeof data['status'] !== 'undefined' && data['status'] === 'error') {
|
|
// If there was an issue with the codec, try the next one.
|
|
// There should be an error with the word 'settings' in it if this happens.
|
|
let nextCodec = false;
|
|
if (typeof data['text'] !== 'undefined' && data['text'].toLowerCase().indexOf('settings') !== -1) {
|
|
// Select the next codec
|
|
|
|
// Desktop
|
|
if (!goodTube_mobile) {
|
|
if (vCodec === 'vp9') {
|
|
nextCodec = 'av1';
|
|
}
|
|
else if (vCodec === 'av1') {
|
|
nextCodec = 'h264';
|
|
}
|
|
}
|
|
|
|
// Mobile
|
|
if (goodTube_mobile) {
|
|
if (vCodec === 'h264') {
|
|
nextCodec = 'av1';
|
|
}
|
|
else if (vCodec === 'av1') {
|
|
nextCodec = 'vp9';
|
|
}
|
|
}
|
|
|
|
// If there's a next codec available (and we're not out of options)
|
|
if (nextCodec) {
|
|
// Retry with the next codec
|
|
if (typeof goodTube_pendingRetry['download_'+youtubeId] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['download_'+youtubeId]);
|
|
}
|
|
|
|
goodTube_pendingRetry['download_'+youtubeId] = setTimeout(function() {
|
|
goodTube_download_addToQue(serverIndex, type, youtubeId, fileName, nextCodec);
|
|
}, goodTube_retryDelay);
|
|
|
|
return;
|
|
}
|
|
// Otherwise, there's no more codecs to try, so display an error
|
|
else {
|
|
// Debug message
|
|
console.log('[GoodTube] Could not download '+type+' - '+fileName);
|
|
|
|
// Remove from pending downloads
|
|
if (typeof goodTube_pendingDownloads[youtubeId] !== 'undefined') {
|
|
delete goodTube_pendingDownloads[youtubeId];
|
|
}
|
|
|
|
// Hide the downloading indicator
|
|
setTimeout(function() {
|
|
goodTube_download_hideDownloading();
|
|
}, 1000);
|
|
|
|
return;
|
|
}
|
|
}
|
|
// All other errors, just try again
|
|
else {
|
|
if (typeof goodTube_pendingRetry['download_'+youtubeId] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['download_'+youtubeId]);
|
|
}
|
|
|
|
serverIndex++;
|
|
|
|
goodTube_pendingRetry['download_'+youtubeId] = setTimeout(function() {
|
|
goodTube_download_addToQue(serverIndex, type, youtubeId, fileName);
|
|
}, goodTube_retryDelay);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If the data is all good
|
|
else if (typeof data['status'] !== 'undefined' && typeof data['url'] !== 'undefined') {
|
|
// Download the file
|
|
goodTube_download_file(data['url'], type, fileName, youtubeId, serverIndex);
|
|
}
|
|
})
|
|
// If anything went wrong, try again
|
|
.catch((error) => {
|
|
if (typeof goodTube_pendingRetry['download_'+youtubeId] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['download_'+youtubeId]);
|
|
}
|
|
|
|
serverIndex++;
|
|
|
|
goodTube_pendingRetry['download_'+youtubeId] = setTimeout(function() {
|
|
goodTube_download_addToQue(serverIndex, type, youtubeId, fileName);
|
|
}, goodTube_retryDelay);
|
|
});
|
|
}, (delaySeconds * 1000));
|
|
}
|
|
|
|
// Download the entire playlist
|
|
function goodTube_download_playlist(type, noPrompt) {
|
|
// Show a "are you sure cus it takes some time" sort of message
|
|
if (typeof noPrompt === 'undefined' && !confirm("Are you sure you want to download this playlist ("+type+")?\r\rYou can keep playing and downloading other videos, just don't close the tab :)")) {
|
|
return;
|
|
}
|
|
|
|
// Debug message
|
|
if (typeof noPrompt === 'undefined') {
|
|
console.log('[GoodTube] Downloading '+type+' playlist...');
|
|
}
|
|
|
|
// Get the playlist items
|
|
let playlistItems = document.querySelectorAll('#goodTube_playlistContainer a');
|
|
|
|
// Make sure the data is all good
|
|
if (playlistItems.length <= 0) {
|
|
console.log('[GoodTube] Downloading failed, could not find playlist data.');
|
|
return;
|
|
}
|
|
|
|
let track = 0;
|
|
playlistItems.forEach((playlistItem) => {
|
|
// Get playlist info
|
|
let fileName = goodTube_helper_padNumber((track + 1), 2)+' - '+playlistItem.innerHTML.trim();
|
|
let url = playlistItem.href;
|
|
|
|
// Make sure the data is all good
|
|
if (!fileName || !url) {
|
|
console.log('[GoodTube] Downloading failed, could not find playlist data.');
|
|
return;
|
|
}
|
|
|
|
let urlGet = url.split('?')[1];
|
|
|
|
let getParams = {};
|
|
urlGet.replace(/\??(?:([^=]+)=([^&]*)&?)/g, function() {
|
|
function decode(s) {
|
|
return decodeURIComponent(s.split("+").join(" "));
|
|
}
|
|
|
|
getParams[decode(arguments[1])] = decode(arguments[2]);
|
|
});
|
|
|
|
let id = getParams['v'];
|
|
|
|
// Add to pending downloads
|
|
goodTube_pendingDownloads[id] = true;
|
|
|
|
// Download the video
|
|
goodTube_download_addToQue(0, type, id, fileName);
|
|
|
|
track++;
|
|
});
|
|
}
|
|
|
|
// Download a file as blob (this allows us to name it so we use it for playlists - but it doesn't actually download the file until it's fully loaded in the browser, which is kinda bad UX...but for now, it works!)
|
|
function goodTube_download_file(url, type, fileName, youtubeId, serverIndex) {
|
|
// Stop if this is no longer a pending download
|
|
if (typeof goodTube_pendingDownloads[youtubeId] === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
// Show the downloading indicator
|
|
goodTube_download_showDownloading();
|
|
|
|
// Set the file extension based on the type
|
|
let fileExtension = '.mp4';
|
|
if (type === 'audio') {
|
|
fileExtension = '.mp3';
|
|
}
|
|
|
|
// Download as a blob on desktop (only if we have a filename / as that's a playlist)
|
|
if (!goodTube_mobile && fileName !== '') {
|
|
// Get the file
|
|
fetch(url)
|
|
.then(response => response.blob())
|
|
.then(blob => {
|
|
// Stop if this is no longer a pending download
|
|
if (typeof goodTube_pendingDownloads[youtubeId] === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
// Get the blob
|
|
let blobUrl = URL.createObjectURL(blob);
|
|
|
|
// Create a download link element and set params
|
|
let a = document.createElement('a');
|
|
a.style.display = 'none';
|
|
a.href = blobUrl;
|
|
a.download = fileName+fileExtension;
|
|
document.body.appendChild(a);
|
|
|
|
// Click the link to download
|
|
a.click();
|
|
|
|
// Remove the blob from memory
|
|
window.URL.revokeObjectURL(blobUrl);
|
|
|
|
// Remove the link
|
|
a.remove();
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Downloaded '+type+' - '+fileName);
|
|
|
|
// Remove from pending downloads
|
|
if (typeof goodTube_pendingDownloads[youtubeId] !== 'undefined') {
|
|
delete goodTube_pendingDownloads[youtubeId];
|
|
}
|
|
|
|
// Hide the downloading indicator
|
|
goodTube_download_hideDownloading();
|
|
})
|
|
// If anything went wrong, try again (next download server)
|
|
.catch((error) => {
|
|
if (typeof goodTube_pendingRetry['download_'+youtubeId] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['download_'+youtubeId]);
|
|
}
|
|
|
|
serverIndex++;
|
|
|
|
goodTube_pendingRetry['download_'+youtubeId] = setTimeout(function() {
|
|
goodTube_download_addToQue(serverIndex, type, youtubeId, fileName);
|
|
}, goodTube_retryDelay);
|
|
});
|
|
}
|
|
// Just open the stream URL on mobile (or for single files without a filename)
|
|
else {
|
|
window.open(url, '_self');
|
|
|
|
// Debug message
|
|
if (fileName !== '') {
|
|
console.log('[GoodTube] Downloaded '+type+' - '+fileName);
|
|
}
|
|
else {
|
|
console.log('[GoodTube] Downloaded '+type);
|
|
}
|
|
|
|
// Remove from pending downloads
|
|
if (typeof goodTube_pendingDownloads[youtubeId] !== 'undefined') {
|
|
delete goodTube_pendingDownloads[youtubeId];
|
|
}
|
|
|
|
// Hide the downloading indicator
|
|
setTimeout(function() {
|
|
goodTube_download_hideDownloading();
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
// Cancel all pending downloads
|
|
function goodTube_download_cancelAll() {
|
|
// Show "are you sure" prompt
|
|
if (!confirm("Are you sure you want to cancel all downloads?")) {
|
|
return;
|
|
}
|
|
|
|
// Remove all pending downloads
|
|
goodTube_pendingDownloads = [];
|
|
|
|
// Clear all download timeouts
|
|
for (let key in goodTube_downloadTimeouts) {
|
|
clearTimeout(goodTube_downloadTimeouts[key]);
|
|
delete goodTube_downloadTimeouts[key];
|
|
}
|
|
|
|
// Hide the downloading indicator
|
|
goodTube_download_hideDownloading(true);
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Downloads cancelled');
|
|
}
|
|
|
|
// Show downloading indicator
|
|
function goodTube_download_showDownloading() {
|
|
let loadingElement = document.querySelector('.vjs-download-button');
|
|
|
|
// If there's no spinner, add one
|
|
let spinnerElement = document.querySelector('.vjs-download-button .goodTube_spinner');
|
|
if (!spinnerElement) {
|
|
let spinnerIcon = document.createElement('div');
|
|
spinnerIcon.classList.add('goodTube_spinner');
|
|
spinnerIcon.innerHTML = "<div></div><div></div>";
|
|
|
|
loadingElement.append(spinnerIcon);
|
|
}
|
|
|
|
if (loadingElement && !loadingElement.classList.contains('goodTube_loading')) {
|
|
loadingElement.classList.add('goodTube_loading');
|
|
}
|
|
}
|
|
|
|
// Hide downloading indicator
|
|
function goodTube_download_hideDownloading(hideMessage) {
|
|
// Only do this if we've finished all downloads (this is a weird if statement, but it works to check the length of an associative array)
|
|
if (Reflect.ownKeys(goodTube_pendingDownloads).length > 1) {
|
|
return;
|
|
}
|
|
|
|
let loadingElement = document.querySelector('.vjs-download-button');
|
|
|
|
if (loadingElement && loadingElement.classList.contains('goodTube_loading')) {
|
|
loadingElement.classList.remove('goodTube_loading');
|
|
}
|
|
|
|
// Set the last download time in seconds to now
|
|
goodTube_helper_setCookie('goodTube_lastDownloadTimeSeconds', (new Date().getTime() / 1000));
|
|
|
|
// Debug message
|
|
if (typeof hideMessage === 'undefined') {
|
|
console.log('[GoodTube] Downloads finished');
|
|
}
|
|
}
|
|
|
|
// Show or hide the download playlist buttons
|
|
function goodTube_download_showHideDownloadPlaylistButtons() {
|
|
// Target the playlist buttons
|
|
let playlistButton_cancel = document.querySelector('.goodTube_download_playlist_cancel');
|
|
let goodTube_download_playlist_video = document.querySelector('.goodTube_download_playlist_video');
|
|
let goodTube_download_playlist_audio = document.querySelector('.goodTube_download_playlist_audio');
|
|
|
|
// Make sure the playlist buttons exist
|
|
if (!playlistButton_cancel || !goodTube_download_playlist_video || !goodTube_download_playlist_audio) {
|
|
return;
|
|
}
|
|
|
|
// If we're viewing a playlist
|
|
if (typeof goodTube_getParams['i'] !== 'undefined' || typeof goodTube_getParams['index'] !== 'undefined' || typeof goodTube_getParams['list'] !== 'undefined') {
|
|
// Show the download playlist buttons
|
|
goodTube_helper_showElement(goodTube_download_playlist_video);
|
|
goodTube_helper_showElement(goodTube_download_playlist_audio);
|
|
}
|
|
// If we're not viewing a playlist
|
|
else {
|
|
// Hide the download playlist buttons
|
|
goodTube_helper_hideElement(goodTube_download_playlist_video);
|
|
goodTube_helper_hideElement(goodTube_download_playlist_audio);
|
|
}
|
|
|
|
// If there's pendng downloads (this is a weird if statement, but it works to check the length of an associative array)
|
|
if (Reflect.ownKeys(goodTube_pendingDownloads).length > 1) {
|
|
// Show the cancel button
|
|
goodTube_helper_showElement(playlistButton_cancel);
|
|
}
|
|
// If there's no pending downloads
|
|
else {
|
|
// Hide the cancel button
|
|
goodTube_helper_hideElement(playlistButton_cancel);
|
|
}
|
|
}
|
|
|
|
|
|
/* Navigation (playlists and autoplay)
|
|
------------------------------------------------------------------------------------------ */
|
|
let goodTube_nav_clickedPlaylistOpen = false;
|
|
let goodTube_nav_prevVideo = [];
|
|
let goodTube_nav_prevButton = false;
|
|
let goodTube_nav_nextButton = true;
|
|
|
|
// Generate playlist links (these are internally used to help us navigate through playlists and use autoplay)
|
|
function goodTube_nav_generatePlaylistLinks() {
|
|
// If we're not viewing a playlist, just return.
|
|
if (typeof goodTube_getParams['i'] === 'undefined' && typeof goodTube_getParams['index'] === 'undefined' && typeof goodTube_getParams['list'] === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
// Get the playlist items
|
|
let playlistLinks = false;
|
|
let playlistTitles = false;
|
|
|
|
// Desktop
|
|
if (!goodTube_mobile) {
|
|
playlistLinks = document.querySelectorAll('#playlist-items > a');
|
|
playlistTitles = document.querySelectorAll('#playlist-items #video-title');
|
|
}
|
|
// Mobile
|
|
else {
|
|
playlistLinks = document.querySelectorAll('ytm-playlist-panel-renderer a.compact-media-item-image');
|
|
playlistTitles = document.querySelectorAll('ytm-playlist-panel-renderer .compact-media-item-headline span');
|
|
}
|
|
|
|
// If the playlist links exist
|
|
if (playlistLinks.length > 0) {
|
|
// Target the playlist container
|
|
let playlistContainer = document.getElementById('goodTube_playlistContainer');
|
|
|
|
// Add the playlist container if we don't have it
|
|
if (!playlistContainer) {
|
|
playlistContainer = document.createElement('div');
|
|
playlistContainer.setAttribute('id', 'goodTube_playlistContainer');
|
|
playlistContainer.style.display = 'none';
|
|
document.body.appendChild(playlistContainer);
|
|
}
|
|
|
|
// Empty the playlist container
|
|
playlistContainer.innerHTML = '';
|
|
|
|
// For each playlist item
|
|
let i = 0;
|
|
playlistLinks.forEach((playlistItem) => {
|
|
// Create a link element
|
|
let playlistItemElement = document.createElement('a');
|
|
|
|
// Set the href
|
|
playlistItemElement.href = playlistItem.href;
|
|
|
|
// Set the title
|
|
playlistItemElement.innerHTML = playlistTitles[i].innerHTML.trim();
|
|
|
|
// If we're currently on this item, set the selected class
|
|
if (playlistItem.href.indexOf('v='+goodTube_getParams['v']) !== -1) {
|
|
playlistItemElement.classList.add('goodTube_selected');
|
|
}
|
|
|
|
// Add the item to the playlist container
|
|
playlistContainer.appendChild(playlistItemElement);
|
|
|
|
i++;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Play the previous video
|
|
function goodTube_nav_prev() {
|
|
// Check if we clicked a playlist item
|
|
let clickedPlaylistItem = false;
|
|
|
|
// If we are viewing a playlist
|
|
if (typeof goodTube_getParams['i'] !== 'undefined' || typeof goodTube_getParams['index'] !== 'undefined' || typeof goodTube_getParams['list'] !== 'undefined') {
|
|
// Get the playlist items
|
|
let playlistItems = document.querySelectorAll('#goodTube_playlistContainer a');
|
|
|
|
// For each playlist item
|
|
let clickNext = false;
|
|
|
|
// Loop in reverse
|
|
for (let i = (playlistItems.length - 1); i >= 0; i--) {
|
|
let playlistItem = playlistItems[i];
|
|
|
|
if (clickNext) {
|
|
// Find the matching playlist item on the page and click it
|
|
let bits = playlistItem.href.split('/watch');
|
|
let findUrl = '/watch'+bits[1];
|
|
|
|
// Desktop
|
|
if (!goodTube_mobile) {
|
|
clickedPlaylistItem = true;
|
|
document.querySelector('#playlist-items > a[href="'+findUrl+'"]')?.click();
|
|
}
|
|
// Mobile
|
|
else {
|
|
clickedPlaylistItem = true;
|
|
document.querySelector('ytm-playlist-panel-renderer a.compact-media-item-image[href="'+findUrl+'"]')?.click();
|
|
}
|
|
|
|
if (clickedPlaylistItem) {
|
|
clickedPlaylistItem = true;
|
|
|
|
// Double check that the playlist is open, if not - open it.
|
|
let playlistContainer = document.querySelector('ytm-playlist-panel-renderer');
|
|
if (!playlistContainer) {
|
|
let openButton = document.querySelector('ytm-playlist-panel-entry-point');
|
|
|
|
if (openButton && !goodTube_nav_clickedPlaylistOpen) {
|
|
goodTube_nav_clickedPlaylistOpen = true;
|
|
openButton.click();
|
|
setTimeout(goodTube_nav_prev, 500);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
goodTube_nav_clickedPlaylistOpen = false;
|
|
|
|
// Click the matching playlist item
|
|
document.querySelector('ytm-playlist-panel-renderer a.compact-media-item-image[href="'+findUrl+'"]')?.click();
|
|
}
|
|
}
|
|
|
|
if (playlistItem.classList.contains('goodTube_selected')) {
|
|
clickNext = true;
|
|
}
|
|
else {
|
|
clickNext = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we didn't click a playlist item, play previous video (if it exists in our history)
|
|
if (!clickedPlaylistItem && goodTube_nav_prevVideo[goodTube_nav_prevVideo.length - 2] && goodTube_nav_prevVideo[goodTube_nav_prevVideo.length - 2] !== window.location.href) {
|
|
// Debug message
|
|
console.log('[GoodTube] Playing previous video...');
|
|
|
|
// Go back to the previous video
|
|
goodTube_helper_setCookie('goodTube_previous', 'true');
|
|
window.history.go(-1);
|
|
}
|
|
}
|
|
|
|
// Play the next video
|
|
function goodTube_nav_next(pressedButton = false) {
|
|
// Is autoplay turned on?
|
|
let autoplay = goodTube_helper_getCookie('goodTube_autoplay');
|
|
|
|
// Check if we clicked a playlist item
|
|
let clickedPlaylistItem = false;
|
|
|
|
// If we are viewing a playlist
|
|
if (typeof goodTube_getParams['i'] !== 'undefined' || typeof goodTube_getParams['index'] !== 'undefined' || typeof goodTube_getParams['list'] !== 'undefined') {
|
|
// Get the playlist items
|
|
let playlistItems = document.querySelectorAll('#goodTube_playlistContainer a');
|
|
|
|
// For each playlist item
|
|
let clickNext = false;
|
|
|
|
playlistItems.forEach((playlistItem) => {
|
|
if (clickNext) {
|
|
// Find the matching playlist item on the page and click it
|
|
let bits = playlistItem.href.split('/watch');
|
|
let findUrl = '/watch'+bits[1];
|
|
|
|
// Desktop
|
|
if (!goodTube_mobile) {
|
|
clickedPlaylistItem = true;
|
|
document.querySelector('#playlist-items > a[href="'+findUrl+'"]')?.click();
|
|
}
|
|
// Mobile
|
|
else {
|
|
clickedPlaylistItem = true;
|
|
|
|
// Double check that the playlist is open, if not - open it.
|
|
let playlistContainer = document.querySelector('ytm-playlist-panel-renderer');
|
|
if (!playlistContainer) {
|
|
let openButton = document.querySelector('ytm-playlist-panel-entry-point');
|
|
|
|
if (openButton && !goodTube_nav_clickedPlaylistOpen) {
|
|
goodTube_nav_clickedPlaylistOpen = true;
|
|
openButton.click();
|
|
setTimeout(goodTube_nav_next, 500);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
goodTube_nav_clickedPlaylistOpen = false;
|
|
|
|
// Click the matching playlist item
|
|
document.querySelector('ytm-playlist-panel-renderer a.compact-media-item-image[href="'+findUrl+'"]')?.click();
|
|
}
|
|
|
|
if (clickedPlaylistItem) {
|
|
// Debug message
|
|
console.log('[GoodTube] Playing next video in playlist...');
|
|
}
|
|
}
|
|
|
|
if (playlistItem.classList.contains('goodTube_selected')) {
|
|
clickNext = true;
|
|
}
|
|
else {
|
|
clickNext = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// If we didn't click a playlist item, autoplay next video (only if they pressed the next button or autoplay is on)
|
|
if (!clickedPlaylistItem && (autoplay !== 'off' || pressedButton)) {
|
|
let youtubeFrameAPI = document.getElementById('movie_player');
|
|
youtubeFrameAPI.nextVideo();
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Autoplaying next video...');
|
|
}
|
|
}
|
|
|
|
// Setup the previous button history
|
|
function goodTube_nav_setupPrevHistory() {
|
|
// If we've hit the previous button
|
|
if (goodTube_helper_getCookie('goodTube_previous') === 'true') {
|
|
// Remove the last item from the previous video array
|
|
goodTube_nav_prevVideo.pop();
|
|
|
|
goodTube_helper_setCookie('goodTube_previous', 'false');
|
|
}
|
|
// Otherwise it's a normal video load
|
|
else {
|
|
// Add this page to the previous video array
|
|
goodTube_nav_prevVideo.push(window.location.href);
|
|
}
|
|
}
|
|
|
|
// Show or hide the next and previous button
|
|
function goodTube_nav_showHideNextPrevButtons() {
|
|
goodTube_nav_prevButton = false;
|
|
goodTube_nav_nextButton = true;
|
|
|
|
// Don't show next / prev in the miniplayer / pip unless we're viewing a video
|
|
if ((goodTube_miniplayer || goodTube_pip) && typeof goodTube_getParams['v'] === 'undefined') {
|
|
goodTube_nav_prevButton = false;
|
|
goodTube_nav_nextButton = false;
|
|
}
|
|
// For the regular player
|
|
else {
|
|
// If we're viewing a playlist
|
|
if (typeof goodTube_getParams['i'] !== 'undefined' || typeof goodTube_getParams['index'] !== 'undefined' || typeof goodTube_getParams['list'] !== 'undefined') {
|
|
let playlist = document.querySelectorAll('#goodTube_playlistContainer a');
|
|
|
|
if (!playlist || !playlist.length) {
|
|
return;
|
|
}
|
|
|
|
// If the first video is NOT selected
|
|
if (!playlist[0].classList.contains('goodTube_selected')) {
|
|
// Enable the previous button
|
|
goodTube_nav_prevButton = true;
|
|
}
|
|
}
|
|
// Otherwise we're not in a playlist, so if a previous video exists
|
|
else if (goodTube_nav_prevVideo[goodTube_nav_prevVideo.length - 2] && goodTube_nav_prevVideo[goodTube_nav_prevVideo.length - 2] !== window.location.href) {
|
|
// Enable the previous button
|
|
goodTube_nav_prevButton = true;
|
|
}
|
|
}
|
|
|
|
// Show or hide the previous button
|
|
let prevButton = document.querySelector('.vjs-prev-button');
|
|
if (prevButton) {
|
|
if (!goodTube_nav_prevButton) {
|
|
goodTube_helper_hideElement(prevButton);
|
|
}
|
|
else {
|
|
goodTube_helper_showElement(prevButton);
|
|
}
|
|
}
|
|
|
|
// Show or hide the next button
|
|
let nextButton = document.querySelector('.vjs-next-button');
|
|
if (nextButton) {
|
|
if (!goodTube_nav_nextButton) {
|
|
goodTube_helper_hideElement(nextButton);
|
|
}
|
|
else {
|
|
goodTube_helper_showElement(nextButton);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Keyboard shortcuts
|
|
------------------------------------------------------------------------------------------ */
|
|
// Add keyboard shortcuts
|
|
function goodTube_shortcuts_init(player) {
|
|
document.addEventListener('keydown', function(event) {
|
|
// Don't do anything if we're holding control
|
|
if (event.ctrlKey) {
|
|
return;
|
|
}
|
|
|
|
// Get the key pressed in lower case
|
|
let keyPressed = event.key.toLowerCase();
|
|
|
|
// Support bluetooth headset play/pause
|
|
if (keyPressed === 'mediaplaypause' || event.keyCode === 179) {
|
|
if (player.paused) {
|
|
player.play();
|
|
}
|
|
else {
|
|
player.pause();
|
|
}
|
|
}
|
|
|
|
// If we're not focused on a HTML form element
|
|
let focusedElement = event.srcElement;
|
|
let focusedElement_tag = false;
|
|
let focusedElement_id = false;
|
|
if (focusedElement) {
|
|
if (typeof focusedElement.nodeName !== 'undefined') {
|
|
focusedElement_tag = focusedElement.nodeName.toLowerCase();
|
|
}
|
|
|
|
if (typeof focusedElement.getAttribute !== 'undefined') {
|
|
focusedElement_id = focusedElement.getAttribute('id');
|
|
}
|
|
}
|
|
|
|
if (
|
|
!focusedElement ||
|
|
(
|
|
focusedElement_tag.indexOf('input') === -1 &&
|
|
focusedElement_tag.indexOf('label') === -1 &&
|
|
focusedElement_tag.indexOf('select') === -1 &&
|
|
focusedElement_tag.indexOf('textarea') === -1 &&
|
|
focusedElement_tag.indexOf('fieldset') === -1 &&
|
|
focusedElement_tag.indexOf('legend') === -1 &&
|
|
focusedElement_tag.indexOf('datalist') === -1 &&
|
|
focusedElement_tag.indexOf('output') === -1 &&
|
|
focusedElement_tag.indexOf('option') === -1 &&
|
|
focusedElement_tag.indexOf('optgroup') === -1 &&
|
|
focusedElement_id !== 'contenteditable-root'
|
|
)
|
|
) {
|
|
// Speed up playback
|
|
if (keyPressed === '>') {
|
|
if (parseFloat(player.playbackRate) == .25) {
|
|
player.playbackRate = .5;
|
|
}
|
|
else if (parseFloat(player.playbackRate) == .5) {
|
|
player.playbackRate = .75;
|
|
}
|
|
else if (parseFloat(player.playbackRate) == .75) {
|
|
player.playbackRate = 1;
|
|
}
|
|
else if (parseFloat(player.playbackRate) == 1) {
|
|
player.playbackRate = 1.25;
|
|
}
|
|
else if (parseFloat(player.playbackRate) == 1.25) {
|
|
player.playbackRate = 1.5;
|
|
}
|
|
else if (parseFloat(player.playbackRate) == 1.5) {
|
|
player.playbackRate = 1.75;
|
|
}
|
|
else if (parseFloat(player.playbackRate) == 1.75) {
|
|
player.playbackRate = 2;
|
|
}
|
|
}
|
|
|
|
// Slow down playback
|
|
else if (keyPressed === '<') {
|
|
if (parseFloat(player.playbackRate) == .5) {
|
|
player.playbackRate = .25;
|
|
}
|
|
else if (parseFloat(player.playbackRate) == .75) {
|
|
player.playbackRate = .5;
|
|
}
|
|
else if (parseFloat(player.playbackRate) == 1) {
|
|
player.playbackRate = .75;
|
|
}
|
|
else if (parseFloat(player.playbackRate) == 1.25) {
|
|
player.playbackRate = 1;
|
|
}
|
|
else if (parseFloat(player.playbackRate) == 1.5) {
|
|
player.playbackRate = 1.25;
|
|
}
|
|
else if (parseFloat(player.playbackRate) == 1.75) {
|
|
player.playbackRate = 1.5;
|
|
}
|
|
else if (parseFloat(player.playbackRate) == 2) {
|
|
player.playbackRate = 1.75;
|
|
}
|
|
}
|
|
|
|
// If we're not holding down the shift key
|
|
if (!event.shiftKey) {
|
|
|
|
// If we're focused on the video element
|
|
if (focusedElement && typeof focusedElement.closest !== 'undefined' && focusedElement.closest('#goodTube_player')) {
|
|
// Volume down
|
|
if (keyPressed === 'arrowdown') {
|
|
if (player.volume >= .05) {
|
|
player.volume -= .05;
|
|
}
|
|
else {
|
|
player.volume = 0;
|
|
}
|
|
|
|
// No scroll
|
|
event.preventDefault();
|
|
}
|
|
|
|
// Volume up
|
|
if (keyPressed === 'arrowup') {
|
|
if (player.volume <= .95) {
|
|
player.volume += .05;
|
|
}
|
|
else {
|
|
player.volume = 1;
|
|
}
|
|
|
|
// No scroll
|
|
event.preventDefault();
|
|
}
|
|
|
|
// Theater mode (focus the body, this makes the default youtube shortcut work)
|
|
if (keyPressed === 't') {
|
|
document.querySelector('body').focus();
|
|
}
|
|
}
|
|
|
|
// Prev 5 seconds
|
|
if (keyPressed === 'arrowleft') {
|
|
player.currentTime -= 5;
|
|
}
|
|
|
|
// Next 5 seconds
|
|
if (keyPressed === 'arrowright') {
|
|
player.currentTime += 5;
|
|
}
|
|
|
|
// Toggle play/pause
|
|
if (keyPressed === ' ' || keyPressed === 'k') {
|
|
if (player.paused || player.ended) {
|
|
player.play();
|
|
}
|
|
else {
|
|
player.pause();
|
|
}
|
|
}
|
|
|
|
// Toggle mute
|
|
if (keyPressed === 'm') {
|
|
// Also check the volume, because player.muted isn't reliable
|
|
if (player.muted || player.volume <= 0) {
|
|
player.muted = false;
|
|
|
|
// Small fix to make unmute work if you've manually turned it all the way down
|
|
if (player.volume <= 0) {
|
|
player.volume = 1;
|
|
}
|
|
}
|
|
else {
|
|
player.muted = true;
|
|
}
|
|
}
|
|
|
|
// Toggle miniplayer
|
|
if (keyPressed === 'i') {
|
|
event.stopImmediatePropagation();
|
|
goodTube_miniplayer_showHide();
|
|
}
|
|
|
|
// Toggle fullscreen
|
|
if (keyPressed === 'f') {
|
|
document.querySelector('.vjs-fullscreen-control')?.click();
|
|
}
|
|
|
|
// Prev 10 seconds
|
|
else if (keyPressed === 'j') {
|
|
player.currentTime -= 10;
|
|
}
|
|
|
|
// Next 10 seconds
|
|
else if (keyPressed === 'l') {
|
|
player.currentTime += 10;
|
|
}
|
|
|
|
// Start of video
|
|
else if (keyPressed === 'home') {
|
|
player.currentTime = 0;
|
|
}
|
|
|
|
// End of video
|
|
else if (keyPressed === 'end') {
|
|
player.currentTime += player.duration;
|
|
}
|
|
|
|
// Skip to percentage
|
|
if (keyPressed === '0') {
|
|
player.currentTime = 0;
|
|
}
|
|
else if (keyPressed === '1') {
|
|
player.currentTime = ((player.duration / 100) * 10);
|
|
}
|
|
else if (keyPressed === '2') {
|
|
player.currentTime = ((player.duration / 100) * 20);
|
|
}
|
|
else if (keyPressed === '3') {
|
|
player.currentTime = ((player.duration / 100) * 30);
|
|
}
|
|
else if (keyPressed === '4') {
|
|
player.currentTime = ((player.duration / 100) * 40);
|
|
}
|
|
else if (keyPressed === '5') {
|
|
player.currentTime = ((player.duration / 100) * 50);
|
|
}
|
|
else if (keyPressed === '6') {
|
|
player.currentTime = ((player.duration / 100) * 60);
|
|
}
|
|
else if (keyPressed === '7') {
|
|
player.currentTime = ((player.duration / 100) * 70);
|
|
}
|
|
else if (keyPressed === '8') {
|
|
player.currentTime = ((player.duration / 100) * 80);
|
|
}
|
|
else if (keyPressed === '9') {
|
|
player.currentTime = ((player.duration / 100) * 90);
|
|
}
|
|
}
|
|
}
|
|
}, true);
|
|
}
|
|
|
|
// Trigger a keyboard shortcut
|
|
function goodTube_shortcuts_trigger(shortcut) {
|
|
let theKey = false;
|
|
let keyCode = false;
|
|
let shiftKey = false;
|
|
|
|
if (shortcut === 'next') {
|
|
theKey = 'n';
|
|
keyCode = 78;
|
|
shiftKey = true;
|
|
}
|
|
else if (shortcut === 'prev') {
|
|
theKey = 'p';
|
|
keyCode = 80;
|
|
shiftKey = true;
|
|
}
|
|
else if (shortcut === 'theater') {
|
|
theKey = 't';
|
|
keyCode = 84;
|
|
shiftKey = false;
|
|
}
|
|
else if (shortcut === 'fullscreen') {
|
|
theKey = 'f';
|
|
keyCode = 70;
|
|
shiftKey = false;
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
|
|
let e = false;
|
|
e = new window.KeyboardEvent('focus', {
|
|
bubbles: true,
|
|
key: theKey,
|
|
keyCode: keyCode,
|
|
shiftKey: shiftKey,
|
|
charCode: 0,
|
|
});
|
|
document.dispatchEvent(e);
|
|
|
|
e = new window.KeyboardEvent('keydown', {
|
|
bubbles: true,
|
|
key: theKey,
|
|
keyCode: keyCode,
|
|
shiftKey: shiftKey,
|
|
charCode: 0,
|
|
});
|
|
document.dispatchEvent(e);
|
|
|
|
e = new window.KeyboardEvent('beforeinput', {
|
|
bubbles: true,
|
|
key: theKey,
|
|
keyCode: keyCode,
|
|
shiftKey: shiftKey,
|
|
charCode: 0,
|
|
});
|
|
document.dispatchEvent(e);
|
|
|
|
e = new window.KeyboardEvent('keypress', {
|
|
bubbles: true,
|
|
key: theKey,
|
|
keyCode: keyCode,
|
|
shiftKey: shiftKey,
|
|
charCode: 0,
|
|
});
|
|
document.dispatchEvent(e);
|
|
|
|
e = new window.KeyboardEvent('input', {
|
|
bubbles: true,
|
|
key: theKey,
|
|
keyCode: keyCode,
|
|
shiftKey: shiftKey,
|
|
charCode: 0,
|
|
});
|
|
document.dispatchEvent(e);
|
|
|
|
e = new window.KeyboardEvent('change', {
|
|
bubbles: true,
|
|
key: theKey,
|
|
keyCode: keyCode,
|
|
shiftKey: shiftKey,
|
|
charCode: 0,
|
|
});
|
|
document.dispatchEvent(e);
|
|
|
|
e = new window.KeyboardEvent('keyup', {
|
|
bubbles: true,
|
|
key: theKey,
|
|
keyCode: keyCode,
|
|
shiftKey: shiftKey,
|
|
charCode: 0,
|
|
});
|
|
document.dispatchEvent(e);
|
|
}
|
|
|
|
|
|
/* Player functions
|
|
------------------------------------------------------------------------------------------ */
|
|
// Play
|
|
function goodTube_player_play(player) {
|
|
player.play();
|
|
}
|
|
|
|
// Pause
|
|
function goodTube_player_pause(player) {
|
|
player.pause();
|
|
}
|
|
|
|
// Toggle play pause
|
|
function goodTube_player_togglePlayPause() {
|
|
let playPauseButton = document.querySelector('.vjs-play-control');
|
|
|
|
if (playPauseButton.classList.contains('vjs-playing')) {
|
|
goodTube_player_play(goodTube_player);
|
|
}
|
|
else {
|
|
goodTube_player_pause(goodTube_player);
|
|
}
|
|
}
|
|
|
|
// Set volume
|
|
function goodTube_player_volume(player, volume) {
|
|
player.volume = volume;
|
|
}
|
|
|
|
// Skip to
|
|
function goodTube_player_skipTo(player, time) {
|
|
player.currentTime = time;
|
|
}
|
|
|
|
// Clear the player
|
|
function goodTube_player_clear(player) {
|
|
goodTube_error_hide();
|
|
player.currentTime = 0;
|
|
player.setAttribute('src', '');
|
|
player.pause();
|
|
|
|
// Clear any existing chapters
|
|
goodTube_chapters_remove();
|
|
|
|
// Remove the storyboard
|
|
document.querySelector('.vjs-vtt-thumbnail-display')?.remove();
|
|
|
|
// Remove any existing subtitles from videojs
|
|
let existingSubtitles = goodTube_videojs_player.remoteTextTracks();
|
|
if (typeof existingSubtitles['tracks_'] !== 'undefined') {
|
|
existingSubtitles['tracks_'].forEach((existingSubtitle) => {
|
|
goodTube_videojs_player.removeRemoteTextTrack(existingSubtitle);
|
|
});
|
|
}
|
|
|
|
// Clear all qualities
|
|
let qualityMenus = document.querySelectorAll('.vjs-quality-selector');
|
|
if (qualityMenus && typeof qualityMenus[1] !== 'undefined') {
|
|
let menuInner = qualityMenus[1].querySelector('ul');
|
|
if (menuInner) {
|
|
menuInner.innerHTML = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add loading state
|
|
function goodTube_player_addLoadingState() {
|
|
let player = document.getElementById('goodTube_player');
|
|
|
|
if (!player.classList.contains('vjs-loading')) {
|
|
player.classList.add('vjs-loading');
|
|
}
|
|
if (!player.classList.contains('vjs-waiting')) {
|
|
player.classList.add('vjs-waiting');
|
|
}
|
|
}
|
|
|
|
// Remove loading state
|
|
function goodTube_player_removeLoadingState() {
|
|
let player = document.getElementById('goodTube_player');
|
|
|
|
if (player.classList.contains('vjs-loading')) {
|
|
player.classList.remove('vjs-loading');
|
|
}
|
|
if (player.classList.contains('vjs-waiting')) {
|
|
player.classList.remove('vjs-waiting');
|
|
}
|
|
}
|
|
|
|
// Select video server
|
|
function goodTube_player_selectVideoServer(url, reloadVideoData) {
|
|
// Target the source menu
|
|
let menu = document.querySelector('.vjs-source-button .vjs-menu');
|
|
|
|
// Deselect the currently selected menu items
|
|
let selectedMenuItems = menu.querySelectorAll('.vjs-selected');
|
|
selectedMenuItems.forEach((selectedMenuItem) => {
|
|
selectedMenuItem.classList.remove('vjs-selected');
|
|
});
|
|
|
|
// Automatic option
|
|
if (url === 'automatic') {
|
|
// Increment first to skip the first server (which is actually the automatic option itself)
|
|
goodTube_videoServer_automaticIndex++;
|
|
|
|
// If we're out of options, show an error
|
|
if (typeof goodTube_videoServers[goodTube_videoServer_automaticIndex] === 'undefined') {
|
|
goodTube_error_show();
|
|
return;
|
|
}
|
|
|
|
// Select the next server
|
|
goodTube_videoServer_type = goodTube_videoServers[goodTube_videoServer_automaticIndex]['type'];
|
|
goodTube_videoServer_proxy = goodTube_videoServers[goodTube_videoServer_automaticIndex]['proxy'];
|
|
goodTube_videoServer_url = goodTube_videoServers[goodTube_videoServer_automaticIndex]['url'];
|
|
goodTube_videoServer_name = goodTube_videoServers[goodTube_videoServer_automaticIndex]['name'];
|
|
|
|
// Set cookie to remember we're on automatic
|
|
goodTube_helper_setCookie('goodTube_videoServer_withauto', url);
|
|
|
|
// Add class from wrapper for styling automatic option
|
|
let wrapper = document.querySelector('#goodTube_playerWrapper');
|
|
if (!wrapper.classList.contains('goodTube_automaticServer')) {
|
|
wrapper.classList.add('goodTube_automaticServer');
|
|
}
|
|
|
|
// Select the automatic menu item
|
|
let automaticMenuOption = menu.querySelector('ul li:first-child');
|
|
if (!automaticMenuOption.classList.contains('vjs-selected')) {
|
|
automaticMenuOption.classList.add('vjs-selected');
|
|
}
|
|
}
|
|
// Manual selection
|
|
else {
|
|
goodTube_videoServers.forEach((api) => {
|
|
if (url == api['url']) {
|
|
goodTube_videoServer_type = api['type'];
|
|
goodTube_videoServer_proxy = api['proxy'];
|
|
goodTube_videoServer_url = api['url'];
|
|
goodTube_videoServer_name = api['name'];
|
|
|
|
goodTube_helper_setCookie('goodTube_videoServer_withauto', url);
|
|
}
|
|
});
|
|
|
|
// Remove class from wrapper for styling automatic option
|
|
let wrapper = document.querySelector('#goodTube_playerWrapper');
|
|
if (wrapper.classList.contains('goodTube_automaticServer')) {
|
|
wrapper.classList.remove('goodTube_automaticServer');
|
|
}
|
|
|
|
// Reset the automatic selection
|
|
goodTube_videoServer_automaticIndex = 0;
|
|
}
|
|
|
|
// Select the currently selected item
|
|
let menuItems = menu.querySelectorAll('ul li');
|
|
menuItems.forEach((menuItem) => {
|
|
if (menuItem.getAttribute('api') == goodTube_videoServer_url) {
|
|
menuItem.classList.add('vjs-selected');
|
|
}
|
|
});
|
|
|
|
// Reload video data
|
|
if (reloadVideoData) {
|
|
goodTube_video_reloadData();
|
|
}
|
|
}
|
|
|
|
// Init player
|
|
function goodTube_player_init() {
|
|
// Wait until the assets are loaded
|
|
if (goodTube_assets_loaded < goodTube_assets.length) {
|
|
setTimeout(function() {
|
|
goodTube_player_init();
|
|
}, 0);
|
|
|
|
return;
|
|
}
|
|
|
|
// Add CSS styles for the player
|
|
let style = document.createElement('style');
|
|
style.textContent = `
|
|
/* Default quality modal */
|
|
#goodTube_playerWrapper .goodTube_defaultQualityModal {
|
|
position: absolute;
|
|
z-index: 99999;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
padding: 24px;
|
|
transition: opacity .2s linear;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#goodTube_playerWrapper .goodTube_defaultQualityModal .goodTube_defaultQualityModal_overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0,0,0,.8);
|
|
z-index: 1;
|
|
}
|
|
|
|
#goodTube_playerWrapper .goodTube_defaultQualityModal.goodTube_defaultQualityModal_visible {
|
|
opacity: 1;
|
|
pointer-events: all;
|
|
}
|
|
|
|
#goodTube_playerWrapper .goodTube_defaultQualityModal .goodTube_defaultQualityModal_inner {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 264px;
|
|
max-width: calc(100% - 32px);
|
|
max-height: calc(100% - 32px);
|
|
overflow: auto;
|
|
background: #ffffff;
|
|
border-radius: 12px;
|
|
padding: 0;
|
|
z-index: 2;
|
|
box-shadow: 0 0 16px rgba(15, 15, 15, .3);
|
|
}
|
|
|
|
#goodTube_playerWrapper .goodTube_defaultQualityModal .goodTube_defaultQualityModal_title {
|
|
color: rgba(15, 15, 15);
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
padding: 12px;
|
|
padding-top: 16px;
|
|
text-align: center;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
font-family: Roboto, Arial, Helvetica, sans-serif;
|
|
}
|
|
|
|
#goodTube_playerWrapper .goodTube_defaultQualityModal .goodTube_defaultQualityModal_options {
|
|
padding-bottom: 12px;
|
|
}
|
|
|
|
#goodTube_playerWrapper .goodTube_defaultQualityModal .goodTube_defaultQualityModal_options .goodTube_defaultQualityModal_option {
|
|
color: rgba(15, 15, 15);
|
|
font-size: 14px;
|
|
display: block;
|
|
width: 100%;
|
|
padding: 10px;
|
|
text-align: center;
|
|
font-weight: 400;
|
|
text-decoration: none;
|
|
box-sizing: border-box;
|
|
transition: background-color .2s linear;
|
|
cursor: pointer;
|
|
font-family: Roboto, Arial, Helvetica, sans-serif;
|
|
}
|
|
|
|
#goodTube_playerWrapper .goodTube_defaultQualityModal .goodTube_defaultQualityModal_options .goodTube_defaultQualityModal_option.goodTube_defaultQualityModal_selected {
|
|
background: rgba(15,15,15,.15);
|
|
font-weight: 700;
|
|
}
|
|
|
|
#goodTube_playerWrapper .goodTube_defaultQualityModal .goodTube_defaultQualityModal_options .goodTube_defaultQualityModal_option:hover {
|
|
background: rgba(15,15,15,.1);
|
|
}
|
|
|
|
|
|
/* Automatic server styling */
|
|
#goodTube_playerWrapper.goodTube_automaticServer .vjs-source-button ul li:first-child,
|
|
#goodTube_playerWrapper.goodTube_automaticServer .vjs-source-button ul li.vjs-selected:first-child {
|
|
background: #ffffff !important;
|
|
color: #000000 !important;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_automaticServer .vjs-source-button ul li.vjs-selected {
|
|
background-color: rgba(255, 255, 255, .2) !important;
|
|
color: #ffffff !important;
|
|
}
|
|
|
|
|
|
/* Hide the volume tooltip */
|
|
#goodTube_playerWrapper .vjs-volume-bar .vjs-mouse-display {
|
|
display: none !important;
|
|
}
|
|
|
|
#contentContainer.tp-yt-app-drawer[swipe-open].tp-yt-app-drawer::after {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Live streams */
|
|
#goodTube_playerWrapper .vjs-live .vjs-progress-control {
|
|
display: block;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-live .vjs-duration-display,
|
|
#goodTube_playerWrapper .vjs-live .vjs-time-divider {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Seek bar */
|
|
#goodTube_playerWrapper .vjs-progress-control {
|
|
position: absolute;
|
|
bottom: 48px;
|
|
left: 0;
|
|
right: 0;
|
|
width: 100%;
|
|
height: calc(24px + 3px);
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control .vjs-slider {
|
|
margin: 0;
|
|
background: transparent;
|
|
position: absolute;
|
|
bottom: 3px;
|
|
left: 8px;
|
|
right: 8px;
|
|
top: auto;
|
|
transition: height .1s linear, bottom .1s linear;
|
|
z-index: 1;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control:hover .vjs-slider {
|
|
pointer-events: none;
|
|
height: 5px;
|
|
bottom: 2px;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control .vjs-slider .vjs-load-progress {
|
|
height: 100%;
|
|
background: rgba(255, 255, 255, .2);
|
|
transition: none;
|
|
position: static;
|
|
margin-bottom: -3px;
|
|
transition: margin .1s linear;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control:hover .vjs-slider .vjs-load-progress {
|
|
margin-bottom: -5px;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control .vjs-slider .vjs-load-progress .vjs-control-text {
|
|
display: none;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control .vjs-slider .vjs-load-progress > div {
|
|
background: transparent !important;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control .vjs-slider .vjs-play-progress {
|
|
background: transparent;
|
|
position: static;
|
|
z-index: 1;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control .vjs-slider .vjs-play-progress::before {
|
|
content: '';
|
|
background: #ff0000;
|
|
width: 100%;
|
|
height: 100%;
|
|
position: static;
|
|
display: block;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control .vjs-slider .vjs-play-progress::after {
|
|
content: '';
|
|
display: block;
|
|
float: right;
|
|
background: #ff0000;
|
|
border-radius: 50%;
|
|
opacity: 0;
|
|
width: 13px;
|
|
height: 13px;
|
|
right: -7px;
|
|
top: -8px;
|
|
transition: opacity .1s linear, top .1s linear;
|
|
position: relative;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control:hover .vjs-slider .vjs-play-progress::after {
|
|
opacity: 1;
|
|
top: -9px;
|
|
}
|
|
|
|
|
|
/* Without chapters */
|
|
#goodTube_playerWrapper:not(.goodTube_hasChapters) .vjs-progress-control::before {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 3px;
|
|
left: 8px;
|
|
right: 8px;
|
|
height: 3px;
|
|
background: rgba(255, 255, 255, .2);
|
|
transition: height .1s linear, bottom .1s linear;
|
|
}
|
|
|
|
#goodTube_playerWrapper:not(.goodTube_hasChapters) .vjs-progress-control:hover::before {
|
|
height: 5px;
|
|
bottom: 2px;
|
|
}
|
|
|
|
|
|
/* With chapters */
|
|
#goodTube_playerWrapper.goodTube_hasChapters .vjs-progress-control .goodTube_chapters {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 8px;
|
|
right: 8px;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_hasChapters .vjs-progress-control .goodTube_chapters .goodTube_chapter {
|
|
height: 100%;
|
|
position: absolute;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_hasChapters .vjs-progress-control .goodTube_chapters .goodTube_chapter::before {
|
|
content: '';
|
|
background: rgba(255, 255, 255, .2);
|
|
position: absolute;
|
|
left: 0;
|
|
right: 2px;
|
|
bottom: 3px;
|
|
height: 3px;
|
|
transition: height .1s linear, bottom .1s linear, background .1s linear;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_hasChapters .vjs-progress-control .goodTube_chapters .goodTube_chapter.goodTube_redChapter::before {
|
|
background: #ff0000 !important;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_hasChapters .vjs-progress-control .goodTube_chapters .goodTube_chapter:last-child::before {
|
|
right: 0;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_hasChapters .vjs-progress-control:hover .goodTube_chapters .goodTube_chapter::before {
|
|
height: 5px;
|
|
bottom: 2px;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_hasChapters:not(.goodTube_mobile) .vjs-progress-control .goodTube_chapters .goodTube_chapter:hover::before {
|
|
height: 9px;
|
|
bottom: 0;
|
|
background: rgba(255, 255, 255, .4);
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_hasChapters .vjs-progress-control .goodTube_markers {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_hasChapters .vjs-progress-control .goodTube_marker {
|
|
width: 2px;
|
|
height: 100%;
|
|
position: absolute;
|
|
background: rgba(0, 0, 0, .2);
|
|
margin-left: -2px;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_hasChapters .vjs-progress-control .goodTube_marker.goodTube_showMarker {
|
|
background: rgba(0, 0, 0, .6);
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_hasChapters .vjs-progress-control .goodTube_marker:last-child {
|
|
display: none;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control .vjs-mouse-display {
|
|
background: transparent;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_hasChapters .vjs-progress-control .vjs-mouse-display .vjs-time-tooltip::before {
|
|
content: attr(chapter-title);
|
|
display: block;
|
|
white-space: nowrap;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control .goodTube_hoverBar {
|
|
background: rgba(255, 255, 255, .4);
|
|
position: absolute;
|
|
bottom: 3px;
|
|
left: 8px;
|
|
height: 3px;
|
|
opacity: 0;
|
|
transition: height .1s linear, bottom .1s linear, opacity .1s linear;
|
|
}
|
|
|
|
#goodTube_playerWrapper .vjs-progress-control:hover .goodTube_hoverBar {
|
|
height: 5px;
|
|
bottom: 2px;
|
|
opacity: 1;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_mobile .vjs-time-control .vjs-duration-display {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_mobile .vjs-time-control .vjs-duration-display::after {
|
|
content: attr(chapter-title);
|
|
display: inline-block;
|
|
color: #ffffff;
|
|
margin-left: 3px;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_mobile .vjs-progress-control .vjs-slider,
|
|
#goodTube_playerWrapper.goodTube_mobile:not(.goodTube_hasChapters) .vjs-progress-control::before,
|
|
#goodTube_playerWrapper.goodTube_mobile.goodTube_hasChapters .vjs-progress-control .goodTube_chapters,
|
|
#goodTube_playerWrapper.goodTube_mobile .vjs-progress-control .goodTube_hoverBar {
|
|
left: 16px;
|
|
right: 16px;
|
|
}
|
|
|
|
|
|
/* Audio only view */
|
|
#goodTube_playerWrapper.goodTube_audio {
|
|
background: #000000;
|
|
position: relative;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_audio .video-js::after {
|
|
content: '\\f107';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: #ffffff;
|
|
font-family: VideoJS;
|
|
font-weight: 400;
|
|
font-style: normal;
|
|
font-size: 148px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
#goodTube_playerWrapper.goodTube_audio .video-js::after {
|
|
font-size: 100px;
|
|
}
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_mobile #goodTube_playerWrapper.goodTube_audio .video-js::after {
|
|
font-size: 100px;
|
|
}
|
|
|
|
/* Double tap or tap and hold elements for seeking on mobile */
|
|
#goodTube_seekBackwards {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 48px;
|
|
content: '';
|
|
width: 25%;
|
|
}
|
|
|
|
#goodTube_seekForwards {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 48px;
|
|
content: '';
|
|
width: 25%;
|
|
}
|
|
|
|
/* Desktop */
|
|
#goodTube_playerWrapper {
|
|
border-radius: 12px;
|
|
background: #ffffff;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
z-index: 999;
|
|
overflow: hidden;
|
|
}
|
|
|
|
html[dark] #goodTube_playerWrapper {
|
|
background: #0f0f0f;
|
|
}
|
|
|
|
/* Mobile */
|
|
#goodTube_playerWrapper.goodTube_mobile {
|
|
position: fixed;
|
|
background: #000000;
|
|
border-radius: 0;
|
|
z-index: 3;
|
|
}
|
|
|
|
/* Theater mode */
|
|
#goodTube_playerWrapper.goodTube_theater {
|
|
background: #000000;
|
|
border-radius: 0;
|
|
}
|
|
|
|
/* Miniplayer */
|
|
#goodTube_playerWrapper.goodTube_miniplayer {
|
|
z-index: 999 !important;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_miniplayer .video-js {
|
|
position: fixed;
|
|
bottom: 12px;
|
|
right: 12px;
|
|
width: 400px;
|
|
max-width: calc(100% - 24px);
|
|
min-height: 0;
|
|
padding-top: 0;
|
|
z-index: 999;
|
|
height: auto;
|
|
left: auto;
|
|
aspect-ratio: 16 / 9;
|
|
top: auto;
|
|
overflow: hidden;
|
|
background: #000000;
|
|
border-radius: 12px;
|
|
}
|
|
#goodTube_playerWrapper.goodTube_miniplayer .video-js::before {
|
|
content: none !important;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_miniplayer.goodTube_mobile .video-js {
|
|
bottom: 60px;
|
|
}
|
|
|
|
ytd-watch-flexy.goodTube_miniplayer {
|
|
display: block !important;
|
|
top: 0;
|
|
left: 0;
|
|
position: fixed;
|
|
z-index: 999;
|
|
top: -9999px;
|
|
left: -9999px;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-source-button,
|
|
#goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-autoplay-button,
|
|
#goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-miniplayer-button,
|
|
#goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-theater-button {
|
|
display: none !important;
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js #goodTube_miniplayer_closeButton,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js #goodTube_miniplayer_expandButton {
|
|
font-family: VideoJS;
|
|
font-weight: 400;
|
|
font-style: normal;
|
|
cursor: pointer;
|
|
position: absolute;
|
|
top: 0;
|
|
width: 48px;
|
|
height: 48px;
|
|
line-height: 48px;
|
|
text-align: center;
|
|
z-index: 999;
|
|
color: #ffffff;
|
|
opacity: 0;
|
|
transition: opacity .2s linear;
|
|
}
|
|
|
|
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js #goodTube_miniplayer_closeButton::after {
|
|
content: 'Close';
|
|
right: 12px;
|
|
}
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js #goodTube_miniplayer_expandButton::after {
|
|
content: 'Expand';
|
|
left: 12px;
|
|
}
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js #goodTube_miniplayer_closeButton::after,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js #goodTube_miniplayer_expandButton::after {
|
|
position: absolute;
|
|
bottom: -24px;
|
|
background: rgba(0, 0, 0, .75);
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
padding: 8px;
|
|
white-space: nowrap;
|
|
opacity: 0;
|
|
transition: opacity .1s;
|
|
pointer-events: none;
|
|
text-shadow: none !important;
|
|
z-index: 1;
|
|
font-family: 'MS Shell Dlg 2', sans-serif;
|
|
line-height: initial;
|
|
}
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js #goodTube_miniplayer_closeButton:hover::after,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js #goodTube_miniplayer_expandButton:hover::after {
|
|
opacity: 1;
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js #goodTube_miniplayer_closeButton {
|
|
right: 0;
|
|
font-size: 24px;
|
|
}
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js #goodTube_miniplayer_closeButton::before {
|
|
content: "\\f119";
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js #goodTube_miniplayer_expandButton {
|
|
left: 0;
|
|
font-size: 18px;
|
|
}
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js #goodTube_miniplayer_expandButton::before {
|
|
content: "\\f128";
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js.vjs-paused:not(.vjs-user-inactive) #goodTube_miniplayer_expandButton,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js.vjs-user-active #goodTube_miniplayer_expandButton,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js.vjs-paused:not(.vjs-user-inactive) #goodTube_miniplayer_closeButton,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js.vjs-user-active #goodTube_miniplayer_closeButton {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Mobile */
|
|
html body #goodTube_playerWrapper.goodTube_mobile {
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js .vjs-control.vjs-play-control,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-control.vjs-play-control {
|
|
position: absolute;
|
|
top: calc(50% - 48px);
|
|
left: calc(50% - 32px);
|
|
width: 64px;
|
|
height: 64px;
|
|
background: rgba(0, 0, 0, .3);
|
|
border-radius: 50%;
|
|
max-width: 999px !important;
|
|
box-sizing: border-box;
|
|
}
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js .vjs-play-control .vjs-icon-placeholder::before,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-play-control .vjs-icon-placeholder::before {
|
|
font-size: 44px !important;
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js .vjs-prev-button,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-prev-button {
|
|
position: absolute;
|
|
top: calc(50% - 40px);
|
|
left: calc(50% - 104px);
|
|
width: 48px;
|
|
height: 48px;
|
|
background: rgba(0, 0, 0, .3);
|
|
border-radius: 50%;
|
|
max-width: 999px !important;
|
|
}
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js .vjs-prev-button .vjs-icon-placeholder::before,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-prev-button .vjs-icon-placeholder::before {
|
|
font-size: 32px !important;
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js .vjs-next-button,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-next-button {
|
|
position: absolute;
|
|
top: calc(50% - 40px);
|
|
left: calc(50% + 56px);
|
|
width: 48px;
|
|
height: 48px;
|
|
background: rgba(0, 0, 0, .3);
|
|
border-radius: 50%;
|
|
max-width: 999px !important;
|
|
}
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js .vjs-next-button .vjs-icon-placeholder::before,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-next-button .vjs-icon-placeholder::before {
|
|
font-size: 32px !important;
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js .vjs-control-bar,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-control-bar {
|
|
z-index: 1;
|
|
position: static;
|
|
margin-top: auto;
|
|
justify-content: space-around;
|
|
}
|
|
|
|
ytd-watch-flexy:not([theater]) #primary {
|
|
min-width: 721px !important;
|
|
}
|
|
|
|
@media (max-width: 1100px) {
|
|
ytd-watch-flexy:not([theater]) #primary {
|
|
min-width: 636px !important;
|
|
}
|
|
|
|
#goodTube_playerWrapper:not(.goodTube_mobile):not(.goodTube_theater) .video-js .vjs-control-bar .vjs-button {
|
|
zoom: .88;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 1016px) {
|
|
ytd-watch-flexy:not([theater]) #primary {
|
|
min-width: 0 !important;
|
|
}
|
|
|
|
#goodTube_playerWrapper:not(.goodTube_mobile):not(.goodTube_theater) .video-js .vjs-control-bar .vjs-button {
|
|
zoom: 1;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 786px) {
|
|
#goodTube_playerWrapper:not(.goodTube_mobile):not(.goodTube_theater) .video-js .vjs-control-bar .vjs-button {
|
|
zoom: .9;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 715px) {
|
|
#goodTube_playerWrapper:not(.goodTube_mobile):not(.goodTube_theater) .video-js .vjs-control-bar .vjs-button {
|
|
zoom: .85;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 680px) {
|
|
#goodTube_playerWrapper:not(.goodTube_mobile):not(.goodTube_theater) .video-js .vjs-control-bar .vjs-button {
|
|
zoom: .8;
|
|
}
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js {
|
|
display: flex;
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js .vjs-source-button,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-source-button {
|
|
margin-left: 0 !important;
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js .vjs-source-button .vjs-menu,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-source-button .vjs-menu {
|
|
left: auto !important;
|
|
transform: none !important;
|
|
}
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js .vjs-loading-spinner,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js .vjs-loading-spinner {
|
|
top: calc(50% - 16px);
|
|
}
|
|
|
|
html body #goodTube_playerWrapper .video-js.vjs-loading {
|
|
background: #000000;
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js::before,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js::before {
|
|
content: '';
|
|
background: transparent;
|
|
transition: background .2s ease-in-out;
|
|
pointer-events: none;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: 1;
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js.vjs-paused::before,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js.vjs-paused::before,
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js.vjs-user-active::before,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js.vjs-user-active::before {
|
|
background: rgba(0,0,0,.6);
|
|
}
|
|
|
|
html body #goodTube_playerWrapper.goodTube_mobile .video-js.vjs-user-inactive:not(.vjs-paused) .vjs-control-bar,
|
|
html body #goodTube_playerWrapper.goodTube_miniplayer .video-js.vjs-user-inactive:not(.vjs-paused) .vjs-control-bar {
|
|
visibility: visible;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_mobile .video-js .vjs-theater-button,
|
|
#goodTube_playerWrapper.goodTube_mobile .video-js .vjs-autoplay-button {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Video */
|
|
#goodTube_player {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: transparent;
|
|
z-index: 1;
|
|
}
|
|
|
|
#goodTube_playerWrapper.goodTube_mobile #goodTube_player,
|
|
#goodTube_player.vjs-loading {
|
|
background: #000000;
|
|
}
|
|
|
|
#goodTube_player:focus {
|
|
outline: 0;
|
|
}
|
|
|
|
/* Error */
|
|
#goodTube_error {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 40px;
|
|
right: 40px;
|
|
transform: translateY(-50%);
|
|
text-align: center;
|
|
color: #ffffff;
|
|
font-size: 20px;
|
|
padding: 16px;
|
|
background: #000000;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
#goodTube_error small {
|
|
padding-top: 8px;
|
|
display: block;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
// Setup player layout
|
|
let playerWrapper = document.createElement('div');
|
|
playerWrapper.id = 'goodTube_playerWrapper';
|
|
|
|
// Add a mobile class
|
|
if (goodTube_mobile) {
|
|
playerWrapper.classList.add('goodTube_mobile');
|
|
}
|
|
|
|
// Setup player dynamic positioning and sizing
|
|
goodTube_player_positionAndSize(playerWrapper);
|
|
|
|
// Add player to the page
|
|
document.body.appendChild(playerWrapper);
|
|
|
|
// Add video
|
|
let player = document.createElement('video');
|
|
player.id = 'goodTube_player';
|
|
player.classList.add('video-js');
|
|
player.controls = true;
|
|
player.setAttribute('tab-index', '1');
|
|
playerWrapper.appendChild(player);
|
|
|
|
// Expose the player globally
|
|
goodTube_player = player;
|
|
|
|
// Init picture in picture
|
|
goodTube_pip_init();
|
|
|
|
// Init videojs
|
|
goodTube_videojs_init();
|
|
|
|
// Init default quality modal
|
|
goodTube_defaultQuality_initModal();
|
|
|
|
// Sync players every 10s
|
|
setInterval(goodTube_youtube_syncPlayers, 10000);
|
|
|
|
// Run the main actions (these setup the player when the page changes)
|
|
goodTube_actions();
|
|
setInterval(goodTube_actions, 100);
|
|
|
|
// Support timestamp links in comments
|
|
setInterval(goodTube_youtube_timestampLinks, 500);
|
|
|
|
// Generate the playlist links (used by GoodTube to navigate playlists correctly)
|
|
setInterval(goodTube_nav_generatePlaylistLinks, 500);
|
|
|
|
// Update our next / prev buttons to show or hide every 100ms
|
|
setInterval(goodTube_nav_showHideNextPrevButtons, 100);
|
|
|
|
// Update the download playlist buttons visibility
|
|
setInterval(goodTube_download_showHideDownloadPlaylistButtons, 500);
|
|
|
|
// Update pip actions
|
|
setInterval(goodTube_pip_update, 100);
|
|
|
|
// Update miniplayer
|
|
setInterval(goodTube_miniplayer_update, 100);
|
|
|
|
// Position timestamp (mobile only)
|
|
if (goodTube_mobile) {
|
|
setInterval(goodTube_player_positionTimestamp, 100);
|
|
}
|
|
|
|
// Add keyboard shortcuts
|
|
goodTube_shortcuts_init(player);
|
|
|
|
// If we're on mobile, set the volume to 100%
|
|
if (goodTube_mobile) {
|
|
goodTube_player_volume(goodTube_player, 1);
|
|
}
|
|
}
|
|
|
|
|
|
/* Update player display
|
|
------------------------------------------------------------------------------------------ */
|
|
// Position and size the player
|
|
function goodTube_player_positionAndSize(playerWrapper) {
|
|
// If we're viewing a video
|
|
if (typeof goodTube_getParams['v'] !== 'undefined') {
|
|
// Show the GoodTube player
|
|
goodTube_helper_showElement(playerWrapper);
|
|
|
|
// This is used to position and size the player
|
|
let positionElement = false;
|
|
|
|
// Desktop
|
|
if (!goodTube_mobile) {
|
|
// Theater mode
|
|
if (document.querySelector('ytd-watch-flexy[theater]')) {
|
|
positionElement = document.getElementById('full-bleed-container');
|
|
|
|
if (!playerWrapper.classList.contains('goodTube_theater')) {
|
|
playerWrapper.classList.add('goodTube_theater');
|
|
}
|
|
}
|
|
// Regular mode
|
|
else {
|
|
positionElement = document.getElementById('player');
|
|
|
|
if (playerWrapper.classList.contains('goodTube_theater')) {
|
|
playerWrapper.classList.remove('goodTube_theater');
|
|
}
|
|
}
|
|
|
|
// Position the player
|
|
if (positionElement && positionElement.offsetHeight > 0) {
|
|
// Our wrapper has "position: absolute" so take into account the window scroll
|
|
let rect = positionElement.getBoundingClientRect();
|
|
playerWrapper.style.top = (rect.top + window.scrollY)+'px';
|
|
playerWrapper.style.left = (rect.left + window.scrollX)+'px';
|
|
|
|
// Match the size of the position element
|
|
playerWrapper.style.width = positionElement.offsetWidth+'px';
|
|
playerWrapper.style.height = positionElement.offsetHeight+'px';
|
|
}
|
|
}
|
|
|
|
// Mobile
|
|
else {
|
|
positionElement = document.getElementById('player');
|
|
|
|
// Position the player
|
|
if (positionElement && positionElement.offsetHeight > 0) {
|
|
// Our wrapper has "position: absolute" so don't take into account the window scroll
|
|
let rect = positionElement.getBoundingClientRect();
|
|
playerWrapper.style.top = rect.top+'px';
|
|
playerWrapper.style.left = rect.left+'px';
|
|
|
|
// Match the size of the position element
|
|
playerWrapper.style.width = positionElement.offsetWidth+'px';
|
|
playerWrapper.style.height = positionElement.offsetHeight+'px';
|
|
}
|
|
}
|
|
|
|
// Fix the menu max heights
|
|
if (positionElement) {
|
|
let menus = document.querySelectorAll('.vjs-menu-content');
|
|
menus.forEach((menu) => {
|
|
menu.style.maxHeight = (positionElement.offsetHeight - 72)+'px';
|
|
});
|
|
}
|
|
|
|
|
|
}
|
|
// If we're not viewing a video
|
|
else {
|
|
// Hide the GoodTube player
|
|
goodTube_helper_hideElement(playerWrapper);
|
|
}
|
|
|
|
// Call this function again on next draw frame
|
|
window.requestAnimationFrame(function() {
|
|
goodTube_player_positionAndSize(playerWrapper);
|
|
});
|
|
}
|
|
|
|
// Position the timestamp (mobile only)
|
|
function goodTube_player_positionTimestamp() {
|
|
let currentTime = document.querySelector('.vjs-current-time');
|
|
let divider = document.querySelector('.vjs-time-divider');
|
|
let duration = document.querySelector('.vjs-duration');
|
|
|
|
if (currentTime && divider && duration) {
|
|
let leftOffset = 16;
|
|
let padding = 4;
|
|
|
|
currentTime.style.left = leftOffset+'px';
|
|
divider.style.left = (leftOffset+currentTime.offsetWidth+padding)+'px';
|
|
duration.style.left = (leftOffset+currentTime.offsetWidth+divider.offsetWidth+padding+padding)+'px';
|
|
}
|
|
}
|
|
|
|
|
|
/* Video functions
|
|
------------------------------------------------------------------------------------------ */
|
|
let goodTube_player_loadVideoDataAttempts = 0;
|
|
let goodTube_player_reloadVideoAttempts = 1;
|
|
let goodTube_player_highestQuality = false;
|
|
let goodTube_player_selectedQuality = false;
|
|
let goodTube_player_manuallySelectedQuality = false;
|
|
|
|
// Load video
|
|
function goodTube_video_load(player) {
|
|
// If we're not viewing a video
|
|
if (typeof goodTube_getParams['v'] === 'undefined') {
|
|
// Empty the previous video history
|
|
goodTube_nav_prevVideo = [];
|
|
|
|
// Then return, we don't do anything else.
|
|
return;
|
|
}
|
|
|
|
// Clear any pending reloadVideo attempts
|
|
goodTube_player_reloadVideoAttempts = 1;
|
|
if (typeof goodTube_pendingRetry['reloadVideo'] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['reloadVideo']);
|
|
}
|
|
|
|
// Clear any pending loadVideoData attempts
|
|
if (typeof goodTube_pendingRetry['loadVideoData'] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['loadVideoData']);
|
|
}
|
|
|
|
// Clear the player
|
|
goodTube_player_clear(player);
|
|
|
|
// Add the loading state
|
|
goodTube_player_addLoadingState();
|
|
|
|
// Only re-attempt to load the video data max configured retry attempts
|
|
goodTube_player_loadVideoDataAttempts++;
|
|
if (goodTube_player_loadVideoDataAttempts > goodTube_retryAttempts) {
|
|
// Show an error or select next server if we're on automatic mode
|
|
goodTube_error_show();
|
|
|
|
return;
|
|
}
|
|
|
|
// Remove any existing video sources
|
|
let videoSources_existing = player.querySelectorAll('source');
|
|
videoSources_existing.forEach((videoSource) => {
|
|
videoSource.remove();
|
|
});
|
|
|
|
// Setup API endpoint to get video data from
|
|
let apiEndpoint = false;
|
|
|
|
// Invidious (360p / HD)
|
|
if (goodTube_videoServer_type === 1 || goodTube_videoServer_type === 2) {
|
|
apiEndpoint = goodTube_videoServer_url+"/api/v1/videos/"+goodTube_getParams['v'];
|
|
}
|
|
// Piped (HD)
|
|
else if (goodTube_videoServer_type === 3) {
|
|
apiEndpoint = goodTube_videoServer_url+"/streams/"+goodTube_getParams['v'];
|
|
}
|
|
|
|
// Call the API (die after 5s)
|
|
fetch(apiEndpoint, {
|
|
signal: AbortSignal.timeout(5000)
|
|
})
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
// Add the loading state
|
|
goodTube_player_addLoadingState();
|
|
|
|
// Turn video data into JSON
|
|
let videoData = JSON.parse(data);
|
|
|
|
// Setup variables to hold the data
|
|
let sourceData = false;
|
|
let subtitleData = false;
|
|
let storyboardData = false;
|
|
let chaptersData = false;
|
|
let videoDescription = false;
|
|
let videoDuration = false;
|
|
|
|
// Below populates the source data - but first, if there's any issues with the source data, try again (after configured delay time)
|
|
let retry = false;
|
|
|
|
// Invidious (360p)
|
|
if (goodTube_videoServer_type === 1) {
|
|
if (typeof videoData['formatStreams'] === 'undefined') {
|
|
retry = true;
|
|
}
|
|
else {
|
|
sourceData = videoData['formatStreams'];
|
|
subtitleData = videoData['captions'];
|
|
storyboardData = videoData['storyboards'];
|
|
videoDescription = videoData['description'];
|
|
videoDuration = videoData['lengthSeconds'];
|
|
chaptersData = false;
|
|
}
|
|
}
|
|
// Invidious (HD)
|
|
else if (goodTube_videoServer_type === 2) {
|
|
if (typeof videoData['dashUrl'] === 'undefined' && typeof videoData['hlsUrl'] === 'undefined') {
|
|
retry = true;
|
|
}
|
|
else {
|
|
sourceData = false;
|
|
subtitleData = videoData['captions'];
|
|
storyboardData = videoData['storyboards'];
|
|
videoDescription = videoData['description'];
|
|
videoDuration = videoData['lengthSeconds'];
|
|
chaptersData = false;
|
|
}
|
|
}
|
|
// Piped (HD)
|
|
else if (goodTube_videoServer_type === 3) {
|
|
if (typeof videoData['hls'] === 'undefined' && typeof videoData['dash'] === 'undefined') {
|
|
retry = true;
|
|
}
|
|
else {
|
|
// Leave the subtitle data as false, because these are still fetched from invidious (using fallback servers)
|
|
subtitleData = false;
|
|
|
|
// Leave the storyboard data as false, because this is baked into the stream (yay!)
|
|
storyboardData = false;
|
|
|
|
// Replace <br> with a newline, and strip the html from the desc. We need this to generate chapters properly.
|
|
videoDescription = videoData['description'].replace(/<br>/g, '\r\n').replace(/<[^>]*>?/gm, '');
|
|
videoDuration = videoData['duration'];
|
|
|
|
// Chapters come from the API
|
|
if (typeof videoData['chapters'] !== 'undefined' && videoData['chapters'].length && videoData['chapters'].length > 0) {
|
|
chaptersData = [];
|
|
videoData['chapters'].forEach((chapter) => {
|
|
chaptersData.push({
|
|
time: parseFloat(chapter['start']),
|
|
title: chapter['title']
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Try again if data wasn't all good
|
|
if (retry) {
|
|
if (typeof goodTube_pendingRetry['loadVideoData'] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['loadVideoData']);
|
|
}
|
|
|
|
goodTube_pendingRetry['loadVideoData'] = setTimeout(function() {
|
|
goodTube_video_load(player);
|
|
}, goodTube_retryDelay);
|
|
|
|
// Add the loading state
|
|
goodTube_player_addLoadingState();
|
|
|
|
return;
|
|
}
|
|
// Otherwise the data was all good so load the sources
|
|
else {
|
|
// Debug message
|
|
console.log('[GoodTube] Video data loaded');
|
|
|
|
// Invidious (360p)
|
|
if (goodTube_videoServer_type === 1) {
|
|
// If we've manually selected a quality, and it exists for this video, select it
|
|
if (goodTube_player_manuallySelectedQuality && player.querySelector('.goodTube_source_'+goodTube_player_manuallySelectedQuality)) {
|
|
player.querySelector('.goodTube_source_'+goodTube_player_manuallySelectedQuality).setAttribute('selected', true);
|
|
|
|
// Save the currently selected quality, this is used when we change quality to know weather or not the new quality has been manually selected
|
|
goodTube_player_selectedQuality = goodTube_player_manuallySelectedQuality;
|
|
}
|
|
// Otherwise select the highest quality source
|
|
else {
|
|
player.querySelector('.goodTube_source_'+goodTube_player_highestQuality)?.setAttribute('selected', true);
|
|
|
|
// Save the currently selected quality, this is used when we change quality to know weather or not the new quality has been manually selected
|
|
goodTube_player_selectedQuality = goodTube_player_highestQuality;
|
|
}
|
|
|
|
// Add audio only source
|
|
let audio_element = document.createElement('source');
|
|
audio_element.setAttribute('src', goodTube_videoServer_url+"/watch?v="+goodTube_getParams['v']+'&raw=1&listen=1');
|
|
audio_element.setAttribute('type', 'audio/mp3');
|
|
audio_element.setAttribute('label', 'Audio');
|
|
audio_element.setAttribute('video', true);
|
|
audio_element.setAttribute('class', 'goodTube_source_audio');
|
|
player.appendChild(audio_element);
|
|
|
|
// For each source
|
|
let i = 0;
|
|
goodTube_player_highestQuality = false;
|
|
sourceData.forEach((source) => {
|
|
// Format the data correctly
|
|
let source_src = false;
|
|
let source_type = false;
|
|
let source_label = false;
|
|
let source_quality = false;
|
|
|
|
source_src = goodTube_videoServer_url+'/latest_version?id='+goodTube_getParams['v']+'&itag='+source['itag'];
|
|
if (goodTube_videoServer_proxy) {
|
|
source_src = source_src+'&local=true';
|
|
}
|
|
|
|
source_type = source['type'];
|
|
source_label = parseFloat(source['resolution'].replace('p', '').replace('hd', ''))+'p';
|
|
source_quality = parseFloat(source['resolution'].replace('p', '').replace('hd', ''));
|
|
|
|
// Only add the source to the player if the data is populated
|
|
if (source_src && source_type && source_label) {
|
|
|
|
// Add video
|
|
if (source_type.toLowerCase().indexOf('video') !== -1) {
|
|
let video_element = document.createElement('source');
|
|
video_element.setAttribute('src', source_src);
|
|
video_element.setAttribute('type', source_type);
|
|
video_element.setAttribute('label', source_label);
|
|
video_element.setAttribute('video', true);
|
|
video_element.setAttribute('class', 'goodTube_source_'+source_quality);
|
|
player.appendChild(video_element);
|
|
|
|
// Keep track of the highest quality item
|
|
if (!goodTube_player_highestQuality || source_quality > goodTube_player_highestQuality) {
|
|
goodTube_player_highestQuality = source_quality;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Increment the loop
|
|
i++;
|
|
});
|
|
|
|
// If we've manually selected a quality, and it exists for this video, select it
|
|
if (goodTube_player_manuallySelectedQuality && player.querySelector('.goodTube_source_'+goodTube_player_manuallySelectedQuality)) {
|
|
player.querySelector('.goodTube_source_'+goodTube_player_manuallySelectedQuality).setAttribute('selected', true);
|
|
|
|
// Save the currently selected quality, this is used when we change quality to know weather or not the new quality has been manually selected
|
|
goodTube_player_selectedQuality = goodTube_player_manuallySelectedQuality;
|
|
}
|
|
// Otherwise select the highest quality source
|
|
else {
|
|
player.querySelector('.goodTube_source_'+goodTube_player_highestQuality)?.setAttribute('selected', true);
|
|
|
|
// Save the currently selected quality, this is used when we change quality to know weather or not the new quality has been manually selected
|
|
goodTube_player_selectedQuality = goodTube_player_highestQuality;
|
|
}
|
|
|
|
|
|
// Enable the videojs quality selector
|
|
let qualities = [];
|
|
player.querySelectorAll('source[video=true]').forEach((quality) => {
|
|
qualities.push({
|
|
src: quality.getAttribute('src'),
|
|
type: quality.getAttribute('type'),
|
|
label: quality.getAttribute('label'),
|
|
selected: quality.getAttribute('selected')
|
|
});
|
|
});
|
|
|
|
goodTube_videojs_player.src(qualities);
|
|
|
|
|
|
// Show the correct quality menu item
|
|
let qualityButtons = document.querySelectorAll('.vjs-quality-selector');
|
|
if (qualityButtons.length === 2) {
|
|
qualityButtons[1].style.display = 'none';
|
|
qualityButtons[0].style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Invidious (HD)
|
|
else if (goodTube_videoServer_type === 2) {
|
|
// Format manifest source data
|
|
let manifestUrl = false;
|
|
let manifestType = false;
|
|
|
|
// Add manifest source
|
|
let proxyUrlPart = 'false';
|
|
if (goodTube_videoServer_proxy) {
|
|
proxyUrlPart = 'true';
|
|
}
|
|
|
|
// HLS stream
|
|
if (typeof videoData['hlsUrl'] !== 'undefined' && videoData['hlsUrl']) {
|
|
manifestUrl = videoData['hlsUrl']+'?local='+proxyUrlPart+'&unique_res=1';
|
|
manifestType = 'application/x-mpegURL';
|
|
}
|
|
// DASH stream
|
|
else if (typeof videoData['dashUrl'] !== 'undefined' && videoData['dashUrl']) {
|
|
manifestUrl = videoData['dashUrl']+'?local='+proxyUrlPart+'&unique_res=1';
|
|
manifestType = 'application/dash+xml';
|
|
}
|
|
|
|
// Does the manifest URL start with a slash?
|
|
if (manifestUrl && manifestUrl[0] === '/') {
|
|
// If this happens, prepend the API url to make the link whole.
|
|
manifestUrl = goodTube_videoServer_url+manifestUrl;
|
|
}
|
|
|
|
// Add the HLS or DASH source
|
|
goodTube_videojs_player.src({
|
|
src: manifestUrl,
|
|
type: manifestType
|
|
});
|
|
|
|
// Update manifest quality menu
|
|
goodTube_defaultQuality_updateMenu();
|
|
}
|
|
|
|
// Piped (HD)
|
|
else if (goodTube_videoServer_type === 3) {
|
|
// Format manifest source data
|
|
let manifestUrl = false;
|
|
let manifestType = false;
|
|
|
|
// Add manifest source
|
|
let proxyUrlPart = 'false';
|
|
if (goodTube_videoServer_proxy) {
|
|
proxyUrlPart = 'true';
|
|
}
|
|
|
|
// HLS stream
|
|
if (typeof videoData['hls'] !== 'undefined' && videoData['hls']) {
|
|
manifestUrl = videoData['hls'];
|
|
manifestType = 'application/x-mpegURL';
|
|
}
|
|
// DASH stream
|
|
else if (typeof videoData['dash'] !== 'undefined' && videoData['dash']) {
|
|
manifestUrl = videoData['dash'];
|
|
manifestType = 'application/dash+xml';
|
|
}
|
|
|
|
// Does the manifest URL start with a slash?
|
|
if (manifestUrl && manifestUrl[0] === '/') {
|
|
// If this happens, prepend the API url to make the link whole.
|
|
manifestUrl = goodTube_videoServer_url+manifestUrl;
|
|
}
|
|
|
|
// Add the HLS or DASH source
|
|
goodTube_videojs_player.src({
|
|
src: manifestUrl,
|
|
type: manifestType
|
|
});
|
|
|
|
// Update manifest quality menu
|
|
goodTube_defaultQuality_updateMenu();
|
|
}
|
|
|
|
|
|
// Play the video
|
|
setTimeout(function() {
|
|
goodTube_player_play(player);
|
|
}, 1);
|
|
|
|
// Load the subtitles into the player
|
|
goodTube_subtitles_load(player, subtitleData);
|
|
|
|
// Load the chapters into the player
|
|
// Debug message
|
|
console.log('[GoodTube] Loading chapters...');
|
|
|
|
goodTube_chapters_load(player, videoDescription, videoDuration, chaptersData);
|
|
|
|
// Load storyboards into the player (desktop only)
|
|
if (!goodTube_mobile) {
|
|
// Debug message
|
|
console.log('[GoodTube] Loading storyboard...');
|
|
|
|
goodTube_storyboard_loaded = false;
|
|
goodTube_storyboard_load(player, storyboardData, 0);
|
|
}
|
|
}
|
|
})
|
|
// If there's any issues loading the video data, try again (after configured delay time)
|
|
.catch((error) => {
|
|
if (typeof goodTube_pendingRetry['loadVideoData'] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['loadVideoData']);
|
|
}
|
|
|
|
goodTube_pendingRetry['loadVideoData'] = setTimeout(function() {
|
|
goodTube_video_load(player);
|
|
}, goodTube_retryDelay);
|
|
|
|
// Add the loading state
|
|
goodTube_player_addLoadingState();
|
|
});
|
|
}
|
|
|
|
// Reload the video data
|
|
function goodTube_video_reloadData() {
|
|
// Debug message
|
|
console.log('\n-------------------------\n\n');
|
|
console.log('[GoodTube] Loading video data from '+goodTube_videoServer_name+'...');
|
|
|
|
let delay = 0;
|
|
if (goodTube_mobile) {
|
|
delay = 400;
|
|
}
|
|
|
|
setTimeout(function() {
|
|
goodTube_player_loadVideoDataAttempts = 0;
|
|
goodTube_video_load(goodTube_player);
|
|
}, delay);
|
|
}
|
|
|
|
// Reload the video
|
|
function goodTube_video_reloadVideo(player) {
|
|
// If we're not viewing a video, just return
|
|
if (typeof goodTube_getParams['v'] === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
// Clear any pending timeouts to prevent double ups
|
|
if (typeof goodTube_pendingRetry['reloadVideo'] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['reloadVideo']);
|
|
}
|
|
|
|
// Only re-attempt to load these max configured retry attempts
|
|
if (goodTube_player_reloadVideoAttempts > goodTube_retryAttempts) {
|
|
// Show an error or select next server if we're on automatic mode
|
|
goodTube_error_show();
|
|
|
|
return;
|
|
}
|
|
|
|
// Store the current video src
|
|
let currentSrc = player.src;
|
|
|
|
// Clear the player
|
|
goodTube_player_clear(player);
|
|
|
|
// Now use the next javascript animation frame (via set timeout so it still works when you're not focused on the tab) to load the actual video
|
|
setTimeout(function() {
|
|
player.setAttribute('src', currentSrc);
|
|
}, 0);
|
|
|
|
goodTube_player_reloadVideoAttempts++;
|
|
}
|
|
|
|
|
|
/* Default quality selection
|
|
------------------------------------------------------------------------------------------ */
|
|
let goodTube_updateManifestQualityTimeout = false;
|
|
|
|
// Setup the default quality modal
|
|
function goodTube_defaultQuality_initModal() {
|
|
// Create the modal
|
|
let defaultQualityModal = document.createElement('div');
|
|
defaultQualityModal.classList.add('goodTube_defaultQualityModal');
|
|
defaultQualityModal.innerHTML = `
|
|
<div class='goodTube_defaultQualityModal_overlay'></div>
|
|
|
|
<div class='goodTube_defaultQualityModal_inner'>
|
|
<div class='goodTube_defaultQualityModal_title'>Select default quality</div>
|
|
<div class='goodTube_defaultQualityModal_options'>
|
|
<div class='goodTube_defaultQualityModal_option' id='goodTube_defaultQualityModal_option_4320'>4320p</div>
|
|
<div class='goodTube_defaultQualityModal_option' id='goodTube_defaultQualityModal_option_2160'>2160p</div>
|
|
<div class='goodTube_defaultQualityModal_option' id='goodTube_defaultQualityModal_option_1440'>1440p</div>
|
|
<div class='goodTube_defaultQualityModal_option' id='goodTube_defaultQualityModal_option_1080'>1080p</div>
|
|
<div class='goodTube_defaultQualityModal_option' id='goodTube_defaultQualityModal_option_720'>720p</div>
|
|
<div class='goodTube_defaultQualityModal_option' id='goodTube_defaultQualityModal_option_480'>480p</div>
|
|
<div class='goodTube_defaultQualityModal_option' id='goodTube_defaultQualityModal_option_360'>360p</div>
|
|
<div class='goodTube_defaultQualityModal_option' id='goodTube_defaultQualityModal_option_240'>240p</div>
|
|
<div class='goodTube_defaultQualityModal_option' id='goodTube_defaultQualityModal_option_144'>144p</div>
|
|
<div class='goodTube_defaultQualityModal_option' id='goodTube_defaultQualityModal_option_auto'>Auto</div>
|
|
</div> <!-- .goodTube_defaultQualityModal_inner -->
|
|
</div> <!-- .goodTube_defaultQualityModal_options -->
|
|
`;
|
|
|
|
// Add it to the DOM
|
|
document.querySelector('#goodTube_playerWrapper .video-js').appendChild(defaultQualityModal);
|
|
|
|
// Add click events to buttons
|
|
let defaultQualityOptions = document.querySelectorAll('.goodTube_defaultQualityModal .goodTube_defaultQualityModal_option');
|
|
defaultQualityOptions.forEach((defaultQualityOption) => {
|
|
defaultQualityOption.addEventListener('click', function() {
|
|
goodTube_defaultQuality_select(this.innerHTML.replace('p', ''));
|
|
});
|
|
});
|
|
|
|
// Add close click event to overlay
|
|
document.querySelector('.goodTube_defaultQualityModal .goodTube_defaultQualityModal_overlay').addEventListener('click', function() {
|
|
// Target the default quality modal
|
|
let defaultQualityModal = document.querySelector('.goodTube_defaultQualityModal');
|
|
|
|
// Hide it
|
|
if (defaultQualityModal.classList.contains('goodTube_defaultQualityModal_visible')) {
|
|
defaultQualityModal.classList.remove('goodTube_defaultQualityModal_visible');
|
|
}
|
|
});
|
|
|
|
// Esc keypress to close
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.keyCode == 27) {
|
|
// Target the default quality modal
|
|
let defaultQualityModal = document.querySelector('.goodTube_defaultQualityModal');
|
|
|
|
// Hide it
|
|
if (defaultQualityModal.classList.contains('goodTube_defaultQualityModal_visible')) {
|
|
defaultQualityModal.classList.remove('goodTube_defaultQualityModal_visible');
|
|
}
|
|
}
|
|
}, true);
|
|
}
|
|
|
|
// Update default manifest quality menu
|
|
function goodTube_defaultQuality_updateMenu() {
|
|
// This stops this function from ever accidentially firing twice
|
|
if (goodTube_updateManifestQualityTimeout) {
|
|
clearTimeout(goodTube_updateManifestQualityTimeout);
|
|
}
|
|
|
|
// Find and click the default quality button (if it can't be found, this will call itself again until it works)
|
|
let qualityButtons = document.querySelectorAll('.vjs-quality-selector');
|
|
if (qualityButtons && typeof qualityButtons[1] !== 'undefined') {
|
|
if (qualityButtons.length === 2) {
|
|
// Show the correct quality menu item
|
|
qualityButtons[0].style.display = 'none';
|
|
qualityButtons[1].style.display = 'block';
|
|
|
|
// Target the manifest quality menu
|
|
let manifestQualityMenu = qualityButtons[1].querySelector('ul');
|
|
|
|
// Check if the first menu item is "Select default quality"
|
|
let firstMenuItem = manifestQualityMenu.querySelector('li.vjs-menu-item:first-child .vjs-menu-item-text');
|
|
|
|
// If it's not populated yet, try again
|
|
if (!firstMenuItem) {
|
|
if (goodTube_updateManifestQualityTimeout) {
|
|
clearTimeout(goodTube_updateManifestQualityTimeout);
|
|
}
|
|
|
|
goodTube_updateManifestQualityTimeout = setTimeout(goodTube_defaultQuality_updateMenu, 100);
|
|
return;
|
|
}
|
|
|
|
// Does the 'Select default quality' menu item exist?
|
|
let selectDefaultMenuItem = firstMenuItem;
|
|
|
|
// If it does not
|
|
if (firstMenuItem.innerHTML !== 'Select default quality') {
|
|
// Add the 'always use max' menu item
|
|
selectDefaultMenuItem = document.createElement('li');
|
|
selectDefaultMenuItem.classList.add('vjs-menu-item');
|
|
selectDefaultMenuItem.classList.add('select-default');
|
|
selectDefaultMenuItem.innerHTML = `
|
|
<span class="vjs-menu-item-text">Select default quality</span>
|
|
<span class="vjs-control-text" aria-live="polite"></span>
|
|
`;
|
|
selectDefaultMenuItem.addEventListener('click', goodTube_defaultQuality_showModal);
|
|
manifestQualityMenu.prepend(selectDefaultMenuItem);
|
|
|
|
// Add a click action to all the other menu options (this turns off 'Select default quality')
|
|
let otherMenuItems = manifestQualityMenu.querySelectorAll('li.vjs-menu-item:not(.select-default)');
|
|
otherMenuItems.forEach((otherMenuItem) => {
|
|
otherMenuItem.addEventListener('click', goodTube_defaultQuality_disable);
|
|
|
|
// For some reason we need this to support mobile devices, but not for other event listeners here? Weird.
|
|
otherMenuItem.addEventListener('touchstart', goodTube_defaultQuality_disable);
|
|
});
|
|
}
|
|
|
|
// Get the default quality cookie
|
|
let defaultQuality = goodTube_helper_getCookie('goodTube_selectDefaultNew');
|
|
|
|
// If it's not set, use 1080p by default
|
|
if (!defaultQuality) {
|
|
goodTube_helper_setCookie('goodTube_selectDefaultNew', '1080');
|
|
defaultQuality = '1080';
|
|
}
|
|
|
|
// Select the default quality
|
|
goodTube_defaultQuality_select(defaultQuality);
|
|
}
|
|
}
|
|
else {
|
|
if (goodTube_updateManifestQualityTimeout) {
|
|
clearTimeout(goodTube_updateManifestQualityTimeout);
|
|
}
|
|
|
|
goodTube_updateManifestQualityTimeout = setTimeout(goodTube_defaultQuality_updateMenu, 100);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Show the default quality modal
|
|
function goodTube_defaultQuality_showModal() {
|
|
// Target the default quality modal
|
|
let defaultQualityModal = document.querySelector('.goodTube_defaultQualityModal');
|
|
|
|
// Show it
|
|
if (!defaultQualityModal.classList.contains('goodTube_defaultQualityModal_visible')) {
|
|
defaultQualityModal.classList.add('goodTube_defaultQualityModal_visible');
|
|
}
|
|
}
|
|
|
|
// Select the default manifest quality
|
|
function goodTube_defaultQuality_select(defaultQuality) {
|
|
// Set the cookie to remember the default quality
|
|
goodTube_helper_setCookie('goodTube_selectDefaultNew', defaultQuality);
|
|
|
|
// Target the default quality modal
|
|
let defaultQualityModal = document.querySelector('.goodTube_defaultQualityModal');
|
|
|
|
// Hide the modal if it's showing
|
|
if (defaultQualityModal.classList.contains('goodTube_defaultQualityModal_visible')) {
|
|
defaultQualityModal.classList.remove('goodTube_defaultQualityModal_visible');
|
|
}
|
|
|
|
// Select the modal option
|
|
document.querySelector('.goodTube_defaultQualityModal_selected')?.classList.remove('goodTube_defaultQualityModal_selected');
|
|
document.querySelector('#goodTube_defaultQualityModal_option_'+defaultQuality.toLowerCase())?.classList.add('goodTube_defaultQualityModal_selected');
|
|
|
|
// Find the correct manifest quality menu item
|
|
let defaultQualityButton = false;
|
|
|
|
// Get the quality menu items
|
|
let qualityMenuItems = document.querySelectorAll('.vjs-quality-selector li.vjs-menu-item');
|
|
|
|
// For each quality menu item (this will be highest to lowest order)
|
|
qualityMenuItems.forEach((qualityMenuItem) => {
|
|
// Get the value of this quality menu item
|
|
let value = qualityMenuItem.querySelector('.vjs-menu-item-text').innerHTML.replace('p', '');
|
|
|
|
// If they selected 'auto' and the item is 'Auto'
|
|
// OR
|
|
// If the value is less than or equal to the default quality AND we haven't found one yet
|
|
if ((value.toLowerCase() === 'auto' && defaultQuality.toLowerCase() === 'auto') || (parseFloat(value) <= parseFloat(defaultQuality) && !defaultQualityButton)) {
|
|
// Target the default quality button
|
|
defaultQualityButton = qualityMenuItem;
|
|
}
|
|
});
|
|
|
|
// If we didn't find the quality menu item, just return - safety check
|
|
if (!defaultQualityButton) {
|
|
return;
|
|
}
|
|
|
|
// Click the quality menu item
|
|
defaultQualityButton.click();
|
|
|
|
// Debug message
|
|
if (defaultQuality.toLowerCase() === 'auto') {
|
|
console.log('[GoodTube] Setting default quality to '+defaultQuality[0].toUpperCase()+defaultQuality.slice(1));
|
|
}
|
|
else {
|
|
console.log('[GoodTube] Selecting nearest default quality to '+defaultQuality+'p ('+defaultQualityButton.querySelector('.vjs-menu-item-text').innerHTML+')');
|
|
}
|
|
}
|
|
|
|
// Turn off the default manifest quality option
|
|
function goodTube_defaultQuality_disable() {
|
|
// Find the manifest quality menu
|
|
let qualityMenu = document.querySelectorAll('.vjs-quality-selector')[1];
|
|
|
|
// Find the 'Select default quality' quality button
|
|
let selectDefaultMenuItem = qualityMenu.querySelector('li.select-default');
|
|
|
|
// Remove the selected class
|
|
if (selectDefaultMenuItem.classList.contains('vjs-selected')) {
|
|
selectDefaultMenuItem.classList.remove('vjs-selected');
|
|
}
|
|
|
|
// Remove any auto selected classes
|
|
let autoSelectedItem = qualityMenu.querySelector('li.vjs-auto-selected');
|
|
if (autoSelectedItem) {
|
|
autoSelectedItem.classList.remove('vjs-auto-selected');
|
|
}
|
|
}
|
|
|
|
|
|
/* Chapters
|
|
------------------------------------------------------------------------------------------ */
|
|
let goodTube_chapters_updateDisplayInterval = false;
|
|
let goodTube_chapters_showTitleInterval = false;
|
|
let goodTube_chapters_updateDataInterval = false;
|
|
|
|
// Load chapters
|
|
function goodTube_chapters_load(player, description, totalDuration, chaptersData) {
|
|
// Clear any existing chapters
|
|
goodTube_chapters_remove();
|
|
|
|
// Create a variable to store the chapters
|
|
let chapters = [];
|
|
|
|
// If we don't have chapters data already
|
|
if (!chaptersData) {
|
|
// First up, try to get the chapters from the video description
|
|
let lines = description.split("\n");
|
|
let regex = /(\d{0,2}:?\d{1,2}:\d{2})/g;
|
|
|
|
for (let line of lines) {
|
|
const matches = line.match(regex);
|
|
if (matches) {
|
|
let ts = matches[0];
|
|
let title = line
|
|
.split(" ")
|
|
.filter((l) => !l.includes(ts))
|
|
.join(" ");
|
|
|
|
chapters.push({
|
|
time: ts,
|
|
title: title,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Ensure the first chapter is 0 (sometimes the video descriptions are off)
|
|
if (!chapters.length || chapters.length <= 0 || chapters[0]['time'].split(':').reduce((acc,time) => (60 * acc) + +time) > 0) {
|
|
chapters = [];
|
|
}
|
|
|
|
// If that didn't work, get them from the DOM (this works for desktop only)
|
|
if ((!chapters.length || chapters.length <= 0) && !goodTube_mobile) {
|
|
// Target the chapters in the DOM
|
|
let uiChapters = Array.from(document.querySelectorAll("#panels ytd-engagement-panel-section-list-renderer:nth-child(2) #content ytd-macro-markers-list-renderer #contents ytd-macro-markers-list-item-renderer #endpoint #details"));
|
|
|
|
// If the chapters from the DOM change, reload the chapters. This is important because it's async data that changes.
|
|
// ----------------------------------------
|
|
if (goodTube_chapters_updateDataInterval) {
|
|
clearInterval(goodTube_chapters_updateDataInterval);
|
|
}
|
|
|
|
let prevUIChapters = JSON.stringify(document.querySelectorAll("#panels ytd-engagement-panel-section-list-renderer:nth-child(2) #content ytd-macro-markers-list-renderer #contents ytd-macro-markers-list-item-renderer #endpoint #details"));
|
|
goodTube_chapters_updateDataInterval = setInterval(function() {
|
|
let chaptersInnerHTML = JSON.stringify(document.querySelectorAll("#panels ytd-engagement-panel-section-list-renderer:nth-child(2) #content ytd-macro-markers-list-renderer #contents ytd-macro-markers-list-item-renderer #endpoint #details"));
|
|
|
|
if (chaptersInnerHTML !== prevUIChapters) {
|
|
prevUIChapters = chaptersInnerHTML;
|
|
goodTube_chapters_load(player, description, totalDuration);
|
|
}
|
|
}, 1000);
|
|
// ----------------------------------------
|
|
|
|
let withTitleAndTime = uiChapters.map((node) => ({
|
|
title: node.querySelector(".macro-markers")?.textContent,
|
|
time: node.querySelector("#time")?.textContent,
|
|
}));
|
|
|
|
let filtered = withTitleAndTime.filter(
|
|
(element) =>
|
|
element.title !== undefined &&
|
|
element.title !== null &&
|
|
element.time !== undefined &&
|
|
element.time !== null
|
|
);
|
|
|
|
chapters = [
|
|
...new Map(filtered.map((node) => [node.time, node])).values(),
|
|
];
|
|
}
|
|
}
|
|
// If we do have chapters data
|
|
else {
|
|
chapters = chaptersData;
|
|
}
|
|
|
|
// Ensure the first chapter is 0 (sometimes the video descriptions are off)
|
|
let firstChapterTime = 0;
|
|
|
|
if (chapters.length && chapters.length > 0) {
|
|
firstChapterTime = chapters[0]['time'];
|
|
|
|
if (typeof firstChapterTime !== 'number') {
|
|
firstChapterTime = firstChapterTime.split(':').reduce((acc,time) => (60 * acc) + +time);
|
|
}
|
|
}
|
|
|
|
if (!chapters.length || chapters.length <= 0 || firstChapterTime > 0) {
|
|
chapters = [];
|
|
}
|
|
|
|
// If we found the chapters data
|
|
if (chapters.length > 0) {
|
|
// Load chapters into the player
|
|
goodTube_chapters_add(player, chapters, totalDuration);
|
|
}
|
|
// Otherwise this video does not have chapters
|
|
else {
|
|
// Debug message
|
|
console.log('[GoodTube] No chapters found');
|
|
}
|
|
}
|
|
|
|
function goodTube_chapters_add(player, chapters, totalDuration) {
|
|
// Create a container for our chapters
|
|
let chaptersContainer = document.createElement('div');
|
|
chaptersContainer.classList.add('goodTube_chapters');
|
|
|
|
let markersContainer = document.createElement('div');
|
|
markersContainer.classList.add('goodTube_markers');
|
|
|
|
// For each chapter
|
|
let i = 0;
|
|
chapters.forEach((chapter) => {
|
|
// Create a chapter element
|
|
let chapterDiv = document.createElement('div');
|
|
chapterDiv.classList.add('goodTube_chapter');
|
|
if (typeof chapters[i+1] !== 'undefined') {
|
|
if (typeof chapters[i+1]['time'] === 'number') {
|
|
chapterDiv.setAttribute('chapter-time', chapters[i+1]['time']);
|
|
}
|
|
else {
|
|
chapterDiv.setAttribute('chapter-time', chapters[i+1]['time'].split(':').reduce((acc,time) => (60 * acc) + +time));
|
|
}
|
|
}
|
|
|
|
|
|
// Create a marker element
|
|
let markerDiv = document.createElement('div');
|
|
markerDiv.classList.add('goodTube_marker');
|
|
if (typeof chapters[i+1] !== 'undefined') {
|
|
if (typeof chapters[i+1]['time'] === 'number') {
|
|
markerDiv.setAttribute('marker-time', chapters[i+1]['time']);
|
|
}
|
|
else {
|
|
markerDiv.setAttribute('marker-time', chapters[i+1]['time'].split(':').reduce((acc,time) => (60 * acc) + +time));
|
|
}
|
|
}
|
|
|
|
// Add a hover action to show the title in the tooltip (desktop only)
|
|
if (!goodTube_mobile) {
|
|
chapterDiv.addEventListener('mouseover', function() {
|
|
document.querySelector('#goodTube_playerWrapper .vjs-progress-control .vjs-mouse-display .vjs-time-tooltip')?.setAttribute('chapter-title', chapter['title']);
|
|
});
|
|
}
|
|
|
|
// Position the chapter with CSS
|
|
// ------------------------------
|
|
|
|
// Convert the timestamp (HH:MM:SS) to seconds
|
|
let time = 0;
|
|
if (typeof chapter['time'] === 'number') {
|
|
time = chapter['time'];
|
|
}
|
|
else {
|
|
time = chapter['time'].split(':').reduce((acc,time) => (60 * acc) + +time);
|
|
}
|
|
|
|
// Get time as percentage. This is the starting point of this chapter.
|
|
let startingPercentage = (time / totalDuration) * 100;
|
|
|
|
// Set the starting point
|
|
chapterDiv.style.left = startingPercentage+'%';
|
|
|
|
// Get the starting point of the next chapter (HH:MM:SS) and convert it to seconds
|
|
// If there's no next chapter, use 100%
|
|
let nextChapterStart = totalDuration;
|
|
if (typeof chapters[i+1] !== 'undefined') {
|
|
if (typeof chapters[i+1]['time'] === 'number') {
|
|
nextChapterStart = chapters[i+1]['time'];
|
|
}
|
|
else {
|
|
nextChapterStart = chapters[i+1]['time'].split(':').reduce((acc,time) => (60 * acc) + +time);
|
|
}
|
|
}
|
|
|
|
// Get the starting point of the next chapter as percentage. This is the starting point of this chapter.
|
|
let endingPercentage = (nextChapterStart / totalDuration) * 100;
|
|
|
|
// Set the width to be the ending point MINUS the starting point (difference between them = length)
|
|
chapterDiv.style.width = (endingPercentage - startingPercentage)+'%';
|
|
|
|
// Position the marker
|
|
markerDiv.style.left = endingPercentage+'%';
|
|
|
|
// ------------------------------
|
|
|
|
|
|
// Add the chapter to the chapters container
|
|
chaptersContainer.appendChild(chapterDiv);
|
|
|
|
// Add the marker to the markers container
|
|
markersContainer.appendChild(markerDiv);
|
|
|
|
// Increment the loop
|
|
i++;
|
|
});
|
|
|
|
// Add an action to show the chapter title next to the time duration (mobile only)
|
|
if (goodTube_mobile) {
|
|
goodTube_chapters_showTitleInterval = setInterval(function() {
|
|
let currentPlayerTime = parseFloat(player.currentTime);
|
|
let currentChapterTitle = false;
|
|
chapters.forEach((chapter) => {
|
|
let chapterTime = false;
|
|
|
|
if (typeof chapter['time'] === 'number') {
|
|
chapterTime = chapter['time'];
|
|
}
|
|
else {
|
|
chapterTime = chapter['time'].split(':').reduce((acc,time) => (60 * acc) + +time);
|
|
}
|
|
|
|
if (parseFloat(currentPlayerTime) >= parseFloat(chapterTime)) {
|
|
currentChapterTitle = chapter['title'];
|
|
}
|
|
});
|
|
|
|
if (currentChapterTitle) {
|
|
document.querySelector('#goodTube_playerWrapper .vjs-time-control .vjs-duration-display')?.setAttribute('chapter-title', '· '+currentChapterTitle);
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
// Add the chapters container to the player
|
|
document.querySelector('#goodTube_playerWrapper .vjs-progress-control')?.appendChild(chaptersContainer);
|
|
|
|
// Add the markers container to the player
|
|
document.querySelector('#goodTube_playerWrapper .vjs-progress-control .vjs-play-progress')?.appendChild(markersContainer);
|
|
|
|
// Add chapters class to the player
|
|
if (!document.querySelector('#goodTube_playerWrapper').classList.contains('goodTube_hasChapters')) {
|
|
document.querySelector('#goodTube_playerWrapper').classList.add('goodTube_hasChapters');
|
|
}
|
|
|
|
// Update the chapters display as we play the video
|
|
goodTube_chapters_updateDisplayInterval = setInterval(function() {
|
|
// Hide markers that are before the current play position / red play bar
|
|
let markerElements = document.querySelectorAll('.goodTube_markers .goodTube_marker');
|
|
|
|
markerElements.forEach((element) => {
|
|
if (element.getAttribute('marker-time')) {
|
|
if (parseFloat(player.currentTime) >= parseFloat(element.getAttribute('marker-time'))) {
|
|
if (!element.classList.contains('goodTube_showMarker')) {
|
|
element.classList.add('goodTube_showMarker')
|
|
}
|
|
}
|
|
else {
|
|
if (element.classList.contains('goodTube_showMarker')) {
|
|
element.classList.remove('goodTube_showMarker')
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Make chapter hover RED for chapters that are before the current play position / red play bar
|
|
let chapterElements = document.querySelectorAll('.goodTube_chapters .goodTube_chapter');
|
|
|
|
chapterElements.forEach((element) => {
|
|
if (element.getAttribute('chapter-time')) {
|
|
if (parseFloat(player.currentTime) >= parseFloat(element.getAttribute('chapter-time'))) {
|
|
if (!element.classList.contains('goodTube_redChapter')) {
|
|
element.classList.add('goodTube_redChapter')
|
|
}
|
|
}
|
|
else {
|
|
if (element.classList.contains('goodTube_redChapter')) {
|
|
element.classList.remove('goodTube_redChapter')
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
}, 100);
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Chapters loaded');
|
|
}
|
|
|
|
function goodTube_chapters_remove() {
|
|
// Remove timeouts and intervals
|
|
if (goodTube_chapters_updateDisplayInterval) {
|
|
clearInterval(goodTube_chapters_updateDisplayInterval);
|
|
goodTube_chapters_updateDisplayInterval = false;
|
|
}
|
|
|
|
if (goodTube_chapters_showTitleInterval) {
|
|
clearInterval(goodTube_chapters_showTitleInterval);
|
|
goodTube_chapters_showTitleInterval = false;
|
|
}
|
|
|
|
if (goodTube_chapters_updateDataInterval) {
|
|
clearInterval(goodTube_chapters_updateDataInterval);
|
|
goodTube_chapters_updateDataInterval = false;
|
|
}
|
|
|
|
// Remove interface elements
|
|
document.querySelector('#goodTube_playerWrapper .vjs-time-control .vjs-duration-display')?.setAttribute('chapter-title', '');
|
|
document.querySelector('.goodTube_chapters')?.remove();
|
|
document.querySelector('.goodTube_markers')?.remove();
|
|
if (document.querySelector('#goodTube_playerWrapper').classList.contains('goodTube_hasChapters')) {
|
|
document.querySelector('#goodTube_playerWrapper').classList.remove('goodTube_hasChapters');
|
|
}
|
|
}
|
|
|
|
|
|
/* Subtitles
|
|
------------------------------------------------------------------------------------------ */
|
|
// Load subtitles
|
|
function goodTube_subtitles_load(player, subtitleData) {
|
|
// If subtitle data is set to false (so we always do it for Piped servers), or if it exists for an invidious server
|
|
if (!subtitleData || subtitleData.length > 0) {
|
|
// Debug message
|
|
console.log('[GoodTube] Loading subtitles...');
|
|
|
|
// If there's no subtitle data, start with the first fallback server (Piped)
|
|
if (!subtitleData) {
|
|
goodTube_storyboardSubtitleServers_subtitleIndex = 1;
|
|
}
|
|
// Otherwise start with your current server (Invidious)
|
|
else {
|
|
goodTube_storyboardSubtitleServers_subtitleIndex = 0;
|
|
}
|
|
|
|
// Check the subtitle server works
|
|
goodTube_subtitles_checkServer(player, subtitleData, goodTube_videoServer_url);
|
|
}
|
|
}
|
|
|
|
// Check the subtitle server
|
|
function goodTube_subtitles_checkServer(player, subtitleData, subtitleApi) {
|
|
// If our selected index is greater than 0, the first selected server failed to load the subtitles
|
|
// So we use the next configured fallback server
|
|
if (goodTube_storyboardSubtitleServers_subtitleIndex > 0) {
|
|
// If we're out of fallback servers, show an error
|
|
if (typeof goodTube_storyboardSubtitleServers[(goodTube_storyboardSubtitleServers_subtitleIndex-1)] === 'undefined') {
|
|
// Debug message
|
|
console.log('[GoodTube] Subtitles could not be loaded');
|
|
|
|
return;
|
|
}
|
|
|
|
// Otherwise select the next fallback server
|
|
subtitleApi = goodTube_storyboardSubtitleServers[(goodTube_storyboardSubtitleServers_subtitleIndex-1)];
|
|
|
|
// Re fetch the video data for this server (always invidious)
|
|
|
|
// Call the API (die after 5s)
|
|
fetch(subtitleApi+"/api/v1/videos/"+goodTube_getParams['v'], {
|
|
signal: AbortSignal.timeout(5000)
|
|
})
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
// Turn video data into JSON
|
|
let videoData = JSON.parse(data);
|
|
|
|
// Get the subtitle data
|
|
let subtitleData = videoData['captions'];
|
|
|
|
// If there are subtitles
|
|
if (subtitleData && subtitleData.length > 0) {
|
|
// Get the subtitle (die after 5s)
|
|
fetch(subtitleApi+subtitleData[0]['url'], {
|
|
signal: AbortSignal.timeout(5000)
|
|
})
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
// If the data wasn't right, try the next fallback server
|
|
if (data.substr(0,6) !== 'WEBVTT') {
|
|
goodTube_subtitles_checkServer(player, subtitleData, subtitleApi);
|
|
}
|
|
// If the data was good, load the subtitles
|
|
else {
|
|
goodTube_subtitles_add(player, subtitleData, subtitleApi);
|
|
}
|
|
})
|
|
// If the fetch failed, try the next fallback server
|
|
.catch((error) => {
|
|
goodTube_subtitles_checkServer(player, subtitleData, subtitleApi);
|
|
});
|
|
}
|
|
else {
|
|
// Debug message
|
|
console.log('[GoodTube] This video does not have subtitles');
|
|
|
|
return;
|
|
}
|
|
})
|
|
// If the fetch failed, try the next fallback server
|
|
.catch((error) => {
|
|
goodTube_subtitles_checkServer(player, subtitleData, subtitleApi);
|
|
});
|
|
|
|
|
|
}
|
|
// If our selected index is 0, just use the data from the current subtitle server (Invidious)
|
|
else {
|
|
// Get the subtitle (die after 5s)
|
|
fetch(subtitleApi+subtitleData[0]['url'], {
|
|
signal: AbortSignal.timeout(5000)
|
|
})
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
// If the data wasn't right, try the next fallback server
|
|
if (data.substr(0,6) !== 'WEBVTT') {
|
|
goodTube_subtitles_checkServer(player, subtitleData, subtitleApi);
|
|
}
|
|
// If the data was good, load the subtitles
|
|
else {
|
|
goodTube_subtitles_add(player, subtitleData, subtitleApi);
|
|
}
|
|
})
|
|
// If the fetch failed, try the next fallback server
|
|
.catch((error) => {
|
|
goodTube_subtitles_checkServer(player, subtitleData, subtitleApi);
|
|
});
|
|
}
|
|
|
|
goodTube_storyboardSubtitleServers_subtitleIndex++;
|
|
}
|
|
|
|
// Add the subtitles into the player
|
|
function goodTube_subtitles_add(player, subtitleData, subtitleApi) {
|
|
// For each subtitle
|
|
let previous_subtitle = false;
|
|
subtitleData.forEach((subtitle) => {
|
|
// Format the data
|
|
let subtitle_url = false;
|
|
let subtitle_label = false;
|
|
|
|
subtitle_url = subtitleApi+subtitle['url'];
|
|
subtitle_label = subtitle['label'];
|
|
|
|
// Ensure we have all the subtitle data AND don't load a subtitle with the same label twice (this helps Piped to load actual captions over auto-generated captions if both exist)
|
|
if (subtitle_url && subtitle_label && subtitle_label !== previous_subtitle) {
|
|
previous_subtitle = subtitle_label;
|
|
|
|
// Capitalise the first letter of the label, this looks a bit better
|
|
subtitle_label = subtitle_label[0].toUpperCase() + subtitle_label.slice(1);
|
|
|
|
// Add the subtitle to videojs
|
|
goodTube_videojs_player.addRemoteTextTrack({
|
|
kind: 'captions',
|
|
language: subtitle_label,
|
|
src: subtitle_url
|
|
}, false);
|
|
}
|
|
});
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Subtitles loaded');
|
|
}
|
|
|
|
|
|
/* Storyboards
|
|
------------------------------------------------------------------------------------------ */
|
|
let goodTube_storyboard_vttThumbnailsFunction = false;
|
|
let goodTube_storyboard_loaded = false;
|
|
|
|
// Load storyboard
|
|
function goodTube_storyboard_load(player, storyboardData, fallbackServerIndex) {
|
|
// If our storyboard has already loaded, just return.
|
|
if (goodTube_storyboard_loaded) {
|
|
return;
|
|
}
|
|
|
|
// If we're out of fallback servers, show an error
|
|
if (typeof goodTube_storyboardSubtitleServers[fallbackServerIndex] === 'undefined') {
|
|
// Debug message
|
|
console.log('[GoodTube] Storyboard could not be loaded');
|
|
return;
|
|
}
|
|
|
|
|
|
// If we're using Piped, then we need to fetch the storyboard data from a fallback server before checking anything
|
|
if (goodTube_videoServer_type === 3) {
|
|
let apiEndpoint = goodTube_storyboardSubtitleServers[fallbackServerIndex]+"/api/v1/videos/"+goodTube_getParams['v'];
|
|
|
|
// Get the video data (die after 5s)
|
|
fetch(apiEndpoint, {
|
|
signal: AbortSignal.timeout(5000)
|
|
})
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
// If our storyboard has already loaded, just return.
|
|
if (goodTube_storyboard_loaded) {
|
|
return;
|
|
}
|
|
|
|
// Turn video data into JSON
|
|
let videoData = JSON.parse(data);
|
|
|
|
// Check the storyboard data is all good, if not try the next fallback server
|
|
if (typeof videoData['storyboards'] === 'undefined') {
|
|
fallbackServerIndex++;
|
|
goodTube_storyboard_load(player, storyboardData, fallbackServerIndex);
|
|
}
|
|
// Otherwise get the storyboard data and check the server works
|
|
else {
|
|
storyboardData = videoData['storyboards'];
|
|
goodTube_storyboard_checkServer(player, storyboardData, goodTube_storyboardSubtitleServers[fallbackServerIndex]);
|
|
}
|
|
})
|
|
// If the fetch failed, try the next fallback server
|
|
.catch((error) => {
|
|
fallbackServerIndex++;
|
|
goodTube_storyboard_load(player, storyboardData, fallbackServerIndex);
|
|
});
|
|
}
|
|
|
|
|
|
// Otherwise for Invidious, check straight away
|
|
else {
|
|
// Check the storyboard server works
|
|
goodTube_storyboardSubtitleServers_storyboardIndex = 0;
|
|
goodTube_storyboard_checkServer(player, storyboardData, goodTube_videoServer_url);
|
|
}
|
|
}
|
|
|
|
// Check the storyboard server
|
|
function goodTube_storyboard_checkServer(player, storyboardData, storyboardApi) {
|
|
// If our storyboard has already loaded, just return.
|
|
if (goodTube_storyboard_loaded) {
|
|
return;
|
|
}
|
|
|
|
// If our selected index is greater than 0, the first selected server failed to load the storyboard
|
|
// So we use the next configured fallback server
|
|
if (goodTube_storyboardSubtitleServers_storyboardIndex > 0) {
|
|
// If we're out of fallback servers, show an error
|
|
if (typeof goodTube_storyboardSubtitleServers[(goodTube_storyboardSubtitleServers_storyboardIndex-1)] === 'undefined') {
|
|
// Debug message
|
|
console.log('[GoodTube] Storyboard could not be loaded');
|
|
return;
|
|
}
|
|
|
|
// Otherwise select the next fallback server
|
|
storyboardApi = goodTube_storyboardSubtitleServers[(goodTube_storyboardSubtitleServers_storyboardIndex-1)];
|
|
}
|
|
goodTube_storyboardSubtitleServers_storyboardIndex++;
|
|
|
|
// If there's no storyboard data, try the next fallback server
|
|
if (!storyboardData.length || storyboardData.length <= 0) {
|
|
goodTube_storyboard_checkServer(player, storyboardData, storyboardApi);
|
|
}
|
|
// Otherwise we have data, so check the storyboard returned actually loads
|
|
else {
|
|
// Call the API (die after 5s)
|
|
fetch(storyboardApi+storyboardData[0]['url'], {
|
|
signal: AbortSignal.timeout(5000)
|
|
})
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
// If our storyboard has already loaded, just return.
|
|
if (goodTube_storyboard_loaded) {
|
|
return;
|
|
}
|
|
|
|
// If it failed to get WEBVTT format, try the next fallback server
|
|
if (data.substr(0,6) !== 'WEBVTT') {
|
|
goodTube_storyboard_checkServer(player, storyboardData, storyboardApi);
|
|
}
|
|
// If it got WEBVTT format, find the URL of the first storyboard image inside that data (we've got to fish this out of a plain text return)
|
|
else {
|
|
let gotTheUrl = false;
|
|
let storyboardUrl = false;
|
|
let items = data.split('\n\n');
|
|
if (items.length && items.length > 1) {
|
|
let itemBits = items[1].split('\n');
|
|
|
|
if (itemBits.length && itemBits.length > 1) {
|
|
storyboardUrl = itemBits[1];
|
|
|
|
if (storyboardUrl.indexOf('https') !== -1) {
|
|
gotTheUrl = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we found the URL of the first storyboard image, check it loads
|
|
if (gotTheUrl) {
|
|
// Call the API (die after 5s)
|
|
fetch(storyboardUrl, {
|
|
signal: AbortSignal.timeout(5000)
|
|
})
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
// If our storyboard has already loaded, just return.
|
|
if (goodTube_storyboard_loaded) {
|
|
return;
|
|
}
|
|
|
|
// Check the data returned, it should be an image not a HTML document (this often comes back when it fails to load)
|
|
if (data.indexOf('<html') === -1) {
|
|
// All good, load the storyboard
|
|
goodTube_storyboard_add(player, storyboardData, storyboardApi);
|
|
}
|
|
// It's a HTML document and not an image, so try the next fallback server
|
|
else {
|
|
goodTube_storyboard_checkServer(player, storyboardData, storyboardApi);
|
|
}
|
|
})
|
|
// If the fetch failed, try the next fallback server
|
|
.catch((error) => {
|
|
goodTube_storyboard_checkServer(player, storyboardData, storyboardApi);
|
|
});
|
|
}
|
|
// Otherwise we didn't find the URL of the first storyboard image, so try the next fallback server
|
|
else {
|
|
goodTube_storyboard_checkServer(player, storyboardData, storyboardApi);
|
|
}
|
|
}
|
|
})
|
|
// If the fetch failed, try the next fallback server
|
|
.catch((error) => {
|
|
goodTube_storyboard_checkServer(player, storyboardData, storyboardApi);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Load the storyboard into the player after checking the server
|
|
function goodTube_storyboard_add(player, storyboardData, storyboardApi) {
|
|
// If our storyboard has already loaded, just return.
|
|
if (goodTube_storyboard_loaded) {
|
|
return;
|
|
}
|
|
|
|
// Go through each storyboard and find the highest quality (up to max 100px in height, this helps to speed up the preview time)
|
|
let highestQualityStoryboardUrl = false;
|
|
let highestQualityStoryboardWidth = 0;
|
|
storyboardData.forEach((storyboard) => {
|
|
if (parseFloat(storyboard['width']) > highestQualityStoryboardWidth && parseFloat(storyboard['height']) < 100) {
|
|
highestQualityStoryboardUrl = storyboard['url'];
|
|
highestQualityStoryboardWidth = parseFloat(storyboard['width']);
|
|
}
|
|
});
|
|
|
|
// If we have a storyboard to load
|
|
if (highestQualityStoryboardUrl) {
|
|
// Store the core vttThumbnails function so we can call it again, because this plugin overwrites it's actual function once loaded!
|
|
if (typeof goodTube_videojs_player.vttThumbnails === 'function') {
|
|
goodTube_storyboard_vttThumbnailsFunction = goodTube_videojs_player.vttThumbnails;
|
|
}
|
|
|
|
// Restore the core function
|
|
goodTube_videojs_player.vttThumbnails = goodTube_storyboard_vttThumbnailsFunction;
|
|
|
|
// Load the highest quality storyboard
|
|
goodTube_videojs_player.vttThumbnails({
|
|
src: storyboardApi+highestQualityStoryboardUrl
|
|
});
|
|
|
|
goodTube_storyboard_loaded = true;
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Storyboard loaded');
|
|
}
|
|
}
|
|
|
|
|
|
/* Picture in picture
|
|
------------------------------------------------------------------------------------------ */
|
|
let goodTube_pip = false;
|
|
function goodTube_pip_init() {
|
|
// If we leave the picture in picture
|
|
addEventListener('leavepictureinpicture', (event) => {
|
|
// If we're not viewing a video
|
|
if (typeof goodTube_getParams['v'] === 'undefined') {
|
|
// Pause the player
|
|
goodTube_player_pause(goodTube_player);
|
|
}
|
|
|
|
goodTube_pip = false;
|
|
});
|
|
|
|
// If we enter the picture in picture
|
|
addEventListener('enterpictureinpicture', (event) => {
|
|
goodTube_pip = true;
|
|
});
|
|
}
|
|
|
|
function goodTube_pip_update() {
|
|
if (!goodTube_pip) {
|
|
return;
|
|
}
|
|
|
|
// Support play and pause (but only attach these events once!)
|
|
if ("mediaSession" in navigator) {
|
|
// Play
|
|
navigator.mediaSession.setActionHandler("play", () => {
|
|
goodTube_player_play(goodTube_player);
|
|
});
|
|
|
|
// Pause
|
|
navigator.mediaSession.setActionHandler("pause", () => {
|
|
goodTube_player_pause(goodTube_player);
|
|
});
|
|
|
|
// Next track
|
|
if (goodTube_nav_nextButton) {
|
|
navigator.mediaSession.setActionHandler("nexttrack", () => {
|
|
goodTube_nav_next(true);
|
|
});
|
|
}
|
|
else {
|
|
navigator.mediaSession.setActionHandler('nexttrack', null);
|
|
}
|
|
|
|
// Prev track
|
|
if (goodTube_nav_prevButton) {
|
|
navigator.mediaSession.setActionHandler("previoustrack", () => {
|
|
goodTube_nav_prev();
|
|
});
|
|
}
|
|
else {
|
|
navigator.mediaSession.setActionHandler('previoustrack', null);
|
|
}
|
|
}
|
|
}
|
|
|
|
function goodTube_pip_showHide() {
|
|
if (goodTube_pip) {
|
|
document.exitPictureInPicture();
|
|
goodTube_pip = false;
|
|
}
|
|
else {
|
|
goodTube_player.requestPictureInPicture();
|
|
goodTube_pip = true;
|
|
|
|
// If the miniplayer is open, remove it
|
|
if (goodTube_miniplayer) {
|
|
goodTube_miniplayer_showHide();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Miniplayer
|
|
------------------------------------------------------------------------------------------ */
|
|
let goodTube_miniplayer = false;
|
|
let goodTube_miniplayer_video = false;
|
|
function goodTube_miniplayer_update() {
|
|
if (!goodTube_miniplayer) {
|
|
return;
|
|
}
|
|
|
|
// This is needed to show it differently when we're off a video page (desktop only)
|
|
if (!goodTube_mobile) {
|
|
let youtube_wrapper = document.querySelector('ytd-watch-flexy');
|
|
|
|
if (youtube_wrapper) {
|
|
if (typeof goodTube_getParams['v'] !== 'undefined') {
|
|
youtube_wrapper.classList.remove('goodTube_miniplayer');
|
|
}
|
|
else {
|
|
youtube_wrapper.classList.add('goodTube_miniplayer');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set the video id, this is used for the expand button
|
|
if (typeof goodTube_getParams['v'] !== 'undefined') {
|
|
goodTube_miniplayer_video = goodTube_getParams['v'];
|
|
}
|
|
}
|
|
|
|
function goodTube_miniplayer_showHide() {
|
|
// If we have real picture in picture, use that instead!
|
|
if (document.pictureInPictureEnabled) {
|
|
goodTube_pip_showHide();
|
|
return;
|
|
}
|
|
|
|
let goodTube_wrapper = document.querySelector('#goodTube_playerWrapper');
|
|
|
|
if (goodTube_miniplayer) {
|
|
goodTube_wrapper.classList.remove('goodTube_miniplayer');
|
|
goodTube_miniplayer = false;
|
|
|
|
// If we're not viewing a video, clear the player
|
|
if (typeof goodTube_getParams['v'] === 'undefined') {
|
|
goodTube_player_clear(goodTube_player);
|
|
}
|
|
}
|
|
else {
|
|
goodTube_wrapper.classList.add('goodTube_miniplayer');
|
|
goodTube_miniplayer = true;
|
|
goodTube_miniplayer_video = goodTube_getParams['v'];
|
|
}
|
|
}
|
|
|
|
|
|
/* Error display
|
|
------------------------------------------------------------------------------------------ */
|
|
// Show an error on screen (or select next server if we're on automatic mode)
|
|
function goodTube_error_show() {
|
|
// Clear any buffering and loading timeouts
|
|
if (goodTube_bufferingTimeout) {
|
|
clearTimeout(goodTube_bufferingTimeout);
|
|
}
|
|
if (goodTube_bufferCountTimeout) {
|
|
clearTimeout(goodTube_bufferCountTimeout);
|
|
}
|
|
if (goodTube_loadingTimeout) {
|
|
clearTimeout(goodTube_loadingTimeout);
|
|
}
|
|
|
|
// What api are we on?
|
|
let selectedApi = goodTube_helper_getCookie('goodTube_videoServer_withauto');
|
|
|
|
// Are we out of automatic servers?
|
|
let showNoServersError = false;
|
|
if (typeof goodTube_videoServers[goodTube_videoServer_automaticIndex] === 'undefined') {
|
|
showNoServersError = true;
|
|
}
|
|
|
|
// If it's automatic and we're out of servers
|
|
if (selectedApi === 'automatic' && showNoServersError) {
|
|
let player = document.querySelector('#goodTube_player');
|
|
|
|
// Remove the loading state
|
|
goodTube_player_removeLoadingState();
|
|
|
|
// Clear the player
|
|
goodTube_player_clear(goodTube_player);
|
|
|
|
let error = document.createElement('div');
|
|
error.setAttribute('id', 'goodTube_error');
|
|
error.innerHTML = "Video could not be loaded. The servers are not responding :(<br><small>Please refresh the page / try again soon!</small>";
|
|
player.appendChild(error);
|
|
}
|
|
|
|
|
|
// If it's automatic and we have more servers
|
|
else if (selectedApi === 'automatic') {
|
|
// Debug message
|
|
console.log('[GoodTube] Video could not be loaded - selecting next video source...');
|
|
|
|
// Set the player time to be restored when the new server loads
|
|
if (goodTube_player.currentTime > 0) {
|
|
goodTube_player_restoreTime = goodTube_player.currentTime;
|
|
}
|
|
|
|
// Select next server
|
|
goodTube_player_selectVideoServer('automatic', true);
|
|
}
|
|
|
|
|
|
// If it's manual
|
|
else {
|
|
// Debug message
|
|
console.log('[GoodTube] Video could not be loaded - selecting next video source...');
|
|
|
|
// Set the player time to be restored when the new server loads
|
|
if (goodTube_player.currentTime > 0) {
|
|
goodTube_player_restoreTime = goodTube_player.currentTime;
|
|
}
|
|
|
|
// Go to automatic mode
|
|
goodTube_videoServer_automaticIndex = 0;
|
|
goodTube_player_selectVideoServer('automatic', true);
|
|
}
|
|
}
|
|
|
|
// Hide an error on screen
|
|
function goodTube_error_hide() {
|
|
let error = document.querySelector('#goodTube_error');
|
|
if (error) {
|
|
error.remove();
|
|
}
|
|
}
|
|
|
|
|
|
/* Load assets
|
|
------------------------------------------------------------------------------------------ */
|
|
let goodTube_assets = [
|
|
goodTube_github+'/js/assets.min.js',
|
|
goodTube_github+'/css/assets.min.css'
|
|
];
|
|
let goodTube_assets_loaded = 0;
|
|
let goodTube_assets_loadAttempts = 0;
|
|
|
|
// Load assets
|
|
function goodTube_assets_init() {
|
|
// Debug message
|
|
console.log('[GoodTube] Loading player assets...');
|
|
|
|
// Load the first asset, this will then load the others sequentially
|
|
goodTube_assets_loadAttempts = 0;
|
|
goodTube_assets_loadAsset(goodTube_assets[goodTube_assets_loaded]);
|
|
}
|
|
|
|
function goodTube_assets_loadAsset(asset) {
|
|
// Only re-attempt to load the video data max configured retry attempts
|
|
goodTube_assets_loadAttempts++;
|
|
if (goodTube_assets_loadAttempts > goodTube_retryAttempts) {
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Player assets could not be loaded');
|
|
|
|
return;
|
|
}
|
|
|
|
fetch(asset)
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
let asset_element = false;
|
|
|
|
if (asset.indexOf('/js/') !== -1) {
|
|
asset_element = document.createElement('script');
|
|
}
|
|
else if (asset.indexOf('/css/') !== -1) {
|
|
asset_element = document.createElement('style');
|
|
}
|
|
|
|
asset_element.innerHTML = data;
|
|
document.head.appendChild(asset_element);
|
|
|
|
goodTube_assets_loaded++;
|
|
|
|
// If we've loaded all the assets
|
|
if (goodTube_assets_loaded >= goodTube_assets.length) {
|
|
// Debug message
|
|
console.log('[GoodTube] Player assets loaded');
|
|
}
|
|
// Otherwise load the next asset
|
|
else {
|
|
goodTube_assets_loadAttempts = 0;
|
|
goodTube_assets_loadAsset(goodTube_assets[goodTube_assets_loaded]);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if (typeof goodTube_pendingRetry['loadAsset'] !== 'undefined') {
|
|
clearTimeout(goodTube_pendingRetry['loadAsset']);
|
|
}
|
|
|
|
goodTube_pendingRetry['loadAsset'] = setTimeout(function() {
|
|
goodTube_assets_loadAsset(asset);
|
|
}, goodTube_retryDelay);
|
|
});
|
|
}
|
|
|
|
|
|
/* Core functions
|
|
------------------------------------------------------------------------------------------ */
|
|
// This targets our HTML <video> element
|
|
let goodTube_player = false;
|
|
|
|
// Are we on mobile?
|
|
let goodTube_mobile = false;
|
|
|
|
// This holds the GET params
|
|
let goodTube_getParams = false;
|
|
|
|
// Actions
|
|
let goodTube_previousUrl = false;
|
|
let goodTube_previousPlaylist = false;
|
|
function goodTube_actions() {
|
|
// If the assets are loaded AND the player is loaded
|
|
if (goodTube_assets_loaded >= goodTube_assets.length && goodTube_videojs_player_loaded) {
|
|
// Get the previous and current URL
|
|
|
|
// Remove hashes, these mess with things sometimes
|
|
// ALso remove "index="
|
|
let previousUrl = goodTube_previousUrl;
|
|
if (previousUrl) {
|
|
previousUrl = previousUrl.split('#')[0];
|
|
previousUrl = previousUrl.split('index=')[0];
|
|
}
|
|
|
|
let currentUrl = window.location.href;
|
|
if (currentUrl) {
|
|
currentUrl = currentUrl.split('#')[0];
|
|
currentUrl = currentUrl.split('index=')[0];
|
|
}
|
|
|
|
// If the URL hasn't changed, don't do anything (this does not apply to first page load)
|
|
if (previousUrl === currentUrl) {
|
|
return;
|
|
}
|
|
|
|
// The URL has changed, so setup our player
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
// Setup GET parameters
|
|
goodTube_getParams = goodTube_helper_setupGetParams();
|
|
|
|
// If we're viewing a video
|
|
if (typeof goodTube_getParams['v'] !== 'undefined') {
|
|
// Debug message
|
|
console.log('\n-------------------------\n\n');
|
|
|
|
// Setup the previous button history
|
|
goodTube_nav_setupPrevHistory();
|
|
|
|
// Reset the load video attempts
|
|
goodTube_player_loadVideoDataAttempts = 0;
|
|
|
|
// Remove the "restore to" time
|
|
goodTube_player_restoreTime = 0;
|
|
|
|
// Select the server if we're on automatic
|
|
if (goodTube_helper_getCookie('goodTube_videoServer_withauto') === 'automatic') {
|
|
// Get the current playlist (if we're on one)
|
|
let currentPlaylist = false;
|
|
if (typeof goodTube_getParams['list'] !== 'undefined') {
|
|
currentPlaylist = goodTube_getParams['list'];
|
|
}
|
|
|
|
// Reset to first server for automatic, only if we've changed playlist.
|
|
if (!currentPlaylist || goodTube_previousPlaylist !== currentPlaylist) {
|
|
goodTube_videoServer_automaticIndex = 0;
|
|
}
|
|
else if (goodTube_videoServer_automaticIndex > 0) {
|
|
// Otherwise stay on the same server
|
|
goodTube_videoServer_automaticIndex--;
|
|
}
|
|
|
|
// Select the automatic server
|
|
goodTube_player_selectVideoServer('automatic', false);
|
|
}
|
|
|
|
// Debug message
|
|
console.log('[GoodTube] Loading video data from '+goodTube_videoServer_name+'...');
|
|
|
|
// Load the video
|
|
goodTube_video_load(goodTube_player);
|
|
|
|
// Usage stats
|
|
goodTube_stats_video();
|
|
}
|
|
// Otherwise we're not viewing a video, and we're not in the miniplayer or pip
|
|
else if (!goodTube_miniplayer && !goodTube_pip) {
|
|
// Clear the player
|
|
goodTube_player_clear(goodTube_player);
|
|
|
|
// Empty the previous video history
|
|
goodTube_nav_prevVideo = [];
|
|
|
|
// Clear any pending retry attempts
|
|
for (let key in goodTube_pendingRetry) {
|
|
if (goodTube_pendingRetry.hasOwnProperty(key)) {
|
|
clearTimeout(goodTube_pendingRetry[key]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
// Set the previous playlist
|
|
if (typeof goodTube_getParams['list'] !== 'undefined') {
|
|
goodTube_previousPlaylist = goodTube_getParams['list'];
|
|
}
|
|
else {
|
|
goodTube_previousPlaylist = false;
|
|
}
|
|
|
|
// Set the previous URL (which pauses this function until the URL changes again)
|
|
goodTube_previousUrl = window.location.href;
|
|
}
|
|
}
|
|
|
|
// Init
|
|
function goodTube_init() {
|
|
/* General setup
|
|
---------------------------------------------------------------------------------------------------- */
|
|
// Define if we're on mobile or not
|
|
if (window.location.href.indexOf('m.youtube') !== -1) {
|
|
goodTube_mobile = true;
|
|
}
|
|
|
|
// Setup GET parameters
|
|
goodTube_getParams = goodTube_helper_setupGetParams();
|
|
|
|
// Check for custom video servers
|
|
goodTube_server_custom(0);
|
|
|
|
// Check for a local video server
|
|
goodTube_server_local();
|
|
|
|
// If there's a cookie for our previously chosen API, select it
|
|
let goodTube_videoServer_cookie = goodTube_helper_getCookie('goodTube_videoServer_withauto');
|
|
if (goodTube_videoServer_cookie) {
|
|
goodTube_videoServers.forEach((api) => {
|
|
if (api['url'] === goodTube_videoServer_cookie) {
|
|
goodTube_videoServer_type = api['type'];
|
|
goodTube_videoServer_proxy = api['proxy'];
|
|
goodTube_videoServer_url = api['url'];
|
|
goodTube_videoServer_name = api['name'];
|
|
}
|
|
});
|
|
}
|
|
|
|
// Ensure that if they close the window in the middle of downloads, we reset the last download time
|
|
window.addEventListener("beforeunload", (event) => {
|
|
goodTube_helper_setCookie('goodTube_lastDownloadTimeSeconds', (new Date().getTime() / 1000));
|
|
});
|
|
|
|
|
|
/* Disable Youtube
|
|
---------------------------------------------------------------------------------------------------- */
|
|
// Add CSS to hide ads, shorts, etc
|
|
goodTube_youtube_hideAdsShortsEtc();
|
|
|
|
// Add CSS classes to hide elements (without Youtube knowing)
|
|
goodTube_helper_hideElement_init();
|
|
|
|
// Mute, pause and skip ads
|
|
goodTube_youtube_mutePauseSkipAds();
|
|
setInterval(goodTube_youtube_mutePauseSkipAds, 1);
|
|
|
|
// Hide the youtube players
|
|
goodTube_youtube_hidePlayers();
|
|
setInterval(goodTube_youtube_hidePlayers, 1);
|
|
|
|
// Make the youtube player the lowest quality to save on bandwidth
|
|
setInterval(goodTube_youtube_lowestQuality, 1000);
|
|
|
|
// Turn off autoplay
|
|
setInterval(goodTube_youtube_turnOffAutoplay, 1000);
|
|
|
|
// Hide shorts
|
|
setInterval(goodTube_youtube_hideShorts, 100);
|
|
|
|
|
|
/* Load GoodTube
|
|
---------------------------------------------------------------------------------------------------- */
|
|
// Load required assets
|
|
goodTube_assets_init();
|
|
|
|
// Init our player (after DOM is loaded)
|
|
document.addEventListener("DOMContentLoaded", goodTube_player_init);
|
|
|
|
// Also check if the DOM is already loaded, as if it is, the above event listener will not trigger.
|
|
if (document.readyState === "interactive" || document.readyState === "complete") {
|
|
goodTube_player_init();
|
|
}
|
|
|
|
// Usage stats
|
|
goodTube_stats_user();
|
|
}
|
|
|
|
|
|
/* Start GoodTube
|
|
------------------------------------------------------------------------------------------ */
|
|
goodTube_init();
|
|
|
|
|
|
})();
|