aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/js/controls.js72
-rw-r--r--src/js/defaults.js22
-rw-r--r--src/js/html5.js146
-rw-r--r--src/js/listeners.js13
-rw-r--r--src/js/media.js25
-rw-r--r--src/js/plugins/youtube.js83
-rw-r--r--src/js/plyr.js16
-rw-r--r--src/js/source.js6
-rw-r--r--src/js/ui.js4
-rw-r--r--src/js/utils.js15
10 files changed, 299 insertions, 103 deletions
diff --git a/src/js/controls.js b/src/js/controls.js
index 5d6d98a1..828f0dcd 100644
--- a/src/js/controls.js
+++ b/src/js/controls.js
@@ -7,6 +7,7 @@ import utils from './utils';
import ui from './ui';
import i18n from './i18n';
import captions from './captions';
+import html5 from './html5';
// Sniff out the browser
const browser = utils.getBrowser();
@@ -435,8 +436,8 @@ const controls = {
utils.toggleHidden(pane, !toggle);
},
- // Set the YouTube quality menu
- // TODO: Support for HTML5
+ // Set the quality menu
+ // TODO: Vimeo support
setQualityMenu(options) {
// Menu required
if (!utils.is.element(this.elements.settings.panes.quality)) {
@@ -449,12 +450,10 @@ const controls = {
// Set options if passed and filter based on config
if (utils.is.array(options)) {
this.options.quality = options.filter(quality => this.config.quality.options.includes(quality));
- } else {
- this.options.quality = this.config.quality.options;
}
// Toggle the pane and tab
- const toggle = !utils.is.empty(this.options.quality) && this.isYouTube;
+ const toggle = !utils.is.empty(this.options.quality) && this.options.quality.length > 1;
controls.toggleTab.call(this, type, toggle);
// If we're hiding, nothing more to do
@@ -470,22 +469,26 @@ const controls = {
let label = '';
switch (quality) {
- case 'hd2160':
+ case 2160:
label = '4K';
break;
- case 'hd1440':
+ case 1440:
label = 'WQHD';
break;
- case 'hd1080':
+ case 1080:
label = 'HD';
break;
- case 'hd720':
+ case 720:
label = 'HD';
break;
+ case 576:
+ label = 'SD';
+ break;
+
default:
break;
}
@@ -497,9 +500,14 @@ const controls = {
return controls.createBadge.call(this, label);
};
- this.options.quality.forEach(quality =>
- controls.createMenuItem.call(this, quality, list, type, controls.getLabel.call(this, 'quality', quality), getBadge(quality)),
- );
+ // Sort options by the config and then render options
+ this.options.quality.sort((a, b) => {
+ const sorting = this.config.quality.options;
+ return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
+ }).forEach(quality => {
+ const label = controls.getLabel.call(this, 'quality', quality);
+ controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality));
+ });
controls.updateSetting.call(this, type, list);
},
@@ -512,28 +520,10 @@ const controls = {
return value === 1 ? 'Normal' : `${value}×`;
case 'quality':
- switch (value) {
- case 'hd2160':
- return '2160P';
- case 'hd1440':
- return '1440P';
- case 'hd1080':
- return '1080P';
- case 'hd720':
- return '720P';
- case 'large':
- return '480P';
- case 'medium':
- return '360P';
- case 'small':
- return '240P';
- case 'tiny':
- return 'Tiny';
- case 'default':
- return 'Auto';
- default:
- return value;
+ if (utils.is.number(value)) {
+ return `${value}p`;
}
+ return utils.toTitleCase(value);
case 'captions':
return controls.getLanguage.call(this);
@@ -544,7 +534,7 @@ const controls = {
},
// Update the selected setting
- updateSetting(setting, container) {
+ updateSetting(setting, container, input) {
const pane = this.elements.settings.panes[setting];
let value = null;
let list = container;
@@ -555,7 +545,7 @@ const controls = {
break;
default:
- value = this[setting];
+ value = !utils.is.empty(input) ? input : this[setting];
// Get default
if (utils.is.empty(value)) {
@@ -563,7 +553,7 @@ const controls = {
}
// Unsupported value
- if (!this.options[setting].includes(value)) {
+ if (!utils.is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
this.debug.warn(`Unsupported value of '${value}' for ${setting}`);
return;
}
@@ -767,10 +757,10 @@ const controls = {
// Check if we need to hide/show the settings menu
checkMenu() {
- const speedHidden = this.elements.settings.tabs.speed.getAttribute('hidden') !== null;
- const languageHidden = this.elements.settings.tabs.captions.getAttribute('hidden') !== null;
+ const { tabs } = this.elements.settings;
+ const visible = !utils.is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden);
- utils.toggleHidden(this.elements.settings.menu, speedHidden && languageHidden);
+ utils.toggleHidden(this.elements.settings.menu, !visible);
},
// Show/hide menu
@@ -1179,6 +1169,10 @@ const controls = {
controls.setSpeedMenu.call(this);
+ if (this.isHTML5) {
+ controls.setQualityMenu.call(this, html5.getQualityOptions.call(this));
+ }
+
return container;
},
diff --git a/src/js/defaults.js b/src/js/defaults.js
index 98df8b85..3e053f2f 100644
--- a/src/js/defaults.js
+++ b/src/js/defaults.js
@@ -63,17 +63,19 @@ const defaults = {
// Quality default
quality: {
- default: 'default',
+ default: 720,
options: [
- 'hd2160',
- 'hd1440',
- 'hd1080',
- 'hd720',
- 'large',
- 'medium',
- 'small',
- 'tiny',
- 'default',
+ 4320,
+ 2880,
+ 2160,
+ 1440,
+ 1080,
+ 720,
+ 576,
+ 480,
+ 360,
+ 240,
+ 'default', // YouTube's "auto"
],
},
diff --git a/src/js/html5.js b/src/js/html5.js
new file mode 100644
index 00000000..3818a441
--- /dev/null
+++ b/src/js/html5.js
@@ -0,0 +1,146 @@
+// ==========================================================================
+// Plyr HTML5 helpers
+// ==========================================================================
+
+import support from './support';
+import utils from './utils';
+
+const html5 = {
+ getSources() {
+ if (!this.isHTML5) {
+ return null;
+ }
+
+ return this.media.querySelectorAll('source');
+ },
+
+ // Get quality levels
+ getQualityOptions() {
+ if (!this.isHTML5) {
+ return null;
+ }
+
+ // Get sources
+ const sources = html5.getSources.call(this);
+
+ if (utils.is.empty(sources)) {
+ return null;
+ }
+
+ // Get <source> with size attribute
+ const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size')));
+
+ // If none, bail
+ if (utils.is.empty(sizes)) {
+ return null;
+ }
+
+ // Reduce to unique list
+ return utils.dedupe(sizes.map(source => Number(source.getAttribute('size'))));
+ },
+
+ extend() {
+ if (!this.isHTML5) {
+ return;
+ }
+
+ const player = this;
+
+ // Quality
+ Object.defineProperty(player.media, 'quality', {
+ get() {
+ // Get sources
+ const sources = html5.getSources.call(player);
+
+ if (utils.is.empty(sources)) {
+ return null;
+ }
+
+ const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
+
+ if (utils.is.empty(matches)) {
+ return null;
+ }
+
+ return Number(matches[0].getAttribute('size'));
+ },
+ set(input) {
+ // Get sources
+ const sources = html5.getSources.call(player);
+
+ if (utils.is.empty(sources)) {
+ return;
+ }
+
+ // Get matches for requested size
+ const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
+
+ // No matches for requested size
+ if (utils.is.empty(matches)) {
+ return;
+ }
+
+ // Get supported sources
+ const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
+
+ // No supported sources
+ if (utils.is.empty(supported)) {
+ return;
+ }
+
+ // Trigger change event
+ utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
+ quality: input,
+ });
+
+ // Get current state
+ const { currentTime, playing } = player;
+
+ // Set new source
+ player.media.src = supported[0].getAttribute('src');
+
+ // Load new source
+ player.media.load();
+
+ // Resume playing
+ if (playing) {
+ player.play();
+ }
+
+ // Restore time
+ player.currentTime = currentTime;
+
+ // Trigger change event
+ utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
+ quality: input,
+ });
+ },
+ });
+ },
+
+ // Cancel current network requests
+ // See https://github.com/sampotts/plyr/issues/174
+ cancelRequests() {
+ if (!this.isHTML5) {
+ return;
+ }
+
+ // Remove child sources
+ utils.removeElement(html5.getSources());
+
+ // Set blank video src attribute
+ // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
+ // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
+ this.media.setAttribute('src', this.config.blankVideo);
+
+ // Load the new empty source
+ // This will cancel existing requests
+ // See https://github.com/sampotts/plyr/issues/174
+ this.media.load();
+
+ // Debugging
+ this.debug.log('Cancelled network requests');
+ },
+};
+
+export default html5;
diff --git a/src/js/listeners.js b/src/js/listeners.js
index 7c39ece7..be7a53ef 100644
--- a/src/js/listeners.js
+++ b/src/js/listeners.js
@@ -355,13 +355,16 @@ class Listeners {
this.player.storage.set({ speed: this.player.speed });
});
+ // Quality request
+ utils.on(this.player.media, 'qualityrequested', event => {
+ // Save to storage
+ this.player.storage.set({ quality: event.detail.quality });
+ });
+
// Quality change
- utils.on(this.player.media, 'qualitychange', () => {
+ utils.on(this.player.media, 'qualitychange', event => {
// Update UI
- controls.updateSetting.call(this.player, 'quality');
-
- // Save to storage
- this.player.storage.set({ quality: this.player.quality });
+ controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
});
// Caption language change
diff --git a/src/js/media.js b/src/js/media.js
index 3a97a9d9..bba2c62b 100644
--- a/src/js/media.js
+++ b/src/js/media.js
@@ -6,6 +6,7 @@ import support from './support';
import utils from './utils';
import youtube from './plugins/youtube';
import vimeo from './plugins/vimeo';
+import html5 from './html5';
import ui from './ui';
// Sniff out the browser
@@ -75,31 +76,9 @@ const media = {
}
} else if (this.isHTML5) {
ui.setTitle.call(this);
- }
- },
- // Cancel current network requests
- // See https://github.com/sampotts/plyr/issues/174
- cancelRequests() {
- if (!this.isHTML5) {
- return;
+ html5.extend.call(this);
}
-
- // Remove child sources
- utils.removeElement(this.media.querySelectorAll('source'));
-
- // Set blank video src attribute
- // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
- // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
- this.media.setAttribute('src', this.config.blankVideo);
-
- // Load the new empty source
- // This will cancel existing requests
- // See https://github.com/sampotts/plyr/issues/174
- this.media.load();
-
- // Debugging
- this.debug.log('Cancelled network requests');
},
};
diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js
index 93f8cd33..27865d6f 100644
--- a/src/js/plugins/youtube.js
+++ b/src/js/plugins/youtube.js
@@ -6,6 +6,64 @@ import utils from './../utils';
import controls from './../controls';
import ui from './../ui';
+// Standardise YouTube quality unit
+function mapQualityUnit(input) {
+ switch (input) {
+ case 'hd2160':
+ return 2160;
+
+ case 2160:
+ return 'hd2160';
+
+ case 'hd1440':
+ return 1440;
+
+ case 1440:
+ return 'hd1440';
+
+ case 'hd1080':
+ return 1080;
+
+ case 1080:
+ return 'hd1080';
+
+ case 'hd720':
+ return 720;
+
+ case 720:
+ return 'hd720';
+
+ case 'large':
+ return 480;
+
+ case 480:
+ return 'large';
+
+ case 'medium':
+ return 360;
+
+ case 360:
+ return 'medium';
+
+ case 'small':
+ return 240;
+
+ case 240:
+ return 'small';
+
+ default:
+ return 'default';
+ }
+}
+
+function mapQualityUnits(levels) {
+ if (utils.is.empty(levels)) {
+ return levels;
+ }
+
+ return utils.dedupe(levels.map(level => mapQualityUnit(level)));
+}
+
const youtube = {
setup() {
// Add embed class for responsive
@@ -168,14 +226,10 @@ const youtube = {
utils.dispatchEvent.call(player, player.media, 'error');
},
- onPlaybackQualityChange(event) {
- // Get the instance
- const instance = event.target;
-
- // Get current quality
- player.media.quality = instance.getPlaybackQuality();
-
- utils.dispatchEvent.call(player, player.media, 'qualitychange');
+ onPlaybackQualityChange() {
+ utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
+ quality: player.media.quality,
+ });
},
onPlaybackRateChange(event) {
// Get the instance
@@ -240,15 +294,18 @@ const youtube = {
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
- return instance.getPlaybackQuality();
+ return mapQualityUnit(instance.getPlaybackQuality());
},
set(input) {
+ const quality = input;
+
+ // Set via API
+ instance.setPlaybackQuality(mapQualityUnit(quality));
+
// Trigger request event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
- quality: input,
+ quality,
});
-
- instance.setPlaybackQuality(input);
},
});
@@ -401,7 +458,7 @@ const youtube = {
}
// Get quality
- controls.setQualityMenu.call(player, instance.getAvailableQualityLevels());
+ controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
break;
diff --git a/src/js/plyr.js b/src/js/plyr.js
index 5c3aeea2..75d75ef9 100644
--- a/src/js/plyr.js
+++ b/src/js/plyr.js
@@ -670,24 +670,28 @@ class Plyr {
/**
* Set playback quality
- * Currently YouTube only
- * @param {string} input - Quality level
+ * Currently HTML5 & YouTube only
+ * @param {number} input - Quality level
*/
set quality(input) {
let quality = null;
- if (utils.is.string(input)) {
- quality = input;
+ if (!utils.is.empty(input)) {
+ quality = Number(input);
}
- if (!utils.is.string(quality)) {
+ if (!utils.is.number(quality) || quality === 0) {
quality = this.storage.get('quality');
}
- if (!utils.is.string(quality)) {
+ if (!utils.is.number(quality)) {
quality = this.config.quality.selected;
}
+ if (!utils.is.number(quality)) {
+ quality = this.config.quality.default;
+ }
+
if (!this.options.quality.includes(quality)) {
this.debug.warn(`Unsupported quality option (${quality})`);
return;
diff --git a/src/js/source.js b/src/js/source.js
index d252ba6b..3e713102 100644
--- a/src/js/source.js
+++ b/src/js/source.js
@@ -4,6 +4,7 @@
import { providers } from './types';
import utils from './utils';
+import html5 from './html5';
import media from './media';
import ui from './ui';
import support from './support';
@@ -31,13 +32,14 @@ const source = {
}
// Cancel current network requests
- media.cancelRequests.call(this);
+ html5.cancelRequests.call(this);
// Destroy instance and re-setup
this.destroy.call(
this,
() => {
- // TODO: Reset menus here
+ // Reset quality options
+ this.options.quality = [];
// Remove elements
utils.removeElement(this.media);
diff --git a/src/js/ui.js b/src/js/ui.js
index 97281b28..1d671577 100644
--- a/src/js/ui.js
+++ b/src/js/ui.js
@@ -71,8 +71,8 @@ const ui = {
// Reset loop state
this.loop = null;
- // Reset quality options
- this.options.quality = [];
+ // Reset quality setting
+ this.quality = null;
// Reset volume display
ui.updateVolume.call(this);
diff --git a/src/js/utils.js b/src/js/utils.js
index e9687a14..dd1466df 100644
--- a/src/js/utils.js
+++ b/src/js/utils.js
@@ -586,15 +586,15 @@ const utils = {
},
// Trigger event
- dispatchEvent(element, type, bubbles, detail) {
+ dispatchEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element
- if (!utils.is.element(element) || !utils.is.string(type)) {
+ if (!utils.is.element(element) || utils.is.empty(type)) {
return;
}
// Create and dispatch the event
const event = new CustomEvent(type, {
- bubbles: utils.is.boolean(bubbles) ? bubbles : false,
+ bubbles,
detail: Object.assign({}, detail, {
plyr: utils.is.plyr(this) ? this : null,
}),
@@ -737,6 +737,15 @@ const utils = {
return utils.extend(target, ...sources);
},
+ // Remove duplicates in an array
+ dedupe(array) {
+ if (!utils.is.array(array)) {
+ return array;
+ }
+
+ return array.filter((item, index) => array.indexOf(item) === index);
+ },
+
// Get the provider for a given URL
getProviderByUrl(url) {
// YouTube