Skip to content
Open
2 changes: 1 addition & 1 deletion src/js/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const defaults = {

// Quality default
quality: {
default: 576,
default: 0, // Represents the index instead of label
options: [
4320,
2880,
Expand Down
95 changes: 61 additions & 34 deletions src/js/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import html5 from './html5';
import i18n from './i18n';
import support from './support';
import { repaint, transitionEndEvent } from './utils/animation';
import { dedupe } from './utils/arrays';
import browser from './utils/browser';
import {
createElement,
Expand Down Expand Up @@ -671,6 +670,16 @@ const controls = {
},

// Set the quality menu
// options is expected to be an array of objects
// Each entry in this array should be of the type:
// {
// height: Number, // mandatory
// label: String, // optional
// badge: String, // optional
// }
// The order of qualities will be based on height. If there are multiple
// entries with the same height, then we use the entry's array index instead.
// If badge is not specified, it will be looked up by height.
setQualityMenu(options) {
// Menu required
if (!is.element(this.elements.settings.panes.quality)) {
Expand All @@ -680,9 +689,9 @@ const controls = {
const type = 'quality';
const list = this.elements.settings.panes.quality.querySelector('ul');

// Set options if passed and filter based on uniqueness and config
// Set options if passed
if (is.array(options)) {
this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));
this.options.quality = options;
}

// Toggle the pane and tab
Expand All @@ -702,53 +711,62 @@ const controls = {

// Get the badge HTML for HD, 4K etc
const getBadge = quality => {
const label = i18n.get(`qualityBadge.${quality}`, this.config);
const {
height,
badge = i18n.get(`qualityBadge.${height}`, this.config),
} = quality;

if (!label.length) {
return null;
}

return controls.createBadge.call(this, label);
return badge ? controls.createBadge.call(this, badge) : null;
};

// Sort options by the config and then render options
this.options.quality
const finalOptions = this.options.quality
.sort((a, b) => {
const sorting = this.config.quality.options;
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
})
.forEach(quality => {
controls.createMenuItem.call(this, {
value: quality,
list,
type,
title: controls.getLabel.call(this, 'quality', quality),
badge: getBadge(quality),
});
if (a.height === b.height) {
return this.options.quality.indexOf(a) > this.options.quality.indexOf(b);
}
return a.height > b.height;
}).map(quality => ({
value: this.options.quality.indexOf(quality),
list,
type,
title: quality.label || controls.getLabel.call(this, 'quality', quality.height),
badge: getBadge(quality),
})).reverse(); // We reverse it so that the lower height options appear at the bottom of the quality menu

// The incoming quality option may not have labels
// If this is the case, then the text displayed as part of qualityMenu
// comes from 'title' of finalOptions
// Thus, since finalOptions contains all the relevant data for each quality
// entry, we update options.quality with finalOptions
this.options.quality = options.map((quality, index) => {
const finalOpt = finalOptions.find(opt => opt.value === index);
return Object.assign({}, quality, {
label: finalOpt.title,
badge: finalOpt.badge,
});

controls.updateSetting.call(this, type, list);
});
finalOptions.forEach(controls.createMenuItem.bind(this));
controls.updateSetting.call(this, type, list, this.config[type].default);
},

// Translate a value into a nice label
getLabel(setting, value) {
let label;
switch (setting) {
case 'speed':
return value === 1 ? i18n.get('normal', this.config) : `${value}×`;

case 'quality':
if (is.number(value)) {
const label = i18n.get(`qualityLabel.${value}`, this.config);
label = i18n.get(`qualityLabel.${value}`, this.config);

if (!label.length) {
if (!label.length) {
// Only return with p if value is a number
if (is.number(value)) {
return `${value}p`;
}

return label;
}

return toTitleCase(value);

case 'captions':
return captions.getLabel.call(this);

Expand All @@ -773,16 +791,25 @@ const controls = {
value = this.config[setting].default;
}

let settingOptions = this.options[setting];
if (setting === 'quality') {
value = settingOptions[value].label;
settingOptions = settingOptions.map(quality => quality.label);
}

// Unsupported value
if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
if (!is.empty(settingOptions) && !settingOptions.includes(value)) {
this.debug.warn(`Unsupported value of '${value}' for ${setting}`);
return;
}

// Disabled value
if (!this.config[setting].options.includes(value)) {
this.debug.warn(`Disabled value of '${value}' for ${setting}`);
return;
// Don't check for quality which is permissive and accepts anything
if (setting !== 'quality') {
if (!this.config[setting].options.includes(value)) {
this.debug.warn(`Disabled value of '${value}' for ${setting}`);
return;
}
}
}

Expand Down
26 changes: 21 additions & 5 deletions src/js/html5.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,20 @@ const html5 = {
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map(source => Number(source.getAttribute('size')))
.filter(Boolean);
.map((source) => {
const {
// For backward-compatibility, fallback on the old size-attribute (which I think we should deprecate)
height = Number(source.getAttribute('size')),
label,
badge,
} = source.dataset;

return {
badge,
label,
height,
};
});
},

extend() {
Expand All @@ -40,16 +52,20 @@ const html5 = {
// Get sources
const sources = html5.getSources.call(player);
const source = sources.find(source => source.getAttribute('src') === player.source);
if (!source) {
return undefined;
}
const sourceIndex = sources.indexOf(source);

// Return size, if match is found
return source && Number(source.getAttribute('size'));
// Return label, if match is found
return player.options.quality[sourceIndex].label;
},
set(input) {
// Get sources
const sources = html5.getSources.call(player);

// Get first match for requested size
const source = sources.find(source => Number(source.getAttribute('size')) === input);
const source = sources[input.index];

// No matching source found
if (!source) {
Expand Down
6 changes: 4 additions & 2 deletions src/js/listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ class Listeners {
// Quality change
on.call(this.player, this.player.media, 'qualitychange', event => {
// Update UI
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality.index);
});

// Proxy events to container
Expand Down Expand Up @@ -511,7 +511,9 @@ class Listeners {
proxy(
event,
() => {
this.player.quality = event.target.value;
this.player.quality = {
index: Number(event.target.value),
};
showHomeTab();
},
'quality',
Expand Down
14 changes: 12 additions & 2 deletions src/js/plugins/youtube.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ function mapQualityUnits(levels) {
return levels;
}

return dedupe(levels.map(level => mapQualityUnit(level)));
const mappedLevels = dedupe(levels.map(level => mapQualityUnit(level)));
return mappedLevels.map((level, index) => ({
label: levels[index],
height: level,
}));
}

// Set playback state and trigger change (only on actual change)
Expand Down Expand Up @@ -300,7 +304,13 @@ const youtube = {
return mapQualityUnit(instance.getPlaybackQuality());
},
set(input) {
instance.setPlaybackQuality(mapQualityUnit(input));
let label;
if (is.string(input)) {
label = input;
} else if (is.object(input)) {
({label} = input);
}
instance.setPlaybackQuality(mapQualityUnit(label));
},
});

Expand Down
45 changes: 33 additions & 12 deletions src/js/plyr.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import source from './source';
import Storage from './storage';
import support from './support';
import ui from './ui';
import { closest } from './utils/arrays';
import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';
import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
import is from './utils/is';
Expand Down Expand Up @@ -677,21 +676,43 @@ class Plyr {
const config = this.config.quality;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old quality setter code was already too complex and hard to understand, so I think this is a step in the wrong direction. I think you should restore the old setter and the getter in the html5/youtube plugins and add a new getter/setter that sets only by index. videoTrack or level perhaps?

Copy link
Contributor Author

@gurupras gurupras Jul 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of your comment, I think, is referring to the fact that this setter is trying to account for number/string/object all-in-one.

The number version was added just now to be backwards compatible. At least with plyr.io. It wasn't meant to handle index.

The string version is to cater to users wanting to set it by label.

The object version is mainly to handle the internal implementation wherein:
user clicks quality option --> translate event.target.value into quality somehow. This was where the object came into play.

Now, with the suggestion to remove index attribute and use it as the value while creating radio button, we can update the quality setter to receive, for example, either ["0", 0, { index: 0 }]. I opted for the object implementation to allow sufficient differentiation.

const options = this.options.quality;

let quality;
// Create a local copy of input so we can handle modifications
let qualityInput = input;
if (!options.length) {
return;
}

let quality = [
!is.empty(input) && Number(input),
this.storage.get('quality'),
config.selected,
config.default,
].find(is.number);

if (!options.includes(quality)) {
const value = closest(options, quality);
this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);
quality = value;
// Support setting quality as a Number
if (is.number(qualityInput)) {
// Convert this to a string since quality labels are expected to be strings
// XXX: We really only accept labels, but
// this is left in place so as to be backward compatible
qualityInput = `${qualityInput}p`;
}

// Now, convert qualityInput into a quality object.
// This object is emitted as part of the qualityrequested event
if (is.string(qualityInput)) {
// We have only a label
// Find the index of this label and
// convert this into an Object of the expected type
quality = {
index: options.map(quality => quality.label).indexOf(qualityInput),
};
} else if (is.object(qualityInput)) {
quality = qualityInput;
} else {
this.debug.warn(`Quality option of unknown type: ${input} (${typeof input}). Ignoring`);
return;
}

// Get the corresponding quality listing from options
const entry = options[quality.index];

if (!entry) {
this.debug.warn(`Unsupported quality option: ${input}. Ignoring`);
return;
}

// Trigger request event
Expand Down