Merge branch 'develop' into css-variables

# Conflicts:
#	.eslintrc
#	demo/dist/demo.css
#	demo/dist/demo.js
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	dist/plyr.js
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.min.mjs
#	dist/plyr.min.mjs.map
#	dist/plyr.mjs
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	dist/plyr.polyfilled.min.mjs
#	dist/plyr.polyfilled.min.mjs.map
#	dist/plyr.polyfilled.mjs
#	gulpfile.js
#	package.json
This commit is contained in:
Sam Potts 2019-06-01 19:55:14 +10:00
commit 12c6282d14
52 changed files with 15300 additions and 8732 deletions

View File

@ -1,35 +0,0 @@
{
"parser": "babel-eslint",
"extends": ["airbnb-base", "prettier"],
"env": {
"browser": true,
"es6": true
},
"globals": {
"Plyr": false,
"jQuery": false
},
"rules": {
"import/no-cycle": 1,
"no-const-assign": 1,
"no-shadow": 0,
"no-this-before-super": 1,
"no-undef": 1,
"no-unreachable": 1,
"no-unused-vars": 1,
"constructor-super": 1,
"valid-typeof": 1,
"indent": [2, 4, { "SwitchCase": 1 }],
"quotes": [2, "single", "avoid-escape"],
"semi": [2, "always"],
"eqeqeq": [2, "always"],
"one-var": [2, "never"],
"comma-dangle": [2, "always-multiline"],
"spaced-comment": [2, "always"],
"no-restricted-globals": 2,
"no-param-reassign": [2, { "props": false }]
},
"parserOptions": {
"sourceType": "module"
}
}

View File

@ -1,3 +1,24 @@
## 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

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

1344
demo/dist/demo.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
demo/dist/error.css vendored

File diff suppressed because one or more lines are too long

View File

@ -50,6 +50,17 @@
type="font/woff2"
href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2"
/>
<!-- Google Analytics-->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-132699580-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-132699580-1');
</script>
</head>
<body>
@ -265,7 +276,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>

View File

@ -5,6 +5,7 @@
// ==========================================================================
import Raven from 'raven-js';
import Plyr from '../../../src/js/plyr';
(() => {

View File

@ -38,7 +38,6 @@
&:hover {
box-shadow: 0 2px 2px rgba(#000, 0.1);
transform: translateY(-1px);
}
&:focus {
@ -50,7 +49,7 @@
}
&:active {
transform: translateY(1px);
top: 1px;
}
}

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

1338
dist/plyr.js vendored

File diff suppressed because it is too large Load Diff

2
dist/plyr.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/plyr.min.mjs vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1338
dist/plyr.mjs vendored

File diff suppressed because it is too large Load Diff

8594
dist/plyr.polyfilled.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8594
dist/plyr.polyfilled.mjs vendored

File diff suppressed because it is too large Load Diff

View File

@ -6,26 +6,30 @@
const path = require('path');
const gulp = require('gulp');
// ------------------------------------
// JavaScript
// ------------------------------------
const terser = require('gulp-terser');
const rollup = require('gulp-better-rollup');
const babel = require('rollup-plugin-babel');
const commonjs = require('rollup-plugin-commonjs');
const resolve = require('rollup-plugin-node-resolve');
// ------------------------------------
// CSS
// ------------------------------------
const sass = require('gulp-sass');
const clean = require('gulp-clean-css');
const prefix = require('gulp-autoprefixer');
const postcss = require('gulp-postcss');
const customprops = require('postcss-custom-properties');
// ------------------------------------
// Images
// ------------------------------------
const svgstore = require('gulp-svgstore');
const imagemin = require('gulp-imagemin');
// ------------------------------------
// Utils
// ------------------------------------
const del = require('del');
const filter = require('gulp-filter');
const header = require('gulp-header');
@ -39,18 +43,22 @@ const plumber = require('gulp-plumber');
const size = require('gulp-size');
const sourcemaps = require('gulp-sourcemaps');
const through = require('through2');
// ------------------------------------
// Deployment
// ------------------------------------
const aws = require('aws-sdk');
const publish = require('gulp-awspublish');
const FastlyPurge = require('fastly-purge');
// ------------------------------------
// Configs
// ------------------------------------
const pkg = require('./package.json');
const build = require('./build.json');
const deploy = require('./deploy.json');
// ------------------------------------
// Info from package
// ------------------------------------
const { browserslist: browsers, version } = pkg;
const minSuffix = '.min';
// Get AWS config
@ -151,6 +159,7 @@ Object.entries(build.js).forEach(([filename, entry]) => {
{
// debug: true,
useBuiltIns: polyfill ? 'usage' : false,
corejs: polyfill ? 3 : undefined,
},
],
],

View File

@ -1,6 +1,6 @@
{
"name": "plyr",
"version": "3.5.2",
"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>",
@ -31,31 +31,33 @@
"scripts": {
"build": "gulp build",
"lint": "eslint src/js && npm run-script remark",
"lint:fix": "eslint --fix src/js",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"deploy": "yarn lint && gulp deploy"
},
"devDependencies": {
"ansi-colors": "^3.2.4",
"aws-sdk": "^2.427.0",
"@babel/core": "^7.4.0",
"@babel/preset-env": "^7.4.2",
"aws-sdk": "^2.466.0",
"@babel/core": "^7.4.5",
"@babel/preset-env": "^7.4.5",
"babel-eslint": "^10.0.1",
"del": "^4.0.0",
"eslint": "^5.15.3",
"del": "^4.1.1",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-import": "^2.16.0",
"eslint-config-prettier": "^4.3.0",
"eslint-plugin-import": "^2.17.3",
"eslint-plugin-simple-import-sort": "^3.1.1",
"fancy-log": "^1.3.3",
"fastly-purge": "^1.0.1",
"git-branch": "^2.0.1",
"gulp": "^4.0.0",
"gulp-autoprefixer": "^6.0.0",
"gulp": "^4.0.2",
"gulp-autoprefixer": "^6.1.0",
"gulp-awspublish": "^4.0.0",
"gulp-better-rollup": "^4.0.1",
"gulp-clean-css": "^4.0.0",
"gulp-filter": "^5.1.0",
"gulp-clean-css": "^4.2.0",
"gulp-filter": "^6.0.0",
"gulp-header": "^2.0.7",
"gulp-imagemin": "^5.0.3",
"gulp-imagemin": "^6.0.0",
"gulp-open": "^3.0.1",
"gulp-plumber": "^1.2.1",
"gulp-postcss": "^8.0.0",
@ -65,31 +67,31 @@
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-svgstore": "^7.0.1",
"gulp-terser": "^1.1.7",
"postcss-custom-properties": "^8.0.9",
"gulp-terser": "^1.2.0",
"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.2",
"rollup": "^1.7.3",
"rollup": "^1.13.0",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-commonjs": "^9.2.1",
"rollup-plugin-node-resolve": "^4.0.1",
"stylelint": "^9.10.1",
"stylelint-config-prettier": "^5.0.0",
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.3.0",
"stylelint-order": "^2.1.0",
"stylelint-scss": "^3.5.4",
"stylelint-selector-bem-pattern": "^2.0.0",
"rollup-plugin-commonjs": "^10.0.0",
"rollup-plugin-node-resolve": "^5.0.1",
"stylelint": "^10.0.1",
"stylelint-config-prettier": "^5.2.0",
"stylelint-config-recommended": "^2.2.0",
"stylelint-config-sass-guidelines": "^6.0.0",
"stylelint-order": "^3.0.0",
"stylelint-scss": "^3.8.0",
"stylelint-selector-bem-pattern": "^2.1.0",
"through2": "^3.0.1"
},
"dependencies": {
"core-js": "^3.0.0",
"custom-event-polyfill": "^1.0.6",
"loadjs": "^3.6.0",
"core-js": "^3.1.3",
"custom-event-polyfill": "^1.0.7",
"loadjs": "^3.6.1",
"rangetouch": "^2.0.0",
"raven-js": "^3.27.0",
"raven-js": "^3.27.1",
"url-polyfill": "^1.1.5"
}
}

View File

@ -23,9 +23,6 @@
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
// Trim on save
"files.trimTrailingWhitespace": true

View File

@ -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.2/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.2/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.2/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.2/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`&sup2; | 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`&sup1; | 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,7 +293,7 @@ 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: 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. |
@ -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`&sup2; | ✓ | ✓ | 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`&sup1; | ✓ | ✓ | 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

View File

@ -85,7 +85,6 @@ const captions = {
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
// Use first browser language when language is 'auto'
@ -124,19 +123,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
Object.assign(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
@ -164,7 +165,6 @@ const captions = {
const { toggled } = this.captions; // Current state
const activeClass = this.config.classNames.captions.active;
// Get the next state
// If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
@ -300,10 +300,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);
track = sorted.find(t => t.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 +362,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);

View File

@ -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,
@ -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: {

630
src/js/controls.js vendored
View File

@ -4,6 +4,7 @@
// ==========================================================================
import RangeTouch from 'rangetouch';
import captions from './captions';
import html5 from './html5';
import support from './support';
@ -105,7 +106,6 @@ const controls = {
const namespace = 'http://www.w3.org/2000/svg';
const iconUrl = controls.getIconUrl.call(this);
const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
// Create <svg>
const icon = document.createElementNS(namespace, 'svg');
setAttributes(
@ -172,7 +172,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 +198,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 +379,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',
@ -491,15 +493,15 @@ const controls = {
get() {
return menuItem.getAttribute('aria-checked') === 'true';
},
set(checked) {
set(check) {
// Ensure exclusivity
if (checked) {
if (check) {
Array.from(menuItem.parentNode.children)
.filter(node => matches(node, '[role="menuitemradio"]'))
.forEach(node => node.setAttribute('aria-checked', 'false'));
}
menuItem.setAttribute('aria-checked', checked ? 'true' : 'false');
menuItem.setAttribute('aria-checked', check ? 'true' : 'false');
},
});
@ -607,17 +609,17 @@ const controls = {
let value = 0;
const setProgress = (target, input) => {
const value = is.number(input) ? input : 0;
const val = is.number(input) ? input : 0;
const progress = is.element(target) ? target : this.elements.display.buffer;
// Update value and label
if (is.element(progress)) {
progress.value = value;
progress.value = val;
// Update text label inside
const label = progress.getElementsByTagName('span')[0];
if (is.element(label)) {
label.childNodes[0].nodeValue = value;
label.childNodes[0].nodeValue = val;
}
}
};
@ -699,14 +701,8 @@ const controls = {
return;
}
// Calculate percentage
let percent = 0;
const clientRect = this.elements.progress.getBoundingClientRect();
const visible = `${this.config.classNames.tooltip}--visible`;
const toggle = toggle => {
toggleClass(this.elements.display.seekTooltip, visible, toggle);
};
const toggle = show => toggleClass(this.elements.display.seekTooltip, visible, show);
// Hide on touch
if (this.touch) {
@ -715,6 +711,9 @@ const controls = {
}
// Determine percentage, if already visible
let percent = 0;
const clientRect = this.elements.progress.getBoundingClientRect();
if (is.event(event)) {
percent = (100 / clientRect.width) * (event.pageX - clientRect.left);
} else if (hasClass(this.elements.display.seekTooltip, visible)) {
@ -1111,7 +1110,7 @@ const controls = {
let target = pane;
if (!is.element(target)) {
target = Object.values(this.elements.settings.panels).find(pane => !pane.hidden);
target = Object.values(this.elements.settings.panels).find(p => !p.hidden);
}
const firstItem = target.querySelector('[role^="menuitem"]');
@ -1138,7 +1137,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 +1193,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 +1246,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 +1255,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 wrapper = 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',
wrapper.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);
wrapper.appendChild(popup);
container.appendChild(wrapper);
this.elements.settings.popup = popup;
this.elements.settings.menu = wrapper;
}
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;
},

View File

@ -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,12 +44,15 @@ const html5 = {
const player = this;
// Set aspect ratio if set
setAspectRatio.call(player);
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
// Get sources
const sources = html5.getSources.call(player);
const source = sources.find(source => source.getAttribute('src') === player.source);
const source = sources.find(s => s.getAttribute('src') === player.source);
// Return size, if match is found
return source && Number(source.getAttribute('size'));
@ -56,9 +60,8 @@ const html5 = {
set(input) {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find(source => Number(source.getAttribute('size')) === input);
const source = sources.find(s => Number(s.getAttribute('size')) === input);
// No matching source found
if (!source) {

View File

@ -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) {
@ -147,7 +147,7 @@ class Listeners {
player.loop = !player.loop;
break;
/* case 73:
/* case 73:
this.setLoop('start');
break;
@ -275,17 +275,16 @@ class Listeners {
elements.container,
'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',
event => {
const { controls } = elements;
const { controls: controlsElement } = elements;
// Remove button states for fullscreen
if (controls && event.type === 'enterfullscreen') {
controls.pressed = false;
controls.hover = false;
if (controlsElement && event.type === 'enterfullscreen') {
controlsElement.pressed = false;
controlsElement.hover = false;
}
// Show, then hide after a timeout unless another control event occurs
const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
let delay = 0;
if (show) {
@ -317,10 +316,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;
};
@ -338,8 +337,8 @@ class Listeners {
};
const resized = () => {
window.clearTimeout(timers.resized);
timers.resized = window.setTimeout(setPlayerSize, 50);
clearTimeout(timers.resized);
timers.resized = setTimeout(setPlayerSize, 50);
};
on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {
@ -351,7 +350,6 @@ class Listeners {
}
const isEnter = event.type === 'enterfullscreen';
// Set the player size when entering fullscreen to viewport size
const { padding, ratio } = setPlayerSize(isEnter);
@ -486,7 +484,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
@ -542,7 +540,6 @@ class Listeners {
controls() {
const { player } = this;
const { elements } = player;
// IE doesn't support input event, so we fallback to change
const inputEvent = browser.isIE ? 'change' : 'input';
@ -678,7 +675,6 @@ class Listeners {
// Was playing before?
const play = seek.hasAttribute(attribute);
// Done seeking
const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
@ -706,7 +702,6 @@ class Listeners {
inputEvent,
event => {
const seek = event.currentTarget;
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
let seekTo = seek.getAttribute('seek-value');
@ -806,7 +801,7 @@ class Listeners {
// Show controls when they receive focus (e.g., when using keyboard tab key)
this.bind(elements.controls, 'focusin', () => {
const { config, elements, timers } = player;
const { config, timers } = player;
// Skip transition to prevent focus from scrolling the parent element
toggleClass(elements.controls, config.classNames.noTransition, true);
@ -837,10 +832,8 @@ class Listeners {
// Detect "natural" scroll - suppored on OS X Safari only
// Other browsers on OS X will be inverted until support improves
const inverted = event.webkitDirectionInvertedFromDevice;
// Get delta from event. Invert if `inverted` is true
const [x, y] = [event.deltaX, -event.deltaY].map(value => (inverted ? -value : value));
// Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)
const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);

View File

@ -14,6 +14,20 @@ 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.
@ -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()');
@ -240,16 +261,13 @@ 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));
// Advertisement regular events
Object.keys(google.ima.AdEvent.Type).forEach(type => {
this.manager.addEventListener(google.ima.AdEvent.Type[type], event => this.onAdEvent(event));
this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));
});
// Resolve our adsManager
@ -285,7 +303,6 @@ class Ads {
*/
onAdEvent(event) {
const { container } = this.player.elements;
// Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
// don't have ad object associated
const ad = event.getAd();
@ -293,19 +310,18 @@ class Ads {
// Proxy event
const dispatchEvent = type => {
const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
triggerEvent.call(this.player, this.player.media, event);
triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);
};
// 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 +333,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 +370,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 +378,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 +388,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 +473,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();

View File

@ -2,6 +2,7 @@ import { createElement } from '../utils/elements';
import { once } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import { extend } from '../utils/objects';
import { formatTime } from '../utils/time';
// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg"
@ -17,17 +18,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]}`);
@ -121,7 +122,6 @@ class PreviewThumbnails {
// If string, convert into single-element list
const urls = is.string(src) ? [src] : src;
// Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
const promises = urls.map(u => this.getThumbnail(u));
@ -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);
}
@ -294,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', {
@ -419,7 +426,9 @@ class PreviewThumbnails {
if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {
// Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients
// First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
// eslint-disable-next-line no-param-reassign
image.dataset.deleting = true;
// This has to be set before the timeout - to prevent issues switching between hover and scrub
const { currentImageContainer } = this;
@ -460,7 +469,6 @@ class PreviewThumbnails {
const { urlPrefix } = this.thumbnails[0];
const thumbURL = urlPrefix + newThumbFilename;
const previewImage = new Image();
previewImage.src = thumbURL;
previewImage.onload = () => {
@ -594,11 +602,9 @@ class PreviewThumbnails {
const seekbarRect = this.player.elements.progress.getBoundingClientRect();
const plyrRect = this.player.elements.container.getBoundingClientRect();
const { container } = this.elements.thumb;
// Find the lowest and highest desired left-position, so we don't slide out the side of the video container
const minVal = plyrRect.left - seekbarRect.left + 10;
const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10;
// Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2;
@ -629,9 +635,13 @@ class PreviewThumbnails {
// Find difference between height and preview container height
const multiplier = this.thumbContainerHeight / frame.h;
// eslint-disable-next-line no-param-reassign
previewImage.style.height = `${Math.floor(previewImage.naturalHeight * multiplier)}px`;
// eslint-disable-next-line no-param-reassign
previewImage.style.width = `${Math.floor(previewImage.naturalWidth * multiplier)}px`;
// eslint-disable-next-line no-param-reassign
previewImage.style.left = `-${frame.x * multiplier}px`;
// eslint-disable-next-line no-param-reassign
previewImage.style.top = `-${frame.y * multiplier}px`;
}
}

View File

@ -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);
@ -91,7 +91,6 @@ const vimeo = {
}
const id = parseId(source);
// Build an iframe
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
@ -102,7 +101,6 @@ const vimeo = {
// Get poster, if already set
const { poster } = player;
// Inject the package
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe);
@ -259,7 +257,7 @@ const vimeo = {
.getVideoUrl()
.then(value => {
currentSrc = value;
controls.setDownloadLink.call(player);
controls.setDownloadUrl.call(player);
})
.catch(error => {
this.debug.warn(error);
@ -281,8 +279,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

View File

@ -34,78 +34,78 @@ 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);
} else {
// Load the API
loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
// Setup callback for the API
// YouTube has it's own system of course...
window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || [];
// Add to queue
window.onYouTubeReadyCallbacks.push(() => {
youtube.ready.call(this);
});
// Reference current global callback
const callback = window.onYouTubeIframeAPIReady;
// Set callback to process queue
window.onYouTubeIframeAPIReady = () => {
window.onYouTubeReadyCallbacks.forEach(callback => {
// Call global callback if set
if (is.function(callback)) {
callback();
});
}
youtube.ready.call(this);
};
// Load the SDK
loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
}
},
// 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
ready() {
const player = this;
// Ignore already setup (race condition)
const currentId = player.media.getAttribute('id');
const currentId = player.media && player.media.getAttribute('id');
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
@ -121,25 +121,23 @@ const youtube = {
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = parseId(source);
const id = generateId(player.provider);
// Get poster, if already set
const { poster } = player;
// Replace media element
const container = createElement('div', { id, poster });
player.media = replaceElement(container, player.media);
// Id to poster wrapper
const posterSrc = format => `https://i.ytimg.com/vi/${videoId}/${format}default.jpg`;
const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then(image => ui.setPoster.call(player, image.src))
.then(posterSrc => {
.then(src => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!posterSrc.includes('maxres')) {
if (!src.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
})
@ -151,7 +149,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 +384,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);

View File

@ -1,6 +1,6 @@
// ==========================================================================
// Plyr
// plyr.js v3.5.2
// plyr.js v3.5.3
// 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
@ -149,7 +151,6 @@ class Plyr {
// Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube
const type = this.media.tagName.toLowerCase();
// Embed properties
let iframe = null;
let url = null;
@ -301,8 +302,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
@ -322,27 +323,27 @@ class Plyr {
* Types and provider helpers
*/
get isHTML5() {
return Boolean(this.provider === providers.html5);
return this.provider === providers.html5;
}
get isEmbed() {
return Boolean(this.isYouTube || this.isVimeo);
return this.isYouTube || this.isVimeo;
}
get isYouTube() {
return Boolean(this.provider === providers.youtube);
return this.provider === providers.youtube;
}
get isVimeo() {
return Boolean(this.provider === providers.vimeo);
return this.provider === providers.vimeo;
}
get isVideo() {
return Boolean(this.type === types.video);
return this.type === types.video;
}
get isAudio() {
return Boolean(this.type === types.audio);
return this.type === types.audio;
}
/**
@ -512,7 +513,6 @@ class Plyr {
get duration() {
// Faux duration set via config
const fauxDuration = parseFloat(this.config.duration);
// Media duration can be NaN or Infinity before the media has loaded
const realDuration = (this.media || {}).duration;
const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;
@ -660,24 +660,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,6 +680,42 @@ 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
@ -822,6 +851,19 @@ 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 {String} input - the URL for the new poster image
@ -846,6 +888,38 @@ 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
@ -969,10 +1043,8 @@ class Plyr {
if (this.supported.ui && !this.isAudio) {
// Get state before change
const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);
// Negate the argument if not undefined since adding the class to hides the controls
const force = typeof toggle === 'undefined' ? undefined : !toggle;
// Apply and get updated state
const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
@ -1088,11 +1160,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);

View File

@ -1,12 +1,13 @@
// ==========================================================================
// Plyr Polyfilled Build
// plyr.js v3.5.2
// plyr.js v3.5.3
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import 'custom-event-polyfill';
import 'url-polyfill';
import Plyr from './plyr';
export default Plyr;

View File

@ -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);
@ -213,7 +213,7 @@ const ui = {
// Set state
Array.from(this.elements.buttons.play || []).forEach(target => {
target.pressed = this.playing;
Object.assign(target, { pressed: this.playing });
});
// Only update controls on non timeupdate events
@ -233,25 +233,37 @@ 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
toggleControls(force) {
const { controls } = this.elements;
const { controls: controlsElement } = this.elements;
if (controls && this.config.hideControls) {
if (controlsElement && 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 ||
controlsElement.pressed ||
controlsElement.hover ||
recentTouchSeek,
),
);
}
},
};

View File

@ -4,6 +4,7 @@
import { toggleListener } from './events';
import is from './is';
import { extend } from './objects';
// Wrap an element
export function wrap(elements, wrapper) {
@ -16,7 +17,6 @@ export function wrap(elements, wrapper) {
.reverse()
.forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling.
const parent = element.parentNode;
const sibling = element.nextSibling;
@ -137,30 +137,28 @@ export function getAttributesFromSelector(sel, existingAttributes) {
}
const attributes = {};
const existing = existingAttributes;
const existing = extend({}, existingAttributes);
sel.split(',').forEach(s => {
// Remove whitespace
const selector = s.trim();
const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value
const parts = stripped.split('=');
const key = parts[0];
const [key] = parts;
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character
const start = selector.charAt(0);
switch (start) {
case '.':
// Add to existing classname
if (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 +177,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
}
});
return attributes;
return extend(existing, attributes);
}
// Toggle hidden
@ -233,14 +231,14 @@ export function matches(element, selector) {
return Array.from(document.querySelectorAll(selector)).includes(this);
}
const matches =
const method =
prototype.matches ||
prototype.webkitMatchesSelector ||
prototype.mozMatchesSelector ||
prototype.msMatchesSelector ||
match;
return matches.call(element, selector);
return method.call(element, selector);
}
// Find all elements

View File

@ -35,7 +35,6 @@ export function toggleListener(element, event, callback, toggle = false, passive
// Allow multiple events
const events = event.split(' ');
// Build options
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;

View File

@ -36,8 +36,8 @@ const i18n = {
'{title}': config.title,
};
Object.entries(replace).forEach(([key, value]) => {
string = replaceAll(string, key, value);
Object.entries(replace).forEach(([k, v]) => {
string = replaceAll(string, k, v);
});
return string;

View File

@ -15,10 +15,10 @@ export default function loadSprite(url, id) {
const prefix = 'cache';
const hasId = is.string(id);
let isCached = false;
const exists = () => document.getElementById(id) !== null;
const update = (container, data) => {
// eslint-disable-next-line no-param-reassign
container.innerHTML = data;
// Check again incase of race condition
@ -33,7 +33,6 @@ export default function loadSprite(url, id) {
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
container.setAttribute('hidden', '');

17
src/js/utils/numbers.js Normal file
View File

@ -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 };

View File

@ -4,26 +4,68 @@
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 +74,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 };

View File

@ -18,7 +18,6 @@ export function formatTime(time = 0, displayHours = false, inverted = false) {
// Format time component to add leading zero
const format = value => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs
let hours = getHours(time);
const mins = getMinutes(time);

View File

@ -12,44 +12,49 @@
align-items: center;
display: flex;
justify-content: flex-end;
min-width: 0; // Fix for Edge issue where content would overflow
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 +67,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;

View File

@ -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);
}
}

View File

@ -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'] {

View File

@ -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);
}
}

View File

@ -32,7 +32,6 @@ $css-vars-use-native: true;
@import 'components/captions';
@import 'components/control';
@import 'components/controls';
@import 'components/embed';
@import 'components/menus';
@import 'components/sliders';
@import 'components/poster';

1218
yarn.lock

File diff suppressed because it is too large Load Diff