diff --git a/goodtube.js b/goodtube.js index b4034cb..8d11cb8 100644 --- a/goodtube.js +++ b/goodtube.js @@ -1,336 +1,25 @@ - /* Player functions +(function() { + 'use strict'; + + + /* 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 = []; - let goodTube_player_restoreTime = 0; - let goodTube_player_assets = [ - goodTube_github+'/js/assets.min.js', - goodTube_github+'/css/assets.min.css' - ]; - let goodTube_player_loadedAssets = 0; - let goodTube_player_loadAssetAttempts = 0; - let goodTube_player_loadVideoDataAttempts = 0; - let goodTube_player_loadChaptersAttempts = 0; - let goodTube_player_vttThumbnailsFunction = false; - let goodTube_player_reloadVideoAttempts = 1; - let goodTube_player_pip = false; - let goodTube_player_miniplayer = false; - let goodTube_player_miniplayer_video = false; - let goodTube_player_highestQuality = false; - let goodTube_player_selectedQuality = false; - let goodTube_player_manuallySelectedQuality = false; - let goodTube_player_storyboardLoaded = false; - let goodTube_updateChapters = false; - let goodTube_chapterTitleInterval = false; - let goodTube_chaptersChangeInterval = false; - let goodTube_updateManifestQualityTimeout = false; - - // Play / pause - function goodTube_player_videojs_playPause() { - let playPauseButton = document.querySelector('.vjs-play-control'); - - if (playPauseButton.classList.contains('vjs-playing')) { - goodTube_player_play(goodTube_player); - } - else { - goodTube_player_pause(goodTube_player); - } - } - - // Update the video js player - function goodTube_player_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; - }); - - }); - } - } - } - - // Show or hide the next and previous button - function goodTube_player_videojs_showHideNextPrevButtons() { - goodTube_videojs_prevButton = false; - goodTube_videojs_nextButton = true; - - // Don't show next / prev in the miniplayer / pip unless we're viewing a video - if ((goodTube_player_miniplayer || goodTube_player_pip) && typeof goodTube_getParams['v'] === 'undefined') { - goodTube_videojs_prevButton = false; - goodTube_videojs_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_videojs_prevButton = true; - } - } - // Otherwise we're not in a playlist, so if a previous video exists - else if (goodTube_videojs_previousVideo[goodTube_videojs_previousVideo.length - 2] && goodTube_videojs_previousVideo[goodTube_videojs_previousVideo.length - 2] !== window.location.href) { - // Enable the previous button - goodTube_videojs_prevButton = true; - } - } - - // Show or hide the previous button - let prevButton = document.querySelector('.vjs-prev-button'); - if (prevButton) { - if (!goodTube_videojs_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_videojs_nextButton) { - goodTube_helper_hideElement(nextButton); - } - else { - goodTube_helper_showElement(nextButton); - } - } - } - - // Position the timestamp (mobile only) - function goodTube_player_videojs_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 JS functions + /* Video servers ------------------------------------------------------------------------------------------ */ - let goodTube_videojs_player = false; - let goodTube_videojs_player_loaded = false; - let goodTube_videojs_previousVideo = []; - let goodTube_videojs_prevButton = false; - let goodTube_videojs_nextButton = true; - 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; - - - - - /* GoodTube general functions - ------------------------------------------------------------------------------------------ */ - let goodTube_previousUrl = false; - let goodTube_previousPlaylist = false; - let goodTube_player = false; - let goodTube_getParams = false; - let goodTube_downloadTimeouts = []; - let goodTube_pendingDownloads = []; - let goodTube_mobile = false; - let goodTube_clickedPlaylistOpen = false; - - // API subtitle / storyboard servers - let goodTube_otherDataServersIndex_subtitles = 0; - let goodTube_otherDataServersIndex_storyboard = 0; - let goodTube_otherDataServers = [ - 'https://yt.artemislena.eu', - 'https://invidious.perennialte.ch', - '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 = [ - '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 fail; 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 download servers - goodTube_downloadServers = goodTube_downloadServers.concat(goodTube_downloadServers_community); - - // API Endpoints - let goodTube_apis = [ + let goodTube_videoServers = [ // AUTOMATIC OPTION // -------------------------------------------------------------------------------- { @@ -588,7 +277,6371 @@ ]; // Set the starting server to automatic mode - let goodTube_api_type = goodTube_apis[0]['type']; - let goodTube_api_proxy = goodTube_apis[0]['proxy']; - let goodTube_api_url = goodTube_apis[0]['url']; - let goodTube_api_name = goodTube_apis[0]['name']; + 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; + } + `; + + 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); + } + } + + + /* Youtube functions + ------------------------------------------------------------------------------------------ */ + // Hide ads, shorts, etc - init + function goodTube_youtube_hideAdsShortsEtc() { + let style = document.createElement('style'); + style.textContent = ` + 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), + 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, + + ytm-rich-shelf-renderer, + ytm-shelf-renderer, + ytm-button-renderer.icon-avatar_logged_out, + ytm-companion-slot, + ytm-shelf-renderer, + 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 = "
"; + + 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, or we're not viewing a video + if (event.ctrlKey || typeof goodTube_getParams['v'] === 'undefined') { + return; + } + + // 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' + ) + ) { + let keyPressed = event.key.toLowerCase(); + + // 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 === ' ') { + 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 = ''; + } + } + } + + // Hide the player + function goodTube_player_hide(player) { + goodTube_helper_hideElement(player.closest('#goodTube_playerWrapper')); + } + + // Show the player + function goodTube_player_show(player) { + goodTube_helper_showElement(player.closest('#goodTube_playerWrapper')); + } + + // 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; + } + + /* 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