HTML5 quality selection
This commit is contained in:
72
src/js/controls.js
vendored
72
src/js/controls.js
vendored
@ -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;
|
||||
},
|
||||
|
||||
|
@ -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"
|
||||
],
|
||||
},
|
||||
|
||||
|
146
src/js/html5.js
Normal file
146
src/js/html5.js
Normal file
@ -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;
|
@ -355,13 +355,16 @@ class Listeners {
|
||||
this.player.storage.set({ speed: this.player.speed });
|
||||
});
|
||||
|
||||
// Quality change
|
||||
utils.on(this.player.media, 'qualitychange', () => {
|
||||
// Update UI
|
||||
controls.updateSetting.call(this.player, 'quality');
|
||||
|
||||
// Quality request
|
||||
utils.on(this.player.media, 'qualityrequested', event => {
|
||||
// Save to storage
|
||||
this.player.storage.set({ quality: this.player.quality });
|
||||
this.player.storage.set({ quality: event.detail.quality });
|
||||
});
|
||||
|
||||
// Quality change
|
||||
utils.on(this.player.media, 'qualitychange', event => {
|
||||
// Update UI
|
||||
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
|
||||
});
|
||||
|
||||
// Caption language change
|
||||
|
@ -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,32 +76,10 @@ const media = {
|
||||
}
|
||||
} else if (this.isHTML5) {
|
||||
ui.setTitle.call(this);
|
||||
|
||||
html5.extend.call(this);
|
||||
}
|
||||
},
|
||||
|
||||
// Cancel current network requests
|
||||
// See https://github.com/sampotts/plyr/issues/174
|
||||
cancelRequests() {
|
||||
if (!this.isHTML5) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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');
|
||||
},
|
||||
};
|
||||
|
||||
export default media;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user