Major updates

This commit is contained in:
goodtube4u
2025-06-12 20:42:05 +10:00
parent 20458aea07
commit b29b9b73b3
28 changed files with 2094 additions and 11643 deletions

2
.gitignore vendored
View File

@@ -1 +1 @@
wip/
old/

346
README.md
View File

@@ -1,346 +0,0 @@
# GoodTube
Hello and welcome! I'm glad you're here.
What is GoodTube you ask? It's a little plugin for Youtube (that just works) and;
- REMOVES 100% OF PAGE AND VIDEO ADS.
- Allows background play (so you can turn your phone screen off and keep listening).
- Allows you to download the audio / video / entire playlist in up to 8K quality with the click of a button (at the bottom of the player).
- Works on both desktop and mobile (iOS and Android).
- Works in all major browsers (Chrome, Firefox, Opera, etc).
- Proxies in Youtube videos from different servers (in up to 8k quality).
- Removes shorts.
- Removes unwanted search results ("You might also like this", "Other people also watched", etc).
- Removes thumbnails for other recommended videos that pop up when a video finishes (I really hate these, try without it's nice).
- Keeps you up to date with the latest version automatically.
And it keeps the good stuff like;
- The beloved algorithm / watch history.
- Keyboard shortcuts.
- Subtitles.
- Chapters.
- Autoplay.
- Playlists.
- Picture in picture / the miniplayer.
- Theater mode.
- Live streams.
It's easy to install too;
- [How to install on desktop](#how-to-install-on-desktop)
- [How to install on Android (mobile)](#how-to-install-on-android-mobile)
- [How to install on iOS / iPhone (mobile)](#how-to-install-on-ios--iphone-mobile)
Here's some screenshots;
| ![enter image description here](https://raw.githubusercontent.com/goodtube4u/goodtube/main/installation/demo-desktop.png)| ![enter image description here](https://raw.githubusercontent.com/goodtube4u/goodtube/main/installation/demo-mobile.png)|
|--|--|
## How to install on Desktop
1. Disable your other adblockers! You can do this for Youtube only.
2. Install this browser extension "Tampermonkey":
https://www.tampermonkey.net/
3. Once that's done, simply click on this link and press "Install":
https://github.com/goodtube4u/goodtube/raw/main/goodtube.user.js
4. If you're using CHROME
You need to turn on developer mode for this extension. Check out the screenshots below for instructions.
![enter image description here](https://raw.githubusercontent.com/goodtube4u/goodtube/main/installation/chrome.png)
5. If you're using FIREFOX
You need to enable autoplay. Check out the screenshot below for instructions.
![enter image description here](https://raw.githubusercontent.com/goodtube4u/goodtube/main/installation/firefox.png)
That's it. You're good to go. Open up Youtube and happy days.
## How to install on Android (mobile)
This will take you a few minutes, because mobiles are generally sort of annoying...
Just follow the steps below and I promise it'll work for you! :)
1. First off you'll need to install this app called "Firefox Nightly". You can find it on Google Play.
(Basically it's an official release of Firefox, but it also allows you to install browser extensions.)
**Pro tip - I've recently discovered that you can also install extensions on normal Firefox, so you may not need to download Firefox Nightly! Other than that, the steps are the same.**
2. Once it's installed, open Firefox Nightly.
Now click the 3 dots down the bottom right, and go to "Settings":
![enter image description here](https://raw.githubusercontent.com/goodtube4u/goodtube/main/installation/android-1.png)
3. Scroll down and go to "Extensions":
![enter image description here](https://raw.githubusercontent.com/goodtube4u/goodtube/main/installation/android-2.png)
4. Add the extension "Tampermonkey":
![enter image description here](https://raw.githubusercontent.com/goodtube4u/goodtube/main/installation/android-3.png)
5. Now go back to the main screen of Firefox Nightly (just hit back back back until you're there).
6. Then go to the following webpage:
https://github.com/goodtube4u/goodtube/raw/main/goodtube.user.js
7. Click "Install":
![enter image description here](https://raw.githubusercontent.com/goodtube4u/goodtube/main/installation/android-4.png)
**That's it. You're good to go. Open up Youtube in Firefox Nightly and happy days!!**
I recommend using Firefox Nightly just like you would the Youtube app. Put it on your home screen somewhere / make Youtube the homepage for an even smoother experience :)
*Please note: If you see a mostly blank screen on the Youtube homepage, don't worry! This is normal. Just search for something.*
*Simply sign into Youtube and the homepage will be full of your favorite videos once again.*
## How to install on iOS / iPhone (mobile)
This will take you a few minutes, because mobiles are generally sort of annoying...
Just follow the steps below and I promise it'll work for you! :)
1. First off you'll need to install this app called "Orion Browser". You can find it on the App Store. (Basically it's an alternative browser that allows you to install browser extensions.)
2. Once it's installed, open Orion Browser.
3. Go to Extensions.
4. Install an extension called "Violentmonkey" (do NOT try and use Tampermonkey instead, it doesn't work for iOS).
5. Now go to the following webpage: https://github.com/goodtube4u/goodtube/raw/main/goodtube.user.js
6. Click "Install" on the page the pops up.
**That's it. You're good to go. Open up Youtube in Orion Browser and happy days!!**
I recommend using Orion Browser just like you would the Youtube app. Put it on your home screen somewhere / make Youtube the homepage for an even smoother experience :)
*Please note: If you see a mostly blank screen on the Youtube homepage, don't worry! This is normal. Just search for something.*
*Simply sign into Youtube and the homepage will be full of your favorite videos once again.*
## Why?
Well lately I've been raging as Youtube have successfully stopped most adblockers from working (or they only work sometimes). You've probably been experiencing this same nightmare on and off.
I'm a programmer by trade so decided to try and fix this for everyone.
I'll never pay a company that is slapping ads on one of the largest archives of music / film / art in the world and blackmailing me to remove them. They really have a social responsibility that isn't being taken seriously...
So anyway, screw em. Install this little plugin and enjoy no ads 🎉
This took around 5 weeks to create and a thousand black coffees. I do hope you enjoy it.
Any questions, you can contact me at: goodtube4u@hotmail.com
I'm dedicated to helping every single user get this working, so really - if you have any problems at all hit me up!
## Attributions
Many thanks to https://cobalt.tools for providing the amazing API we're using to download stuff!
You can also use this website to download from other platforms like SoundCloud, give it a try :)
## (Optional) Host your own local video server - to make this load videos FAST!
- This is for advanced users only.
- This is not for phones. You can only do this on a desktop computer (Windows, Mac and Linux are all supported).
- Doing this will significantly speed up GoodTube! You should get normal Youtube speed or very close to.
**Here's how you do it:**
1. Install Docker Desktop (https://www.docker.com/products/docker-desktop/)
2. Install Git (https://git-scm.com/downloads)
3. Open Terminal / Command Prompt and enter the following commands
```
cd c:/
git clone https://github.com/iv-org/invidious.git
```
4. Edit the following file in a text editor like Notepad:
`c:/invidious/docker-compose.yml`
5. Delete all the code in there and replace it with:
```
version: "3"
services:
invidious:
image: quay.io/invidious/invidious:latest
# image: quay.io/invidious/invidious:latest-arm64 # ARM64/AArch64 devices
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
# Please read the following file for a comprehensive list of all available
# configuration options and their associated syntax:
# https://github.com/iv-org/invidious/blob/master/config/config.example.yml
INVIDIOUS_CONFIG: |
db:
dbname: invidious
user: kemal
password: kemal
host: invidious-db
port: 5432
check_tables: true
# external_port:
# domain:
# https_only: false
# statistics_enabled: false
hmac_key: "goodtube4u"
healthcheck:
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1
interval: 30s
timeout: 5s
retries: 2
logging:
options:
max-size: "1G"
max-file: "4"
depends_on:
- invidious-db
invidious-db:
image: docker.io/library/postgres:14
restart: unless-stopped
volumes:
- postgresdata:/var/lib/postgresql/data
- ./config/sql:/config/sql
- ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh
environment:
POSTGRES_DB: invidious
POSTGRES_USER: kemal
POSTGRES_PASSWORD: kemal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
volumes:
postgresdata:
```
6. Open Terminal / Command Prompt and enter the following commands
```
cd c:/invidious
docker-compose up
```
7. Let it run for a few minutes, you'll see a bunch of text appearing while it installs everything. Eventually it will stop outputting text and you can close Terminal / Command Prompt.
8. Open Docker Desktop.
9. On the "Containers" tab you will see a thing called "Invidious". Press the play arrow to start it.
10. Check it's working by going to http://127.0.0.1:3000 in your browser. If you see an instance of Invidious you're good to go!
11. Now that's done, go to https://www.youtube.com/?goodtube_local=true in your browser to enable the local server. You only need to do this once, the setting will be remembered.
12. Now you can visit Youtube like normal, but at the top of the "Video Source" list you will see a server called "LOCAL". This is your local video server, recommended you select the "Automatic" mode from the video servers list. This will try your local server first, and if that fails, fallback to using the normal servers.
13. As long as you've got your Invidious instance running in Docker Desktop, then this local video server will work and should be SUPER FAST. Enjoy :)
Note: If you want to turn this off, go to https://www.youtube.com/?goodtube_local=false in your browser. You only need to do this once, the setting will be remembered.
## (Optional) Add custom video servers
- This is for advanced users only.
- It allows you to add your own video servers to the list.
- If you're hosting your own video server as per the above instructions, but need to change the web address or settings, this provides an easy way to do that.
**To add a custom video server**
Visit `https://www.youtube.com` with the following GET params:
```
goodtube_customserver_0_name=My custom server
goodtube_customserver_0_type=2
goodtube_customserver_0_proxy=true
goodtube_customserver_0_url=https://mycustomserver.com
```
For example:
`https://www.youtube.com?goodtube_customserver_0_name=My custom server&goodtube_customserver_0_type=2&goodtube_customserver_0_proxy=true&goodtube_customserver_0_url=https://myawesomeserver.com`
You can do this for up to 10 servers. Just change the `0` to `1`, `2`, `3` and so on. Example:
`goodtube_customserver_0_name` is the first server
`goodtube_customserver_1_name` is the second server
You only need to do this once. It will remember the setting until you disable it.
**To remove a custom video server**
Visit `https://www.youtube.com` with the following GET param:
`https://www.youtube.com?goodtube_customserver_0=false`
**Server options**
Name (goodtube_customserver_XXX_name):
```
The name of the server
```
Type (goodtube_customserver_XXX_type):
```
1 = Invidious, 360p only
2 = Invidious, DASH stream - all qualities
3 = Piped - all qualities
```
Proxy (goodtube_customserver_XXX_proxy):
```
true = Proxy all video traffic through the server (recommended)
false = Do NOT proxy traffic through the server (not recommended)
```
URL (goodtube_customserver_XXX_url):
```
The web address of the server. Make sure this does not have a trailing slash. For example:
https://myawesomeserver.com
```
**PLEASE NOTE**:
- Custom video servers MUST be served over `https`. They will not work if they are served over `http`.
- You can use `http` if the custom server web address is local, for example: `http://127.0.0.1:3000`

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1
css/assets.min.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
/**
* pthumbnails
* @version 1.2.0
* @copyright 2020 Rigel Kent <sendmemail@rigelk.eu>
* @license MIT
*/
.video-js.vjs-vtt-thumbnails{display:block}.video-js .vjs-vtt-thumbnail-display{opacity:0;position:absolute;transition:opacity .5s;bottom:85%;pointer-events:none;box-shadow:0 0 7px rgba(0,0,0,0.6)}.video-js .vjs-vtt-thumbnail-time{position:absolute;bottom:0;transform:translateX(-50%);background:rgba(0,0,0,0.6);padding:0.5em;border-top-left-radius:10%;border-top-right-radius:10%}

File diff suppressed because it is too large Load Diff

1
goodtube.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,21 +1,23 @@
// ==UserScript==
// @name GoodTube
// @namespace http://tampermonkey.net/
// @version 5.010
// @description Loads Youtube videos from different sources. Also removes ads, shorts, etc.
// @version 2.000
// @description Removes 100% of Youtube ads.
// @author GoodTube
// @match https://*.youtube.com/*
// @updateURL https://github.com/goodtube4u/goodtube/raw/refs/heads/main/beta/goodtube-embed.user.js
// @downloadURL https://github.com/goodtube4u/goodtube/raw/refs/heads/main/beta/goodtube-embed.user.js
// @match *://m.youtube.com/*
// @match *://www.youtube.com/*
// @match *://youtube.com/*
// @match *://*.wikipedia.org/*
// @icon https://cdn-icons-png.flaticon.com/256/1384/1384060.png
// @run-at document-start
// @updateURL https://github.com/goodtube4u/goodtube/raw/main/goodtube.user.js
// @downloadURL https://github.com/goodtube4u/goodtube/raw/main/goodtube.user.js
// @noframes
// ==/UserScript==
// This now automatically loads the latest version. This means that you will never need to update manually again :)
// To view the full source code go here: https://github.com/goodtube4u/goodtube/blob/main/goodtube.js
(function() {
(function () {
'use strict';
// Bypass CSP restrictions, introduced by the latest Chrome updates
@@ -51,22 +53,22 @@
loadAttempts++;
// Load GoodTube
fetch('https://raw.githubusercontent.com/goodtube4u/GoodTube/main/goodtube.min.js?i='+Date.now())
// Success
.then(response => response.text())
.then(data => {
// Put GoodTube code into a <script> tag
let element = document.createElement('script');
element.innerHTML = data;
document.head.appendChild(element);
})
// Error
.catch((error) => {
// Try again after 500ms
setTimeout(function() {
goodTube_load(loadAttempts);
}, 500);
});
fetch('https://raw.githubusercontent.com/goodtube4u/GoodTube/main/goodtube.js?i=' + Date.now())
// Success
.then(response => response.text())
.then(data => {
// Put GoodTube code into a <script> tag
let element = document.createElement('script');
element.innerHTML = data;
document.head.appendChild(element);
})
// Error
.catch((error) => {
// Try again after 500ms
setTimeout(function () {
goodTube_load(loadAttempts);
}, 500);
});
}
// Load GoodTube

74
goodtube.user.min.js vendored
View File

@@ -1,74 +0,0 @@
// ==UserScript==
// @name GoodTube
// @namespace http://tampermonkey.net/
// @version 5.010
// @description Loads Youtube videos from different sources. Also removes ads, shorts, etc.
// @author GoodTube
// @match https://*.youtube.com/*
// @icon https://cdn-icons-png.flaticon.com/256/1384/1384060.png
// @run-at document-start
// @updateURL https://github.com/goodtube4u/goodtube/raw/main/goodtube.user.js
// @downloadURL https://github.com/goodtube4u/goodtube/raw/main/goodtube.user.js
// @noframes
// ==/UserScript==
// This now automatically loads the latest version. This means that you will never need to update manually again :)
// To view the full source code go here: https://github.com/goodtube4u/goodtube/blob/main/goodtube.js
(function() {
'use strict';
// Bypass CSP restrictions, introduced by the latest Chrome updates
if (window.trustedTypes && window.trustedTypes.createPolicy && !window.trustedTypes.defaultPolicy) {
window.trustedTypes.createPolicy('default', {
createHTML: string => string,
createScriptURL: string => string,
createScript: string => string
});
}
// Define load function
function goodTube_load(loadAttempts) {
// If it's the first load attempt
if (loadAttempts === 0) {
// Debug message
console.log('\n==================================================\n ______ ________ __\n / ____/___ ____ ____/ /_ __/_ __/ /_ ___\n / / __/ __ \\/ __ \\/ __ / / / / / / / __ \\/ _ \\\n / /_/ / /_/ / /_/ / /_/ / / / / /_/ / /_/ / __/\n \\____/\\____/\\____/\\____/ /_/ \\____/_____/\\___/\n\n==================================================');
console.log('[GoodTube] Initiating...');
}
// Only re-attempt to load the GoodTube 10 times
if (loadAttempts >= 9) {
// Debug message
console.log('[GoodTube] GoodTube could not be loaded');
// Show prompt
alert('GoodTube could not be loaded! Please refresh the page to try again.');
return;
}
// Increment the load attempts
loadAttempts++;
// Load GoodTube
fetch('https://raw.githubusercontent.com/goodtube4u/GoodTube/main/goodtube.min.js?i='+Date.now())
// Success
.then(response => response.text())
.then(data => {
// Put GoodTube code into a <script> tag
let element = document.createElement('script');
element.innerHTML = data;
document.head.appendChild(element);
})
// Error
.catch((error) => {
// Try again after 500ms
setTimeout(function() {
goodTube_load(loadAttempts);
}, 500);
});
}
// Load GoodTube
goodTube_load(0);
})();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 894 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

8
js/assets.min.js vendored

File diff suppressed because one or more lines are too long

40
js/video.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,312 +0,0 @@
/*! @name videojs-hls-quality-selector @version 2.0.0 @license MIT */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('video.js')) :
typeof define === 'function' && define.amd ? define(['video.js'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojsHlsQualitySelector = factory(global.videojs));
}(this, (function (videojs) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs);
var version = "2.0.0";
const MenuButton = videojs__default['default'].getComponent('MenuButton');
const Menu = videojs__default['default'].getComponent('Menu');
const Component = videojs__default['default'].getComponent('Component');
const Dom = videojs__default['default'].dom;
/**
* Convert string to title case.
*
* @param {string} string - the string to convert
* @return {string} the returned titlecase string
*/
function toTitleCase(string) {
if (typeof string !== 'string') {
return string;
}
return string.charAt(0).toUpperCase() + string.slice(1);
}
/**
* Extend vjs button class for quality button.
*/
class ConcreteButton extends MenuButton {
/**
* Button constructor.
*
* @param {Player} player - videojs player instance
*/
constructor(player) {
super(player, {
title: player.localize('Quality'),
name: 'QualityButton'
});
}
/**
* Creates button items.
*
* @return {Array} - Button items
*/
createItems() {
return [];
}
/**
* Create the menu and add all items to it.
*
* @return {Menu}
* The constructed menu
*/
createMenu() {
const menu = new Menu(this.player_, {
menuButton: this
});
this.hideThreshold_ = 0;
this.items = this.createItems();
if (this.items) {
// Add menu items to the menu
for (let i = 0; i < this.items.length; i++) {
menu.addItem(this.items[i]);
}
}
return menu;
}
}
// Concrete classes
const VideoJsMenuItemClass = videojs__default['default'].getComponent('MenuItem');
/**
* Extend vjs menu item class.
*/
class ConcreteMenuItem extends VideoJsMenuItemClass {
/**
* Menu item constructor.
*
* @param {Player} player - vjs player
* @param {Object} item - Item object
* @param {ConcreteButton} qualityButton - The containing button.
* @param {HlsQualitySelector} plugin - This plugin instance.
*/
constructor(player, item, qualityButton, plugin) {
super(player, {
label: item.label,
selectable: true,
selected: item.selected || false
});
this.item = item;
this.qualityButton = qualityButton;
this.plugin = plugin;
}
/**
* Click event for menu item.
*/
handleClick() {
// Reset other menu items selected status.
for (let i = 0; i < this.qualityButton.items.length; ++i) {
this.qualityButton.items[i].selected(false);
}
// Set this menu item to selected, and set quality.
this.plugin.setQuality(this.item.value);
this.selected(true);
}
}
const Plugin = videojs__default['default'].getPlugin('plugin');
// Default options for the plugin.
const defaults = {};
/**
* An advanced Video.js plugin. For more information on the API
*
* See: https://blog.videojs.com/feature-spotlight-advanced-plugins/
*/
class HlsQualitySelector extends Plugin {
/**
* Create a HlsQualitySelector plugin instance.
*
* @param {Player} player
* A Video.js Player instance.
*
* @param {Object} [options]
* An optional options object.
*
* While not a core part of the Video.js plugin architecture, a
* second argument of options is a convenient way to accept inputs
* from your plugin's caller.
*/
constructor(player, options) {
// the parent class will add player under this.player
super(player);
this.options = videojs__default['default'].obj.merge(defaults, options);
this.player.ready(() => {
// If there is quality levels plugin and the HLS tech exists then continue.
if (this.player.qualityLevels) {
this.player.addClass('vjs-hls-quality-selector');
// Create the quality button.
this.createQualityButton();
this.bindPlayerEvents();
}
});
}
/**
* Binds listener for quality level changes.
*/
bindPlayerEvents() {
this.player.qualityLevels().on('addqualitylevel', this.onAddQualityLevel.bind(this));
}
/**
* Adds the quality menu button to the player control bar.
*/
createQualityButton() {
const player = this.player;
this._qualityButton = new ConcreteButton(player);
const placementIndex = player.controlBar.children().length - 2;
const concreteButtonInstance = player.controlBar.addChild(this._qualityButton, {
componentClass: 'qualitySelector'
}, this.options.placementIndex || placementIndex);
concreteButtonInstance.addClass('vjs-quality-selector');
if (!this.options.displayCurrentQuality) {
const icon = ` ${this.options.vjsIconClass || 'vjs-icon-hd'}`;
concreteButtonInstance.menuButton_.$('.vjs-icon-placeholder').className += icon;
} else {
this.setButtonInnerText(player.localize('Auto'));
}
concreteButtonInstance.removeClass('vjs-hidden');
}
/**
*Set inner button text.
*
* @param {string} text - the text to display in the button.
*/
setButtonInnerText(text) {
this._qualityButton.menuButton_.$('.vjs-icon-placeholder').innerHTML = text;
}
/**
* Builds individual quality menu items.
*
* @param {Object} item - Individual quality menu item.
* @return {ConcreteMenuItem} - Menu item
*/
getQualityMenuItem(item) {
const player = this.player;
return new ConcreteMenuItem(player, item, this._qualityButton, this);
}
/**
* Executed when a quality level is added from HLS playlist.
*/
onAddQualityLevel() {
const player = this.player;
const qualityList = player.qualityLevels();
const levels = qualityList.levels_ || [];
const levelItems = [];
for (let i = 0; i < levels.length; ++i) {
const {
width,
height
} = levels[i];
const pixels = width > height ? height : width;
if (!pixels) {
continue;
}
if (!levelItems.filter(_existingItem => {
return _existingItem.item && _existingItem.item.value === pixels;
}).length) {
const levelItem = this.getQualityMenuItem.call(this, {
label: pixels + 'p',
value: pixels
});
levelItems.push(levelItem);
}
}
levelItems.sort((current, next) => {
if (typeof current !== 'object' || typeof next !== 'object') {
return -1;
}
if (current.item.value < next.item.value) {
return 1;
}
if (current.item.value > next.item.value) {
return -1;
}
return 0;
});
levelItems.push(this.getQualityMenuItem.call(this, {
label: this.player.localize('Auto'),
value: 'auto',
selected: true
}));
if (this._qualityButton) {
this._qualityButton.createItems = () => {
return levelItems;
};
this._qualityButton.update();
}
}
/**
* Sets quality (based on media short side)
*
* @param {number} quality - A number representing HLS playlist.
*/
setQuality(quality) {
const qualityList = this.player.qualityLevels();
// Set quality on plugin
this._currentQuality = quality;
if (this.options.displayCurrentQuality) {
this.setButtonInnerText(quality === 'auto' ? this.player.localize('Auto') : `${quality}p`);
}
for (let i = 0; i < qualityList.length; ++i) {
const {
width,
height
} = qualityList[i];
const pixels = width > height ? height : width;
qualityList[i].enabled = pixels === quality || quality === 'auto';
}
this._qualityButton.unpressButton();
// Select the correct quality menu item on screen
for (let i = 0; i < this._qualityButton.items.length; ++i) {
if (this._qualityButton.items[i]['item']['value'] == this._currentQuality) {
this._qualityButton.items[i].selected(true);
}
else {
this._qualityButton.items[i].selected(false);
}
}
}
/**
* Return the current set quality or 'auto'
*
* @return {string} the currently set quality
*/
getCurrentQuality() {
return this._currentQuality || 'auto';
}
}
// Include the version number.
HlsQualitySelector.VERSION = version;
// Register the plugin with video.js.
videojs__default['default'].registerPlugin('hlsQualitySelector', HlsQualitySelector);
return HlsQualitySelector;
})));

File diff suppressed because one or more lines are too long

View File

@@ -1,603 +0,0 @@
/**
* pthumbnails
* @version 1.2.0
* @copyright 2020 Rigel Kent <sendmemail@rigelk.eu>
* @license MIT
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('video.js')) :
typeof define === 'function' && define.amd ? define(['video.js'], factory) :
(global.videojsVttThumbnails = factory(global.videojs));
}(this, (function (videojs) { 'use strict';
videojs = videojs && videojs.hasOwnProperty('default') ? videojs['default'] : videojs;
var version = "1.2.0";
var asyncGenerator = function () {
function AwaitValue(value) {
this.value = value;
}
function AsyncGenerator(gen) {
var front, back;
function send(key, arg) {
return new Promise(function (resolve, reject) {
var request = {
key: key,
arg: arg,
resolve: resolve,
reject: reject,
next: null
};
if (back) {
back = back.next = request;
} else {
front = back = request;
resume(key, arg);
}
});
}
function resume(key, arg) {
try {
var result = gen[key](arg);
var value = result.value;
if (value instanceof AwaitValue) {
Promise.resolve(value.value).then(function (arg) {
resume("next", arg);
}, function (arg) {
resume("throw", arg);
});
} else {
settle(result.done ? "return" : "normal", result.value);
}
} catch (err) {
settle("throw", err);
}
}
function settle(type, value) {
switch (type) {
case "return":
front.resolve({
value: value,
done: true
});
break;
case "throw":
front.reject(value);
break;
default:
front.resolve({
value: value,
done: false
});
break;
}
front = front.next;
if (front) {
resume(front.key, front.arg);
} else {
back = null;
}
}
this._invoke = send;
if (typeof gen.return !== "function") {
this.return = undefined;
}
}
if (typeof Symbol === "function" && Symbol.asyncIterator) {
AsyncGenerator.prototype[Symbol.asyncIterator] = function () {
return this;
};
}
AsyncGenerator.prototype.next = function (arg) {
return this._invoke("next", arg);
};
AsyncGenerator.prototype.throw = function (arg) {
return this._invoke("throw", arg);
};
AsyncGenerator.prototype.return = function (arg) {
return this._invoke("return", arg);
};
return {
wrap: function (fn) {
return function () {
return new AsyncGenerator(fn.apply(this, arguments));
};
},
await: function (value) {
return new AwaitValue(value);
}
};
}();
var classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
// import request from 'request';
// Default options for the plugin.
var defaults = {};
// Cache for image elements
var cache = {};
// Cross-compatibility for Video.js 5 and 6.
var registerPlugin = videojs.registerPlugin || videojs.plugin;
// const dom = videojs.dom || videojs;
/**
* Function to invoke when the player is ready.
*
* This is a great place for your plugin to initialize itself. When this
* function is called, the player will have its DOM and child components
* in place.
*
* @function onPlayerReady
* @param {Player} player
* A Video.js player object.
*
* @param {Object} [options={}]
* A plain object containing options for the plugin.
*/
var onPlayerReady = function onPlayerReady(player, options) {
player.addClass('vjs-vtt-thumbnails');
player.vttThumbnails = new vttThumbnailsPlugin(player, options);
};
/**
* A video.js plugin.
*
* In the plugin function, the value of `this` is a video.js `Player`
* instance. You cannot rely on the player being in a "ready" state here,
* depending on how the plugin is invoked. This may or may not be important
* to you; if not, remove the wait for "ready"!
*
* @function vttThumbnails
* @param {Object} [options={}]
* An object of options left to the plugin author to define.
*/
var vttThumbnails = function vttThumbnails(options) {
var _this = this;
this.ready(function () {
onPlayerReady(_this, videojs.mergeOptions(defaults, options));
});
};
/**
* VTT Thumbnails class.
*
* This class performs all functions related to displaying the vtt
* thumbnails.
*/
var vttThumbnailsPlugin = function () {
/**
* Plugin class constructor, called by videojs on
* ready event.
*
* @function constructor
* @param {Player} player
* A Video.js player object.
*
* @param {Object} [options={}]
* A plain object containing options for the plugin.
* - src: path to the .vtt file
* - baseUrl (optional): host prepended to the image definitions
* - preloadStrategy (optional): preload images in the cache, for smooth scrubbing (default: none, available: 'all')
*/
function vttThumbnailsPlugin(player, options) {
classCallCheck(this, vttThumbnailsPlugin);
this.player = player;
this.options = options;
this.listenForDurationChange();
this.initializeThumbnails();
this.registeredEvents = {};
return this;
}
vttThumbnailsPlugin.prototype.src = function src(source) {
this.resetPlugin();
this.options.src = source;
this.initializeThumbnails();
};
vttThumbnailsPlugin.prototype.detach = function detach() {
this.resetPlugin();
};
vttThumbnailsPlugin.prototype.resetPlugin = function resetPlugin() {
this.thumbnailHolder && this.thumbnailHolder.parentNode.removeChild(this.thumbnailHolder);
this.progressBar && this.progressBar.removeEventListener('mouseenter', this.registeredEvents.progressBarMouseEnter);
this.progressBar && this.progressBar.removeEventListener('mouseleave', this.registeredEvents.progressBarMouseLeave);
this.progressBar && this.progressBar.removeEventListener('mousemove', this.registeredEvents.progressBarMouseMove);
delete this.registeredEvents.progressBarMouseEnter;
delete this.registeredEvents.progressBarMouseLeave;
delete this.registeredEvents.progressBarMouseMove;
delete this.progressBar;
delete this.vttData;
delete this.thumbnailHolder;
delete this.timeHolder;
delete this.lastStyle;
};
vttThumbnailsPlugin.prototype.listenForDurationChange = function listenForDurationChange() {
this.player.on('durationchange', function () {});
};
/**
* Bootstrap the plugin.
*/
vttThumbnailsPlugin.prototype.initializeThumbnails = function initializeThumbnails() {
var _this2 = this;
if (!this.options.src) {
return;
}
var baseUrl = this.getBaseUrl();
var url = this.getFullyQualifiedUrl(this.options.src, baseUrl);
this.getVttFile(url).then(function (data) {
_this2.vttData = _this2.processVtt(data);
_this2.setupThumbnailElement();
if (_this2.options.hasOwnProperty('preloadStrategy')) {
_this2.preload(_this2.vttData);
}
});
};
/**
* Builds a base URL should we require one.
*
* @returns {string}
*/
vttThumbnailsPlugin.prototype.getBaseUrl = function getBaseUrl() {
return [window.location.protocol, '//', window.location.hostname, window.location.port ? ':' + window.location.port : '', window.location.pathname].join('').split(/([^\/]*)$/gi).shift();
};
/**
* Grabs the contents of the VTT file.
*
* @param url
* @returns {Promise}
*/
vttThumbnailsPlugin.prototype.getVttFile = function getVttFile(url) {
var _this3 = this;
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.data = {
resolve: resolve
};
req.addEventListener('load', _this3.vttFileLoaded);
req.open('GET', url);
req.send();
});
};
/**
* Callback for loaded VTT file.
*/
vttThumbnailsPlugin.prototype.vttFileLoaded = function vttFileLoaded() {
this.data.resolve(this.responseText);
};
/**
* This will fill the cache and thus preload images
*/
vttThumbnailsPlugin.prototype.preload = function preload(data) {
var _this4 = this;
data.forEach(function (item) {
return _this4.setImageInCacheForItem(item);
});
};
vttThumbnailsPlugin.prototype.setupThumbnailElement = function setupThumbnailElement(data) {
var _this5 = this;
var mouseDisplay = this.player.$('.vjs-mouse-display');
this.progressBar = this.player.$('.vjs-progress-control');
// thumbnail element
var thumbHolder = document.createElement('div');
thumbHolder.setAttribute('class', 'vjs-vtt-thumbnail-display');
this.progressBar.appendChild(thumbHolder);
this.thumbnailHolder = thumbHolder;
// time element
var timeHolder = document.createElement('time');
timeHolder.setAttribute('class', 'vjs-vtt-thumbnail-time');
this.thumbnailHolder.appendChild(timeHolder);
this.timeHolder = timeHolder;
if (mouseDisplay) {
mouseDisplay.classList.add('vjs-hidden');
}
this.registeredEvents.progressBarMouseEnter = function () {
return _this5.onBarMouseenter();
};
this.registeredEvents.progressBarMouseLeave = function () {
return _this5.onBarMouseleave();
};
this.progressBar.addEventListener('mouseenter', this.registeredEvents.progressBarMouseEnter);
this.progressBar.addEventListener('mouseleave', this.registeredEvents.progressBarMouseLeave);
};
vttThumbnailsPlugin.prototype.onBarMouseenter = function onBarMouseenter() {
var _this6 = this;
this.mouseMoveCallback = function (e) {
_this6.onBarMousemove(e);
};
this.registeredEvents.progressBarMouseMove = this.mouseMoveCallback;
this.progressBar.addEventListener('mousemove', this.registeredEvents.progressBarMouseMove);
this.showThumbnailHolder();
};
vttThumbnailsPlugin.prototype.onBarMouseleave = function onBarMouseleave() {
if (this.registeredEvents.progressBarMouseMove) {
this.progressBar.removeEventListener('mousemove', this.registeredEvents.progressBarMouseMove);
}
this.hideThumbnailHolder();
};
vttThumbnailsPlugin.prototype.getXCoord = function getXCoord(bar, mouseX) {
var rect = bar.getBoundingClientRect();
var docEl = document.documentElement;
return mouseX - (rect.left + (window.pageXOffset || docEl.scrollLeft || 0));
};
vttThumbnailsPlugin.prototype.onBarMousemove = function onBarMousemove(event) {
this.updateThumbnailStyle(videojs.dom.getPointerPosition(this.progressBar, event).x, this.progressBar.offsetWidth);
};
vttThumbnailsPlugin.prototype.setImageInCacheForItem = function setImageInCacheForItem(item) {
// Cache miss
if (item.css.url && !cache[item.css.url]) {
var image = new Image();
image.src = item.css.url;
cache[item.css.url] = image;
}
};
vttThumbnailsPlugin.prototype.getStyleForTime = function getStyleForTime(time) {
for (var i = 0; i < this.vttData.length; ++i) {
var item = this.vttData[i];
if (time >= item.start && time < item.end) {
this.setImageInCacheForItem(item);
return item.css;
}
}
};
vttThumbnailsPlugin.prototype.showThumbnailHolder = function showThumbnailHolder() {
this.thumbnailHolder.style.opacity = '1';
};
vttThumbnailsPlugin.prototype.hideThumbnailHolder = function hideThumbnailHolder() {
this.thumbnailHolder.style.opacity = '0';
};
vttThumbnailsPlugin.prototype.updateThumbnailStyle = function updateThumbnailStyle(percent, width) {
var duration = this.player.duration();
var time = percent * duration;
var currentStyle = this.getStyleForTime(time);
var timestamp = new Date(Math.round(time) * 1000).toISOString().substr(11, 8);
timestamp = duration > 3599 ? timestamp : timestamp.substring(3);
this.timeHolder.innerText = timestamp;
if (!currentStyle) {
return this.hideThumbnailHolder();
}
var xPos = percent * width;
var thumbnailWidth = parseInt(this.thumbnailHolder.offsetWidth);
var halfthumbnailWidth = thumbnailWidth / 2;
var marginRight = width - (xPos + halfthumbnailWidth);
var marginLeft = xPos - halfthumbnailWidth;
if (marginLeft > 0 && marginRight > 0) {
this.thumbnailHolder.style.transform = 'translateX(' + (xPos - halfthumbnailWidth) + 'px)';
} else if (marginLeft <= 0) {
this.thumbnailHolder.style.transform = 'translateX(' + 0 + 'px)';
} else if (marginRight <= 0) {
this.thumbnailHolder.style.transform = 'translateX(' + (xPos + marginRight - halfthumbnailWidth) + 'px)';
}
if (this.lastStyle && this.lastStyle === currentStyle) {
return;
}
this.lastStyle = currentStyle;
for (var style in currentStyle) {
if (currentStyle.hasOwnProperty(style)) {
this.thumbnailHolder.style[style] = currentStyle[style];
}
}
};
vttThumbnailsPlugin.prototype.processVtt = function processVtt(data) {
var _this7 = this;
var processedVtts = [];
var vttDefinitions = data.split(/[\r\n][\r\n]/i);
vttDefinitions.forEach(function (vttDef) {
if (vttDef.match(/([0-9]{2}:)?([0-9]{2}:)?[0-9]{2}(.[0-9]{3})?( ?--> ?)([0-9]{2}:)?([0-9]{2}:)?[0-9]{2}(.[0-9]{3})?[\r\n]{1}.*/gi)) {
var vttDefSplit = vttDef.split(/[\r\n]/i);
var vttTiming = vttDefSplit[0];
var vttTimingSplit = vttTiming.split(/ ?--> ?/i);
var vttTimeStart = vttTimingSplit[0];
var vttTimeEnd = vttTimingSplit[1];
var vttImageDef = vttDefSplit[1];
var vttCssDef = _this7.getVttCss(vttImageDef);
processedVtts.push({
start: _this7.getSecondsFromTimestamp(vttTimeStart),
end: _this7.getSecondsFromTimestamp(vttTimeEnd),
css: vttCssDef
});
}
});
return processedVtts;
};
vttThumbnailsPlugin.prototype.getFullyQualifiedUrl = function getFullyQualifiedUrl(path, base) {
if (path.indexOf('//') >= 0) {
// We have a fully qualified path.
return path;
}
if (base.indexOf('//') === 0) {
// We don't have a fully qualified path, but need to
// be careful with trimming.
return [base.replace(/\/$/gi, ''), this.trim(path, '/')].join('/');
}
if (base.indexOf('//') > 0) {
// We don't have a fully qualified path, and should
// trim both sides of base and path.
return [this.trim(base, '/'), this.trim(path, '/')].join('/');
}
// If all else fails.
return path;
};
vttThumbnailsPlugin.prototype.getPropsFromDef = function getPropsFromDef(def) {
var imageDefSplit = def.split(/#xywh=/i);
var imageUrl = imageDefSplit[0];
var imageCoords = imageDefSplit[1];
var splitCoords = imageCoords.match(/[0-9]+/gi);
return {
x: splitCoords[0],
y: splitCoords[1],
w: splitCoords[2],
h: splitCoords[3],
image: imageUrl
};
};
vttThumbnailsPlugin.prototype.getVttCss = function getVttCss(vttImageDef) {
var cssObj = {};
// If there isn't a protocol, use the VTT source URL.
var baseSplit = void 0;
if (this.options.baseUrl) {
baseSplit = this.options.baseUrl;
} else if (this.options.src.indexOf('//') >= 0) {
baseSplit = this.options.src.split(/([^\/]*)$/gi).shift();
} else {
baseSplit = this.getBaseUrl() + this.options.src.split(/([^\/]*)$/gi).shift();
}
vttImageDef = this.getFullyQualifiedUrl(vttImageDef, baseSplit);
// deal with regular thumbnails
if (!vttImageDef.match(/#xywh=/i)) {
cssObj.background = 'url("' + vttImageDef + '")';
cssObj.url = vttImageDef;
return cssObj;
}
// deal with sprited thumbnails
var imageProps = this.getPropsFromDef(vttImageDef);
cssObj.background = 'url("' + imageProps.image + '") no-repeat -' + imageProps.x + 'px -' + imageProps.y + 'px';
cssObj.width = imageProps.w + 'px';
cssObj.height = imageProps.h + 'px';
cssObj.url = imageProps.image;
return cssObj;
};
vttThumbnailsPlugin.prototype.doconstructTimestamp = function doconstructTimestamp(timestamp) {
var splitStampMilliseconds = timestamp.split('.');
var timeParts = splitStampMilliseconds[0];
var timePartsSplit = timeParts.split(':');
return {
milliseconds: parseInt(splitStampMilliseconds[1]) || 0,
seconds: parseInt(timePartsSplit.pop()) || 0,
minutes: parseInt(timePartsSplit.pop()) || 0,
hours: parseInt(timePartsSplit.pop()) || 0
};
};
vttThumbnailsPlugin.prototype.getSecondsFromTimestamp = function getSecondsFromTimestamp(timestamp) {
var timestampParts = this.doconstructTimestamp(timestamp);
return parseInt(timestampParts.hours * (60 * 60) + timestampParts.minutes * 60 + timestampParts.seconds + timestampParts.milliseconds / 1000);
};
vttThumbnailsPlugin.prototype.trim = function trim(str, charlist) {
var whitespace = [' ', '\n', '\r', '\t', '\f', '\x0b', '\xa0', '\u2000', '\u2001', '\u2002', '\u2003', '\u2004', '\u2005', '\u2006', '\u2007', '\u2008', '\u2009', '\u200A', '\u200B', '\u2028', '\u2029', '\u3000'].join('');
var l = 0;
var i = 0;
str += '';
if (charlist) {
whitespace = (charlist + '').replace(/([[\]().?/*{}+$^:])/g, '$1');
}
l = str.length;
for (i = 0; i < l; i++) {
if (whitespace.indexOf(str.charAt(i)) === -1) {
str = str.substring(i);
break;
}
}
l = str.length;
for (i = l - 1; i >= 0; i--) {
if (whitespace.indexOf(str.charAt(i)) === -1) {
str = str.substring(0, i + 1);
break;
}
}
return whitespace.indexOf(str.charAt(0)) === -1 ? str : '';
};
return vttThumbnailsPlugin;
}();
// Register the plugin with video.js.
registerPlugin('vttThumbnails', vttThumbnails);
// Include the version number.
vttThumbnails.VERSION = version;
return vttThumbnails;
})));