Compare commits

...

25 Commits

Author SHA1 Message Date
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
42 changed files with 9029 additions and 7159 deletions
+40
View File
@@ -1,3 +1,43 @@
# 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 ## v3.2.0
* Fullscreen fixes (thanks @friday) * Fullscreen fixes (thanks @friday)
+2 -1
View File
@@ -121,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
+53 -20
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
* }); * });
* *
@@ -4011,7 +4045,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
+2 -2
View File
@@ -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 -->
-1
View File
@@ -182,7 +182,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',
+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
File diff suppressed because one or more lines are too long
+3485 -3368
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
+3485 -3368
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);
}); });
} }
+8 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.2.0", "version": "3.3.2",
"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,7 +8,7 @@
"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.3", "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",
@@ -21,7 +21,7 @@
"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",
@@ -37,8 +37,8 @@
"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",
@@ -46,7 +46,7 @@
"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"
} }
} }
+37 -27
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.2.0/plyr.js"></script> <script src="https://cdn.plyr.io/3.3.2/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.2.0/plyr.css"> <link rel="stylesheet" href="https://cdn.plyr.io/3.3.2/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.2.0/plyr.svg`. reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.2/plyr.svg`.
## Ads ## Ads
@@ -279,7 +279,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 +374,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 +389,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 +600,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 +610,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 +672,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 +713,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)
+2 -7
View File
@@ -3,10 +3,10 @@
// TODO: Create as class // TODO: Create as class
// ========================================================================== // ==========================================================================
import support from './support';
import utils from './utils';
import controls from './controls'; import controls from './controls';
import i18n from './i18n'; import i18n from './i18n';
import support from './support';
import utils from './utils';
const captions = { const captions = {
// Setup captions // Setup captions
@@ -250,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');
+141 -60
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
@@ -483,6 +540,7 @@ const controls = {
break; break;
case 576: case 576:
case 480:
label = 'SD'; label = 'SD';
break; break;
@@ -645,7 +703,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?
// Set a list of available captions languages // Set a list of available captions languages
setCaptionsMenu() { setCaptionsMenu() {
// TODO: Captions or language? Currently it's mixed // TODO: Captions or language? Currently it's mixed
@@ -745,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);
@@ -779,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);
@@ -804,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) {
@@ -820,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 => {
@@ -845,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)) {
@@ -864,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
@@ -908,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
@@ -1054,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,
@@ -1064,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',
}); });
@@ -1115,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(
@@ -1190,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');
} }
} }
@@ -1202,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, {
@@ -1224,6 +1281,30 @@ const controls = {
// 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
@@ -1248,7 +1329,7 @@ const controls = {
// 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
+13 -7
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.2.0/plyr.svg', iconUrl: 'https://cdn.plyr.io/3.3.2/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',
@@ -192,13 +192,17 @@ const defaults = {
// 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',
}, },
}, },
@@ -324,12 +328,14 @@ const defaults = {
classNames: { classNames: {
video: 'plyr__video-wrapper', video: 'plyr__video-wrapper',
embed: 'plyr__video-embed', embed: 'plyr__video-embed',
poster: 'plyr__poster',
ads: 'plyr__ads', ads: 'plyr__ads',
control: 'plyr__control', control: 'plyr__control',
type: 'plyr--{0}', type: 'plyr--{0}',
provider: 'plyr--{0}', provider: 'plyr--{0}',
stopped: 'plyr--stopped',
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',
+7 -7
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}`]();
} }
} }
@@ -196,7 +196,7 @@ class Fullscreen {
(document.cancelFullScreen || document.exitFullscreen).call(document); (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}`]();
} }
} }
+16 -9
View File
@@ -2,9 +2,9 @@
// Plyr Event Listeners // Plyr Event Listeners
// ========================================================================== // ==========================================================================
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();
@@ -253,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
@@ -265,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();
} }
}); });
@@ -281,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));
@@ -334,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();
@@ -492,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',
); );
@@ -506,6 +510,7 @@ class Listeners {
event, event,
() => { () => {
this.player.quality = event.target.value; this.player.quality = event.target.value;
showHomeTab();
}, },
'quality', 'quality',
); );
@@ -514,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('span', {
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.isHTML5 && this.player.isVideo && this.player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length;
}
/** /**
* 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);
+35 -5
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);
}) })
@@ -68,14 +68,14 @@ 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', '');
@@ -86,6 +86,25 @@ const vimeo = {
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);
@@ -134,7 +153,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) {
@@ -320,6 +341,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', () => {
+6 -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, {
+33 -40
View File
@@ -1,26 +1,24 @@
// ========================================================================== // ==========================================================================
// Plyr // Plyr
// plyr.js v3.2.0 // plyr.js v3.3.2
// 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
@@ -134,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
@@ -343,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();
} }
@@ -363,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
*/ */
@@ -371,10 +363,10 @@ class Plyr {
} }
/** /**
* Get playing state * Get stopped state
*/ */
get playing() { get stopped() {
return Boolean(this.ready && !this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true)); return Boolean(this.paused && this.currentTime === 0);
} }
/** /**
@@ -452,7 +444,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`);
@@ -498,7 +490,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;
@@ -799,17 +791,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);
} }
} }
@@ -817,7 +810,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;
} }
@@ -845,8 +838,8 @@ 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;
} }
@@ -1076,8 +1069,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) {
+1 -3
View File
@@ -1,14 +1,12 @@
// ========================================================================== // ==========================================================================
// Plyr Polyfilled Build // Plyr Polyfilled Build
// plyr.js v3.2.0 // plyr.js v3.3.2
// 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;
+5 -5
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)
@@ -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', '');
+37 -10
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,16 +140,29 @@ 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() {
// 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);
@@ -277,10 +304,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
+33 -73
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
+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
+22
View File
@@ -0,0 +1,22 @@
// --------------------------------------------------------------
// Faux poster overlay
// --------------------------------------------------------------
.plyr__poster {
background-color: #000;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: 100% 100%;
height: 100%;
left: 0;
opacity: 0;
position: absolute;
top: 0;
transition: opacity 0.3s ease;
width: 100%;
z-index: 1;
}
.plyr--stopped .plyr__poster {
opacity: 1;
}
+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);
+1518 -77
View File
File diff suppressed because it is too large Load Diff