Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0694e58650 | |||
| e644eeb5b6 | |||
| 5ddd9e02de | |||
| a064f0b4fd | |||
| b9dbcc30fa | |||
| 7ab34c7d2e | |||
| 17caa3c57b | |||
| 2e6361898b | |||
| a1b2c0419f | |||
| 9b81e776fb | |||
| 7a4a7dece8 | |||
| f0d3e8c3b9 | |||
| ad68d9484f | |||
| c3fd822857 | |||
| e147f3a754 | |||
| b694e7d3ab | |||
| b2fff4c33f | |||
| 9c1060d9b0 | |||
| a91652287b | |||
| 2cf44c236d | |||
| 243db9eda3 | |||
| b675ba1f35 | |||
| e9367ee85e | |||
| d9b7928ce6 | |||
| cdacae6697 | |||
| 2bd08cdc28 | |||
| 5fefabe3bd | |||
| e281078441 | |||
| 9bb75f6f52 | |||
| 3b7a24456d | |||
| c885d59270 | |||
| 9f30d54963 | |||
| b247093495 | |||
| 9ca7b861a9 | |||
| 2eccf0dd05 | |||
| 566b957e65 | |||
| a8456f4ca7 | |||
| 0f3098040d | |||
| 21539be3f2 | |||
| c22f5c4b39 | |||
| f4b47a9275 | |||
| 266b70d9d0 | |||
| 6e68ad6d15 | |||
| c202551e6d | |||
| 5b7a025d26 | |||
| 26bcf83960 | |||
| e4acff4f8d | |||
| 568ddf2390 | |||
| ce91945544 | |||
| 11fed8d1b5 | |||
| 4c3bf25b8a | |||
| 215fc3677a | |||
| 4d3b6b882e | |||
| 83eda174af | |||
| 1c29cb890e | |||
| 438ebe5013 | |||
| 4335a2a0d1 | |||
| 825fd292ae |
+2
-1
@@ -2,8 +2,9 @@ demo
|
||||
.github
|
||||
.vscode
|
||||
*.code-workspace
|
||||
build.json
|
||||
credentials.json
|
||||
bundles.json
|
||||
deploy.json
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
*.mp4
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
## v3.5.4
|
||||
|
||||
- Added: Set download URL via new setter
|
||||
- Improvement: The order of the `controls` option now effects the order in the DOM - i.e. you can re-order the controls - Note: this may break any custom CSS you have setup. Please see the changes in the PR to the default SASS
|
||||
- Fixed issue with empty controls and preview thumbs
|
||||
- Fixed issue with setGutter call (from Sentry)
|
||||
- Fixed issue with initial selected speed not working
|
||||
- Added notes on `autoplay` config option and browser compatibility
|
||||
- Fixed issue with ads volume not matching current content volume
|
||||
- Fixed race condition where ads were loading during source change
|
||||
- Improvement: Automatic aspect ratio for YouTube is now supported, meaning all aspect ratios are set based on media content - Note: we're now using a different API to get YouTube video metadata so you may need to adjust any CSPs you have setup
|
||||
- Fix for menu in the Shadow DOM (thanks @emielbeinema)
|
||||
|
||||
## v3.5.3
|
||||
|
||||
- Improved the usage of the `ratio` config option; it now works as expected and for all video types. The default has not changed, it is to dynamically, where possible (except YouTube where 16:9 is used) determine the ratio from the media source so this is not a breaking change.
|
||||
- Added new `ratio` getter and setter
|
||||
- Fix: Properly clear all timeouts on destroy
|
||||
- Fix: Allow absolute paths in preview thumbnails
|
||||
- Improvement: Allow optional hours and ms in VTT parser in preview thumbnails
|
||||
|
||||
## v3.5.2
|
||||
|
||||
- Fixed issue where the preview thumbnail was present while scrubbing
|
||||
|
||||
## v3.5.1
|
||||
|
||||
- Fixed build issues with babel and browserslist
|
||||
|
||||
## v3.5.0
|
||||
|
||||
- Preview seek/scrubbing thumbnails (thanks @jamesoflol)
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+5122
-5102
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -265,7 +265,7 @@
|
||||
|
||||
<!-- Polyfills -->
|
||||
<script
|
||||
src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL,Math.trunc"
|
||||
src="https://cdn.polyfill.io/v3/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL,Math.trunc&flags=gated"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1285
-1030
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1282
-1027
File diff suppressed because it is too large
Load Diff
Vendored
+6854
-3915
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+6851
-3912
File diff suppressed because it is too large
Load Diff
+40
-43
@@ -30,6 +30,7 @@ const header = require('gulp-header');
|
||||
const gitbranch = require('git-branch');
|
||||
const rename = require('gulp-rename');
|
||||
const replace = require('gulp-replace');
|
||||
const ansi = require('ansi-colors');
|
||||
const log = require('fancy-log');
|
||||
const open = require('gulp-open');
|
||||
const plumber = require('gulp-plumber');
|
||||
@@ -104,32 +105,14 @@ const tasks = {
|
||||
css: [],
|
||||
js: [],
|
||||
sprite: [],
|
||||
clean: ['clean'],
|
||||
clean: 'clean',
|
||||
};
|
||||
|
||||
// Size plugin
|
||||
const sizeOptions = { showFiles: true, gzip: true };
|
||||
|
||||
// Babel config
|
||||
const babelrc = (polyfill = false) => ({
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
browsers,
|
||||
},
|
||||
useBuiltIns: polyfill ? 'usage' : false,
|
||||
modules: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
babelrc: false,
|
||||
exclude: 'node_modules/**',
|
||||
});
|
||||
|
||||
// Clean out /dist
|
||||
gulp.task('clean', done => {
|
||||
gulp.task(tasks.clean, done => {
|
||||
const dirs = [paths.plyr.output, paths.demo.output].map(dir => path.join(dir, '**/*'));
|
||||
|
||||
// Don't delete the mp4
|
||||
@@ -148,19 +131,35 @@ Object.entries(build.js).forEach(([filename, entry]) => {
|
||||
const polyfill = filename.includes('polyfilled');
|
||||
const extension = format === 'es' ? 'mjs' : 'js';
|
||||
|
||||
gulp.task(name, () => {
|
||||
return gulp
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(entry.src)
|
||||
.pipe(plumber())
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(
|
||||
rollup(
|
||||
{
|
||||
plugins: [resolve(), commonjs(), babel(babelrc(polyfill))],
|
||||
plugins: [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
babel({
|
||||
presets: [
|
||||
[
|
||||
'@babel/env',
|
||||
{
|
||||
// debug: true,
|
||||
useBuiltIns: polyfill ? 'usage' : false,
|
||||
corejs: polyfill ? 3 : undefined,
|
||||
},
|
||||
],
|
||||
],
|
||||
babelrc: false,
|
||||
exclude: [/\/core-js\//],
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: entry.namespace,
|
||||
// exports: 'named',
|
||||
format,
|
||||
},
|
||||
),
|
||||
@@ -172,13 +171,13 @@ Object.entries(build.js).forEach(([filename, entry]) => {
|
||||
}),
|
||||
)
|
||||
.pipe(gulp.dest(entry.dist))
|
||||
.pipe(filter(`**/*${extension}`))
|
||||
.pipe(filter(`**/*.${extension}`))
|
||||
.pipe(terser())
|
||||
.pipe(rename({ suffix: minSuffix }))
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(sourcemaps.write(''))
|
||||
.pipe(gulp.dest(entry.dist));
|
||||
});
|
||||
.pipe(gulp.dest(entry.dist)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,8 +186,8 @@ Object.entries(build.css).forEach(([filename, entry]) => {
|
||||
const name = `css:${filename}`;
|
||||
tasks.css.push(name);
|
||||
|
||||
gulp.task(name, () => {
|
||||
return gulp
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(entry.src)
|
||||
.pipe(plumber())
|
||||
.pipe(sass())
|
||||
@@ -199,8 +198,8 @@ Object.entries(build.css).forEach(([filename, entry]) => {
|
||||
)
|
||||
.pipe(clean())
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(gulp.dest(entry.dist));
|
||||
});
|
||||
.pipe(gulp.dest(entry.dist)),
|
||||
);
|
||||
});
|
||||
|
||||
// SVG Sprites
|
||||
@@ -208,18 +207,16 @@ Object.entries(build.sprite).forEach(([filename, entry]) => {
|
||||
const name = `sprite:${filename}`;
|
||||
tasks.sprite.push(name);
|
||||
|
||||
log(path.basename(filename));
|
||||
|
||||
gulp.task(name, () => {
|
||||
return gulp
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(entry.src)
|
||||
.pipe(plumber())
|
||||
.pipe(imagemin())
|
||||
.pipe(svgstore())
|
||||
.pipe(rename({ basename: path.parse(filename).name }))
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(gulp.dest(entry.dist));
|
||||
});
|
||||
.pipe(gulp.dest(entry.dist)),
|
||||
);
|
||||
});
|
||||
|
||||
// Build all JS
|
||||
@@ -317,7 +314,7 @@ gulp.task('version', done => {
|
||||
|
||||
const { domain } = deploy.cdn;
|
||||
|
||||
console.log(`Updating versions to '${version}'...`);
|
||||
log(`Uploading ${ansi.green.bold(version)} to ${ansi.cyan(domain)}...`);
|
||||
|
||||
// Replace versioned URLs in source
|
||||
const files = ['plyr.js', 'plyr.polyfilled.js', 'config/defaults.js'];
|
||||
@@ -342,7 +339,7 @@ gulp.task('cdn', done => {
|
||||
throw new Error('No publisher instance. Check AWS configuration.');
|
||||
}
|
||||
|
||||
console.log(`Uploading '${version}' to ${domain}...`);
|
||||
log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`);
|
||||
|
||||
// Upload to CDN
|
||||
return (
|
||||
@@ -385,13 +382,13 @@ gulp.task('purge', () => {
|
||||
const purge = new FastlyPurge(fastly.token);
|
||||
|
||||
list.forEach(url => {
|
||||
console.log(`Purging ${url}...`);
|
||||
log(`Purging ${ansi.cyan(url)}...`);
|
||||
|
||||
purge.url(url, (error, result) => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
log.error(error);
|
||||
} else if (result) {
|
||||
console.log(result);
|
||||
log(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -412,7 +409,7 @@ gulp.task('demo', done => {
|
||||
throw new Error('No publisher instance. Check AWS configuration.');
|
||||
}
|
||||
|
||||
console.log(`Uploading '${version}' demo to ${deploy.demo.domain}...`);
|
||||
log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`);
|
||||
|
||||
// Replace versioned files in readme.md
|
||||
gulp.src([`${__dirname}/readme.md`])
|
||||
|
||||
+26
-28
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plyr",
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.4",
|
||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||
"homepage": "https://plyr.io",
|
||||
"author": "Sam Potts <sam@potts.es>",
|
||||
@@ -27,10 +27,7 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/sampotts/plyr/issues"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"not dead"
|
||||
],
|
||||
"browserslist": "> 1%",
|
||||
"scripts": {
|
||||
"build": "gulp build",
|
||||
"lint": "eslint src/js && npm run-script remark",
|
||||
@@ -38,15 +35,15 @@
|
||||
"deploy": "yarn lint && gulp deploy"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-sdk": "^2.404.0",
|
||||
"@babel/core": "^7.3.3",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"ansi-colors": "^3.2.4",
|
||||
"aws-sdk": "^2.437.0",
|
||||
"@babel/core": "^7.4.3",
|
||||
"@babel/preset-env": "^7.4.3",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-preset-minify": "^0.5.0",
|
||||
"del": "^3.0.0",
|
||||
"eslint": "^5.14.0",
|
||||
"del": "^4.1.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-airbnb-base": "^13.1.0",
|
||||
"eslint-config-prettier": "^4.0.0",
|
||||
"eslint-config-prettier": "^4.1.0",
|
||||
"eslint-plugin-import": "^2.16.0",
|
||||
"fancy-log": "^1.3.3",
|
||||
"fastly-purge": "^1.0.1",
|
||||
@@ -54,7 +51,7 @@
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-autoprefixer": "^6.0.0",
|
||||
"gulp-awspublish": "^4.0.0",
|
||||
"gulp-better-rollup": "^3.4.0",
|
||||
"gulp-better-rollup": "^4.0.1",
|
||||
"gulp-clean-css": "^4.0.0",
|
||||
"gulp-filter": "^5.1.0",
|
||||
"gulp-header": "^2.0.7",
|
||||
@@ -66,32 +63,33 @@
|
||||
"gulp-replace": "^1.0.0",
|
||||
"gulp-sass": "^4.0.2",
|
||||
"gulp-size": "^3.0.0",
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"gulp-sourcemaps": "^2.6.5",
|
||||
"gulp-svgstore": "^7.0.1",
|
||||
"gulp-terser": "^1.1.7",
|
||||
"postcss-custom-properties": "^8.0.9",
|
||||
"postcss-custom-properties": "^8.0.10",
|
||||
"prettier-eslint": "^8.8.2",
|
||||
"prettier-stylelint": "^0.4.2",
|
||||
"remark-cli": "^6.0.1",
|
||||
"remark-validate-links": "^8.0.0",
|
||||
"remark-validate-links": "^8.0.2",
|
||||
"rollup": "^1.10.0",
|
||||
"rollup-plugin-babel": "^4.3.2",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-node-resolve": "^4.0.0",
|
||||
"rollup-plugin-commonjs": "^9.3.4",
|
||||
"rollup-plugin-node-resolve": "^4.2.3",
|
||||
"stylelint": "^9.10.1",
|
||||
"stylelint-config-prettier": "^4.0.0",
|
||||
"stylelint-config-prettier": "^5.0.0",
|
||||
"stylelint-config-recommended": "^2.1.0",
|
||||
"stylelint-config-sass-guidelines": "^5.3.0",
|
||||
"stylelint-order": "^2.0.0",
|
||||
"stylelint-scss": "^3.5.3",
|
||||
"stylelint-selector-bem-pattern": "^2.0.0",
|
||||
"through2": "^3.0.0"
|
||||
"stylelint-config-sass-guidelines": "^5.4.0",
|
||||
"stylelint-order": "^2.2.1",
|
||||
"stylelint-scss": "^3.5.4",
|
||||
"stylelint-selector-bem-pattern": "^2.1.0",
|
||||
"through2": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^2.6.5",
|
||||
"custom-event-polyfill": "^1.0.6",
|
||||
"loadjs": "^3.5.5",
|
||||
"core-js": "^3.0.1",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"loadjs": "^3.6.1",
|
||||
"rangetouch": "^2.0.0",
|
||||
"raven-js": "^3.27.0",
|
||||
"url-polyfill": "^1.1.3"
|
||||
"url-polyfill": "^1.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
+5
-7
@@ -5,23 +5,20 @@
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
// Exclude from the editor
|
||||
"files.exclude": {
|
||||
"**/node_modules": true
|
||||
},
|
||||
// Exclude from search
|
||||
"search.exclude": {
|
||||
"dist/": true,
|
||||
"demo/dist/": true
|
||||
"**/node_modules": true,
|
||||
"**/dist": true
|
||||
},
|
||||
// Linting
|
||||
"stylelint.enable": true,
|
||||
"css.validate": false,
|
||||
"scss.validate": false,
|
||||
"javascript.validate.enable": false,
|
||||
|
||||
// Prettier
|
||||
"prettier.eslintIntegration": true,
|
||||
"prettier.stylelintIntegration": true,
|
||||
|
||||
// Formatting
|
||||
"editor.tabSize": 4,
|
||||
"editor.insertSpaces": true,
|
||||
@@ -29,6 +26,7 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
|
||||
// Trim on save
|
||||
"files.trimTrailingWhitespace": true
|
||||
}
|
||||
|
||||
@@ -123,13 +123,13 @@ See [initialising](#initialising) for more information on advanced setups.
|
||||
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.5.0/plyr.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.5.4/plyr.js"></script>
|
||||
```
|
||||
|
||||
...or...
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.5.0/plyr.polyfilled.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.5.4/plyr.polyfilled.js"></script>
|
||||
```
|
||||
|
||||
## CSS
|
||||
@@ -143,13 +143,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:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.0/plyr.css" />
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.4/plyr.css" />
|
||||
```
|
||||
|
||||
## 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
|
||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.5.0/plyr.svg`.
|
||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.5.4/plyr.svg`.
|
||||
|
||||
# Ads
|
||||
|
||||
@@ -275,7 +275,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
||||
| `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. |
|
||||
| `iconPrefix` | String | `plyr` | Specify the id prefix for the icons used in the default controls (e.g. "plyr-play" would be "plyr"). This is to prevent clashes if you're using your own SVG sprite but with the default controls. Most people can ignore this option. |
|
||||
| `blankVideo` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
|
||||
| `autoplay` | Boolean | `false` | Autoplay the media on load. This is generally advised against on UX grounds. It is also disabled by default in some browsers. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
|
||||
| `autoplay`² | Boolean | `false` | Autoplay the media on load. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
|
||||
| `autopause`¹ | Boolean | `true` | Only allow one player playing at once. |
|
||||
| `seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind. |
|
||||
| `volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player. |
|
||||
@@ -293,10 +293,10 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
||||
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
|
||||
| `captions` | Object | `{ active: false, language: 'auto', update: false }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). 'auto' uses the browser language. `update`: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in unselectable language options). |
|
||||
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution (`true`/`false`/`'force'`). `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
|
||||
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
|
||||
| `ratio` | String | `null` | Force an aspect ratio for all videos. The format is `'w:h'` - e.g. `'16:9'` or `'4:3'`. If this is not specified then the default for HTML5 and Vimeo is to use the native resolution of the video. As dimensions are not available from YouTube via SDK, 16:9 is forced as a sensible default. |
|
||||
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
|
||||
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. |
|
||||
| `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. |
|
||||
| `quality` | Object | `{ default: 576, options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240] }` | `default` is the default quality level (if it exists in your sources). `options` are the options to display. This is used to filter the available sources. |
|
||||
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
|
||||
| `ads` | Object | `{ enabled: false, publisherId: '' }` | `enabled`: Whether to enable advertisements. `publisherId`: Your unique [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) publisher ID. |
|
||||
| `urls` | Object | See source. | If you wish to override any API URLs then you can do so here. You can also set a custom download URL for the download button. |
|
||||
@@ -305,6 +305,11 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
||||
| `previewThumbnails` | Object | `{ enabled: false, src: '' }` | `enabled`: Whether to enable the preview thumbnails (they must be generated by you). `src` must be either a string or an array of strings representing URLs for the VTT files containing the image URL(s). Learn more about [preview thumbnails](#preview-thumbnails) below. |
|
||||
|
||||
1. Vimeo only
|
||||
2. Autoplay is generally not recommended as it is seen as a negative user experience. It is also disabled in many browsers. Before raising issues, do your homework. More info can be found here:
|
||||
|
||||
- https://webkit.org/blog/6784/new-video-policies-for-ios/
|
||||
- https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
|
||||
- https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/
|
||||
|
||||
# API
|
||||
|
||||
@@ -404,10 +409,11 @@ player.fullscreen.active; // false;
|
||||
| `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. If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use `currentTrack` instead. |
|
||||
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
|
||||
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
|
||||
| `pip`² | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ (on MacOS Sierra+ and iOS 10+) and Chrome 70+. |
|
||||
| `pip`¹ | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ (on MacOS Sierra+ and iOS 10+) and Chrome 70+. |
|
||||
| `ratio` | ✓ | ✓ | Gets or sets the video aspect ratio. The setter accepts a string in the same format as the `ratio` option. |
|
||||
| `download` | ✓ | ✓ | Gets or sets the URL for the download button. The setter accepts a string containing a valid absolute URL. |
|
||||
|
||||
1. YouTube only. HTML5 will follow.
|
||||
2. HTML5 only
|
||||
1. HTML5 only
|
||||
|
||||
### The `.source` setter
|
||||
|
||||
|
||||
+17
-12
@@ -124,19 +124,21 @@ const captions = {
|
||||
|
||||
// Handle tracks (add event listener and "pseudo"-default)
|
||||
if (this.isHTML5 && this.isVideo) {
|
||||
tracks.filter(track => !meta.get(track)).forEach(track => {
|
||||
this.debug.log('Track added', track);
|
||||
// Attempt to store if the original dom element was "default"
|
||||
meta.set(track, {
|
||||
default: track.mode === 'showing',
|
||||
tracks
|
||||
.filter(track => !meta.get(track))
|
||||
.forEach(track => {
|
||||
this.debug.log('Track added', track);
|
||||
// Attempt to store if the original dom element was "default"
|
||||
meta.set(track, {
|
||||
default: track.mode === 'showing',
|
||||
});
|
||||
|
||||
// Turn off native caption rendering to avoid double captions
|
||||
track.mode = 'hidden';
|
||||
|
||||
// Add event listener for cue changes
|
||||
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
|
||||
});
|
||||
|
||||
// Turn off native caption rendering to avoid double captions
|
||||
track.mode = 'hidden';
|
||||
|
||||
// Add event listener for cue changes
|
||||
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
|
||||
});
|
||||
}
|
||||
|
||||
// Update language first time it matches, or if the previous matching track was removed
|
||||
@@ -300,10 +302,12 @@ const captions = {
|
||||
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
|
||||
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
|
||||
let track;
|
||||
|
||||
languages.every(language => {
|
||||
track = sorted.find(track => track.language === language);
|
||||
return !track; // Break iteration if there is a match
|
||||
});
|
||||
|
||||
// If no match is found but is required, get first
|
||||
return track || (force ? sorted[0] : undefined);
|
||||
},
|
||||
@@ -360,6 +364,7 @@ const captions = {
|
||||
// Get cues from track
|
||||
if (!cues) {
|
||||
const track = captions.getCurrentTrack.call(this);
|
||||
|
||||
cues = Array.from((track || {}).activeCues || [])
|
||||
.map(cue => cue.getCueAsHTML())
|
||||
.map(getHTML);
|
||||
|
||||
@@ -42,8 +42,9 @@ const defaults = {
|
||||
// Clicking the currentTime inverts it's value to show time left rather than elapsed
|
||||
toggleInvert: true,
|
||||
|
||||
// Aspect ratio (for embeds)
|
||||
ratio: '16:9',
|
||||
// Force an aspect ratio
|
||||
// The format must be `'w:h'` (e.g. `'16:9'`)
|
||||
ratio: null,
|
||||
|
||||
// Click video container to play/pause
|
||||
clickToPlay: true,
|
||||
@@ -60,7 +61,7 @@ const defaults = {
|
||||
// Sprite (for icons)
|
||||
loadSprite: true,
|
||||
iconPrefix: 'plyr',
|
||||
iconUrl: 'https://cdn.plyr.io/3.5.0/plyr.svg',
|
||||
iconUrl: 'https://cdn.plyr.io/3.5.4/plyr.svg',
|
||||
|
||||
// Blank video (used to prevent errors on source change)
|
||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||
@@ -194,8 +195,7 @@ const defaults = {
|
||||
},
|
||||
youtube: {
|
||||
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',
|
||||
api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}', // 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title),fileDetails)&part=snippet',
|
||||
},
|
||||
googleIMA: {
|
||||
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
||||
@@ -319,9 +319,6 @@ const defaults = {
|
||||
progress: '.plyr__progress',
|
||||
captions: '.plyr__captions',
|
||||
caption: '.plyr__caption',
|
||||
menu: {
|
||||
quality: '.js-plyr__menu__list--quality',
|
||||
},
|
||||
},
|
||||
|
||||
// Class hooks added to the player in different states
|
||||
@@ -330,6 +327,7 @@ const defaults = {
|
||||
provider: 'plyr--{0}',
|
||||
video: 'plyr__video-wrapper',
|
||||
embed: 'plyr__video-embed',
|
||||
videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',
|
||||
embedContainer: 'plyr__video-embed__container',
|
||||
poster: 'plyr__poster',
|
||||
posterEnabled: 'plyr__poster-enabled',
|
||||
@@ -394,11 +392,6 @@ const defaults = {
|
||||
},
|
||||
},
|
||||
|
||||
// API keys
|
||||
keys: {
|
||||
google: null,
|
||||
},
|
||||
|
||||
// Advertisements plugin
|
||||
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
|
||||
ads: {
|
||||
|
||||
Vendored
+321
-297
@@ -10,20 +10,7 @@ import support from './support';
|
||||
import { repaint, transitionEndEvent } from './utils/animation';
|
||||
import { dedupe } from './utils/arrays';
|
||||
import browser from './utils/browser';
|
||||
import {
|
||||
createElement,
|
||||
emptyElement,
|
||||
getAttributesFromSelector,
|
||||
getElement,
|
||||
getElements,
|
||||
hasClass,
|
||||
matches,
|
||||
removeElement,
|
||||
setAttributes,
|
||||
setFocus,
|
||||
toggleClass,
|
||||
toggleHidden,
|
||||
} from './utils/elements';
|
||||
import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, matches, removeElement, setAttributes, setFocus, toggleClass, toggleHidden } from './utils/elements';
|
||||
import { off, on } from './utils/events';
|
||||
import i18n from './utils/i18n';
|
||||
import is from './utils/is';
|
||||
@@ -172,7 +159,7 @@ const controls = {
|
||||
|
||||
// Create a <button>
|
||||
createButton(buttonType, attr) {
|
||||
const attributes = Object.assign({}, attr);
|
||||
const attributes = extend({}, attr);
|
||||
let type = toCamelCase(buttonType);
|
||||
|
||||
const props = {
|
||||
@@ -198,8 +185,10 @@ const controls = {
|
||||
|
||||
// Set class name
|
||||
if (Object.keys(attributes).includes('class')) {
|
||||
if (!attributes.class.includes(this.config.classNames.control)) {
|
||||
attributes.class += ` ${this.config.classNames.control}`;
|
||||
if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {
|
||||
extend(attributes, {
|
||||
class: `${attributes.class} ${this.config.classNames.control}`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
attributes.class = this.config.classNames.control;
|
||||
@@ -377,13 +366,13 @@ const controls = {
|
||||
},
|
||||
|
||||
// Create time display
|
||||
createTime(type) {
|
||||
const attributes = getAttributesFromSelector(this.config.selectors.display[type]);
|
||||
createTime(type, attrs) {
|
||||
const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);
|
||||
|
||||
const container = createElement(
|
||||
'div',
|
||||
extend(attributes, {
|
||||
class: `${this.config.classNames.display.time} ${attributes.class ? attributes.class : ''}`.trim(),
|
||||
class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),
|
||||
'aria-label': i18n.get(type, this.config),
|
||||
}),
|
||||
'00:00',
|
||||
@@ -1138,7 +1127,10 @@ const controls = {
|
||||
} else if (is.keyboardEvent(input) && input.which === 27) {
|
||||
show = false;
|
||||
} else if (is.event(input)) {
|
||||
const isMenuItem = popup.contains(input.target);
|
||||
// If Plyr is in a shadowDOM, the event target is set to the component, instead of the
|
||||
// Element in the shadowDOM. The path, if available, is complete.
|
||||
const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;
|
||||
const isMenuItem = popup.contains(target);
|
||||
|
||||
// If the click was inside the menu or if the click
|
||||
// wasn't the button or menu item and we're trying to
|
||||
@@ -1191,7 +1183,7 @@ const controls = {
|
||||
|
||||
// Show a panel in the menu
|
||||
showMenuPanel(type = '', tabFocus = false) {
|
||||
const target = document.getElementById(`plyr-settings-${this.id}-${type}`);
|
||||
const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);
|
||||
|
||||
// Nothing to show, bail
|
||||
if (!is.element(target)) {
|
||||
@@ -1244,8 +1236,8 @@ const controls = {
|
||||
controls.focusFirstMenuItem.call(this, target, tabFocus);
|
||||
},
|
||||
|
||||
// Set the download link
|
||||
setDownloadLink() {
|
||||
// Set the download URL
|
||||
setDownloadUrl() {
|
||||
const button = this.elements.buttons.download;
|
||||
|
||||
// Bail if no button
|
||||
@@ -1253,324 +1245,356 @@ const controls = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set download link
|
||||
// Set attribute
|
||||
button.setAttribute('href', this.download);
|
||||
},
|
||||
|
||||
// Build the default HTML
|
||||
// TODO: Set order based on order in the config.controls array?
|
||||
create(data) {
|
||||
const {
|
||||
bindMenuItemShortcuts,
|
||||
createButton,
|
||||
createProgress,
|
||||
createRange,
|
||||
createTime,
|
||||
setQualityMenu,
|
||||
setSpeedMenu,
|
||||
showMenuPanel,
|
||||
} = controls;
|
||||
this.elements.controls = null;
|
||||
|
||||
// Larger overlaid play button
|
||||
if (this.config.controls.includes('play-large')) {
|
||||
this.elements.container.appendChild(createButton.call(this, 'play-large'));
|
||||
}
|
||||
|
||||
// Create the container
|
||||
const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));
|
||||
this.elements.controls = container;
|
||||
|
||||
// Restart button
|
||||
if (this.config.controls.includes('restart')) {
|
||||
container.appendChild(controls.createButton.call(this, 'restart'));
|
||||
}
|
||||
// Default item attributes
|
||||
const defaultAttributes = { class: 'plyr__controls__item' };
|
||||
|
||||
// Rewind button
|
||||
if (this.config.controls.includes('rewind')) {
|
||||
container.appendChild(controls.createButton.call(this, 'rewind'));
|
||||
}
|
||||
// Loop through controls in order
|
||||
dedupe(this.config.controls).forEach(control => {
|
||||
// Restart button
|
||||
if (control === 'restart') {
|
||||
container.appendChild(createButton.call(this, 'restart', defaultAttributes));
|
||||
}
|
||||
|
||||
// Play/Pause button
|
||||
if (this.config.controls.includes('play')) {
|
||||
container.appendChild(controls.createButton.call(this, 'play'));
|
||||
}
|
||||
// Rewind button
|
||||
if (control === 'rewind') {
|
||||
container.appendChild(createButton.call(this, 'rewind', defaultAttributes));
|
||||
}
|
||||
|
||||
// Fast forward button
|
||||
if (this.config.controls.includes('fast-forward')) {
|
||||
container.appendChild(controls.createButton.call(this, 'fast-forward'));
|
||||
}
|
||||
// Play/Pause button
|
||||
if (control === 'play') {
|
||||
container.appendChild(createButton.call(this, 'play', defaultAttributes));
|
||||
}
|
||||
|
||||
// Progress
|
||||
if (this.config.controls.includes('progress')) {
|
||||
const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
|
||||
// Fast forward button
|
||||
if (control === 'fast-forward') {
|
||||
container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));
|
||||
}
|
||||
|
||||
// Seek range slider
|
||||
progress.appendChild(
|
||||
controls.createRange.call(this, 'seek', {
|
||||
id: `plyr-seek-${data.id}`,
|
||||
}),
|
||||
);
|
||||
// Progress
|
||||
if (control === 'progress') {
|
||||
const progressContainer = createElement('div', {
|
||||
class: `${defaultAttributes.class} plyr__progress__container`,
|
||||
});
|
||||
|
||||
// Buffer progress
|
||||
progress.appendChild(controls.createProgress.call(this, 'buffer'));
|
||||
const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
|
||||
|
||||
// TODO: Add loop display indicator
|
||||
|
||||
// Seek tooltip
|
||||
if (this.config.tooltips.seek) {
|
||||
const tooltip = createElement(
|
||||
'span',
|
||||
{
|
||||
class: this.config.classNames.tooltip,
|
||||
},
|
||||
'00:00',
|
||||
// Seek range slider
|
||||
progress.appendChild(
|
||||
createRange.call(this, 'seek', {
|
||||
id: `plyr-seek-${data.id}`,
|
||||
}),
|
||||
);
|
||||
|
||||
progress.appendChild(tooltip);
|
||||
this.elements.display.seekTooltip = tooltip;
|
||||
// Buffer progress
|
||||
progress.appendChild(createProgress.call(this, 'buffer'));
|
||||
|
||||
// TODO: Add loop display indicator
|
||||
|
||||
// Seek tooltip
|
||||
if (this.config.tooltips.seek) {
|
||||
const tooltip = createElement(
|
||||
'span',
|
||||
{
|
||||
class: this.config.classNames.tooltip,
|
||||
},
|
||||
'00:00',
|
||||
);
|
||||
|
||||
progress.appendChild(tooltip);
|
||||
this.elements.display.seekTooltip = tooltip;
|
||||
}
|
||||
|
||||
this.elements.progress = progress;
|
||||
progressContainer.appendChild(this.elements.progress);
|
||||
container.appendChild(progressContainer);
|
||||
}
|
||||
|
||||
this.elements.progress = progress;
|
||||
container.appendChild(this.elements.progress);
|
||||
}
|
||||
|
||||
// Media current time display
|
||||
if (this.config.controls.includes('current-time')) {
|
||||
container.appendChild(controls.createTime.call(this, 'currentTime'));
|
||||
}
|
||||
|
||||
// Media duration display
|
||||
if (this.config.controls.includes('duration')) {
|
||||
container.appendChild(controls.createTime.call(this, 'duration'));
|
||||
}
|
||||
|
||||
// Volume controls
|
||||
if (this.config.controls.includes('mute') || this.config.controls.includes('volume')) {
|
||||
const volume = createElement('div', {
|
||||
class: 'plyr__volume',
|
||||
});
|
||||
|
||||
// Toggle mute button
|
||||
if (this.config.controls.includes('mute')) {
|
||||
volume.appendChild(controls.createButton.call(this, 'mute'));
|
||||
// Media current time display
|
||||
if (control === 'current-time') {
|
||||
container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));
|
||||
}
|
||||
|
||||
// Volume range control
|
||||
if (this.config.controls.includes('volume')) {
|
||||
// Set the attributes
|
||||
const attributes = {
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
value: this.config.volume,
|
||||
};
|
||||
// Media duration display
|
||||
if (control === 'duration') {
|
||||
container.appendChild(createTime.call(this, 'duration', defaultAttributes));
|
||||
}
|
||||
|
||||
// Create the volume range slider
|
||||
volume.appendChild(
|
||||
controls.createRange.call(
|
||||
this,
|
||||
'volume',
|
||||
extend(attributes, {
|
||||
id: `plyr-volume-${data.id}`,
|
||||
// Volume controls
|
||||
if (control === 'mute' || control === 'volume') {
|
||||
let { volume } = this.elements;
|
||||
|
||||
// Create the volume container if needed
|
||||
if (!is.element(volume) || !container.contains(volume)) {
|
||||
volume = createElement(
|
||||
'div',
|
||||
extend({}, defaultAttributes, {
|
||||
class: `${defaultAttributes.class} plyr__volume`.trim(),
|
||||
}),
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
this.elements.volume = volume;
|
||||
this.elements.volume = volume;
|
||||
|
||||
container.appendChild(volume);
|
||||
}
|
||||
|
||||
// Toggle mute button
|
||||
if (control === 'mute') {
|
||||
volume.appendChild(createButton.call(this, 'mute'));
|
||||
}
|
||||
|
||||
// Volume range control
|
||||
if (control === 'volume') {
|
||||
// Set the attributes
|
||||
const attributes = {
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
value: this.config.volume,
|
||||
};
|
||||
|
||||
// Create the volume range slider
|
||||
volume.appendChild(
|
||||
createRange.call(
|
||||
this,
|
||||
'volume',
|
||||
extend(attributes, {
|
||||
id: `plyr-volume-${data.id}`,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
container.appendChild(volume);
|
||||
}
|
||||
// Toggle captions button
|
||||
if (control === 'captions') {
|
||||
container.appendChild(createButton.call(this, 'captions', defaultAttributes));
|
||||
}
|
||||
|
||||
// Toggle captions button
|
||||
if (this.config.controls.includes('captions')) {
|
||||
container.appendChild(controls.createButton.call(this, 'captions'));
|
||||
}
|
||||
|
||||
// Settings button / menu
|
||||
if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
|
||||
const control = createElement('div', {
|
||||
class: 'plyr__menu',
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
control.appendChild(
|
||||
controls.createButton.call(this, 'settings', {
|
||||
'aria-haspopup': true,
|
||||
'aria-controls': `plyr-settings-${data.id}`,
|
||||
'aria-expanded': false,
|
||||
}),
|
||||
);
|
||||
|
||||
const popup = createElement('div', {
|
||||
class: 'plyr__menu__container',
|
||||
id: `plyr-settings-${data.id}`,
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
const inner = createElement('div');
|
||||
|
||||
const home = createElement('div', {
|
||||
id: `plyr-settings-${data.id}-home`,
|
||||
});
|
||||
|
||||
// Create the menu
|
||||
const menu = createElement('div', {
|
||||
role: 'menu',
|
||||
});
|
||||
|
||||
home.appendChild(menu);
|
||||
inner.appendChild(home);
|
||||
this.elements.settings.panels.home = home;
|
||||
|
||||
// Build the menu items
|
||||
this.config.settings.forEach(type => {
|
||||
// TODO: bundle this with the createMenuItem helper and bindings
|
||||
const menuItem = createElement(
|
||||
'button',
|
||||
extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
|
||||
type: 'button',
|
||||
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
|
||||
role: 'menuitem',
|
||||
'aria-haspopup': true,
|
||||
// Settings button / menu
|
||||
if (control === 'settings' && !is.empty(this.config.settings)) {
|
||||
const control = createElement(
|
||||
'div',
|
||||
extend({}, defaultAttributes, {
|
||||
class: `${defaultAttributes.class} plyr__menu`.trim(),
|
||||
hidden: '',
|
||||
}),
|
||||
);
|
||||
|
||||
// Bind menu shortcuts for keyboard users
|
||||
controls.bindMenuItemShortcuts.call(this, menuItem, type);
|
||||
|
||||
// Show menu on click
|
||||
on(menuItem, 'click', () => {
|
||||
controls.showMenuPanel.call(this, type, false);
|
||||
});
|
||||
|
||||
const flex = createElement('span', null, i18n.get(type, this.config));
|
||||
|
||||
const value = createElement('span', {
|
||||
class: this.config.classNames.menu.value,
|
||||
});
|
||||
|
||||
// Speed contains HTML entities
|
||||
value.innerHTML = data[type];
|
||||
|
||||
flex.appendChild(value);
|
||||
menuItem.appendChild(flex);
|
||||
menu.appendChild(menuItem);
|
||||
|
||||
// Build the panes
|
||||
const pane = createElement('div', {
|
||||
id: `plyr-settings-${data.id}-${type}`,
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
// Back button
|
||||
const backButton = createElement('button', {
|
||||
type: 'button',
|
||||
class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,
|
||||
});
|
||||
|
||||
// Visible label
|
||||
backButton.appendChild(
|
||||
createElement(
|
||||
'span',
|
||||
{
|
||||
'aria-hidden': true,
|
||||
},
|
||||
i18n.get(type, this.config),
|
||||
),
|
||||
);
|
||||
|
||||
// Screen reader label
|
||||
backButton.appendChild(
|
||||
createElement(
|
||||
'span',
|
||||
{
|
||||
class: this.config.classNames.hidden,
|
||||
},
|
||||
i18n.get('menuBack', this.config),
|
||||
),
|
||||
);
|
||||
|
||||
// Go back via keyboard
|
||||
on(
|
||||
pane,
|
||||
'keydown',
|
||||
event => {
|
||||
// We only care about <-
|
||||
if (event.which !== 37) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent seek
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Show the respective menu
|
||||
controls.showMenuPanel.call(this, 'home', true);
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
// Go back via button click
|
||||
on(backButton, 'click', () => {
|
||||
controls.showMenuPanel.call(this, 'home', false);
|
||||
});
|
||||
|
||||
// Add to pane
|
||||
pane.appendChild(backButton);
|
||||
|
||||
// Menu
|
||||
pane.appendChild(
|
||||
createElement('div', {
|
||||
role: 'menu',
|
||||
control.appendChild(
|
||||
createButton.call(this, 'settings', {
|
||||
'aria-haspopup': true,
|
||||
'aria-controls': `plyr-settings-${data.id}`,
|
||||
'aria-expanded': false,
|
||||
}),
|
||||
);
|
||||
|
||||
inner.appendChild(pane);
|
||||
|
||||
this.elements.settings.buttons[type] = menuItem;
|
||||
this.elements.settings.panels[type] = pane;
|
||||
});
|
||||
|
||||
popup.appendChild(inner);
|
||||
control.appendChild(popup);
|
||||
container.appendChild(control);
|
||||
|
||||
this.elements.settings.popup = popup;
|
||||
this.elements.settings.menu = control;
|
||||
}
|
||||
|
||||
// Picture in picture button
|
||||
if (this.config.controls.includes('pip') && support.pip) {
|
||||
container.appendChild(controls.createButton.call(this, 'pip'));
|
||||
}
|
||||
|
||||
// Airplay button
|
||||
if (this.config.controls.includes('airplay') && support.airplay) {
|
||||
container.appendChild(controls.createButton.call(this, 'airplay'));
|
||||
}
|
||||
|
||||
// Download button
|
||||
if (this.config.controls.includes('download')) {
|
||||
const attributes = {
|
||||
element: 'a',
|
||||
href: this.download,
|
||||
target: '_blank',
|
||||
};
|
||||
|
||||
const { download } = this.config.urls;
|
||||
|
||||
if (!is.url(download) && this.isEmbed) {
|
||||
extend(attributes, {
|
||||
icon: `logo-${this.provider}`,
|
||||
label: this.provider,
|
||||
const popup = createElement('div', {
|
||||
class: 'plyr__menu__container',
|
||||
id: `plyr-settings-${data.id}`,
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
const inner = createElement('div');
|
||||
|
||||
const home = createElement('div', {
|
||||
id: `plyr-settings-${data.id}-home`,
|
||||
});
|
||||
|
||||
// Create the menu
|
||||
const menu = createElement('div', {
|
||||
role: 'menu',
|
||||
});
|
||||
|
||||
home.appendChild(menu);
|
||||
inner.appendChild(home);
|
||||
this.elements.settings.panels.home = home;
|
||||
|
||||
// Build the menu items
|
||||
this.config.settings.forEach(type => {
|
||||
// TODO: bundle this with the createMenuItem helper and bindings
|
||||
const menuItem = createElement(
|
||||
'button',
|
||||
extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
|
||||
type: 'button',
|
||||
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
|
||||
role: 'menuitem',
|
||||
'aria-haspopup': true,
|
||||
hidden: '',
|
||||
}),
|
||||
);
|
||||
|
||||
// Bind menu shortcuts for keyboard users
|
||||
bindMenuItemShortcuts.call(this, menuItem, type);
|
||||
|
||||
// Show menu on click
|
||||
on(menuItem, 'click', () => {
|
||||
showMenuPanel.call(this, type, false);
|
||||
});
|
||||
|
||||
const flex = createElement('span', null, i18n.get(type, this.config));
|
||||
|
||||
const value = createElement('span', {
|
||||
class: this.config.classNames.menu.value,
|
||||
});
|
||||
|
||||
// Speed contains HTML entities
|
||||
value.innerHTML = data[type];
|
||||
|
||||
flex.appendChild(value);
|
||||
menuItem.appendChild(flex);
|
||||
menu.appendChild(menuItem);
|
||||
|
||||
// Build the panes
|
||||
const pane = createElement('div', {
|
||||
id: `plyr-settings-${data.id}-${type}`,
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
// Back button
|
||||
const backButton = createElement('button', {
|
||||
type: 'button',
|
||||
class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,
|
||||
});
|
||||
|
||||
// Visible label
|
||||
backButton.appendChild(
|
||||
createElement(
|
||||
'span',
|
||||
{
|
||||
'aria-hidden': true,
|
||||
},
|
||||
i18n.get(type, this.config),
|
||||
),
|
||||
);
|
||||
|
||||
// Screen reader label
|
||||
backButton.appendChild(
|
||||
createElement(
|
||||
'span',
|
||||
{
|
||||
class: this.config.classNames.hidden,
|
||||
},
|
||||
i18n.get('menuBack', this.config),
|
||||
),
|
||||
);
|
||||
|
||||
// Go back via keyboard
|
||||
on(
|
||||
pane,
|
||||
'keydown',
|
||||
event => {
|
||||
// We only care about <-
|
||||
if (event.which !== 37) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent seek
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Show the respective menu
|
||||
showMenuPanel.call(this, 'home', true);
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
// Go back via button click
|
||||
on(backButton, 'click', () => {
|
||||
showMenuPanel.call(this, 'home', false);
|
||||
});
|
||||
|
||||
// Add to pane
|
||||
pane.appendChild(backButton);
|
||||
|
||||
// Menu
|
||||
pane.appendChild(
|
||||
createElement('div', {
|
||||
role: 'menu',
|
||||
}),
|
||||
);
|
||||
|
||||
inner.appendChild(pane);
|
||||
|
||||
this.elements.settings.buttons[type] = menuItem;
|
||||
this.elements.settings.panels[type] = pane;
|
||||
});
|
||||
|
||||
popup.appendChild(inner);
|
||||
control.appendChild(popup);
|
||||
container.appendChild(control);
|
||||
|
||||
this.elements.settings.popup = popup;
|
||||
this.elements.settings.menu = control;
|
||||
}
|
||||
|
||||
container.appendChild(controls.createButton.call(this, 'download', attributes));
|
||||
}
|
||||
// Picture in picture button
|
||||
if (control === 'pip' && support.pip) {
|
||||
container.appendChild(createButton.call(this, 'pip', defaultAttributes));
|
||||
}
|
||||
|
||||
// Toggle fullscreen button
|
||||
if (this.config.controls.includes('fullscreen')) {
|
||||
container.appendChild(controls.createButton.call(this, 'fullscreen'));
|
||||
}
|
||||
// Airplay button
|
||||
if (control === 'airplay' && support.airplay) {
|
||||
container.appendChild(createButton.call(this, 'airplay', defaultAttributes));
|
||||
}
|
||||
|
||||
// Larger overlaid play button
|
||||
if (this.config.controls.includes('play-large')) {
|
||||
this.elements.container.appendChild(controls.createButton.call(this, 'play-large'));
|
||||
}
|
||||
// Download button
|
||||
if (control === 'download') {
|
||||
const attributes = extend({}, defaultAttributes, {
|
||||
element: 'a',
|
||||
href: this.download,
|
||||
target: '_blank',
|
||||
});
|
||||
|
||||
this.elements.controls = container;
|
||||
const { download } = this.config.urls;
|
||||
|
||||
if (!is.url(download) && this.isEmbed) {
|
||||
extend(attributes, {
|
||||
icon: `logo-${this.provider}`,
|
||||
label: this.provider,
|
||||
});
|
||||
}
|
||||
|
||||
container.appendChild(createButton.call(this, 'download', attributes));
|
||||
}
|
||||
|
||||
// Toggle fullscreen button
|
||||
if (control === 'fullscreen') {
|
||||
container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));
|
||||
}
|
||||
});
|
||||
|
||||
// Set available quality levels
|
||||
if (this.isHTML5) {
|
||||
controls.setQualityMenu.call(this, html5.getQualityOptions.call(this));
|
||||
setQualityMenu.call(this, html5.getQualityOptions.call(this));
|
||||
}
|
||||
|
||||
controls.setSpeedMenu.call(this);
|
||||
setSpeedMenu.call(this);
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import support from './support';
|
||||
import { removeElement } from './utils/elements';
|
||||
import { triggerEvent } from './utils/events';
|
||||
import is from './utils/is';
|
||||
import { setAspectRatio } from './utils/style';
|
||||
|
||||
const html5 = {
|
||||
getSources() {
|
||||
@@ -43,6 +44,9 @@ const html5 = {
|
||||
|
||||
const player = this;
|
||||
|
||||
// Set aspect ratio if set
|
||||
setAspectRatio.call(player);
|
||||
|
||||
// Quality
|
||||
Object.defineProperty(player.media, 'quality', {
|
||||
get() {
|
||||
|
||||
+5
-5
@@ -9,7 +9,7 @@ import browser from './utils/browser';
|
||||
import { getElement, getElements, matches, toggleClass, toggleHidden } from './utils/elements';
|
||||
import { off, on, once, toggleListener, triggerEvent } from './utils/events';
|
||||
import is from './utils/is';
|
||||
import { setAspectRatio } from './utils/style';
|
||||
import { getAspectRatio, setAspectRatio } from './utils/style';
|
||||
|
||||
class Listeners {
|
||||
constructor(player) {
|
||||
@@ -317,10 +317,10 @@ class Listeners {
|
||||
}
|
||||
|
||||
const target = player.elements.wrapper.firstChild;
|
||||
const [, height] = ratio.split(':').map(Number);
|
||||
const [videoWidth, videoHeight] = player.embed.ratio.split(':').map(Number);
|
||||
const [, y] = ratio;
|
||||
const [videoX, videoY] = getAspectRatio.call(player);
|
||||
|
||||
target.style.maxWidth = toggle ? `${(height / videoHeight) * videoWidth}px` : null;
|
||||
target.style.maxWidth = toggle ? `${(y / videoY) * videoX}px` : null;
|
||||
target.style.margin = toggle ? '0 auto' : null;
|
||||
};
|
||||
|
||||
@@ -486,7 +486,7 @@ class Listeners {
|
||||
|
||||
// Update download link when ready and if quality changes
|
||||
on.call(player, player.media, 'ready qualitychange', () => {
|
||||
controls.setDownloadLink.call(player);
|
||||
controls.setDownloadUrl.call(player);
|
||||
});
|
||||
|
||||
// Proxy events to container
|
||||
|
||||
+58
-43
@@ -14,10 +14,24 @@ import loadScript from '../utils/loadScript';
|
||||
import { formatTime } from '../utils/time';
|
||||
import { buildUrlParams } from '../utils/urls';
|
||||
|
||||
const destroy = instance => {
|
||||
// Destroy our adsManager
|
||||
if (instance.manager) {
|
||||
instance.manager.destroy();
|
||||
}
|
||||
|
||||
// Destroy our adsManager
|
||||
if (instance.elements.displayContainer) {
|
||||
instance.elements.displayContainer.destroy();
|
||||
}
|
||||
|
||||
instance.elements.container.remove();
|
||||
};
|
||||
|
||||
class Ads {
|
||||
/**
|
||||
* Ads constructor.
|
||||
* @param {object} player
|
||||
* @param {Object} player
|
||||
* @return {Ads}
|
||||
*/
|
||||
constructor(player) {
|
||||
@@ -63,20 +77,22 @@ class Ads {
|
||||
* Load the IMA SDK
|
||||
*/
|
||||
load() {
|
||||
if (this.enabled) {
|
||||
// Check if the Google IMA3 SDK is loaded or load it ourselves
|
||||
if (!is.object(window.google) || !is.object(window.google.ima)) {
|
||||
loadScript(this.player.config.urls.googleIMA.sdk)
|
||||
.then(() => {
|
||||
this.ready();
|
||||
})
|
||||
.catch(() => {
|
||||
// Script failed to load or is blocked
|
||||
this.trigger('error', new Error('Google IMA SDK failed to load'));
|
||||
});
|
||||
} else {
|
||||
this.ready();
|
||||
}
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the Google IMA3 SDK is loaded or load it ourselves
|
||||
if (!is.object(window.google) || !is.object(window.google.ima)) {
|
||||
loadScript(this.player.config.urls.googleIMA.sdk)
|
||||
.then(() => {
|
||||
this.ready();
|
||||
})
|
||||
.catch(() => {
|
||||
// Script failed to load or is blocked
|
||||
this.trigger('error', new Error('Google IMA SDK failed to load'));
|
||||
});
|
||||
} else {
|
||||
this.ready();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +100,11 @@ class Ads {
|
||||
* Get the ads instance ready
|
||||
*/
|
||||
ready() {
|
||||
// Double check we're enabled
|
||||
if (!this.enabled) {
|
||||
destroy(this);
|
||||
}
|
||||
|
||||
// Start ticking our safety timer. If the whole advertisement
|
||||
// thing doesn't resolve within our set time; we bail
|
||||
this.startSafetyTimer(12000, 'ready()');
|
||||
@@ -198,7 +219,7 @@ class Ads {
|
||||
|
||||
/**
|
||||
* Update the ad countdown
|
||||
* @param {boolean} start
|
||||
* @param {Boolean} start
|
||||
*/
|
||||
pollCountdown(start = false) {
|
||||
if (!start) {
|
||||
@@ -240,9 +261,6 @@ class Ads {
|
||||
// Get the cue points for any mid-rolls by filtering out the pre- and post-roll
|
||||
this.cuePoints = this.manager.getCuePoints();
|
||||
|
||||
// Set volume to match player
|
||||
this.manager.setVolume(this.player.volume);
|
||||
|
||||
// Add listeners to the required events
|
||||
// Advertisement error events
|
||||
this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));
|
||||
@@ -297,15 +315,15 @@ class Ads {
|
||||
triggerEvent.call(this.player, this.player.media, event);
|
||||
};
|
||||
|
||||
// Bubble the event
|
||||
dispatchEvent(event.type);
|
||||
|
||||
switch (event.type) {
|
||||
case google.ima.AdEvent.Type.LOADED:
|
||||
// This is the first event sent for an ad - it is possible to determine whether the
|
||||
// ad is a video ad or an overlay
|
||||
this.trigger('loaded');
|
||||
|
||||
// Bubble event
|
||||
dispatchEvent(event.type);
|
||||
|
||||
// Start countdown
|
||||
this.pollCountdown(true);
|
||||
|
||||
@@ -317,15 +335,19 @@ class Ads {
|
||||
|
||||
// console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());
|
||||
// console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());
|
||||
|
||||
break;
|
||||
|
||||
case google.ima.AdEvent.Type.STARTED:
|
||||
// Set volume to match player
|
||||
this.manager.setVolume(this.player.volume);
|
||||
|
||||
break;
|
||||
|
||||
case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
|
||||
// All ads for the current videos are done. We can now request new advertisements
|
||||
// in case the video is re-played
|
||||
|
||||
// Fire event
|
||||
dispatchEvent(event.type);
|
||||
|
||||
// TODO: Example for what happens when a next video in a playlist would be loaded.
|
||||
// So here we load a new video when all ads are done.
|
||||
// Then we load new ads within a new adsManager. When the video
|
||||
@@ -350,6 +372,7 @@ class Ads {
|
||||
// playing when the IMA SDK is ready or has failed
|
||||
|
||||
this.loadAds();
|
||||
|
||||
break;
|
||||
|
||||
case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
|
||||
@@ -357,8 +380,6 @@ class Ads {
|
||||
// for example display a pause button and remaining time. Fired when content should
|
||||
// be paused. This usually happens right before an ad is about to cover the content
|
||||
|
||||
dispatchEvent(event.type);
|
||||
|
||||
this.pauseContent();
|
||||
|
||||
break;
|
||||
@@ -369,26 +390,17 @@ class Ads {
|
||||
// Fired when content should be resumed. This usually happens when an ad finishes
|
||||
// or collapses
|
||||
|
||||
dispatchEvent(event.type);
|
||||
|
||||
this.pollCountdown();
|
||||
|
||||
this.resumeContent();
|
||||
|
||||
break;
|
||||
|
||||
case google.ima.AdEvent.Type.STARTED:
|
||||
case google.ima.AdEvent.Type.MIDPOINT:
|
||||
case google.ima.AdEvent.Type.COMPLETE:
|
||||
case google.ima.AdEvent.Type.IMPRESSION:
|
||||
case google.ima.AdEvent.Type.CLICK:
|
||||
dispatchEvent(event.type);
|
||||
break;
|
||||
|
||||
case google.ima.AdEvent.Type.LOG:
|
||||
if (adData.adError) {
|
||||
this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -463,6 +475,9 @@ class Ads {
|
||||
// Play the requested advertisement whenever the adsManager is ready
|
||||
this.managerPromise
|
||||
.then(() => {
|
||||
// Set volume to match player
|
||||
this.manager.setVolume(this.player.volume);
|
||||
|
||||
// Initialize the container. Must be done via a user action on mobile devices
|
||||
this.elements.displayContainer.initialize();
|
||||
|
||||
@@ -559,7 +574,7 @@ class Ads {
|
||||
|
||||
/**
|
||||
* Handles callbacks after an ad event was invoked
|
||||
* @param {string} event - Event type
|
||||
* @param {String} event - Event type
|
||||
*/
|
||||
trigger(event, ...args) {
|
||||
const handlers = this.events[event];
|
||||
@@ -575,8 +590,8 @@ class Ads {
|
||||
|
||||
/**
|
||||
* Add event listeners
|
||||
* @param {string} event - Event type
|
||||
* @param {function} callback - Callback for when event occurs
|
||||
* @param {String} event - Event type
|
||||
* @param {Function} callback - Callback for when event occurs
|
||||
* @return {Ads}
|
||||
*/
|
||||
on(event, callback) {
|
||||
@@ -594,8 +609,8 @@ class Ads {
|
||||
* The advertisement has 12 seconds to get its things together. We stop this timer when the
|
||||
* advertisement is playing, or when a user action is required to start, then we clear the
|
||||
* timer on ad ready
|
||||
* @param {number} time
|
||||
* @param {string} from
|
||||
* @param {Number} time
|
||||
* @param {String} from
|
||||
*/
|
||||
startSafetyTimer(time, from) {
|
||||
this.player.debug.log(`Safety timer invoked from: ${from}`);
|
||||
@@ -608,7 +623,7 @@ class Ads {
|
||||
|
||||
/**
|
||||
* Clear our safety timer(s)
|
||||
* @param {string} from
|
||||
* @param {String} from
|
||||
*/
|
||||
clearSafetyTimer(from) {
|
||||
if (!is.nullOrUndefined(this.safetyTimer)) {
|
||||
|
||||
@@ -17,17 +17,17 @@ const parseVtt = vttDataString => {
|
||||
if (!is.number(result.startTime)) {
|
||||
// The line with start and end times on it is the first line of interest
|
||||
const matchTimes = line.match(
|
||||
/([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,
|
||||
/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,
|
||||
); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT
|
||||
|
||||
if (matchTimes) {
|
||||
result.startTime =
|
||||
Number(matchTimes[1]) * 60 * 60 +
|
||||
Number(matchTimes[1] || 0) * 60 * 60 +
|
||||
Number(matchTimes[2]) * 60 +
|
||||
Number(matchTimes[3]) +
|
||||
Number(`0.${matchTimes[4]}`);
|
||||
result.endTime =
|
||||
Number(matchTimes[6]) * 60 * 60 +
|
||||
Number(matchTimes[6] || 0) * 60 * 60 +
|
||||
Number(matchTimes[7]) * 60 +
|
||||
Number(matchTimes[8]) +
|
||||
Number(`0.${matchTimes[9]}`);
|
||||
@@ -148,7 +148,12 @@ class PreviewThumbnails {
|
||||
|
||||
// If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file
|
||||
// If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
|
||||
if (!thumbnail.frames[0].text.startsWith('/')) {
|
||||
// If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file
|
||||
if (
|
||||
!thumbnail.frames[0].text.startsWith('/') &&
|
||||
!thumbnail.frames[0].text.startsWith('http://') &&
|
||||
!thumbnail.frames[0].text.startsWith('https://')
|
||||
) {
|
||||
thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
@@ -220,6 +225,7 @@ class PreviewThumbnails {
|
||||
// Only act on left mouse button (0), or touch device (event.button is false)
|
||||
if (event.button === false || event.button === 0) {
|
||||
this.mouseDown = true;
|
||||
|
||||
// Wait until media has a duration
|
||||
if (this.player.media.duration) {
|
||||
this.toggleScrubbingContainer(true);
|
||||
@@ -293,7 +299,9 @@ class PreviewThumbnails {
|
||||
this.elements.thumb.container.appendChild(timeContainer);
|
||||
|
||||
// Inject the whole thumb
|
||||
this.player.elements.progress.appendChild(this.elements.thumb.container);
|
||||
if (is.element(this.player.elements.progress)) {
|
||||
this.player.elements.progress.appendChild(this.elements.thumb.container);
|
||||
}
|
||||
|
||||
// Create HTML element: plyr__preview-scrubbing-container
|
||||
this.elements.scrubbing.container = createElement('div', {
|
||||
@@ -307,7 +315,6 @@ class PreviewThumbnails {
|
||||
if (this.mouseDown) {
|
||||
this.setScrubbingContainerSize();
|
||||
} else {
|
||||
this.toggleThumbContainer(true);
|
||||
this.setThumbContainerSizeAndPos();
|
||||
}
|
||||
|
||||
@@ -319,7 +326,10 @@ class PreviewThumbnails {
|
||||
const hasThumb = thumbNum >= 0;
|
||||
let qualityIndex = 0;
|
||||
|
||||
this.toggleThumbContainer(hasThumb);
|
||||
// Show the thumb container if we're not scrubbing
|
||||
if (!this.mouseDown) {
|
||||
this.toggleThumbContainer(hasThumb);
|
||||
}
|
||||
|
||||
// No matching thumb found
|
||||
if (!hasThumb) {
|
||||
|
||||
@@ -48,14 +48,14 @@ const vimeo = {
|
||||
// Set intial ratio
|
||||
setAspectRatio.call(this);
|
||||
|
||||
// Load the API if not already
|
||||
// Load the SDK if not already
|
||||
if (!is.object(window.Vimeo)) {
|
||||
loadScript(this.config.urls.vimeo.sdk)
|
||||
.then(() => {
|
||||
vimeo.ready.call(this);
|
||||
})
|
||||
.catch(error => {
|
||||
this.debug.warn('Vimeo API failed to load', error);
|
||||
this.debug.warn('Vimeo SDK (player.js) failed to load', error);
|
||||
});
|
||||
} else {
|
||||
vimeo.ready.call(this);
|
||||
@@ -259,7 +259,7 @@ const vimeo = {
|
||||
.getVideoUrl()
|
||||
.then(value => {
|
||||
currentSrc = value;
|
||||
controls.setDownloadLink.call(player);
|
||||
controls.setDownloadUrl.call(player);
|
||||
})
|
||||
.catch(error => {
|
||||
this.debug.warn(error);
|
||||
@@ -281,8 +281,8 @@ const vimeo = {
|
||||
// Set aspect ratio based on video size
|
||||
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
|
||||
const [width, height] = dimensions;
|
||||
player.embed.ratio = `${width}:${height}`;
|
||||
setAspectRatio.call(this, player.embed.ratio);
|
||||
player.embed.ratio = [width, height];
|
||||
setAspectRatio.call(this);
|
||||
});
|
||||
|
||||
// Set autopause
|
||||
|
||||
+34
-30
@@ -34,14 +34,24 @@ function assurePlaybackState(play) {
|
||||
}
|
||||
}
|
||||
|
||||
function getHost(config) {
|
||||
if (config.noCookie) {
|
||||
return 'https://www.youtube-nocookie.com';
|
||||
}
|
||||
|
||||
if (window.location.protocol === 'http:') {
|
||||
return 'http://www.youtube.com';
|
||||
}
|
||||
|
||||
// Use YouTube's default
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const youtube = {
|
||||
setup() {
|
||||
// Add embed class for responsive
|
||||
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
|
||||
|
||||
// Set aspect ratio
|
||||
setAspectRatio.call(this);
|
||||
|
||||
// Setup API
|
||||
if (is.object(window.YT) && is.function(window.YT.Player)) {
|
||||
youtube.ready.call(this);
|
||||
@@ -71,33 +81,27 @@ const youtube = {
|
||||
|
||||
// Get the media title
|
||||
getTitle(videoId) {
|
||||
// Try via undocumented API method first
|
||||
// This method disappears now and then though...
|
||||
// https://github.com/sampotts/plyr/issues/709
|
||||
if (is.function(this.embed.getVideoData)) {
|
||||
const { title } = this.embed.getVideoData();
|
||||
const url = format(this.config.urls.youtube.api, videoId);
|
||||
|
||||
if (is.empty(title)) {
|
||||
this.config.title = title;
|
||||
ui.setTitle.call(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
fetch(url)
|
||||
.then(data => {
|
||||
if (is.object(data)) {
|
||||
const { title, height, width } = data;
|
||||
|
||||
// Or via Google API
|
||||
const key = this.config.keys.google;
|
||||
if (is.string(key) && !is.empty(key)) {
|
||||
const url = format(this.config.urls.youtube.api, videoId, key);
|
||||
// Set title
|
||||
this.config.title = title;
|
||||
ui.setTitle.call(this);
|
||||
|
||||
fetch(url)
|
||||
.then(result => {
|
||||
if (is.object(result)) {
|
||||
this.config.title = result.items[0].snippet.title;
|
||||
ui.setTitle.call(this);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
// Set aspect ratio
|
||||
this.embed.ratio = [width, height];
|
||||
}
|
||||
|
||||
setAspectRatio.call(this);
|
||||
})
|
||||
.catch(() => {
|
||||
// Set aspect ratio
|
||||
setAspectRatio.call(this);
|
||||
});
|
||||
},
|
||||
|
||||
// API ready
|
||||
@@ -130,7 +134,7 @@ const youtube = {
|
||||
player.media = replaceElement(container, player.media);
|
||||
|
||||
// Id to poster wrapper
|
||||
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
|
||||
const posterSrc = format => `https://i.ytimg.com/vi/${videoId}/${format}default.jpg`;
|
||||
|
||||
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
||||
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
|
||||
@@ -151,7 +155,7 @@ const youtube = {
|
||||
// https://developers.google.com/youtube/iframe_api_reference
|
||||
player.embed = new window.YT.Player(id, {
|
||||
videoId,
|
||||
host: config.noCookie ? 'https://www.youtube-nocookie.com' : undefined,
|
||||
host: getHost(config),
|
||||
playerVars: extend(
|
||||
{},
|
||||
{
|
||||
@@ -386,7 +390,7 @@ const youtube = {
|
||||
|
||||
case 1:
|
||||
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
|
||||
if (player.media.paused && !player.embed.hasPlayed) {
|
||||
if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
|
||||
player.media.pause();
|
||||
} else {
|
||||
assurePlaybackState.call(player, true);
|
||||
|
||||
+130
-52
@@ -1,6 +1,6 @@
|
||||
// ==========================================================================
|
||||
// Plyr
|
||||
// plyr.js v3.5.0
|
||||
// plyr.js v3.5.4
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
@@ -25,7 +25,9 @@ import { createElement, hasClass, removeElement, replaceElement, toggleClass, wr
|
||||
import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
|
||||
import is from './utils/is';
|
||||
import loadSprite from './utils/loadSprite';
|
||||
import { clamp } from './utils/numbers';
|
||||
import { cloneDeep, extend } from './utils/objects';
|
||||
import { getAspectRatio, reduceAspectRatio, setAspectRatio, validateRatio } from './utils/style';
|
||||
import { parseUrl } from './utils/urls';
|
||||
|
||||
// Private properties
|
||||
@@ -301,8 +303,8 @@ class Plyr {
|
||||
}
|
||||
|
||||
// Autoplay if required
|
||||
if (this.config.autoplay) {
|
||||
this.play();
|
||||
if (this.isHTML5 && this.config.autoplay) {
|
||||
setTimeout(() => this.play(), 10);
|
||||
}
|
||||
|
||||
// Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
|
||||
@@ -403,7 +405,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Toggle playback based on current status
|
||||
* @param {boolean} input
|
||||
* @param {Boolean} input
|
||||
*/
|
||||
togglePlay(input) {
|
||||
// Toggle based on current state if nothing passed
|
||||
@@ -437,7 +439,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Rewind
|
||||
* @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
|
||||
* @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
|
||||
*/
|
||||
rewind(seekTime) {
|
||||
this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime);
|
||||
@@ -445,7 +447,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Fast forward
|
||||
* @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
|
||||
* @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
|
||||
*/
|
||||
forward(seekTime) {
|
||||
this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime);
|
||||
@@ -453,7 +455,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Seek to a time
|
||||
* @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
|
||||
* @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)
|
||||
*/
|
||||
set currentTime(input) {
|
||||
// Bail if media duration isn't available yet
|
||||
@@ -523,7 +525,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Set the player volume
|
||||
* @param {number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage
|
||||
* @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage
|
||||
*/
|
||||
set volume(value) {
|
||||
let volume = value;
|
||||
@@ -574,7 +576,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Increase volume
|
||||
* @param {boolean} step - How much to decrease by (between 0 and 1)
|
||||
* @param {Boolean} step - How much to decrease by (between 0 and 1)
|
||||
*/
|
||||
increaseVolume(step) {
|
||||
const volume = this.media.muted ? 0 : this.volume;
|
||||
@@ -583,7 +585,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Decrease volume
|
||||
* @param {boolean} step - How much to decrease by (between 0 and 1)
|
||||
* @param {Boolean} step - How much to decrease by (between 0 and 1)
|
||||
*/
|
||||
decreaseVolume(step) {
|
||||
this.increaseVolume(-step);
|
||||
@@ -591,7 +593,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Set muted state
|
||||
* @param {boolean} mute
|
||||
* @param {Boolean} mute
|
||||
*/
|
||||
set muted(mute) {
|
||||
let toggle = mute;
|
||||
@@ -643,7 +645,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Set playback speed
|
||||
* @param {number} speed - the speed of playback (0.5-2.0)
|
||||
* @param {Number} speed - the speed of playback (0.5-2.0)
|
||||
*/
|
||||
set speed(input) {
|
||||
let speed = null;
|
||||
@@ -660,24 +662,17 @@ class Plyr {
|
||||
speed = this.config.speed.selected;
|
||||
}
|
||||
|
||||
// Set min/max
|
||||
if (speed < 0.1) {
|
||||
speed = 0.1;
|
||||
}
|
||||
if (speed > 2.0) {
|
||||
speed = 2.0;
|
||||
}
|
||||
|
||||
if (!this.config.speed.options.includes(speed)) {
|
||||
this.debug.warn(`Unsupported speed (${speed})`);
|
||||
return;
|
||||
}
|
||||
// Clamp to min/max
|
||||
const { minimumSpeed: min, maximumSpeed: max } = this;
|
||||
speed = clamp(speed, min, max);
|
||||
|
||||
// Update config
|
||||
this.config.speed.selected = speed;
|
||||
|
||||
// Set media speed
|
||||
this.media.playbackRate = speed;
|
||||
setTimeout(() => {
|
||||
this.media.playbackRate = speed;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -687,10 +682,46 @@ class Plyr {
|
||||
return Number(this.media.playbackRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimum allowed speed
|
||||
*/
|
||||
get minimumSpeed() {
|
||||
if (this.isYouTube) {
|
||||
// https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
|
||||
return Math.min(...this.options.speed);
|
||||
}
|
||||
|
||||
if (this.isVimeo) {
|
||||
// https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/32320020/1191319
|
||||
return 0.0625;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum allowed speed
|
||||
*/
|
||||
get maximumSpeed() {
|
||||
if (this.isYouTube) {
|
||||
// https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
|
||||
return Math.max(...this.options.speed);
|
||||
}
|
||||
|
||||
if (this.isVimeo) {
|
||||
// https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
|
||||
return 2;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/32320020/1191319
|
||||
return 16;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set playback quality
|
||||
* Currently HTML5 & YouTube only
|
||||
* @param {number} input - Quality level
|
||||
* @param {Number} input - Quality level
|
||||
*/
|
||||
set quality(input) {
|
||||
const config = this.config.quality;
|
||||
@@ -740,7 +771,7 @@ class Plyr {
|
||||
/**
|
||||
* Toggle loop
|
||||
* TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config
|
||||
* @param {boolean} input - Whether to loop or not
|
||||
* @param {Boolean} input - Whether to loop or not
|
||||
*/
|
||||
set loop(input) {
|
||||
const toggle = is.boolean(input) ? input : this.config.loop.active;
|
||||
@@ -800,7 +831,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Set new media source
|
||||
* @param {object} input - The new source object (see docs)
|
||||
* @param {Object} input - The new source object (see docs)
|
||||
*/
|
||||
set source(input) {
|
||||
source.change.call(this, input);
|
||||
@@ -822,9 +853,22 @@ class Plyr {
|
||||
return is.url(download) ? download : this.source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the download URL
|
||||
*/
|
||||
set download(input) {
|
||||
if (!is.url(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.urls.download = input;
|
||||
|
||||
controls.setDownloadUrl.call(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the poster image for a video
|
||||
* @param {input} - the URL for the new poster image
|
||||
* @param {String} input - the URL for the new poster image
|
||||
*/
|
||||
set poster(input) {
|
||||
if (!this.isVideo) {
|
||||
@@ -846,9 +890,41 @@ class Plyr {
|
||||
return this.media.getAttribute('poster');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current aspect ratio in use
|
||||
*/
|
||||
get ratio() {
|
||||
if (!this.isVideo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ratio = reduceAspectRatio(getAspectRatio.call(this));
|
||||
|
||||
return is.array(ratio) ? ratio.join(':') : ratio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set video aspect ratio
|
||||
*/
|
||||
set ratio(input) {
|
||||
if (!this.isVideo) {
|
||||
this.debug.warn('Aspect ratio can only be set for video');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is.string(input) || !validateRatio(input)) {
|
||||
this.debug.error(`Invalid aspect ratio specified (${input})`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.ratio = input;
|
||||
|
||||
setAspectRatio.call(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the autoplay state
|
||||
* @param {boolean} input - Whether to autoplay or not
|
||||
* @param {Boolean} input - Whether to autoplay or not
|
||||
*/
|
||||
set autoplay(input) {
|
||||
const toggle = is.boolean(input) ? input : this.config.autoplay;
|
||||
@@ -864,7 +940,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Toggle captions
|
||||
* @param {boolean} input - Whether to enable captions
|
||||
* @param {Boolean} input - Whether to enable captions
|
||||
*/
|
||||
toggleCaptions(input) {
|
||||
captions.toggle.call(this, input, false);
|
||||
@@ -872,7 +948,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Set the caption track by index
|
||||
* @param {number} - Caption index
|
||||
* @param {Number} - Caption index
|
||||
*/
|
||||
set currentTrack(input) {
|
||||
captions.set.call(this, input, false);
|
||||
@@ -889,7 +965,7 @@ class Plyr {
|
||||
/**
|
||||
* Set the wanted language for captions
|
||||
* Since tracks can be added later it won't update the actual caption track until there is a matching track
|
||||
* @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
|
||||
* @param {String} - Two character ISO language code (e.g. EN, FR, PT, etc)
|
||||
*/
|
||||
set language(input) {
|
||||
captions.setLanguage.call(this, input, false);
|
||||
@@ -962,7 +1038,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Toggle the player controls
|
||||
* @param {boolean} [toggle] - Whether to show the controls
|
||||
* @param {Boolean} [toggle] - Whether to show the controls
|
||||
*/
|
||||
toggleControls(toggle) {
|
||||
// Don't toggle if missing UI support or if it's audio
|
||||
@@ -995,8 +1071,8 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Add event listeners
|
||||
* @param {string} event - Event type
|
||||
* @param {function} callback - Callback for when event occurs
|
||||
* @param {String} event - Event type
|
||||
* @param {Function} callback - Callback for when event occurs
|
||||
*/
|
||||
on(event, callback) {
|
||||
on.call(this, this.elements.container, event, callback);
|
||||
@@ -1004,8 +1080,8 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Add event listeners once
|
||||
* @param {string} event - Event type
|
||||
* @param {function} callback - Callback for when event occurs
|
||||
* @param {String} event - Event type
|
||||
* @param {Function} callback - Callback for when event occurs
|
||||
*/
|
||||
once(event, callback) {
|
||||
once.call(this, this.elements.container, event, callback);
|
||||
@@ -1013,8 +1089,8 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Remove event listeners
|
||||
* @param {string} event - Event type
|
||||
* @param {function} callback - Callback for when event occurs
|
||||
* @param {String} event - Event type
|
||||
* @param {Function} callback - Callback for when event occurs
|
||||
*/
|
||||
off(event, callback) {
|
||||
off(this.elements.container, event, callback);
|
||||
@@ -1024,8 +1100,8 @@ class Plyr {
|
||||
* Destroy an instance
|
||||
* Event listeners are removed when elements are removed
|
||||
* http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory
|
||||
* @param {function} callback - Callback for when destroy is complete
|
||||
* @param {boolean} soft - Whether it's a soft destroy (for source changes etc)
|
||||
* @param {Function} callback - Callback for when destroy is complete
|
||||
* @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)
|
||||
*/
|
||||
destroy(callback, soft = false) {
|
||||
if (!this.ready) {
|
||||
@@ -1088,11 +1164,13 @@ class Plyr {
|
||||
// Stop playback
|
||||
this.stop();
|
||||
|
||||
// Clear timeouts
|
||||
clearTimeout(this.timers.loading);
|
||||
clearTimeout(this.timers.controls);
|
||||
clearTimeout(this.timers.resized);
|
||||
|
||||
// Provider specific stuff
|
||||
if (this.isHTML5) {
|
||||
// Clear timeout
|
||||
clearTimeout(this.timers.loading);
|
||||
|
||||
// Restore native video controls
|
||||
ui.toggleNativeControls.call(this, true);
|
||||
|
||||
@@ -1124,7 +1202,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Check for support for a mime type (HTML5 only)
|
||||
* @param {string} type - Mime type
|
||||
* @param {String} type - Mime type
|
||||
*/
|
||||
supports(type) {
|
||||
return support.mime.call(this, type);
|
||||
@@ -1132,9 +1210,9 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Check for support
|
||||
* @param {string} type - Player type (audio/video)
|
||||
* @param {string} provider - Provider (html5/youtube/vimeo)
|
||||
* @param {bool} inline - Where player has `playsinline` sttribute
|
||||
* @param {String} type - Player type (audio/video)
|
||||
* @param {String} provider - Provider (html5/youtube/vimeo)
|
||||
* @param {Boolean} inline - Where player has `playsinline` sttribute
|
||||
*/
|
||||
static supported(type, provider, inline) {
|
||||
return support.check(type, provider, inline);
|
||||
@@ -1142,8 +1220,8 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Load an SVG sprite into the page
|
||||
* @param {string} url - URL for the SVG sprite
|
||||
* @param {string} [id] - Unique ID
|
||||
* @param {String} url - URL for the SVG sprite
|
||||
* @param {String} [id] - Unique ID
|
||||
*/
|
||||
static loadSprite(url, id) {
|
||||
return loadSprite(url, id);
|
||||
@@ -1152,7 +1230,7 @@ class Plyr {
|
||||
/**
|
||||
* Setup multiple instances
|
||||
* @param {*} selector
|
||||
* @param {object} options
|
||||
* @param {Object} options
|
||||
*/
|
||||
static setup(selector, options = {}) {
|
||||
let targets = null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// ==========================================================================
|
||||
// Plyr Polyfilled Build
|
||||
// plyr.js v3.5.0
|
||||
// plyr.js v3.5.4
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
|
||||
+16
-11
@@ -67,15 +67,15 @@ const ui = {
|
||||
// Reset mute state
|
||||
this.muted = null;
|
||||
|
||||
// Reset speed
|
||||
this.speed = null;
|
||||
|
||||
// Reset loop state
|
||||
this.loop = null;
|
||||
|
||||
// Reset quality setting
|
||||
this.quality = null;
|
||||
|
||||
// Reset speed
|
||||
this.speed = null;
|
||||
|
||||
// Reset volume display
|
||||
controls.updateVolume.call(this);
|
||||
|
||||
@@ -233,13 +233,16 @@ const ui = {
|
||||
clearTimeout(this.timers.loading);
|
||||
|
||||
// Timer to prevent flicker when seeking
|
||||
this.timers.loading = setTimeout(() => {
|
||||
// Update progress bar loading class state
|
||||
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||
this.timers.loading = setTimeout(
|
||||
() => {
|
||||
// Update progress bar loading class state
|
||||
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||
|
||||
// Update controls visibility
|
||||
ui.toggleControls.call(this);
|
||||
}, this.loading ? 250 : 0);
|
||||
// Update controls visibility
|
||||
ui.toggleControls.call(this);
|
||||
},
|
||||
this.loading ? 250 : 0,
|
||||
);
|
||||
},
|
||||
|
||||
// Toggle controls based on state and `force` argument
|
||||
@@ -248,10 +251,12 @@ const ui = {
|
||||
|
||||
if (controls && this.config.hideControls) {
|
||||
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
|
||||
const recentTouchSeek = (this.touch && this.lastSeekTime + 2000 > Date.now());
|
||||
const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();
|
||||
|
||||
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
|
||||
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover || recentTouchSeek));
|
||||
this.toggleControls(
|
||||
Boolean(force || this.loading || this.paused || controls.pressed || controls.hover || recentTouchSeek),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { toggleListener } from './events';
|
||||
import is from './is';
|
||||
import { extend } from './objects';
|
||||
|
||||
// Wrap an element
|
||||
export function wrap(elements, wrapper) {
|
||||
@@ -137,7 +138,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
|
||||
}
|
||||
|
||||
const attributes = {};
|
||||
const existing = existingAttributes;
|
||||
const existing = extend({}, existingAttributes);
|
||||
|
||||
sel.split(',').forEach(s => {
|
||||
// Remove whitespace
|
||||
@@ -147,7 +148,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
|
||||
|
||||
// Get the parts and value
|
||||
const parts = stripped.split('=');
|
||||
const key = parts[0];
|
||||
const [key] = parts;
|
||||
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
|
||||
|
||||
// Get the first character
|
||||
@@ -156,11 +157,11 @@ export function getAttributesFromSelector(sel, existingAttributes) {
|
||||
switch (start) {
|
||||
case '.':
|
||||
// Add to existing classname
|
||||
if (is.object(existing) && is.string(existing.class)) {
|
||||
existing.class += ` ${className}`;
|
||||
if (is.string(existing.class)) {
|
||||
attributes.class = `${existing.class} ${className}`;
|
||||
} else {
|
||||
attributes.class = className;
|
||||
}
|
||||
|
||||
attributes.class = className;
|
||||
break;
|
||||
|
||||
case '#':
|
||||
@@ -179,7 +180,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
|
||||
}
|
||||
});
|
||||
|
||||
return attributes;
|
||||
return extend(existing, attributes);
|
||||
}
|
||||
|
||||
// Toggle hidden
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Returns a number whose value is limited to the given range.
|
||||
*
|
||||
* Example: limit the output of this computation to between 0 and 255
|
||||
* (x * 255).clamp(0, 255)
|
||||
*
|
||||
* @param {Number} input
|
||||
* @param {Number} min The lower boundary of the output range
|
||||
* @param {Number} max The upper boundary of the output range
|
||||
* @returns A number in the range [min, max]
|
||||
* @type Number
|
||||
*/
|
||||
export function clamp(input = 0, min = 0, max = 255) {
|
||||
return Math.min(Math.max(input, min), max);
|
||||
}
|
||||
|
||||
export default { clamp };
|
||||
+58
-13
@@ -4,26 +4,69 @@
|
||||
|
||||
import is from './is';
|
||||
|
||||
/* function reduceAspectRatio(width, height) {
|
||||
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
|
||||
const ratio = getRatio(width, height);
|
||||
return `${width / ratio}:${height / ratio}`;
|
||||
} */
|
||||
export function validateRatio(input) {
|
||||
if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set aspect ratio for responsive container
|
||||
export function setAspectRatio(input) {
|
||||
let ratio = input;
|
||||
const ratio = is.array(input) ? input : input.split(':');
|
||||
|
||||
if (!is.string(ratio) && !is.nullOrUndefined(this.embed)) {
|
||||
return ratio.map(Number).every(is.number);
|
||||
}
|
||||
|
||||
export function reduceAspectRatio(ratio) {
|
||||
if (!is.array(ratio) || !ratio.every(is.number)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [width, height] = ratio;
|
||||
const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h));
|
||||
const divider = getDivider(width, height);
|
||||
|
||||
return [width / divider, height / divider];
|
||||
}
|
||||
|
||||
export function getAspectRatio(input) {
|
||||
const parse = ratio => {
|
||||
if (!validateRatio(ratio)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ratio.split(':').map(Number);
|
||||
};
|
||||
|
||||
// Provided ratio
|
||||
let ratio = parse(input);
|
||||
|
||||
// Get from config
|
||||
if (ratio === null) {
|
||||
ratio = parse(this.config.ratio);
|
||||
}
|
||||
|
||||
// Get from embed
|
||||
if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {
|
||||
({ ratio } = this.embed);
|
||||
}
|
||||
|
||||
if (!is.string(ratio)) {
|
||||
({ ratio } = this.config);
|
||||
// Get from HTML5 video
|
||||
if (ratio === null && this.isHTML5) {
|
||||
const { videoWidth, videoHeight } = this.media;
|
||||
ratio = reduceAspectRatio([videoWidth, videoHeight]);
|
||||
}
|
||||
|
||||
const [x, y] = ratio.split(':').map(Number);
|
||||
const padding = (100 / x) * y;
|
||||
return ratio;
|
||||
}
|
||||
|
||||
// Set aspect ratio for responsive container
|
||||
export function setAspectRatio(input) {
|
||||
if (!this.isVideo) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const ratio = getAspectRatio.call(this, input);
|
||||
|
||||
const [w, h] = is.array(ratio) ? ratio : [0, 0];
|
||||
const padding = (100 / w) * h;
|
||||
|
||||
this.elements.wrapper.style.paddingBottom = `${padding}%`;
|
||||
|
||||
@@ -32,6 +75,8 @@ export function setAspectRatio(input) {
|
||||
const height = 240;
|
||||
const offset = (height - padding) / (height / 50);
|
||||
this.media.style.transform = `translateY(-${offset}%)`;
|
||||
} else if (this.isHTML5) {
|
||||
this.elements.wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
|
||||
}
|
||||
|
||||
return { padding, ratio };
|
||||
|
||||
@@ -6,8 +6,8 @@ import is from './is';
|
||||
|
||||
/**
|
||||
* Parse a string to a URL object
|
||||
* @param {string} input - the URL to be parsed
|
||||
* @param {boolean} safe - failsafe parsing
|
||||
* @param {String} input - the URL to be parsed
|
||||
* @param {Boolean} safe - failsafe parsing
|
||||
*/
|
||||
export function parseUrl(input, safe = true) {
|
||||
let url = input;
|
||||
|
||||
@@ -14,42 +14,46 @@
|
||||
justify-content: flex-end;
|
||||
text-align: center;
|
||||
|
||||
.plyr__progress__container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// Spacing
|
||||
> .plyr__control,
|
||||
.plyr__progress,
|
||||
.plyr__time,
|
||||
.plyr__menu,
|
||||
.plyr__volume {
|
||||
margin-left: ($plyr-control-spacing / 2);
|
||||
}
|
||||
.plyr__controls__item {
|
||||
margin-left: ($plyr-control-spacing / 4);
|
||||
|
||||
.plyr__menu + .plyr__control,
|
||||
> .plyr__control + .plyr__menu,
|
||||
> .plyr__control + .plyr__control,
|
||||
.plyr__progress + .plyr__control {
|
||||
margin-left: floor($plyr-control-spacing / 4);
|
||||
}
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
> .plyr__control:first-child,
|
||||
> .plyr__control:first-child + [data-plyr='pause'] {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
&.plyr__progress__container {
|
||||
padding-left: ($plyr-control-spacing / 4);
|
||||
}
|
||||
|
||||
&.plyr__time {
|
||||
padding: 0 ($plyr-control-spacing / 2);
|
||||
}
|
||||
|
||||
&.plyr__progress__container:first-child,
|
||||
&.plyr__time:first-child,
|
||||
&.plyr__time + .plyr__time {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&.plyr__volume {
|
||||
padding-right: ($plyr-control-spacing / 2);
|
||||
}
|
||||
|
||||
&.plyr__volume:first-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide empty controls
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: $plyr-bp-sm) {
|
||||
> .plyr__control,
|
||||
.plyr__menu,
|
||||
.plyr__progress,
|
||||
.plyr__time,
|
||||
.plyr__volume {
|
||||
margin-left: $plyr-control-spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Audio controls
|
||||
@@ -62,10 +66,7 @@
|
||||
|
||||
// Video controls
|
||||
.plyr--video .plyr__controls {
|
||||
background: linear-gradient(
|
||||
rgba($plyr-video-controls-bg, 0),
|
||||
rgba($plyr-video-controls-bg, 0.7)
|
||||
);
|
||||
background: linear-gradient(rgba($plyr-video-controls-bg, 0), rgba($plyr-video-controls-bg, 0.7));
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
bottom: 0;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// --------------------------------------------------------------
|
||||
// Embedded players
|
||||
// YouTube, Vimeo, etc
|
||||
// --------------------------------------------------------------
|
||||
|
||||
// Default to 16:9 ratio but this is set by JavaScript based on config
|
||||
$embed-padding: ((100 / 16) * 9);
|
||||
|
||||
.plyr__video-embed {
|
||||
height: 0;
|
||||
padding-bottom: to-percentage($embed-padding);
|
||||
position: relative;
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// If the full custom UI is supported
|
||||
.plyr--full-ui .plyr__video-embed {
|
||||
$height: 240;
|
||||
$offset: to-percentage(($height - $embed-padding) / ($height / 50));
|
||||
|
||||
// Only used for Vimeo
|
||||
> .plyr__video-embed__container {
|
||||
padding-bottom: to-percentage($height);
|
||||
position: relative;
|
||||
transform: translateY(-$offset);
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,19 @@
|
||||
// Playback progress
|
||||
// --------------------------------------------------------------
|
||||
|
||||
// Offset the range thumb in order to be able to calculate the relative progress (#954)
|
||||
$plyr-progress-offset: $plyr-range-thumb-height;
|
||||
|
||||
.plyr__progress {
|
||||
flex: 1;
|
||||
left: $plyr-range-thumb-height / 2;
|
||||
margin-right: $plyr-range-thumb-height;
|
||||
left: $plyr-progress-offset / 2;
|
||||
margin-right: $plyr-progress-offset;
|
||||
position: relative;
|
||||
|
||||
input[type='range'],
|
||||
&__buffer {
|
||||
margin-left: -($plyr-range-thumb-height / 2);
|
||||
margin-right: -($plyr-range-thumb-height / 2);
|
||||
// Offset the range thumb in order to be able to calculate the relative progress (#954)
|
||||
width: calc(100% + #{$plyr-range-thumb-height});
|
||||
margin-left: -($plyr-progress-offset / 2);
|
||||
margin-right: -($plyr-progress-offset / 2);
|
||||
width: calc(100% + #{$plyr-progress-offset});
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
|
||||
@@ -20,3 +20,36 @@
|
||||
// Require z-index to force border-radius
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
// Default to 16:9 ratio but this is set by JavaScript based on config
|
||||
$embed-padding: ((100 / 16) * 9);
|
||||
|
||||
.plyr__video-embed,
|
||||
.plyr__video-wrapper--fixed-ratio {
|
||||
height: 0;
|
||||
padding-bottom: to-percentage($embed-padding);
|
||||
}
|
||||
|
||||
.plyr__video-embed iframe,
|
||||
.plyr__video-wrapper--fixed-ratio video {
|
||||
border: 0;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// If the full custom UI is supported
|
||||
.plyr--full-ui .plyr__video-embed {
|
||||
$height: 240;
|
||||
$offset: to-percentage(($height - $embed-padding) / ($height / 50));
|
||||
|
||||
// Only used for Vimeo
|
||||
> .plyr__video-embed__container {
|
||||
padding-bottom: to-percentage($height);
|
||||
position: relative;
|
||||
transform: translateY(-$offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
@import 'components/captions';
|
||||
@import 'components/control';
|
||||
@import 'components/controls';
|
||||
@import 'components/embed';
|
||||
@import 'components/menus';
|
||||
@import 'components/sliders';
|
||||
@import 'components/poster';
|
||||
|
||||
Reference in New Issue
Block a user