ES6-ified
This commit is contained in:
parent
3d50936b47
commit
1cc2930dc0
@ -1,9 +1,11 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "prettier"],
|
||||
"parser": "babel-eslint",
|
||||
"extends": ["airbnb", "prettier"],
|
||||
"env": {
|
||||
"browser": true
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {},
|
||||
"globals": { "Plyr": true },
|
||||
"rules": {
|
||||
"no-const-assign": 1,
|
||||
"no-this-before-super": 1,
|
||||
@ -18,5 +20,8 @@
|
||||
"eqeqeq": [2, "always"],
|
||||
"one-var": [2, "never"],
|
||||
"comma-dangle": [2, "always-multiline"]
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
"error.css": "demo/src/less/bundles/error.less"
|
||||
},
|
||||
"js": {
|
||||
"demo.js": ["demo/src/js/lib/classlist.js", "demo/src/js/lib/tab-focus.js", "demo/src/js/main.js"]
|
||||
"demo.js": "demo/src/js/demo.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
demo/dist/demo.css
vendored
2
demo/dist/demo.css
vendored
File diff suppressed because one or more lines are too long
4
demo/dist/demo.js
vendored
4
demo/dist/demo.js
vendored
File diff suppressed because one or more lines are too long
1
demo/dist/demo.js.map
vendored
Normal file
1
demo/dist/demo.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
demo/dist/demo.svg
vendored
1
demo/dist/demo.svg
vendored
@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg"><symbol id="icon-github" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 .2c-4.4 0-8 3.6-8 8 0 3.5 2.3 6.5 5.5 7.6.4.1.5-.2.5-.4V14c-2.2.5-2.7-1-2.7-1-.4-.9-.9-1.2-.9-1.2-.7-.5.1-.5.1-.5.8.1 1.2.8 1.2.8.7 1.3 1.9.9 2.3.7.1-.5.3-.9.5-1.1-1.8-.2-3.6-.9-3.6-4 0-.9.3-1.6.8-2.1-.1-.2-.4-1 .1-2.1 0 0 .7-.2 2.2.8.6-.2 1.3-.3 2-.3s1.4.1 2 .3c1.5-1 2.2-.8 2.2-.8.4 1.1.2 1.9.1 2.1.5.6.8 1.3.8 2.1 0 3.1-1.9 3.7-3.7 3.9.3.4.6.9.6 1.6v2.2c0 .2.1.5.6.4 3.2-1.1 5.5-4.1 5.5-7.6-.1-4.4-3.7-8-8.1-8z"/></symbol><symbol id="icon-twitter" viewBox="0 0 16 16"><title>Twitter</title><path d="M16 3c-.6.3-1.2.4-1.9.5.7-.4 1.2-1 1.4-1.8-.6.4-1.3.6-2.1.8-.6-.6-1.5-1-2.4-1-1.7 0-3.2 1.5-3.2 3.3 0 .3 0 .5.1.7-2.7-.1-5.2-1.4-6.8-3.4-.3.5-.4 1-.4 1.7 0 1.1.6 2.1 1.5 2.7-.5 0-1-.2-1.5-.4C.7 7.7 1.8 9 3.3 9.3c-.3.1-.6.1-.9.1-.2 0-.4 0-.6-.1.4 1.3 1.6 2.3 3.1 2.3-1.1.9-2.5 1.4-4.1 1.4H0c1.5.9 3.2 1.5 5 1.5 6 0 9.3-5 9.3-9.3v-.4C15 4.3 15.6 3.7 16 3z"/></symbol><symbol id="icon-vimeo" viewBox="0 0 16 16"><path d="M16 4.3c-.1 1.6-1.2 3.7-3.3 6.4-2.2 2.8-4 4.2-5.5 4.2-.9 0-1.7-.9-2.4-2.6C4 9.9 3.4 5 2 5c-.1 0-.5.3-1.2.8l-.8-1c.8-.7 3.5-3.4 4.7-3.5 1.2-.1 2 .7 2.3 2.5.3 2 .8 6.1 1.8 6.1.9 0 2.5-3.4 2.6-4 .1-.9-.3-1.9-2.3-1.1.8-2.6 2.3-3.8 4.5-3.8 1.7.1 2.5 1.2 2.4 3.3z"/></symbol><symbol id="icon-youtube" viewBox="0 0 16 16"><path d="M15.8 4.8c-.2-1.3-.8-2.2-2.2-2.4C11.4 2 8 2 8 2s-3.4 0-5.6.4C1 2.6.3 3.5.2 4.8 0 6.1 0 8 0 8s0 1.9.2 3.2c.2 1.3.8 2.2 2.2 2.4C4.6 14 8 14 8 14s3.4 0 5.6-.4c1.4-.3 2-1.1 2.2-2.4C16 9.9 16 8 16 8s0-1.9-.2-3.2zM6 11V5l5 3-5 3z"/></symbol></svg>
|
Before Width: | Height: | Size: 1.7 KiB |
@ -24,20 +24,26 @@
|
||||
<div class="grid">
|
||||
<header>
|
||||
<h1>Plyr</h1>
|
||||
<p>A simple, accessible HTML5
|
||||
<button type="button" class="faux-link" data-source="video">Video</button>,
|
||||
<button type="button" class="faux-link" data-source="audio">Audio</button>,
|
||||
<p>A simple, accessible
|
||||
<button type="button" class="faux-link" data-source="video">
|
||||
<svg class="icon" title="HTML5">
|
||||
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
|
||||
</svg>Video</button>,
|
||||
<button type="button" class="faux-link" data-source="audio">
|
||||
<svg class="icon" title="HTML5">
|
||||
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
|
||||
</svg>Audio</button>,
|
||||
<button type="button" class="faux-link" data-source="youtube">
|
||||
<svg class="icon">
|
||||
<svg class="icon" title="YouTube">
|
||||
<path d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
|
||||
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
|
||||
M6,11V5l5,3L6,11z" />
|
||||
M6,11V5l5,3L6,11z"></path>
|
||||
</svg>YouTube</button> and
|
||||
<button type="button" class="faux-link" data-source="vimeo">
|
||||
<svg class="icon">
|
||||
<svg class="icon" title="Vimeo">
|
||||
<path d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
|
||||
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
|
||||
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z" />
|
||||
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"></path>
|
||||
</svg>Vimeo</button> media player.
|
||||
</p>
|
||||
|
||||
@ -49,7 +55,7 @@
|
||||
c0.8,0.1,1.2,0.8,1.2,0.8C4.4,13.4,5.6,13,6,12.8c0.1-0.5,0.3-0.9,0.5-1.1c-1.8-0.2-3.6-0.9-3.6-4c0-0.9,0.3-1.6,0.8-2.1
|
||||
c-0.1-0.2-0.4-1,0.1-2.1c0,0,0.7-0.2,2.2,0.8c0.6-0.2,1.3-0.3,2-0.3c0.7,0,1.4,0.1,2,0.3c1.5-1,2.2-0.8,2.2-0.8
|
||||
c0.4,1.1,0.2,1.9,0.1,2.1c0.5,0.6,0.8,1.3,0.8,2.1c0,3.1-1.9,3.7-3.7,3.9C9.7,12,10,12.5,10,13.2c0,1.1,0,1.9,0,2.2
|
||||
c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z" />
|
||||
c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z"></path>
|
||||
</svg>
|
||||
Download on GitHub
|
||||
</a>
|
||||
@ -72,22 +78,28 @@
|
||||
<ul>
|
||||
<li class="plyr__cite plyr__cite--video" hidden>
|
||||
<small>
|
||||
<a href="http://viewfromabluemoon.com/" target="_blank">View From A Blue Moon</a> © Brainfarm
|
||||
<a href="http://viewfromabluemoon.com/" target="_blank">
|
||||
<svg class="icon" title="HTML5">
|
||||
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
|
||||
</svg>View From A Blue Moon</a> © Brainfarm
|
||||
</small>
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--audio" hidden>
|
||||
<small>
|
||||
<a href="http://www.kishibashi.com/" target="_blank">Kishi Bashi – “It All Began With A Burst”</a> © Kishi Bashi
|
||||
<a href="http://www.kishibashi.com/" target="_blank">
|
||||
<svg class="icon" title="HTML5">
|
||||
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
|
||||
</svg>Kishi Bashi – “It All Began With A Burst”</a> © Kishi Bashi
|
||||
</small>
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--youtube" hidden>
|
||||
<small>
|
||||
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank">View From A Blue Moon</a> on
|
||||
<span class="color--youtube">
|
||||
<svg class="icon">
|
||||
<svg class="icon" title="YouTube">
|
||||
<path d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
|
||||
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
|
||||
M6,11V5l5,3L6,11z" />
|
||||
M6,11V5l5,3L6,11z"></path>
|
||||
</svg>YouTube
|
||||
</span>
|
||||
</small>
|
||||
@ -96,10 +108,10 @@
|
||||
<small>
|
||||
<a href="https://vimeo.com/ondemand/viewfromabluemoon4k" target="_blank">View From A Blue Moon</a> on
|
||||
<span class="color--vimeo">
|
||||
<svg class="icon">
|
||||
<svg class="icon" title="Vimeo">
|
||||
<path d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
|
||||
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
|
||||
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z" />
|
||||
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"></path>
|
||||
</svg>Vimeo
|
||||
</span>
|
||||
</small>
|
||||
@ -114,8 +126,7 @@
|
||||
<path d="M16,3c-0.6,0.3-1.2,0.4-1.9,0.5c0.7-0.4,1.2-1,1.4-1.8c-0.6,0.4-1.3,0.6-2.1,0.8c-0.6-0.6-1.5-1-2.4-1
|
||||
C9.3,1.5,7.8,3,7.8,4.8c0,0.3,0,0.5,0.1,0.7C5.2,5.4,2.7,4.1,1.1,2.1c-0.3,0.5-0.4,1-0.4,1.7c0,1.1,0.6,2.1,1.5,2.7
|
||||
c-0.5,0-1-0.2-1.5-0.4c0,0,0,0,0,0c0,1.6,1.1,2.9,2.6,3.2C3,9.4,2.7,9.4,2.4,9.4c-0.2,0-0.4,0-0.6-0.1c0.4,1.3,1.6,2.3,3.1,2.3
|
||||
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"
|
||||
/>
|
||||
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"></path>
|
||||
</svg>
|
||||
<p>If you think others would like Plyr,
|
||||
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&url=http%3A%2F%2Fplyr.io&via=Sam_Potts"
|
||||
@ -123,8 +134,11 @@
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
<!-- Polyfills -->
|
||||
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent"></script>
|
||||
|
||||
<!-- Plyr core script -->
|
||||
<script src="../src/js/plyr.js"></script>
|
||||
<script src="../dist/plyr.js"></script>
|
||||
|
||||
<!-- Sharing libary (https://shr.one) -->
|
||||
<script src="https://cdn.shr.one/1.0.1/shr.js"></script>
|
||||
|
@ -4,9 +4,7 @@
|
||||
// Please see readme.md in the root or github.com/sampotts/plyr
|
||||
// ==========================================================================
|
||||
|
||||
/*global Plyr*/
|
||||
|
||||
(function() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (window.shr) {
|
||||
window.shr.setup({
|
||||
count: {
|
||||
@ -15,12 +13,33 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Setup tab focus
|
||||
const tabClassName = 'tab-focus';
|
||||
|
||||
// Remove class on blur
|
||||
document.addEventListener('focusout', event => {
|
||||
event.target.classList.remove(tabClassName);
|
||||
});
|
||||
|
||||
// Add classname to tabbed elements
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.keyCode !== 9) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay the adding of classname until the focus has changed
|
||||
// This event fires before the focusin event
|
||||
window.setTimeout(() => {
|
||||
document.activeElement.classList.add(tabClassName);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
/* document.body.addEventListener('ready', function(event) {
|
||||
console.log(event);
|
||||
}); */
|
||||
|
||||
// Setup the player
|
||||
var player = new Plyr('#player', {
|
||||
const player = new window.Plyr('#player', {
|
||||
debug: true,
|
||||
title: 'View From A Blue Moon',
|
||||
iconUrl: '../dist/plyr.svg',
|
||||
@ -28,70 +47,36 @@
|
||||
controls: true,
|
||||
},
|
||||
captions: {
|
||||
defaultActive: true,
|
||||
active: true,
|
||||
},
|
||||
controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'fullscreen', 'pip', 'airplay'],
|
||||
controls: [
|
||||
'play-large',
|
||||
'play',
|
||||
'progress',
|
||||
'current-time',
|
||||
'mute',
|
||||
'volume',
|
||||
'captions',
|
||||
'settings',
|
||||
'fullscreen',
|
||||
'pip',
|
||||
'airplay',
|
||||
],
|
||||
});
|
||||
|
||||
// Expose for testing
|
||||
window.player = player;
|
||||
|
||||
// Setup type toggle
|
||||
var buttons = document.querySelectorAll('[data-source]');
|
||||
var types = {
|
||||
const buttons = document.querySelectorAll('[data-source]');
|
||||
const types = {
|
||||
video: 'video',
|
||||
audio: 'audio',
|
||||
youtube: 'youtube',
|
||||
vimeo: 'vimeo',
|
||||
};
|
||||
var currentType = window.location.hash.replace('#', '');
|
||||
var historySupport = window.history && window.history.pushState;
|
||||
|
||||
// Bind to each button
|
||||
[].forEach.call(buttons, function(button) {
|
||||
button.addEventListener('click', function() {
|
||||
var type = this.getAttribute('data-source');
|
||||
|
||||
newSource(type);
|
||||
|
||||
if (historySupport) {
|
||||
history.pushState({ type: type }, '', '#' + type);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// List for backwards/forwards
|
||||
window.addEventListener('popstate', function(event) {
|
||||
if (event.state && 'type' in event.state) {
|
||||
newSource(event.state.type);
|
||||
}
|
||||
});
|
||||
|
||||
// On load
|
||||
if (historySupport) {
|
||||
var video = !currentType.length;
|
||||
|
||||
// If there's no current type set, assume video
|
||||
if (video) {
|
||||
currentType = types.video;
|
||||
}
|
||||
|
||||
// Replace current history state
|
||||
if (currentType in types) {
|
||||
history.replaceState(
|
||||
{
|
||||
type: currentType,
|
||||
},
|
||||
'',
|
||||
video ? '' : '#' + currentType
|
||||
);
|
||||
}
|
||||
|
||||
// If it's not video, load the source
|
||||
if (currentType !== types.video) {
|
||||
newSource(currentType, true);
|
||||
}
|
||||
}
|
||||
let currentType = window.location.hash.replace('#', '');
|
||||
const historySupport = window.history && window.history.pushState;
|
||||
|
||||
// Toggle class on an element
|
||||
function toggleClass(element, className, state) {
|
||||
@ -109,7 +94,7 @@
|
||||
|
||||
switch (type) {
|
||||
case types.video:
|
||||
player.source({
|
||||
player.src = {
|
||||
type: 'video',
|
||||
title: 'View From A Blue Moon',
|
||||
sources: [
|
||||
@ -134,11 +119,12 @@
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case types.audio:
|
||||
player.source({
|
||||
player.src = {
|
||||
type: 'audio',
|
||||
title: 'Kishi Bashi – “It All Began With A Burst”',
|
||||
sources: [
|
||||
@ -151,11 +137,12 @@
|
||||
type: 'audio/ogg',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case types.youtube:
|
||||
player.source({
|
||||
player.src = {
|
||||
type: 'video',
|
||||
title: 'View From A Blue Moon',
|
||||
sources: [
|
||||
@ -164,11 +151,12 @@
|
||||
type: 'youtube',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case types.vimeo:
|
||||
player.source({
|
||||
player.src = {
|
||||
type: 'video',
|
||||
title: 'View From A Blue Moon',
|
||||
sources: [
|
||||
@ -177,7 +165,11 @@
|
||||
type: 'vimeo',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@ -185,23 +177,68 @@
|
||||
currentType = type;
|
||||
|
||||
// Remove active classes
|
||||
for (var x = buttons.length - 1; x >= 0; x--) {
|
||||
toggleClass(buttons[x].parentElement, 'active', false);
|
||||
}
|
||||
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
|
||||
|
||||
// Set active on parent
|
||||
toggleClass(document.querySelector('[data-source="' + type + '"]'), 'active', true);
|
||||
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
|
||||
|
||||
// Show cite
|
||||
[].forEach.call(document.querySelectorAll('.plyr__cite'), function(cite) {
|
||||
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
|
||||
cite.setAttribute('hidden', '');
|
||||
});
|
||||
document.querySelector('.plyr__cite--' + type).removeAttribute('hidden');
|
||||
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
|
||||
}
|
||||
})();
|
||||
|
||||
// Bind to each button
|
||||
Array.from(buttons).forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const type = button.getAttribute('data-source');
|
||||
|
||||
newSource(type);
|
||||
|
||||
if (historySupport) {
|
||||
window.history.pushState({ type }, '', `#${type}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// List for backwards/forwards
|
||||
window.addEventListener('popstate', event => {
|
||||
if (event.state && 'type' in event.state) {
|
||||
newSource(event.state.type);
|
||||
}
|
||||
});
|
||||
|
||||
// On load
|
||||
if (historySupport) {
|
||||
const video = !currentType.length;
|
||||
|
||||
// If there's no current type set, assume video
|
||||
if (video) {
|
||||
currentType = types.video;
|
||||
}
|
||||
|
||||
// Replace current history state
|
||||
if (currentType in types) {
|
||||
window.history.replaceState(
|
||||
{
|
||||
type: currentType,
|
||||
},
|
||||
'',
|
||||
video ? '' : `#${currentType}`
|
||||
);
|
||||
}
|
||||
|
||||
// If it's not video, load the source
|
||||
if (currentType !== types.video) {
|
||||
newSource(currentType, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Google analytics
|
||||
// For demo site (https://plyr.io) only
|
||||
/* eslint-disable */
|
||||
if (window.location.host === 'plyr.io') {
|
||||
(function(i, s, o, g, r, a, m) {
|
||||
i.GoogleAnalyticsObject = r;
|
||||
@ -220,3 +257,4 @@ if (window.location.host === 'plyr.io') {
|
||||
window.ga('create', 'UA-40881672-11', 'auto');
|
||||
window.ga('send', 'pageview');
|
||||
}
|
||||
/* eslint-enable */
|
@ -1,237 +0,0 @@
|
||||
/*
|
||||
* classList.js: Cross-browser full element.classList implementation.
|
||||
* 1.1.20150312
|
||||
*
|
||||
* By Eli Grey, http://eligrey.com
|
||||
* License: Dedicated to the public domain.
|
||||
* See https://github.com/eligrey/classList.js/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/*global self, document, DOMException */
|
||||
|
||||
/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */
|
||||
|
||||
if ("document" in self) {
|
||||
|
||||
// Full polyfill for browsers with no classList support
|
||||
if (!("classList" in document.createElement("_"))) {
|
||||
|
||||
(function (view) {
|
||||
|
||||
"use strict";
|
||||
|
||||
if (!('Element' in view)) return;
|
||||
|
||||
var
|
||||
classListProp = "classList"
|
||||
, protoProp = "prototype"
|
||||
, elemCtrProto = view.Element[protoProp]
|
||||
, objCtr = Object
|
||||
, strTrim = String[protoProp].trim || function () {
|
||||
return this.replace(/^\s+|\s+$/g, "");
|
||||
}
|
||||
, arrIndexOf = Array[protoProp].indexOf || function (item) {
|
||||
var
|
||||
i = 0
|
||||
, len = this.length
|
||||
;
|
||||
for (; i < len; i++) {
|
||||
if (i in this && this[i] === item) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
// Vendors: please allow content code to instantiate DOMExceptions
|
||||
, DOMEx = function (type, message) {
|
||||
this.name = type;
|
||||
this.code = DOMException[type];
|
||||
this.message = message;
|
||||
}
|
||||
, checkTokenAndGetIndex = function (classList, token) {
|
||||
if (token === "") {
|
||||
throw new DOMEx(
|
||||
"SYNTAX_ERR"
|
||||
, "An invalid or illegal string was specified"
|
||||
);
|
||||
}
|
||||
if (/\s/.test(token)) {
|
||||
throw new DOMEx(
|
||||
"INVALID_CHARACTER_ERR"
|
||||
, "String contains an invalid character"
|
||||
);
|
||||
}
|
||||
return arrIndexOf.call(classList, token);
|
||||
}
|
||||
, ClassList = function (elem) {
|
||||
var
|
||||
trimmedClasses = strTrim.call(elem.getAttribute("class") || "")
|
||||
, classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
|
||||
, i = 0
|
||||
, len = classes.length
|
||||
;
|
||||
for (; i < len; i++) {
|
||||
this.push(classes[i]);
|
||||
}
|
||||
this._updateClassName = function () {
|
||||
elem.setAttribute("class", this.toString());
|
||||
};
|
||||
}
|
||||
, classListProto = ClassList[protoProp] = []
|
||||
, classListGetter = function () {
|
||||
return new ClassList(this);
|
||||
}
|
||||
;
|
||||
// Most DOMException implementations don't allow calling DOMException's toString()
|
||||
// on non-DOMExceptions. Error's toString() is sufficient here.
|
||||
DOMEx[protoProp] = Error[protoProp];
|
||||
classListProto.item = function (i) {
|
||||
return this[i] || null;
|
||||
};
|
||||
classListProto.contains = function (token) {
|
||||
token += "";
|
||||
return checkTokenAndGetIndex(this, token) !== -1;
|
||||
};
|
||||
classListProto.add = function () {
|
||||
var
|
||||
tokens = arguments
|
||||
, i = 0
|
||||
, l = tokens.length
|
||||
, token
|
||||
, updated = false
|
||||
;
|
||||
do {
|
||||
token = tokens[i] + "";
|
||||
if (checkTokenAndGetIndex(this, token) === -1) {
|
||||
this.push(token);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
while (++i < l);
|
||||
|
||||
if (updated) {
|
||||
this._updateClassName();
|
||||
}
|
||||
};
|
||||
classListProto.remove = function () {
|
||||
var
|
||||
tokens = arguments
|
||||
, i = 0
|
||||
, l = tokens.length
|
||||
, token
|
||||
, updated = false
|
||||
, index
|
||||
;
|
||||
do {
|
||||
token = tokens[i] + "";
|
||||
index = checkTokenAndGetIndex(this, token);
|
||||
while (index !== -1) {
|
||||
this.splice(index, 1);
|
||||
updated = true;
|
||||
index = checkTokenAndGetIndex(this, token);
|
||||
}
|
||||
}
|
||||
while (++i < l);
|
||||
|
||||
if (updated) {
|
||||
this._updateClassName();
|
||||
}
|
||||
};
|
||||
classListProto.toggle = function (token, force) {
|
||||
token += "";
|
||||
|
||||
var
|
||||
result = this.contains(token)
|
||||
, method = result ?
|
||||
force !== true && "remove"
|
||||
:
|
||||
force !== false && "add"
|
||||
;
|
||||
|
||||
if (method) {
|
||||
this[method](token);
|
||||
}
|
||||
|
||||
if (force === true || force === false) {
|
||||
return force;
|
||||
} else {
|
||||
return !result;
|
||||
}
|
||||
};
|
||||
classListProto.toString = function () {
|
||||
return this.join(" ");
|
||||
};
|
||||
|
||||
if (objCtr.defineProperty) {
|
||||
var classListPropDesc = {
|
||||
get: classListGetter
|
||||
, enumerable: true
|
||||
, configurable: true
|
||||
};
|
||||
try {
|
||||
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
|
||||
} catch (ex) { // IE 8 doesn't support enumerable:true
|
||||
if (ex.number === -0x7FF5EC54) {
|
||||
classListPropDesc.enumerable = false;
|
||||
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
|
||||
}
|
||||
}
|
||||
} else if (objCtr[protoProp].__defineGetter__) {
|
||||
elemCtrProto.__defineGetter__(classListProp, classListGetter);
|
||||
}
|
||||
|
||||
}(self));
|
||||
|
||||
} else {
|
||||
// There is full or partial native classList support, so just check if we need
|
||||
// to normalize the add/remove and toggle APIs.
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var testElement = document.createElement("_");
|
||||
|
||||
testElement.classList.add("c1", "c2");
|
||||
|
||||
// Polyfill for IE 10/11 and Firefox <26, where classList.add and
|
||||
// classList.remove exist but support only one argument at a time.
|
||||
if (!testElement.classList.contains("c2")) {
|
||||
var createMethod = function(method) {
|
||||
var original = DOMTokenList.prototype[method];
|
||||
|
||||
DOMTokenList.prototype[method] = function(token) {
|
||||
var i, len = arguments.length;
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
token = arguments[i];
|
||||
original.call(this, token);
|
||||
}
|
||||
};
|
||||
};
|
||||
createMethod('add');
|
||||
createMethod('remove');
|
||||
}
|
||||
|
||||
testElement.classList.toggle("c3", false);
|
||||
|
||||
// Polyfill for IE 10 and Firefox <24, where classList.toggle does not
|
||||
// support the second argument.
|
||||
if (testElement.classList.contains("c3")) {
|
||||
var _toggle = DOMTokenList.prototype.toggle;
|
||||
|
||||
DOMTokenList.prototype.toggle = function(token, force) {
|
||||
if (1 in arguments && !this.contains(token) === !force) {
|
||||
return force;
|
||||
} else {
|
||||
return _toggle.call(this, token);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
testElement = null;
|
||||
}());
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
// ==========================================================================
|
||||
// tab-focus.js
|
||||
// Detect keyboard tabbing
|
||||
// ==========================================================================
|
||||
|
||||
(function() {
|
||||
var className = 'tab-focus';
|
||||
|
||||
// Remove class on blur
|
||||
document.addEventListener('focusout', function(event) {
|
||||
event.target.classList.remove(className);
|
||||
});
|
||||
|
||||
// Add classname to tabbed elements
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.keyCode !== 9) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay the adding of classname until the focus has changed
|
||||
// This event fires before the focusin event
|
||||
window.setTimeout(function() {
|
||||
document.activeElement.classList.add(className);
|
||||
}, 0);
|
||||
});
|
||||
})();
|
@ -7,6 +7,7 @@
|
||||
fill: currentColor;
|
||||
width: @icon-size;
|
||||
height: @icon-size;
|
||||
vertical-align: -0.15em;
|
||||
}
|
||||
|
||||
// Within elements
|
||||
@ -18,5 +19,5 @@ label svg {
|
||||
|
||||
a .icon,
|
||||
.btn .icon {
|
||||
margin-right: (@spacing-base / 2);
|
||||
margin-right: (@spacing-base / 4);
|
||||
}
|
||||
|
@ -2,12 +2,21 @@
|
||||
// Core
|
||||
// ==========================================================================
|
||||
|
||||
html {
|
||||
background: @page-background;
|
||||
background-attachment: fixed;
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
background: @page-background;
|
||||
background-attachment: fixed;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
2
dist/plyr.css
vendored
2
dist/plyr.css
vendored
File diff suppressed because one or more lines are too long
4
dist/plyr.js
vendored
4
dist/plyr.js
vendored
File diff suppressed because one or more lines are too long
1
dist/plyr.js.map
vendored
Normal file
1
dist/plyr.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
238
gulpfile.js
238
gulpfile.js
@ -4,27 +4,34 @@
|
||||
/* global require, __dirname */
|
||||
/* eslint no-console: "off" */
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var gulp = require('gulp');
|
||||
var gutil = require('gulp-util');
|
||||
var concat = require('gulp-concat');
|
||||
var uglify = require('gulp-uglify');
|
||||
var less = require('gulp-less');
|
||||
var sass = require('gulp-sass');
|
||||
var cleanCSS = require('gulp-clean-css');
|
||||
var run = require('run-sequence');
|
||||
var prefix = require('gulp-autoprefixer');
|
||||
var svgstore = require('gulp-svgstore');
|
||||
var svgmin = require('gulp-svgmin');
|
||||
var rename = require('gulp-rename');
|
||||
var s3 = require('gulp-s3');
|
||||
var replace = require('gulp-replace');
|
||||
var open = require('gulp-open');
|
||||
var size = require('gulp-size');
|
||||
var root = __dirname;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
const gutil = require('gulp-util');
|
||||
const concat = require('gulp-concat');
|
||||
const less = require('gulp-less');
|
||||
const sass = require('gulp-sass');
|
||||
const cleancss = require('gulp-clean-css');
|
||||
const run = require('run-sequence');
|
||||
const prefix = require('gulp-autoprefixer');
|
||||
const svgstore = require('gulp-svgstore');
|
||||
const svgmin = require('gulp-svgmin');
|
||||
const rename = require('gulp-rename');
|
||||
const s3 = require('gulp-s3');
|
||||
const replace = require('gulp-replace');
|
||||
const open = require('gulp-open');
|
||||
const size = require('gulp-size');
|
||||
const rollup = require('gulp-better-rollup');
|
||||
const babel = require('rollup-plugin-babel');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
const uglify = require('rollup-plugin-uglify');
|
||||
const { minify } = require('uglify-es');
|
||||
const commonjs = require('rollup-plugin-commonjs');
|
||||
const resolve = require('rollup-plugin-node-resolve');
|
||||
|
||||
var paths = {
|
||||
const root = __dirname;
|
||||
|
||||
const paths = {
|
||||
plyr: {
|
||||
// Source paths
|
||||
src: {
|
||||
@ -54,82 +61,117 @@ var paths = {
|
||||
};
|
||||
|
||||
// Task arrays
|
||||
var tasks = {
|
||||
const tasks = {
|
||||
less: [],
|
||||
scss: [],
|
||||
js: [],
|
||||
sprite: [],
|
||||
};
|
||||
|
||||
// Fetch bundles from JSON
|
||||
var bundles = loadJSON(path.join(root, 'bundles.json'));
|
||||
var package = loadJSON(path.join(root, 'package.json'));
|
||||
|
||||
// Load json
|
||||
function loadJSON(path) {
|
||||
function loadJSON(pathname) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path));
|
||||
return JSON.parse(fs.readFileSync(pathname));
|
||||
} catch (err) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
var build = {
|
||||
js: function(files, bundle) {
|
||||
Object.keys(files).forEach(function(key) {
|
||||
var name = 'js-' + key;
|
||||
// Fetch bundles from JSON
|
||||
const bundles = loadJSON(path.join(root, 'bundles.json'));
|
||||
const pkg = loadJSON(path.join(root, 'package.json'));
|
||||
const sizeOptions = { showFiles: true, gzip: true };
|
||||
|
||||
// Browserlist
|
||||
const browsers = ['> 1%', 'last 2 versions'];
|
||||
|
||||
// Babel config
|
||||
const babelrc = {
|
||||
presets: [
|
||||
[
|
||||
'env',
|
||||
{
|
||||
targets: {
|
||||
browsers,
|
||||
},
|
||||
useBuiltIns: true,
|
||||
modules: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: ['external-helpers'],
|
||||
babelrc: false,
|
||||
exclude: 'node_modules/**',
|
||||
};
|
||||
|
||||
const build = {
|
||||
js(files, bundle, options) {
|
||||
Object.keys(files).forEach(key => {
|
||||
const name = `js-${key}`;
|
||||
tasks.js.push(name);
|
||||
|
||||
gulp.task(name, function() {
|
||||
return gulp
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(bundles[bundle].js[key])
|
||||
.pipe(concat(key))
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest(paths[bundle].output));
|
||||
});
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(
|
||||
rollup(
|
||||
{
|
||||
plugins: [resolve(), commonjs(), babel(babelrc), uglify({}, minify)],
|
||||
},
|
||||
options
|
||||
)
|
||||
)
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(sourcemaps.write(''))
|
||||
.pipe(gulp.dest(paths[bundle].output))
|
||||
);
|
||||
});
|
||||
},
|
||||
less: function(files, bundle) {
|
||||
Object.keys(files).forEach(function(key) {
|
||||
var name = 'less-' + key;
|
||||
less(files, bundle) {
|
||||
Object.keys(files).forEach(key => {
|
||||
const name = `less-${key}`;
|
||||
tasks.less.push(name);
|
||||
|
||||
gulp.task(name, function() {
|
||||
return gulp
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(bundles[bundle].less[key])
|
||||
.pipe(less())
|
||||
.on('error', gutil.log)
|
||||
.pipe(concat(key))
|
||||
.pipe(prefix(['last 2 versions'], { cascade: false }))
|
||||
.pipe(cleanCSS())
|
||||
.pipe(gulp.dest(paths[bundle].output));
|
||||
});
|
||||
.pipe(prefix(browsers, { cascade: false }))
|
||||
.pipe(cleancss())
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(gulp.dest(paths[bundle].output))
|
||||
);
|
||||
});
|
||||
},
|
||||
scss: function(files, bundle) {
|
||||
Object.keys(files).forEach(function(key) {
|
||||
var name = 'scss-' + key;
|
||||
scss(files, bundle) {
|
||||
Object.keys(files).forEach(key => {
|
||||
const name = `scss-${key}`;
|
||||
tasks.scss.push(name);
|
||||
|
||||
gulp.task(name, function() {
|
||||
return gulp
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(bundles[bundle].scss[key])
|
||||
.pipe(sass())
|
||||
.on('error', gutil.log)
|
||||
.pipe(concat(key))
|
||||
.pipe(prefix(['last 2 versions'], { cascade: false }))
|
||||
.pipe(cleanCSS())
|
||||
.pipe(gulp.dest(paths[bundle].output));
|
||||
});
|
||||
.pipe(prefix(browsers, { cascade: false }))
|
||||
.pipe(cleancss())
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(gulp.dest(paths[bundle].output))
|
||||
);
|
||||
});
|
||||
},
|
||||
sprite: function(bundle) {
|
||||
var name = 'sprite-' + bundle;
|
||||
sprite(bundle) {
|
||||
const name = `sprite-${bundle}`;
|
||||
tasks.sprite.push(name);
|
||||
|
||||
// Process Icons
|
||||
gulp.task(name, function() {
|
||||
return gulp
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(paths[bundle].src.sprite)
|
||||
.pipe(
|
||||
svgmin({
|
||||
@ -142,33 +184,35 @@ var build = {
|
||||
)
|
||||
.pipe(svgstore())
|
||||
.pipe(rename({ basename: bundle }))
|
||||
.pipe(gulp.dest(paths[bundle].output));
|
||||
});
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(gulp.dest(paths[bundle].output))
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// Plyr core files
|
||||
build.js(bundles.plyr.js, 'plyr');
|
||||
build.js(bundles.plyr.js, 'plyr', { name: 'Plyr', format: 'umd' });
|
||||
|
||||
build.less(bundles.plyr.less, 'plyr');
|
||||
build.scss(bundles.plyr.scss, 'plyr');
|
||||
build.sprite('plyr');
|
||||
|
||||
// Demo files
|
||||
build.less(bundles.demo.less, 'demo');
|
||||
build.js(bundles.demo.js, 'demo');
|
||||
build.js(bundles.demo.js, 'demo', { format: 'es' });
|
||||
|
||||
// Build all JS
|
||||
gulp.task('js', function() {
|
||||
gulp.task('js', () => {
|
||||
run(tasks.js);
|
||||
});
|
||||
|
||||
// Build SCSS (for testing, default is LESS)
|
||||
gulp.task('scss', function() {
|
||||
gulp.task('scss', () => {
|
||||
run(tasks.scss);
|
||||
});
|
||||
|
||||
// Watch for file changes
|
||||
gulp.task('watch', function() {
|
||||
gulp.task('watch', () => {
|
||||
// Plyr core
|
||||
gulp.watch(paths.plyr.src.js, tasks.js);
|
||||
gulp.watch(paths.plyr.src.less, tasks.less);
|
||||
@ -180,7 +224,7 @@ gulp.task('watch', function() {
|
||||
});
|
||||
|
||||
// Default gulp task
|
||||
gulp.task('default', function() {
|
||||
gulp.task('default', () => {
|
||||
run(tasks.js, tasks.less, tasks.sprite, 'watch');
|
||||
});
|
||||
|
||||
@ -188,13 +232,13 @@ gulp.task('default', function() {
|
||||
// --------------------------------------------
|
||||
|
||||
// Some options
|
||||
var aws = loadJSON(path.join(root, 'aws.json'));
|
||||
var version = package.version;
|
||||
var maxAge = 31536000; // 1 year
|
||||
var options = {
|
||||
const aws = loadJSON(path.join(root, 'aws.json'));
|
||||
const { version } = pkg;
|
||||
const maxAge = 31536000; // 1 year
|
||||
const options = {
|
||||
cdn: {
|
||||
headers: {
|
||||
'Cache-Control': 'max-age=' + maxAge,
|
||||
'Cache-Control': `max-age=${maxAge}`,
|
||||
Vary: 'Accept-Encoding',
|
||||
},
|
||||
},
|
||||
@ -204,11 +248,11 @@ var options = {
|
||||
Vary: 'Accept-Encoding',
|
||||
},
|
||||
},
|
||||
symlinks: function(version, filename) {
|
||||
symlinks(ver, filename) {
|
||||
return {
|
||||
headers: {
|
||||
// http://stackoverflow.com/questions/2272835/amazon-s3-object-redirect
|
||||
'x-amz-website-redirect-location': '/' + version + '/' + filename,
|
||||
'x-amz-website-redirect-location': `/${ver}/${filename}`,
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
|
||||
},
|
||||
};
|
||||
@ -217,17 +261,16 @@ var options = {
|
||||
|
||||
// If aws is setup
|
||||
if ('cdn' in aws) {
|
||||
var regex =
|
||||
const regex =
|
||||
'(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?';
|
||||
var cdnpath = new RegExp(aws.cdn.domain + '/' + regex, 'gi');
|
||||
var semver = new RegExp('v' + regex, 'gi');
|
||||
var localPath = new RegExp('(../)?dist', 'gi');
|
||||
var versionPath = 'https://' + aws.cdn.domain + '/' + version;
|
||||
}
|
||||
const cdnpath = new RegExp(`${aws.cdn.domain}/${regex}`, 'gi');
|
||||
const semver = new RegExp(`v${regex}`, 'gi');
|
||||
const localPath = new RegExp('(../)?dist', 'gi');
|
||||
const versionPath = `https://${aws.cdn.domain}/${version}`;
|
||||
|
||||
// Publish version to CDN bucket
|
||||
gulp.task('cdn', function() {
|
||||
console.log('Uploading ' + version + ' to ' + aws.cdn.domain + '...');
|
||||
gulp.task('cdn', () => {
|
||||
console.log(`Uploading ${version} to ${aws.cdn.domain}...`);
|
||||
|
||||
// Upload to CDN
|
||||
return gulp
|
||||
@ -239,8 +282,8 @@ gulp.task('cdn', function() {
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
rename(function(path) {
|
||||
path.dirname = path.dirname.replace('.', version);
|
||||
rename(p => {
|
||||
p.dirname = path.dirname.replace('.', version);
|
||||
})
|
||||
)
|
||||
.pipe(replace(localPath, versionPath))
|
||||
@ -248,37 +291,37 @@ gulp.task('cdn', function() {
|
||||
});
|
||||
|
||||
// Publish to demo bucket
|
||||
gulp.task('demo', function() {
|
||||
console.log('Uploading ' + version + ' demo to ' + aws.demo.domain + '...');
|
||||
gulp.task('demo', () => {
|
||||
console.log(`Uploading ${version} demo to ${aws.demo.domain}...`);
|
||||
|
||||
// Replace versioned files in readme.md
|
||||
gulp
|
||||
.src([root + '/readme.md'])
|
||||
.pipe(replace(cdnpath, aws.cdn.domain + '/' + version))
|
||||
.src([`${root}/readme.md`])
|
||||
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}`))
|
||||
.pipe(gulp.dest(root));
|
||||
|
||||
// Replace versioned files in plyr.js
|
||||
gulp
|
||||
.src(path.join(root, 'src/js/plyr.js'))
|
||||
.pipe(replace(semver, 'v' + version))
|
||||
.pipe(replace(cdnpath, aws.cdn.domain + '/' + version))
|
||||
.pipe(replace(semver, `v${version}`))
|
||||
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}`))
|
||||
.pipe(gulp.dest(path.join(root, 'src/js/')));
|
||||
|
||||
// Replace local file paths with remote paths in demo HTML
|
||||
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
|
||||
gulp
|
||||
.src([paths.demo.root + '*.html'])
|
||||
.src([`${paths.demo.root}*.html`])
|
||||
.pipe(replace(localPath, versionPath))
|
||||
.pipe(s3(aws.demo, options.demo));
|
||||
|
||||
// Upload error.html to cdn (as well as demo site)
|
||||
return gulp
|
||||
.src([paths.demo.root + 'error.html'])
|
||||
.src([`${paths.demo.root}error.html`])
|
||||
.pipe(replace(localPath, versionPath))
|
||||
.pipe(s3(aws.cdn, options.demo));
|
||||
});
|
||||
|
||||
// Open the demo site to check it's sweet
|
||||
// Update symlinks for latest
|
||||
/* gulp.task("symlinks", function () {
|
||||
console.log("Updating symlinks...");
|
||||
|
||||
@ -302,20 +345,21 @@ gulp.task('demo', function() {
|
||||
}); */
|
||||
|
||||
// Open the demo site to check it's sweet
|
||||
gulp.task('open', function() {
|
||||
console.log('Opening ' + aws.demo.domain + '...');
|
||||
gulp.task('open', () => {
|
||||
console.log(`Opening ${aws.demo.domain}...`);
|
||||
|
||||
// A file must be specified or gulp will skip the task
|
||||
// Doesn't matter which file since we set the URL above
|
||||
// Weird, I know...
|
||||
return gulp.src([paths.demo.root + 'index.html']).pipe(
|
||||
return gulp.src([`${paths.demo.root}index.html`]).pipe(
|
||||
open('', {
|
||||
url: 'http://' + aws.demo.domain,
|
||||
url: `http://${aws.demo.domain}`,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Do everything
|
||||
gulp.task('publish', function() {
|
||||
gulp.task('publish', () => {
|
||||
run(tasks.js, tasks.less, tasks.sprite, 'cdn', 'demo');
|
||||
});
|
||||
}
|
||||
|
10122
package-lock.json
generated
10122
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@ -4,12 +4,20 @@
|
||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||
"homepage": "http://plyr.io",
|
||||
"main": "src/js/plyr.js",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"eslint": "^4.9.0",
|
||||
"eslint-config-prettier": "^2.6.0",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.0.1",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"eslint": "^4.10.0",
|
||||
"eslint-config-airbnb": "^16.1.0",
|
||||
"eslint-config-prettier": "^2.7.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.2",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-autoprefixer": "^4.0.0",
|
||||
"gulp-better-rollup": "^2.0.0",
|
||||
"gulp-clean-css": "^3.9.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-less": "^3.3.2",
|
||||
@ -19,15 +27,20 @@
|
||||
"gulp-s3": "^0.11.0",
|
||||
"gulp-sass": "^3.1.0",
|
||||
"gulp-size": "^2.1.0",
|
||||
"gulp-sourcemaps": "^2.6.1",
|
||||
"gulp-svgmin": "^1.2.4",
|
||||
"gulp-svgstore": "^6.1.0",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"gulp-util": "^3.0.8",
|
||||
"rollup-plugin-babel": "^3.0.2",
|
||||
"rollup-plugin-commonjs": "^8.2.6",
|
||||
"rollup-plugin-node-resolve": "^3.0.0",
|
||||
"rollup-plugin-uglify": "^2.0.1",
|
||||
"run-sequence": "^2.2.0",
|
||||
"stylelint": "^8.2.0",
|
||||
"stylelint-config-prettier": "^1.0.2",
|
||||
"stylelint-config-standard": "^17.0.0",
|
||||
"through2": "^2.0.3"
|
||||
"through2": "^2.0.3",
|
||||
"uglify-es": "^3.1.6"
|
||||
},
|
||||
"keywords": [
|
||||
"HTML5 Video",
|
||||
|
212
src/js/captions.js
Normal file
212
src/js/captions.js
Normal file
@ -0,0 +1,212 @@
|
||||
// ==========================================================================
|
||||
// Plyr Captions
|
||||
// ==========================================================================
|
||||
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
import controls from './controls';
|
||||
|
||||
const captions = {
|
||||
// Setup captions
|
||||
setup() {
|
||||
// Requires UI support
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set default language if not set
|
||||
if (!utils.is.empty(this.storage.language)) {
|
||||
this.captions.language = this.storage.language;
|
||||
} else if (utils.is.empty(this.captions.language)) {
|
||||
this.captions.language = this.config.captions.language.toLowerCase();
|
||||
}
|
||||
|
||||
// Set captions enabled state if not set
|
||||
if (!utils.is.boolean(this.captions.enabled)) {
|
||||
if (!utils.is.empty(this.storage.language)) {
|
||||
this.captions.enabled = this.storage.captions;
|
||||
} else {
|
||||
this.captions.enabled = this.config.captions.active;
|
||||
}
|
||||
}
|
||||
|
||||
// Only Vimeo and HTML5 video supported at this point
|
||||
if (!['video', 'vimeo'].includes(this.type) || (this.type === 'video' && !support.textTracks)) {
|
||||
this.captions.tracks = null;
|
||||
|
||||
// Clear menu and hide
|
||||
if (this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
|
||||
controls.setCaptionsMenu.call(this);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject the container
|
||||
if (!utils.is.htmlElement(this.elements.captions)) {
|
||||
this.elements.captions = utils.createElement(
|
||||
'div',
|
||||
utils.getAttributesFromSelector(this.config.selectors.captions)
|
||||
);
|
||||
utils.insertAfter(this.elements.captions, this.elements.wrapper);
|
||||
}
|
||||
|
||||
// Get tracks from HTML5
|
||||
if (this.type === 'video') {
|
||||
this.captions.tracks = this.media.textTracks;
|
||||
}
|
||||
|
||||
// Set the class hook
|
||||
utils.toggleClass(
|
||||
this.elements.container,
|
||||
this.config.classNames.captions.enabled,
|
||||
!utils.is.empty(this.captions.tracks)
|
||||
);
|
||||
|
||||
// If no caption file exists, hide container for caption text
|
||||
if (utils.is.empty(this.captions.tracks)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable UI
|
||||
captions.show.call(this);
|
||||
|
||||
// Get a track
|
||||
const setCurrentTrack = () => {
|
||||
// Reset by default
|
||||
this.captions.currentTrack = null;
|
||||
|
||||
// Filter doesn't seem to work for a TextTrackList :-(
|
||||
Array.from(this.captions.tracks).forEach(track => {
|
||||
if (track.language === this.captions.language.toLowerCase()) {
|
||||
this.captions.currentTrack = track;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Get current track
|
||||
setCurrentTrack();
|
||||
|
||||
// If we couldn't get the requested language, revert to default
|
||||
if (!utils.is.track(this.captions.currentTrack)) {
|
||||
const { language } = this.config.captions;
|
||||
|
||||
// Reset to default
|
||||
// We don't update user storage as the selected language could become available
|
||||
this.captions.language = language;
|
||||
|
||||
// Get fallback track
|
||||
setCurrentTrack();
|
||||
|
||||
// If no match, disable captions
|
||||
if (!utils.is.track(this.captions.currentTrack)) {
|
||||
this.toggleCaptions(false);
|
||||
}
|
||||
|
||||
controls.updateSetting.call(this, 'captions');
|
||||
}
|
||||
|
||||
// Setup HTML5 track rendering
|
||||
if (this.type === 'video') {
|
||||
// Turn off native caption rendering to avoid double captions
|
||||
Array.from(this.captions.tracks).forEach(track => {
|
||||
// Remove previous bindings (if we've changed source or language)
|
||||
utils.off(track, 'cuechange', event => captions.setCue.call(this, event));
|
||||
|
||||
// Hide captions
|
||||
track.mode = 'hidden';
|
||||
});
|
||||
|
||||
// Check if suported kind
|
||||
const supported =
|
||||
this.captions.currentTrack && ['captions', 'subtitles'].includes(this.captions.currentTrack.kind);
|
||||
|
||||
if (utils.is.track(this.captions.currentTrack) && supported) {
|
||||
utils.on(this.captions.currentTrack, 'cuechange', event => captions.setCue.call(this, event));
|
||||
|
||||
// If we change the active track while a cue is already displayed we need to update it
|
||||
if (this.captions.currentTrack.activeCues && this.captions.currentTrack.activeCues.length > 0) {
|
||||
controls.setCue.call(this, this.captions.currentTrack);
|
||||
}
|
||||
}
|
||||
} else if (this.type === 'vimeo' && this.captions.active) {
|
||||
this.embed.enableTextTrack(this.captions.language);
|
||||
}
|
||||
|
||||
// Set available languages in list
|
||||
if (this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
|
||||
controls.setCaptionsMenu.call(this);
|
||||
}
|
||||
},
|
||||
|
||||
// Display active caption if it contains text
|
||||
setCue(input) {
|
||||
// Get the track from the event if needed
|
||||
const track = utils.is.event(input) ? input.target : input;
|
||||
const active = track.activeCues[0];
|
||||
|
||||
// Display a cue, if there is one
|
||||
if (utils.is.cue(active)) {
|
||||
captions.set.call(this, active.getCueAsHTML());
|
||||
} else {
|
||||
captions.set.call(this);
|
||||
}
|
||||
|
||||
utils.dispatchEvent.call(this, this.media, 'cuechange');
|
||||
},
|
||||
|
||||
// Set the current caption
|
||||
set(input) {
|
||||
// Requires UI
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils.is.htmlElement(this.elements.captions)) {
|
||||
const content = utils.createElement('span');
|
||||
|
||||
// Empty the container
|
||||
utils.emptyElement(this.elements.captions);
|
||||
|
||||
// Default to empty
|
||||
const caption = !utils.is.undefined(input) ? input : '';
|
||||
|
||||
// Set the span content
|
||||
if (utils.is.string(caption)) {
|
||||
content.textContent = caption.trim();
|
||||
} else {
|
||||
content.appendChild(caption);
|
||||
}
|
||||
|
||||
// Set new caption text
|
||||
this.elements.captions.appendChild(content);
|
||||
} else {
|
||||
this.warn('No captions element to render to');
|
||||
}
|
||||
},
|
||||
|
||||
// Display captions container and button (for initialization)
|
||||
show() {
|
||||
// If there's no caption toggle, bail
|
||||
if (!this.elements.buttons.captions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to load the value from storage
|
||||
let active = this.storage.captions;
|
||||
|
||||
// Otherwise fall back to the default config
|
||||
if (!utils.is.boolean(active)) {
|
||||
({ active } = this.captions);
|
||||
} else {
|
||||
this.captions.active = active;
|
||||
}
|
||||
|
||||
if (active) {
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true);
|
||||
utils.toggleState(this.elements.buttons.captions, true);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default captions;
|
1177
src/js/controls.js
vendored
Normal file
1177
src/js/controls.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
301
src/js/defaults.js
Normal file
301
src/js/defaults.js
Normal file
@ -0,0 +1,301 @@
|
||||
// Default config
|
||||
const defaults = {
|
||||
// Disable
|
||||
enabled: true,
|
||||
|
||||
// Custom media title
|
||||
title: '',
|
||||
|
||||
// Logging to console
|
||||
debug: false,
|
||||
|
||||
// Auto play (if supported)
|
||||
autoplay: false,
|
||||
|
||||
// Default time to skip when rewind/fast forward
|
||||
seekTime: 10,
|
||||
|
||||
// Default volume
|
||||
volume: 1,
|
||||
muted: false,
|
||||
|
||||
// Display the media duration
|
||||
displayDuration: true,
|
||||
|
||||
// Click video to play
|
||||
clickToPlay: true,
|
||||
|
||||
// Auto hide the controls
|
||||
hideControls: true,
|
||||
|
||||
// Revert to poster on finish (HTML5 - will cause reload)
|
||||
showPosterOnEnd: false,
|
||||
|
||||
// Disable the standard context menu
|
||||
disableContextMenu: true,
|
||||
|
||||
// Sprite (for icons)
|
||||
loadSprite: true,
|
||||
iconPrefix: 'plyr',
|
||||
iconUrl: 'https://cdn.plyr.io/2.0.10/plyr.svg',
|
||||
|
||||
// Blank video (used to prevent errors on source change)
|
||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||
|
||||
// Pass a custom duration
|
||||
duration: null,
|
||||
|
||||
// Quality default
|
||||
quality: {
|
||||
default: 'default',
|
||||
options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'],
|
||||
},
|
||||
|
||||
// Set loops
|
||||
loop: {
|
||||
active: false,
|
||||
start: null,
|
||||
end: null,
|
||||
},
|
||||
|
||||
// Speed default and options to display
|
||||
speed: {
|
||||
default: 1,
|
||||
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
|
||||
},
|
||||
|
||||
// Keyboard shortcut settings
|
||||
keyboard: {
|
||||
focused: true,
|
||||
global: false,
|
||||
},
|
||||
|
||||
// Display tooltips
|
||||
tooltips: {
|
||||
controls: false,
|
||||
seek: true,
|
||||
},
|
||||
|
||||
// Captions settings
|
||||
captions: {
|
||||
active: false,
|
||||
language: window.navigator.language.split('-')[0],
|
||||
},
|
||||
|
||||
// Fullscreen settings
|
||||
fullscreen: {
|
||||
enabled: true, // Allow fullscreen?
|
||||
fallback: true, // Fallback for vintage browsers
|
||||
},
|
||||
|
||||
// Local storage
|
||||
storage: {
|
||||
enabled: true,
|
||||
key: 'plyr',
|
||||
},
|
||||
|
||||
// Default controls
|
||||
controls: [
|
||||
'play-large',
|
||||
'play',
|
||||
'progress',
|
||||
'current-time',
|
||||
'mute',
|
||||
'volume',
|
||||
'captions',
|
||||
'settings',
|
||||
'pip',
|
||||
'airplay',
|
||||
'fullscreen',
|
||||
],
|
||||
settings: ['captions', 'quality', 'speed', 'loop'],
|
||||
|
||||
// Localisation
|
||||
i18n: {
|
||||
restart: 'Restart',
|
||||
rewind: 'Rewind {seektime} secs',
|
||||
play: 'Play',
|
||||
pause: 'Pause',
|
||||
forward: 'Forward {seektime} secs',
|
||||
seek: 'Seek',
|
||||
played: 'Played',
|
||||
buffered: 'Buffered',
|
||||
currentTime: 'Current time',
|
||||
duration: 'Duration',
|
||||
volume: 'Volume',
|
||||
toggleMute: 'Toggle Mute',
|
||||
toggleCaptions: 'Toggle Captions',
|
||||
toggleFullscreen: 'Toggle Fullscreen',
|
||||
frameTitle: 'Player for {title}',
|
||||
captions: 'Captions',
|
||||
settings: 'Settings',
|
||||
speed: 'Speed',
|
||||
quality: 'Quality',
|
||||
loop: 'Loop',
|
||||
start: 'Start',
|
||||
end: 'End',
|
||||
all: 'All',
|
||||
reset: 'Reset',
|
||||
none: 'None',
|
||||
disabled: 'Disabled',
|
||||
},
|
||||
|
||||
// URLs
|
||||
urls: {
|
||||
vimeo: {
|
||||
api: 'https://player.vimeo.com/api/player.js',
|
||||
},
|
||||
youtube: {
|
||||
api: 'https://www.youtube.com/iframe_api',
|
||||
},
|
||||
},
|
||||
|
||||
// Custom control listeners
|
||||
listeners: {
|
||||
seek: null,
|
||||
play: null,
|
||||
pause: null,
|
||||
restart: null,
|
||||
rewind: null,
|
||||
forward: null,
|
||||
mute: null,
|
||||
volume: null,
|
||||
captions: null,
|
||||
fullscreen: null,
|
||||
pip: null,
|
||||
airplay: null,
|
||||
speed: null,
|
||||
quality: null,
|
||||
loop: null,
|
||||
language: null,
|
||||
},
|
||||
|
||||
// Events to watch and bubble
|
||||
events: [
|
||||
// Events to watch on HTML5 media elements and bubble
|
||||
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
|
||||
'ended',
|
||||
'progress',
|
||||
'stalled',
|
||||
'playing',
|
||||
'waiting',
|
||||
'canplay',
|
||||
'canplaythrough',
|
||||
'loadstart',
|
||||
'loadeddata',
|
||||
'loadedmetadata',
|
||||
'timeupdate',
|
||||
'volumechange',
|
||||
'play',
|
||||
'pause',
|
||||
'error',
|
||||
'seeking',
|
||||
'seeked',
|
||||
'emptied',
|
||||
'ratechange',
|
||||
'cuechange',
|
||||
|
||||
// Custom events
|
||||
'enterfullscreen',
|
||||
'exitfullscreen',
|
||||
'captionsenabled',
|
||||
'captionsdisabled',
|
||||
'captionchange',
|
||||
'controlshidden',
|
||||
'controlsshown',
|
||||
'ready',
|
||||
|
||||
// YouTube
|
||||
'statechange',
|
||||
'qualitychange',
|
||||
'qualityrequested',
|
||||
],
|
||||
|
||||
// Selectors
|
||||
// Change these to match your template if using custom HTML
|
||||
selectors: {
|
||||
editable: 'input, textarea, select, [contenteditable]',
|
||||
container: '.plyr',
|
||||
controls: {
|
||||
container: null,
|
||||
wrapper: '.plyr__controls',
|
||||
},
|
||||
labels: '[data-plyr]',
|
||||
buttons: {
|
||||
play: '[data-plyr="play"]',
|
||||
pause: '[data-plyr="pause"]',
|
||||
restart: '[data-plyr="restart"]',
|
||||
rewind: '[data-plyr="rewind"]',
|
||||
forward: '[data-plyr="fast-forward"]',
|
||||
mute: '[data-plyr="mute"]',
|
||||
captions: '[data-plyr="captions"]',
|
||||
fullscreen: '[data-plyr="fullscreen"]',
|
||||
pip: '[data-plyr="pip"]',
|
||||
airplay: '[data-plyr="airplay"]',
|
||||
settings: '[data-plyr="settings"]',
|
||||
loop: '[data-plyr="loop"]',
|
||||
},
|
||||
inputs: {
|
||||
seek: '[data-plyr="seek"]',
|
||||
volume: '[data-plyr="volume"]',
|
||||
speed: '[data-plyr="speed"]',
|
||||
language: '[data-plyr="language"]',
|
||||
quality: '[data-plyr="quality"]',
|
||||
},
|
||||
display: {
|
||||
currentTime: '.plyr__time--current',
|
||||
duration: '.plyr__time--duration',
|
||||
buffer: '.plyr__progress--buffer',
|
||||
played: '.plyr__progress--played',
|
||||
loop: '.plyr__progress--loop',
|
||||
volume: '.plyr__volume--display',
|
||||
},
|
||||
progress: '.plyr__progress',
|
||||
captions: '.plyr__captions',
|
||||
menu: {
|
||||
quality: '.js-plyr__menu__list--quality',
|
||||
},
|
||||
},
|
||||
|
||||
// Class hooks added to the player in different states
|
||||
classNames: {
|
||||
video: 'plyr__video-wrapper',
|
||||
embed: 'plyr__video-embed',
|
||||
control: 'plyr__control',
|
||||
type: 'plyr--{0}',
|
||||
stopped: 'plyr--stopped',
|
||||
playing: 'plyr--playing',
|
||||
muted: 'plyr--muted',
|
||||
loading: 'plyr--loading',
|
||||
hover: 'plyr--hover',
|
||||
tooltip: 'plyr__tooltip',
|
||||
hidden: 'plyr__sr-only',
|
||||
hideControls: 'plyr--hide-controls',
|
||||
isIos: 'plyr--is-ios',
|
||||
isTouch: 'plyr--is-touch',
|
||||
uiSupported: 'plyr--full-ui',
|
||||
menu: {
|
||||
value: 'plyr__menu__value',
|
||||
badge: 'plyr__badge',
|
||||
},
|
||||
captions: {
|
||||
enabled: 'plyr--captions-enabled',
|
||||
active: 'plyr--captions-active',
|
||||
},
|
||||
fullscreen: {
|
||||
enabled: 'plyr--fullscreen-enabled',
|
||||
fallback: 'plyr--fullscreen-fallback',
|
||||
},
|
||||
pip: {
|
||||
supported: 'plyr--pip-supported',
|
||||
active: 'plyr--pip-active',
|
||||
},
|
||||
airplay: {
|
||||
supported: 'plyr--airplay-supported',
|
||||
active: 'plyr--airplay-active',
|
||||
},
|
||||
tabFocus: 'tab-focus',
|
||||
},
|
||||
};
|
||||
|
||||
export default defaults;
|
129
src/js/fullscreen.js
Normal file
129
src/js/fullscreen.js
Normal file
@ -0,0 +1,129 @@
|
||||
// ==========================================================================
|
||||
// Plyr fullscreen API
|
||||
// ==========================================================================
|
||||
|
||||
import utils from './utils';
|
||||
|
||||
// Determine the prefix
|
||||
const prefix = (() => {
|
||||
let value = false;
|
||||
|
||||
if (utils.is.function(document.cancelFullScreen)) {
|
||||
value = '';
|
||||
} else {
|
||||
// Check for fullscreen support by vendor prefix
|
||||
['webkit', 'o', 'moz', 'ms', 'khtml'].some(pre => {
|
||||
if (utils.is.function(document[`${pre}CancelFullScreen`])) {
|
||||
value = pre;
|
||||
return true;
|
||||
} else if (utils.is.function(document.msExitFullscreen) && document.msFullscreenEnabled) {
|
||||
// Special case for MS (when isn't it?)
|
||||
value = 'ms';
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
})();
|
||||
|
||||
// Fullscreen API
|
||||
const fullscreen = {
|
||||
// Get the prefix
|
||||
prefix,
|
||||
|
||||
// Check if we can use it
|
||||
enabled:
|
||||
document.fullscreenEnabled ||
|
||||
document.webkitFullscreenEnabled ||
|
||||
document.mozFullScreenEnabled ||
|
||||
document.msFullscreenEnabled,
|
||||
|
||||
// Yet again Microsoft awesomeness,
|
||||
// Sometimes the prefix is 'ms', sometimes 'MS' to keep you on your toes
|
||||
eventType: prefix === 'ms' ? 'MSFullscreenChange' : `${prefix}fullscreenchange`,
|
||||
|
||||
// Is an element fullscreen
|
||||
isFullScreen(element) {
|
||||
if (!fullscreen.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = utils.is.undefined(element) ? document.body : element;
|
||||
|
||||
switch (prefix) {
|
||||
case '':
|
||||
return document.fullscreenElement === target;
|
||||
|
||||
case 'moz':
|
||||
return document.mozFullScreenElement === target;
|
||||
|
||||
default:
|
||||
return document[`${prefix}FullscreenElement`] === target;
|
||||
}
|
||||
},
|
||||
|
||||
// Make an element fullscreen
|
||||
requestFullScreen(element) {
|
||||
if (!fullscreen.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = utils.is.undefined(element) ? document.body : element;
|
||||
|
||||
return !prefix.length
|
||||
? target.requestFullScreen()
|
||||
: target[prefix + (prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')]();
|
||||
},
|
||||
|
||||
// Bail from fullscreen
|
||||
cancelFullScreen() {
|
||||
if (!fullscreen.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !prefix.length
|
||||
? document.cancelFullScreen()
|
||||
: document[prefix + (prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')]();
|
||||
},
|
||||
|
||||
// Get the current element
|
||||
element() {
|
||||
if (!fullscreen.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return !prefix.length ? document.fullscreenElement : document[`${prefix}FullscreenElement`];
|
||||
},
|
||||
|
||||
// Setup fullscreen
|
||||
setup() {
|
||||
if (!this.supported.ui || this.type === 'audio' || !this.config.fullscreen.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for native support
|
||||
const nativeSupport = fullscreen.enabled;
|
||||
|
||||
if (nativeSupport || (this.config.fullscreen.fallback && !utils.inFrame())) {
|
||||
this.log(`${nativeSupport ? 'Native' : 'Fallback'} fullscreen enabled`);
|
||||
|
||||
// Add styling hook to show button
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.fullscreen.enabled, true);
|
||||
} else {
|
||||
this.log('Fullscreen not supported and fallback disabled');
|
||||
}
|
||||
|
||||
// Toggle state
|
||||
if (this.elements.buttons && this.elements.buttons.fullscreen) {
|
||||
utils.toggleState(this.elements.buttons.fullscreen, false);
|
||||
}
|
||||
|
||||
// Trap focus in container
|
||||
utils.trapFocus.call(this);
|
||||
},
|
||||
};
|
||||
|
||||
export default fullscreen;
|
569
src/js/listeners.js
Normal file
569
src/js/listeners.js
Normal file
@ -0,0 +1,569 @@
|
||||
// ==========================================================================
|
||||
// Plyr Event Listeners
|
||||
// ==========================================================================
|
||||
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
import controls from './controls';
|
||||
import fullscreen from './fullscreen';
|
||||
import storage from './storage';
|
||||
import ui from './ui';
|
||||
|
||||
const listeners = {
|
||||
// Listen for media events
|
||||
media() {
|
||||
// Time change on media
|
||||
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));
|
||||
|
||||
// Handle the media finishing
|
||||
utils.on(this.media, 'ended', () => {
|
||||
// Show poster on end
|
||||
if (this.type === 'video' && this.config.showPosterOnEnd) {
|
||||
// Restart
|
||||
this.restart();
|
||||
|
||||
// Re-load media
|
||||
this.media.load();
|
||||
}
|
||||
});
|
||||
|
||||
// Check for buffer progress
|
||||
utils.on(this.media, 'progress playing', event => ui.updateProgress.call(this, event));
|
||||
|
||||
// Handle native mute
|
||||
utils.on(this.media, 'volumechange', event => ui.updateVolume.call(this, event));
|
||||
|
||||
// Handle native play/pause
|
||||
utils.on(this.media, 'play pause ended', event => ui.checkPlaying.call(this, event));
|
||||
|
||||
// Loading
|
||||
utils.on(this.media, 'waiting canplay seeked', event => ui.checkLoading.call(this, event));
|
||||
|
||||
// Click video
|
||||
if (this.supported.ui && this.config.clickToPlay && this.type !== 'audio') {
|
||||
// Re-fetch the wrapper
|
||||
const wrapper = utils.getElement.call(this, `.${this.config.classNames.video}`);
|
||||
|
||||
// Bail if there's no wrapper (this should never happen)
|
||||
if (!wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set cursor
|
||||
wrapper.style.cursor = 'pointer';
|
||||
|
||||
// On click play, pause ore restart
|
||||
utils.on(wrapper, 'click', () => {
|
||||
// Touch devices will just show controls (if we're hiding controls)
|
||||
if (this.config.hideControls && support.touch && !this.media.paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.media.paused) {
|
||||
this.play();
|
||||
} else if (this.media.ended) {
|
||||
this.restart();
|
||||
this.play();
|
||||
} else {
|
||||
this.pause();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Disable right click
|
||||
if (this.config.disableContextMenu) {
|
||||
utils.on(
|
||||
this.media,
|
||||
'contextmenu',
|
||||
event => {
|
||||
event.preventDefault();
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Speed change
|
||||
utils.on(this.media, 'ratechange', () => {
|
||||
// Update UI
|
||||
controls.updateSetting.call(this, 'speed');
|
||||
|
||||
// Save speed to localStorage
|
||||
storage.set.call(this, {
|
||||
speed: this.speed,
|
||||
});
|
||||
});
|
||||
|
||||
// Quality change
|
||||
utils.on(this.media, 'qualitychange', () => {
|
||||
// Update UI
|
||||
controls.updateSetting.call(this, 'quality');
|
||||
|
||||
// Save speed to localStorage
|
||||
storage.set.call(this, {
|
||||
quality: this.quality,
|
||||
});
|
||||
});
|
||||
|
||||
// Caption language change
|
||||
utils.on(this.media, 'captionchange', () => {
|
||||
// Save speed to localStorage
|
||||
storage.set.call(this, {
|
||||
language: this.captions.language,
|
||||
});
|
||||
});
|
||||
|
||||
// Captions toggle
|
||||
utils.on(this.media, 'captionsenabled captionsdisabled', () => {
|
||||
// Update UI
|
||||
controls.updateSetting.call(this, 'captions');
|
||||
|
||||
// Save speed to localStorage
|
||||
storage.set.call(this, {
|
||||
captions: this.captions.enabled,
|
||||
});
|
||||
});
|
||||
|
||||
// Proxy events to container
|
||||
// Bubble up key events for Edge
|
||||
utils.on(this.media, this.config.events.concat(['keyup', 'keydown']).join(' '), event => {
|
||||
utils.dispatchEvent.call(this, this.elements.container, event.type, true);
|
||||
});
|
||||
},
|
||||
|
||||
// Listen for control events
|
||||
controls() {
|
||||
// IE doesn't support input event, so we fallback to change
|
||||
const inputEvent = this.browser.isIE ? 'change' : 'input';
|
||||
let last = null;
|
||||
|
||||
// Click play/pause helper
|
||||
const togglePlay = () => {
|
||||
const play = this.togglePlay();
|
||||
|
||||
// Determine which buttons
|
||||
const target = this.elements.buttons[play ? 'pause' : 'play'];
|
||||
|
||||
// Transfer focus
|
||||
if (utils.is.htmlElement(target)) {
|
||||
target.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Get the key code for an event
|
||||
function getKeyCode(event) {
|
||||
return event.keyCode ? event.keyCode : event.which;
|
||||
}
|
||||
|
||||
function handleKey(event) {
|
||||
const code = getKeyCode(event);
|
||||
const pressed = event.type === 'keydown';
|
||||
const held = pressed && code === last;
|
||||
|
||||
// If the event is bubbled from the media element
|
||||
// Firefox doesn't get the keycode for whatever reason
|
||||
if (!utils.is.number(code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Seek by the number keys
|
||||
function seekByKey() {
|
||||
// Divide the max duration into 10th's and times by the number value
|
||||
this.currentTime = this.duration / 10 * (code - 48);
|
||||
}
|
||||
|
||||
// Handle the key on keydown
|
||||
// Reset on keyup
|
||||
if (pressed) {
|
||||
// Which keycodes should we prevent default
|
||||
const preventDefault = [
|
||||
48,
|
||||
49,
|
||||
50,
|
||||
51,
|
||||
52,
|
||||
53,
|
||||
54,
|
||||
56,
|
||||
57,
|
||||
32,
|
||||
75,
|
||||
38,
|
||||
40,
|
||||
77,
|
||||
39,
|
||||
37,
|
||||
70,
|
||||
67,
|
||||
73,
|
||||
76,
|
||||
79,
|
||||
];
|
||||
const checkFocus = [38, 40];
|
||||
|
||||
if (checkFocus.includes(code)) {
|
||||
const focused = utils.getFocusElement();
|
||||
|
||||
if (utils.is.htmlElement(focused) && utils.getFocusElement().type === 'radio') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If the code is found prevent default (e.g. prevent scrolling for arrows)
|
||||
if (preventDefault.includes(code)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
switch (code) {
|
||||
case 48:
|
||||
case 49:
|
||||
case 50:
|
||||
case 51:
|
||||
case 52:
|
||||
case 53:
|
||||
case 54:
|
||||
case 55:
|
||||
case 56:
|
||||
case 57:
|
||||
// 0-9
|
||||
if (!held) {
|
||||
seekByKey();
|
||||
}
|
||||
break;
|
||||
|
||||
case 32:
|
||||
case 75:
|
||||
// Space and K key
|
||||
if (!held) {
|
||||
togglePlay();
|
||||
}
|
||||
break;
|
||||
|
||||
case 38:
|
||||
// Arrow up
|
||||
this.increaseVolume(0.1);
|
||||
break;
|
||||
|
||||
case 40:
|
||||
// Arrow down
|
||||
this.decreaseVolume(0.1);
|
||||
break;
|
||||
|
||||
case 77:
|
||||
// M key
|
||||
if (!held) {
|
||||
this.toggleMute();
|
||||
}
|
||||
break;
|
||||
|
||||
case 39:
|
||||
// Arrow forward
|
||||
this.forward();
|
||||
break;
|
||||
|
||||
case 37:
|
||||
// Arrow back
|
||||
this.rewind();
|
||||
break;
|
||||
|
||||
case 70:
|
||||
// F key
|
||||
this.toggleFullscreen();
|
||||
break;
|
||||
|
||||
case 67:
|
||||
// C key
|
||||
if (!held) {
|
||||
this.toggleCaptions();
|
||||
}
|
||||
break;
|
||||
|
||||
case 73:
|
||||
this.setLoop('start');
|
||||
break;
|
||||
|
||||
case 76:
|
||||
this.setLoop();
|
||||
break;
|
||||
|
||||
case 79:
|
||||
this.setLoop('end');
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Escape is handle natively when in full screen
|
||||
// So we only need to worry about non native
|
||||
if (!fullscreen.enabled && this.fullscreen.active && code === 27) {
|
||||
this.toggleFullscreen();
|
||||
}
|
||||
|
||||
// Store last code for next cycle
|
||||
last = code;
|
||||
} else {
|
||||
last = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
if (this.config.keyboard.focused) {
|
||||
// Handle global presses
|
||||
if (this.config.keyboard.global) {
|
||||
utils.on(
|
||||
window,
|
||||
'keydown keyup',
|
||||
event => {
|
||||
const code = getKeyCode(event);
|
||||
const focused = utils.getFocusElement();
|
||||
const allowed = [48, 49, 50, 51, 52, 53, 54, 56, 57, 75, 77, 70, 67, 73, 76, 79];
|
||||
|
||||
// Only handle global key press if key is in the allowed keys
|
||||
// and if the focused element is not editable (e.g. text input)
|
||||
// and any that accept key input http://webaim.org/techniques/keyboard/
|
||||
if (
|
||||
allowed.includes(code) &&
|
||||
(!utils.is.htmlElement(focused) || !utils.matches(focused, this.config.selectors.editable))
|
||||
) {
|
||||
handleKey(event);
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Handle presses on focused
|
||||
utils.on(this.elements.container, 'keydown keyup', handleKey, false);
|
||||
}
|
||||
|
||||
// Detect tab focus
|
||||
// Remove class on blur/focusout
|
||||
utils.on(this.elements.container, 'focusout', event => {
|
||||
utils.toggleClass(event.target, this.config.classNames.tabFocus, false);
|
||||
});
|
||||
|
||||
// Add classname to tabbed elements
|
||||
utils.on(this.elements.container, 'keydown', event => {
|
||||
if (event.keyCode !== 9) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay the adding of classname until the focus has changed
|
||||
// This event fires before the focusin event
|
||||
window.setTimeout(() => {
|
||||
utils.toggleClass(utils.getFocusElement(), this.config.classNames.tabFocus, true);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Trigger custom and default handlers
|
||||
const handlerProxy = (event, customHandler, defaultHandler) => {
|
||||
if (utils.is.function(customHandler)) {
|
||||
customHandler.call(this, event);
|
||||
}
|
||||
if (utils.is.function(defaultHandler)) {
|
||||
defaultHandler.call(this, event);
|
||||
}
|
||||
};
|
||||
|
||||
// Play
|
||||
utils.proxy(this.elements.buttons.play, 'click', this.config.listeners.play, togglePlay);
|
||||
utils.proxy(this.elements.buttons.playLarge, 'click', this.config.listeners.play, togglePlay);
|
||||
|
||||
// Pause
|
||||
utils.proxy(this.elements.buttons.pause, 'click', this.config.listeners.pause, togglePlay);
|
||||
|
||||
// Pause
|
||||
utils.proxy(this.elements.buttons.restart, 'click', this.config.listeners.restart, () => {
|
||||
this.restart();
|
||||
});
|
||||
|
||||
// Rewind
|
||||
utils.proxy(this.elements.buttons.rewind, 'click', this.config.listeners.rewind, () => {
|
||||
this.rewind();
|
||||
});
|
||||
|
||||
// Rewind
|
||||
utils.proxy(this.elements.buttons.forward, 'click', this.config.listeners.forward, () => {
|
||||
this.forward();
|
||||
});
|
||||
|
||||
// Mute
|
||||
utils.proxy(this.elements.buttons.mute, 'click', this.config.listeners.mute, () => {
|
||||
this.toggleMute();
|
||||
});
|
||||
|
||||
// Captions
|
||||
utils.proxy(this.elements.buttons.captions, 'click', this.config.listeners.captions, () => {
|
||||
this.toggleCaptions();
|
||||
});
|
||||
|
||||
// Fullscreen
|
||||
utils.proxy(this.elements.buttons.fullscreen, 'click', this.config.listeners.fullscreen, () => {
|
||||
this.toggleFullscreen();
|
||||
});
|
||||
|
||||
// Picture-in-Picture
|
||||
utils.proxy(this.elements.buttons.pip, 'click', this.config.listeners.pip, () => {
|
||||
this.togglePictureInPicture();
|
||||
});
|
||||
|
||||
// Airplay
|
||||
utils.proxy(this.elements.buttons.airplay, 'click', this.config.listeners.airplay, () => {
|
||||
this.airPlay();
|
||||
});
|
||||
|
||||
// Settings menu
|
||||
utils.on(this.elements.buttons.settings, 'click', event => {
|
||||
controls.toggleMenu.call(this, event);
|
||||
});
|
||||
|
||||
// Click anywhere closes menu
|
||||
utils.on(document.documentElement, 'click', event => {
|
||||
controls.toggleMenu.call(this, event);
|
||||
});
|
||||
|
||||
// Settings menu
|
||||
utils.on(this.elements.settings.form, 'click', event => {
|
||||
// Show tab in menu
|
||||
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)) {
|
||||
handlerProxy.call(this, event, this.config.listeners.language, () => {
|
||||
this.toggleCaptions(true);
|
||||
this.language = event.target.value.toLowerCase();
|
||||
});
|
||||
} else if (utils.matches(event.target, this.config.selectors.inputs.quality)) {
|
||||
// Settings - Quality
|
||||
handlerProxy.call(this, event, this.config.listeners.quality, () => {
|
||||
this.quality = event.target.value;
|
||||
});
|
||||
} else if (utils.matches(event.target, this.config.selectors.inputs.speed)) {
|
||||
// Settings - Speed
|
||||
handlerProxy.call(this, event, this.config.listeners.speed, () => {
|
||||
this.speed = parseFloat(event.target.value);
|
||||
});
|
||||
} else if (utils.matches(event.target, this.config.selectors.buttons.loop)) {
|
||||
// Settings - Looping
|
||||
// TODO: use toggle buttons
|
||||
handlerProxy.call(this, event, this.config.listeners.loop, () => {
|
||||
// TODO: This should be done in the method itself I think
|
||||
// var value = event.target.getAttribute('data-loop__value') || event.target.getAttribute('data-loop__type');
|
||||
|
||||
this.warn('Set loop');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Seek
|
||||
utils.proxy(this.elements.inputs.seek, inputEvent, this.config.listeners.seek, event => {
|
||||
this.currentTime = event.target.value / event.target.max * this.duration;
|
||||
});
|
||||
|
||||
// Volume
|
||||
utils.proxy(this.elements.inputs.volume, inputEvent, this.config.listeners.volume, event => {
|
||||
this.setVolume(event.target.value);
|
||||
});
|
||||
|
||||
// Polyfill for lower fill in <input type="range"> for webkit
|
||||
if (this.browser.isWebkit) {
|
||||
utils.on(utils.getElements.call(this, 'input[type="range"]'), 'input', event => {
|
||||
controls.updateRangeFill.call(this, event.target);
|
||||
});
|
||||
}
|
||||
|
||||
// Seek tooltip
|
||||
utils.on(this.elements.progress, 'mouseenter mouseleave mousemove', event =>
|
||||
ui.updateSeekTooltip.call(this, event)
|
||||
);
|
||||
|
||||
// Toggle controls visibility based on mouse movement
|
||||
if (this.config.hideControls) {
|
||||
// Toggle controls on mouse events and entering fullscreen
|
||||
utils.on(
|
||||
this.elements.container,
|
||||
'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen',
|
||||
event => {
|
||||
this.toggleControls(event);
|
||||
}
|
||||
);
|
||||
|
||||
// Watch for cursor over controls so they don't hide when trying to interact
|
||||
utils.on(this.elements.controls, 'mouseenter mouseleave', event => {
|
||||
this.elements.controls.hover = event.type === 'mouseenter';
|
||||
});
|
||||
|
||||
// Watch for cursor over controls so they don't hide when trying to interact
|
||||
utils.on(this.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
||||
this.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
|
||||
});
|
||||
|
||||
// Focus in/out on controls
|
||||
// TODO: Check we need capture here
|
||||
utils.on(
|
||||
this.elements.controls,
|
||||
'focus blur',
|
||||
event => {
|
||||
this.toggleControls(event);
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Mouse wheel for volume
|
||||
utils.proxy(
|
||||
this.elements.inputs.volume,
|
||||
'wheel',
|
||||
this.config.listeners.volume,
|
||||
event => {
|
||||
// Detect "natural" scroll - suppored on OS X Safari only
|
||||
// Other browsers on OS X will be inverted until support improves
|
||||
const inverted = event.webkitDirectionInvertedFromDevice;
|
||||
const step = 1 / 50;
|
||||
let direction = 0;
|
||||
|
||||
// Scroll down (or up on natural) to decrease
|
||||
if (event.deltaY < 0 || event.deltaX > 0) {
|
||||
if (inverted) {
|
||||
this.decreaseVolume(step);
|
||||
direction = -1;
|
||||
} else {
|
||||
this.increaseVolume(step);
|
||||
direction = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll up (or down on natural) to increase
|
||||
if (event.deltaY > 0 || event.deltaX < 0) {
|
||||
if (inverted) {
|
||||
this.increaseVolume(step);
|
||||
direction = 1;
|
||||
} else {
|
||||
this.decreaseVolume(step);
|
||||
direction = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't break page scrolling at max and min
|
||||
if ((direction === 1 && this.media.volume < 1) || (direction === -1 && this.media.volume > 0)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// Handle user exiting fullscreen by escaping etc
|
||||
if (fullscreen.enabled) {
|
||||
utils.on(document, fullscreen.eventType, event => {
|
||||
this.toggleFullscreen(event);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default listeners;
|
109
src/js/media.js
Normal file
109
src/js/media.js
Normal file
@ -0,0 +1,109 @@
|
||||
// ==========================================================================
|
||||
// Plyr Media
|
||||
// ==========================================================================
|
||||
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
import youtube from './plugins/youtube';
|
||||
import vimeo from './plugins/vimeo';
|
||||
import ui from './ui';
|
||||
|
||||
const media = {
|
||||
// Setup media
|
||||
setup() {
|
||||
// If there's no media, bail
|
||||
if (!this.media) {
|
||||
this.warn('No media element found!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add type class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
|
||||
|
||||
// Add video class for embeds
|
||||
// This will require changes if audio embeds are added
|
||||
if (this.isEmbed) {
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
|
||||
}
|
||||
|
||||
if (this.supported.ui) {
|
||||
// Check for picture-in-picture support
|
||||
utils.toggleClass(
|
||||
this.elements.container,
|
||||
this.config.classNames.pip.supported,
|
||||
support.pip && this.type === 'video'
|
||||
);
|
||||
|
||||
// Check for airplay support
|
||||
utils.toggleClass(
|
||||
this.elements.container,
|
||||
this.config.classNames.airplay.supported,
|
||||
support.airplay && this.isHTML5
|
||||
);
|
||||
|
||||
// If there's no autoplay attribute, assume the video is stopped and add state class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.config.autoplay);
|
||||
|
||||
// Add iOS class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.isIos, this.browser.isIos);
|
||||
|
||||
// Add touch class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, support.touch);
|
||||
}
|
||||
|
||||
// Inject the player wrapper
|
||||
if (['video', 'youtube', 'vimeo'].includes(this.type)) {
|
||||
// Create the wrapper div
|
||||
this.elements.wrapper = utils.createElement('div', {
|
||||
class: this.config.classNames.video,
|
||||
});
|
||||
|
||||
// Wrap the video in a container
|
||||
utils.wrap(this.media, this.elements.wrapper);
|
||||
}
|
||||
|
||||
// Embeds
|
||||
if (this.isEmbed) {
|
||||
switch (this.type) {
|
||||
case 'youtube':
|
||||
youtube.setup.call(this);
|
||||
break;
|
||||
|
||||
case 'vimeo':
|
||||
vimeo.setup.call(this);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ui.setTitle.call(this);
|
||||
},
|
||||
|
||||
// Cancel current network requests
|
||||
// See https://github.com/sampotts/plyr/issues/174
|
||||
cancelRequests() {
|
||||
if (!this.isHTML5) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove child sources
|
||||
Array.from(this.media.querySelectorAll('source')).forEach(utils.removeElement);
|
||||
|
||||
// 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.log('Cancelled network requests');
|
||||
},
|
||||
};
|
||||
|
||||
export default media;
|
165
src/js/plugins/vimeo.js
Normal file
165
src/js/plugins/vimeo.js
Normal file
@ -0,0 +1,165 @@
|
||||
// ==========================================================================
|
||||
// Vimeo plugin
|
||||
// ==========================================================================
|
||||
|
||||
import utils from './../utils';
|
||||
import captions from './../captions';
|
||||
import ui from './../ui';
|
||||
|
||||
const vimeo = {
|
||||
// Setup YouTube
|
||||
setup() {
|
||||
// Remove old containers
|
||||
const containers = utils.getElements.call(this, `[id^="${this.type}-"]`);
|
||||
Array.from(containers).forEach(utils.removeElement);
|
||||
|
||||
// Add embed class for responsive
|
||||
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
|
||||
|
||||
// Set ID
|
||||
this.media.setAttribute('id', utils.generateId(this.type));
|
||||
|
||||
// Load the API if not already
|
||||
if (!utils.is.object(window.Vimeo)) {
|
||||
utils.loadScript(this.config.urls.vimeo.api);
|
||||
// Wait for load
|
||||
const vimeoTimer = window.setInterval(() => {
|
||||
if (utils.is.object(window.Vimeo)) {
|
||||
window.clearInterval(vimeoTimer);
|
||||
vimeo.ready.call(this);
|
||||
}
|
||||
}, 50);
|
||||
} else {
|
||||
vimeo.ready.call(this);
|
||||
}
|
||||
},
|
||||
|
||||
// Ready
|
||||
ready() {
|
||||
const player = this;
|
||||
|
||||
// Get Vimeo params for the iframe
|
||||
const options = {
|
||||
loop: this.config.loop.active,
|
||||
autoplay: this.config.autoplay,
|
||||
byline: false,
|
||||
portrait: false,
|
||||
title: false,
|
||||
transparent: 0,
|
||||
};
|
||||
const params = utils.buildUrlParameters(options);
|
||||
const id = utils.parseVimeoId(this.embedId);
|
||||
|
||||
// Build an iframe
|
||||
const iframe = utils.createElement('iframe');
|
||||
const src = `https://player.vimeo.com/video/${id}?${params}`;
|
||||
iframe.setAttribute('src', src);
|
||||
iframe.setAttribute('allowfullscreen', '');
|
||||
player.media.appendChild(iframe);
|
||||
|
||||
// Setup instance
|
||||
// https://github.com/vimeo/this.js
|
||||
player.embed = new window.Vimeo.Player(iframe);
|
||||
|
||||
// Create a faux HTML5 API using the Vimeo API
|
||||
player.media.play = () => {
|
||||
player.embed.play();
|
||||
player.media.paused = false;
|
||||
};
|
||||
player.media.pause = () => {
|
||||
player.embed.pause();
|
||||
player.media.paused = true;
|
||||
};
|
||||
player.media.stop = () => {
|
||||
player.embed.stop();
|
||||
player.media.paused = true;
|
||||
};
|
||||
|
||||
player.media.paused = true;
|
||||
player.media.currentTime = 0;
|
||||
|
||||
// Rebuild UI
|
||||
ui.build.call(player);
|
||||
|
||||
player.embed.getCurrentTime().then(value => {
|
||||
player.media.currentTime = value;
|
||||
utils.dispatchEvent.call(this, this.media, 'timeupdate');
|
||||
});
|
||||
|
||||
player.embed.getDuration().then(value => {
|
||||
player.media.duration = value;
|
||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
||||
});
|
||||
|
||||
// Get captions
|
||||
player.embed.getTextTracks().then(tracks => {
|
||||
player.captions.tracks = tracks;
|
||||
|
||||
captions.setup.call(player);
|
||||
});
|
||||
|
||||
player.embed.on('cuechange', data => {
|
||||
let cue = null;
|
||||
|
||||
if (data.cues.length) {
|
||||
cue = utils.stripHTML(data.cues[0].text);
|
||||
}
|
||||
|
||||
captions.set.call(player, cue);
|
||||
});
|
||||
|
||||
player.embed.on('loaded', () => {
|
||||
if (utils.is.htmlElement(player.embed.element) && player.supported.ui) {
|
||||
const frame = player.embed.element;
|
||||
|
||||
// Fix Vimeo controls issue
|
||||
// https://github.com/sampotts/plyr/issues/697
|
||||
// frame.src = `${frame.src}&transparent=0`;
|
||||
|
||||
// Fix keyboard focus issues
|
||||
// https://github.com/sampotts/plyr/issues/317
|
||||
frame.setAttribute('tabindex', -1);
|
||||
}
|
||||
});
|
||||
|
||||
player.embed.on('play', () => {
|
||||
player.media.paused = false;
|
||||
utils.dispatchEvent.call(player, player.media, 'play');
|
||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
||||
});
|
||||
|
||||
player.embed.on('pause', () => {
|
||||
player.media.paused = true;
|
||||
utils.dispatchEvent.call(player, player.media, 'pause');
|
||||
});
|
||||
|
||||
this.embed.on('timeupdate', data => {
|
||||
this.media.seeking = false;
|
||||
this.media.currentTime = data.seconds;
|
||||
utils.dispatchEvent.call(this, this.media, 'timeupdate');
|
||||
});
|
||||
|
||||
this.embed.on('progress', data => {
|
||||
this.media.buffered = data.percent;
|
||||
utils.dispatchEvent.call(this, this.media, 'progress');
|
||||
|
||||
if (parseInt(data.percent, 10) === 1) {
|
||||
// Trigger event
|
||||
utils.dispatchEvent.call(this, this.media, 'canplaythrough');
|
||||
}
|
||||
});
|
||||
|
||||
this.embed.on('seeked', () => {
|
||||
this.media.seeking = false;
|
||||
utils.dispatchEvent.call(this, this.media, 'seeked');
|
||||
utils.dispatchEvent.call(this, this.media, 'play');
|
||||
});
|
||||
|
||||
this.embed.on('ended', () => {
|
||||
this.media.paused = true;
|
||||
utils.dispatchEvent.call(this, this.media, 'ended');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default vimeo;
|
256
src/js/plugins/youtube.js
Normal file
256
src/js/plugins/youtube.js
Normal file
@ -0,0 +1,256 @@
|
||||
// ==========================================================================
|
||||
// YouTube plugin
|
||||
// ==========================================================================
|
||||
|
||||
import utils from './../utils';
|
||||
import controls from './../controls';
|
||||
import ui from './../ui';
|
||||
|
||||
/* Object.defineProperty(input, "value", {
|
||||
get: function() {return this._value;},
|
||||
set: function(v) {
|
||||
// Do your stuff
|
||||
this._value = v;
|
||||
}
|
||||
}); */
|
||||
|
||||
const youtube = {
|
||||
// Setup YouTube
|
||||
setup() {
|
||||
const videoId = utils.parseYouTubeId(this.embedId);
|
||||
|
||||
// Remove old containers
|
||||
const containers = utils.getElements.call(this, `[id^="${this.type}-"]`);
|
||||
Array.from(containers).forEach(utils.removeElement);
|
||||
|
||||
// Add embed class for responsive
|
||||
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
|
||||
|
||||
// Set ID
|
||||
this.media.setAttribute('id', utils.generateId(this.type));
|
||||
|
||||
// Setup API
|
||||
if (utils.is.object(window.YT)) {
|
||||
youtube.ready.call(this, videoId);
|
||||
} else {
|
||||
// Load the API
|
||||
utils.loadScript(this.config.urls.youtube.api);
|
||||
|
||||
// Setup callback for the API
|
||||
window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || [];
|
||||
|
||||
// Add to queue
|
||||
window.onYouTubeReadyCallbacks.push(() => {
|
||||
youtube.ready.call(this, videoId);
|
||||
});
|
||||
|
||||
// Set callback to process queue
|
||||
window.onYouTubeIframeAPIReady = () => {
|
||||
window.onYouTubeReadyCallbacks.forEach(callback => {
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Handle YouTube API ready
|
||||
ready(videoId) {
|
||||
const player = this;
|
||||
|
||||
// Setup instance
|
||||
// https://developers.google.com/youtube/iframe_api_reference
|
||||
player.embed = new window.YT.Player(player.media.id, {
|
||||
videoId,
|
||||
playerVars: {
|
||||
autoplay: player.config.autoplay ? 1 : 0, // Autoplay
|
||||
controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
|
||||
rel: 0, // No related vids
|
||||
showinfo: 0, // Hide info
|
||||
iv_load_policy: 3, // Hide annotations
|
||||
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
|
||||
disablekb: 1, // Disable keyboard as we handle it
|
||||
playsinline: 1, // Allow iOS inline playback
|
||||
|
||||
// Tracking for stats
|
||||
origin: window && window.location.hostname,
|
||||
widget_referrer: window && window.location.href,
|
||||
|
||||
// Captions is flaky on YouTube
|
||||
// cc_load_policy: (this.captions.active ? 1 : 0),
|
||||
// cc_lang_pref: 'en',
|
||||
},
|
||||
events: {
|
||||
onError(event) {
|
||||
utils.dispatchEvent.call(player, player.media, 'error', true, {
|
||||
code: event.data,
|
||||
embed: event.target,
|
||||
});
|
||||
},
|
||||
onPlaybackQualityChange(event) {
|
||||
// Get the instance
|
||||
const instance = event.target;
|
||||
|
||||
// Get current quality
|
||||
player.media.quality = instance.getPlaybackQuality();
|
||||
|
||||
utils.dispatchEvent.call(player, player.media, 'qualitychange');
|
||||
},
|
||||
onPlaybackRateChange(event) {
|
||||
// Get the instance
|
||||
const instance = event.target;
|
||||
|
||||
// Get current speed
|
||||
player.media.playbackRate = instance.getPlaybackRate();
|
||||
|
||||
utils.dispatchEvent.call(player, player.media, 'ratechange');
|
||||
},
|
||||
onReady(event) {
|
||||
// Get the instance
|
||||
const instance = event.target;
|
||||
|
||||
// Create a faux HTML5 API using the YouTube API
|
||||
player.media.play = () => {
|
||||
instance.playVideo();
|
||||
player.media.paused = false;
|
||||
};
|
||||
player.media.pause = () => {
|
||||
instance.pauseVideo();
|
||||
player.media.paused = true;
|
||||
};
|
||||
player.media.stop = () => {
|
||||
instance.stopVideo();
|
||||
player.media.paused = true;
|
||||
};
|
||||
player.media.duration = instance.getDuration();
|
||||
player.media.paused = true;
|
||||
player.media.muted = instance.isMuted();
|
||||
player.media.currentTime = 0;
|
||||
|
||||
// Get available speeds
|
||||
if (player.config.controls.includes('settings') && player.config.settings.includes('speed')) {
|
||||
controls.setSpeedMenu.call(player, instance.getAvailablePlaybackRates());
|
||||
}
|
||||
|
||||
// Set title
|
||||
player.config.title = instance.getVideoData().title;
|
||||
|
||||
// Set the tabindex to avoid focus entering iframe
|
||||
if (player.supported.ui) {
|
||||
player.media.setAttribute('tabindex', -1);
|
||||
}
|
||||
|
||||
// Rebuild UI
|
||||
ui.build.call(player);
|
||||
|
||||
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
||||
|
||||
// Reset timer
|
||||
window.clearInterval(player.timers.buffering);
|
||||
|
||||
// Setup buffering
|
||||
player.timers.buffering = window.setInterval(() => {
|
||||
// Get loaded % from YouTube
|
||||
player.media.buffered = instance.getVideoLoadedFraction();
|
||||
|
||||
// Trigger progress only when we actually buffer something
|
||||
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
|
||||
utils.dispatchEvent.call(player, player.media, 'progress');
|
||||
}
|
||||
|
||||
// Set last buffer point
|
||||
player.media.lastBuffered = player.media.buffered;
|
||||
|
||||
// Bail if we're at 100%
|
||||
if (player.media.buffered === 1) {
|
||||
window.clearInterval(player.timers.buffering);
|
||||
|
||||
// Trigger event
|
||||
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
|
||||
}
|
||||
}, 200);
|
||||
},
|
||||
onStateChange(event) {
|
||||
// Get the instance
|
||||
const instance = event.target;
|
||||
|
||||
// Reset timer
|
||||
window.clearInterval(player.timers.playing);
|
||||
|
||||
// Handle events
|
||||
// -1 Unstarted
|
||||
// 0 Ended
|
||||
// 1 Playing
|
||||
// 2 Paused
|
||||
// 3 Buffering
|
||||
// 5 Video cued
|
||||
switch (event.data) {
|
||||
case 0:
|
||||
// YouTube doesn't support loop for a single video, so mimick it.
|
||||
if (player.config.loop.active) {
|
||||
// YouTube needs a call to `stopVideo` before playing again
|
||||
instance.stopVideo();
|
||||
instance.playVideo();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
player.media.paused = true;
|
||||
|
||||
utils.dispatchEvent.call(player, player.media, 'ended');
|
||||
|
||||
break;
|
||||
|
||||
case 1:
|
||||
player.media.paused = false;
|
||||
|
||||
// If we were seeking, fire seeked event
|
||||
if (player.media.seeking) {
|
||||
utils.dispatchEvent.call(player, player.media, 'seeked');
|
||||
}
|
||||
|
||||
player.media.seeking = false;
|
||||
|
||||
utils.dispatchEvent.call(player, player.media, 'play');
|
||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
||||
|
||||
// Poll to get playback progress
|
||||
player.timers.playing = window.setInterval(() => {
|
||||
player.media.currentTime = instance.getCurrentTime();
|
||||
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
||||
}, 100);
|
||||
|
||||
// Check duration again due to YouTube bug
|
||||
// https://github.com/sampotts/plyr/issues/374
|
||||
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
|
||||
if (player.media.duration !== instance.getDuration()) {
|
||||
player.media.duration = instance.getDuration();
|
||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
||||
}
|
||||
|
||||
// Get quality
|
||||
controls.setQualityMenu.call(player, instance.getAvailableQualityLevels());
|
||||
|
||||
break;
|
||||
|
||||
case 2:
|
||||
player.media.paused = true;
|
||||
|
||||
utils.dispatchEvent.call(player, player.media, 'pause');
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, {
|
||||
code: event.data,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default youtube;
|
5656
src/js/plyr.js
5656
src/js/plyr.js
File diff suppressed because it is too large
Load Diff
162
src/js/source.js
Normal file
162
src/js/source.js
Normal file
@ -0,0 +1,162 @@
|
||||
// ==========================================================================
|
||||
// Plyr source update
|
||||
// ==========================================================================
|
||||
|
||||
import types from './types';
|
||||
import utils from './utils';
|
||||
import media from './media';
|
||||
import ui from './ui';
|
||||
import support from './support';
|
||||
|
||||
const source = {
|
||||
// Add elements to HTML5 media (source, tracks, etc)
|
||||
insertElements(type, attributes) {
|
||||
if (utils.is.string(attributes)) {
|
||||
utils.insertElement(type, this.media, {
|
||||
src: attributes,
|
||||
});
|
||||
} else if (utils.is.array(attributes)) {
|
||||
this.warn(attributes);
|
||||
|
||||
attributes.forEach(attribute => {
|
||||
utils.insertElement(type, this.media, attribute);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Update source
|
||||
// Sources are not checked for support so be careful
|
||||
change(input) {
|
||||
if (!utils.is.object(input) || !('sources' in input) || !input.sources.length) {
|
||||
this.warn('Invalid source format');
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel current network requests
|
||||
media.cancelRequests.call(this);
|
||||
|
||||
// Destroy instance and re-setup
|
||||
this.destroy.call(
|
||||
this,
|
||||
() => {
|
||||
// TODO: Reset menus here
|
||||
|
||||
// Remove elements
|
||||
utils.removeElement(this.media);
|
||||
this.media = null;
|
||||
|
||||
// Reset class name
|
||||
if (utils.is.htmlElement(this.elements.container)) {
|
||||
this.elements.container.removeAttribute('class');
|
||||
}
|
||||
|
||||
// Set the type
|
||||
if ('type' in input) {
|
||||
this.type = input.type;
|
||||
|
||||
// Get child type for video (it might be an embed)
|
||||
if (this.type === 'video') {
|
||||
const firstSource = input.sources[0];
|
||||
|
||||
if ('type' in firstSource && types.embed.includes(firstSource.type)) {
|
||||
this.type = firstSource.type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for support
|
||||
this.supported = support.check(this.type, this.config.inline);
|
||||
|
||||
// Create new markup
|
||||
switch (this.type) {
|
||||
case 'video':
|
||||
this.media = utils.createElement('video');
|
||||
break;
|
||||
|
||||
case 'audio':
|
||||
this.media = utils.createElement('audio');
|
||||
break;
|
||||
|
||||
case 'youtube':
|
||||
case 'vimeo':
|
||||
this.media = utils.createElement('div');
|
||||
this.embedId = input.sources[0].src;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Inject the new element
|
||||
this.elements.container.appendChild(this.media);
|
||||
|
||||
// Autoplay the new source?
|
||||
if (utils.is.boolean(input.autoplay)) {
|
||||
this.config.autoplay = input.autoplay;
|
||||
}
|
||||
|
||||
// Set attributes for audio and video
|
||||
if (this.isHTML5) {
|
||||
if (this.config.crossorigin) {
|
||||
this.media.setAttribute('crossorigin', '');
|
||||
}
|
||||
if (this.config.autoplay) {
|
||||
this.media.setAttribute('autoplay', '');
|
||||
}
|
||||
if ('poster' in input) {
|
||||
this.media.setAttribute('poster', input.poster);
|
||||
}
|
||||
if (this.config.loop.active) {
|
||||
this.media.setAttribute('loop', '');
|
||||
}
|
||||
if (this.config.muted) {
|
||||
this.media.setAttribute('muted', '');
|
||||
}
|
||||
if (this.config.inline) {
|
||||
this.media.setAttribute('playsinline', '');
|
||||
}
|
||||
}
|
||||
|
||||
// Restore class hooks
|
||||
utils.toggleClass(
|
||||
this.elements.container,
|
||||
this.config.classNames.captions.active,
|
||||
this.supported.ui && this.captions.enabled
|
||||
);
|
||||
|
||||
ui.addStyleHook.call(this);
|
||||
|
||||
// Set new sources for html5
|
||||
if (this.isHTML5) {
|
||||
source.insertElements.call(this, 'source', input.sources);
|
||||
}
|
||||
|
||||
// Set video title
|
||||
this.config.title = input.title;
|
||||
|
||||
// Set up from scratch
|
||||
media.setup.call(this);
|
||||
|
||||
// HTML5 stuff
|
||||
if (this.isHTML5) {
|
||||
// Setup captions
|
||||
if ('tracks' in input) {
|
||||
source.insertElements.call(this, 'track', input.tracks);
|
||||
}
|
||||
|
||||
// Load HTML5 sources
|
||||
this.media.load();
|
||||
}
|
||||
|
||||
// If HTML5 or embed but not fully supported, setupInterface and call ready now
|
||||
if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {
|
||||
// Setup interface
|
||||
ui.build.call(this);
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default source;
|
56
src/js/storage.js
Normal file
56
src/js/storage.js
Normal file
@ -0,0 +1,56 @@
|
||||
// ==========================================================================
|
||||
// Plyr storage
|
||||
// ==========================================================================
|
||||
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
|
||||
// Save a value back to local storage
|
||||
function set(value) {
|
||||
// Bail if we don't have localStorage support or it's disabled
|
||||
if (!support.storage || !this.config.storage.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the working copy of the values
|
||||
utils.extend(this.storage, value);
|
||||
|
||||
// Update storage
|
||||
window.localStorage.setItem(this.config.storage.key, JSON.stringify(this.storage));
|
||||
}
|
||||
|
||||
// Setup localStorage
|
||||
function setup() {
|
||||
let value = null;
|
||||
let storage = {};
|
||||
|
||||
// Bail if we don't have localStorage support or it's disabled
|
||||
if (!support.storage || !this.config.storage.enabled) {
|
||||
return storage;
|
||||
}
|
||||
|
||||
// Clean up old volume
|
||||
// https://github.com/sampotts/plyr/issues/171
|
||||
window.localStorage.removeItem('plyr-volume');
|
||||
|
||||
// load value from the current key
|
||||
value = window.localStorage.getItem(this.config.storage.key);
|
||||
|
||||
if (!value) {
|
||||
// Key wasn't set (or had been cleared), move along
|
||||
} else if (/^\d+(\.\d+)?$/.test(value)) {
|
||||
// If value is a number, it's probably volume from an older
|
||||
// version of this. See: https://github.com/sampotts/plyr/pull/313
|
||||
// Update the key to be JSON
|
||||
set({
|
||||
volume: parseFloat(value),
|
||||
});
|
||||
} else {
|
||||
// Assume it's JSON from this or a later version of plyr
|
||||
storage = JSON.parse(value);
|
||||
}
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
export default { setup, set };
|
174
src/js/support.js
Normal file
174
src/js/support.js
Normal file
@ -0,0 +1,174 @@
|
||||
// ==========================================================================
|
||||
// Plyr support checks
|
||||
// ==========================================================================
|
||||
|
||||
import utils from './utils';
|
||||
|
||||
// Check for feature support
|
||||
const support = {
|
||||
// Basic support
|
||||
audio: 'canPlayType' in document.createElement('audio'),
|
||||
video: 'canPlayType' in document.createElement('video'),
|
||||
|
||||
// Check for support
|
||||
// Basic functionality vs full UI
|
||||
check(type, inline) {
|
||||
let api = false;
|
||||
let ui = false;
|
||||
const browser = utils.getBrowser();
|
||||
const playsInline = browser.isIPhone && inline && support.inline;
|
||||
|
||||
switch (type) {
|
||||
case 'video':
|
||||
api = support.video;
|
||||
ui = api && support.rangeInput && (!browser.isIPhone || playsInline);
|
||||
break;
|
||||
|
||||
case 'audio':
|
||||
api = support.audio;
|
||||
ui = api && support.rangeInput;
|
||||
break;
|
||||
|
||||
case 'youtube':
|
||||
api = true;
|
||||
ui = support.rangeInput && (!browser.isIPhone || playsInline);
|
||||
break;
|
||||
|
||||
case 'vimeo':
|
||||
api = true;
|
||||
ui = support.rangeInput && !browser.isIPhone;
|
||||
break;
|
||||
|
||||
default:
|
||||
api = support.audio && support.video;
|
||||
ui = api && support.rangeInput;
|
||||
}
|
||||
|
||||
return {
|
||||
api,
|
||||
ui,
|
||||
};
|
||||
},
|
||||
|
||||
// Local storage
|
||||
// We can't assume if local storage is present that we can use it
|
||||
storage: (() => {
|
||||
if (!('localStorage' in window)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to use it (it might be disabled, e.g. user is in private/porn mode)
|
||||
// see: https://github.com/sampotts/plyr/issues/131
|
||||
const test = '___test';
|
||||
try {
|
||||
window.localStorage.setItem(test, test);
|
||||
window.localStorage.removeItem(test);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})(),
|
||||
|
||||
// Picture-in-picture support
|
||||
// Safari only currently
|
||||
pip: (() => {
|
||||
const browser = utils.getBrowser();
|
||||
return !browser.isIPhone && utils.is.function(utils.createElement('video').webkitSetPresentationMode);
|
||||
})(),
|
||||
|
||||
// Airplay support
|
||||
// Safari only currently
|
||||
airplay: utils.is.function(window.WebKitPlaybackTargetAvailabilityEvent),
|
||||
|
||||
// Inline playback support
|
||||
// https://webkit.org/blog/6784/new-video-policies-for-ios/
|
||||
inline: 'playsInline' in document.createElement('video'),
|
||||
|
||||
// Check for mime type support against a player instance
|
||||
// Credits: http://diveintohtml5.info/everything.html
|
||||
// Related: http://www.leanbackplayer.com/test/h5mt.html
|
||||
mime(player, type) {
|
||||
const media = { player };
|
||||
|
||||
try {
|
||||
// Bail if no checking function
|
||||
if (!utils.is.function(media.canPlayType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Type specific checks
|
||||
if (player.type === 'video') {
|
||||
switch (type) {
|
||||
case 'video/webm':
|
||||
return media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, '');
|
||||
case 'video/mp4':
|
||||
return media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, '');
|
||||
case 'video/ogg':
|
||||
return media.canPlayType('video/ogg; codecs="theora"').replace(/no/, '');
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} else if (player.type === 'audio') {
|
||||
switch (type) {
|
||||
case 'audio/mpeg':
|
||||
return media.canPlayType('audio/mpeg;').replace(/no/, '');
|
||||
case 'audio/ogg':
|
||||
return media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '');
|
||||
case 'audio/wav':
|
||||
return media.canPlayType('audio/wav; codecs="1"').replace(/no/, '');
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we got this far, we're stuffed
|
||||
return false;
|
||||
},
|
||||
|
||||
// Check for textTracks support
|
||||
textTracks: 'textTracks' in document.createElement('video'),
|
||||
|
||||
// Check for passive event listener support
|
||||
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
|
||||
// https://www.youtube.com/watch?v=NPM6172J22g
|
||||
passiveListeners: (() => {
|
||||
// Test via a getter in the options object to see if the passive property is accessed
|
||||
let supported = false;
|
||||
try {
|
||||
const options = Object.defineProperty({}, 'passive', {
|
||||
get() {
|
||||
supported = true;
|
||||
return null;
|
||||
},
|
||||
});
|
||||
window.addEventListener('test', null, options);
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
return supported;
|
||||
})(),
|
||||
|
||||
// <input type="range"> Sliders
|
||||
rangeInput: (() => {
|
||||
const range = document.createElement('input');
|
||||
range.type = 'range';
|
||||
return range.type === 'range';
|
||||
})(),
|
||||
|
||||
// Touch
|
||||
// Remember a device can be moust + touch enabled
|
||||
touch: 'ontouchstart' in document.documentElement,
|
||||
|
||||
// Detect transitions support
|
||||
transitions: utils.transitionEnd !== false,
|
||||
|
||||
// Reduced motion iOS & MacOS setting
|
||||
// https://webkit.org/blog/7551/responsive-design-for-motion/
|
||||
reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,
|
||||
};
|
||||
|
||||
export default support;
|
10
src/js/types.js
Normal file
10
src/js/types.js
Normal file
@ -0,0 +1,10 @@
|
||||
// ==========================================================================
|
||||
// Plyr supported types
|
||||
// ==========================================================================
|
||||
|
||||
const types = {
|
||||
embed: ['youtube', 'vimeo'],
|
||||
html5: ['video', 'audio'],
|
||||
};
|
||||
|
||||
export default types;
|
381
src/js/ui.js
Normal file
381
src/js/ui.js
Normal file
@ -0,0 +1,381 @@
|
||||
// ==========================================================================
|
||||
// Plyr UI
|
||||
// ==========================================================================
|
||||
|
||||
import utils from './utils';
|
||||
import captions from './captions';
|
||||
import controls from './controls';
|
||||
import fullscreen from './fullscreen';
|
||||
import listeners from './listeners';
|
||||
import storage from './storage';
|
||||
|
||||
const ui = {
|
||||
addStyleHook() {
|
||||
utils.toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
|
||||
},
|
||||
|
||||
// Toggle native HTML5 media controls
|
||||
toggleNativeControls(toggle) {
|
||||
if (toggle && this.isHTML5) {
|
||||
this.media.setAttribute('controls', '');
|
||||
} else {
|
||||
this.media.removeAttribute('controls');
|
||||
}
|
||||
},
|
||||
|
||||
// Setup the UI
|
||||
build() {
|
||||
// Re-attach media element listeners
|
||||
// TODO: Use event bubbling
|
||||
listeners.media.call(this);
|
||||
|
||||
// Don't setup interface if no support
|
||||
if (!this.supported.ui) {
|
||||
this.warn(`Basic support only for ${this.type}`);
|
||||
|
||||
// Remove controls
|
||||
utils.removeElement.call(this, 'controls');
|
||||
|
||||
// Remove large play
|
||||
utils.removeElement.call(this, 'buttons.play');
|
||||
|
||||
// Restore native controls
|
||||
ui.toggleNativeControls.call(this, true);
|
||||
|
||||
// Bail
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject custom controls if not present
|
||||
if (!utils.is.htmlElement(this.elements.controls)) {
|
||||
// Inject custom controls
|
||||
controls.inject.call(this);
|
||||
|
||||
// Re-attach control listeners
|
||||
listeners.controls.call(this);
|
||||
}
|
||||
|
||||
// If there's no controls, bail
|
||||
if (!utils.is.htmlElement(this.elements.controls)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove native controls
|
||||
ui.toggleNativeControls.call(this);
|
||||
|
||||
// Setup fullscreen
|
||||
fullscreen.setup.call(this);
|
||||
|
||||
// Captions
|
||||
captions.setup.call(this);
|
||||
|
||||
// Set volume
|
||||
this.volume = null;
|
||||
ui.updateVolume.call(this);
|
||||
|
||||
// Set playback speed
|
||||
this.speed = null;
|
||||
|
||||
// Set loop
|
||||
// this.setLoop();
|
||||
|
||||
// Reset time display
|
||||
ui.timeUpdate.call(this);
|
||||
|
||||
// Update the UI
|
||||
ui.checkPlaying.call(this);
|
||||
|
||||
this.ready = true;
|
||||
|
||||
// Ready event at end of execution stack
|
||||
utils.dispatchEvent.call(this, this.media, 'ready');
|
||||
|
||||
// Autoplay
|
||||
if (this.config.autoplay) {
|
||||
this.play();
|
||||
}
|
||||
},
|
||||
|
||||
// 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.media.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)
|
||||
ui.updateSeekTooltip.call(this);
|
||||
},
|
||||
|
||||
// Setup aria attribute for play and iframe title
|
||||
setTitle() {
|
||||
// Find the current text
|
||||
let label = this.config.i18n.play;
|
||||
|
||||
// If there's a media title set, use that for the label
|
||||
if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) {
|
||||
label += `, ${this.config.title}`;
|
||||
|
||||
// Set container label
|
||||
this.elements.container.setAttribute('aria-label', this.config.title);
|
||||
}
|
||||
|
||||
// If there's a play button, set label
|
||||
if (this.supported.ui) {
|
||||
if (utils.is.htmlElement(this.elements.buttons.play)) {
|
||||
this.elements.buttons.play.setAttribute('aria-label', label);
|
||||
}
|
||||
if (utils.is.htmlElement(this.elements.buttons.playLarge)) {
|
||||
this.elements.buttons.playLarge.setAttribute('aria-label', label);
|
||||
}
|
||||
}
|
||||
|
||||
// Set iframe title
|
||||
// https://github.com/sampotts/plyr/issues/124
|
||||
if (this.isEmbed) {
|
||||
const iframe = utils.getElement.call(this, 'iframe');
|
||||
|
||||
if (!utils.is.htmlElement(iframe)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Default to media type
|
||||
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
|
||||
|
||||
iframe.setAttribute('title', this.config.i18n.frameTitle.replace('{title}', title));
|
||||
}
|
||||
},
|
||||
|
||||
// Check playing state
|
||||
checkPlaying() {
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.playing, !this.media.paused);
|
||||
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.media.paused);
|
||||
|
||||
this.toggleControls(this.media.paused);
|
||||
},
|
||||
|
||||
// Update volume UI and storage
|
||||
updateVolume() {
|
||||
// Update the <input type="range"> if present
|
||||
if (this.supported.ui) {
|
||||
const value = this.media.muted ? 0 : this.media.volume;
|
||||
|
||||
if (this.elements.inputs.volume) {
|
||||
ui.setRange.call(this, this.elements.inputs.volume, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the volume in storage
|
||||
storage.set.call(this, {
|
||||
volume: this.media.volume,
|
||||
});
|
||||
|
||||
// Toggle class if muted
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.muted, this.media.muted);
|
||||
|
||||
// Update checkbox for mute state
|
||||
if (this.supported.ui && this.elements.buttons.mute) {
|
||||
utils.toggleState(this.elements.buttons.mute, this.media.muted);
|
||||
}
|
||||
},
|
||||
|
||||
// Check if media is loading
|
||||
checkLoading(event) {
|
||||
this.loading = event.type === 'waiting';
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(this.timers.loading);
|
||||
|
||||
// Timer to prevent flicker when seeking
|
||||
this.timers.loading = setTimeout(() => {
|
||||
// Toggle container class hook
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||
|
||||
// Show controls if loading, hide if done
|
||||
this.toggleControls(this.loading);
|
||||
}, this.loading ? 250 : 0);
|
||||
},
|
||||
|
||||
// Update seek value and lower fill
|
||||
setRange(target, value) {
|
||||
if (!utils.is.htmlElement(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.value = value;
|
||||
|
||||
// Webkit range fill
|
||||
controls.updateRangeFill.call(this, target);
|
||||
},
|
||||
|
||||
// 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;
|
||||
|
||||
// Update value and label
|
||||
if (utils.is.htmlElement(progress)) {
|
||||
progress.value = value;
|
||||
|
||||
// Update text label inside
|
||||
const label = progress.getElementsByTagName('span')[0];
|
||||
if (utils.is.htmlElement(label)) {
|
||||
label.childNodes[0].nodeValue = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Update <progress> elements
|
||||
updateProgress(event) {
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = 0;
|
||||
|
||||
if (event) {
|
||||
switch (event.type) {
|
||||
// Video playing
|
||||
case 'timeupdate':
|
||||
case 'seeking':
|
||||
value = utils.getPercentage(this.currentTime, this.duration);
|
||||
|
||||
// Set seek range value only if it's a 'natural' time event
|
||||
if (event.type === 'timeupdate') {
|
||||
ui.setRange.call(this, this.elements.inputs.seek, value);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Check buffer status
|
||||
case 'playing':
|
||||
case 'progress':
|
||||
value = (() => {
|
||||
const { buffered } = this.media;
|
||||
|
||||
if (buffered && buffered.length) {
|
||||
// HTML5
|
||||
return utils.getPercentage(buffered.end(0), this.duration);
|
||||
} else if (utils.is.number(buffered)) {
|
||||
// YouTube returns between 0 and 1
|
||||
return buffered * 100;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})();
|
||||
|
||||
ui.setProgress.call(this, this.elements.display.buffer, value);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Update the displayed time
|
||||
updateTimeDisplay(value, element) {
|
||||
// Bail if there's no duration display
|
||||
if (!utils.is.htmlElement(element)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fallback to 0
|
||||
const time = !Number.isNaN(value) ? value : 0;
|
||||
|
||||
let secs = parseInt(time % 60, 10);
|
||||
let mins = parseInt((time / 60) % 60, 10);
|
||||
const hours = parseInt((time / 60 / 60) % 60, 10);
|
||||
|
||||
// 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}`;
|
||||
|
||||
// Render
|
||||
element.textContent = display;
|
||||
|
||||
// Return for looping
|
||||
return display;
|
||||
},
|
||||
|
||||
// Handle time change event
|
||||
timeUpdate(event) {
|
||||
// Duration
|
||||
ui.updateTimeDisplay.call(this, this.currentTime, this.elements.display.currentTime);
|
||||
|
||||
// Ignore updates while seeking
|
||||
if (event && event.type === 'timeupdate' && this.media.seeking) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Playing progress
|
||||
ui.updateProgress.call(this, event);
|
||||
},
|
||||
|
||||
// Update hover tooltip for seeking
|
||||
updateSeekTooltip(event) {
|
||||
// Bail if setting not true
|
||||
if (
|
||||
!this.config.tooltips.seek ||
|
||||
!utils.is.htmlElement(this.elements.inputs.seek) ||
|
||||
!utils.is.htmlElement(this.elements.display.seekTooltip) ||
|
||||
this.duration === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate percentage
|
||||
const clientRect = this.elements.inputs.seek.getBoundingClientRect();
|
||||
let percent = 0;
|
||||
const visible = `${this.config.classNames.tooltip}--visible`;
|
||||
|
||||
// Determine percentage, if already visible
|
||||
if (utils.is.event(event)) {
|
||||
percent = 100 / clientRect.width * (event.pageX - clientRect.left);
|
||||
} else if (utils.hasClass(this.elements.display.seekTooltip, visible)) {
|
||||
percent = this.elements.display.seekTooltip.style.left.replace('%', '');
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set bounds
|
||||
if (percent < 0) {
|
||||
percent = 0;
|
||||
} else if (percent > 100) {
|
||||
percent = 100;
|
||||
}
|
||||
|
||||
// Display the time a click would seek to
|
||||
ui.updateTimeDisplay.call(this, this.duration / 100 * percent, this.elements.display.seekTooltip);
|
||||
|
||||
// Set position
|
||||
this.elements.display.seekTooltip.style.left = `${percent}%`;
|
||||
|
||||
// Show/hide the tooltip
|
||||
// If the event is a moues in/out and percentage is inside bounds
|
||||
if (utils.is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {
|
||||
utils.toggleClass(this.elements.display.seekTooltip, visible, event.type === 'mouseenter');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default ui;
|
667
src/js/utils.js
Normal file
667
src/js/utils.js
Normal file
@ -0,0 +1,667 @@
|
||||
// ==========================================================================
|
||||
// Plyr utils
|
||||
// ==========================================================================
|
||||
|
||||
import support from './support';
|
||||
|
||||
const utils = {
|
||||
// Check variable types
|
||||
is: {
|
||||
object(input) {
|
||||
return this.getConstructor(input) === Object;
|
||||
},
|
||||
number(input) {
|
||||
return this.getConstructor(input) === Number && !Number.isNaN(input);
|
||||
},
|
||||
string(input) {
|
||||
return this.getConstructor(input) === String;
|
||||
},
|
||||
boolean(input) {
|
||||
return this.getConstructor(input) === Boolean;
|
||||
},
|
||||
function(input) {
|
||||
return this.getConstructor(input) === Function;
|
||||
},
|
||||
array(input) {
|
||||
return !this.undefined(input) && Array.isArray(input);
|
||||
},
|
||||
nodeList(input) {
|
||||
return !this.undefined(input) && input instanceof NodeList;
|
||||
},
|
||||
htmlElement(input) {
|
||||
return !this.undefined(input) && input instanceof HTMLElement;
|
||||
},
|
||||
event(input) {
|
||||
return !this.undefined(input) && input instanceof Event;
|
||||
},
|
||||
cue(input) {
|
||||
return this.instanceOf(input, window.TextTrackCue) || this.instanceOf(input, window.VTTCue);
|
||||
},
|
||||
track(input) {
|
||||
return (
|
||||
!this.undefined(input) && (this.instanceOf(input, window.TextTrack) || typeof input.kind === 'string')
|
||||
);
|
||||
},
|
||||
undefined(input) {
|
||||
return input !== null && typeof input === 'undefined';
|
||||
},
|
||||
empty(input) {
|
||||
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)
|
||||
);
|
||||
},
|
||||
getConstructor(input) {
|
||||
if (input === null || typeof input === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return input.constructor;
|
||||
},
|
||||
instanceOf(input, constructor) {
|
||||
return Boolean(input && constructor && input instanceof constructor);
|
||||
},
|
||||
},
|
||||
|
||||
// Unfortunately, due to mixed support, UA sniffing is required
|
||||
getBrowser() {
|
||||
return {
|
||||
isIE: /* @cc_on!@ */ false || !!document.documentMode,
|
||||
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
|
||||
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
|
||||
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
|
||||
};
|
||||
},
|
||||
|
||||
// Load an external script
|
||||
loadScript(url) {
|
||||
// Check script is not already referenced
|
||||
if (document.querySelectorAll(`script[src="${url}"]`).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = document.createElement('script');
|
||||
tag.src = url;
|
||||
|
||||
const firstScriptTag = document.getElementsByTagName('script')[0];
|
||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||
},
|
||||
|
||||
// Generate a random ID
|
||||
generateId(prefix) {
|
||||
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
|
||||
},
|
||||
|
||||
// Determine if we're in an iframe
|
||||
inFrame() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
// Wrap an element
|
||||
wrap(elements, wrapper) {
|
||||
// Convert `elements` to an array, if necessary.
|
||||
const targets = elements.length ? elements : [elements];
|
||||
|
||||
// Loops backwards to prevent having to clone the wrapper on the
|
||||
// first element (see `child` below).
|
||||
Array.from(targets)
|
||||
.reverse()
|
||||
.forEach((element, index) => {
|
||||
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
|
||||
|
||||
// Cache the current parent and sibling.
|
||||
const parent = element.parentNode;
|
||||
const sibling = element.nextSibling;
|
||||
|
||||
// Wrap the element (is automatically removed from its current
|
||||
// parent).
|
||||
child.appendChild(element);
|
||||
|
||||
// If the element had a sibling, insert the wrapper before
|
||||
// the sibling to maintain the HTML structure; otherwise, just
|
||||
// append it to the parent.
|
||||
if (sibling) {
|
||||
parent.insertBefore(child, sibling);
|
||||
} else {
|
||||
parent.appendChild(child);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 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>
|
||||
const element = document.createElement(type);
|
||||
|
||||
// Set all passed attributes
|
||||
if (utils.is.object(attributes)) {
|
||||
utils.setAttributes(element, attributes);
|
||||
}
|
||||
|
||||
// Add text node
|
||||
if (utils.is.string(text)) {
|
||||
element.textContent = text;
|
||||
}
|
||||
|
||||
// Return built element
|
||||
return element;
|
||||
},
|
||||
|
||||
// Insert a DocumentFragment
|
||||
insertElement(type, parent, attributes, text) {
|
||||
// Inject the new <element>
|
||||
parent.appendChild(utils.createElement(type, attributes, text));
|
||||
},
|
||||
|
||||
// Remove all child elements
|
||||
emptyElement(element) {
|
||||
let { length } = element.childNodes;
|
||||
|
||||
while (length > 0) {
|
||||
element.removeChild(element.lastChild);
|
||||
length -= 1;
|
||||
}
|
||||
},
|
||||
|
||||
// Set attributes
|
||||
setAttributes(element, attributes) {
|
||||
Object.keys(attributes).forEach(key => {
|
||||
element.setAttribute(key, attributes[key]);
|
||||
});
|
||||
},
|
||||
|
||||
// Get an attribute object from a string selector
|
||||
getAttributesFromSelector(sel, existingAttributes) {
|
||||
// For example:
|
||||
// '.test' to { class: 'test' }
|
||||
// '#test' to { id: 'test' }
|
||||
// '[data-test="test"]' to { 'data-test': 'test' }
|
||||
|
||||
if (!utils.is.string(sel) || utils.is.empty(sel)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const attributes = {};
|
||||
const existing = existingAttributes;
|
||||
|
||||
sel.split(',').forEach(s => {
|
||||
// Remove whitespace
|
||||
const selector = s.trim();
|
||||
const className = selector.replace('.', '');
|
||||
const stripped = selector.replace(/[[\]]/g, '');
|
||||
|
||||
// Get the parts and value
|
||||
const parts = stripped.split('=');
|
||||
const key = parts[0];
|
||||
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
|
||||
|
||||
// Get the first character
|
||||
const start = selector.charAt(0);
|
||||
|
||||
switch (start) {
|
||||
case '.':
|
||||
// Add to existing classname
|
||||
if (utils.is.object(existing) && utils.is.string(existing.class)) {
|
||||
existing.class += ` ${className}`;
|
||||
}
|
||||
|
||||
attributes.class = className;
|
||||
break;
|
||||
|
||||
case '#':
|
||||
// ID selector
|
||||
attributes.id = selector.replace('#', '');
|
||||
break;
|
||||
|
||||
case '[':
|
||||
// Attribute selector
|
||||
attributes[key] = value;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return attributes;
|
||||
},
|
||||
|
||||
// Toggle class on an element
|
||||
toggleClass(element, className, toggle) {
|
||||
if (utils.is.htmlElement(element)) {
|
||||
const contains = element.classList.contains(className);
|
||||
|
||||
element.classList[toggle ? 'add' : 'remove'](className);
|
||||
|
||||
return (toggle && !contains) || (!toggle && contains);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
// Has class name
|
||||
hasClass(element, className) {
|
||||
return utils.is.htmlElement(element) && element.classList.contains(className);
|
||||
},
|
||||
|
||||
// Element matches selector
|
||||
matches(element, selector) {
|
||||
const prototype = { Element };
|
||||
|
||||
function match() {
|
||||
return Array.from(document.querySelectorAll(selector)).includes(this);
|
||||
}
|
||||
|
||||
const matches =
|
||||
prototype.matches ||
|
||||
prototype.webkitMatchesSelector ||
|
||||
prototype.mozMatchesSelector ||
|
||||
prototype.msMatchesSelector ||
|
||||
match;
|
||||
|
||||
return matches.call(element, selector);
|
||||
},
|
||||
|
||||
// Find all elements
|
||||
getElements(selector) {
|
||||
return this.elements.container.querySelectorAll(selector);
|
||||
},
|
||||
|
||||
// Find a single element
|
||||
getElement(selector) {
|
||||
return this.elements.container.querySelector(selector);
|
||||
},
|
||||
|
||||
// Find the UI controls and store references in custom controls
|
||||
// TODO: Allow settings menus with custom controls
|
||||
findElements() {
|
||||
try {
|
||||
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
|
||||
|
||||
// Buttons
|
||||
this.elements.buttons = {
|
||||
play: utils.getElements.call(this, this.config.selectors.buttons.play),
|
||||
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
|
||||
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
|
||||
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
|
||||
forward: utils.getElement.call(this, this.config.selectors.buttons.forward),
|
||||
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
|
||||
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
|
||||
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
|
||||
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
|
||||
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
|
||||
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
|
||||
};
|
||||
|
||||
// Progress
|
||||
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
|
||||
|
||||
// Inputs
|
||||
this.elements.inputs = {
|
||||
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
|
||||
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
|
||||
};
|
||||
|
||||
// Display
|
||||
this.elements.display = {
|
||||
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
|
||||
duration: utils.getElement.call(this, this.config.selectors.display.duration),
|
||||
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
|
||||
};
|
||||
|
||||
// Seek tooltip
|
||||
if (utils.is.htmlElement(this.elements.progress)) {
|
||||
this.elements.display.seekTooltip = this.elements.progress.querySelector(
|
||||
`.${this.config.classNames.tooltip}`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Log it
|
||||
this.warn('It looks like there is a problem with your custom controls HTML', error);
|
||||
|
||||
// Restore native video controls
|
||||
this.toggleNativeControls(true);
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Get the focused element
|
||||
getFocusElement() {
|
||||
let focused = document.activeElement;
|
||||
|
||||
if (!focused || focused === document.body) {
|
||||
focused = null;
|
||||
} else {
|
||||
focused = document.querySelector(':focus');
|
||||
}
|
||||
|
||||
return focused;
|
||||
},
|
||||
|
||||
// Trap focus inside container
|
||||
trapFocus() {
|
||||
const tabbables = utils.getElements.call(this, 'input:not([disabled]), button:not([disabled])');
|
||||
const first = tabbables[0];
|
||||
const last = tabbables[tabbables.length - 1];
|
||||
|
||||
utils.on(
|
||||
this.elements.container,
|
||||
'keydown',
|
||||
event => {
|
||||
// If it is tab
|
||||
if (event.which === 9 && this.fullscreen.active) {
|
||||
if (event.target === last && !event.shiftKey) {
|
||||
// Move focus to first element that can be tabbed if Shift isn't used
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
} else if (event.target === first && event.shiftKey) {
|
||||
// Move focus to last element that can be tabbed if Shift is used
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
},
|
||||
|
||||
// Bind along with custom handler
|
||||
proxy(element, eventName, customListener, defaultListener, passive, capture) {
|
||||
utils.on(
|
||||
element,
|
||||
eventName,
|
||||
event => {
|
||||
if (customListener) {
|
||||
customListener.apply(element, [event]);
|
||||
}
|
||||
defaultListener.apply(element, [event]);
|
||||
},
|
||||
passive,
|
||||
capture
|
||||
);
|
||||
},
|
||||
|
||||
// Toggle event listener
|
||||
toggleListener(elements, event, callback, toggle, passive, capture) {
|
||||
// Bail if no elements
|
||||
if (elements === null || utils.is.undefined(elements)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If a nodelist is passed, call itself on each node
|
||||
if (elements instanceof NodeList) {
|
||||
// Create listener for each node
|
||||
Array.from(elements).forEach(element => {
|
||||
if (element instanceof Node) {
|
||||
utils.toggleListener.call(null, element, event, callback, toggle, passive, capture);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow multiple events
|
||||
const events = event.split(' ');
|
||||
|
||||
// Build options
|
||||
// Default to just capture boolean
|
||||
let options = utils.is.boolean(capture) ? capture : false;
|
||||
|
||||
// If passive events listeners are supported
|
||||
if (support.passiveListeners) {
|
||||
options = {
|
||||
// Whether the listener can be passive (i.e. default never prevented)
|
||||
passive: utils.is.boolean(passive) ? passive : true,
|
||||
// Whether the listener is a capturing listener or not
|
||||
capture: utils.is.boolean(capture) ? capture : false,
|
||||
};
|
||||
}
|
||||
|
||||
// If a single node is passed, bind the event listener
|
||||
events.forEach(type => {
|
||||
elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
|
||||
});
|
||||
},
|
||||
|
||||
// Bind event handler
|
||||
on(element, events, callback, passive, capture) {
|
||||
utils.toggleListener(element, events, callback, true, passive, capture);
|
||||
},
|
||||
|
||||
// Unbind event handler
|
||||
off(element, events, callback, passive, capture) {
|
||||
utils.toggleListener(element, events, callback, false, passive, capture);
|
||||
},
|
||||
|
||||
// Trigger event
|
||||
dispatchEvent(element, type, bubbles, properties) {
|
||||
// Bail if no element
|
||||
if (!element || !type) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and dispatch the event
|
||||
const event = new CustomEvent(type, {
|
||||
bubbles: utils.is.boolean(bubbles) ? bubbles : false,
|
||||
detail: Object.assign({}, properties, {
|
||||
plyr: this instanceof Plyr ? this : null,
|
||||
}),
|
||||
});
|
||||
|
||||
// Dispatch the event
|
||||
element.dispatchEvent(event);
|
||||
},
|
||||
|
||||
// Toggle aria-pressed state on a toggle button
|
||||
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
|
||||
toggleState(target, state) {
|
||||
// Bail if no target
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get state
|
||||
const newState = utils.is.boolean(state) ? state : !target.getAttribute('aria-pressed');
|
||||
|
||||
// Set the attribute on target
|
||||
target.setAttribute('aria-pressed', newState);
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
// Get percentage
|
||||
getPercentage(current, max) {
|
||||
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
|
||||
return 0;
|
||||
}
|
||||
return (current / max * 100).toFixed(2);
|
||||
},
|
||||
|
||||
// Deep extend/merge destination object with N more objects
|
||||
// http://andrewdupont.net/2009/08/28/deep-extending-objects-in-javascript/
|
||||
// Removed call to arguments.callee (used explicit function name instead)
|
||||
extend(...objects) {
|
||||
const { length } = objects;
|
||||
|
||||
// Bail if nothing to merge
|
||||
if (!length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return first if specified but nothing to merge
|
||||
if (length === 1) {
|
||||
return objects[0];
|
||||
}
|
||||
|
||||
// First object is the destination
|
||||
let destination = Array.prototype.shift.call(objects);
|
||||
if (!utils.is.object(destination)) {
|
||||
destination = {};
|
||||
}
|
||||
|
||||
// Loop through all objects to merge
|
||||
objects.forEach(source => {
|
||||
if (!utils.is.object(source)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(source).forEach(property => {
|
||||
if (source[property] && source[property].constructor && source[property].constructor === Object) {
|
||||
destination[property] = destination[property] || {};
|
||||
utils.extend(destination[property], source[property]);
|
||||
} else {
|
||||
destination[property] = source[property];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return destination;
|
||||
},
|
||||
|
||||
// Parse YouTube ID from URL
|
||||
parseYouTubeId(url) {
|
||||
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
|
||||
return url.match(regex) ? RegExp.$2 : url;
|
||||
},
|
||||
|
||||
// Parse Vimeo ID from URL
|
||||
parseVimeoId(url) {
|
||||
if (utils.is.number(Number(url))) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
|
||||
return url.match(regex) ? RegExp.$2 : url;
|
||||
},
|
||||
|
||||
// Convert object to URL parameters
|
||||
buildUrlParameters(input) {
|
||||
if (!utils.is.object(input)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Object.keys(input)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(input[key])}`)
|
||||
.join('&');
|
||||
},
|
||||
|
||||
// Remove HTML from a string
|
||||
stripHTML(source) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
const element = document.createElement('div');
|
||||
fragment.appendChild(element);
|
||||
element.innerHTML = source;
|
||||
return fragment.firstChild.innerText;
|
||||
},
|
||||
|
||||
// Load an SVG sprite
|
||||
loadSprite(url, id) {
|
||||
if (typeof url !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix = 'cache-';
|
||||
const hasId = typeof id === 'string';
|
||||
let isCached = false;
|
||||
|
||||
function updateSprite(data) {
|
||||
// Inject content
|
||||
this.innerHTML = data;
|
||||
|
||||
// Inject the SVG to the body
|
||||
document.body.insertBefore(this, document.body.childNodes[0]);
|
||||
}
|
||||
|
||||
// Only load once
|
||||
if (!hasId || !document.querySelectorAll(`#${id}`).length) {
|
||||
// Create container
|
||||
const container = document.createElement('div');
|
||||
container.setAttribute('hidden', '');
|
||||
|
||||
if (hasId) {
|
||||
container.setAttribute('id', id);
|
||||
}
|
||||
|
||||
// Check in cache
|
||||
if (support.storage) {
|
||||
const cached = window.localStorage.getItem(prefix + id);
|
||||
isCached = cached !== null;
|
||||
|
||||
if (isCached) {
|
||||
const data = JSON.parse(cached);
|
||||
updateSprite.call(container, data.content);
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
// XHR for Chrome/Firefox/Opera/Safari
|
||||
if ('withCredentials' in xhr) {
|
||||
xhr.open('GET', url, true);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Once loaded, inject to container and body
|
||||
xhr.onload = () => {
|
||||
if (support.storage) {
|
||||
window.localStorage.setItem(
|
||||
prefix + id,
|
||||
JSON.stringify({
|
||||
content: xhr.responseText,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updateSprite.call(container, xhr.responseText);
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
}
|
||||
},
|
||||
|
||||
// Get the transition end event
|
||||
transitionEnd: (() => {
|
||||
const element = document.createElement('span');
|
||||
|
||||
const events = {
|
||||
WebkitTransition: 'webkitTransitionEnd',
|
||||
MozTransition: 'transitionend',
|
||||
OTransition: 'oTransitionEnd otransitionend',
|
||||
transition: 'transitionend',
|
||||
};
|
||||
|
||||
const type = Object.keys(events).find(event => element.style[event] !== undefined);
|
||||
|
||||
return typeof type === 'string' ? type : false;
|
||||
})(),
|
||||
};
|
||||
|
||||
export default utils;
|
@ -7,7 +7,6 @@
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
min-width: 200px;
|
||||
overflow: hidden;
|
||||
font-family: @plyr-font-family;
|
||||
font-weight: @plyr-font-weight-normal;
|
||||
direction: ltr;
|
||||
|
@ -58,7 +58,7 @@
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
color: @plyr-video-control-color;
|
||||
transition: all 0.4s ease-in-out;
|
||||
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
|
||||
|
||||
.plyr__control {
|
||||
svg {
|
||||
|
@ -16,6 +16,13 @@
|
||||
border: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// Vimeo hack
|
||||
> div {
|
||||
position: relative;
|
||||
padding-bottom: 200%;
|
||||
transform: translateY(-35.95%);
|
||||
}
|
||||
}
|
||||
// To allow mouse events to be captured if full support
|
||||
.plyr--full-ui .plyr__video-embed iframe {
|
||||
|
@ -46,10 +46,8 @@
|
||||
|
||||
// Microsoft
|
||||
&::-ms-track {
|
||||
height: @plyr-range-track-height;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: transparent;
|
||||
.plyr-range-track();
|
||||
}
|
||||
|
||||
&::-ms-fill-upper {
|
||||
|
@ -2,6 +2,10 @@
|
||||
// Video styles
|
||||
// --------------------------------------------------------------
|
||||
|
||||
.plyr--video {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plyr__video-wrapper {
|
||||
position: relative;
|
||||
background: #000;
|
||||
|
Loading…
x
Reference in New Issue
Block a user