Compare commits

...

67 Commits

Author SHA1 Message Date
Sam Potts 33a11fb53a v3.3.7 2018-05-09 09:50:22 +10:00
Sam Potts d1d41ca49a Merge branch 'master' of github.com:sampotts/plyr 2018-05-09 09:48:52 +10:00
Sam Potts c06e0ee5e9 Grid tweak 2018-05-09 09:48:46 +10:00
Sam Potts 83f80ccc40 Merge pull request #950 from friday/poster-fixes
Poster fixes
2018-05-09 09:44:36 +10:00
Albin Larsson 069065ea3a Fix #946 - poster getting click events 2018-05-08 16:50:40 +02:00
Albin Larsson 1672e78041 Fix poster being stretched 2018-05-08 16:49:32 +02:00
Sam Potts f687b81b70 v3.3.6 2018-05-08 13:18:30 +10:00
Sam Potts bbb11e611e Vimeo options, docs for multiple players 2018-05-08 13:12:39 +10:00
Sam Potts 90919411e9 Use div for poster, Vimeo fixes, Tooltip fixes 2018-05-08 12:57:24 +10:00
Sam Potts 1491b017a0 Setup multiple players 2018-05-06 16:18:10 +10:00
Sam Potts 1655150092 v3.3.5 2018-05-06 01:32:51 +10:00
Sam Potts ceb6c9a100 v3.3.3 2018-05-06 01:16:41 +10:00
Sam Potts 00bbce08fb Reverted menu change 2018-05-06 01:14:41 +10:00
Sam Potts 91a4b86860 Small bug fixes 2018-05-06 01:03:38 +10:00
Sam Potts 5aece6fa06 Merge 2018-05-06 00:50:02 +10:00
Sam Potts a70b94afe2 Merge branch 'master' of github.com:sampotts/plyr 2018-05-06 00:49:22 +10:00
Sam Potts 9ebc2719d3 v3.3.0 2018-05-06 00:49:12 +10:00
Sam Potts b46aae1833 Merge pull request #939 from Billybobbonnet/patch-1
Added 480p to SD labels
2018-05-03 20:28:03 +10:00
Antoine Cordelois 30e6a40865 Added 480p to SD labels 2018-05-03 11:42:09 +02:00
Sam Potts 5ca769807e Merge pull request #923 from friday/922
Only add hideControls class if config.hideControls is truthy
2018-04-27 20:07:18 +10:00
Sam Potts 72a71a605b Fix for default timestamp 2018-04-27 20:06:14 +10:00
Sam Potts 261cd086c7 Update readme.md 2018-04-27 12:44:58 +10:00
Albin Larsson 9e19b526b9 Only add hideControls class if config.hideControls is truthy 2018-04-26 17:51:14 +02:00
Sam Potts 6c617a0ef1 Readme fix 2018-04-27 01:12:04 +10:00
Sam Potts a812650fea v3.2.4 2018-04-27 00:47:51 +10:00
Sam Potts fec7a77d6f v3.2.3 2018-04-25 20:02:36 +10:00
Sam Potts 971e261067 Fix for iOS 9 throwing error for name property in fullscreen API (fixes #908) 2018-04-25 19:59:22 +10:00
Sam Potts 27407ba021 v3.2.2 2018-04-25 19:46:39 +10:00
Sam Potts ef8e58ede4 Fix for hidden buffer and incorrect use of aria-hidden 2018-04-25 19:40:23 +10:00
Sam Potts f1b275aedc v3.2.1 2018-04-23 00:53:54 +10:00
Sam Potts b647af256c More a11y stuff and context menu fix 2018-04-23 00:01:19 +10:00
Sam Potts d2e9ed3467 Merge 2018-04-18 18:34:59 +10:00
Sam Potts 5b39986835 Merge branch 'master' of github.com:sampotts/plyr 2018-04-18 18:29:50 +10:00
Sam Potts a97b08e8ea ARIA and Vimeo fixes 2018-04-18 18:29:43 +10:00
Sam Potts 56d1be9447 Merge pull request #903 from friday/901
Show captions even if toggle button is omitted from controls
2018-04-18 08:49:05 +10:00
Sam Potts a241cb5215 Merge pull request #904 from friday/881
Fullscreen aria-pressed event listened fix for Chrome
2018-04-18 08:48:08 +10:00
Albin Larsson 042b1a8294 Fullscreen aria-pressed event listened fix for Chrome 2018-04-17 20:28:47 +02:00
Albin Larsson 6d79b8cd4c Don't require captions toggle button to be enabled in order to show captions 2018-04-17 18:59:19 +02:00
Sam Potts 88d766aeae v3.2.0 2018-04-17 23:54:38 +10:00
Sam Potts 119b471b84 More bug fixes 2018-04-17 23:51:23 +10:00
Sam Potts 7f079e0ec3 Fix for playing false positive (fixes #898) 2018-04-17 22:52:46 +10:00
Sam Potts 46fe3eecff Fixed bug for captions with no srclang and labels and improved logic (fixes #875) 2018-04-17 22:49:28 +10:00
Sam Potts 3061a701d5 PR merge 2018-04-14 14:58:09 +10:00
Sam Potts e45109e1d7 Merge branch 'master' of github.com:sampotts/plyr 2018-04-14 14:48:20 +10:00
Sam Potts e138e6d51e Merge pull request #895 from nicolasthy/patch-1
Fix IE10 split error
2018-04-14 14:47:57 +10:00
Nicolas Thiry aef1363b04 Fix IE10 with default captions.language 2018-04-13 14:44:05 +02:00
Nicolas Thiry 766dd03d81 Fix IE10 split error
On IE10, Plyr throws the error `Unable to get property 'split' of undefined or null reference`. This fixes the case when `window.navigator.language` is null and can't use the `split()` function.
2018-04-12 22:12:12 +02:00
Sam Potts ab393651ec Merge branch 'master' of github.com:sampotts/plyr 2018-04-11 23:44:44 +10:00
Sam Potts ffd265d0ae Merge pull request #888 from Antonio-Laguna/master
Safer check for active caption
2018-04-11 23:42:40 +10:00
Antonio Laguna 72155472dd Safer check for active caption 2018-04-11 15:39:12 +02:00
Sam Potts 9b7170834e Merge pull request #887 from danielsarin/use-i18n-for-normal-speed
Add i18n support for "Normal" value in speed options
2018-04-11 22:43:47 +10:00
Daniel Sarin 3e57a87bf7 Add i18n support for "Normal" value in speed options 2018-04-11 15:39:23 +03:00
Sam Potts a15d1c9f1c Merge pull request #886 from danielsarin/increate-menu-z-index
Increase menu container z-index to be higher than controls
2018-04-11 22:23:46 +10:00
Daniel Sarin a095a64f90 Increase menu container z-index to be higher than controls 2018-04-11 15:13:34 +03:00
Sam Potts 2374d6b1c4 Merge branch 'master' of github.com:sampotts/plyr 2018-04-11 21:52:36 +10:00
Sam Potts 5ed3ff9084 Restore paused state after seek 2018-04-11 21:52:31 +10:00
Sam Potts 385be55510 Merge pull request #874 from friday/873
Fixes issue leaving fullscreen in Chrome using button
2018-04-10 17:21:15 +10:00
Albin Larsson 3082d0d128 Fixes #873 Can't leave fullscreen in Chrome (using button) 2018-04-05 20:29:01 +02:00
Sam Potts f7e242f054 Merge pull request #871 from friday/867
Fix #867: Add custom property fallback
2018-04-05 09:19:46 +10:00
Sam Potts 2874505004 Merge pull request #868 from friday/null-no-controls
Fix string "null" being appended after the video if controls argument is empty.
2018-04-05 09:19:05 +10:00
Albin Larsson ed9e0c13d7 Fix #867: Add custom property fallback 2018-04-05 00:33:09 +02:00
Albin Larsson 10be94fa99 Fix 'null' being appended after the video if controls is empty array 2018-04-04 21:33:14 +02:00
Sam Potts ee79c46145 Merge branch 'master' of github.com:sampotts/plyr 2018-04-04 16:50:06 +10:00
Sam Potts 384010a2c0 Style fixes 2018-04-04 16:50:00 +10:00
Sam Potts 1e47019122 Merge pull request #863 from friday/data-plyr-config-no-options
Fix loading data-plyr-config when initiating Plyr without any options
2018-04-04 11:33:30 +10:00
Albin Larsson 536c65e82c Fix loading data-plyr-config when initiating Plyr without any options 2018-04-04 03:16:25 +02:00
Sam Potts cdf14932ec Changelog 2018-04-03 23:03:16 +10:00
53 changed files with 7735 additions and 5312 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
"demo": { "demo": {
"sass": { "sass": {
"demo.css": "demo/src/sass/bundles/demo.scss", "demo.css": "demo/src/sass/bundles/demo.scss",
"error.css": "demo/src/sass/bundles/error.csss" "error.css": "demo/src/sass/bundles/error.scss"
}, },
"js": { "js": {
"demo.js": "demo/src/js/demo.js" "demo.js": "demo/src/js/demo.js"
+87
View File
@@ -1,3 +1,90 @@
# v3.3.7
* Poster fixes (thanks @friday)
* Grid tweak
# v3.3.6
* Vimeo fixes for mute state
* Vimeo ID fix (fixes #945)
* Use `<div>` for poster container
* Tooltip fixes for unicode languages (fixes #943)
# v3.3.5
* Removed `.load()` call as it breaks HLS (see #870)
# v3.3.4
* Fix for controls sometimes not showing while video is playing
* Fixed logic for show home tab on option select
# v3.3.3
* Reverted change to show home tab on option select due to usability regression
# v3.3.2
* Fix for ads running in audio
* Fix for setting poster on source change
## v3.3.0
* Now using a custom poster image element to hide the YouTube play button and give more control over when the poster image shows
* Renamed `showPosterOnEnd` to `resetOnEnd` as it makes more sense and now works for all players and does not reload media
* Fix for same domain SVG URLs (raised by Jochem in Slack)
* [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/Window/URL) is polyfill now required
* Added pause className (fixes #941)
* Button height set in CSS (auto) (fixes #928)
* Don't autoplay cloned original media (fixes #936)
* Return to the home menu pane after selecting an option
## v3.2.4
* Fix issue wher player never reports as ready if controls is empty array
* Fix issue where screen reader labels were removed from time displays
* Fix issue where custom controls placeholders were not populated
* Custom controls HTML example updated
* Fix for aria-label being set to the initial state on toggle buttons, overriding the inner labels
* Fix for hidden mute button on iOS (not functional for Vimeo due to API limitations) (fixes #656)
## v3.2.3
* Fix for iOS 9 throwing error for `name` property in fullscreen API (fixes #908)
## v3.2.2
* Fix for regression in 3.2.1 resulting in hidden buffer display (fixes #920)
* Cleaned up incorrect use of `aria-hidden` attribute
## v3.2.1
* Accessibility improvements for the controls (part of #905 fixes)
* Fix for context menu showing on YouTube (thanks Anthony Recenello in Slack)
* Vimeo fix for their API not returning the right duration until playback begins (fixes #891)
## v3.2.0
* Fullscreen fixes (thanks @friday)
* Menu fix for if speed not in config
* Menu z-index fix (thanks @danielsarin)
* i18n fix for missing "Normal" string (thanks @danielsarin)
* Safer check for active caption (thanks @Antonio-Laguna)
* Add custom property fallback (thanks @friday)
* Fixed bug for captions with no srclang and labels and improved logic (fixes #875)
* Fix for `playing` false positive (fixes #898)
* Fix for IE issue with navigator.language (thanks @nicolasthy) (fixes #893)
* Fix for Vimeo controls missing on iOS (thanks @verde-io) (fixes #807)
* Fix for double vimeo caption rendering (fixes #877)
## v3.1.0
* Styling fixes
## v3.1.0-beta.2
* YouTube playback speed fixes
## v3.1.0-beta.1 ## v3.1.0-beta.1
* HTML5 quality selection * HTML5 quality selection
+3 -1
View File
@@ -59,6 +59,7 @@ i18n: {
captions: 'Captions', captions: 'Captions',
settings: 'Settings', settings: 'Settings',
speed: 'Speed', speed: 'Speed',
normal: 'Normal',
quality: 'Quality', quality: 'Quality',
loop: 'Loop', loop: 'Loop',
start: 'Start', start: 'Start',
@@ -120,7 +121,8 @@ const controls = `
<progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress> <progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress>
<span role="tooltip" class="plyr__tooltip">00:00</span> <span role="tooltip" class="plyr__tooltip">00:00</span>
</div> </div>
<div class="plyr__time">00:00</div> <div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute"> <button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg> <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg> <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
+1 -1
View File
File diff suppressed because one or more lines are too long
+87 -21
View File
@@ -245,7 +245,13 @@ function objectFrozen(obj) {
} }
function truncate(str, max) { function truncate(str, max) {
return !max || str.length <= max ? str : str.substr(0, max) + '\u2026'; if (typeof max !== 'number') {
throw new Error('2nd argument to `truncate` function should be a number');
}
if (typeof str !== 'string' || max === 0) {
return str;
}
return str.length <= max ? str : str.substr(0, max) + '\u2026';
} }
/** /**
@@ -544,10 +550,9 @@ function jsonSize(value) {
} }
function serializeValue(value) { function serializeValue(value) {
var maxLength = 40;
if (typeof value === 'string') { if (typeof value === 'string') {
return value.length <= maxLength ? value : value.substr(0, maxLength - 1) + '\u2026'; var maxLength = 40;
return truncate(value, maxLength);
} else if ( } else if (
typeof value === 'number' || typeof value === 'number' ||
typeof value === 'boolean' || typeof value === 'boolean' ||
@@ -1777,7 +1782,7 @@ Raven.prototype = {
// webpack (using a build step causes webpack #1617). Grunt verifies that // webpack (using a build step causes webpack #1617). Grunt verifies that
// this value matches package.json during build. // this value matches package.json during build.
// See: https://github.com/getsentry/raven-js/issues/465 // See: https://github.com/getsentry/raven-js/issues/465
VERSION: '3.24.0', VERSION: '3.24.2',
debug: false, debug: false,
@@ -2066,7 +2071,11 @@ Raven.prototype = {
*/ */
_promiseRejectionHandler: function(event) { _promiseRejectionHandler: function(event) {
this._logDebug('debug', 'Raven caught unhandled promise rejection:', event); this._logDebug('debug', 'Raven caught unhandled promise rejection:', event);
this.captureException(event.reason); this.captureException(event.reason, {
extra: {
unhandledPromiseRejection: true
}
});
}, },
/** /**
@@ -2207,6 +2216,14 @@ Raven.prototype = {
// stack[0] is `throw new Error(msg)` call itself, we are interested in the frame that was just before that, stack[1] // stack[0] is `throw new Error(msg)` call itself, we are interested in the frame that was just before that, stack[1]
var initialCall = isArray$1(stack.stack) && stack.stack[1]; var initialCall = isArray$1(stack.stack) && stack.stack[1];
// if stack[1] is `Raven.captureException`, it means that someone passed a string to it and we redirected that call
// to be handled by `captureMessage`, thus `initialCall` is the 3rd one, not 2nd
// initialCall => captureException(string) => captureMessage(string)
if (initialCall && initialCall.func === 'Raven.captureException') {
initialCall = stack.stack[2];
}
var fileurl = (initialCall && initialCall.url) || ''; var fileurl = (initialCall && initialCall.url) || '';
if ( if (
@@ -3004,17 +3021,30 @@ Raven.prototype = {
status_code: null status_code: null
}; };
return origFetch.apply(this, args).then(function(response) { return origFetch
fetchData.status_code = response.status; .apply(this, args)
.then(function(response) {
fetchData.status_code = response.status;
self.captureBreadcrumb({ self.captureBreadcrumb({
type: 'http', type: 'http',
category: 'fetch', category: 'fetch',
data: fetchData data: fetchData
});
return response;
})
['catch'](function(err) {
// if there is an error performing the request
self.captureBreadcrumb({
type: 'http',
category: 'fetch',
data: fetchData,
level: 'error'
});
throw err;
}); });
return response;
});
}; };
}, },
wrappedBuiltIns wrappedBuiltIns
@@ -3027,7 +3057,7 @@ Raven.prototype = {
if (_document.addEventListener) { if (_document.addEventListener) {
_document.addEventListener('click', self._breadcrumbEventHandler('click'), false); _document.addEventListener('click', self._breadcrumbEventHandler('click'), false);
_document.addEventListener('keypress', self._keypressEventHandler(), false); _document.addEventListener('keypress', self._keypressEventHandler(), false);
} else { } else if (_document.attachEvent) {
// IE8 Compatibility // IE8 Compatibility
_document.attachEvent('onclick', self._breadcrumbEventHandler('click')); _document.attachEvent('onclick', self._breadcrumbEventHandler('click'));
_document.attachEvent('onkeypress', self._keypressEventHandler()); _document.attachEvent('onkeypress', self._keypressEventHandler());
@@ -3750,7 +3780,11 @@ Raven.prototype = {
}, },
_logDebug: function(level) { _logDebug: function(level) {
if (this._originalConsoleMethods[level] && this.debug) { // We allow `Raven.debug` and `Raven.config(DSN, { debug: true })` to not make backward incompatible API change
if (
this._originalConsoleMethods[level] &&
(this.debug || this._globalOptions.debug)
) {
// In IE<10 console methods do not have their own 'apply' method // In IE<10 console methods do not have their own 'apply' method
Function.prototype.apply.call( Function.prototype.apply.call(
this._originalConsoleMethods[level], this._originalConsoleMethods[level],
@@ -3823,11 +3857,11 @@ var singleton = Raven$1;
* const someAppReporter = new Raven.Client(); * const someAppReporter = new Raven.Client();
* const someOtherAppReporter = new Raven.Client(); * const someOtherAppReporter = new Raven.Client();
* *
* someAppReporter('__DSN__', { * someAppReporter.config('__DSN__', {
* ...config goes here * ...config goes here
* }); * });
* *
* someOtherAppReporter('__OTHER_DSN__', { * someOtherAppReporter.config('__OTHER_DSN__', {
* ...config goes here * ...config goes here
* }); * });
* *
@@ -3887,7 +3921,7 @@ singleton.Client = Client;
}); });
// Setup the player // Setup the player
var player = new Plyr('video', { var player = new Plyr('#player', {
debug: true, debug: true,
title: 'View From A Blue Moon', title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg', iconUrl: '../dist/plyr.svg',
@@ -3914,6 +3948,39 @@ singleton.Client = Client;
'airplay', 'airplay',
'fullscreen', 'fullscreen',
], */ ], */
/* i18n: {
restart: '重新開始',
rewind: '快退{seektime}秒',
play: '播放',
pause: '暫停',
fastForward: '快進{seektime}秒',
seek: '尋求',
played: '發揮',
buffered: '緩衝的',
currentTime: '當前時間戳',
duration: '長短',
volume: '音量',
mute: '靜音',
unmute: '取消靜音',
enableCaptions: '開啟字幕',
disableCaptions: '關閉字幕',
enterFullscreen: '進入全螢幕',
exitFullscreen: '退出全螢幕',
frameTitle: '球員為{title}',
captions: '字幕',
settings: '設定',
speed: '速度',
normal: '正常',
quality: '質量',
loop: '循環',
start: 'Start',
end: 'End',
all: 'All',
reset: '重啟',
disabled: '殘',
enabled: '啟用',
advertisement: '廣告',
}, */
captions: { captions: {
active: true active: true
}, },
@@ -4011,7 +4078,6 @@ singleton.Client = Client;
case types.youtube: case types.youtube:
player.source = { player.source = {
type: 'video', type: 'video',
title: 'View From A Blue Moon',
sources: [{ sources: [{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY', src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube' provider: 'youtube'
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+7 -1
View File
@@ -6,8 +6,14 @@
<title>Doh. Looks like something went wrong.</title> <title>Doh. Looks like something went wrong.</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Icons -->
<link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico">
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16">
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png">
<!-- Docs styles --> <!-- Docs styles -->
<link rel="stylesheet" href="dist/error.css"> <link rel="stylesheet" href="dist/error.css?v=2">
<!-- Preload --> <!-- Preload -->
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2"> <link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
+4 -4
View File
@@ -27,7 +27,7 @@
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<!-- Docs styles --> <!-- Docs styles -->
<link rel="stylesheet" href="dist/demo.css"> <link rel="stylesheet" href="dist/demo.css?v=2">
<!-- Preload --> <!-- Preload -->
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2"> <link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
@@ -114,7 +114,7 @@
<title>HTML5</title> <title>HTML5</title>
<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> <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> </svg>
<a href="http://viewfromabluemoon.com/" target="_blank">View From A Blue Moon</a> &copy; Brainfarm <a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank">View From A Blue Moon</a> &copy; Brainfarm
</small> </small>
</li> </li>
<li class="plyr__cite plyr__cite--audio" hidden> <li class="plyr__cite plyr__cite--audio" hidden>
@@ -141,7 +141,7 @@
</li> </li>
<li class="plyr__cite plyr__cite--vimeo" hidden> <li class="plyr__cite plyr__cite--vimeo" hidden>
<small> <small>
<a href="https://vimeo.com/ondemand/viewfromabluemoon4k" target="_blank">View From A Blue Moon</a> on&nbsp; <a href="https://vimeo.com/76979871" target="_blank">The New Vimeo Player</a> on&nbsp;
<span class="color--vimeo"> <span class="color--vimeo">
<svg class="icon" role="presentation"> <svg class="icon" role="presentation">
<title>Vimeo</title> <title>Vimeo</title>
@@ -171,7 +171,7 @@
</aside> </aside>
<!-- Polyfills --> <!-- Polyfills -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values" <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<!-- Plyr core script --> <!-- Plyr core script -->
+34 -2
View File
@@ -47,7 +47,7 @@ import Raven from 'raven-js';
}); });
// Setup the player // Setup the player
const player = new Plyr('video', { const player = new Plyr('#player', {
debug: true, debug: true,
title: 'View From A Blue Moon', title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg', iconUrl: '../dist/plyr.svg',
@@ -74,6 +74,39 @@ import Raven from 'raven-js';
'airplay', 'airplay',
'fullscreen', 'fullscreen',
], */ ], */
/* i18n: {
restart: '重新開始',
rewind: '快退{seektime}秒',
play: '播放',
pause: '暫停',
fastForward: '快進{seektime}秒',
seek: '尋求',
played: '發揮',
buffered: '緩衝的',
currentTime: '當前時間戳',
duration: '長短',
volume: '音量',
mute: '靜音',
unmute: '取消靜音',
enableCaptions: '開啟字幕',
disableCaptions: '關閉字幕',
enterFullscreen: '進入全螢幕',
exitFullscreen: '退出全螢幕',
frameTitle: '球員為{title}',
captions: '字幕',
settings: '設定',
speed: '速度',
normal: '正常',
quality: '質量',
loop: '循環',
start: 'Start',
end: 'End',
all: 'All',
reset: '重啟',
disabled: '殘',
enabled: '啟用',
advertisement: '廣告',
}, */
captions: { captions: {
active: true, active: true,
}, },
@@ -182,7 +215,6 @@ import Raven from 'raven-js';
case types.youtube: case types.youtube:
player.source = { player.source = {
type: 'video', type: 'video',
title: 'View From A Blue Moon',
sources: [{ sources: [{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY', src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube', provider: 'youtube',
+6 -6
View File
@@ -3,12 +3,6 @@
// ========================================================================== // ==========================================================================
@charset 'UTF-8'; @charset 'UTF-8';
// Libs
@import '../lib/fontface';
@import '../lib/mixins';
@import '../lib/normalize';
@import '../lib/reset';
// Settings // Settings
@import '../settings/colors'; @import '../settings/colors';
@import '../settings/cosmetic'; @import '../settings/cosmetic';
@@ -17,6 +11,12 @@
@import '../settings/spacing'; @import '../settings/spacing';
@import '../settings/type'; @import '../settings/type';
// Libs
@import '../lib/fontface';
@import '../lib/mixins';
@import '../lib/normalize';
@import '../lib/reset';
// Layout // Layout
@import '../layout/error'; @import '../layout/error';
+1
View File
@@ -29,6 +29,7 @@ video {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
z-index: 3;
} }
// Style full supported player // Style full supported player
+1 -1
View File
@@ -2,4 +2,4 @@
// Layout // Layout
// ========================================================================== // ==========================================================================
$container-max-width: 1280px; $container-max-width: 1260px;
+2 -1
View File
@@ -6,5 +6,6 @@ h1 {
@include font-size($font-size-h1); @include font-size($font-size-h1);
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-headings; letter-spacing: $letter-spacing-headings;
margin: 0 0 ($spacing-base / 2); line-height: 1.2;
margin: 0 0 $spacing-base;
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+2649 -2390
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2649 -2390
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+7 -6
View File
@@ -129,7 +129,7 @@ const build = {
tasks.js.push(name); tasks.js.push(name);
const { output } = paths[bundle]; const { output } = paths[bundle];
gulp.task(name, () => return gulp.task(name, () =>
gulp gulp
.src(bundles[bundle].js[key]) .src(bundles[bundle].js[key])
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
@@ -162,7 +162,7 @@ const build = {
const name = `sass:${key}`; const name = `sass:${key}`;
tasks.sass.push(name); tasks.sass.push(name);
gulp.task(name, () => return gulp.task(name, () =>
gulp gulp
.src(bundles[bundle].sass[key]) .src(bundles[bundle].sass[key])
.pipe(sass()) .pipe(sass())
@@ -180,7 +180,7 @@ const build = {
tasks.sprite.push(name); tasks.sprite.push(name);
// Process Icons // Process Icons
gulp.task(name, () => return gulp.task(name, () =>
gulp gulp
.src(paths[bundle].src.sprite) .src(paths[bundle].src.sprite)
.pipe( .pipe(
@@ -287,7 +287,8 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
'plyr.polyfilled.js', 'plyr.polyfilled.js',
'defaults.js', 'defaults.js',
]; ];
gulp
return gulp
.src(files.map(file => path.join(root, `src/js/${file}`))) .src(files.map(file => path.join(root, `src/js/${file}`)))
.pipe(replace(semver, `v${version}`)) .pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`)) .pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
@@ -406,7 +407,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
}); });
// Do everything // Do everything
gulp.task('publish', () => { gulp.task('publish', callback => {
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo'); run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo', callback);
}); });
} }
+12 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.1.0", "version": "3.3.7",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player", "description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io", "homepage": "https://plyr.io",
"main": "./dist/plyr.js", "main": "./dist/plyr.js",
@@ -8,27 +8,27 @@
"sass": "./src/sass/plyr.scss", "sass": "./src/sass/plyr.scss",
"style": "./dist/plyr.css", "style": "./dist/plyr.css",
"devDependencies": { "devDependencies": {
"babel-core": "^6.26.0", "babel-core": "^6.26.3",
"babel-eslint": "^8.2.2", "babel-eslint": "^8.2.3",
"babel-plugin-external-helpers": "^6.22.0", "babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
"del": "^3.0.0", "del": "^3.0.0",
"eslint": "^4.19.1", "eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0", "eslint-config-airbnb-base": "^12.1.0",
"eslint-config-prettier": "^2.9.0", "eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.10.0", "eslint-plugin-import": "^2.11.0",
"git-branch": "^2.0.1", "git-branch": "^2.0.1",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-autoprefixer": "^5.0.0", "gulp-autoprefixer": "^5.0.0",
"gulp-better-rollup": "^3.1.0", "gulp-better-rollup": "^3.1.0",
"gulp-clean-css": "^3.9.3", "gulp-clean-css": "^3.9.4",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-filter": "^5.1.0", "gulp-filter": "^5.1.0",
"gulp-open": "^3.0.1", "gulp-open": "^3.0.1",
"gulp-rename": "^1.2.2", "gulp-rename": "^1.2.2",
"gulp-replace": "^0.6.1", "gulp-replace": "^0.6.1",
"gulp-s3": "^0.11.0", "gulp-s3": "^0.11.0",
"gulp-sass": "^3.2.1", "gulp-sass": "^4.0.1",
"gulp-size": "^3.0.0", "gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.4", "gulp-sourcemaps": "^2.6.4",
"gulp-svgmin": "^1.2.4", "gulp-svgmin": "^1.2.4",
@@ -37,16 +37,16 @@
"gulp-util": "^3.0.8", "gulp-util": "^3.0.8",
"prettier-eslint": "^8.8.1", "prettier-eslint": "^8.8.1",
"prettier-stylelint": "^0.4.2", "prettier-stylelint": "^0.4.2",
"rollup-plugin-babel": "^3.0.3", "rollup-plugin-babel": "^3.0.4",
"rollup-plugin-commonjs": "^9.1.0", "rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^3.3.0", "rollup-plugin-node-resolve": "^3.3.0",
"run-sequence": "^2.2.1", "run-sequence": "^2.2.1",
"stylelint": "^9.2.0", "stylelint": "^9.2.0",
"stylelint-config-prettier": "^3.0.4", "stylelint-config-prettier": "^3.2.0",
"stylelint-config-recommended": "^2.1.0", "stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.0.0", "stylelint-config-sass-guidelines": "^5.0.0",
"stylelint-order": "^0.8.1", "stylelint-order": "^0.8.1",
"stylelint-scss": "^3.0.0", "stylelint-scss": "^3.1.0",
"stylelint-selector-bem-pattern": "^2.0.0" "stylelint-selector-bem-pattern": "^2.0.0"
}, },
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"], "keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
@@ -69,6 +69,7 @@
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"custom-event-polyfill": "^0.3.0", "custom-event-polyfill": "^0.3.0",
"loadjs": "^3.5.4", "loadjs": "^3.5.4",
"raven-js": "^3.24.0" "npm": "^6.0.0",
"raven-js": "^3.24.2"
} }
} }
+54 -32
View File
@@ -39,13 +39,13 @@ Check out the [changelog](changelog.md) to see what's new with Plyr.
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks: Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
| Type | Maintainer | Link | | Type | Maintainer | Link |
| --------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | --------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| WordPress | Ryan Anthony Drake ([@iamryandrake](https://github.com/iamryandrake)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) | | WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) | | React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) | | Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) | | Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) | | Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
## Quick setup ## Quick setup
@@ -128,7 +128,7 @@ See [initialising](#initialising) for more information on advanced setups.
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following: If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
```html ```html
<script src="https://cdn.plyr.io/3.1.0/plyr.js"></script> <script src="https://cdn.plyr.io/3.3.7/plyr.js"></script>
``` ```
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility _Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
@@ -144,13 +144,13 @@ Include the `plyr.css` stylsheet into your `<head>`
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following: If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html ```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.1.0/plyr.css"> <link rel="stylesheet" href="https://cdn.plyr.io/3.3.7/plyr.css">
``` ```
### SVG Sprite ### SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.1.0/plyr.svg`. reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.7/plyr.svg`.
## Ads ## Ads
@@ -210,7 +210,7 @@ You can specify a range of arguments for the constructor to use:
* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList) * A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
* A [jQuery](https://jquery.com) object * A [jQuery](https://jquery.com) object
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
Here's some examples Here's some examples
@@ -226,20 +226,32 @@ Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElemen
const player = new Plyr(document.getElementById('player')); const player = new Plyr(document.getElementById('player'));
``` ```
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList): Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) (see note below):
```javascript ```javascript
const player = new Plyr(document.querySelectorAll('.js-player')); const player = new Plyr(document.querySelectorAll('.js-player'));
``` ```
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds.
##### Setting up multiple players ##### Setting up multiple players
You have two choices here. You can either use a simple array loop to map the constructor:
```javascript ```javascript
const players = Array.from(document.querySelectorAll('.js-player')).map(player => new Plyr(player)); const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p));
``` ```
...or use a static method where you can pass a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of elements:
```javascript
const players = Plyr.setup('.js-player');
```
Both options will also return an array of instances in the order of they were in the DOM for the string selector or the source NodeList or Array.
##### Passing options
The second argument for the constructor is the [options](#options) object: The second argument for the constructor is the [options](#options) object:
```javascript ```javascript
@@ -248,7 +260,7 @@ const player = new Plyr('#player', {
}); });
``` ```
The constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info. In all cases, the constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
#### Options #### Options
@@ -279,7 +291,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. | | `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. | | `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. | | `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
| `showPosterOnEnd` | Boolean | false | This will restore and _reload_ HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution. | | `resetOnEnd` | Boolean | false | Reset the playback to the start once playback is complete. |
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally | | `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. | | `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
| `duration` | Number | `null` | Specify a custom duration for media. | | `duration` | Number | `null` | Specify a custom duration for media. |
@@ -374,8 +386,9 @@ player.fullscreen.active; // false;
| -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. | | `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. |
| `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. | | `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. |
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. | | `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `stopped` | ✓ | - | Returns a boolean indicating if the current player is stopped. |
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. | | `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered | | `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. | | `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
@@ -388,7 +401,7 @@ player.fullscreen.active; // false;
| `quality`&sup1; | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. | | `quality`&sup1; | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. | | `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. | | `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
| `poster`&sup2; | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. | | `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. | | `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. | | `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. |
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. | | `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
@@ -599,6 +612,8 @@ Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR) * Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN) * Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
_Note_: These need updating to use the new v3 syntax but would still work.
## Fullscreen ## Fullscreen
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen). Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
@@ -607,19 +622,20 @@ Fullscreen in Plyr is supported by all browsers that [currently support it](http
Plyr supports the last 2 versions of most _modern_ browsers. Plyr supports the last 2 versions of most _modern_ browsers.
| Browser | Supported | | Browser | Supported |
| ------------- | --------- | | ------------- | ------------- |
| Safari | ✓ | | Safari | ✓ |
| Mobile Safari | ✓&sup1; | | Mobile Safari | ✓&sup1; |
| Firefox | ✓ | | Firefox | ✓ |
| Chrome | ✓ | | Chrome | ✓ |
| Opera | ✓ | | Opera | ✓ |
| Edge | ✓ | | Edge | ✓ |
| IE11 | ✓ | | IE11 | ✓&sup3; |
| IE10 | ✓&sup2; | | IE10 | ✓&sup2;&sup3; |
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide. 1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)) 2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)).
3. Polyfills required. See below.
### Polyfills ### Polyfills
@@ -668,8 +684,10 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
## Donate ## Donate
Plyr costs money to run, not only my time - I donate that for free but domains, hosting and more. Any help is appreciated... Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated...
[Donate to support Plyr](https://www.paypal.me/pottsy/20usd)
* [Donate via Patron](https://www.patreon.com/plyr)
* [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
## Mentions ## Mentions
@@ -707,10 +725,14 @@ Credit to the PayPal HTML5 Video player from which Plyr's caption functionality
## Thanks ## Thanks
[![Fastly](https://cdn.plyr.io/static/demo/fastly-logo.png)](https://www.fastly.com/) [![Fastly](https://cdn.plyr.io/static/fastly-logo.png)](https://www.fastly.com/)
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services. Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.
[![Sentry](https://cdn.plyr.io/static/sentry-logo-black.svg)](https://sentry.io/)
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
## Copyright and License ## Copyright and License
[The MIT license](license.md) [The MIT license](license.md)
+46 -7
View File
@@ -3,9 +3,10 @@
// TODO: Create as class // TODO: Create as class
// ========================================================================== // ==========================================================================
import controls from './controls';
import i18n from './i18n';
import support from './support'; import support from './support';
import utils from './utils'; import utils from './utils';
import controls from './controls';
const captions = { const captions = {
// Setup captions // Setup captions
@@ -46,6 +47,7 @@ const captions = {
return; return;
} }
// Inject the container // Inject the container
if (!utils.is.element(this.elements.captions)) { if (!utils.is.element(this.elements.captions)) {
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions)); this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
@@ -148,7 +150,49 @@ const captions = {
// Get the current track for the current language // Get the current track for the current language
getCurrentTrack() { getCurrentTrack() {
return captions.getTracks.call(this).find(track => track.language.toLowerCase() === this.language); const tracks = captions.getTracks.call(this);
if (!tracks.length) {
return null;
}
// Get track based on current language
let track = tracks.find(track => track.language.toLowerCase() === this.language);
// Get the <track> with default attribute
if (!track) {
track = utils.getElement.call(this, 'track[default]');
}
// Get the first track
if (!track) {
[track] = tracks;
}
return track;
},
// Get UI label for track
getLabel(track) {
let currentTrack = track;
if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) {
currentTrack = captions.getCurrentTrack.call(this);
}
if (utils.is.track(currentTrack)) {
if (!utils.is.empty(currentTrack.label)) {
return currentTrack.label;
}
if (!utils.is.empty(currentTrack.language)) {
return track.language.toUpperCase();
}
return i18n.get('enabled', this.config);
}
return i18n.get('disabled', this.config);
}, },
// Display active caption if it contains text // Display active caption if it contains text
@@ -206,11 +250,6 @@ const captions = {
// Display captions container and button (for initialization) // Display captions container and button (for initialization)
show() { show() {
// If there's no caption toggle, bail
if (!utils.is.element(this.elements.buttons.captions)) {
return;
}
// Try to load the value from storage // Try to load the value from storage
let active = this.storage.get('captions'); let active = this.storage.get('captions');
+186 -96
View File
@@ -2,12 +2,12 @@
// Plyr controls // Plyr controls
// ========================================================================== // ==========================================================================
import support from './support';
import utils from './utils';
import ui from './ui';
import i18n from './i18n';
import captions from './captions'; import captions from './captions';
import html5 from './html5'; import html5 from './html5';
import i18n from './i18n';
import support from './support';
import ui from './ui';
import utils from './utils';
// Sniff out the browser // Sniff out the browser
const browser = utils.getBrowser(); const browser = utils.getBrowser();
@@ -15,11 +15,6 @@ const browser = utils.getBrowser();
const controls = { const controls = {
// Webkit polyfill for lower fill range // Webkit polyfill for lower fill range
updateRangeFill(target) { updateRangeFill(target) {
// WebKit only
if (!browser.isWebkit) {
return;
}
// Get range from event if event passed // Get range from event if event passed
const range = utils.is.event(target) ? target.target : target; const range = utils.is.event(target) ? target.target : target;
@@ -28,23 +23,88 @@ const controls = {
return; return;
} }
// Set aria value for https://github.com/sampotts/plyr/issues/905
range.setAttribute('aria-valuenow', range.value);
// WebKit only
if (!browser.isWebkit) {
return;
}
// Set CSS custom property // Set CSS custom property
range.style.setProperty('--value', `${range.value / range.max * 100}%`); range.style.setProperty('--value', `${range.value / range.max * 100}%`);
}, },
// Get icon URL // Get icon URL
getIconUrl() { getIconUrl() {
const url = new URL(this.config.iconUrl, window.location);
const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody);
return { return {
url: this.config.iconUrl, url: this.config.iconUrl,
absolute: this.config.iconUrl.indexOf('http') === 0 || (browser.isIE && !window.svg4everybody), cors,
}; };
}, },
// 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),
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
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),
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
duration: utils.getElement.call(this, this.config.selectors.display.duration),
};
// Seek tooltip
if (utils.is.element(this.elements.progress)) {
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
}
return true;
} catch (error) {
// Log it
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
// Restore native video controls
this.toggleNativeControls(true);
return false;
}
},
// Create <svg> icon // Create <svg> icon
createIcon(type, attributes) { createIcon(type, attributes) {
const namespace = 'http://www.w3.org/2000/svg'; const namespace = 'http://www.w3.org/2000/svg';
const iconUrl = controls.getIconUrl.call(this); const iconUrl = controls.getIconUrl.call(this);
const iconPath = `${!iconUrl.absolute ? iconUrl.url : ''}#${this.config.iconPrefix}`; const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
// Create <svg> // Create <svg>
const icon = document.createElementNS(namespace, 'svg'); const icon = document.createElementNS(namespace, 'svg');
@@ -52,6 +112,7 @@ const controls = {
icon, icon,
utils.extend(attributes, { utils.extend(attributes, {
role: 'presentation', role: 'presentation',
focusable: 'false',
}), }),
); );
@@ -206,7 +267,6 @@ const controls = {
// Add aria attributes // Add aria attributes
attributes['aria-pressed'] = false; attributes['aria-pressed'] = false;
attributes['aria-label'] = i18n.get(label, this.config);
} else { } else {
button.appendChild(controls.createIcon.call(this, icon)); button.appendChild(controls.createIcon.call(this, icon));
button.appendChild(controls.createLabel.call(this, label)); button.appendChild(controls.createLabel.call(this, label));
@@ -238,6 +298,7 @@ const controls = {
'label', 'label',
{ {
for: attributes.id, for: attributes.id,
id: `${attributes.id}-label`,
class: this.config.classNames.hidden, class: this.config.classNames.hidden,
}, },
i18n.get(type, this.config), i18n.get(type, this.config),
@@ -255,6 +316,12 @@ const controls = {
step: 0.01, step: 0.01,
value: 0, value: 0,
autocomplete: 'off', autocomplete: 'off',
// A11y fixes for https://github.com/sampotts/plyr/issues/905
role: 'slider',
'aria-labelledby': `${attributes.id}-label`,
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-valuenow': 0,
}, },
attributes, attributes,
), ),
@@ -281,6 +348,8 @@ const controls = {
min: 0, min: 0,
max: 100, max: 100,
value: 0, value: 0,
role: 'presentation',
'aria-hidden': true,
}, },
attributes, attributes,
), ),
@@ -314,22 +383,14 @@ const controls = {
// Create time display // Create time display
createTime(type) { createTime(type) {
const container = utils.createElement('div', { const attributes = utils.getAttributesFromSelector(this.config.selectors.display[type]);
class: 'plyr__time',
});
container.appendChild( const container = utils.createElement('div', utils.extend(attributes, {
utils.createElement( class: `plyr__time ${attributes.class}`,
'span', 'aria-label': i18n.get(type, this.config),
{ }), '00:00');
class: this.config.classNames.hidden,
},
i18n.get(type, this.config),
),
);
container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.display[type]), '00:00'));
// Reference for updates
this.elements.display[type] = container; this.elements.display[type] = container;
return container; return container;
@@ -354,7 +415,7 @@ const controls = {
}), }),
); );
const faux = utils.createElement('span', { 'aria-hidden': true }); const faux = utils.createElement('span', { hidden: '' });
label.appendChild(radio); label.appendChild(radio);
label.appendChild(faux); label.appendChild(faux);
@@ -429,11 +490,7 @@ const controls = {
// Hide/show a tab // Hide/show a tab
toggleTab(setting, toggle) { toggleTab(setting, toggle) {
const tab = this.elements.settings.tabs[setting]; utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
const pane = this.elements.settings.panes[setting];
utils.toggleHidden(tab, !toggle);
utils.toggleHidden(pane, !toggle);
}, },
// Set the quality menu // Set the quality menu
@@ -456,6 +513,9 @@ const controls = {
const toggle = !utils.is.empty(this.options.quality) && this.options.quality.length > 1; const toggle = !utils.is.empty(this.options.quality) && this.options.quality.length > 1;
controls.toggleTab.call(this, type, toggle); controls.toggleTab.call(this, type, toggle);
// Check if we need to toggle the parent
controls.checkMenu.call(this);
// If we're hiding, nothing more to do // If we're hiding, nothing more to do
if (!toggle) { if (!toggle) {
return; return;
@@ -480,6 +540,7 @@ const controls = {
break; break;
case 576: case 576:
case 480:
label = 'SD'; label = 'SD';
break; break;
@@ -495,13 +556,15 @@ const controls = {
}; };
// Sort options by the config and then render options // Sort options by the config and then render options
this.options.quality.sort((a, b) => { this.options.quality
const sorting = this.config.quality.options; .sort((a, b) => {
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1; const sorting = this.config.quality.options;
}).forEach(quality => { return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
const label = controls.getLabel.call(this, 'quality', quality); })
controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality)); .forEach(quality => {
}); const label = controls.getLabel.call(this, 'quality', quality);
controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality));
});
controls.updateSetting.call(this, type, list); controls.updateSetting.call(this, type, list);
}, },
@@ -511,16 +574,17 @@ const controls = {
getLabel(setting, value) { getLabel(setting, value) {
switch (setting) { switch (setting) {
case 'speed': case 'speed':
return value === 1 ? 'Normal' : `${value}&times;`; return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;
case 'quality': case 'quality':
if (utils.is.number(value)) { if (utils.is.number(value)) {
return `${value}p`; return `${value}p`;
} }
return utils.toTitleCase(value); return utils.toTitleCase(value);
case 'captions': case 'captions':
return controls.getLanguage.call(this); return captions.getLabel.call(this);
default: default:
return null; return null;
@@ -535,7 +599,16 @@ const controls = {
switch (setting) { switch (setting) {
case 'captions': case 'captions':
value = this.captions.active ? this.captions.language : i18n.get('disabled', this.config); if (this.captions.active) {
if (this.options.captions.length > 2 || !this.options.captions.some(lang => lang === 'enabled')) {
value = this.captions.language;
} else {
value = 'enabled';
}
} else {
value = '';
}
break; break;
default: default:
@@ -566,17 +639,19 @@ const controls = {
list = pane && pane.querySelector('ul'); list = pane && pane.querySelector('ul');
} }
// Update the label // If there's no list it means it's not been rendered...
if (!utils.is.empty(value)) { if (!utils.is.element(list)) {
const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`); return;
label.innerHTML = controls.getLabel.call(this, setting, value);
} }
// Find the radio option // Update the label
const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`);
label.innerHTML = controls.getLabel.call(this, setting, value);
// Find the radio option and check it
const target = list && list.querySelector(`input[value="${value}"]`); const target = list && list.querySelector(`input[value="${value}"]`);
if (utils.is.element(target)) { if (utils.is.element(target)) {
// Check it
target.checked = true; target.checked = true;
} }
}, },
@@ -627,21 +702,6 @@ const controls = {
// Get current selected caption language // Get current selected caption language
// TODO: rework this to user the getter in the API? // TODO: rework this to user the getter in the API?
getLanguage() {
if (!this.supported.ui) {
return null;
}
if (support.textTracks && captions.getTracks.call(this).length && this.captions.active) {
const currentTrack = captions.getCurrentTrack.call(this);
if (utils.is.track(currentTrack)) {
return currentTrack.label;
}
}
return i18n.get('disabled', this.config);
},
// Set a list of available captions languages // Set a list of available captions languages
setCaptionsMenu() { setCaptionsMenu() {
@@ -656,6 +716,9 @@ const controls = {
// Empty the menu // Empty the menu
utils.emptyElement(list); utils.emptyElement(list);
// Check if we need to toggle the parent
controls.checkMenu.call(this);
// If there's no captions, bail // If there's no captions, bail
if (!toggle) { if (!toggle) {
return; return;
@@ -663,8 +726,8 @@ const controls = {
// Re-map the tracks into just the data we need // Re-map the tracks into just the data we need
const tracks = captions.getTracks.call(this).map(track => ({ const tracks = captions.getTracks.call(this).map(track => ({
language: track.language, language: !utils.is.empty(track.language) ? track.language : 'enabled',
label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(), label: captions.getLabel.call(this, track),
})); }));
// Add the "Disabled" option to turn off captions // Add the "Disabled" option to turn off captions
@@ -680,12 +743,15 @@ const controls = {
track.language, track.language,
list, list,
'language', 'language',
track.label || track.language, track.label,
controls.createBadge.call(this, track.language.toUpperCase()), track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null,
track.language.toLowerCase() === this.captions.language.toLowerCase(), track.language.toLowerCase() === this.captions.language.toLowerCase(),
); );
}); });
// Store reference
this.options.captions = tracks.map(track => track.language);
controls.updateSetting.call(this, type, list); controls.updateSetting.call(this, type, list);
}, },
@@ -736,10 +802,6 @@ const controls = {
// Get the list to populate // Get the list to populate
const list = this.elements.settings.panes.speed.querySelector('ul'); const list = this.elements.settings.panes.speed.querySelector('ul');
// Show the pane and tab
utils.toggleHidden(this.elements.settings.tabs.speed, false);
utils.toggleHidden(this.elements.settings.panes.speed, false);
// Empty the menu // Empty the menu
utils.emptyElement(list); utils.emptyElement(list);
@@ -770,7 +832,7 @@ const controls = {
return; return;
} }
const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.getAttribute('aria-hidden') === 'true'; const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.hasAttribute('hidden');
if (utils.is.event(event)) { if (utils.is.event(event)) {
const isMenuItem = utils.is.element(form) && form.contains(event.target); const isMenuItem = utils.is.element(form) && form.contains(event.target);
@@ -795,7 +857,7 @@ const controls = {
} }
if (utils.is.element(form)) { if (utils.is.element(form)) {
form.setAttribute('aria-hidden', !show); utils.toggleHidden(form, !show);
utils.toggleClass(this.elements.container, this.config.classNames.menu.open, show); utils.toggleClass(this.elements.container, this.config.classNames.menu.open, show);
if (show) { if (show) {
@@ -811,7 +873,7 @@ const controls = {
const clone = tab.cloneNode(true); const clone = tab.cloneNode(true);
clone.style.position = 'absolute'; clone.style.position = 'absolute';
clone.style.opacity = 0; clone.style.opacity = 0;
clone.setAttribute('aria-hidden', false); clone.removeAttribute('hidden');
// Prevent input's being unchecked due to the name being identical // Prevent input's being unchecked due to the name being identical
Array.from(clone.querySelectorAll('input[name]')).forEach(input => { Array.from(clone.querySelectorAll('input[name]')).forEach(input => {
@@ -836,11 +898,9 @@ const controls = {
}, },
// Toggle Menu // Toggle Menu
showTab(event) { showTab(target = '') {
const { menu } = this.elements.settings; const { menu } = this.elements.settings;
const tab = event.target; const pane = document.getElementById(target);
const show = tab.getAttribute('aria-expanded') === 'false';
const pane = document.getElementById(tab.getAttribute('aria-controls'));
// Nothing to show, bail // Nothing to show, bail
if (!utils.is.element(pane)) { if (!utils.is.element(pane)) {
@@ -855,7 +915,7 @@ const controls = {
// Hide all other tabs // Hide all other tabs
// Get other tabs // Get other tabs
const current = menu.querySelector('[role="tabpanel"][aria-hidden="false"]'); const current = menu.querySelector('[role="tabpanel"]:not([hidden])');
const container = current.parentNode; const container = current.parentNode;
// Set other toggles to be expanded false // Set other toggles to be expanded false
@@ -899,12 +959,16 @@ const controls = {
} }
// Set attributes on current tab // Set attributes on current tab
current.setAttribute('aria-hidden', true); utils.toggleHidden(current, true);
current.setAttribute('tabindex', -1); current.setAttribute('tabindex', -1);
// Set attributes on target // Set attributes on target
pane.setAttribute('aria-hidden', !show); utils.toggleHidden(pane, false);
tab.setAttribute('aria-expanded', show);
const tabs = utils.getElements.call(this, `[aria-controls="${target}"]`);
Array.from(tabs).forEach(tab => {
tab.setAttribute('aria-expanded', true);
});
pane.removeAttribute('tabindex'); pane.removeAttribute('tabindex');
// Focus the first item // Focus the first item
@@ -1045,7 +1109,7 @@ const controls = {
const form = utils.createElement('form', { const form = utils.createElement('form', {
class: 'plyr__menu__container', class: 'plyr__menu__container',
id: `plyr-settings-${data.id}`, id: `plyr-settings-${data.id}`,
'aria-hidden': true, hidden: '',
'aria-labelled-by': `plyr-settings-toggle-${data.id}`, 'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
role: 'tablist', role: 'tablist',
tabindex: -1, tabindex: -1,
@@ -1055,7 +1119,6 @@ const controls = {
const home = utils.createElement('div', { const home = utils.createElement('div', {
id: `plyr-settings-${data.id}-home`, id: `plyr-settings-${data.id}-home`,
'aria-hidden': false,
'aria-labelled-by': `plyr-settings-toggle-${data.id}`, 'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
role: 'tabpanel', role: 'tabpanel',
}); });
@@ -1106,11 +1169,10 @@ const controls = {
this.config.settings.forEach(type => { this.config.settings.forEach(type => {
const pane = utils.createElement('div', { const pane = utils.createElement('div', {
id: `plyr-settings-${data.id}-${type}`, id: `plyr-settings-${data.id}-${type}`,
'aria-hidden': true, hidden: '',
'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`, 'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`,
role: 'tabpanel', role: 'tabpanel',
tabindex: -1, tabindex: -1,
hidden: '',
}); });
const back = utils.createElement( const back = utils.createElement(
@@ -1181,7 +1243,7 @@ const controls = {
const icon = controls.getIconUrl.call(this); const icon = controls.getIconUrl.call(this);
// Only load external sprite using AJAX // Only load external sprite using AJAX
if (icon.absolute) { if (icon.cors) {
utils.loadSprite(icon.url, 'sprite-plyr'); utils.loadSprite(icon.url, 'sprite-plyr');
} }
} }
@@ -1193,17 +1255,21 @@ const controls = {
let container = null; let container = null;
this.elements.controls = null; this.elements.controls = null;
// HTML or Element passed as the option // Set template properties
const props = {
id: this.id,
seektime: this.config.seekTime,
title: this.config.title,
};
let update = true;
if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) { if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) {
// String or HTMLElement passed as the option
container = this.config.controls; container = this.config.controls;
} else if (utils.is.function(this.config.controls)) { } else if (utils.is.function(this.config.controls)) {
// A custom function to build controls // A custom function to build controls
// The function can return a HTMLElement or String // The function can return a HTMLElement or String
container = this.config.controls({ container = this.config.controls.call(this, props);
id: this.id,
seektime: this.config.seekTime,
title: this.config.title,
});
} else { } else {
// Create controls // Create controls
container = controls.create.call(this, { container = controls.create.call(this, {
@@ -1211,10 +1277,34 @@ const controls = {
seektime: this.config.seekTime, seektime: this.config.seekTime,
speed: this.speed, speed: this.speed,
quality: this.quality, quality: this.quality,
captions: controls.getLanguage.call(this), captions: captions.getLabel.call(this),
// TODO: Looping // TODO: Looping
// loop: 'None', // loop: 'None',
}); });
update = false;
}
// Replace props with their value
const replace = input => {
let result = input;
Object.entries(props).forEach(([
key,
value,
]) => {
result = utils.replaceAll(result, `{${key}}`, value);
});
return result;
};
// Update markup
if (update) {
if (utils.is.string(this.config.controls)) {
container = replace(container);
} else if (utils.is.element(container)) {
container.innerHTML = replace(container.innerHTML);
}
} }
// Controls container // Controls container
@@ -1233,13 +1323,13 @@ const controls = {
// Inject controls HTML // Inject controls HTML
if (utils.is.element(container)) { if (utils.is.element(container)) {
target.appendChild(container); target.appendChild(container);
} else { } else if (container) {
target.insertAdjacentHTML('beforeend', container); target.insertAdjacentHTML('beforeend', container);
} }
// Find the elements if need be // Find the elements if need be
if (!utils.is.element(this.elements.controls)) { if (!utils.is.element(this.elements.controls)) {
utils.findElements.call(this); controls.findElements.call(this);
} }
// Edge sometimes doesn't finish the paint so force a redraw // Edge sometimes doesn't finish the paint so force a redraw
+23 -14
View File
@@ -47,8 +47,8 @@ const defaults = {
// Auto hide the controls // Auto hide the controls
hideControls: true, hideControls: true,
// Revert to poster on finish (HTML5 - will cause reload) // Reset to start when playback ended
showPosterOnEnd: false, resetOnEnd: false,
// Disable the standard context menu // Disable the standard context menu
disableContextMenu: true, disableContextMenu: true,
@@ -56,7 +56,7 @@ const defaults = {
// Sprite (for icons) // Sprite (for icons)
loadSprite: true, loadSprite: true,
iconPrefix: 'plyr', iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.1.0/plyr.svg', iconUrl: 'https://cdn.plyr.io/3.3.7/plyr.svg',
// Blank video (used to prevent errors on source change) // Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4', blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@@ -115,7 +115,7 @@ const defaults = {
// Captions settings // Captions settings
captions: { captions: {
active: false, active: false,
language: window.navigator.language.split('-')[0], language: (navigator.language || navigator.userLanguage).split('-')[0],
}, },
// Fullscreen settings // Fullscreen settings
@@ -157,10 +157,10 @@ const defaults = {
// Localisation // Localisation
i18n: { i18n: {
restart: 'Restart', restart: 'Restart',
rewind: 'Rewind {seektime} secs', rewind: 'Rewind {seektime}s',
play: 'Play', play: 'Play',
pause: 'Pause', pause: 'Pause',
fastForward: 'Forward {seektime} secs', fastForward: 'Forward {seektime}s',
seek: 'Seek', seek: 'Seek',
played: 'Played', played: 'Played',
buffered: 'Buffered', buffered: 'Buffered',
@@ -177,6 +177,7 @@ const defaults = {
captions: 'Captions', captions: 'Captions',
settings: 'Settings', settings: 'Settings',
speed: 'Speed', speed: 'Speed',
normal: 'Normal',
quality: 'Quality', quality: 'Quality',
loop: 'Loop', loop: 'Loop',
start: 'Start', start: 'Start',
@@ -184,19 +185,24 @@ const defaults = {
all: 'All', all: 'All',
reset: 'Reset', reset: 'Reset',
disabled: 'Disabled', disabled: 'Disabled',
enabled: 'Enabled',
advertisement: 'Ad', advertisement: 'Ad',
}, },
// URLs // URLs
urls: { urls: {
vimeo: { vimeo: {
api: 'https://player.vimeo.com/api/player.js', sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
api: 'https://vimeo.com/api/v2/video/{0}.json',
}, },
youtube: { youtube: {
api: 'https://www.youtube.com/iframe_api', sdk: 'https://www.youtube.com/iframe_api',
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
poster: 'https://img.youtube.com/vi/{0}/maxresdefault.jpg,https://img.youtube.com/vi/{0}/hqdefault.jpg',
}, },
googleIMA: { googleIMA: {
api: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
}, },
}, },
@@ -320,14 +326,17 @@ const defaults = {
// Class hooks added to the player in different states // Class hooks added to the player in different states
classNames: { classNames: {
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
ads: 'plyr__ads',
control: 'plyr__control',
type: 'plyr--{0}', type: 'plyr--{0}',
provider: 'plyr--{0}', provider: 'plyr--{0}',
stopped: 'plyr--stopped', video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
ads: 'plyr__ads',
control: 'plyr__control',
playing: 'plyr--playing', playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading', loading: 'plyr--loading',
error: 'plyr--has-error', error: 'plyr--has-error',
hover: 'plyr--hover', hover: 'plyr--hover',
+8 -8
View File
@@ -55,7 +55,7 @@ class Fullscreen {
// Get prefix // Get prefix
this.prefix = Fullscreen.prefix; this.prefix = Fullscreen.prefix;
this.name = Fullscreen.name; this.property = Fullscreen.property;
// Scroll position // Scroll position
this.scrollPosition = { x: 0, y: 0 }; this.scrollPosition = { x: 0, y: 0 };
@@ -70,7 +70,7 @@ class Fullscreen {
// Fullscreen toggle on double click // Fullscreen toggle on double click
utils.on(this.player.elements.container, 'dblclick', event => { utils.on(this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls // Ignore double click in controls
if (this.player.elements.controls.contains(event.target)) { if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return; return;
} }
@@ -90,7 +90,7 @@ class Fullscreen {
static get prefix() { static get prefix() {
// No prefix // No prefix
if (utils.is.function(document.exitFullscreen)) { if (utils.is.function(document.exitFullscreen)) {
return false; return '';
} }
// Check for fullscreen support by vendor prefix // Check for fullscreen support by vendor prefix
@@ -113,7 +113,7 @@ class Fullscreen {
return value; return value;
} }
static get name() { static get property() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen'; return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
} }
@@ -138,7 +138,7 @@ class Fullscreen {
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback); return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
} }
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.name}Element`]; const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
return element === this.target; return element === this.target;
} }
@@ -176,7 +176,7 @@ class Fullscreen {
} else if (!this.prefix) { } else if (!this.prefix) {
this.target.requestFullscreen(); this.target.requestFullscreen();
} else if (!utils.is.empty(this.prefix)) { } else if (!utils.is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.name}`](); this.target[`${this.prefix}Request${this.property}`]();
} }
} }
@@ -193,10 +193,10 @@ class Fullscreen {
} else if (!Fullscreen.native) { } else if (!Fullscreen.native) {
toggleFallback.call(this, false); toggleFallback.call(this, false);
} else if (!this.prefix) { } else if (!this.prefix) {
document.cancelFullScreen(); (document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!utils.is.empty(this.prefix)) { } else if (!utils.is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit'; const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.name}`](); document[`${this.prefix}${action}${this.property}`]();
} }
} }
+20 -10
View File
@@ -2,10 +2,9 @@
// Plyr Event Listeners // Plyr Event Listeners
// ========================================================================== // ==========================================================================
import support from './support';
import utils from './utils';
import controls from './controls'; import controls from './controls';
import ui from './ui'; import ui from './ui';
import utils from './utils';
// Sniff out the browser // Sniff out the browser
const browser = utils.getBrowser(); const browser = utils.getBrowser();
@@ -254,7 +253,7 @@ class Listeners {
utils.on(this.player.media, 'timeupdate seeking', event => ui.timeUpdate.call(this.player, event)); utils.on(this.player.media, 'timeupdate seeking', event => ui.timeUpdate.call(this.player, event));
// Display duration // Display duration
utils.on(this.player.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this.player, event)); utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => ui.durationUpdate.call(this.player, event));
// Check for audio tracks on load // Check for audio tracks on load
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
@@ -266,12 +265,9 @@ class Listeners {
// Handle the media finishing // Handle the media finishing
utils.on(this.player.media, 'ended', () => { utils.on(this.player.media, 'ended', () => {
// Show poster on end // Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.showPosterOnEnd) { if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
// Restart // Restart
this.player.restart(); this.player.restart();
// Re-load media
this.player.media.load();
} }
}); });
@@ -282,7 +278,7 @@ class Listeners {
utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event)); utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event));
// Handle play/pause // Handle play/pause
utils.on(this.player.media, 'playing play pause ended emptied', event => ui.checkPlaying.call(this.player, event)); utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event));
// Loading state // Loading state
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event)); utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
@@ -293,6 +289,10 @@ class Listeners {
// If autoplay, then load advertisement if required // If autoplay, then load advertisement if required
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
utils.on(this.player.media, 'playing', () => { utils.on(this.player.media, 'playing', () => {
if (!this.player.ads) {
return;
}
// If ads are enabled, wait for them first // If ads are enabled, wait for them first
if (this.player.ads.enabled && !this.player.ads.initialized) { if (this.player.ads.enabled && !this.player.ads.initialized) {
// Wait for manager response // Wait for manager response
@@ -331,7 +331,7 @@ class Listeners {
// Disable right click // Disable right click
if (this.player.supported.ui && this.player.config.disableContextMenu) { if (this.player.supported.ui && this.player.config.disableContextMenu) {
utils.on( utils.on(
this.player.media, this.player.elements.wrapper,
'contextmenu', 'contextmenu',
event => { event => {
event.preventDefault(); event.preventDefault();
@@ -489,12 +489,19 @@ class Listeners {
on(this.player.elements.settings.form, 'click', event => { on(this.player.elements.settings.form, 'click', event => {
event.stopPropagation(); event.stopPropagation();
// Go back to home tab on click
const showHomeTab = () => {
const id = `plyr-settings-${this.player.id}-home`;
controls.showTab.call(this.player, id);
};
// Settings menu items - use event delegation as items are added/removed // Settings menu items - use event delegation as items are added/removed
if (utils.matches(event.target, this.player.config.selectors.inputs.language)) { if (utils.matches(event.target, this.player.config.selectors.inputs.language)) {
proxy( proxy(
event, event,
() => { () => {
this.player.language = event.target.value; this.player.language = event.target.value;
showHomeTab();
}, },
'language', 'language',
); );
@@ -503,6 +510,7 @@ class Listeners {
event, event,
() => { () => {
this.player.quality = event.target.value; this.player.quality = event.target.value;
showHomeTab();
}, },
'quality', 'quality',
); );
@@ -511,11 +519,13 @@ class Listeners {
event, event,
() => { () => {
this.player.speed = parseFloat(event.target.value); this.player.speed = parseFloat(event.target.value);
showHomeTab();
}, },
'speed', 'speed',
); );
} else { } else {
controls.showTab.call(this.player, event); const tab = event.target;
controls.showTab.call(this.player, tab.getAttribute('aria-controls'));
} }
}); });
+10 -27
View File
@@ -2,15 +2,10 @@
// Plyr Media // Plyr Media
// ========================================================================== // ==========================================================================
import support from './support';
import utils from './utils';
import youtube from './plugins/youtube';
import vimeo from './plugins/vimeo';
import html5 from './html5'; import html5 from './html5';
import ui from './ui'; import vimeo from './plugins/vimeo';
import youtube from './plugins/youtube';
// Sniff out the browser import utils from './utils';
const browser = utils.getBrowser();
const media = { const media = {
// Setup media // Setup media
@@ -33,23 +28,6 @@ const media = {
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true); 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.isHTML5 && this.isVideo);
// 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, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
}
// Inject the player wrapper // Inject the player wrapper
if (this.isVideo) { if (this.isVideo) {
// Create the wrapper div // Create the wrapper div
@@ -59,6 +37,13 @@ const media = {
// Wrap the video in a container // Wrap the video in a container
utils.wrap(this.media, this.elements.wrapper); utils.wrap(this.media, this.elements.wrapper);
// Faux poster container
this.elements.poster = utils.createElement('div', {
class: this.config.classNames.poster,
});
this.elements.wrapper.appendChild(this.elements.poster);
} }
if (this.isEmbed) { if (this.isEmbed) {
@@ -75,8 +60,6 @@ const media = {
break; break;
} }
} else if (this.isHTML5) { } else if (this.isHTML5) {
ui.setTitle.call(this);
html5.extend.call(this); html5.extend.call(this);
} }
}, },
+10 -4
View File
@@ -6,8 +6,8 @@
/* global google */ /* global google */
import utils from '../utils';
import i18n from '../i18n'; import i18n from '../i18n';
import utils from '../utils';
class Ads { class Ads {
/** /**
@@ -18,7 +18,6 @@ class Ads {
constructor(player) { constructor(player) {
this.player = player; this.player = player;
this.publisherId = player.config.ads.publisherId; this.publisherId = player.config.ads.publisherId;
this.enabled = player.isHTML5 && player.isVideo && player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length;
this.playing = false; this.playing = false;
this.initialized = false; this.initialized = false;
this.elements = { this.elements = {
@@ -44,6 +43,10 @@ class Ads {
this.load(); this.load();
} }
get enabled() {
return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId);
}
/** /**
* Load the IMA SDK * Load the IMA SDK
*/ */
@@ -52,7 +55,7 @@ class Ads {
// Check if the Google IMA3 SDK is loaded or load it ourselves // Check if the Google IMA3 SDK is loaded or load it ourselves
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) { if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
utils utils
.loadScript(this.player.config.urls.googleIMA.api) .loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => { .then(() => {
this.ready(); this.ready();
}) })
@@ -160,6 +163,9 @@ class Ads {
// We only overlay ads as we only support video. // We only overlay ads as we only support video.
request.forceNonLinearFullSlot = false; request.forceNonLinearFullSlot = false;
// Mute based on current state
request.setAdWillPlayMuted(!this.player.muted);
this.loader.requestAds(request); this.loader.requestAds(request);
} catch (e) { } catch (e) {
this.onAdError(e); this.onAdError(e);
@@ -226,7 +232,7 @@ class Ads {
// Get skippable state // Get skippable state
// TODO: Skip button // TODO: Skip button
// this.manager.getAdSkippableState(); // this.player.debug.warn(this.manager.getAdSkippableState());
// Set volume to match player // Set volume to match player
this.manager.setVolume(this.player.volume); this.manager.setVolume(this.player.volume);
+54 -10
View File
@@ -2,10 +2,10 @@
// Vimeo plugin // Vimeo plugin
// ========================================================================== // ==========================================================================
import utils from './../utils';
import captions from './../captions'; import captions from './../captions';
import controls from './../controls'; import controls from './../controls';
import ui from './../ui'; import ui from './../ui';
import utils from './../utils';
const vimeo = { const vimeo = {
setup() { setup() {
@@ -18,7 +18,7 @@ const vimeo = {
// Load the API if not already // Load the API if not already
if (!utils.is.object(window.Vimeo)) { if (!utils.is.object(window.Vimeo)) {
utils utils
.loadScript(this.config.urls.vimeo.api) .loadScript(this.config.urls.vimeo.sdk)
.then(() => { .then(() => {
vimeo.ready.call(this); vimeo.ready.call(this);
}) })
@@ -35,10 +35,14 @@ const vimeo = {
setAspectRatio(input) { setAspectRatio(input) {
const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':'); const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':');
const padding = 100 / ratio[0] * ratio[1]; const padding = 100 / ratio[0] * ratio[1];
const height = 240;
const offset = (height - padding) / (height / 50);
this.elements.wrapper.style.paddingBottom = `${padding}%`; this.elements.wrapper.style.paddingBottom = `${padding}%`;
this.media.style.transform = `translateY(-${offset}%)`;
if (this.supported.ui) {
const height = 240;
const offset = (height - padding) / (height / 50);
this.media.style.transform = `translateY(-${offset}%)`;
}
}, },
// API Ready // API Ready
@@ -49,12 +53,14 @@ const vimeo = {
const options = { const options = {
loop: player.config.loop.active, loop: player.config.loop.active,
autoplay: player.autoplay, autoplay: player.autoplay,
// muted: player.muted,
byline: false, byline: false,
portrait: false, portrait: false,
title: false, title: false,
speed: true, speed: true,
transparent: 0, transparent: 0,
gesture: 'media', gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
}; };
const params = utils.buildUrlParams(options); const params = utils.buildUrlParams(options);
@@ -63,31 +69,58 @@ const vimeo = {
// Get from <div> if needed // Get from <div> if needed
if (utils.is.empty(source)) { if (utils.is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id); source = player.media.getAttribute(player.config.attributes.embed.id);
} }
const id = utils.parseVimeoId(source); const id = utils.parseVimeoId(source);
// Build an iframe // Build an iframe
const iframe = utils.createElement('iframe'); const iframe = utils.createElement('iframe');
const src = `https://player.vimeo.com/video/${id}?${params}`; const src = utils.format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src); iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', ''); iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', ''); iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay'); iframe.setAttribute('allow', 'autoplay');
// Inject the package // Inject the package
const wrapper = utils.createElement('div'); const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe); wrapper.appendChild(iframe);
player.media = utils.replaceElement(wrapper, player.media); player.media = utils.replaceElement(wrapper, player.media);
// Get poster image
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (utils.is.empty(response)) {
return;
}
// Get the URL for thumbnail
const url = new URL(response[0].thumbnail_large);
// Get original image
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set attribute
player.media.setAttribute('poster', url.href);
// Update
ui.setPoster.call(player);
});
// Setup instance // Setup instance
// https://github.com/vimeo/player.js // https://github.com/vimeo/player.js
player.embed = new window.Vimeo.Player(iframe); player.embed = new window.Vimeo.Player(iframe, {
autopause: player.config.autopause,
muted: player.muted,
});
player.media.paused = true; player.media.paused = true;
player.media.currentTime = 0; player.media.currentTime = 0;
// Disable native text track rendering
if (player.supported.ui) {
player.embed.disableTextTrack();
}
// Create a faux HTML5 API using the Vimeo API // Create a faux HTML5 API using the Vimeo API
player.media.play = () => { player.media.play = () => {
player.embed.play().then(() => { player.embed.play().then(() => {
@@ -124,7 +157,9 @@ const vimeo = {
utils.dispatchEvent.call(player, player.media, 'seeking'); utils.dispatchEvent.call(player, player.media, 'seeking');
// Seek after events // Seek after events
player.embed.setCurrentTime(time); player.embed.setCurrentTime(time).catch(() => {
// Do nothing
});
// Restore pause state // Restore pause state
if (paused) { if (paused) {
@@ -310,6 +345,15 @@ const vimeo = {
if (parseInt(data.percent, 10) === 1) { if (parseInt(data.percent, 10) === 1) {
utils.dispatchEvent.call(player, player.media, 'canplaythrough'); utils.dispatchEvent.call(player, player.media, 'canplaythrough');
} }
// Get duration as if we do it before load, it gives an incorrect value
// https://github.com/sampotts/plyr/issues/891
player.embed.getDuration().then(value => {
if (value !== player.media.duration) {
player.media.duration = value;
utils.dispatchEvent.call(player, player.media, 'durationchange');
}
});
}); });
player.embed.on('seeked', () => { player.embed.on('seeked', () => {
+14 -3
View File
@@ -2,9 +2,9 @@
// YouTube plugin // YouTube plugin
// ========================================================================== // ==========================================================================
import utils from './../utils';
import controls from './../controls'; import controls from './../controls';
import ui from './../ui'; import ui from './../ui';
import utils from './../utils';
// Standardise YouTube quality unit // Standardise YouTube quality unit
function mapQualityUnit(input) { function mapQualityUnit(input) {
@@ -77,7 +77,7 @@ const youtube = {
youtube.ready.call(this); youtube.ready.call(this);
} else { } else {
// Load the API // Load the API
utils.loadScript(this.config.urls.youtube.api).catch(error => { utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error); this.debug.warn('YouTube API failed to load', error);
}); });
@@ -117,7 +117,7 @@ const youtube = {
// Or via Google API // Or via Google API
const key = this.config.keys.google; const key = this.config.keys.google;
if (utils.is.string(key) && !utils.is.empty(key)) { if (utils.is.string(key) && !utils.is.empty(key)) {
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`; const url = utils.format(this.config.urls.youtube.api, videoId, key);
utils utils
.fetch(url) .fetch(url)
@@ -161,6 +161,9 @@ const youtube = {
const container = utils.createElement('div', { id }); const container = utils.createElement('div', { id });
player.media = utils.replaceElement(container, player.media); player.media = utils.replaceElement(container, player.media);
// Set poster image
player.media.setAttribute('poster', utils.format(player.config.urls.youtube.poster, videoId));
// Setup instance // Setup instance
// https://developers.google.com/youtube/iframe_api_reference // https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(id, { player.embed = new window.YT.Player(id, {
@@ -270,6 +273,9 @@ const youtube = {
return Number(instance.getCurrentTime()); return Number(instance.getCurrentTime());
}, },
set(time) { set(time) {
// Vimeo will automatically play on seek
const { paused } = player.media;
// Set seeking flag // Set seeking flag
player.media.seeking = true; player.media.seeking = true;
@@ -278,6 +284,11 @@ const youtube = {
// Seek after events sent // Seek after events sent
instance.seekTo(time); instance.seekTo(time);
// Restore pause state
if (paused) {
player.pause();
}
}, },
}); });
+86 -51
View File
@@ -1,26 +1,24 @@
// ========================================================================== // ==========================================================================
// Plyr // Plyr
// plyr.js v3.1.0 // plyr.js v3.3.7
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
import { providers, types } from './types'; import captions from './captions';
import defaults from './defaults';
import support from './support';
import utils from './utils';
import Console from './console'; import Console from './console';
import controls from './controls';
import defaults from './defaults';
import Fullscreen from './fullscreen'; import Fullscreen from './fullscreen';
import Listeners from './listeners'; import Listeners from './listeners';
import Storage from './storage';
import Ads from './plugins/ads';
import captions from './captions';
import controls from './controls';
import media from './media'; import media from './media';
import Ads from './plugins/ads';
import source from './source'; import source from './source';
import Storage from './storage';
import support from './support';
import { providers, types } from './types';
import ui from './ui'; import ui from './ui';
import utils from './utils';
// Private properties // Private properties
// TODO: Use a WeakMap for private globals // TODO: Use a WeakMap for private globals
@@ -57,7 +55,7 @@ class Plyr {
this.config = utils.extend( this.config = utils.extend(
{}, {},
defaults, defaults,
options, options || {},
(() => { (() => {
try { try {
return JSON.parse(this.media.getAttribute('data-plyr-config')); return JSON.parse(this.media.getAttribute('data-plyr-config'));
@@ -97,6 +95,7 @@ class Plyr {
this.options = { this.options = {
speed: [], speed: [],
quality: [], quality: [],
captions: [],
}; };
// Debugging // Debugging
@@ -133,17 +132,9 @@ class Plyr {
} }
// Cache original element state for .destroy() // Cache original element state for .destroy()
// TODO: Investigate a better solution as I suspect this causes reported double load issues? const clone = this.media.cloneNode(true);
setTimeout(() => { clone.autoplay = false;
const clone = this.media.cloneNode(true); this.elements.original = clone;
// Prevent the clone autoplaying
if (clone.getAttribute('autoplay')) {
clone.pause();
}
this.elements.original = clone;
}, 0);
// Set media type based on tag or data attribute // Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube // Supported: video, audio, vimeo, youtube
@@ -184,12 +175,17 @@ class Plyr {
if (truthy.includes(params.autoplay)) { if (truthy.includes(params.autoplay)) {
this.config.autoplay = true; this.config.autoplay = true;
} }
if (truthy.includes(params.playsinline)) {
this.config.inline = true;
}
if (truthy.includes(params.loop)) { if (truthy.includes(params.loop)) {
this.config.loop.active = true; this.config.loop.active = true;
} }
// TODO: replace fullscreen.iosNative with this playsinline config option
// YouTube requires the playsinline in the URL
if (this.isYouTube) {
this.config.playsinline = truthy.includes(params.playsinline);
} else {
this.config.playsinline = true;
}
} }
} else { } else {
// <div> with attributes // <div> with attributes
@@ -223,7 +219,7 @@ class Plyr {
this.config.autoplay = true; this.config.autoplay = true;
} }
if (this.media.hasAttribute('playsinline')) { if (this.media.hasAttribute('playsinline')) {
this.config.inline = true; this.config.playsinline = true;
} }
if (this.media.hasAttribute('muted')) { if (this.media.hasAttribute('muted')) {
this.config.muted = true; this.config.muted = true;
@@ -240,7 +236,7 @@ class Plyr {
} }
// Check for support again but with type // Check for support again but with type
this.supported = support.check(this.type, this.provider, this.config.inline); this.supported = support.check(this.type, this.provider, this.config.playsinline);
// If no support for even API, bail // If no support for even API, bail
if (!this.supported.api) { if (!this.supported.api) {
@@ -337,11 +333,6 @@ class Plyr {
return null; return null;
} }
// If ads are enabled, wait for them first
/* if (this.ads.enabled && !this.ads.initialized) {
return this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play());
} */
// Return the promise (for HTML5) // Return the promise (for HTML5)
return this.media.play(); return this.media.play();
} }
@@ -357,6 +348,13 @@ class Plyr {
this.media.pause(); this.media.pause();
} }
/**
* Get playing state
*/
get playing() {
return Boolean(this.ready && !this.paused && !this.ended);
}
/** /**
* Get paused state * Get paused state
*/ */
@@ -365,10 +363,10 @@ class Plyr {
} }
/** /**
* Get playing state * Get stopped state
*/ */
get playing() { get stopped() {
return Boolean(!this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true)); return Boolean(this.paused && this.currentTime === 0);
} }
/** /**
@@ -398,7 +396,8 @@ class Plyr {
*/ */
stop() { stop() {
if (this.isHTML5) { if (this.isHTML5) {
this.media.load(); this.pause();
this.restart();
} else if (utils.is.function(this.media.stop)) { } else if (utils.is.function(this.media.stop)) {
this.media.stop(); this.media.stop();
} }
@@ -446,7 +445,7 @@ class Plyr {
} }
// Set // Set
this.media.currentTime = parseFloat(targetTime.toFixed(4)); this.media.currentTime = targetTime;
// Logging // Logging
this.debug.log(`Seeking to ${this.currentTime} seconds`); this.debug.log(`Seeking to ${this.currentTime} seconds`);
@@ -492,7 +491,7 @@ class Plyr {
*/ */
get duration() { get duration() {
// Faux duration set via config // Faux duration set via config
const fauxDuration = parseInt(this.config.duration, 10); const fauxDuration = parseFloat(this.config.duration);
// True duration // True duration
const realDuration = this.media ? Number(this.media.duration) : 0; const realDuration = this.media ? Number(this.media.duration) : 0;
@@ -793,17 +792,18 @@ class Plyr {
} }
/** /**
* Set the poster image for a HTML5 video * Set the poster image for a video
* @param {input} - the URL for the new poster image * @param {input} - the URL for the new poster image
*/ */
set poster(input) { set poster(input) {
if (!this.isHTML5 || !this.isVideo) { if (!this.isVideo) {
this.debug.warn('Poster can only be set on HTML5 video'); this.debug.warn('Poster can only be set for video');
return; return;
} }
if (utils.is.string(input)) { if (utils.is.string(input)) {
this.media.setAttribute('poster', input); this.media.setAttribute('poster', input);
ui.setPoster.call(this);
} }
} }
@@ -811,7 +811,7 @@ class Plyr {
* Get the current poster image * Get the current poster image
*/ */
get poster() { get poster() {
if (!this.isHTML5 || !this.isVideo) { if (!this.isVideo) {
return null; return null;
} }
@@ -839,13 +839,13 @@ class Plyr {
* @param {boolean} input - Whether to enable captions * @param {boolean} input - Whether to enable captions
*/ */
toggleCaptions(input) { toggleCaptions(input) {
// If there's no full support, or there's no caption toggle // If there's no full support
if (!this.supported.ui || !utils.is.element(this.elements.buttons.captions)) { if (!this.supported.ui) {
return; return;
} }
// If the method is called without parameter, toggle based on current value // If the method is called without parameter, toggle based on current value
const show = utils.is.boolean(input) ? input : this.elements.container.className.indexOf(this.config.classNames.captions.active) === -1; const show = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
// Nothing to change... // Nothing to change...
if (this.captions.active === show) { if (this.captions.active === show) {
@@ -875,17 +875,29 @@ class Plyr {
return; return;
} }
// Toggle captions based on input
this.toggleCaptions(!utils.is.empty(input));
// If empty string is passed, assume disable captions // If empty string is passed, assume disable captions
if (utils.is.empty(input)) { if (utils.is.empty(input)) {
this.toggleCaptions(false);
return; return;
} }
// Normalize // Normalize
const language = input.toLowerCase(); const language = input.toLowerCase();
// Check for support
if (!this.options.captions.includes(language)) {
this.debug.warn(`Unsupported language option: ${language}`);
return;
}
// Ensure captions are enabled
this.toggleCaptions(true);
// Enabled only
if (language === 'enabled') {
return;
}
// If nothing to change, bail // If nothing to change, bail
if (this.language === language) { if (this.language === language) {
return; return;
@@ -1058,8 +1070,8 @@ class Plyr {
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false); utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
} }
// Check if controls toggled // Set hideControls class
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, true); const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, this.config.hideControls);
// Trigger event and close menu // Trigger event and close menu
if (toggled) { if (toggled) {
@@ -1232,6 +1244,29 @@ class Plyr {
static loadSprite(url, id) { static loadSprite(url, id) {
return utils.loadSprite(url, id); return utils.loadSprite(url, id);
} }
/**
* Setup multiple instances
* @param {*} selector
* @param {object} options
*/
static setup(selector, options = {}) {
let targets = null;
if (utils.is.string(selector)) {
targets = Array.from(document.querySelectorAll(selector));
} else if (utils.is.nodeList(selector)) {
targets = Array.from(selector);
} else if (utils.is.array(selector)) {
targets = selector.filter(i => utils.is.element(i));
}
if (utils.is.empty(targets)) {
return null;
}
return targets.map(t => new Plyr(t, options));
}
} }
export default Plyr; export default Plyr;
+1 -3
View File
@@ -1,14 +1,12 @@
// ========================================================================== // ==========================================================================
// Plyr Polyfilled Build // Plyr Polyfilled Build
// plyr.js v3.1.0 // plyr.js v3.3.7
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
import 'babel-polyfill'; import 'babel-polyfill';
import 'custom-event-polyfill'; import 'custom-event-polyfill';
import Plyr from './plyr'; import Plyr from './plyr';
export default Plyr; export default Plyr;
+7 -7
View File
@@ -2,12 +2,12 @@
// Plyr source update // Plyr source update
// ========================================================================== // ==========================================================================
import { providers } from './types';
import utils from './utils';
import html5 from './html5'; import html5 from './html5';
import media from './media'; import media from './media';
import ui from './ui';
import support from './support'; import support from './support';
import { providers } from './types';
import ui from './ui';
import utils from './utils';
const source = { const source = {
// Add elements to HTML5 media (source, tracks, etc) // Add elements to HTML5 media (source, tracks, etc)
@@ -55,7 +55,7 @@ const source = {
this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5; this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5;
// Check for support // Check for support
this.supported = support.check(this.type, this.provider, this.config.inline); this.supported = support.check(this.type, this.provider, this.config.playsinline);
// Create new markup // Create new markup
switch (`${this.provider}:${this.type}`) { switch (`${this.provider}:${this.type}`) {
@@ -94,8 +94,8 @@ const source = {
if (this.config.autoplay) { if (this.config.autoplay) {
this.media.setAttribute('autoplay', ''); this.media.setAttribute('autoplay', '');
} }
if ('poster' in input) { if (!utils.is.empty(input.poster)) {
this.media.setAttribute('poster', input.poster); this.poster = input.poster;
} }
if (this.config.loop.active) { if (this.config.loop.active) {
this.media.setAttribute('loop', ''); this.media.setAttribute('loop', '');
@@ -103,7 +103,7 @@ const source = {
if (this.config.muted) { if (this.config.muted) {
this.media.setAttribute('muted', ''); this.media.setAttribute('muted', '');
} }
if (this.config.inline) { if (this.config.playsinline) {
this.media.setAttribute('playsinline', ''); this.media.setAttribute('playsinline', '');
} }
} }
+5 -5
View File
@@ -12,16 +12,16 @@ const support = {
// Check for support // Check for support
// Basic functionality vs full UI // Basic functionality vs full UI
check(type, provider, inline) { check(type, provider, playsinline) {
let api = false; let api = false;
let ui = false; let ui = false;
const browser = utils.getBrowser(); const browser = utils.getBrowser();
const playsInline = browser.isIPhone && inline && support.inline; const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
switch (`${provider}:${type}`) { switch (`${provider}:${type}`) {
case 'html5:video': case 'html5:video':
api = support.video; api = support.video;
ui = api && support.rangeInput && (!browser.isIPhone || playsInline); ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline);
break; break;
case 'html5:audio': case 'html5:audio':
@@ -32,7 +32,7 @@ const support = {
case 'youtube:video': case 'youtube:video':
case 'vimeo:video': case 'vimeo:video':
api = true; api = true;
ui = support.rangeInput && (!browser.isIPhone || playsInline); ui = support.rangeInput && (!browser.isIPhone || canPlayInline);
break; break;
default: default:
@@ -59,7 +59,7 @@ const support = {
// Inline playback support // Inline playback support
// https://webkit.org/blog/6784/new-video-policies-for-ios/ // https://webkit.org/blog/6784/new-video-policies-for-ios/
inline: 'playsInline' in document.createElement('video'), playsinline: 'playsInline' in document.createElement('video'),
// Check for mime type support against a player instance // Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html // Credits: http://diveintohtml5.info/everything.html
+43 -11
View File
@@ -2,10 +2,14 @@
// Plyr UI // Plyr UI
// ========================================================================== // ==========================================================================
import utils from './utils';
import captions from './captions'; import captions from './captions';
import controls from './controls'; import controls from './controls';
import i18n from './i18n'; import i18n from './i18n';
import support from './support';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
const ui = { const ui = {
addStyleHook() { addStyleHook() {
@@ -48,11 +52,6 @@ const ui = {
this.listeners.controls(); this.listeners.controls();
} }
// If there's no controls, bail
if (!utils.is.element(this.elements.controls)) {
return;
}
// Remove native controls // Remove native controls
ui.toggleNativeControls.call(this); ui.toggleNativeControls.call(this);
@@ -83,6 +82,18 @@ const ui = {
// Update the UI // Update the UI
ui.checkPlaying.call(this); ui.checkPlaying.call(this);
// Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
// Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls // Ready for API calls
this.ready = true; this.ready = true;
@@ -93,6 +104,9 @@ const ui = {
// Set the title // Set the title
ui.setTitle.call(this); ui.setTitle.call(this);
// Set the poster image
ui.setPoster.call(this);
}, },
// Setup aria attribute for play and iframe title // Setup aria attribute for play and iframe title
@@ -126,20 +140,38 @@ const ui = {
// Default to media type // Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video'; const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', i18n.get('frameTitle', this.config)); iframe.setAttribute('title', format.replace('{title}', title));
} }
}, },
// Set the poster image
setPoster() {
if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) {
return;
}
// Set the inline style
const posters = this.poster.split(',');
this.elements.poster.style.backgroundImage = posters.map(p => `url('${p}')`).join(',');
},
// Check playing state // Check playing state
checkPlaying() { checkPlaying(event) {
// Class hooks // Class hooks
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing); utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused); utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set ARIA state // Set ARIA state
utils.toggleState(this.elements.buttons.play, this.playing); utils.toggleState(this.elements.buttons.play, this.playing);
// Only update controls on non timeupdate events
if (utils.is.event(event) && event.type === 'timeupdate') {
return;
}
// Toggle controls // Toggle controls
this.toggleControls(!this.playing); this.toggleControls(!this.playing);
}, },
@@ -277,10 +309,10 @@ const ui = {
} }
// Always display hours if duration is over an hour // Always display hours if duration is over an hour
const displayHours = utils.getHours(this.duration) > 0; const forceHours = utils.getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
target.textContent = utils.formatTime(time, displayHours, inverted); target.textContent = utils.formatTime(time, forceHours, inverted);
}, },
// Handle time change event // Handle time change event
+34 -74
View File
@@ -3,7 +3,6 @@
// ========================================================================== // ==========================================================================
import loadjs from 'loadjs'; import loadjs from 'loadjs';
import support from './support'; import support from './support';
import { providers } from './types'; import { providers } from './types';
@@ -269,14 +268,14 @@ const utils = {
parent.appendChild(utils.createElement(type, attributes, text)); parent.appendChild(utils.createElement(type, attributes, text));
}, },
// Remove an element // Remove element(s)
removeElement(element) { removeElement(element) {
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) { if (utils.is.nodeList(element) || utils.is.array(element)) {
Array.from(element).forEach(utils.removeElement);
return; return;
} }
if (utils.is.nodeList(element) || utils.is.array(element)) { if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
Array.from(element).forEach(utils.removeElement);
return; return;
} }
@@ -375,6 +374,25 @@ const utils = {
return attributes; return attributes;
}, },
// Toggle hidden
toggleHidden(element, hidden) {
if (!utils.is.element(element)) {
return;
}
let hide = hidden;
if (!utils.is.boolean(hide)) {
hide = !element.hasAttribute('hidden');
}
if (hide) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
},
// Toggle class on an element // Toggle class on an element
toggleClass(element, className, toggle) { toggleClass(element, className, toggle) {
if (utils.is.element(element)) { if (utils.is.element(element)) {
@@ -393,19 +411,6 @@ const utils = {
return utils.is.element(element) && element.classList.contains(className); return utils.is.element(element) && element.classList.contains(className);
}, },
// Toggle hidden attribute on an element
toggleHidden(element, toggle) {
if (!utils.is.element(element)) {
return;
}
if (toggle) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
},
// Element matches selector // Element matches selector
matches(element, selector) { matches(element, selector) {
const prototype = { Element }; const prototype = { Element };
@@ -429,60 +434,6 @@ const utils = {
return this.elements.container.querySelector(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),
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
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.element(this.elements.progress)) {
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
}
return true;
} catch (error) {
// Log it
this.debug.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 // Get the focused element
getFocusElement() { getFocusElement() {
let focused = document.activeElement; let focused = document.activeElement;
@@ -626,6 +577,15 @@ const utils = {
element.setAttribute('aria-pressed', state); element.setAttribute('aria-pressed', state);
}, },
// Format string
format(input, ...args) {
if (utils.is.empty(input)) {
return input;
}
return input.toString().replace(/{(\d+)}/g, (match, i) => utils.is.string(args[i]) ? args[i] : '');
},
// Get percentage // Get percentage
getPercentage(current, max) { getPercentage(current, max) {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
@@ -752,7 +712,7 @@ const utils = {
return null; return null;
} }
return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev); return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
}, },
// Get the provider for a given URL // Get the provider for a given URL
@@ -763,7 +723,7 @@ const utils = {
} }
// Vimeo // Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{8,}(?=\b|\/)/.test(url)) { if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo; return providers.vimeo;
} }
+1
View File
@@ -29,6 +29,7 @@
button { button {
font: inherit; font: inherit;
line-height: inherit; line-height: inherit;
width: auto;
} }
// Ignore focus // Ignore focus
+17 -12
View File
@@ -3,14 +3,12 @@
// YouTube, Vimeo, etc // YouTube, Vimeo, etc
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr__video-embed { // Default to 16:9 ratio but this is set by JavaScript based on config
// Default to 16:9 ratio but this is set by JavaScript based on config $embed-padding: ((100 / 16) * 9);
$padding: ((100 / 16) * 9);
$height: 240;
$offset: to-percentage(($height - $padding) / ($height / 50));
.plyr__video-embed {
height: 0; height: 0;
padding-bottom: to-percentage($padding); padding-bottom: to-percentage($embed-padding);
position: relative; position: relative;
iframe { iframe {
@@ -22,15 +20,22 @@
user-select: none; user-select: none;
width: 100%; width: 100%;
} }
}
// Vimeo hack // If the full custom UI is supported
> div { .plyr--full-ui .plyr__video-embed {
$height: 240;
$offset: to-percentage(($height - $embed-padding) / ($height / 50));
// To allow mouse events to be captured if full support
iframe {
pointer-events: none;
}
// Only used for Vimeo
> .plyr__video-embed__container {
padding-bottom: to-percentage($height); padding-bottom: to-percentage($height);
position: relative; position: relative;
transform: translateY(-$offset); transform: translateY(-$offset);
} }
} }
// To allow mouse events to be captured if full support
.plyr--full-ui .plyr__video-embed iframe {
pointer-events: none;
}
+1 -1
View File
@@ -35,7 +35,7 @@
right: -3px; right: -3px;
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
z-index: 1; z-index: 3;
> div { > div {
overflow: hidden; overflow: hidden;
+23
View File
@@ -0,0 +1,23 @@
// --------------------------------------------------------------
// Faux poster overlay
// --------------------------------------------------------------
.plyr__poster {
background-color: #000;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: contain;
height: 100%;
left: 0;
opacity: 0;
position: absolute;
top: 0;
transition: opacity 0.3s ease;
width: 100%;
z-index: 1;
pointer-events: none;
}
.plyr--stopped .plyr__poster {
opacity: 1;
}
+1 -1
View File
@@ -19,7 +19,7 @@
&::-webkit-slider-runnable-track { &::-webkit-slider-runnable-track {
@include plyr-range-track(); @include plyr-range-track();
background-image: linear-gradient(to right, currentColor var(--value), transparent var(--value)); background-image: linear-gradient(to right, currentColor var(--value, 0%), transparent var(--value, 0%));
} }
&::-webkit-slider-thumb { &::-webkit-slider-thumb {
+1
View File
@@ -19,6 +19,7 @@
transform: translate(-50%, 10px) scale(0.8); transform: translate(-50%, 10px) scale(0.8);
transform-origin: 50% 100%; transform-origin: 50% 100%;
transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease; transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;
white-space: nowrap;
z-index: 2; z-index: 2;
// The background triangle // The background triangle
+7 -2
View File
@@ -23,7 +23,12 @@
// Hide sound controls on iOS // Hide sound controls on iOS
// It's not supported to change volume using JavaScript: // It's not supported to change volume using JavaScript:
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
.plyr--is-ios .plyr__volume, .plyr--is-ios .plyr__volume {
.plyr--is-ios [data-plyr='mute'] { display: none !important;
}
// Vimeo has no toggle mute method so hide mute button
// https://github.com/vimeo/player.js/issues/236#issuecomment-384663183
.plyr--is-ios.plyr--vimeo [data-plyr='mute'] {
display: none !important; display: none !important;
} }
+1
View File
@@ -32,6 +32,7 @@
@import 'components/embed'; @import 'components/embed';
@import 'components/menus'; @import 'components/menus';
@import 'components/progress'; @import 'components/progress';
@import 'components/poster';
@import 'components/sliders'; @import 'components/sliders';
@import 'components/times'; @import 'components/times';
@import 'components/tooltips'; @import 'components/tooltips';
-9
View File
@@ -2,15 +2,6 @@
// Hiding content nicely // Hiding content nicely
// -------------------------------------------------------------- // --------------------------------------------------------------
// Attributes
.plyr--full-ui [hidden] {
display: none;
}
.plyr--full-ui [aria-hidden='true'] {
display: none;
}
// Screen reader only elements // Screen reader only elements
.plyr__sr-only { .plyr__sr-only {
clip: rect(1px, 1px, 1px, 1px); clip: rect(1px, 1px, 1px, 1px);
+1520 -79
View File
File diff suppressed because it is too large Load Diff