-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Improve flexibility of quality selector #1081
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 6 commits
14d0ce3
6bd9462
fda8424
6e41d5e
faaeb5d
28add9b
c3103f4
99613ca
41fe611
2e20972
3643a03
b081ca0
d50c65a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -373,7 +373,7 @@ const controls = { | |
| }, | ||
|
|
||
| // Create a settings menu item | ||
| createMenuItem({ value, list, type, title, badge = null, checked = false }) { | ||
| createMenuItem({ value, list, type, title, badge = null, checked = false, index = null }) { | ||
| const item = createElement('li'); | ||
|
|
||
| const label = createElement('label', { | ||
|
|
@@ -388,6 +388,7 @@ const controls = { | |
| value, | ||
| checked, | ||
| class: 'plyr__sr-only', | ||
| index, | ||
| }), | ||
| ); | ||
|
|
||
|
|
@@ -671,6 +672,17 @@ 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: | ||
| // { | ||
| // label: String, // mandatory | ||
| // height: Number, // mandatory | ||
| // index: Number, // 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 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)) { | ||
|
|
@@ -680,9 +692,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 = dedupe(options); | ||
|
||
| } | ||
|
|
||
| // Toggle the pane and tab | ||
|
|
@@ -702,10 +714,18 @@ const controls = { | |
|
|
||
| // Get the badge HTML for HD, 4K etc | ||
| const getBadge = quality => { | ||
| const label = i18n.get(`qualityBadge.${quality}`, this.config); | ||
| let label; | ||
| if (quality.badge) { | ||
| label = quality.badge; | ||
| } else { | ||
| // The badge is based on the height of the video | ||
| // TODO: We need some rounding logic here | ||
| label = i18n.get(`qualityBadge.${quality.label}`, this.config); | ||
|
|
||
| if (!label.length) { | ||
| return null; | ||
| if (!label.length) { | ||
| // TODO: Try to find a badge based on height | ||
| return null; | ||
| } | ||
| } | ||
|
||
|
|
||
| return controls.createBadge.call(this, label); | ||
|
|
@@ -714,41 +734,40 @@ const controls = { | |
| // 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; | ||
| if (a.height === b.height) { | ||
| return a.index > b.index ? -1 : 1; | ||
| } | ||
| return a.height > b.height ? -1 : 1; | ||
| }) | ||
|
||
| .forEach(quality => { | ||
| controls.createMenuItem.call(this, { | ||
| value: quality, | ||
| value: quality.label, | ||
| list, | ||
|
||
| type, | ||
| title: controls.getLabel.call(this, 'quality', quality), | ||
| title: controls.getLabel.call(this, 'quality', quality.height, toTitleCase(quality.label)), | ||
| badge: getBadge(quality), | ||
|
||
| index: quality.index, | ||
| }); | ||
| }); | ||
|
|
||
| controls.updateSetting.call(this, type, list); | ||
| }, | ||
|
|
||
| // Translate a value into a nice label | ||
| getLabel(setting, value) { | ||
| getLabel(setting, value, defaultLabel) { | ||
| let label; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you should restore getLabel (see above). Not sure why you removed
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No that was
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, replacing with the old logic for now. |
||
| 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); | ||
|
|
||
| if (!label.length) { | ||
| return `${value}p`; | ||
| } | ||
| label = i18n.get(`qualityLabel.${value}`, this.config); | ||
|
|
||
| return label; | ||
| // If we don't find a valid label, we return passed in defaultLabel | ||
| if (!label.length) { | ||
| return defaultLabel || toTitleCase(value); | ||
| } | ||
|
|
||
| return toTitleCase(value); | ||
|
|
||
| return label; | ||
| case 'captions': | ||
| return captions.getLabel.call(this); | ||
|
|
||
|
|
@@ -773,16 +792,26 @@ const controls = { | |
| value = this.config[setting].default; | ||
| } | ||
|
|
||
| let settingOptions = this.options[setting]; | ||
| if (setting === 'quality') { | ||
| // Quality is not an array of strings. It's an array of Objects | ||
| // Extract out the strings | ||
| settingOptions = this.options[setting].map(level => controls.getLabel.call(this, 'quality', level.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; | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,10 +21,15 @@ const html5 = { | |
| // Get quality levels | ||
| getQualityOptions() { | ||
| // Get sizes from <source> elements | ||
| return html5.getSources | ||
| const sizes = html5.getSources | ||
| .call(this) | ||
| .map(source => Number(source.getAttribute('size'))) | ||
| .filter(Boolean); | ||
| return sizes.map((size, index) => ({ | ||
| label: `${size}`, | ||
| height: size, | ||
| index, | ||
| })); | ||
| }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are multiple problems here.
I suggest you change it to something like this: // I haven't tested this code, and the comments are meant for you, not to stay in the code
return html5.getSources.call(this)
.map((source, index) => {
// Get data from the dataset (https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset)
const {
// For backward-compatibility, fallback on the old size-attribute (which I think we should deprecate)
height = source.getAttribute('size'),
label,
badge,
} = source.dataset;
return {
index, // Really don't think we need index here at all, as mentioned
badge,
label,
height,
};
});
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The template literal is to ensure that label conforms to the laid down rule (by me) that label is a Updated with your suggestion. This is neat. I like being able to specify label, badge via |
||
|
|
||
| extend() { | ||
|
|
@@ -35,21 +40,26 @@ const html5 = { | |
| const player = this; | ||
|
|
||
| // Quality | ||
| const qualityLevels = html5.getQualityOptions.call(player); | ||
| Object.defineProperty(player.media, 'quality', { | ||
| get() { | ||
| // 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 qualityLevels[sourceIndex].label; | ||
| }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be |
||
| 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.find(source => source.getAttribute('size') === input.label); | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be |
||
| // No matching source found | ||
| if (!source) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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.label); | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be |
||
|
|
||
| // Proxy events to container | ||
|
|
@@ -511,7 +511,10 @@ class Listeners { | |
| proxy( | ||
| event, | ||
| () => { | ||
| this.player.quality = event.target.value; | ||
| this.player.quality = { | ||
| label: event.target.value, | ||
| index: Number(event.target.attributes.index.value), | ||
| }; | ||
| showHomeTab(); | ||
| }, | ||
| 'quality', | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,7 +51,13 @@ 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], | ||
| index, | ||
| height: level, | ||
| badge: level >= 720 ? 'HD' : 'SD', | ||
|
||
| })); | ||
| } | ||
|
|
||
| // Set playback state and trigger change (only on actual change) | ||
|
|
@@ -300,7 +306,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)); | ||
| }, | ||
| }); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -677,21 +676,40 @@ class Plyr { | |
| const config = this.config.quality; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: 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 |
||
| 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 | ||
| qualityInput = `${qualityInput}`; | ||
| } | ||
|
|
||
| // 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 | ||
| // Convert this into an Object of the expected type | ||
| quality = { | ||
| label: 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.find((level) => level.label === quality.label && (quality.index ? level.index === quality.index : true)); | ||
|
|
||
| if (!entry) { | ||
| this.debug.warn(`Unsupported quality option: ${input}. Ignoring`); | ||
| return; | ||
| } | ||
|
|
||
| // Trigger request event | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optimally you shouldn't need to add index here. You can use
valuefor index (like captions do). This of course means you need to create the arguments forcreateMenuItembefore any sorting/dedupe/filtering.