Controls cleanup, work on captions bug, click to invert time

This commit is contained in:
Sam Potts 2017-11-18 19:30:26 +11:00
parent d7a1c44281
commit 6984d6fb16
32 changed files with 360 additions and 254 deletions

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

2
demo/dist/demo.js vendored
View File

@ -1,3 +1,3 @@
document.addEventListener("DOMContentLoaded",function(){function e(e,t,o){e&&e.classList[o?"add":"remove"](t)}function t(t,a){if(t in n&&(a||t!==r)&&(r.length||t!==n.video)){switch(t){case n.video:o.source={type:"video",title:"View From A Blue Moon",sources:[{src:"https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4",type:"video/mp4"}],poster:"https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg",tracks:[{kind:"captions",label:"English",srclang:"en",src:"https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt",default:!0},{kind:"captions",label:"French",srclang:"fr",src:"https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"}]};break;case n.audio:o.source={type:"audio",title:"Kishi Bashi – “It All Began With A Burst”",sources:[{src:"https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3",type:"audio/mp3"},{src:"https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg",type:"audio/ogg"}]};break;case n.youtube:o.source={type:"video",title:"View From A Blue Moon",sources:[{src:"https://youtube.com/watch?v=bTqVqk7FSmY",type:"youtube"}]};break;case n.vimeo:o.source={type:"video",sources:[{src:"https://vimeo.com/76979871",type:"vimeo"}]}}r=t,Array.from(i).forEach(function(t){return e(t.parentElement,"active",!1)}),e(document.querySelector('[data-source="'+t+'"]'),"active",!0),Array.from(document.querySelectorAll(".plyr__cite")).forEach(function(e){e.setAttribute("hidden","")}),document.querySelector(".plyr__cite--"+t).removeAttribute("hidden")}}window.shr&&window.shr.setup({count:{classname:"button__count"}});document.addEventListener("focusout",function(e){e.target.classList.remove("tab-focus")}),document.addEventListener("keydown",function(e){9===e.keyCode&&window.setTimeout(function(){document.activeElement.classList.add("tab-focus")},0)});var o=new window.Plyr("#player",{debug:!0,title:"View From A Blue Moon",iconUrl:"../dist/plyr.svg",keyboard:{global:!0},tooltips:{controls:!0},captions:{active:!0},controls:["play-large","play","progress","current-time","mute","volume","captions","settings","fullscreen","pip","airplay"]});window.player=o;var i=document.querySelectorAll("[data-source]"),n={video:"video",audio:"audio",youtube:"youtube",vimeo:"vimeo"},r=window.location.hash.replace("#",""),a=window.history&&window.history.pushState;if(Array.from(i).forEach(function(e){e.addEventListener("click",function(){var o=e.getAttribute("data-source");t(o),a&&window.history.pushState({type:o},"","#"+o)})}),window.addEventListener("popstate",function(e){e.state&&"type"in e.state&&t(e.state.type)}),a){var s=!r.length;s&&(r=n.video),r in n&&window.history.replaceState({type:r},"",s?"":"#"+r),r!==n.video&&t(r,!0)}}),"plyr.io"===window.location.host&&(!function(e,t,o,i,n,r,a){e.GoogleAnalyticsObject=n,e.ga=e.ga||function(){(e.ga.q=e.ga.q||[]).push(arguments)},e.ga.l=1*new Date,r=t.createElement(o),a=t.getElementsByTagName(o)[0],r.async=1,r.src="//www.google-analytics.com/analytics.js",a.parentNode.insertBefore(r,a)}(window,document,"script",0,"ga"),window.ga("create","UA-40881672-11","auto"),window.ga("send","pageview"));
document.addEventListener("DOMContentLoaded",function(){function e(e,t,o){e&&e.classList[o?"add":"remove"](t)}function t(t,r){if(t in a&&(r||t!==n)&&(n.length||t!==a.video)){switch(t){case a.video:o.source={type:"video",title:"View From A Blue Moon",sources:[{src:"media/View_From_A_Blue_Moon_Trailer-HD.mp4",type:"video/mp4"}],poster:"hmedia/View_From_A_Blue_Moon_Trailer-HD.jpg",tracks:[{kind:"captions",label:"English",srclang:"en",src:"media/View_From_A_Blue_Moon_Trailer-HD.en.vtt",default:!0},{kind:"captions",label:"French",srclang:"fr",src:"media/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"}]};break;case a.audio:o.source={type:"audio",title:"Kishi Bashi – “It All Began With A Burst”",sources:[{src:"https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3",type:"audio/mp3"},{src:"https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg",type:"audio/ogg"}]};break;case a.youtube:o.source={type:"video",title:"View From A Blue Moon",sources:[{src:"https://youtube.com/watch?v=bTqVqk7FSmY",type:"youtube"}]};break;case a.vimeo:o.source={type:"video",sources:[{src:"https://vimeo.com/76979871",type:"vimeo"}]}}n=t,Array.from(i).forEach(function(t){return e(t.parentElement,"active",!1)}),e(document.querySelector('[data-source="'+t+'"]'),"active",!0),Array.from(document.querySelectorAll(".plyr__cite")).forEach(function(e){e.setAttribute("hidden","")}),document.querySelector(".plyr__cite--"+t).removeAttribute("hidden")}}window.shr&&window.shr.setup({count:{classname:"button__count"}});document.addEventListener("focusout",function(e){e.target.classList.remove("tab-focus")}),document.addEventListener("keydown",function(e){9===e.keyCode&&window.setTimeout(function(){document.activeElement.classList.add("tab-focus")},0)});var o=new window.Plyr("#player",{debug:!0,title:"View From A Blue Moon",iconUrl:"../dist/plyr.svg",keyboard:{global:!0},tooltips:{controls:!0},captions:{active:!0},controls:["play-large","play","progress","current-time","mute","volume","captions","settings","fullscreen","pip","airplay"],keys:{google:"AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c"}});window.player=o;var i=document.querySelectorAll("[data-source]"),a={video:"video",audio:"audio",youtube:"youtube",vimeo:"vimeo"},n=window.location.hash.replace("#",""),r=window.history&&window.history.pushState;if(Array.from(i).forEach(function(e){e.addEventListener("click",function(){var o=e.getAttribute("data-source");t(o),r&&window.history.pushState({type:o},"","#"+o)})}),window.addEventListener("popstate",function(e){e.state&&"type"in e.state&&t(e.state.type)}),r){var s=!n.length;s&&(n=a.video),n in a&&window.history.replaceState({type:n},"",s?"":"#"+n),n!==a.video&&t(n,!0)}}),"plyr.io"===window.location.host&&(!function(e,t,o,i,a,n,r){e.GoogleAnalyticsObject=a,e.ga=e.ga||function(){(e.ga.q=e.ga.q||[]).push(arguments)},e.ga.l=1*new Date,n=t.createElement(o),r=t.getElementsByTagName(o)[0],n.async=1,n.src="//www.google-analytics.com/analytics.js",r.parentNode.insertBefore(n,r)}(window,document,"script",0,"ga"),window.ga("create","UA-40881672-11","auto"),window.ga("send","pageview"));
//# sourceMappingURL=demo.js.map

File diff suppressed because one or more lines are too long

2
demo/dist/error.css vendored

File diff suppressed because one or more lines are too long

View File

@ -82,17 +82,16 @@
</header>
<main>
<video controls crossorigin playsinline loop poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg"
id="player">
<video controls crossorigin playsinline loop poster="media/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
<!-- Video files -->
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" type="video/mp4">
<source src="media/View_From_A_Blue_Moon_Trailer-HD.mp4" type="video/mp4">
<!-- Text track file -->
<track kind="captions" label="English" srclang="en" src="webvtt/View_From_A_Blue_Moon_Trailer-HD.en.vtt" default>
<track kind="captions" label="Français" srclang="fr" src="webvtt/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
<track kind="captions" label="English" srclang="en" src="media/View_From_A_Blue_Moon_Trailer-HD.en.vtt" default>
<track kind="captions" label="Français" srclang="fr" src="media/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
<!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" download>Download</a>
<a href="media/View_From_A_Blue_Moon_Trailer-HD.mp4" download>Download</a>
</video>
<ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@ -65,6 +65,9 @@ document.addEventListener('DOMContentLoaded', () => {
'pip',
'airplay',
],
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
});
// Expose for testing
@ -102,24 +105,24 @@ document.addEventListener('DOMContentLoaded', () => {
title: 'View From A Blue Moon',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4',
src: 'media/View_From_A_Blue_Moon_Trailer-HD.mp4',
type: 'video/mp4',
},
],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
poster: 'hmedia/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
src: 'media/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
src: 'media/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
};

View File

@ -10,7 +10,7 @@ video {
// Example players
.plyr {
margin: 0 auto;
margin: @spacing-base auto;
border-radius: @border-radius-base;
box-shadow: 0 2px 5px fade(#000, 20%);
}

View File

@ -27,15 +27,18 @@ body {
flex-direction: column;
}
.grid {
flex: 1;
overflow: auto;
}
main {
margin: auto;
text-align: center;
}
.grid {
flex: 1;
}
aside {
position: relative;
display: flex;
flex-shrink: 0;
align-items: center;

View File

@ -4,7 +4,7 @@
@font-face {
font-family: 'Avenir';
src: url('https://cdn.plyr.io/static/fonts/avenir-medium.woff2') format('woff2'),
src: local('Avenir-Medium'), url('https://cdn.plyr.io/static/fonts/avenir-medium.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/avenir-medium.woff') format('woff');
font-style: normal;
font-weight: @font-weight-base;
@ -13,7 +13,7 @@
@font-face {
font-family: 'Avenir';
src: url('https://cdn.plyr.io/static/fonts/avenir-bold.woff2') format('woff2'),
src: local('Avenir-Heavy'), url('https://cdn.plyr.io/static/fonts/avenir-bold.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/avenir-bold.woff') format('woff');
font-style: normal;
font-weight: @font-weight-bold;
@ -22,7 +22,7 @@
@font-face {
font-family: 'Avenir';
src: url('https://cdn.plyr.io/static/fonts/avenir-black.woff2?v=3') format('woff2'),
src: local('Avenir-Black'), url('https://cdn.plyr.io/static/fonts/avenir-black.woff2?v=3') format('woff2'),
url('https://cdn.plyr.io/static/fonts/avenir-black.woff?v=3') format('woff');
font-style: normal;
font-weight: @font-weight-heavy;

View File

@ -33,6 +33,7 @@
// ---------------------------------------
.font-size(@font-size: 16) {
@rem: round((@font-size / 16), 3);
font-size: (@font-size * 1px);
font-size: ~'@{rem}rem';
}

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.js vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -233,7 +233,7 @@ Option | Type | Default | Description
`seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind.
`volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player.
`muted` | Boolean | `false` | Whether to start playback muted. If the `muted` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true.
`clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle pause/play.
`clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause.
`disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content.
`hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly.
`showPosterOnEnd` | Boolean | false | This will restore and *reload* HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution.
@ -241,9 +241,12 @@ Option | Type | Default | Description
`tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to.
`duration` | Number | `null` | Specify a custom duration for media.
`displayDuration` | Boolean | `true` | Displays the duration of the media on the "metadataloaded" event (on startup) in the current time display. This will only work if the `preload` attribute is not set to `none` (or is not set at all) and you choose not to display the duration (see `controls` option).
`invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter.
`toggleInvert` | Boolean | `true` | Allow users to click to toggle the above.
`listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. IF your handler prevents default on the event, the default handler will not fire.
`captions` | Object | `{ active: false, language: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available).
`fullscreen` | Object | `{ enabled: true, fallback: true }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution.
`ratio` | String | `16:9` | The aspect ratio you want to use for embedded players.
`storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use.
`speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5.
`quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display.

View File

@ -79,8 +79,9 @@ const captions = {
// Filter doesn't seem to work for a TextTrackList :-(
Array.from(this.captions.tracks).forEach(track => {
if (track.language === this.captions.language.toLowerCase()) {
if (track.language.toLowerCase() === this.language.toLowerCase()) {
this.captions.currentTrack = track;
console.warn(`Set current track to ${this.language}`);
}
});
};

171
src/js/controls.js vendored
View File

@ -115,6 +115,10 @@ const controls = {
// Create a badge
createBadge(text) {
if (utils.is.empty(text)) {
return null;
}
const badge = utils.createElement('span', {
class: this.config.classNames.menu.value,
});
@ -319,6 +323,39 @@ const controls = {
return container;
},
// Create a settings menu item
createMenuItem(value, list, type, title, badge = null, checked = false) {
const item = utils.createElement('li');
const label = utils.createElement('label', {
class: this.config.classNames.control,
});
const radio = utils.createElement(
'input',
utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs[type]), {
type: 'radio',
name: `plyr-${type}`,
value,
checked,
class: 'plyr__sr-only',
})
);
const faux = utils.createElement('span', { 'aria-hidden': true });
label.appendChild(radio);
label.appendChild(faux);
label.insertAdjacentHTML('beforeend', title);
if (utils.is.htmlElement(badge)) {
label.appendChild(badge);
}
item.appendChild(label);
list.appendChild(item);
},
// Update hover tooltip for seeking
updateSeekTooltip(event) {
// Bail if setting not true
@ -353,7 +390,7 @@ const controls = {
}
// Display the time a click would seek to
ui.updateTimeDisplay.call(this, this.duration / 100 * percent, this.elements.display.seekTooltip);
ui.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent);
// Set position
this.elements.display.seekTooltip.style.left = `${percent}%`;
@ -390,6 +427,7 @@ const controls = {
// Set the YouTube quality menu
// TODO: Support for HTML5
setQualityMenu(options) {
const type = 'quality';
const list = this.elements.settings.panes.quality.querySelector('ul');
// Set options if passed and filter based on config
@ -401,7 +439,7 @@ const controls = {
// Toggle the pane and tab
const toggle = !utils.is.empty(this.options.quality) && this.type === 'youtube';
controls.toggleTab.call(this, 'quality', toggle);
controls.toggleTab.call(this, type, toggle);
// If we're hiding, nothing more to do
if (!toggle) {
@ -443,35 +481,18 @@ const controls = {
return controls.createBadge.call(this, label);
};
this.options.quality.forEach(quality => {
const item = utils.createElement('li');
this.options.quality.forEach(quality =>
controls.createMenuItem.call(
this,
quality,
list,
type,
controls.getLabel.call(this, 'quality', quality),
getBadge(quality)
)
);
const label = utils.createElement('label', {
class: this.config.classNames.control,
});
const radio = utils.createElement(
'input',
utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs.quality), {
type: 'radio',
name: 'plyr-quality',
value: quality,
})
);
label.appendChild(radio);
label.appendChild(document.createTextNode(controls.getLabel.call(this, 'quality', quality)));
const badge = getBadge(quality);
if (utils.is.htmlElement(badge)) {
label.appendChild(badge);
}
item.appendChild(label);
list.appendChild(item);
});
controls.updateSetting.call(this, 'quality', list);
controls.updateSetting.call(this, type, list);
},
// Translate a value into a nice label
@ -573,7 +594,7 @@ const controls = {
},
// Set the looping options
setLoopMenu() {
/* setLoopMenu() {
const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panes.loop.querySelector('ul');
@ -609,7 +630,7 @@ const controls = {
item.appendChild(button);
list.appendChild(item);
});
},
}, */
// Get current selected caption language
// TODO: rework this to user the getter in the API?
@ -631,11 +652,13 @@ const controls = {
// Set a list of available captions languages
setCaptionsMenu() {
// TODO: Captions or language? Currently it's mixed
const type = 'captions';
const list = this.elements.settings.panes.captions.querySelector('ul');
// Toggle the pane and tab
const toggle = !utils.is.empty(this.captions.tracks);
controls.toggleTab.call(this, 'captions', toggle);
controls.toggleTab.call(this, type, toggle);
// Empty the menu
utils.emptyElement(list);
@ -648,7 +671,6 @@ const controls = {
// Re-map the tracks into just the data we need
const tracks = Array.from(this.captions.tracks).map(track => ({
language: track.language,
badge: true,
label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(),
}));
@ -660,41 +682,24 @@ const controls = {
// Generate options
tracks.forEach(track => {
const item = utils.createElement('li');
const label = utils.createElement('label', {
class: this.config.classNames.control,
});
const radio = utils.createElement(
'input',
utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs.language), {
type: 'radio',
name: 'plyr-language',
value: track.language,
})
controls.createMenuItem.call(
this,
track.language,
list,
'language',
track.label || track.language,
controls.createBadge.call(this, track.language.toUpperCase()),
track.language.toLowerCase() === this.captions.language.toLowerCase()
);
if (track.language.toLowerCase() === this.captions.language.toLowerCase()) {
radio.checked = true;
}
label.appendChild(radio);
label.appendChild(document.createTextNode(track.label || track.language));
if (track.badge) {
label.appendChild(controls.createBadge.call(this, track.language.toUpperCase()));
}
item.appendChild(label);
list.appendChild(item);
});
controls.updateSetting.call(this, 'captions', list);
controls.updateSetting.call(this, type, list);
},
// Set a list of available captions languages
setSpeedMenu(options) {
const type = 'speed';
// Set options if passed and filter based on config
if (utils.is.array(options)) {
this.options.speed = options.filter(speed => this.config.speed.options.includes(speed));
@ -704,7 +709,7 @@ const controls = {
// Toggle the pane and tab
const toggle = !utils.is.empty(this.options.speed);
controls.toggleTab.call(this, 'speed', toggle);
controls.toggleTab.call(this, type, toggle);
// If we're hiding, nothing more to do
if (!toggle) {
@ -722,39 +727,23 @@ const controls = {
utils.emptyElement(list);
// Create items
this.options.speed.forEach(speed => {
const item = utils.createElement('li');
this.options.speed.forEach(speed =>
controls.createMenuItem.call(this, speed, list, type, controls.getLabel.call(this, 'speed', speed))
);
const label = utils.createElement('label', {
class: this.config.classNames.control,
});
const radio = utils.createElement(
'input',
utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs.speed), {
type: 'radio',
name: 'plyr-speed',
value: speed,
})
);
label.appendChild(radio);
label.insertAdjacentHTML('beforeend', controls.getLabel.call(this, 'speed', speed));
item.appendChild(label);
list.appendChild(item);
});
controls.updateSetting.call(this, 'speed', list);
controls.updateSetting.call(this, type, list);
},
// Show/hide menu
toggleMenu(event) {
const { form } = this.elements.settings;
const button = this.elements.buttons.settings;
const show = utils.is.boolean(event) ? event : form && form.getAttribute('aria-hidden') === 'true';
const show = utils.is.boolean(event)
? event
: utils.is.htmlElement(form) && form.getAttribute('aria-hidden') === 'true';
if (utils.is.event(event)) {
const isMenuItem = form && form.contains(event.target);
const isMenuItem = utils.is.htmlElement(form) && form.contains(event.target);
const isButton = event.target === this.elements.buttons.settings;
// If the click was inside the form or if the click
@ -771,10 +760,11 @@ const controls = {
}
// Set form and button attributes
if (button) {
if (utils.is.htmlElement(button)) {
button.setAttribute('aria-expanded', show);
}
if (form) {
if (utils.is.htmlElement(form)) {
form.setAttribute('aria-hidden', !show);
if (show) {
@ -882,6 +872,9 @@ const controls = {
pane.setAttribute('aria-hidden', !show);
tab.setAttribute('aria-expanded', show);
pane.removeAttribute('tabindex');
// Focus the first item
pane.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]')[0].focus();
},
// Build the default HTML

View File

@ -22,13 +22,20 @@ const defaults = {
// Pass a custom duration
duration: null,
// Display the media duration
// Display the media duration on load in the current time position
// If you have opted to display both duration and currentTime, this is ignored
displayDuration: true,
// Invert the current time to be a countdown
invertTime: true,
// Clicking the currentTime inverts it's value to show time left rather than elapsed
toggleInvert: true,
// Aspect ratio (for embeds)
ratio: '16:9',
// Click video to play
// Click video container to play/pause
clickToPlay: true,
// Auto hide the controls
@ -276,6 +283,7 @@ const defaults = {
isIos: 'plyr--is-ios',
isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
menu: {
value: 'plyr__menu__value',
badge: 'plyr__badge',
@ -298,6 +306,11 @@ const defaults = {
},
tabFocus: 'plyr__tab-focus',
},
// API keys
keys: {
google: null,
},
};
export default defaults;

View File

@ -209,7 +209,7 @@ const listeners = {
// Toggle controls on mouse events and entering fullscreen
utils.on(
this.elements.container,
'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen',
'click mouseenter mouseleave mousemove touchmove enterfullscreen exitfullscreen',
event => {
this.toggleControls(event);
}
@ -217,11 +217,11 @@ const listeners = {
}
// Handle user exiting fullscreen by escaping etc
if (fullscreen.enabled) {
/* if (fullscreen.enabled) {
utils.on(document, fullscreen.eventType, event => {
this.toggleFullscreen(event);
});
}
} */
},
// Listen for media events
@ -230,7 +230,7 @@ const listeners = {
utils.on(this.media, 'timeupdate seeking', event => ui.timeUpdate.call(this, event));
// Display duration
utils.on(this.media, 'durationchange loadedmetadata', event => ui.displayDuration.call(this, event));
utils.on(this.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this, event));
// Handle the media finishing
utils.on(this.media, 'ended', () => {
@ -463,11 +463,16 @@ const listeners = {
controls.showTab.call(this, event);
// Settings menu items - use event delegation as items are added/removed
// Settings - Language
if (utils.matches(event.target, this.config.selectors.inputs.language)) {
// Settings - Language
proxy(event, 'language', () => {
this.toggleCaptions(true);
this.language = event.target.value.toLowerCase();
const language = event.target.value;
this.toggleCaptions(!utils.is.empty(language));
if (!utils.is.empty(language)) {
this.language = event.target.value.toLowerCase();
}
});
} else if (utils.matches(event.target, this.config.selectors.inputs.quality)) {
// Settings - Quality
@ -479,7 +484,7 @@ const listeners = {
proxy(event, 'speed', () => {
this.speed = parseFloat(event.target.value);
});
} else if (utils.matches(event.target, this.config.selectors.buttons.loop)) {
} /* else if (utils.matches(event.target, this.config.selectors.buttons.loop)) {
// Settings - Looping
// TODO: use toggle buttons
proxy(event, 'loop', () => {
@ -488,7 +493,7 @@ const listeners = {
this.console.warn('Set loop');
});
}
} */
});
// Seek
@ -498,6 +503,20 @@ const listeners = {
})
);
// Current time invert
// Only if one time element is used for both currentTime and duration
if (this.config.toggleInvert && !utils.is.htmlElement(this.elements.display.duration)) {
utils.on(this.elements.display.currentTime, 'click', () => {
// Do nothing if we're at the start
if (this.currentTime === 0) {
return;
}
this.config.invertTime = !this.config.invertTime;
ui.timeUpdate.call(this);
});
}
// Volume
utils.on(this.elements.inputs.volume, inputEvent, event =>
proxy(event, 'volume', () => {
@ -533,7 +552,7 @@ const listeners = {
// TODO: Check we need capture here
utils.on(
this.elements.controls,
'focus blur',
'focusin focusout',
event => {
this.toggleControls(event);
},

View File

@ -23,19 +23,21 @@ const youtube = {
// Set ID
this.media.setAttribute('id', utils.generateId(this.type));
// Get the title
const key = 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c';
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&fields=items(snippet(title))&part=snippet&key=${key}`;
// Get the media title via Google API
const key = this.config.keys.google;
if (utils.is.string(key) && !utils.is.empty(key)) {
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`;
fetch(url)
.then(response => response.json())
.then(obj => {
if (utils.is.object(obj)) {
this.config.title = obj.items[0].snippet.title;
ui.setTitle.call(this);
}
})
.catch(() => {});
fetch(url)
.then(response => (response.ok ? response.json() : null))
.then(result => {
if (result !== null && utils.is.object(result)) {
this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this);
}
})
.catch(() => {});
}
// Setup API
if (utils.is.object(window.YT)) {

View File

@ -669,7 +669,7 @@ class Plyr {
const language = input.toLowerCase();
// If nothing to change, bail
if (this.captions.language === language) {
if (this.language === language) {
return;
}
@ -797,31 +797,28 @@ class Plyr {
// Show the player controls in fullscreen mode
toggleControls(toggle) {
const player = this;
// We need controls of course...
if (!utils.is.htmlElement(this.elements.controls)) {
return player;
return this;
}
// Don't hide if config says not to, it's audio, or not ready or loading
if (!this.supported.ui || !this.config.hideControls || this.type === 'audio') {
return player;
return this;
}
let delay = 0;
let show = toggle;
let isEnterFullscreen = false;
const loading = utils.hasClass(this.elements.container, this.config.classNames.loading);
// Default to false if no boolean
// Get toggle state if not set
if (!utils.is.boolean(toggle)) {
if (utils.is.event(toggle)) {
// Is the enter fullscreen event
isEnterFullscreen = toggle.type === 'enterfullscreen';
// Whether to show controls
show = ['mousemove', 'touchstart', 'mouseenter', 'focus'].includes(toggle.type);
show = ['click', 'mousemove', 'touchmove', 'mouseenter', 'focusin'].includes(toggle.type);
// Delay hiding on move events
if (['mousemove', 'touchmove'].includes(toggle.type)) {
@ -829,8 +826,9 @@ class Plyr {
}
// Delay a little more for keyboard users
if (toggle.type === 'focus') {
if (toggle.type === 'focusin') {
delay = 3000;
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
}
} else {
show = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
@ -841,7 +839,7 @@ class Plyr {
window.clearTimeout(this.timers.hover);
// If the mouse is not over the controls, set a timeout to hide them
if (show || this.media.paused || loading) {
if (show || this.media.paused || this.loading) {
// Check if controls toggled
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false);
@ -851,8 +849,8 @@ class Plyr {
}
// Always show controls when paused or if touch
if (this.media.paused || loading) {
return player;
if (this.media.paused || this.loading) {
return this;
}
// Delay for hiding on touch
@ -870,6 +868,11 @@ class Plyr {
return;
}
// Restore transition behaviour
if (!utils.hasClass(this.elements.container, this.config.classNames.hideControls)) {
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
}
// Check if controls toggled
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, true);

View File

@ -15,7 +15,7 @@ const ui = {
},
// Toggle native HTML5 media controls
toggleNativeControls(toggle) {
toggleNativeControls(toggle = false) {
if (toggle && this.isHTML5) {
this.media.setAttribute('controls', '');
} else {
@ -100,26 +100,6 @@ const ui = {
ui.setTitle.call(this);
},
// Show the duration on metadataloaded
displayDuration() {
if (!this.supported.ui) {
return;
}
// If there's only one time display, display duration there
if (!this.elements.display.duration && this.config.displayDuration && this.paused) {
ui.updateTimeDisplay.call(this, this.duration, this.elements.display.currentTime);
}
// If there's a duration element, update content
if (this.elements.display.duration) {
ui.updateTimeDisplay.call(this, this.duration, this.elements.display.duration);
}
// Update the tooltip (if visible)
controls.updateSeekTooltip.call(this);
},
// Setup aria attribute for play and iframe title
setTitle() {
// Find the current text
@ -165,23 +145,6 @@ const ui = {
this.toggleControls(this.paused);
},
// Update volume UI and storage
updateVolume() {
if (!this.supported.ui) {
return;
}
// Update range
if (utils.is.htmlElement(this.elements.inputs.volume)) {
ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
}
// Update checkbox for mute state
if (utils.is.htmlElement(this.elements.buttons.mute)) {
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
}
},
// Check if media is loading
checkLoading(event) {
this.loading = event.type === 'waiting';
@ -199,8 +162,25 @@ const ui = {
}, this.loading ? 250 : 0);
},
// Update volume UI and storage
updateVolume() {
if (!this.supported.ui) {
return;
}
// Update range
if (utils.is.htmlElement(this.elements.inputs.volume)) {
ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
}
// Update checkbox for mute state
if (utils.is.htmlElement(this.elements.buttons.mute)) {
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
}
},
// Update seek value and lower fill
setRange(target, value) {
setRange(target, value = 0) {
if (!utils.is.htmlElement(target)) {
return;
}
@ -214,9 +194,8 @@ const ui = {
// Set <progress> value
setProgress(target, input) {
// Default to 0
const value = !utils.is.undefined(input) ? input : 0;
const progress = !utils.is.undefined(target) ? target : this.elements.display.buffer;
const value = utils.is.number(input) ? input : 0;
const progress = utils.is.htmlElement(target) ? target : this.elements.display.buffer;
// Update value and label
if (utils.is.htmlElement(progress)) {
@ -232,7 +211,7 @@ const ui = {
// Update <progress> elements
updateProgress(event) {
if (!this.supported.ui) {
if (!this.supported.ui || !utils.is.event(event)) {
return;
}
@ -280,41 +259,49 @@ const ui = {
},
// Update the displayed time
updateTimeDisplay(value, element) {
// Bail if there's no duration display
if (!utils.is.htmlElement(element)) {
return null;
updateTimeDisplay(target = null, time = 0, inverted = false) {
// Bail if there's no element to display or the value isn't a number
if (!utils.is.htmlElement(target) || !utils.is.number(time)) {
return;
}
// Fallback to 0
const time = !Number.isNaN(value) ? value : 0;
// Format time component to add leading zero
const format = value => `0${value}`.slice(-2);
let secs = parseInt(time % 60, 10);
let mins = parseInt((time / 60) % 60, 10);
const hours = parseInt((time / 60 / 60) % 60, 10);
// Helpers
const getHours = value => parseInt((value / 60 / 60) % 60, 10);
const getMinutes = value => parseInt((value / 60) % 60, 10);
const getSeconds = value => parseInt(value % 60, 10);
// Breakdown to hours, mins, secs
let hours = getHours(time);
const mins = getMinutes(time);
const secs = getSeconds(time);
// Do we need to display hours?
const displayHours = parseInt((this.duration / 60 / 60) % 60, 10) > 0;
// Ensure it's two digits. For example, 03 rather than 3.
secs = `0${secs}`.slice(-2);
mins = `0${mins}`.slice(-2);
// Generate display
const display = `${(displayHours ? `${hours}:` : '') + mins}:${secs}`;
if (getHours(this.duration) > 0) {
hours = `${hours}:`;
} else {
hours = '';
}
// Render
// eslint-disable-next-line
element.textContent = display;
// Return for looping
return display;
// eslint-disable-next-line no-param-reassign
target.textContent = `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
},
// Handle time change event
timeUpdate(event) {
// Only invert if only one time element is displayed and used for both duration and currentTime
const invert = !utils.is.htmlElement(this.elements.display.duration) && this.config.invertTime;
// Duration
ui.updateTimeDisplay.call(this, this.currentTime, this.elements.display.currentTime);
ui.updateTimeDisplay.call(
this,
this.elements.display.currentTime,
invert ? this.duration - this.currentTime : this.currentTime,
invert
);
// Ignore updates while seeking
if (event && event.type === 'timeupdate' && this.media.seeking) {
@ -324,6 +311,26 @@ const ui = {
// Playing progress
ui.updateProgress.call(this, event);
},
// Show the duration on metadataloaded
durationUpdate() {
if (!this.supported.ui) {
return;
}
// If there's only one time display, display duration there
if (!utils.is.htmlElement(this.elements.display.duration) && this.config.displayDuration && this.paused) {
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
}
// If there's a duration element, update content
if (utils.is.htmlElement(this.elements.display.duration)) {
ui.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
}
// Update the tooltip (if visible)
controls.updateSeekTooltip.call(this);
},
};
export default ui;

View File

@ -31,6 +31,9 @@ const utils = {
htmlElement(input) {
return !this.undefined(input) && input instanceof HTMLElement;
},
textNode(input) {
return this.getConstructor(input) === Text;
},
event(input) {
return !this.undefined(input) && input instanceof Event;
},
@ -49,8 +52,8 @@ const utils = {
return (
input === null ||
typeof input === 'undefined' ||
((this.string(input) || this.array(input) || this.nodeList(input)) && input.length === 0) ||
(this.object(input) && Object.keys(input).length === 0)
((this.string(input) || this.array(input) || this.nodeList(input)) && !input.length) ||
(this.object(input) && !Object.keys(input).length)
);
},
getConstructor(input) {
@ -140,8 +143,12 @@ const utils = {
// Get the sprite
fetch(url)
.then(response => response.text())
.then(response => (response.ok ? response.text() : null))
.then(text => {
if (text === null) {
return;
}
if (support.storage) {
window.localStorage.setItem(
prefix + id,
@ -152,7 +159,8 @@ const utils = {
}
updateSprite.call(container, text);
});
})
.catch(() => {});
}
},
@ -201,22 +209,6 @@ const utils = {
});
},
// Remove an element
removeElement(element) {
if (!utils.is.htmlElement(element) || !utils.is.htmlElement(element.parentNode)) {
return null;
}
element.parentNode.removeChild(element);
return element;
},
// Inaert an element after another
insertAfter(element, target) {
target.parentNode.insertBefore(element, target.nextSibling);
},
// Create a DocumentFragment
createElement(type, attributes, text) {
// Create a new <element>
@ -236,12 +228,28 @@ const utils = {
return element;
},
// Inaert an element after another
insertAfter(element, target) {
target.parentNode.insertBefore(element, target.nextSibling);
},
// Insert a DocumentFragment
insertElement(type, parent, attributes, text) {
// Inject the new <element>
parent.appendChild(utils.createElement(type, attributes, text));
},
// Remove an element
removeElement(element) {
if (!utils.is.htmlElement(element) || !utils.is.htmlElement(element.parentNode)) {
return null;
}
element.parentNode.removeChild(element);
return element;
},
// Remove all child elements
emptyElement(element) {
let { length } = element.childNodes;
@ -433,9 +441,9 @@ const utils = {
// Trap focus inside container
trapFocus() {
const tabbables = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = tabbables[0];
const last = tabbables[tabbables.length - 1];
const focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = focusable[0];
const last = focusable[focusable.length - 1];
utils.on(
this.elements.container,

View File

@ -12,6 +12,7 @@
line-height: @plyr-line-height;
direction: ltr;
text-shadow: none;
transition: box-shadow 0.3s ease;
.plyr-font-smoothing(off);
// Media elements
@ -22,6 +23,11 @@
vertical-align: middle;
border-radius: inherit;
}
&:focus {
outline: 0;
box-shadow: 0 0 0 3px fade(#000, 10%);
}
}
// Full UI only

View File

@ -26,4 +26,5 @@
@import 'states/fullscreen';
@import 'utils/animation';
@import 'utils/hidden';

View File

@ -3,11 +3,11 @@
// --------------------------------------------------------------
.plyr__badge {
padding: 0 4px;
padding: 3px 4px;
border-radius: 2px;
background: @plyr-menu-color;
color: @plyr-menu-bg;
font-size: @plyr-font-size-tiny;
line-height: 1.5;
line-height: 1;
.plyr-font-smoothing(on);
}

View File

@ -57,9 +57,9 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: ceil(@plyr-control-spacing * 1.25);
padding: ceil(@plyr-control-spacing * 1.5);
background: fade(@plyr-video-control-bg-hover, 80%);
border: 3px solid currentColor;
border: 0;
border-radius: 100%;
box-shadow: 0 1px 1px fade(#000, 15%);
color: @plyr-video-control-color;
@ -81,7 +81,12 @@
}
&:focus {
outline: 1px dotted fade(@plyr-video-control-color, 50%);
outline: 0;
}
&.plyr__tab-focus {
outline: 0;
box-shadow: 0 0 0 3px fade(@plyr-video-control-color, 50%);
}
}

View File

@ -129,11 +129,45 @@
}
label.plyr__control {
padding-left: ceil(@plyr-control-padding * 2.5);
padding-left: @plyr-control-padding;
input[type='radio'] {
/*input[type='radio'] {
position: relative;
left: -@plyr-control-padding;
}*/
input[type='radio'] + span {
position: relative;
display: block;
height: 14px;
width: 14px;
border-radius: 100%;
background: fade(#000, 10%);
margin-right: @plyr-control-spacing;
box-shadow: inset 0 1px 1px fade(#000, 15%);
&::after {
content: '';
position: absolute;
height: 6px;
width: 6px;
top: 4px;
left: 4px;
transform: scale(0);
opacity: 0;
background: #fff;
border-radius: 100%;
transition: transform 0.3s ease, opacity 0.3s ease;
}
}
input[type='radio']:checked + span {
background: @plyr-color-main;
&::after {
transform: scale(1);
opacity: 1;
}
}
}

View File

@ -12,6 +12,8 @@
cursor: pointer;
border: none;
background: transparent;
transition: box-shadow 0.3s ease;
border-radius: (@plyr-range-thumb-height * 2);
// Used in JS to populate lower fill for WebKit
color: @plyr-range-selected-bg;
@ -79,10 +81,6 @@
border: 0;
}
&.plyr__tab-focus {
outline-offset: 3px;
}
// Pressed styles
&:active {
&::-webkit-slider-thumb {
@ -114,7 +112,7 @@
}
&.plyr__tab-focus {
outline: 1px dotted fade(@plyr-video-control-color, 50%);
box-shadow: 0 0 0 3px fade(@plyr-video-control-color, 50%);
}
}
@ -133,6 +131,6 @@
}
&.plyr__tab-focus {
outline: 1px dotted fade(@plyr-audio-control-color, 50%);
box-shadow: 0 0 0 3px fade(@plyr-audio-control-color, 50%);
}
}

View File

@ -0,0 +1,7 @@
// --------------------------------------------------------------
// Animation utils
// --------------------------------------------------------------
.plyr--no-transition {
transition: none !important;
}