Fix merge conflicts

This commit is contained in:
Danielh112
2020-08-18 11:29:25 +01:00
144 changed files with 23742 additions and 18900 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
indent_size = 4 indent_size = 2
indent_style = space indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
+1 -1
View File
@@ -1,3 +1,3 @@
PLEASE USE OUR SPECIFIC ISSUE TEMPLATES for bug reports, features and improvement suggestions. PLEASE USE OUR SPECIFIC ISSUE TEMPLATES for bug reports, features and improvement suggestions.
Our issue tracker is not for support questions. If you need help, follow our support instructions: https://github.com/sampotts/plyr/blob/master/contributing.md#support Our issue tracker is not for support questions. If you need help, follow our support instructions: https://github.com/sampotts/plyr/blob/master/CONTRIBUTING.md#support
+1 -1
View File
@@ -1,5 +1,5 @@
tasks: tasks:
- init: npm install && npm i gulp -g - before: npm install && npm i gulp -g
command: gulp command: gulp
ports: ports:
- port: 3000 - port: 3000
+1 -1
View File
@@ -9,4 +9,4 @@ yarn.lock
package-lock.json package-lock.json
*.mp4 *.mp4
*.webm *.webm
!dist/blank.mp4 !dist/blank.mp4
+1 -1
View File
@@ -1 +1 @@
v13.8.0 v13.8.0
+5 -5
View File
@@ -1,7 +1,7 @@
{ {
"useTabs": false, "useTabs": false,
"tabWidth": 4, "tabWidth": 2,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all", "trailingComma": "all",
"printWidth": 120 "printWidth": 120
} }
+22 -22
View File
@@ -1,25 +1,25 @@
{ {
"plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"], "plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"],
"extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"], "extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"],
"rules": { "rules": {
"selector-class-pattern": null, "selector-class-pattern": null,
"selector-no-qualifying-type": [ "selector-no-qualifying-type": [
true, true,
{ {
"ignore": ["attribute", "class"] "ignore": ["attribute", "class"]
} }
], ],
"string-no-newline": null, "string-no-newline": null,
"indentation": 4, "indentation": 2,
"string-quotes": "single", "string-quotes": "single",
"max-nesting-depth": 2, "max-nesting-depth": 2,
"plugin/selector-bem-pattern": { "plugin/selector-bem-pattern": {
"preset": "bem", "preset": "bem",
"componentName": "(([a-z0-9]+(?!-$)-?)+)", "componentName": "(([a-z0-9]+(?!-$)-?)+)",
"componentSelectors": { "componentSelectors": {
"initial": "\\.{componentName}(((__|--)(([a-z0-9\\[\\]'=]+(?!-$)-?)+))+)?$" "initial": "\\.{componentName}(((__|--)(([a-z0-9\\[\\]'=]+(?!-$)-?)+))+)?$"
}, },
"ignoreSelectors": [".*\\.has-.*", ".*\\.is-.*"] "ignoreSelectors": [".*\\.has-.*", ".*\\.is-.*"]
}
} }
}
} }
+1183
View File
File diff suppressed because it is too large Load Diff
+21 -13
View File
@@ -7,15 +7,17 @@ We welcome bug reports, feature requests and pull requests. If you want to help
Before asking questions, read our [documentation](https://github.com/sampotts/plyr) and [FAQ](https://github.com/sampotts/plyr/wiki/FAQ). Before asking questions, read our [documentation](https://github.com/sampotts/plyr) and [FAQ](https://github.com/sampotts/plyr/wiki/FAQ).
If these doesn't answer your question If these doesn't answer your question
* Use [Stack Overflow](https://stackoverflow.com/) for questions that doesn't directly involve Plyr. This includes for example how to use Javascript, CSS or HTML5 media in general, and how to use other frameworks, libraries and technology.
* Use [our Slack](https://bit.ly/plyr-chat) if you need help using Plyr or have questions about Plyr. - Use [Stack Overflow](https://stackoverflow.com/) for questions that doesn't directly involve Plyr. This includes for example how to use Javascript, CSS or HTML5 media in general, and how to use other frameworks, libraries and technology.
- Use [our Slack](https://bit.ly/plyr-chat) if you need help using Plyr or have questions about Plyr.
## Commenting ## Commenting
When commenting, keep a civil tone and stay on topic. Don't ask for [support](#support), or post "+1" or "I agree" type of comments. Use the emojis instead. When commenting, keep a civil tone and stay on topic. Don't ask for [support](#support), or post "+1" or "I agree" type of comments. Use the emojis instead.
Asking for the status on issues is discouraged. Unless someone has explicitly said in an issue that it's work in progress, most likely that means no one is working on it. We have a lot to do, and it may not be a top priority for us. Asking for the status on issues is discouraged. Unless someone has explicitly said in an issue that it's work in progress, most likely that means no one is working on it. We have a lot to do, and it may not be a top priority for us.
We *may* moderate discussions. We do this to avoid threads being "hijacked", to avoid confusion in case the content is misleading or outdated, and to avoid bothering people with github notifications. We _may_ moderate discussions. We do this to avoid threads being "hijacked", to avoid confusion in case the content is misleading or outdated, and to avoid bothering people with github notifications.
## Creating issues ## Creating issues
@@ -23,24 +25,30 @@ Please follow the instructions in our issue templates. Don't use github issues t
## Contributing features and documentation ## Contributing features and documentation
* If you want to add a feature or make critical changes, you may want to ensure that this is something we also want (so you don't waste your time). Ask us about this in the corresponding issue if there is one, or on [our Slack](https://bit.ly/plyr-chat) otherwise. - If you want to add a feature or make critical changes, you may want to ensure that this is something we also want (so you don't waste your time). Ask us about this in the corresponding issue if there is one, or on [our Slack](https://bit.ly/plyr-chat) otherwise.
* Fork Plyr, and create a new branch in your fork, based on the **develop** branch - Fork Plyr, and create a new branch in your fork, based on the **develop** branch
* To test locally, you can use the demo site. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build and it will run a local web server for development and watch for any changes. - To test locally, you can use the demo site. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build and it will run a local web server for development and watch for any changes.
### Online one-click setup ### Online one-click setup for contributing
Alternatively can also use Gitpod, a free online Visual Studio Code-like IDE. With a single click it will automatically launch a ready-to-code workspace with all the dependencies pre-installed, gulp watching for changes and the web server running, so that you can start coding straightaway. You can use Gitpod (a free online VS Code-like IDE) for contributing. With a single click it will launch a workspace and automatically:
- clone the plyr repo.
- install the dependencies.
- run `gulp` to the start the server.
So that you can start straight away.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/from-referrer/) [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/from-referrer/)
* Develop and test your modifications. - Develop and test your modifications.
* Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as the build output, or logging and breakpoints you added for testing. - Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as the build output, or logging and breakpoints you added for testing.
* If your modifications changes the documented behavior or add new features, document these changes in readme.md. - If your modifications changes the documented behavior or add new features, document these changes in [README.md](README.md).
* When finished, push the changes to your GitHub repository and send a pull request to **develop**. Describe what your PR does. - When finished, push the changes to your GitHub repository and send a pull request to **develop**. Describe what your PR does.
* If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch. - If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch.
View File
View File
+276 -180
View File
@@ -2,32 +2,32 @@ Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vi
[Checkout the demo](https://plyr.io) - [Donate](#donate) - [Slack](https://bit.ly/plyr--chat) [Checkout the demo](https://plyr.io) - [Donate](#donate) - [Slack](https://bit.ly/plyr--chat)
[![npm version](https://badge.fury.io/js/plyr.svg)](https://badge.fury.io/js/plyr) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/sampotts/plyr) [![Financial Contributors on Open Collective](https://opencollective.com/plyr/all/badge.svg?label=financial+contributors)](https://opencollective.com/plyr) [![npm version](https://badge.fury.io/js/plyr.svg)](https://badge.fury.io/js/plyr) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/sampotts/plyr) [![Financial Contributors on Open Collective](https://opencollective.com/plyr/all/badge.svg?label=financial+contributors)](https://opencollective.com/plyr)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io) [![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io)
# Features # Features
- 📼 **HTML Video & Audio, YouTube & Vimeo** - support for the major formats - 📼 **HTML Video & Audio, YouTube & Vimeo** - support for the major formats
- 💪 **Accessible** - full support for VTT captions and screen readers - 💪 **Accessible** - full support for VTT captions and screen readers
- 🔧 **[Customizable](#html)** - make the player look how you want with the markup you want - 🔧 **[Customizable](#html)** - make the player look how you want with the markup you want
- 😎 **Clean HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no - 😎 **Clean HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
`<span>` or `<a href="#">` button hacks `<span>` or `<a href="#">` button hacks
- 📱 **Responsive** - works with any screen size - 📱 **Responsive** - works with any screen size
- 💵 **[Monetization](#ads)** - make money from your videos - 💵 **[Monetization](#ads)** - make money from your videos
- 📹 **[Streaming](#demos)** - support for hls.js, Shaka and dash.js streaming playback - 📹 **[Streaming](#demos)** - support for hls.js, Shaka and dash.js streaming playback
- 🎛 **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API - 🎛 **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
- 🎤 **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats - 🎤 **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
- 🔎 **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes - 🔎 **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
- ⌨️ **[Shortcuts](#shortcuts)** - supports keyboard shortcuts - ⌨️ **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
- 🖥 **Picture-in-Picture** - supports picture-in-picture mode - 🖥 **Picture-in-Picture** - supports picture-in-picture mode
- 📱 **Playsinline** - supports the `playsinline` attribute - 📱 **Playsinline** - supports the `playsinline` attribute
- 🏎 **Speed controls** - adjust speed on the fly - 🏎 **Speed controls** - adjust speed on the fly
- 📖 **Multiple captions** - support for multiple caption tracks - 📖 **Multiple captions** - support for multiple caption tracks
- 🌎 **i18n support** - support for internationalization of controls - 🌎 **i18n support** - support for internationalization of controls
- 👌 **[Preview thumbnails](#preview-thumbnails)** - support for displaying preview thumbnails - 👌 **[Preview thumbnails](#preview-thumbnails)** - support for displaying preview thumbnails
- 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required - 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required
- 💁‍♀️ **SASS** - to include in your build processes - 💁‍♀️ **SASS** - to include in your build processes
### Demos ### Demos
@@ -42,21 +42,23 @@ Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.o
### HTML5 Video ### HTML5 Video
```html ```html
<video poster="/path/to/poster.jpg" id="player" playsinline controls> <video id="player" playsinline controls data-poster="/path/to/poster.jpg">
<source src="/path/to/video.mp4" type="video/mp4" /> <source src="/path/to/video.mp4" type="video/mp4" />
<source src="/path/to/video.webm" type="video/webm" /> <source src="/path/to/video.webm" type="video/webm" />
<!-- Captions are optional --> <!-- Captions are optional -->
<track kind="captions" label="English captions" src="/path/to/captions.vtt" srclang="en" default /> <track kind="captions" label="English captions" src="/path/to/captions.vtt" srclang="en" default />
</video> </video>
``` ```
**Note**: The poster image should be specified using `data-poster`. This is to prevent it [being downloaded twice](https://github.com/sampotts/plyr/issues/1531). If you're sure the image will be cached, you can still use the `poster` attribute for true progressive enhancement.
### HTML5 Audio ### HTML5 Audio
```html ```html
<audio id="player" controls> <audio id="player" controls>
<source src="/path/to/audio.mp3" type="audio/mp3" /> <source src="/path/to/audio.mp3" type="audio/mp3" />
<source src="/path/to/audio.ogg" type="audio/ogg" /> <source src="/path/to/audio.ogg" type="audio/ogg" />
</audio> </audio>
``` ```
@@ -68,12 +70,12 @@ We recommend [progressive enhancement](https://www.smashingmagazine.com/2009/04/
```html ```html
<div class="plyr__video-embed" id="player"> <div class="plyr__video-embed" id="player">
<iframe <iframe
src="https://www.youtube.com/embed/bTqVqk7FSmY?origin=https://plyr.io&amp;iv_load_policy=3&amp;modestbranding=1&amp;playsinline=1&amp;showinfo=0&amp;rel=0&amp;enablejsapi=1" src="https://www.youtube.com/embed/bTqVqk7FSmY?origin=https://plyr.io&amp;iv_load_policy=3&amp;modestbranding=1&amp;playsinline=1&amp;showinfo=0&amp;rel=0&amp;enablejsapi=1"
allowfullscreen allowfullscreen
allowtransparency allowtransparency
allow="autoplay" allow="autoplay"
></iframe> ></iframe>
</div> </div>
``` ```
@@ -93,12 +95,12 @@ Much the same as YouTube above.
```html ```html
<div class="plyr__video-embed" id="player"> <div class="plyr__video-embed" id="player">
<iframe <iframe
src="https://player.vimeo.com/video/76979871?loop=false&amp;byline=false&amp;portrait=false&amp;title=false&amp;speed=true&amp;transparent=0&amp;gesture=media" src="https://player.vimeo.com/video/76979871?loop=false&amp;byline=false&amp;portrait=false&amp;title=false&amp;speed=true&amp;transparent=0&amp;gesture=media"
allowfullscreen allowfullscreen
allowtransparency allowtransparency
allow="autoplay" allow="autoplay"
></iframe> ></iframe>
</div> </div>
``` ```
@@ -123,7 +125,7 @@ Alternatively you can include the `plyr.js` script before the closing `</body>`
```html ```html
<script src="path/to/plyr.js"></script> <script src="path/to/plyr.js"></script>
<script> <script>
const player = new Plyr('#player'); const player = new Plyr('#player');
</script> </script>
``` ```
@@ -132,13 +134,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. 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 ```html
<script src="https://cdn.plyr.io/3.5.10/plyr.js"></script> <script src="https://cdn.plyr.io/3.6.1/plyr.js"></script>
``` ```
...or... ...or...
```html ```html
<script src="https://cdn.plyr.io/3.5.10/plyr.polyfilled.js"></script> <script src="https://cdn.plyr.io/3.6.1/plyr.polyfilled.js"></script>
``` ```
## CSS ## CSS
@@ -152,30 +154,127 @@ Include the `plyr.css` stylsheet into your `<head>`.
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following: If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html ```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.10/plyr.css" /> <link rel="stylesheet" href="https://cdn.plyr.io/3.6.1/plyr.css" />
``` ```
## SVG Sprite ## SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.5.10/plyr.svg`. reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.6.1/plyr.svg`.
# Ads # Ads
Plyr has partnered up with [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) to offer monetization options for your videos. Getting setup is easy: Plyr has partnered up with [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) to offer monetization options for your videos. Getting setup is easy:
- [Sign up for a vi.ai account](https://vi.ai/publisher-video-monetization/?aid=plyrio) - [Sign up for a vi.ai account](https://vi.ai/publisher-video-monetization/?aid=plyrio)
- Grab your publisher ID from the code snippet - Grab your publisher ID from the code snippet
- Enable ads in the [config options](#options) and enter your publisher ID - Enable ads in the [config options](#options) and enter your publisher ID
Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues. Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues.
If you do not wish to use Vi, you can set your own `ads.tagUrl` [option](#options).
# Advanced # Advanced
## SASS ## Customizing the CSS
You can use `bundle.scss` file included in `/src` as part of your build and change variables to suit your design. The SASS require you to If you want to change any design tokens used for the rendering of the player, you can do so using [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties).
use the [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) plugin (you should be already!) as all declarations use the W3C definitions.
Here's a list of the properties and what they are used for:
| Name | Description | Default / Fallback |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `--plyr-color-main` | The primary UI color. | ![#f03c15](https://placehold.it/15/00b3ff/000000?text=+) `#00b3ff` |
| `--plyr-tab-focus-color` | The color used for the dotted outline when an element is `:focus-visible` (equivalent) keyboard focus. | `--plyr-color-main` |
| `--plyr-badge-background` | The background color for badges in the menu. | ![#4a5464](https://placehold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-badge-text-color` | The text color for badges. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-badge-border-radius` | The border radius used for badges. | `2px` |
| `--plyr-tab-focus-color` | The color used to highlight tab (keyboard) focus. | `--plyr-color-main` |
| `--plyr-captions-background` | The color for the background of captions. | `rgba(0, 0, 0, 0.8)` |
| `--plyr-captions-text-color` | The color used for the captions text. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-control-icon-size` | The size of the icons used in the controls. | `18px` |
| `--plyr-control-spacing` | The space between controls (sometimes used in a multiple - e.g. `10px / 2 = 5px`). | `10px` |
| `--plyr-control-padding` | The padding inside controls. | `--plyr-control-spacing * 0.7` (`7px`) |
| `--plyr-control-radius` | The border radius used on controls. | `3px` |
| `--plyr-control-toggle-checked-background` | The background color used for checked menu items. | `--plyr-color-main` |
| `--plyr-video-controls-background` | The background for the video controls. | `linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75))` |
| `--plyr-video-control-color` | The text/icon color for video controls. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-video-control-color-hover` | The text/icon color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-video-control-background-hover` | The background color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | `--plyr-color-main` |
| `--plyr-audio-controls-background` | The background for the audio controls. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-audio-control-color` | The text/icon color for audio controls. | ![#4a5464](https://placehold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-audio-control-color-hover` | The text/icon color used when audio controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-audio-control-background-hover` | The background color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | `--plyr-color-main` |
| `--plyr-menu-background` | The background color for menus. | `rgba(255, 255, 255, 0.9)` |
| `--plyr-menu-color` | The text/icon color for menu items. | ![#4a5464](https://placehold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-menu-shadow` | The shadow used on menus. | `0 1px 2px rgba(0, 0, 0, 0.15)` |
| `--plyr-menu-radius` | The border radius on the menu. | `4px` |
| `--plyr-menu-arrow-size` | The size of the arrow on the bottom of the menu. | `6px` |
| `--plyr-menu-item-arrow-color` | The color of the arrows in the menu. | ![#728197](https://placehold.it/15/728197/000000?text=+) `#728197` |
| `--plyr-menu-item-arrow-size` | The size of the arrows in the menu. | `4px` |
| `--plyr-menu-border-color` | The border color for the bottom of the back button in the top of the sub menu pages. | ![#dcdfe5](https://placehold.it/15/dcdfe5/000000?text=+) `#dcdfe5` |
| `--plyr-menu-border-shadow-color` | The shadow below the border of the back button in the top of the sub menu pages. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-progress-loading-size` | The size of the stripes in the loading state in the scrubber. | `25px` |
| `--plyr-progress-loading-background` | The background color on the loading state in the scrubber. | `rgba(35, 40, 47, 0.6)` |
| `--plyr-video-progress-buffered-background` | The fill color for the buffer indication in the scrubber for video. | `rgba(255, 255, 255, 0.25)` |
| `--plyr-audio-progress-buffered-background` | The fill color for the buffer indication in the scrubber for audio. | `rgba(193, 200, 209, 0.6)` |
| `--plyr-range-thumb-height` | The height of the scrubber handle/thumb. | `13px` |
| `--plyr-range-thumb-background` | The background of the scrubber handle/thumb. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-range-thumb-shadow` | The shadow of the scrubber handle/thumb. | `0 1px 1px rgba(215, 26, 18, 0.15), 0 0 0 1px rgba(215, 26, 18, 0.2)` |
| `--plyr-range-thumb-active-shadow-width` | The width of the shadow when the scrubber handle/thumb is `:active` (pressed). | `3px` |
| `--plyr-range-track-height` | The height of the scrubber/progress track. | `5px` |
| `--plyr-range-fill-background` | The fill color of the scrubber/progress. | `--plyr-color-main` |
| `--plyr-video-range-track-background` | The background of the scrubber/progress. | `--plyr-video-progress-buffered-background` |
| `--plyr-video-range-thumb-active-shadow-color` | The color of the shadow when the video scrubber handle/thumb is `:active` (pressed). | `rgba(255, 255, 255, 0.5)` |
| `--plyr-audio-range-track-background` | The background of the scrubber/progress. | `--plyr-video-progress-buffered-background` |
| `--plyr-audio-range-thumb-active-shadow-color` | The color of the shadow when the audio scrubber handle/thumb is `:active` (pressed). | `rgba(215, 26, 18, 0.1)` |
| `--plyr-tooltip-background` | The background color for tooltips. | `rgba(255, 255, 255, 0.9)` |
| `--plyr-tooltip-color` | The text color for tooltips. | ![#4a5464](https://placehold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-tooltip-padding` | The padding for tooltips. | `calc(var(--plyr-control-spacing) / 2))` |
| `--plyr-tooltip-arrow-size` | The size of the arrow under tooltips. | `4px` |
| `--plyr-tooltip-radius` | The border radius on tooltips. | `3px` |
| `--plyr-tooltip-shadow` | The shadow on tooltips. | `0 1px 2px rgba(0, 0, 0, 0.15)` |
| `--plyr-font-family` | The font family used in the player. | |
| `--plyr-font-size-base` | The base font size. Mainly used for captions. | `15px` |
| `--plyr-font-size-small` | The smaller font size. Mainly used for captions. | `13px` |
| `--plyr-font-size-large` | The larger font size. Mainly used for captions. | `18px` |
| `--plyr-font-size-xlarge` | The even larger font size. Mainly used for captions. | `21px` |
| `--plyr-font-size-time` | The font size for the time. | `--plyr-font-size-small` |
| `--plyr-font-size-menu` | The font size used in the menu. | `--plyr-font-size-small` |
| `--plyr-font-size-badge` | The font size used for badges. | `9px` |
| `--plyr-font-weight-regular` | The regular font weight. | `400` |
| `--plyr-font-weight-bold` | The bold font weight. | `600` |
| `--plyr-line-height` | The line height used within the player. | `1.7` |
| `--plyr-font-smoothing` | Whether to enable font antialiasing within the player. | `false` |
You can set them in your CSS for all players:
```css
:root {
--plyr-color-main: #1ac266;
}
```
...or for a specific class name:
```css
.player {
--plyr-color-main: #1ac266;
}
```
...or in your HTML:
```html
<video class="player" style="--plyr-color-main: #1ac266;">
...
</vieo>
```
### SASS
You can use `plyr.scss` file included in `/src/sass` as part of your build and change variables to suit your design. The SASS requires you to
use [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) (you should be already!) as all declarations use the W3C definitions.
The HTML markup uses the BEM methodology with `plyr` as the block, e.g. `.plyr__controls`. You can change the class hooks in the options to match any custom CSS The HTML markup uses the BEM methodology with `plyr` as the block, e.g. `.plyr__controls`. You can change the class hooks in the options to match any custom CSS
you write. Check out the JavaScript source for more on this. you write. Check out the JavaScript source for more on this.
@@ -213,9 +312,9 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
You can specify a range of arguments for the constructor to use: You can specify a range of arguments for the constructor to use:
- A [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) - A [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
- A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) - A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
- A [jQuery](https://jquery.com) object - A [jQuery](https://jquery.com) object
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [multiple players](#multiple-players) below. _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [multiple players](#multiple-players) below.
@@ -261,7 +360,7 @@ The second argument for the constructor is the [options](#options) object:
```javascript ```javascript
const player = new Plyr('#player', { const player = new Plyr('#player', {
title: 'Example Title', title: 'Example Title',
}); });
``` ```
@@ -277,7 +376,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. | | `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. |
| `debug` | Boolean | `false` | Display debugging information in the console | | `debug` | Boolean | `false` | Display debugging information in the console |
| `controls` | Array, Function or Element | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; `id` (the unique id for the player), `seektime` (the seektime step in seconds), and `title` (the media title). See [controls.md](controls.md) for more info on how the html needs to be structured. | | `controls` | Array, Function or Element | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; `id` (the unique id for the player), `seektime` (the seektime step in seconds), and `title` (the media title). See [CONTROLS.md](CONTROLS.md) for more info on how the html needs to be structured. |
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If the default controls are used, you can specify which settings to show in the menu | | `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If the default controls are used, you can specify which settings to show in the menu |
| `i18n` | Object | See [defaults.js](/src/js/config/defaults.js) | Used for internationalization (i18n) of the text within the UI. | | `i18n` | Object | See [defaults.js](/src/js/config/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
| `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. | | `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. |
@@ -301,13 +400,13 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. | | `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
| `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. | | `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). | | `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) | | `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false, container: null }` | `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). `container`: A selector for an ancestor of the player element, allows contextual content to remain visual in fullscreen mode. Non-ancestors are ignored. |
| `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. | | `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. | | `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`: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically. | | `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically. |
| `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. | | `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. | | `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. | | `ads` | Object | `{ enabled: false, publisherId: '', tagUrl: '' }` | `enabled`: Whether to enable advertisements. `publisherId`: Your unique [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) publisher ID. `tagUrl` is a URL for a custom VAST tag if you're not using Vi. |
| `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. | | `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. |
| `vimeo` | Object | `{ byline: false, portrait: false, title: false, speed: true, transparent: false }` | See [Vimeo embed options](https://github.com/vimeo/player.js/#embed-options). Some are set automatically based on other config options, namely: `loop`, `autoplay`, `muted`, `gesture`, `playsinline` | | `vimeo` | Object | `{ byline: false, portrait: false, title: false, speed: true, transparent: false }` | See [Vimeo embed options](https://github.com/vimeo/player.js/#embed-options). Some are set automatically based on other config options, namely: `loop`, `autoplay`, `muted`, `gesture`, `playsinline` |
| `youtube` | Object | `{ noCookie: false, rel: 0, showinfo: 0, iv_load_policy: 3, modestbranding: 1 }` | See [YouTube embed options](https://developers.google.com/youtube/player_parameters#Parameters). The only custom option is `noCookie` to use an alternative to YouTube that doesn't use cookies (useful for GDPR, etc). Some are set automatically based on other config options, namely: `autoplay`, `hl`, `controls`, `disablekb`, `playsinline`, `cc_load_policy`, `cc_lang_pref`, `widget_referrer` | | `youtube` | Object | `{ noCookie: false, rel: 0, showinfo: 0, iv_load_policy: 3, modestbranding: 1 }` | See [YouTube embed options](https://developers.google.com/youtube/player_parameters#Parameters). The only custom option is `noCookie` to use an alternative to YouTube that doesn't use cookies (useful for GDPR, etc). Some are set automatically based on other config options, namely: `autoplay`, `hl`, `controls`, `disablekb`, `playsinline`, `cc_load_policy`, `cc_lang_pref`, `widget_referrer` |
@@ -316,9 +415,9 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
1. Vimeo only 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: 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://webkit.org/blog/6784/new-video-policies-for-ios/
- https://developers.google.com/web/updates/2017/09/autoplay-policy-changes - 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/ - https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/
# API # API
@@ -330,7 +429,7 @@ The easiest way to access the Plyr object is to set the return value from your c
```javascript ```javascript
const player = new Plyr('#player', { const player = new Plyr('#player', {
/* options */ /* options */
}); });
``` ```
@@ -338,7 +437,7 @@ You can also access the object through any events:
```javascript ```javascript
element.addEventListener('ready', event => { element.addEventListener('ready', event => {
const player = event.detail.plyr; const player = event.detail.plyr;
}); });
``` ```
@@ -351,30 +450,30 @@ player.play(); // Start playback
player.fullscreen.enter(); // Enter fullscreen player.fullscreen.enter(); // Enter fullscreen
``` ```
| Method | Parameters | Description | | Method | Parameters | Description |
| ------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------- | | -------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `play()`&sup1; | - | Start playback. | | `play()`&sup1; | - | Start playback. |
| `pause()` | - | Pause playback. | | `pause()` | - | Pause playback. |
| `togglePlay(toggle)` | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. | | `togglePlay(toggle)`&sup1; | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. |
| `stop()` | - | Stop playback and reset to start. | | `stop()` | - | Stop playback and reset to start. |
| `restart()` | - | Restart playback. | | `restart()` | - | Restart playback. |
| `rewind(seekTime)` | Number | Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used. | | `rewind(seekTime)` | Number | Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used. |
| `forward(seekTime)` | Number | Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used. | | `forward(seekTime)` | Number | Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used. |
| `increaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. | | `increaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
| `decreaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. | | `decreaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
| `toggleCaptions(toggle)` | Boolean | Toggle captions display. If no parameter is passed, it will toggle based on current status. | | `toggleCaptions(toggle)` | Boolean | Toggle captions display. If no parameter is passed, it will toggle based on current status. |
| `fullscreen.enter()` | - | Enter fullscreen. If fullscreen is not supported, a fallback "full window/viewport" is used instead. | | `fullscreen.enter()` | - | Enter fullscreen. If fullscreen is not supported, a fallback "full window/viewport" is used instead. |
| `fullscreen.exit()` | - | Exit fullscreen. | | `fullscreen.exit()` | - | Exit fullscreen. |
| `fullscreen.toggle()` | - | Toggle fullscreen. | | `fullscreen.toggle()` | - | Toggle fullscreen. |
| `airplay()` | - | Trigger the airplay dialog on supported devices. | | `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. | | `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
| `on(event, function)` | String, Function | Add an event listener for the specified event. | | `on(event, function)` | String, Function | Add an event listener for the specified event. |
| `once(event, function)` | String, Function | Add an event listener for the specified event once. | | `once(event, function)` | String, Function | Add an event listener for the specified event once. |
| `off(event, function)` | String, Function | Remove an event listener for the specified event. | | `off(event, function)` | String, Function | Remove an event listener for the specified event. |
| `supports(type)` | String | Check support for a mime type. | | `supports(type)` | String | Check support for a mime type. |
| `destroy()` | - | Destroy the instance and garbage collect any elements. | | `destroy()` | - | Destroy the instance and garbage collect any elements. |
1. For HTML5 players, `play()` will return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) in _some_ browsers - WebKit and Mozilla [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing. 1. For HTML5 players, `play()` will return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) for most browsers - e.g. Chrome, Firefox, Opera, Safari and Edge [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing.
## Getters and Setters ## Getters and Setters
@@ -432,39 +531,39 @@ Video example:
```javascript ```javascript
player.source = { player.source = {
type: 'video', type: 'video',
title: 'Example title', title: 'Example title',
sources: [ sources: [
{ {
src: '/path/to/movie.mp4', src: '/path/to/movie.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 720, size: 720,
},
{
src: '/path/to/movie.webm',
type: 'video/webm',
size: 1080,
},
],
poster: '/path/to/poster.jpg',
previewThumbnails: {
src: '/path/to/thumbnails.vtt'
}, },
tracks: [ {
{ src: '/path/to/movie.webm',
kind: 'captions', type: 'video/webm',
label: 'English', size: 1080,
srclang: 'en', },
src: '/path/to/captions.en.vtt', ],
default: true, poster: '/path/to/poster.jpg',
}, previewThumbnails: {
{ src: '/path/to/thumbnails.vtt',
kind: 'captions', },
label: 'French', tracks: [
srclang: 'fr', {
src: '/path/to/captions.fr.vtt', kind: 'captions',
}, label: 'English',
], srclang: 'en',
src: '/path/to/captions.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: '/path/to/captions.fr.vtt',
},
],
}; };
``` ```
@@ -472,18 +571,18 @@ Audio example:
```javascript ```javascript
player.source = { player.source = {
type: 'audio', type: 'audio',
title: 'Example title', title: 'Example title',
sources: [ sources: [
{ {
src: '/path/to/audio.mp3', src: '/path/to/audio.mp3',
type: 'audio/mp3', type: 'audio/mp3',
}, },
{ {
src: '/path/to/audio.ogg', src: '/path/to/audio.ogg',
type: 'audio/ogg', type: 'audio/ogg',
}, },
], ],
}; };
``` ```
@@ -491,29 +590,27 @@ YouTube example:
```javascript ```javascript
player.source = { player.source = {
type: 'video', type: 'video',
sources: [ sources: [
{ {
src: 'bTqVqk7FSmY', src: 'bTqVqk7FSmY',
provider: 'youtube', provider: 'youtube',
}, },
], ],
}; };
``` ```
_Note_: `src` can be the video ID or URL
Vimeo example Vimeo example
```javascript ```javascript
player.source = { player.source = {
type: 'video', type: 'video',
sources: [ sources: [
{ {
src: '143418951', src: '143418951',
provider: 'vimeo', provider: 'vimeo',
}, },
], ],
}; };
``` ```
@@ -538,7 +635,7 @@ property. Here's an example:
```javascript ```javascript
player.on('ready', event => { player.on('ready', event => {
const instance = event.detail.plyr; const instance = event.detail.plyr;
}); });
``` ```
@@ -597,8 +694,8 @@ YouTube and Vimeo are currently supported and function much like a HTML5 video.
to access the API's directly. You can do so via the `embed` property of your player object - e.g. `player.embed`. You can then use the relevant methods from the to access the API's directly. You can do so via the `embed` property of your player object - e.g. `player.embed`. You can then use the relevant methods from the
third party APIs. More info on the respective API's here: third party APIs. More info on the respective API's here:
- [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference) - [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference)
- [Vimeo player.js Reference](https://github.com/vimeo/player.js) - [Vimeo player.js Reference](https://github.com/vimeo/player.js)
_Note_: Not all API methods may work 100%. Your mileage may vary. It's better to use the Plyr API where possible. _Note_: Not all API methods may work 100%. Your mileage may vary. It's better to use the Plyr API where possible.
@@ -664,9 +761,9 @@ const supported = Plyr.supported('video', 'html5', true);
The arguments are: The arguments are:
- Media type (`audio` or `video`) - Media type (`audio` or `video`)
- Provider (`html5`, `youtube` or `vimeo`) - Provider (`html5`, `youtube` or `vimeo`)
- Whether the player has the `playsinline` attribute (only applicable to iOS 10+) - Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
## Disable support programmatically ## Disable support programmatically
@@ -674,7 +771,7 @@ The `enabled` option can be used to disable certain User Agents. For example, if
```javascript ```javascript
{ {
enabled: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent); enabled: !/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
} }
``` ```
@@ -707,42 +804,42 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated... Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated...
- [Donate via Patreon](https://www.patreon.com/plyr) - [Donate via Patreon](https://www.patreon.com/plyr)
- [Donate via PayPal](https://www.paypal.me/pottsy/20usd) - [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
# Mentions # Mentions
- [ProductHunt](https://www.producthunt.com/tech/plyr) - [ProductHunt](https://www.producthunt.com/tech/plyr)
- [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/) - [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/)
- [HTML5 Weekly #177](http://html5weekly.com/issues/177) - [HTML5 Weekly #177](http://html5weekly.com/issues/177)
- [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f) - [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f)
- [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/) - [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/)
- [Front End Focus #177](https://frontendfoc.us/issues/177) - [Front End Focus #177](https://frontendfoc.us/issues/177)
- [Hacker News](https://news.ycombinator.com/item?id=9136774) - [Hacker News](https://news.ycombinator.com/item?id=9136774)
- [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04) - [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04)
- [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player) - [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player)
- [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images) - [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images)
- [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html) - [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html)
# Used by # Used by
- [Selz.com](https://selz.com) - [Selz.com](https://selz.com)
- [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html) - [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html)
- [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html) - [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html)
- [TomTom.com](http://prioritydriving.tomtom.com/) - [TomTom.com](http://prioritydriving.tomtom.com/)
- [DIGBMX](http://digbmx.com/) - [DIGBMX](http://digbmx.com/)
- [Grime Archive](https://grimearchive.com/) - [Grime Archive](https://grimearchive.com/)
- [koel - A personal music streaming server that works.](http://koel.phanan.net/) - [koel - A personal music streaming server that works.](http://koel.phanan.net/)
- [Oscar Radio](http://oscar-radio.xyz/) - [Oscar Radio](http://oscar-radio.xyz/)
- [Sparkk TV](https://www.sparkktv.com/) - [Sparkk TV](https://www.sparkktv.com/)
- [@halfhalftravel](https://www.halfhalftravel.com/) - [@halfhalftravel](https://www.halfhalftravel.com/)
If you want to be added to the list, open a pull request. It'd be awesome to see how you're using Plyr 😎 If you want to be added to the list, open a pull request. It'd be awesome to see how you're using Plyr 😎
# Useful links and credits # Useful links and credits
- [PayPal's Accessible HTML5 Video Player (which Plyr was originally ported from)](https://github.com/paypal/accessible-html5-video-player) - [PayPal's Accessible HTML5 Video Player (which Plyr was originally ported from)](https://github.com/paypal/accessible-html5-video-player)
- [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw) - [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw)
# Thanks # Thanks
@@ -777,7 +874,6 @@ Support this project with your organization. Your logo will show up here with a
<a href="https://opencollective.com/plyr/organization/0/website"><img src="https://opencollective.com/plyr/organization/0/avatar.svg"></a> <a href="https://opencollective.com/plyr/organization/0/website"><img src="https://opencollective.com/plyr/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/plyr/organization/1/website"><img src="https://opencollective.com/plyr/organization/1/avatar.svg"></a><a href="https://opencollective.com/plyr/organization/2/website"><img src="https://opencollective.com/plyr/organization/2/avatar.svg"></a> <a href="https://opencollective.com/plyr/organization/1/website"><img src="https://opencollective.com/plyr/organization/1/avatar.svg"></a><a href="https://opencollective.com/plyr/organization/2/website"><img src="https://opencollective.com/plyr/organization/2/avatar.svg"></a>
# Copyright and License # Copyright and License
[The MIT license](license.md) [The MIT license](LICENSE.md)
-1153
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+9254 -5614
View File
File diff suppressed because it is too large Load Diff
+17 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+243 -253
View File
@@ -1,278 +1,268 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player</title> <title>Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player</title>
<meta <meta
name="description" name="description"
property="og:description" property="og:description"
content="A simple HTML5 media player with custom controls and WebVTT captions." content="A simple HTML5 media player with custom controls and WebVTT captions."
/> />
<meta name="author" content="Sam Potts" /> <meta name="author" content="Sam Potts" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Icons --> <!-- Icons -->
<link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico" /> <link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico" />
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16" /> <link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16" />
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png" /> <link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png" />
<!-- Open Graph --> <!-- Open Graph -->
<meta <meta property="og:title" content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player" />
property="og:title" <meta property="og:site_name" content="Plyr" />
content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player" <meta property="og:url" content="https://plyr.io" />
/> <meta property="og:image" content="https://cdn.plyr.io/static/icons/1200x630.png" />
<meta property="og:site_name" content="Plyr" />
<meta property="og:url" content="https://plyr.io" />
<meta property="og:image" content="https://cdn.plyr.io/static/icons/1200x630.png" />
<!-- Twitter --> <!-- Twitter -->
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@sam_potts" /> <meta name="twitter:site" content="@sam_potts" />
<meta name="twitter:creator" content="@sam_potts" /> <meta name="twitter:creator" content="@sam_potts" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<!-- Docs styles --> <!-- Docs styles -->
<link rel="stylesheet" href="dist/demo.css" /> <link rel="stylesheet" href="dist/demo.css" />
<!-- Preload --> <!-- Preload -->
<link <link
rel="preload" rel="preload"
as="font" as="font"
crossorigin crossorigin
type="font/woff2" type="font/woff2"
href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2"
/> />
<link <link
rel="preload" rel="preload"
as="font" as="font"
crossorigin crossorigin
type="font/woff2" type="font/woff2"
href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2" href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2"
/> />
<!-- Google Analytics--> <!-- Google Analytics-->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-132699580-1"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=UA-132699580-1"></script>
<script> <script>
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag() { function gtag() {
dataLayer.push(arguments); dataLayer.push(arguments);
} }
gtag('js', new Date()); gtag('js', new Date());
gtag('config', 'UA-132699580-1'); gtag('config', 'UA-132699580-1');
</script> </script>
</head> </head>
<body> <body>
<div class="grid"> <div class="grid">
<header> <header>
<h1>Pl<span>a</span>y<span>e</span>r</h1> <h1>Pl<span>a</span>y<span>e</span>r</h1>
<p> <p>
A simple, accessible and customisable media player for A simple, accessible and customisable media player for
<button type="button" class="faux-link" data-source="video"> <button type="button" class="faux-link" data-source="video">
<svg class="icon"> <svg class="icon">
<title>HTML5</title> <title>HTML5</title>
<path <path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z" d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path></svg ></path></svg
>Video</button >Video</button
>, >,
<button type="button" class="faux-link" data-source="audio"> <button type="button" class="faux-link" data-source="audio">
<svg class="icon"> <svg class="icon">
<title>HTML5</title> <title>HTML5</title>
<path <path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z" d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path></svg ></path></svg
>Audio</button >Audio</button
>, >,
<button type="button" class="faux-link" data-source="youtube"> <button type="button" class="faux-link" data-source="youtube">
<svg class="icon" role="presentation"> <svg class="icon" role="presentation">
<title>YouTube</title> <title>YouTube</title>
<path <path
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8 d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z" M6,11V5l5,3L6,11z"
></path></svg ></path></svg
>YouTube >YouTube
</button> </button>
and and
<button type="button" class="faux-link" data-source="vimeo"> <button type="button" class="faux-link" data-source="vimeo">
<svg class="icon" role="presentation"> <svg class="icon" role="presentation">
<title>Vimeo</title> <title>Vimeo</title>
<path <path
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5 d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4 C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z" c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
></path></svg ></path></svg
>Vimeo >Vimeo
</button> </button>
</p> </p>
<p> <p>
Premium video monetization from Premium video monetization from
<a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border"> <a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi" /> <img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi" />
<span class="sr-only">ai.vi</span> <span class="sr-only">ai.vi</span>
</a> </a>
</p> </p>
<div class="call-to-action"> <div class="call-to-action">
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr"> <a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr">
<svg class="icon" role="presentation"> <svg class="icon" role="presentation">
<title>GitHub</title> <title>GitHub</title>
<path <path
d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6 d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6
C5.9,15.9,6,15.6,6,15.4c0-0.2,0-0.7,0-1.4C3.8,14.5,3.3,13,3.3,13c-0.4-0.9-0.9-1.2-0.9-1.2c-0.7-0.5,0.1-0.5,0.1-0.5 C5.9,15.9,6,15.6,6,15.4c0-0.2,0-0.7,0-1.4C3.8,14.5,3.3,13,3.3,13c-0.4-0.9-0.9-1.2-0.9-1.2c-0.7-0.5,0.1-0.5,0.1-0.5
c0.8,0.1,1.2,0.8,1.2,0.8C4.4,13.4,5.6,13,6,12.8c0.1-0.5,0.3-0.9,0.5-1.1c-1.8-0.2-3.6-0.9-3.6-4c0-0.9,0.3-1.6,0.8-2.1 c0.8,0.1,1.2,0.8,1.2,0.8C4.4,13.4,5.6,13,6,12.8c0.1-0.5,0.3-0.9,0.5-1.1c-1.8-0.2-3.6-0.9-3.6-4c0-0.9,0.3-1.6,0.8-2.1
c-0.1-0.2-0.4-1,0.1-2.1c0,0,0.7-0.2,2.2,0.8c0.6-0.2,1.3-0.3,2-0.3c0.7,0,1.4,0.1,2,0.3c1.5-1,2.2-0.8,2.2-0.8 c-0.1-0.2-0.4-1,0.1-2.1c0,0,0.7-0.2,2.2,0.8c0.6-0.2,1.3-0.3,2-0.3c0.7,0,1.4,0.1,2,0.3c1.5-1,2.2-0.8,2.2-0.8
c0.4,1.1,0.2,1.9,0.1,2.1c0.5,0.6,0.8,1.3,0.8,2.1c0,3.1-1.9,3.7-3.7,3.9C9.7,12,10,12.5,10,13.2c0,1.1,0,1.9,0,2.2 c0.4,1.1,0.2,1.9,0.1,2.1c0.5,0.6,0.8,1.3,0.8,2.1c0,3.1-1.9,3.7-3.7,3.9C9.7,12,10,12.5,10,13.2c0,1.1,0,1.9,0,2.2
c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z" c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z"
></path> ></path>
</svg> </svg>
Download on GitHub Download on GitHub
</a> </a>
</div> </div>
</header> </header>
<main>
<div id="container">
<video
controls
crossorigin
playsinline
data-poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg"
id="player"
>
<!-- Video files -->
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
type="video/mp4"
size="576"
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4"
type="video/mp4"
size="720"
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4"
type="video/mp4"
size="1080"
/>
<main> <!-- Caption files -->
<div id="container"> <track
<video kind="captions"
controls label="English"
crossorigin srclang="en"
playsinline src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" default
id="player" />
> <track
<!-- Video files --> kind="captions"
<source label="Français"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" srclang="fr"
type="video/mp4" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"
size="576" />
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4"
type="video/mp4"
size="720"
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4"
type="video/mp4"
size="1080"
/>
<!-- Caption files --> <!-- Fallback for browsers that don't support the <video> element -->
<track <a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
kind="captions" </video>
label="English"
srclang="en"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default
/>
<track
kind="captions"
label="Français"
srclang="fr"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"
/>
<!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download
>Download</a
>
</video>
</div>
<ul>
<li class="plyr__cite plyr__cite--video" hidden>
<small>
<svg class="icon">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a
href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323"
target="_blank"
>View From A Blue Moon</a
>
&copy; Brainfarm
</small>
</li>
<li class="plyr__cite plyr__cite--audio" hidden>
<small>
<svg class="icon" title="HTML5">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a href="http://www.kishibashi.com/" target="_blank"
>Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;</a
>
&copy; Kishi Bashi
</small>
</li>
<li class="plyr__cite plyr__cite--youtube" hidden>
<small>
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank"
>View From A Blue Moon</a
>
on&nbsp;
<span class="color--youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z"
></path></svg
>YouTube
</span>
</small>
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/40648169" target="_blank">Toob “Wavaphon” Music Video</a>
on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
></path></svg
>Vimeo
</span>
</small>
</li>
</ul>
</main>
</div> </div>
<aside> <ul>
<svg class="icon"> <li class="plyr__cite plyr__cite--video" hidden>
<title>Twitter</title> <small>
<svg class="icon">
<title>HTML5</title>
<path <path
d="M16,3c-0.6,0.3-1.2,0.4-1.9,0.5c0.7-0.4,1.2-1,1.4-1.8c-0.6,0.4-1.3,0.6-2.1,0.8c-0.6-0.6-1.5-1-2.4-1 d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank"
>View From A Blue Moon</a
>
&copy; Brainfarm
</small>
</li>
<li class="plyr__cite plyr__cite--audio" hidden>
<small>
<svg class="icon" title="HTML5">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a href="http://www.kishibashi.com/" target="_blank"
>Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;</a
>
&copy; Kishi Bashi
</small>
</li>
<li class="plyr__cite plyr__cite--youtube" hidden>
<small>
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank">View From A Blue Moon</a>
on&nbsp;
<span class="color--youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z"
></path></svg
>YouTube
</span>
</small>
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/40648169" target="_blank">Toob “Wavaphon” Music Video</a>
on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
></path></svg
>Vimeo
</span>
</small>
</li>
</ul>
</main>
</div>
<aside>
<svg class="icon">
<title>Twitter</title>
<path
d="M16,3c-0.6,0.3-1.2,0.4-1.9,0.5c0.7-0.4,1.2-1,1.4-1.8c-0.6,0.4-1.3,0.6-2.1,0.8c-0.6-0.6-1.5-1-2.4-1
C9.3,1.5,7.8,3,7.8,4.8c0,0.3,0,0.5,0.1,0.7C5.2,5.4,2.7,4.1,1.1,2.1c-0.3,0.5-0.4,1-0.4,1.7c0,1.1,0.6,2.1,1.5,2.7 C9.3,1.5,7.8,3,7.8,4.8c0,0.3,0,0.5,0.1,0.7C5.2,5.4,2.7,4.1,1.1,2.1c-0.3,0.5-0.4,1-0.4,1.7c0,1.1,0.6,2.1,1.5,2.7
c-0.5,0-1-0.2-1.5-0.4c0,0,0,0,0,0c0,1.6,1.1,2.9,2.6,3.2C3,9.4,2.7,9.4,2.4,9.4c-0.2,0-0.4,0-0.6-0.1c0.4,1.3,1.6,2.3,3.1,2.3 c-0.5,0-1-0.2-1.5-0.4c0,0,0,0,0,0c0,1.6,1.1,2.9,2.6,3.2C3,9.4,2.7,9.4,2.4,9.4c-0.2,0-0.4,0-0.6-0.1c0.4,1.3,1.6,2.3,3.1,2.3
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z" c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"
></path> ></path>
</svg> </svg>
<p> <p>
If you think Plyr's good, If you think Plyr's good,
<a <a
href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts" href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank" target="_blank"
class="js-shr" class="js-shr"
>tweet it</a >tweet it</a
> >
👍 👍
</p> </p>
</aside> </aside>
<script src="dist/demo.js" crossorigin="anonymous"></script> <script src="dist/demo.js" crossorigin="anonymous"></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -1,14 +1,14 @@
{ {
"name": "plyr-demo", "name": "plyr-demo",
"version": "1.0.0", "version": "1.0.0",
"description": "Demo for Plyr", "description": "Demo for Plyr",
"homepage": "https://plyr.io", "homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>", "author": "Sam Potts <sam@potts.es>",
"dependencies": { "dependencies": {
"core-js": "^3.6.4", "@sentry/browser": "^5.15.5",
"custom-event-polyfill": "^1.0.7", "core-js": "^3.6.5",
"raven-js": "^3.27.2", "custom-event-polyfill": "^1.0.7",
"shr-buttons": "2.0.3", "shr-buttons": "2.0.3",
"url-polyfill": "^1.1.8" "url-polyfill": "^1.1.8"
} }
} }
+126 -137
View File
@@ -1,14 +1,14 @@
// ========================================================================== // ==========================================================================
// Plyr.io demo // Plyr.io demo
// This code is purely for the https://plyr.io website // This code is purely for the https://plyr.io website
// Please see readme.md in the root or github.com/sampotts/plyr // Please see README.md in the root or github.com/sampotts/plyr
// ========================================================================== // ==========================================================================
import './tab-focus'; import './tab-focus';
import 'custom-event-polyfill'; import 'custom-event-polyfill';
import 'url-polyfill'; import 'url-polyfill';
import Raven from 'raven-js'; import * as Sentry from '@sentry/browser';
import Shr from 'shr-buttons'; import Shr from 'shr-buttons';
import Plyr from '../../../src/js/plyr'; import Plyr from '../../../src/js/plyr';
@@ -16,145 +16,134 @@ import sources from './sources';
import toggleClass from './toggle-class'; import toggleClass from './toggle-class';
(() => { (() => {
const { host } = window.location; const production = 'plyr.io';
const env = {
prod: host === 'plyr.io',
dev: host === 'dev.plyr.io',
};
document.addEventListener('DOMContentLoaded', () => { // Sentry for demo site (https://plyr.io) only
Raven.context(() => { if (window.location.host === production) {
const selector = '#player'; Sentry.init({
dsn: 'https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555',
whitelistUrls: [production].map(d => new RegExp(`https://(([a-z0-9])+(.))*${d}`)),
});
}
// Setup share buttons document.addEventListener('DOMContentLoaded', () => {
Shr.setup('.js-shr', { const selector = '#player';
count: {
className: 'button__count',
},
wrapper: {
className: 'button--with-count',
},
});
// Setup the player // Setup share buttons
const player = new Plyr(selector, { Shr.setup('.js-shr', {
debug: true, count: {
title: 'View From A Blue Moon', className: 'button__count',
iconUrl: 'dist/demo.svg', },
keyboard: { wrapper: {
global: true, className: 'button--with-count',
}, },
tooltips: {
controls: true,
},
captions: {
active: true,
},
ads: {
enabled: env.prod || env.dev,
publisherId: '918848828995742',
},
previewThumbnails: {
enabled: true,
src: [
'https://cdn.plyr.io/static/demo/thumbs/100p.vtt',
'https://cdn.plyr.io/static/demo/thumbs/240p.vtt',
],
},
vimeo: {
// Prevent Vimeo blocking plyr.io demo site
referrerPolicy: 'no-referrer',
},
});
// Expose for tinkering in the console
window.player = player;
// Setup type toggle
const buttons = document.querySelectorAll('[data-source]');
const types = Object.keys(sources);
const historySupport = Boolean(window.history && window.history.pushState);
let currentType = window.location.hash.substring(1);
const hasCurrentType = !currentType.length;
function render(type) {
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
// eslint-disable-next-line no-param-reassign
cite.hidden = true;
});
document.querySelector(`.plyr__cite--${type}`).hidden = false;
}
// Set a new source
function setSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (
!types.includes(type) ||
(!init && type === currentType) ||
(!currentType.length && type === 'video')
) {
return;
}
// Set the new source
player.source = sources[type];
// Set the current type for next time
currentType = type;
render(type);
}
// Bind to each button
Array.from(buttons).forEach(button => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
setSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && Object.keys(event.state).includes('type')) {
setSource(event.state.type);
}
});
// If there's no current type set, assume video
if (hasCurrentType) {
currentType = 'video';
}
// Replace current history state
if (historySupport && types.includes(currentType)) {
window.history.replaceState({ type: currentType }, '', hasCurrentType ? '' : `#${currentType}`);
}
// If it's not video, load the source
if (currentType !== 'video') {
setSource(currentType, true);
}
render(currentType);
});
}); });
// Raven / Sentry // Setup the player
// For demo site (https://plyr.io) only const player = new Plyr(selector, {
if (env.prod) { debug: true,
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install(); title: 'View From A Blue Moon',
iconUrl: 'dist/demo.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
ads: {
enabled: window.location.host.includes(production),
publisherId: '918848828995742',
},
previewThumbnails: {
enabled: true,
src: ['https://cdn.plyr.io/static/demo/thumbs/100p.vtt', 'https://cdn.plyr.io/static/demo/thumbs/240p.vtt'],
},
vimeo: {
// Prevent Vimeo blocking plyr.io demo site
referrerPolicy: 'no-referrer',
},
});
// Expose for tinkering in the console
window.player = player;
// Setup type toggle
const buttons = document.querySelectorAll('[data-source]');
const types = Object.keys(sources);
const historySupport = Boolean(window.history && window.history.pushState);
let currentType = window.location.hash.substring(1);
const hasInitialType = currentType.length;
function render(type) {
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
// eslint-disable-next-line no-param-reassign
cite.hidden = true;
});
document.querySelector(`.plyr__cite--${type}`).hidden = false;
} }
// Set a new source
function setSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (!types.includes(type) || (!init && type === currentType) || (!currentType.length && type === 'video')) {
return;
}
// Set the new source
player.source = sources[type];
// Set the current type for next time
currentType = type;
render(type);
}
// Bind to each button
Array.from(buttons).forEach(button => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
setSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && Object.keys(event.state).includes('type')) {
setSource(event.state.type);
}
});
// If there's no current type set, assume video
if (!hasInitialType) {
currentType = 'video';
}
// Replace current history state
if (historySupport && types.includes(currentType)) {
window.history.replaceState({ type: currentType }, '', hasInitialType ? `#${currentType}` : '');
}
// If it's not video, load the source
if (currentType !== 'video') {
setSource(currentType, true);
}
render(currentType);
});
})(); })();
+76 -73
View File
@@ -1,78 +1,81 @@
const sources = { const sources = {
video: { video: {
type: 'video', type: 'video',
title: 'View From A Blue Moon', title: 'View From A Blue Moon',
sources: [ sources: [
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4', src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 576, size: 576,
}, },
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4', src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 720, size: 720,
}, },
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4', src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 1080, size: 1080,
}, },
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4', src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 1440, size: 1440,
}, },
], ],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [ tracks: [
{ {
kind: 'captions', kind: 'captions',
label: 'English', label: 'English',
srclang: 'en', srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt', src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true, default: true,
}, },
{ {
kind: 'captions', kind: 'captions',
label: 'French', label: 'French',
srclang: 'fr', srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
}, },
], ],
}, previewThumbnails: {
audio: { src: ['https://cdn.plyr.io/static/demo/thumbs/100p.vtt', 'https://cdn.plyr.io/static/demo/thumbs/240p.vtt'],
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
},
youtube: {
type: 'video',
sources: [
{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
},
],
},
vimeo: {
type: 'video',
sources: [
{
src: 'https://vimeo.com/40648169',
provider: 'vimeo',
},
],
}, },
},
audio: {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
},
youtube: {
type: 'video',
sources: [
{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
},
],
},
vimeo: {
type: 'video',
sources: [
{
src: 'https://vimeo.com/40648169',
provider: 'vimeo',
},
],
},
}; };
export default sources; export default sources;
+17 -17
View File
@@ -4,28 +4,28 @@ const tabClassName = 'tab-focus';
// Remove class on blur // Remove class on blur
document.addEventListener('focusout', event => { document.addEventListener('focusout', event => {
if (!event.target.classList || container.contains(event.target)) { if (!event.target.classList || container.contains(event.target)) {
return; return;
} }
event.target.classList.remove(tabClassName); event.target.classList.remove(tabClassName);
}); });
// Add classname to tabbed elements // Add classname to tabbed elements
document.addEventListener('keydown', event => { document.addEventListener('keydown', event => {
if (event.keyCode !== 9) { if (event.keyCode !== 9) {
return; return;
}
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
const focused = document.activeElement;
if (!focused || !focused.classList || container.contains(focused)) {
return;
} }
// Delay the adding of classname until the focus has changed focused.classList.add(tabClassName);
// This event fires before the focusin event }, 10);
setTimeout(() => {
const focused = document.activeElement;
if (!focused || !focused.classList || container.contains(focused)) {
return;
}
focused.classList.add(tabClassName);
}, 10);
}); });
+1 -1
View File
@@ -1,5 +1,5 @@
// Toggle class on an element // Toggle class on an element
const toggleClass = (element, className = '', toggle = false) => const toggleClass = (element, className = '', toggle = false) =>
element && element.classList[toggle ? 'add' : 'remove'](className); element && element.classList[toggle ? 'add' : 'remove'](className);
export default toggleClass; export default toggleClass;
+3
View File
@@ -3,6 +3,9 @@
// ========================================================================== // ==========================================================================
@charset 'UTF-8'; @charset 'UTF-8';
@import '../../../../src/sass/lib/css-vars';
$css-vars-use-native: true;
// Settings // Settings
@import '../settings/breakpoints'; @import '../settings/breakpoints';
@import '../settings/colors'; @import '../settings/colors';
+55 -55
View File
@@ -5,80 +5,80 @@
// Shared // Shared
.button, .button,
.button__count { .button__count {
align-items: center; align-items: center;
border: 0; border: 0;
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 1px 1px rgba(#000, 0.1); box-shadow: 0 1px 1px rgba(#000, 0.1);
display: inline-flex; display: inline-flex;
padding: ($spacing-base * 0.75); padding: ($spacing-base * 0.75);
position: relative; position: relative;
text-shadow: none; text-shadow: none;
user-select: none; user-select: none;
vertical-align: middle; vertical-align: middle;
} }
// Buttons // Buttons
.button { .button {
background: $color-button-background; background: $color-button-background;
color: $color-button-text; color: $color-button-text;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
padding-left: ($spacing-base * 1.25); padding-left: ($spacing-base * 1.25);
padding-right: ($spacing-base * 1.25); padding-right: ($spacing-base * 1.25);
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover, &:hover,
&:focus { &:focus {
background: $color-button-background-hover; background: $color-button-background-hover;
// Remove the underline/border // Remove the underline/border
&::after { &::after {
display: none; display: none;
}
} }
}
&:hover { &:hover {
box-shadow: 0 2px 2px rgba(#000, 0.1); box-shadow: 0 2px 2px rgba(#000, 0.1);
} }
&:focus { &:focus {
outline: 0; outline: 0;
} }
&.tab-focus { &.tab-focus {
@include tab-focus(); @include tab-focus();
} }
&:active { &:active {
top: 1px; top: 1px;
} }
} }
// Button group // Button group
.button--with-count { .button--with-count {
display: inline-flex; display: inline-flex;
.button .icon { .button .icon {
flex-shrink: 0; flex-shrink: 0;
} }
} }
// Count bubble // Count bubble
.button__count { .button__count {
animation: fadein 0.2s ease; animation: fadein 0.2s ease;
background: $color-button-count-background; background: $color-button-count-background;
color: $color-button-count-text; color: $color-button-count-text;
margin-left: ($spacing-base * 0.75); margin-left: ($spacing-base * 0.75);
&::before { &::before {
border: $arrow-size solid transparent; border: $arrow-size solid transparent;
border-left-width: 0; border-left-width: 0;
border-right-color: $color-button-count-background; border-right-color: $color-button-count-background;
content: ''; content: '';
height: 0; height: 0;
position: absolute; position: absolute;
right: 100%; right: 100%;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
width: 0; width: 0;
} }
} }
+22 -22
View File
@@ -3,28 +3,28 @@
// ========================================================================== // ==========================================================================
header { header {
padding-bottom: $spacing-base; padding-bottom: $spacing-base;
text-align: center; text-align: center;
h1 span { h1 span {
animation: shrinkHide 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s forwards; animation: shrinkHide 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s forwards;
display: inline-block; display: inline-block;
font-weight: $font-weight-light; font-weight: $font-weight-light;
opacity: 0.5; opacity: 0.5;
} }
.call-to-action { .call-to-action {
margin-top: ($spacing-base * 1.5); margin-top: ($spacing-base * 1.5);
} }
@media only screen and (min-width: $screen-md) { @media only screen and (min-width: $screen-md) {
margin-right: ($spacing-base * 3); margin-right: ($spacing-base * 3);
max-width: 360px; max-width: 360px;
padding-bottom: ($spacing-base * 2); padding-bottom: ($spacing-base * 2);
text-align: left; text-align: left;
p:first-of-type { p:first-of-type {
@include font-size($font-size-base + 1); @include font-size($font-size-base + 1);
}
} }
}
} }
+6 -6
View File
@@ -4,20 +4,20 @@
// Base size icon styles // Base size icon styles
.icon { .icon {
fill: currentColor; fill: currentColor;
height: $icon-size; height: $icon-size;
vertical-align: -3px; vertical-align: -3px;
width: $icon-size; width: $icon-size;
} }
// Within elements // Within elements
a svg, a svg,
button svg, button svg,
label svg { label svg {
pointer-events: none; pointer-events: none;
} }
a .icon, a .icon,
.btn .icon { .btn .icon {
margin-right: ($spacing-base / 2); margin-right: ($spacing-base / 2);
} }
+32 -32
View File
@@ -4,45 +4,45 @@
// Make a <button> look like an <a> // Make a <button> look like an <a>
button.faux-link { button.faux-link {
@extend a; // stylelint-disable-line @extend a; // stylelint-disable-line
@include cancel-button-styles(); @include cancel-button-styles();
} }
// Links // Links
a { a {
border-bottom: 1px dotted currentColor; border-bottom: 1px dotted currentColor;
color: $color-link; color: $color-link;
position: relative; position: relative;
text-decoration: none; text-decoration: none;
transition: all 0.2s ease; transition: all 0.2s ease;
&::after {
background: currentColor;
content: '';
height: 1px;
left: 50%;
position: absolute;
top: 100%;
transform: translateX(-50%);
transition: width 0.2s ease;
width: 0;
}
&:hover,
&:focus {
border-bottom-color: transparent;
outline: 0;
&::after { &::after {
background: currentColor; width: 100%;
content: '';
height: 1px;
left: 50%;
position: absolute;
top: 100%;
transform: translateX(-50%);
transition: width 0.2s ease;
width: 0;
} }
}
&:hover, &.tab-focus {
&:focus { @include tab-focus();
border-bottom-color: transparent; }
outline: 0;
&::after { &.no-border::after {
width: 100%; display: none;
} }
}
&.tab-focus {
@include tab-focus();
}
&.no-border::after {
display: none;
}
} }
+3 -3
View File
@@ -5,7 +5,7 @@
// Lists // Lists
ul, ul,
li { li {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
+2 -2
View File
@@ -5,6 +5,6 @@
img, img,
video, video,
audio { audio {
max-width: 100%; max-width: 100%;
vertical-align: middle; vertical-align: middle;
} }
+3 -3
View File
@@ -3,7 +3,7 @@
// ========================================================================== // ==========================================================================
nav { nav {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-bottom: $spacing-base; margin-bottom: $spacing-base;
} }
+20 -20
View File
@@ -4,33 +4,33 @@
// Example players // Example players
.plyr { .plyr {
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 2px 15px rgba(#000, 0.1); box-shadow: 0 2px 15px rgba(#000, 0.1);
margin: $spacing-base auto; margin: $spacing-base auto;
&.plyr--audio { &.plyr--audio {
max-width: 480px; max-width: 480px;
} }
} }
.plyr__video-wrapper::after { .plyr__video-wrapper::after {
border: 1px solid rgba(#000, 0.15); border: 1px solid rgba(#000, 0.15);
border-radius: inherit; border-radius: inherit;
bottom: 0; bottom: 0;
content: ''; content: '';
left: 0; left: 0;
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
z-index: 3; z-index: 3;
} }
// Style full supported player // Style full supported player
.plyr__cite { .plyr__cite {
color: $color-gray-5; color: $color-gray-500;
.icon { .icon {
margin-right: ceil($spacing-base / 6); margin-right: ceil($spacing-base / 6);
} }
} }
+39 -39
View File
@@ -4,60 +4,60 @@
html, html,
body { body {
display: flex; display: flex;
width: 100%; width: 100%;
} }
html { html {
background: $page-background; background: $page-background;
background-attachment: fixed; background-attachment: fixed;
height: 100%; height: 100%;
} }
body { body {
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100%; min-height: 100%;
} }
.grid { .grid {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
} }
main { main {
margin: auto; margin: auto;
padding-bottom: 1px; // Collapsing margins padding-bottom: 1px; // Collapsing margins
text-align: center; text-align: center;
} }
aside { aside {
align-items: center; align-items: center;
background: #fff; background: #fff;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
justify-content: center; justify-content: center;
padding: $spacing-base; padding: $spacing-base;
position: relative; position: relative;
text-align: center; text-align: center;
text-shadow: none; text-shadow: none;
width: 100%; width: 100%;
.icon { .icon {
fill: $color-twitter; fill: $color-twitter;
margin-right: ($spacing-base / 2); margin-right: ($spacing-base / 2);
} }
p { p {
margin: 0; margin: 0;
} }
a { a {
color: $color-twitter; color: $color-twitter;
&.tab-focus { &.tab-focus {
@include tab-focus($color-twitter); @include tab-focus($color-twitter);
}
} }
}
} }
+12 -12
View File
@@ -5,26 +5,26 @@
// Error page // Error page
html.error, html.error,
.error body { .error body {
height: 100%; height: 100%;
} }
html.error { html.error {
background: $page-background; background: $page-background;
background-attachment: fixed; background-attachment: fixed;
} }
.error body { .error body {
align-items: center; align-items: center;
display: flex; display: flex;
width: 100%; width: 100%;
} }
.error main { .error main {
padding: $spacing-base; padding: $spacing-base;
text-align: center; text-align: center;
width: 100%; width: 100%;
p { p {
@include font-size($font-size-large); @include font-size($font-size-large);
} }
} }
+10 -10
View File
@@ -3,17 +3,17 @@
// ========================================================================== // ==========================================================================
.grid { .grid {
margin: 0 auto; margin: 0 auto;
padding: $spacing-base; padding: $spacing-base;
@media only screen and (min-width: $screen-md) { @media only screen and (min-width: $screen-md) {
align-items: center; align-items: center;
display: flex; display: flex;
max-width: $container-max-width; max-width: $container-max-width;
width: 100%; width: 100%;
> * { > * {
flex: 1; flex: 1;
}
} }
}
} }
+17 -17
View File
@@ -4,24 +4,24 @@
// Fade // Fade
@keyframes fadein { @keyframes fadein {
0% { 0% {
opacity: 0; opacity: 0;
} }
100% { 100% {
opacity: 1; opacity: 1;
} }
} }
@keyframes shrinkHide { @keyframes shrinkHide {
0% { 0% {
opacity: 0.5; opacity: 0.5;
width: 38px; width: 38px;
} }
20% { 20% {
width: 45px; width: 45px;
} }
100% { 100% {
opacity: 0; opacity: 0;
width: 0; width: 0;
} }
} }
+30 -30
View File
@@ -3,46 +3,46 @@
// ========================================================================== // ==========================================================================
@font-face { @font-face {
font-display: swap; font-display: swap;
font-family: 'Gordita'; font-family: 'Gordita';
font-style: normal; font-style: normal;
font-weight: $font-weight-light; font-weight: $font-weight-light;
src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'), src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff'); url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff');
} }
@font-face { @font-face {
font-display: swap; font-display: swap;
font-family: 'Gordita'; font-family: 'Gordita';
font-style: normal; font-style: normal;
font-weight: $font-weight-regular; font-weight: $font-weight-regular;
src: url('https://cdn.plyr.io/static/fonts/gordita-regular.woff2') format('woff2'), src: url('https://cdn.plyr.io/static/fonts/gordita-regular.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-regular.woff') format('woff'); url('https://cdn.plyr.io/static/fonts/gordita-regular.woff') format('woff');
} }
@font-face { @font-face {
font-display: swap; font-display: swap;
font-family: 'Gordita'; font-family: 'Gordita';
font-style: normal; font-style: normal;
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
src: url('https://cdn.plyr.io/static/fonts/gordita-medium.woff2') format('woff2'), src: url('https://cdn.plyr.io/static/fonts/gordita-medium.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-medium.woff') format('woff'); url('https://cdn.plyr.io/static/fonts/gordita-medium.woff') format('woff');
} }
@font-face { @font-face {
font-display: swap; font-display: swap;
font-family: 'Gordita'; font-family: 'Gordita';
font-style: normal; font-style: normal;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'), src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff'); url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff');
} }
@font-face { @font-face {
font-display: swap; font-display: swap;
font-family: 'Gordita'; font-family: 'Gordita';
font-style: normal; font-style: normal;
font-weight: $font-weight-black; font-weight: $font-weight-black;
src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'), src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff'); url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff');
} }
+27 -27
View File
@@ -5,50 +5,50 @@
// Convert a <button> into an <a> // Convert a <button> into an <a>
// --------------------------------------- // ---------------------------------------
@mixin cancel-button-styles() { @mixin cancel-button-styles() {
background: transparent; background: transparent;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
cursor: pointer; cursor: pointer;
font: inherit; font: inherit;
line-height: $line-height-base; line-height: $line-height-base;
margin: 0; margin: 0;
padding: 0; padding: 0;
position: relative; position: relative;
text-align: inherit; text-align: inherit;
text-shadow: inherit; text-shadow: inherit;
-moz-user-select: text; // stylelint-disable-line -moz-user-select: text; // stylelint-disable-line
vertical-align: baseline; vertical-align: baseline;
width: auto; width: auto;
} }
// Nicer focus styles // Nicer focus styles
// --------------------------------------- // ---------------------------------------
@mixin tab-focus($color: $tab-focus-default-color) { @mixin tab-focus($color: $tab-focus-default-color) {
box-shadow: 0 0 0 3px rgba($color, 0.35); box-shadow: 0 0 0 3px rgba($color, 0.35);
outline: 0; outline: 0;
} }
// Use rems for font sizing // Use rems for font sizing
// Leave <body> at 100%/16px // Leave <body> at 100%/16px
// --------------------------------------- // ---------------------------------------
@function calculate-rem($size) { @function calculate-rem($size) {
$rem: $size / 16; $rem: $size / 16;
@return #{$rem}rem; @return #{$rem}rem;
} }
@mixin font-size($size: $font-size-base) { @mixin font-size($size: $font-size-base) {
font-size: $size * 1px; // Fallback in px font-size: $size * 1px; // Fallback in px
font-size: calculate-rem($size); font-size: calculate-rem($size);
} }
// Font smoothing // Font smoothing
// --------------------------------------- // ---------------------------------------
@mixin font-smoothing($enabled: true) { @mixin font-smoothing($enabled: true) {
@if $enabled { @if $enabled {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} @else { } @else {
-moz-osx-font-smoothing: auto; -moz-osx-font-smoothing: auto;
-webkit-font-smoothing: subpixel-antialiased; -webkit-font-smoothing: subpixel-antialiased;
} }
} }
+74 -74
View File
@@ -10,9 +10,9 @@
*/ */
html { html {
line-height: 1.15; /* 1 */ line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */ -ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */
} }
/* Sections /* Sections
@@ -23,7 +23,7 @@ html {
*/ */
body { body {
margin: 0; margin: 0;
} }
/** /**
@@ -36,7 +36,7 @@ footer,
header, header,
nav, nav,
section { section {
display: block; display: block;
} }
/** /**
@@ -45,8 +45,8 @@ section {
*/ */
h1 { h1 {
font-size: 2em; font-size: 2em;
margin: 0.67em 0; margin: 0.67em 0;
} }
/* Grouping content /* Grouping content
@@ -60,8 +60,8 @@ h1 {
figcaption, figcaption,
figure, figure,
main { main {
/* 1 */ /* 1 */
display: block; display: block;
} }
/** /**
@@ -69,7 +69,7 @@ main {
*/ */
figure { figure {
margin: 1em 40px; margin: 1em 40px;
} }
/** /**
@@ -78,9 +78,9 @@ figure {
*/ */
hr { hr {
box-sizing: content-box; /* 1 */ box-sizing: content-box; /* 1 */
height: 0; /* 1 */ height: 0; /* 1 */
overflow: visible; /* 2 */ overflow: visible; /* 2 */
} }
/** /**
@@ -89,8 +89,8 @@ hr {
*/ */
pre { pre {
font-family: monospace, monospace; /* 1 */ font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */ font-size: 1em; /* 2 */
} }
/* Text-level semantics /* Text-level semantics
@@ -102,8 +102,8 @@ pre {
*/ */
a { a {
background-color: transparent; /* 1 */ background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */ -webkit-text-decoration-skip: objects; /* 2 */
} }
/** /**
@@ -112,9 +112,9 @@ a {
*/ */
abbr[title] { abbr[title] {
border-bottom: none; /* 1 */ border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */ text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */ text-decoration: underline dotted; /* 2 */
} }
/** /**
@@ -123,7 +123,7 @@ abbr[title] {
b, b,
strong { strong {
font-weight: inherit; font-weight: inherit;
} }
/** /**
@@ -132,7 +132,7 @@ strong {
b, b,
strong { strong {
font-weight: bolder; font-weight: bolder;
} }
/** /**
@@ -143,8 +143,8 @@ strong {
code, code,
kbd, kbd,
samp { samp {
font-family: monospace, monospace; /* 1 */ font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */ font-size: 1em; /* 2 */
} }
/** /**
@@ -152,7 +152,7 @@ samp {
*/ */
dfn { dfn {
font-style: italic; font-style: italic;
} }
/** /**
@@ -160,8 +160,8 @@ dfn {
*/ */
mark { mark {
background-color: #ff0; background-color: #ff0;
color: #000; color: #000;
} }
/** /**
@@ -169,7 +169,7 @@ mark {
*/ */
small { small {
font-size: 80%; font-size: 80%;
} }
/** /**
@@ -179,18 +179,18 @@ small {
sub, sub,
sup { sup {
font-size: 75%; font-size: 75%;
line-height: 0; line-height: 0;
position: relative; position: relative;
vertical-align: baseline; vertical-align: baseline;
} }
sub { sub {
bottom: -0.25em; bottom: -0.25em;
} }
sup { sup {
top: -0.5em; top: -0.5em;
} }
/* Embedded content /* Embedded content
@@ -202,7 +202,7 @@ sup {
audio, audio,
video { video {
display: inline-block; display: inline-block;
} }
/** /**
@@ -210,8 +210,8 @@ video {
*/ */
audio:not([controls]) { audio:not([controls]) {
display: none; display: none;
height: 0; height: 0;
} }
/** /**
@@ -219,7 +219,7 @@ audio:not([controls]) {
*/ */
img { img {
border-style: none; border-style: none;
} }
/** /**
@@ -227,7 +227,7 @@ img {
*/ */
svg:not(:root) { svg:not(:root) {
overflow: hidden; overflow: hidden;
} }
/* Forms /* Forms
@@ -243,10 +243,10 @@ input,
optgroup, optgroup,
select, select,
textarea { textarea {
font-family: sans-serif; /* 1 */ font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */ font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */ line-height: 1.15; /* 1 */
margin: 0; /* 2 */ margin: 0; /* 2 */
} }
/** /**
@@ -256,8 +256,8 @@ textarea {
button, button,
input { input {
/* 1 */ /* 1 */
overflow: visible; overflow: visible;
} }
/** /**
@@ -267,8 +267,8 @@ input {
button, button,
select { select {
/* 1 */ /* 1 */
text-transform: none; text-transform: none;
} }
/** /**
@@ -281,7 +281,7 @@ button,
html [type='button'], html [type='button'],
[type='reset'], [type='reset'],
[type='submit'] { [type='submit'] {
-webkit-appearance: button; /* 2 */ -webkit-appearance: button; /* 2 */
} }
/** /**
@@ -292,8 +292,8 @@ button::-moz-focus-inner,
[type='button']::-moz-focus-inner, [type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner, [type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner { [type='submit']::-moz-focus-inner {
border-style: none; border-style: none;
padding: 0; padding: 0;
} }
/** /**
@@ -304,7 +304,7 @@ button:-moz-focusring,
[type='button']:-moz-focusring, [type='button']:-moz-focusring,
[type='reset']:-moz-focusring, [type='reset']:-moz-focusring,
[type='submit']:-moz-focusring { [type='submit']:-moz-focusring {
outline: 1px dotted ButtonText; outline: 1px dotted ButtonText;
} }
/** /**
@@ -312,7 +312,7 @@ button:-moz-focusring,
*/ */
fieldset { fieldset {
padding: 0.35em 0.75em 0.625em; padding: 0.35em 0.75em 0.625em;
} }
/** /**
@@ -323,12 +323,12 @@ fieldset {
*/ */
legend { legend {
box-sizing: border-box; /* 1 */ box-sizing: border-box; /* 1 */
color: inherit; /* 2 */ color: inherit; /* 2 */
display: table; /* 1 */ display: table; /* 1 */
max-width: 100%; /* 1 */ max-width: 100%; /* 1 */
padding: 0; /* 3 */ padding: 0; /* 3 */
white-space: normal; /* 1 */ white-space: normal; /* 1 */
} }
/** /**
@@ -337,8 +337,8 @@ legend {
*/ */
progress { progress {
display: inline-block; /* 1 */ display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */ vertical-align: baseline; /* 2 */
} }
/** /**
@@ -346,7 +346,7 @@ progress {
*/ */
textarea { textarea {
overflow: auto; overflow: auto;
} }
/** /**
@@ -356,8 +356,8 @@ textarea {
[type='checkbox'], [type='checkbox'],
[type='radio'] { [type='radio'] {
box-sizing: border-box; /* 1 */ box-sizing: border-box; /* 1 */
padding: 0; /* 2 */ padding: 0; /* 2 */
} }
/** /**
@@ -366,7 +366,7 @@ textarea {
[type='number']::-webkit-inner-spin-button, [type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button { [type='number']::-webkit-outer-spin-button {
height: auto; height: auto;
} }
/** /**
@@ -375,8 +375,8 @@ textarea {
*/ */
[type='search'] { [type='search'] {
-webkit-appearance: textfield; /* 1 */ -webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */ outline-offset: -2px; /* 2 */
} }
/** /**
@@ -385,7 +385,7 @@ textarea {
[type='search']::-webkit-search-cancel-button, [type='search']::-webkit-search-cancel-button,
[type='search']::-webkit-search-decoration { [type='search']::-webkit-search-decoration {
-webkit-appearance: none; -webkit-appearance: none;
} }
/** /**
@@ -394,8 +394,8 @@ textarea {
*/ */
::-webkit-file-upload-button { ::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */ -webkit-appearance: button; /* 1 */
font: inherit; /* 2 */ font: inherit; /* 2 */
} }
/* Interactive /* Interactive
@@ -408,7 +408,7 @@ textarea {
details, details,
menu { menu {
display: block; display: block;
} }
/* /*
@@ -416,7 +416,7 @@ menu {
*/ */
summary { summary {
display: list-item; display: list-item;
} }
/* Scripting /* Scripting
@@ -427,7 +427,7 @@ summary {
*/ */
canvas { canvas {
display: inline-block; display: inline-block;
} }
/** /**
@@ -435,7 +435,7 @@ canvas {
*/ */
template { template {
display: none; display: none;
} }
/* Hidden /* Hidden
@@ -446,5 +446,5 @@ template {
*/ */
[hidden] { [hidden] {
display: none; display: none;
} }
+1 -1
View File
@@ -7,5 +7,5 @@
*, *,
*::after, *::after,
*::before { *::before {
box-sizing: border-box; box-sizing: border-box;
} }
+12 -12
View File
@@ -3,22 +3,22 @@
// ========================================================================== // ==========================================================================
// Grayscale // Grayscale
$color-gray-9: hsl(210, 15%, 16%); $color-gray-900: hsl(210, 15%, 16%);
$color-gray-8: lighten($color-gray-9, 9%); $color-gray-800: lighten($color-gray-900, 9%);
$color-gray-7: lighten($color-gray-8, 9%); $color-gray-700: lighten($color-gray-800, 9%);
$color-gray-6: lighten($color-gray-7, 9%); $color-gray-600: lighten($color-gray-700, 9%);
$color-gray-5: lighten($color-gray-6, 9%); $color-gray-500: lighten($color-gray-600, 9%);
$color-gray-4: lighten($color-gray-5, 9%); $color-gray-400: lighten($color-gray-500, 9%);
$color-gray-3: lighten($color-gray-4, 9%); $color-gray-300: lighten($color-gray-400, 9%);
$color-gray-2: lighten($color-gray-3, 9%); $color-gray-200: lighten($color-gray-300, 9%);
$color-gray-1: lighten($color-gray-2, 9%); $color-gray-100: lighten($color-gray-200, 9%);
$color-gray-0: lighten($color-gray-1, 9%); $color-gray-50: lighten($color-gray-100, 9%);
// Branding // Branding
$color-brand-primary: hsl(198, 100%, 50%); $color-brand-primary: hsl(198, 100%, 50%);
// Text // Text
$color-text: $color-gray-7; $color-text: $color-gray-700;
$color-headings: $color-brand-primary; $color-headings: $color-brand-primary;
// Brands // Brands
@@ -36,7 +36,7 @@ $color-button-background: $color-brand-primary;
$color-button-text: #fff; $color-button-text: #fff;
$color-button-background-hover: hsl(198, 100%, 55%); $color-button-background-hover: hsl(198, 100%, 55%);
$color-button-count-background: #fff; $color-button-count-background: #fff;
$color-button-count-text: $color-gray-6; $color-button-count-text: $color-gray-600;
// Focus // Focus
$tab-focus-default-color: #fff; $tab-focus-default-color: #fff;
+14 -21
View File
@@ -2,24 +2,17 @@
// Plyr Settings // Plyr Settings
// ========================================================================== // ==========================================================================
// Font @include css-vars(
$plyr-font-family: inherit; (
--plyr-color-main: $color-brand-primary,
// Sizes --plyr-font-size-base: 13px,
$plyr-font-size-base: 13px; --plyr-font-size-small: 12px,
$plyr-font-size-small: 12px; --plyr-font-size-time: 11px,
$plyr-font-size-time: 11px; --plyr-font-size-badges: 9px,
$plyr-font-size-badges: 9px; --plyr-font-size-menu: var(--plyr-font-size-base),
--plyr-font-weight-regular: 500,
// Other --plyr-font-weight-bold: 600,
$plyr-font-smoothing: true; --plyr-font-size-captions-medium: 18px,
--plyr-font-size-captions-large: 21px,
// Colors )
$plyr-color-main: $color-brand-primary; );
// Captions
$plyr-font-size-captions-base: $plyr-font-size-base;
$plyr-font-size-captions-small: $plyr-font-size-small;
$plyr-font-size-captions-medium: 18px;
$plyr-font-size-captions-large: 21px;
$plyr-font-size-menu: $plyr-font-size-base;
+1 -1
View File
@@ -3,7 +3,7 @@
// ========================================================================== // ==========================================================================
$font-sans-serif: 'Gordita', 'Avenir', 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', $font-sans-serif: 'Gordita', 'Avenir', 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol'; 'Segoe UI Symbol';
$font-size-base: 15; $font-size-base: 15;
$font-size-small: 13; $font-size-small: 13;
+11 -11
View File
@@ -4,31 +4,31 @@
// Set to 100% for rem sizing // Set to 100% for rem sizing
html { html {
font-size: 100%; font-size: 100%;
} }
body { body {
@include font-smoothing(); @include font-smoothing();
@include font-size($font-size-base); @include font-size($font-size-base);
color: $color-text; color: $color-text;
font-family: $font-sans-serif; font-family: $font-sans-serif;
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
line-height: $line-height-base; line-height: $line-height-base;
} }
button, button,
input, input,
select, select,
textarea { textarea {
font: inherit; font: inherit;
} }
p, p,
small { small {
margin: 0 0 ($spacing-base * 1.5); margin: 0 0 ($spacing-base * 1.5);
} }
small { small {
@include font-size($font-size-small); @include font-size($font-size-small);
display: block; display: block;
} }
+6 -6
View File
@@ -3,10 +3,10 @@
// ========================================================================== // ==========================================================================
h1 { h1 {
@include font-size($font-size-h1); @include font-size($font-size-h1);
color: $color-headings; color: $color-headings;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-headings; letter-spacing: $letter-spacing-headings;
line-height: 1.2; line-height: 1.2;
margin: 0 0 ($spacing-base * 1.5); margin: 0 0 ($spacing-base * 1.5);
} }
+1 -1
View File
@@ -3,5 +3,5 @@
// ========================================================================== // ==========================================================================
.no-border { .no-border {
border: 0; border: 0;
} }
+10 -10
View File
@@ -3,18 +3,18 @@
// ========================================================================== // ==========================================================================
[hidden] { [hidden] {
display: none; display: none;
} }
// Hide only visually, but have it available for screen readers: h5bp.com/v // Hide only visually, but have it available for screen readers: h5bp.com/v
.sr-only { .sr-only {
border: 0; border: 0;
clip: rect(0 0 0 0); clip: rect(0 0 0 0);
height: 1px; height: 1px;
margin: -1px; margin: -1px;
opacity: 0.001; opacity: 0.001;
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
position: absolute; position: absolute;
width: 1px; width: 1px;
} }
+65 -13
View File
@@ -2,27 +2,79 @@
# yarn lockfile v1 # yarn lockfile v1
core-js@^3.1.4: "@sentry/browser@^5.15.5":
version "3.1.4" version "5.15.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.4.tgz#3a2837fc48e582e1ae25907afcd6cf03b0cc7a07" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.15.5.tgz#d9a51f1388581067b50d30ed9b1aed2cbb333a36"
integrity sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ== integrity sha512-rqDvjk/EvogfdbZ4TiEpxM/lwpPKmq23z9YKEO4q81+1SwJNua53H60dOk9HpRU8nOJ1g84TMKT2Ov8H7sqDWA==
dependencies:
"@sentry/core" "5.15.5"
"@sentry/types" "5.15.5"
"@sentry/utils" "5.15.5"
tslib "^1.9.3"
"@sentry/core@5.15.5":
version "5.15.5"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.15.5.tgz#40ea79bff5272d3fbbeeb4a98cdc59e1adbd2c92"
integrity sha512-enxBLv5eibBMqcWyr+vApqeix8uqkfn0iGsD3piKvoMXCgKsrfMwlb/qo9Ox0lKr71qIlZVt+9/A2vZohdgnlg==
dependencies:
"@sentry/hub" "5.15.5"
"@sentry/minimal" "5.15.5"
"@sentry/types" "5.15.5"
"@sentry/utils" "5.15.5"
tslib "^1.9.3"
"@sentry/hub@5.15.5":
version "5.15.5"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.15.5.tgz#f5abbcdbe656a70e2ff02c02a5a4cffa0f125935"
integrity sha512-zX9o49PcNIVMA4BZHe//GkbQ4Jx+nVofqU/Il32/IbwKhcpPlhGX3c1sOVQo4uag3cqd/JuQsk+DML9TKkN0Lw==
dependencies:
"@sentry/types" "5.15.5"
"@sentry/utils" "5.15.5"
tslib "^1.9.3"
"@sentry/minimal@5.15.5":
version "5.15.5"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.15.5.tgz#a0e4e071f01d9c4d808094ae7203f6c4cca9348a"
integrity sha512-zQkkJ1l9AjmU/Us5IrOTzu7bic4sTPKCatptXvLSTfyKW7N6K9MPIIFeSpZf9o1yM2sRYdK7GV08wS2eCT3JYw==
dependencies:
"@sentry/hub" "5.15.5"
"@sentry/types" "5.15.5"
tslib "^1.9.3"
"@sentry/types@5.15.5":
version "5.15.5"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.15.5.tgz#16c97e464cf09bbd1d2e8ce90d130e781709076e"
integrity sha512-F9A5W7ucgQLJUG4LXw1ZIy4iLevrYZzbeZ7GJ09aMlmXH9PqGThm1t5LSZlVpZvUfQ2rYA8NU6BdKJSt7B5LPw==
"@sentry/utils@5.15.5":
version "5.15.5"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.15.5.tgz#dec1d4c79037c4da08b386f5d34409234dcbfb15"
integrity sha512-Nl9gl/MGnzSkuKeo3QaefoD/OJrFLB8HmwQ7HUbTXb6E7yyEzNKAQMHXGkwNAjbdYyYbd42iABP6Y5F/h39NtA==
dependencies:
"@sentry/types" "5.15.5"
tslib "^1.9.3"
core-js@^3.6.5:
version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
custom-event-polyfill@^1.0.7: custom-event-polyfill@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee" resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w== integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
raven-js@^3.27.2:
version "3.27.2"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.27.2.tgz#6c33df952026cd73820aa999122b7b7737a66775"
integrity sha512-mFWQcXnhRFEQe5HeFroPaEghlnqy7F5E2J3Fsab189ondqUzcjwSVi7el7F36cr6PvQYXoZ1P2F5CSF2/azeMQ==
shr-buttons@2.0.3: shr-buttons@2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/shr-buttons/-/shr-buttons-2.0.3.tgz#2ffd021fc3d789e1510ce2736b938bd09ea1da5a" resolved "https://registry.yarnpkg.com/shr-buttons/-/shr-buttons-2.0.3.tgz#2ffd021fc3d789e1510ce2736b938bd09ea1da5a"
integrity sha512-sPAgHiw4uaIt9TnxTfyZEedDChcldSVtnBHE44cpe/mSC7rqm4IEKZRLYqnVlTcGM+FSDNBPUNpSf50Q2ntd+w== integrity sha512-sPAgHiw4uaIt9TnxTfyZEedDChcldSVtnBHE44cpe/mSC7rqm4IEKZRLYqnVlTcGM+FSDNBPUNpSf50Q2ntd+w==
url-polyfill@^1.1.5: tslib@^1.9.3:
version "1.1.5" version "1.11.1"
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.5.tgz#bec79b72b5407dba6d8cced2e32e4ab273aa9fb1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-9XjIJ6nwrU+nGd8t90Ze0Zs7t8A+SU0gqsqPttj6j3zAVe5q0HFcuv37nDBdVSPpi4aTHTfbUF/i+ZVD+o2EbA== integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
url-polyfill@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.8.tgz#21eb58ad61192f52b77dcac8ab5293ae7bc67060"
integrity sha512-Ey61F4FEqhcu1vHSOMmjl0Vd/RPRLEjMj402qszD/dhMBrVfoUsnIj8KSZo2yj+eIlxJGKFdnm6ES+7UzMgZ3Q==
+10 -10
View File
@@ -1,12 +1,12 @@
{ {
"cdn": { "cdn": {
"bucket": "plyr", "bucket": "plyr",
"domain": "cdn.plyr.io", "domain": "cdn.plyr.io",
"region": "us-east-1" "region": "us-east-1"
}, },
"demo": { "demo": {
"bucket": "plyr.io", "bucket": "plyr.io",
"domain": "plyr.io", "domain": "plyr.io",
"region": "us-west-1" "region": "us-west-1"
} }
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+339 -171
View File
@@ -75,6 +75,42 @@ typeof navigator === "object" && (function (global, factory) {
return target; return target;
} }
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
function _objectWithoutProperties(source, excluded) {
if (source == null) return {};
var target = _objectWithoutPropertiesLoose(source, excluded);
var key, i;
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue;
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
function _slicedToArray(arr, i) { function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
} }
@@ -437,7 +473,7 @@ typeof navigator === "object" && (function (global, factory) {
}; };
var isPromise = function isPromise(input) { var isPromise = function isPromise(input) {
return instanceOf$1(input, Promise); return instanceOf$1(input, Promise) && isFunction$1(input.then);
}; };
var isEmpty$1 = function isEmpty(input) { var isEmpty$1 = function isEmpty(input) {
@@ -779,12 +815,33 @@ typeof navigator === "object" && (function (global, factory) {
} // Element matches selector } // Element matches selector
function matches$1(element, selector) { function matches$1(element, selector) {
var _Element = Element,
prototype = _Element.prototype;
function match() { function match() {
return Array.from(document.querySelectorAll(selector)).includes(this); return Array.from(document.querySelectorAll(selector)).includes(this);
} }
var method = match; var method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;
return method.call(element, selector);
} // Closest ancestor element matching selector (also tests element itself)
function closest(element, selector) {
var _Element2 = Element,
prototype = _Element2.prototype; // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
function closestElement() {
var el = this;
do {
if (matches$1.matches(el, selector)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
}
var method = prototype.closest || closestElement;
return method.call(element, selector); return method.call(element, selector);
} // Find all elements } // Find all elements
@@ -1056,6 +1113,19 @@ typeof navigator === "object" && (function (global, factory) {
}).then(function () {}); }).then(function () {});
} }
/**
* Silence a Promise-like object.
* This is useful for avoiding non-harmful, but potentially confusing "uncaught
* play promise" rejection error messages.
* @param {Object} value An object that may or may not be `Promise`-like.
*/
function silencePromise(value) {
if (is$1.promise(value)) {
value.then(null, function () {});
}
}
function validateRatio(input) { function validateRatio(input) {
if (!is$1.array(input) && (!is$1.string(input) || !input.includes(':'))) { if (!is$1.array(input) && (!is$1.string(input) || !input.includes(':'))) {
return false; return false;
@@ -1124,8 +1194,8 @@ typeof navigator === "object" && (function (global, factory) {
var padding = 100 / w * h; var padding = 100 / w * h;
wrapper.style.paddingBottom = "".concat(padding, "%"); // For Vimeo we have an extra <div> to hide the standard controls and UI wrapper.style.paddingBottom = "".concat(padding, "%"); // For Vimeo we have an extra <div> to hide the standard controls and UI
if (this.isVimeo && this.supported.ui) { if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {
var height = 240; var height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);
var offset = (height - padding) / (height / 50); var offset = (height - padding) / (height / 50);
this.media.style.transform = "translateY(-".concat(offset, "%)"); this.media.style.transform = "translateY(-".concat(offset, "%)");
} else if (this.isHTML5) { } else if (this.isHTML5) {
@@ -1232,7 +1302,7 @@ typeof navigator === "object" && (function (global, factory) {
player.currentTime = currentTime; // Resume playing player.currentTime = currentTime; // Resume playing
if (!paused) { if (!paused) {
player.play(); silencePromise(player.play());
} }
}); // Load new source }); // Load new source
@@ -1281,7 +1351,7 @@ typeof navigator === "object" && (function (global, factory) {
}); });
} // Get the closest value in an array } // Get the closest value in an array
function closest(array, value) { function closest$1(array, value) {
if (!is$1.array(array) || !array.length) { if (!is$1.array(array) || !array.length) {
return null; return null;
} }
@@ -1319,19 +1389,19 @@ typeof navigator === "object" && (function (global, factory) {
return (current / max * 100).toFixed(2); return (current / max * 100).toFixed(2);
} // Replace all occurances of a string in a string } // Replace all occurances of a string in a string
function replaceAll() { var replaceAll = function replaceAll() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
} // Convert to title case }; // Convert to title case
function toTitleCase() { var toTitleCase = function toTitleCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
return input.toString().replace(/\w\S*/g, function (text) { return input.toString().replace(/\w\S*/g, function (text) {
return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
}); });
} // Convert string to pascalCase }; // Convert string to pascalCase
function toPascalCase() { function toPascalCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@@ -2429,39 +2499,39 @@ typeof navigator === "object" && (function (global, factory) {
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panels.loop)) { if (!is.element(this.elements.settings.panels.loop)) {
return; return;
} }
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
// Show the pane and tab // Show the pane and tab
toggleHidden(this.elements.settings.buttons.loop, false); toggleHidden(this.elements.settings.buttons.loop, false);
toggleHidden(this.elements.settings.panels.loop, false); toggleHidden(this.elements.settings.panels.loop, false);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.loop.options); const toggle = !is.empty(this.loop.options);
controls.toggleMenuButton.call(this, 'loop', toggle); controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
options.forEach(option => { options.forEach(option => {
const item = createElement('li'); const item = createElement('li');
const button = createElement( const button = createElement(
'button', 'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.loop), { extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
type: 'button', type: 'button',
class: this.config.classNames.control, class: this.config.classNames.control,
'data-plyr-loop-action': option, 'data-plyr-loop-action': option,
}), }),
i18n.get(option, this.config) i18n.get(option, this.config)
); );
if (['start', 'end'].includes(option)) { if (['start', 'end'].includes(option)) {
const badge = controls.createBadge.call(this, '00:00'); const badge = controls.createBadge.call(this, '00:00');
button.appendChild(badge); button.appendChild(badge);
} }
item.appendChild(button); item.appendChild(button);
list.appendChild(item); list.appendChild(item);
}); });
}, */ }, */
// Get current selected caption language // Get current selected caption language
// TODO: rework this to user the getter in the API? // TODO: rework this to user the getter in the API?
// Set a list of available captions languages // Set a list of available captions languages
@@ -3263,9 +3333,15 @@ typeof navigator === "object" && (function (global, factory) {
meta.set(track, { meta.set(track, {
default: track.mode === 'showing' default: track.mode === 'showing'
}); // Turn off native caption rendering to avoid double captions }); // Turn off native caption rendering to avoid double captions
// Note: mode='hidden' forces a track to download. To ensure every track
// isn't downloaded at once, only 'showing' tracks should be reassigned
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
track.mode = 'hidden'; // Add event listener for cue changes if (track.mode === 'showing') {
// eslint-disable-next-line no-param-reassign
track.mode = 'hidden';
} // Add event listener for cue changes
on.call(_this, track, 'cuechange', function () { on.call(_this, track, 'cuechange', function () {
return captions.updateCues.call(_this); return captions.updateCues.call(_this);
@@ -3289,6 +3365,8 @@ typeof navigator === "object" && (function (global, factory) {
// Toggle captions display // Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false // Used internally for the toggleCaptions method, with the passive option forced to false
toggle: function toggle(input) { toggle: function toggle(input) {
var _this2 = this;
var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
// If there's no full support // If there's no full support
@@ -3335,7 +3413,15 @@ typeof navigator === "object" && (function (global, factory) {
controls.updateSetting.call(this, 'captions'); // Trigger event (not used internally) controls.updateSetting.call(this, 'captions'); // Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
} } // Wait for the call stack to clear before setting mode='hidden'
// on the active track - forcing the browser to download it
setTimeout(function () {
if (active && _this2.captions.toggled) {
_this2.captions.currentTrackNode.mode = 'hidden';
}
});
}, },
// Set captions by track index // Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false // Used internally for the currentTrack setter with the passive option forced to false
@@ -3416,7 +3502,7 @@ typeof navigator === "object" && (function (global, factory) {
// If update is false it will also ignore tracks without metadata // If update is false it will also ignore tracks without metadata
// This is used to "freeze" the language options when captions.update is false // This is used to "freeze" the language options when captions.update is false
getTracks: function getTracks() { getTracks: function getTracks() {
var _this2 = this; var _this3 = this;
var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
// Handle media or textTracks missing or null // Handle media or textTracks missing or null
@@ -3424,20 +3510,20 @@ typeof navigator === "object" && (function (global, factory) {
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata) // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks.filter(function (track) { return tracks.filter(function (track) {
return !_this2.isHTML5 || update || _this2.captions.meta.has(track); return !_this3.isHTML5 || update || _this3.captions.meta.has(track);
}).filter(function (track) { }).filter(function (track) {
return ['captions', 'subtitles'].includes(track.kind); return ['captions', 'subtitles'].includes(track.kind);
}); });
}, },
// Match tracks based on languages and get the first // Match tracks based on languages and get the first
findTrack: function findTrack(languages) { findTrack: function findTrack(languages) {
var _this3 = this; var _this4 = this;
var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var tracks = captions.getTracks.call(this); var tracks = captions.getTracks.call(this);
var sortIsDefault = function sortIsDefault(track) { var sortIsDefault = function sortIsDefault(track) {
return Number((_this3.captions.meta.get(track) || {}).default); return Number((_this4.captions.meta.get(track) || {}).default);
}; };
var sorted = Array.from(tracks).sort(function (a, b) { var sorted = Array.from(tracks).sort(function (a, b) {
@@ -3570,7 +3656,7 @@ typeof navigator === "object" && (function (global, factory) {
// Sprite (for icons) // Sprite (for icons)
loadSprite: true, loadSprite: true,
iconPrefix: 'plyr', iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg', iconUrl: 'https://cdn.plyr.io/3.6.1/plyr.svg',
// Blank video (used to prevent errors on source change) // Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4', blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default // Quality default
@@ -3618,6 +3704,9 @@ typeof navigator === "object" && (function (global, factory) {
fallback: true, fallback: true,
// Fallback using full viewport/window // Fallback using full viewport/window
iosNative: false // Use the native fullscreen in iOS (disables custom controls) iosNative: false // Use the native fullscreen in iOS (disables custom controls)
// Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode
// Non-ancestors of the player element will be ignored
// container: null, // defaults to the player element
}, },
// Local storage // Local storage
@@ -3855,16 +3944,16 @@ typeof navigator === "object" && (function (global, factory) {
title: false, title: false,
speed: true, speed: true,
transparent: false, transparent: false,
// These settings require a pro or premium account to work // Whether the owner of the video has a Pro or Business account
sidedock: false, // (which allows us to properly hide controls without CSS hacks, etc)
controls: false, premium: false,
// Custom settings from Plyr // Custom settings from Plyr
referrerPolicy: null // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy referrerPolicy: null // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
}, },
// YouTube plugin // YouTube plugin
youtube: { youtube: {
noCookie: false, noCookie: true,
// Whether to use an alternative version of YouTube without cookies // Whether to use an alternative version of YouTube without cookies
rel: 0, rel: 0,
// No related vids // No related vids
@@ -3974,7 +4063,10 @@ typeof navigator === "object" && (function (global, factory) {
y: 0 y: 0
}; // Force the use of 'full window/browser' rather than fullscreen }; // Force the use of 'full window/browser' rather than fullscreen
this.forceFallback = player.config.fullscreen.fallback === 'force'; // Register event listeners this.forceFallback = player.config.fullscreen.fallback === 'force'; // Get the fullscreen element
// Checks container is an ancestor, defaults to null
this.player.elements.fullscreen = player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container); // Register event listeners
// Handle event (incase user presses escape etc) // Handle event (incase user presses escape etc)
on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : "".concat(this.prefix, "fullscreenchange"), function () { on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : "".concat(this.prefix, "fullscreenchange"), function () {
@@ -4147,7 +4239,7 @@ typeof navigator === "object" && (function (global, factory) {
if (browser.isIos && this.player.config.fullscreen.iosNative) { if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen(); this.target.webkitExitFullscreen();
this.player.play(); silencePromise(this.player.play());
} else if (!Fullscreen.native || this.forceFallback) { } else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(false); this.toggleFallback(false);
} else if (!this.prefix) { } else if (!this.prefix) {
@@ -4194,13 +4286,13 @@ typeof navigator === "object" && (function (global, factory) {
} }
var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")]; var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")];
return element === this.target; return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
} // Get target element } // Get target element
}, { }, {
key: "target", key: "target",
get: function get() { get: function get() {
return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container; return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen || this.player.elements.container;
} }
}], [{ }], [{
key: "native", key: "native",
@@ -4262,7 +4354,6 @@ typeof navigator === "object" && (function (global, factory) {
}); });
} }
// ==========================================================================
var ui = { var ui = {
addStyleHook: function addStyleHook() { addStyleHook: function addStyleHook() {
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
@@ -4397,12 +4488,7 @@ typeof navigator === "object" && (function (global, factory) {
} // Set property synchronously to respect the call order } // Set property synchronously to respect the call order
this.media.setAttribute('poster', poster); // HTML5 uses native poster attribute this.media.setAttribute('data-poster', poster); // Wait until ui is ready
if (this.isHTML5) {
return Promise.resolve(poster);
} // Wait until ui is ready
return ready.call(this) // Load image return ready.call(this) // Load image
.then(function () { .then(function () {
@@ -4478,6 +4564,26 @@ typeof navigator === "object" && (function (global, factory) {
this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek)); this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));
} }
},
// Migrate any custom properties from the media to the parent
migrateStyles: function migrateStyles() {
var _this5 = this;
// Loop through values (as they are the keys when the object is spread 🤔)
Object.values(_objectSpread2({}, this.media.style)) // We're only fussed about Plyr specific properties
.filter(function (key) {
return !is$1.empty(key) && key.startsWith('--plyr');
}).forEach(function (key) {
// Set on the container
_this5.elements.container.style.setProperty(key, _this5.media.style.getPropertyValue(key)); // Clean up from media element
_this5.media.style.removeProperty(key);
}); // Remove attribute if empty
if (is$1.empty(this.media.style)) {
this.media.removeAttribute('style');
}
} }
}; };
@@ -4572,7 +4678,7 @@ typeof navigator === "object" && (function (global, factory) {
case 75: case 75:
// Space and K key // Space and K key
if (!repeat) { if (!repeat) {
player.togglePlay(); silencePromise(player.togglePlay());
} }
break; break;
@@ -4686,15 +4792,17 @@ typeof navigator === "object" && (function (global, factory) {
removeCurrent(); // Delay the adding of classname until the focus has changed removeCurrent(); // Delay the adding of classname until the focus has changed
// This event fires before the focusin event // This event fires before the focusin event
this.focusTimer = setTimeout(function () { if (event.type !== 'focusout') {
var focused = document.activeElement; // Ignore if current focus element isn't inside the player this.focusTimer = setTimeout(function () {
var focused = document.activeElement; // Ignore if current focus element isn't inside the player
if (!elements.container.contains(focused)) { if (!elements.container.contains(focused)) {
return; return;
} }
toggleClass(document.activeElement, player.config.classNames.tabFocus, true); toggleClass(document.activeElement, player.config.classNames.tabFocus, true);
}, 10); }, 10);
}
} // Global window & document listeners } // Global window & document listeners
}, { }, {
@@ -4712,7 +4820,7 @@ typeof navigator === "object" && (function (global, factory) {
once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection
toggleListener.call(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true); toggleListener.call(player, document.body, 'keydown focus blur focusout', this.setTabFocus, toggle, false, true);
} // Container listeners } // Container listeners
}, { }, {
@@ -4755,7 +4863,7 @@ typeof navigator === "object" && (function (global, factory) {
}); // Set a gutter for Vimeo }); // Set a gutter for Vimeo
var setGutter = function setGutter(ratio, padding, toggle) { var setGutter = function setGutter(ratio, padding, toggle) {
if (!player.isVimeo) { if (!player.isVimeo || player.config.vimeo.premium) {
return; return;
} }
@@ -4812,7 +4920,7 @@ typeof navigator === "object" && (function (global, factory) {
ratio = _setPlayerSize.ratio; // Set Vimeo gutter ratio = _setPlayerSize.ratio; // Set Vimeo gutter
setGutter(ratio, padding, isEnter); // If not using native fullscreen, we need to check for resizes of viewport setGutter(ratio, padding, isEnter); // If not using native browser fullscreen API, we need to check for resizes of viewport
if (!usingNative) { if (!usingNative) {
if (isEnter) { if (isEnter) {
@@ -4890,9 +4998,13 @@ typeof navigator === "object" && (function (global, factory) {
if (player.ended) { if (player.ended) {
_this.proxy(event, player.restart, 'restart'); _this.proxy(event, player.restart, 'restart');
_this.proxy(event, player.play, 'play'); _this.proxy(event, function () {
silencePromise(player.play());
}, 'play');
} else { } else {
_this.proxy(event, player.togglePlay, 'play'); _this.proxy(event, function () {
silencePromise(player.togglePlay());
}, 'play');
} }
}); });
} // Disable right click } // Disable right click
@@ -4990,7 +5102,9 @@ typeof navigator === "object" && (function (global, factory) {
if (elements.buttons.play) { if (elements.buttons.play) {
Array.from(elements.buttons.play).forEach(function (button) { Array.from(elements.buttons.play).forEach(function (button) {
_this3.bind(button, 'click', player.togglePlay, 'play'); _this3.bind(button, 'click', function () {
silencePromise(player.togglePlay());
}, 'play');
}); });
} // Pause } // Pause
@@ -5087,7 +5201,7 @@ typeof navigator === "object" && (function (global, factory) {
if (play && done) { if (play && done) {
seek.removeAttribute(attribute); seek.removeAttribute(attribute);
player.play(); silencePromise(player.play());
} else if (!done && player.playing) { } else if (!done && player.playing) {
seek.setAttribute(attribute, ''); seek.setAttribute(attribute, '');
player.pause(); player.pause();
@@ -5185,7 +5299,18 @@ typeof navigator === "object" && (function (global, factory) {
this.bind(elements.controls, 'mouseenter mouseleave', function (event) { this.bind(elements.controls, 'mouseenter mouseleave', function (event) {
elements.controls.hover = !player.touch && event.type === 'mouseenter'; elements.controls.hover = !player.touch && event.type === 'mouseenter';
}); // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) }); // Also update controls.hover state for any non-player children of fullscreen element (as above)
if (elements.fullscreen) {
Array.from(elements.fullscreen.children).filter(function (c) {
return !c.contains(elements.container);
}).forEach(function (child) {
_this3.bind(child, 'mouseenter mouseleave', function (event) {
elements.controls.hover = !player.touch && event.type === 'mouseenter';
});
});
} // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
@@ -5601,15 +5726,28 @@ typeof navigator === "object" && (function (global, factory) {
var _this = this; var _this = this;
var player = this; var player = this;
var config = player.config.vimeo; // Get Vimeo params for the iframe var config = player.config.vimeo;
var params = buildUrlParams(extend({}, { var premium = config.premium,
referrerPolicy = config.referrerPolicy,
frameParams = _objectWithoutProperties(config, ["premium", "referrerPolicy"]); // If the owner has a pro or premium account then we can hide controls etc
if (premium) {
Object.assign(frameParams, {
controls: false,
sidedock: false
});
} // Get Vimeo params for the iframe
var params = buildUrlParams(_objectSpread2({
loop: player.config.loop.active, loop: player.config.loop.active,
autoplay: player.autoplay, autoplay: player.autoplay,
muted: player.muted, muted: player.muted,
gesture: 'media', gesture: 'media',
playsinline: !this.config.fullscreen.iosNative playsinline: !this.config.fullscreen.iosNative
}, config)); // Get the source URL or ID }, frameParams)); // Get the source URL or ID
var source = player.media.getAttribute('src'); // Get from <div> if needed var source = player.media.getAttribute('src'); // Get from <div> if needed
@@ -5623,22 +5761,27 @@ typeof navigator === "object" && (function (global, factory) {
var src = format(player.config.urls.vimeo.iframe, id, params); var src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src); iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', ''); iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', ''); iframe.setAttribute('allow', 'autoplay,fullscreen,picture-in-picture'); // Set the referrer policy if required
iframe.setAttribute('allow', 'autoplay'); // Set the referrer policy if required
if (!is$1.empty(config.referrerPolicy)) { if (!is$1.empty(referrerPolicy)) {
iframe.setAttribute('referrerPolicy', config.referrerPolicy); iframe.setAttribute('referrerPolicy', referrerPolicy);
} // Get poster, if already set } // Inject the package
var poster = player.poster; // Inject the package var poster = player.poster;
if (premium) {
iframe.setAttribute('data-poster', poster);
player.media = replaceElement(iframe, player.media);
} else {
var wrapper = createElement('div', {
class: player.config.classNames.embedContainer,
'data-poster': poster
});
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
} // Get poster image
var wrapper = createElement('div', {
poster: poster,
class: player.config.classNames.embedContainer
});
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media); // Get poster image
fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) { fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) {
if (is$1.empty(response)) { if (is$1.empty(response)) {
@@ -5722,6 +5865,9 @@ typeof navigator === "object" && (function (global, factory) {
player.embed.setPlaybackRate(input).then(function () { player.embed.setPlaybackRate(input).then(function () {
speed = input; speed = input;
triggerEvent.call(player, player.media, 'ratechange'); triggerEvent.call(player, player.media, 'ratechange');
}).catch(function () {
// Cannot set Playback Rate, Video is probably not on Pro account
player.options.speed = [1];
}); });
} }
}); // Volume }); // Volume
@@ -6008,7 +6154,7 @@ typeof navigator === "object" && (function (global, factory) {
var container = createElement('div', { var container = createElement('div', {
id: id, id: id,
poster: poster 'data-poster': poster
}); });
player.media = replaceElement(container, player.media); // Id to poster wrapper player.media = replaceElement(container, player.media); // Id to poster wrapper
@@ -6327,14 +6473,12 @@ typeof navigator === "object" && (function (global, factory) {
class: this.config.classNames.video class: this.config.classNames.video
}); // Wrap the video in a container }); // Wrap the video in a container
wrap(this.media, this.elements.wrapper); // Faux poster container wrap(this.media, this.elements.wrapper); // Poster image container
if (this.isEmbed) { this.elements.poster = createElement('div', {
this.elements.poster = createElement('div', { class: this.config.classNames.poster
class: this.config.classNames.poster });
}); this.elements.wrapper.appendChild(this.elements.poster);
this.elements.wrapper.appendChild(this.elements.poster);
}
} }
if (this.isHTML5) { if (this.isHTML5) {
@@ -6461,6 +6605,8 @@ typeof navigator === "object" && (function (global, factory) {
* mobile devices, this initialization is done as the result of a user action. * mobile devices, this initialization is done as the result of a user action.
*/ */
value: function setupIMA() { value: function setupIMA() {
var _this4 = this;
// Create the container for our advertisements // Create the container for our advertisements
this.elements.container = createElement('div', { this.elements.container = createElement('div', {
class: this.player.config.classNames.ads class: this.player.config.classNames.ads
@@ -6473,7 +6619,16 @@ typeof navigator === "object" && (function (global, factory) {
google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); // We assume the adContainer is the video container of the plyr element that will house the ads google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); // We assume the adContainer is the video container of the plyr element that will house the ads
this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); // Request video ads to be pre-loaded this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); // Create ads loader
this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) {
return _this4.onAdsManagerLoaded(event);
}, false);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
return _this4.onAdError(error);
}, false); // Request video ads to be pre-loaded
this.requestAds(); this.requestAds();
} }
@@ -6484,21 +6639,10 @@ typeof navigator === "object" && (function (global, factory) {
}, { }, {
key: "requestAds", key: "requestAds",
value: function requestAds() { value: function requestAds() {
var _this4 = this;
var container = this.player.elements.container; var container = this.player.elements.container;
try { try {
// Create ads loader // Request video ads
this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) {
return _this4.onAdsManagerLoaded(event);
}, false);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
return _this4.onAdError(error);
}, false); // Request video ads
var request = new google.ima.AdsRequest(); var request = new google.ima.AdsRequest();
request.adTagUrl = this.tagUrl; // Specify the linear and nonlinear slot sizes. This helps the SDK request.adTagUrl = this.tagUrl; // Specify the linear and nonlinear slot sizes. This helps the SDK
// to select the correct creative if multiple are returned // to select the correct creative if multiple are returned
@@ -6677,7 +6821,13 @@ typeof navigator === "object" && (function (global, factory) {
// }; // };
// TODO: So there is still this thing where a video should only be allowed to start // TODO: So there is still this thing where a video should only be allowed to start
// playing when the IMA SDK is ready or has failed // playing when the IMA SDK is ready or has failed
this.loadAds(); if (this.player.ended) {
this.loadAds();
} else {
// The SDK won't allow new ads to be called without receiving a contentComplete()
this.loader.contentComplete();
}
break; break;
case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
@@ -6813,7 +6963,7 @@ typeof navigator === "object" && (function (global, factory) {
this.playing = false; // Play video this.playing = false; // Play video
this.player.media.play(); silencePromise(this.player.media.play());
} }
/** /**
* Pause our video * Pause our video
@@ -6870,7 +7020,9 @@ typeof navigator === "object" && (function (global, factory) {
_this11.on('loaded', resolve); _this11.on('loaded', resolve);
_this11.player.debug.log(_this11.manager); _this11.player.debug.log(_this11.manager);
}); // Now request some new advertisements }); // Now that the manager has been destroyed set it to also be un-initialized
_this11.initialized = false; // Now request some new advertisements
_this11.requestAds(); _this11.requestAds();
}).catch(function () {}); }).catch(function () {});
@@ -7114,15 +7266,10 @@ typeof navigator === "object" && (function (global, factory) {
if (is$1.empty(src)) { if (is$1.empty(src)) {
throw new Error('Missing previewThumbnails.src config attribute'); throw new Error('Missing previewThumbnails.src config attribute');
} // If string, convert into single-element list } // Resolve promise
var urls = is$1.string(src) ? [src] : src; // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails var sortAndResolve = function sortAndResolve() {
var promises = urls.map(function (u) {
return _this2.getThumbnail(u);
});
Promise.all(promises).then(function () {
// Sort smallest to biggest (e.g., [120p, 480p, 1080p]) // Sort smallest to biggest (e.g., [120p, 480p, 1080p])
_this2.thumbnails.sort(function (x, y) { _this2.thumbnails.sort(function (x, y) {
return x.height - y.height; return x.height - y.height;
@@ -7131,7 +7278,25 @@ typeof navigator === "object" && (function (global, factory) {
_this2.player.debug.log('Preview thumbnails', _this2.thumbnails); _this2.player.debug.log('Preview thumbnails', _this2.thumbnails);
resolve(); resolve();
}); }; // Via callback()
if (is$1.function(src)) {
src(function (thumbnails) {
_this2.thumbnails = thumbnails;
sortAndResolve();
});
} // VTT urls
else {
// If string, convert into single-element list
var urls = is$1.string(src) ? [src] : src; // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
var promises = urls.map(function (u) {
return _this2.getThumbnail(u);
}); // Resolve
Promise.all(promises).then(sortAndResolve);
}
}); });
} // Process individual VTT file } // Process individual VTT file
@@ -7919,6 +8084,7 @@ typeof navigator === "object" && (function (global, factory) {
this.elements = { this.elements = {
container: null, container: null,
fullscreen: null,
captions: null, captions: null,
buttons: {}, buttons: {},
display: {}, display: {},
@@ -8093,9 +8259,11 @@ typeof navigator === "object" && (function (global, factory) {
tabindex: 0 tabindex: 0
}); });
wrap(this.media, this.elements.container); wrap(this.media, this.elements.container);
} // Add style hook } // Migrate custom properties from media to container (so they work 😉)
ui.migrateStyles.call(this); // Add style hook
ui.addStyleHook.call(this); // Setup media ui.addStyleHook.call(this); // Setup media
media.setup.call(this); // Listen for events if debugging media.setup.call(this); // Listen for events if debugging
@@ -8104,10 +8272,12 @@ typeof navigator === "object" && (function (global, factory) {
on.call(this, this.elements.container, this.config.events.join(' '), function (event) { on.call(this, this.elements.container, this.config.events.join(' '), function (event) {
_this.debug.log("event: ".concat(event.type)); _this.debug.log("event: ".concat(event.type));
}); });
} // Setup interface } // Setup fullscreen
// If embed but not fully supported, build interface now to avoid flash of controls
this.fullscreen = new Fullscreen(this); // Setup interface
// If embed but not fully supported, build interface now to avoid flash of controls
if (this.isHTML5 || this.isEmbed && !this.supported.ui) { if (this.isHTML5 || this.isEmbed && !this.supported.ui) {
ui.build.call(this); ui.build.call(this);
} // Container listeners } // Container listeners
@@ -8115,9 +8285,7 @@ typeof navigator === "object" && (function (global, factory) {
this.listeners.container(); // Global listeners this.listeners.container(); // Global listeners
this.listeners.global(); // Setup fullscreen this.listeners.global(); // Setup ads if provided
this.fullscreen = new Fullscreen(this); // Setup ads if provided
if (this.config.ads.enabled) { if (this.config.ads.enabled) {
this.ads = new Ads(this); this.ads = new Ads(this);
@@ -8126,7 +8294,7 @@ typeof navigator === "object" && (function (global, factory) {
if (this.isHTML5 && this.config.autoplay) { if (this.isHTML5 && this.config.autoplay) {
setTimeout(function () { setTimeout(function () {
return _this.play(); return silencePromise(_this.play());
}, 10); }, 10);
} // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek } // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
@@ -8163,7 +8331,7 @@ typeof navigator === "object" && (function (global, factory) {
this.ads.managerPromise.then(function () { this.ads.managerPromise.then(function () {
return _this2.ads.play(); return _this2.ads.play();
}).catch(function () { }).catch(function () {
return _this2.media.play(); return silencePromise(_this2.media.play());
}); });
} // Return the promise (for HTML5) } // Return the promise (for HTML5)
@@ -8816,7 +8984,7 @@ typeof navigator === "object" && (function (global, factory) {
var updateStorage = true; var updateStorage = true;
if (!options.includes(quality)) { if (!options.includes(quality)) {
var value = closest(options, quality); var value = closest$1(options, quality);
this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead")); this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead"));
quality = value; // Don't update storage if quality is not supported quality = value; // Don't update storage if quality is not supported
@@ -8855,41 +9023,41 @@ typeof navigator === "object" && (function (global, factory) {
this.media.loop = toggle; // Set default to be a true toggle this.media.loop = toggle; // Set default to be a true toggle
/* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle'; /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
switch (type) { switch (type) {
case 'start': case 'start':
if (this.config.loop.end && this.config.loop.end <= this.currentTime) { if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
this.config.loop.end = null; this.config.loop.end = null;
} }
this.config.loop.start = this.currentTime; this.config.loop.start = this.currentTime;
// this.config.loop.indicator.start = this.elements.display.played.value; // this.config.loop.indicator.start = this.elements.display.played.value;
break; break;
case 'end': case 'end':
if (this.config.loop.start >= this.currentTime) { if (this.config.loop.start >= this.currentTime) {
return this; return this;
} }
this.config.loop.end = this.currentTime; this.config.loop.end = this.currentTime;
// this.config.loop.indicator.end = this.elements.display.played.value; // this.config.loop.indicator.end = this.elements.display.played.value;
break; break;
case 'all': case 'all':
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
this.config.loop.indicator.start = 0;
this.config.loop.indicator.end = 100;
break;
case 'toggle':
if (this.config.loop.active) {
this.config.loop.start = 0;
this.config.loop.end = null;
} else {
this.config.loop.start = 0; this.config.loop.start = 0;
this.config.loop.end = this.duration - 2; this.config.loop.end = this.duration - 2;
} this.config.loop.indicator.start = 0;
break; this.config.loop.indicator.end = 100;
default: break;
this.config.loop.start = 0; case 'toggle':
this.config.loop.end = null; if (this.config.loop.active) {
break; this.config.loop.start = 0;
} */ this.config.loop.end = null;
} else {
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
}
break;
default:
this.config.loop.start = 0;
this.config.loop.end = null;
break;
} */
} }
/** /**
* Get current loop state * Get current loop state
@@ -8961,7 +9129,7 @@ typeof navigator === "object" && (function (global, factory) {
return null; return null;
} }
return this.media.getAttribute('poster'); return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');
} }
/** /**
* Get the current aspect ratio in use * Get the current aspect ratio in use
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+339 -171
View File
@@ -69,6 +69,42 @@ function _objectSpread2(target) {
return target; return target;
} }
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
function _objectWithoutProperties(source, excluded) {
if (source == null) return {};
var target = _objectWithoutPropertiesLoose(source, excluded);
var key, i;
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue;
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
function _slicedToArray(arr, i) { function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
} }
@@ -431,7 +467,7 @@ var isTrack = function isTrack(input) {
}; };
var isPromise = function isPromise(input) { var isPromise = function isPromise(input) {
return instanceOf$1(input, Promise); return instanceOf$1(input, Promise) && isFunction$1(input.then);
}; };
var isEmpty$1 = function isEmpty(input) { var isEmpty$1 = function isEmpty(input) {
@@ -773,12 +809,33 @@ function hasClass(element, className) {
} // Element matches selector } // Element matches selector
function matches$1(element, selector) { function matches$1(element, selector) {
var _Element = Element,
prototype = _Element.prototype;
function match() { function match() {
return Array.from(document.querySelectorAll(selector)).includes(this); return Array.from(document.querySelectorAll(selector)).includes(this);
} }
var method = match; var method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;
return method.call(element, selector);
} // Closest ancestor element matching selector (also tests element itself)
function closest(element, selector) {
var _Element2 = Element,
prototype = _Element2.prototype; // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
function closestElement() {
var el = this;
do {
if (matches$1.matches(el, selector)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
}
var method = prototype.closest || closestElement;
return method.call(element, selector); return method.call(element, selector);
} // Find all elements } // Find all elements
@@ -1050,6 +1107,19 @@ function ready() {
}).then(function () {}); }).then(function () {});
} }
/**
* Silence a Promise-like object.
* This is useful for avoiding non-harmful, but potentially confusing "uncaught
* play promise" rejection error messages.
* @param {Object} value An object that may or may not be `Promise`-like.
*/
function silencePromise(value) {
if (is$1.promise(value)) {
value.then(null, function () {});
}
}
function validateRatio(input) { function validateRatio(input) {
if (!is$1.array(input) && (!is$1.string(input) || !input.includes(':'))) { if (!is$1.array(input) && (!is$1.string(input) || !input.includes(':'))) {
return false; return false;
@@ -1118,8 +1188,8 @@ function setAspectRatio(input) {
var padding = 100 / w * h; var padding = 100 / w * h;
wrapper.style.paddingBottom = "".concat(padding, "%"); // For Vimeo we have an extra <div> to hide the standard controls and UI wrapper.style.paddingBottom = "".concat(padding, "%"); // For Vimeo we have an extra <div> to hide the standard controls and UI
if (this.isVimeo && this.supported.ui) { if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {
var height = 240; var height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);
var offset = (height - padding) / (height / 50); var offset = (height - padding) / (height / 50);
this.media.style.transform = "translateY(-".concat(offset, "%)"); this.media.style.transform = "translateY(-".concat(offset, "%)");
} else if (this.isHTML5) { } else if (this.isHTML5) {
@@ -1226,7 +1296,7 @@ var html5 = {
player.currentTime = currentTime; // Resume playing player.currentTime = currentTime; // Resume playing
if (!paused) { if (!paused) {
player.play(); silencePromise(player.play());
} }
}); // Load new source }); // Load new source
@@ -1275,7 +1345,7 @@ function dedupe(array) {
}); });
} // Get the closest value in an array } // Get the closest value in an array
function closest(array, value) { function closest$1(array, value) {
if (!is$1.array(array) || !array.length) { if (!is$1.array(array) || !array.length) {
return null; return null;
} }
@@ -1313,19 +1383,19 @@ function getPercentage(current, max) {
return (current / max * 100).toFixed(2); return (current / max * 100).toFixed(2);
} // Replace all occurances of a string in a string } // Replace all occurances of a string in a string
function replaceAll() { var replaceAll = function replaceAll() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
} // Convert to title case }; // Convert to title case
function toTitleCase() { var toTitleCase = function toTitleCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
return input.toString().replace(/\w\S*/g, function (text) { return input.toString().replace(/\w\S*/g, function (text) {
return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
}); });
} // Convert string to pascalCase }; // Convert string to pascalCase
function toPascalCase() { function toPascalCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@@ -2423,39 +2493,39 @@ var controls = {
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panels.loop)) { if (!is.element(this.elements.settings.panels.loop)) {
return; return;
} }
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
// Show the pane and tab // Show the pane and tab
toggleHidden(this.elements.settings.buttons.loop, false); toggleHidden(this.elements.settings.buttons.loop, false);
toggleHidden(this.elements.settings.panels.loop, false); toggleHidden(this.elements.settings.panels.loop, false);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.loop.options); const toggle = !is.empty(this.loop.options);
controls.toggleMenuButton.call(this, 'loop', toggle); controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
options.forEach(option => { options.forEach(option => {
const item = createElement('li'); const item = createElement('li');
const button = createElement( const button = createElement(
'button', 'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.loop), { extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
type: 'button', type: 'button',
class: this.config.classNames.control, class: this.config.classNames.control,
'data-plyr-loop-action': option, 'data-plyr-loop-action': option,
}), }),
i18n.get(option, this.config) i18n.get(option, this.config)
); );
if (['start', 'end'].includes(option)) { if (['start', 'end'].includes(option)) {
const badge = controls.createBadge.call(this, '00:00'); const badge = controls.createBadge.call(this, '00:00');
button.appendChild(badge); button.appendChild(badge);
} }
item.appendChild(button); item.appendChild(button);
list.appendChild(item); list.appendChild(item);
}); });
}, */ }, */
// Get current selected caption language // Get current selected caption language
// TODO: rework this to user the getter in the API? // TODO: rework this to user the getter in the API?
// Set a list of available captions languages // Set a list of available captions languages
@@ -3257,9 +3327,15 @@ var captions = {
meta.set(track, { meta.set(track, {
default: track.mode === 'showing' default: track.mode === 'showing'
}); // Turn off native caption rendering to avoid double captions }); // Turn off native caption rendering to avoid double captions
// Note: mode='hidden' forces a track to download. To ensure every track
// isn't downloaded at once, only 'showing' tracks should be reassigned
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
track.mode = 'hidden'; // Add event listener for cue changes if (track.mode === 'showing') {
// eslint-disable-next-line no-param-reassign
track.mode = 'hidden';
} // Add event listener for cue changes
on.call(_this, track, 'cuechange', function () { on.call(_this, track, 'cuechange', function () {
return captions.updateCues.call(_this); return captions.updateCues.call(_this);
@@ -3283,6 +3359,8 @@ var captions = {
// Toggle captions display // Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false // Used internally for the toggleCaptions method, with the passive option forced to false
toggle: function toggle(input) { toggle: function toggle(input) {
var _this2 = this;
var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
// If there's no full support // If there's no full support
@@ -3329,7 +3407,15 @@ var captions = {
controls.updateSetting.call(this, 'captions'); // Trigger event (not used internally) controls.updateSetting.call(this, 'captions'); // Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
} } // Wait for the call stack to clear before setting mode='hidden'
// on the active track - forcing the browser to download it
setTimeout(function () {
if (active && _this2.captions.toggled) {
_this2.captions.currentTrackNode.mode = 'hidden';
}
});
}, },
// Set captions by track index // Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false // Used internally for the currentTrack setter with the passive option forced to false
@@ -3410,7 +3496,7 @@ var captions = {
// If update is false it will also ignore tracks without metadata // If update is false it will also ignore tracks without metadata
// This is used to "freeze" the language options when captions.update is false // This is used to "freeze" the language options when captions.update is false
getTracks: function getTracks() { getTracks: function getTracks() {
var _this2 = this; var _this3 = this;
var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
// Handle media or textTracks missing or null // Handle media or textTracks missing or null
@@ -3418,20 +3504,20 @@ var captions = {
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata) // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks.filter(function (track) { return tracks.filter(function (track) {
return !_this2.isHTML5 || update || _this2.captions.meta.has(track); return !_this3.isHTML5 || update || _this3.captions.meta.has(track);
}).filter(function (track) { }).filter(function (track) {
return ['captions', 'subtitles'].includes(track.kind); return ['captions', 'subtitles'].includes(track.kind);
}); });
}, },
// Match tracks based on languages and get the first // Match tracks based on languages and get the first
findTrack: function findTrack(languages) { findTrack: function findTrack(languages) {
var _this3 = this; var _this4 = this;
var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var tracks = captions.getTracks.call(this); var tracks = captions.getTracks.call(this);
var sortIsDefault = function sortIsDefault(track) { var sortIsDefault = function sortIsDefault(track) {
return Number((_this3.captions.meta.get(track) || {}).default); return Number((_this4.captions.meta.get(track) || {}).default);
}; };
var sorted = Array.from(tracks).sort(function (a, b) { var sorted = Array.from(tracks).sort(function (a, b) {
@@ -3564,7 +3650,7 @@ var defaults$1 = {
// Sprite (for icons) // Sprite (for icons)
loadSprite: true, loadSprite: true,
iconPrefix: 'plyr', iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg', iconUrl: 'https://cdn.plyr.io/3.6.1/plyr.svg',
// Blank video (used to prevent errors on source change) // Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4', blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default // Quality default
@@ -3612,6 +3698,9 @@ var defaults$1 = {
fallback: true, fallback: true,
// Fallback using full viewport/window // Fallback using full viewport/window
iosNative: false // Use the native fullscreen in iOS (disables custom controls) iosNative: false // Use the native fullscreen in iOS (disables custom controls)
// Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode
// Non-ancestors of the player element will be ignored
// container: null, // defaults to the player element
}, },
// Local storage // Local storage
@@ -3849,16 +3938,16 @@ var defaults$1 = {
title: false, title: false,
speed: true, speed: true,
transparent: false, transparent: false,
// These settings require a pro or premium account to work // Whether the owner of the video has a Pro or Business account
sidedock: false, // (which allows us to properly hide controls without CSS hacks, etc)
controls: false, premium: false,
// Custom settings from Plyr // Custom settings from Plyr
referrerPolicy: null // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy referrerPolicy: null // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
}, },
// YouTube plugin // YouTube plugin
youtube: { youtube: {
noCookie: false, noCookie: true,
// Whether to use an alternative version of YouTube without cookies // Whether to use an alternative version of YouTube without cookies
rel: 0, rel: 0,
// No related vids // No related vids
@@ -3968,7 +4057,10 @@ var Fullscreen = /*#__PURE__*/function () {
y: 0 y: 0
}; // Force the use of 'full window/browser' rather than fullscreen }; // Force the use of 'full window/browser' rather than fullscreen
this.forceFallback = player.config.fullscreen.fallback === 'force'; // Register event listeners this.forceFallback = player.config.fullscreen.fallback === 'force'; // Get the fullscreen element
// Checks container is an ancestor, defaults to null
this.player.elements.fullscreen = player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container); // Register event listeners
// Handle event (incase user presses escape etc) // Handle event (incase user presses escape etc)
on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : "".concat(this.prefix, "fullscreenchange"), function () { on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : "".concat(this.prefix, "fullscreenchange"), function () {
@@ -4141,7 +4233,7 @@ var Fullscreen = /*#__PURE__*/function () {
if (browser.isIos && this.player.config.fullscreen.iosNative) { if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen(); this.target.webkitExitFullscreen();
this.player.play(); silencePromise(this.player.play());
} else if (!Fullscreen.native || this.forceFallback) { } else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(false); this.toggleFallback(false);
} else if (!this.prefix) { } else if (!this.prefix) {
@@ -4188,13 +4280,13 @@ var Fullscreen = /*#__PURE__*/function () {
} }
var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")]; var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")];
return element === this.target; return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
} // Get target element } // Get target element
}, { }, {
key: "target", key: "target",
get: function get() { get: function get() {
return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container; return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen || this.player.elements.container;
} }
}], [{ }], [{
key: "native", key: "native",
@@ -4256,7 +4348,6 @@ function loadImage(src) {
}); });
} }
// ==========================================================================
var ui = { var ui = {
addStyleHook: function addStyleHook() { addStyleHook: function addStyleHook() {
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
@@ -4391,12 +4482,7 @@ var ui = {
} // Set property synchronously to respect the call order } // Set property synchronously to respect the call order
this.media.setAttribute('poster', poster); // HTML5 uses native poster attribute this.media.setAttribute('data-poster', poster); // Wait until ui is ready
if (this.isHTML5) {
return Promise.resolve(poster);
} // Wait until ui is ready
return ready.call(this) // Load image return ready.call(this) // Load image
.then(function () { .then(function () {
@@ -4472,6 +4558,26 @@ var ui = {
this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek)); this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));
} }
},
// Migrate any custom properties from the media to the parent
migrateStyles: function migrateStyles() {
var _this5 = this;
// Loop through values (as they are the keys when the object is spread 🤔)
Object.values(_objectSpread2({}, this.media.style)) // We're only fussed about Plyr specific properties
.filter(function (key) {
return !is$1.empty(key) && key.startsWith('--plyr');
}).forEach(function (key) {
// Set on the container
_this5.elements.container.style.setProperty(key, _this5.media.style.getPropertyValue(key)); // Clean up from media element
_this5.media.style.removeProperty(key);
}); // Remove attribute if empty
if (is$1.empty(this.media.style)) {
this.media.removeAttribute('style');
}
} }
}; };
@@ -4566,7 +4672,7 @@ var Listeners = /*#__PURE__*/function () {
case 75: case 75:
// Space and K key // Space and K key
if (!repeat) { if (!repeat) {
player.togglePlay(); silencePromise(player.togglePlay());
} }
break; break;
@@ -4680,15 +4786,17 @@ var Listeners = /*#__PURE__*/function () {
removeCurrent(); // Delay the adding of classname until the focus has changed removeCurrent(); // Delay the adding of classname until the focus has changed
// This event fires before the focusin event // This event fires before the focusin event
this.focusTimer = setTimeout(function () { if (event.type !== 'focusout') {
var focused = document.activeElement; // Ignore if current focus element isn't inside the player this.focusTimer = setTimeout(function () {
var focused = document.activeElement; // Ignore if current focus element isn't inside the player
if (!elements.container.contains(focused)) { if (!elements.container.contains(focused)) {
return; return;
} }
toggleClass(document.activeElement, player.config.classNames.tabFocus, true); toggleClass(document.activeElement, player.config.classNames.tabFocus, true);
}, 10); }, 10);
}
} // Global window & document listeners } // Global window & document listeners
}, { }, {
@@ -4706,7 +4814,7 @@ var Listeners = /*#__PURE__*/function () {
once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection
toggleListener.call(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true); toggleListener.call(player, document.body, 'keydown focus blur focusout', this.setTabFocus, toggle, false, true);
} // Container listeners } // Container listeners
}, { }, {
@@ -4749,7 +4857,7 @@ var Listeners = /*#__PURE__*/function () {
}); // Set a gutter for Vimeo }); // Set a gutter for Vimeo
var setGutter = function setGutter(ratio, padding, toggle) { var setGutter = function setGutter(ratio, padding, toggle) {
if (!player.isVimeo) { if (!player.isVimeo || player.config.vimeo.premium) {
return; return;
} }
@@ -4806,7 +4914,7 @@ var Listeners = /*#__PURE__*/function () {
ratio = _setPlayerSize.ratio; // Set Vimeo gutter ratio = _setPlayerSize.ratio; // Set Vimeo gutter
setGutter(ratio, padding, isEnter); // If not using native fullscreen, we need to check for resizes of viewport setGutter(ratio, padding, isEnter); // If not using native browser fullscreen API, we need to check for resizes of viewport
if (!usingNative) { if (!usingNative) {
if (isEnter) { if (isEnter) {
@@ -4884,9 +4992,13 @@ var Listeners = /*#__PURE__*/function () {
if (player.ended) { if (player.ended) {
_this.proxy(event, player.restart, 'restart'); _this.proxy(event, player.restart, 'restart');
_this.proxy(event, player.play, 'play'); _this.proxy(event, function () {
silencePromise(player.play());
}, 'play');
} else { } else {
_this.proxy(event, player.togglePlay, 'play'); _this.proxy(event, function () {
silencePromise(player.togglePlay());
}, 'play');
} }
}); });
} // Disable right click } // Disable right click
@@ -4984,7 +5096,9 @@ var Listeners = /*#__PURE__*/function () {
if (elements.buttons.play) { if (elements.buttons.play) {
Array.from(elements.buttons.play).forEach(function (button) { Array.from(elements.buttons.play).forEach(function (button) {
_this3.bind(button, 'click', player.togglePlay, 'play'); _this3.bind(button, 'click', function () {
silencePromise(player.togglePlay());
}, 'play');
}); });
} // Pause } // Pause
@@ -5081,7 +5195,7 @@ var Listeners = /*#__PURE__*/function () {
if (play && done) { if (play && done) {
seek.removeAttribute(attribute); seek.removeAttribute(attribute);
player.play(); silencePromise(player.play());
} else if (!done && player.playing) { } else if (!done && player.playing) {
seek.setAttribute(attribute, ''); seek.setAttribute(attribute, '');
player.pause(); player.pause();
@@ -5179,7 +5293,18 @@ var Listeners = /*#__PURE__*/function () {
this.bind(elements.controls, 'mouseenter mouseleave', function (event) { this.bind(elements.controls, 'mouseenter mouseleave', function (event) {
elements.controls.hover = !player.touch && event.type === 'mouseenter'; elements.controls.hover = !player.touch && event.type === 'mouseenter';
}); // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) }); // Also update controls.hover state for any non-player children of fullscreen element (as above)
if (elements.fullscreen) {
Array.from(elements.fullscreen.children).filter(function (c) {
return !c.contains(elements.container);
}).forEach(function (child) {
_this3.bind(child, 'mouseenter mouseleave', function (event) {
elements.controls.hover = !player.touch && event.type === 'mouseenter';
});
});
} // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
@@ -5595,15 +5720,28 @@ var vimeo = {
var _this = this; var _this = this;
var player = this; var player = this;
var config = player.config.vimeo; // Get Vimeo params for the iframe var config = player.config.vimeo;
var params = buildUrlParams(extend({}, { var premium = config.premium,
referrerPolicy = config.referrerPolicy,
frameParams = _objectWithoutProperties(config, ["premium", "referrerPolicy"]); // If the owner has a pro or premium account then we can hide controls etc
if (premium) {
Object.assign(frameParams, {
controls: false,
sidedock: false
});
} // Get Vimeo params for the iframe
var params = buildUrlParams(_objectSpread2({
loop: player.config.loop.active, loop: player.config.loop.active,
autoplay: player.autoplay, autoplay: player.autoplay,
muted: player.muted, muted: player.muted,
gesture: 'media', gesture: 'media',
playsinline: !this.config.fullscreen.iosNative playsinline: !this.config.fullscreen.iosNative
}, config)); // Get the source URL or ID }, frameParams)); // Get the source URL or ID
var source = player.media.getAttribute('src'); // Get from <div> if needed var source = player.media.getAttribute('src'); // Get from <div> if needed
@@ -5617,22 +5755,27 @@ var vimeo = {
var src = format(player.config.urls.vimeo.iframe, id, params); var src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src); iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', ''); iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', ''); iframe.setAttribute('allow', 'autoplay,fullscreen,picture-in-picture'); // Set the referrer policy if required
iframe.setAttribute('allow', 'autoplay'); // Set the referrer policy if required
if (!is$1.empty(config.referrerPolicy)) { if (!is$1.empty(referrerPolicy)) {
iframe.setAttribute('referrerPolicy', config.referrerPolicy); iframe.setAttribute('referrerPolicy', referrerPolicy);
} // Get poster, if already set } // Inject the package
var poster = player.poster; // Inject the package var poster = player.poster;
if (premium) {
iframe.setAttribute('data-poster', poster);
player.media = replaceElement(iframe, player.media);
} else {
var wrapper = createElement('div', {
class: player.config.classNames.embedContainer,
'data-poster': poster
});
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
} // Get poster image
var wrapper = createElement('div', {
poster: poster,
class: player.config.classNames.embedContainer
});
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media); // Get poster image
fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) { fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) {
if (is$1.empty(response)) { if (is$1.empty(response)) {
@@ -5716,6 +5859,9 @@ var vimeo = {
player.embed.setPlaybackRate(input).then(function () { player.embed.setPlaybackRate(input).then(function () {
speed = input; speed = input;
triggerEvent.call(player, player.media, 'ratechange'); triggerEvent.call(player, player.media, 'ratechange');
}).catch(function () {
// Cannot set Playback Rate, Video is probably not on Pro account
player.options.speed = [1];
}); });
} }
}); // Volume }); // Volume
@@ -6002,7 +6148,7 @@ var youtube = {
var container = createElement('div', { var container = createElement('div', {
id: id, id: id,
poster: poster 'data-poster': poster
}); });
player.media = replaceElement(container, player.media); // Id to poster wrapper player.media = replaceElement(container, player.media); // Id to poster wrapper
@@ -6321,14 +6467,12 @@ var media = {
class: this.config.classNames.video class: this.config.classNames.video
}); // Wrap the video in a container }); // Wrap the video in a container
wrap(this.media, this.elements.wrapper); // Faux poster container wrap(this.media, this.elements.wrapper); // Poster image container
if (this.isEmbed) { this.elements.poster = createElement('div', {
this.elements.poster = createElement('div', { class: this.config.classNames.poster
class: this.config.classNames.poster });
}); this.elements.wrapper.appendChild(this.elements.poster);
this.elements.wrapper.appendChild(this.elements.poster);
}
} }
if (this.isHTML5) { if (this.isHTML5) {
@@ -6455,6 +6599,8 @@ var Ads = /*#__PURE__*/function () {
* mobile devices, this initialization is done as the result of a user action. * mobile devices, this initialization is done as the result of a user action.
*/ */
value: function setupIMA() { value: function setupIMA() {
var _this4 = this;
// Create the container for our advertisements // Create the container for our advertisements
this.elements.container = createElement('div', { this.elements.container = createElement('div', {
class: this.player.config.classNames.ads class: this.player.config.classNames.ads
@@ -6467,7 +6613,16 @@ var Ads = /*#__PURE__*/function () {
google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); // We assume the adContainer is the video container of the plyr element that will house the ads google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); // We assume the adContainer is the video container of the plyr element that will house the ads
this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); // Request video ads to be pre-loaded this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); // Create ads loader
this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) {
return _this4.onAdsManagerLoaded(event);
}, false);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
return _this4.onAdError(error);
}, false); // Request video ads to be pre-loaded
this.requestAds(); this.requestAds();
} }
@@ -6478,21 +6633,10 @@ var Ads = /*#__PURE__*/function () {
}, { }, {
key: "requestAds", key: "requestAds",
value: function requestAds() { value: function requestAds() {
var _this4 = this;
var container = this.player.elements.container; var container = this.player.elements.container;
try { try {
// Create ads loader // Request video ads
this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) {
return _this4.onAdsManagerLoaded(event);
}, false);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
return _this4.onAdError(error);
}, false); // Request video ads
var request = new google.ima.AdsRequest(); var request = new google.ima.AdsRequest();
request.adTagUrl = this.tagUrl; // Specify the linear and nonlinear slot sizes. This helps the SDK request.adTagUrl = this.tagUrl; // Specify the linear and nonlinear slot sizes. This helps the SDK
// to select the correct creative if multiple are returned // to select the correct creative if multiple are returned
@@ -6671,7 +6815,13 @@ var Ads = /*#__PURE__*/function () {
// }; // };
// TODO: So there is still this thing where a video should only be allowed to start // TODO: So there is still this thing where a video should only be allowed to start
// playing when the IMA SDK is ready or has failed // playing when the IMA SDK is ready or has failed
this.loadAds(); if (this.player.ended) {
this.loadAds();
} else {
// The SDK won't allow new ads to be called without receiving a contentComplete()
this.loader.contentComplete();
}
break; break;
case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
@@ -6807,7 +6957,7 @@ var Ads = /*#__PURE__*/function () {
this.playing = false; // Play video this.playing = false; // Play video
this.player.media.play(); silencePromise(this.player.media.play());
} }
/** /**
* Pause our video * Pause our video
@@ -6864,7 +7014,9 @@ var Ads = /*#__PURE__*/function () {
_this11.on('loaded', resolve); _this11.on('loaded', resolve);
_this11.player.debug.log(_this11.manager); _this11.player.debug.log(_this11.manager);
}); // Now request some new advertisements }); // Now that the manager has been destroyed set it to also be un-initialized
_this11.initialized = false; // Now request some new advertisements
_this11.requestAds(); _this11.requestAds();
}).catch(function () {}); }).catch(function () {});
@@ -7108,15 +7260,10 @@ var PreviewThumbnails = /*#__PURE__*/function () {
if (is$1.empty(src)) { if (is$1.empty(src)) {
throw new Error('Missing previewThumbnails.src config attribute'); throw new Error('Missing previewThumbnails.src config attribute');
} // If string, convert into single-element list } // Resolve promise
var urls = is$1.string(src) ? [src] : src; // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails var sortAndResolve = function sortAndResolve() {
var promises = urls.map(function (u) {
return _this2.getThumbnail(u);
});
Promise.all(promises).then(function () {
// Sort smallest to biggest (e.g., [120p, 480p, 1080p]) // Sort smallest to biggest (e.g., [120p, 480p, 1080p])
_this2.thumbnails.sort(function (x, y) { _this2.thumbnails.sort(function (x, y) {
return x.height - y.height; return x.height - y.height;
@@ -7125,7 +7272,25 @@ var PreviewThumbnails = /*#__PURE__*/function () {
_this2.player.debug.log('Preview thumbnails', _this2.thumbnails); _this2.player.debug.log('Preview thumbnails', _this2.thumbnails);
resolve(); resolve();
}); }; // Via callback()
if (is$1.function(src)) {
src(function (thumbnails) {
_this2.thumbnails = thumbnails;
sortAndResolve();
});
} // VTT urls
else {
// If string, convert into single-element list
var urls = is$1.string(src) ? [src] : src; // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
var promises = urls.map(function (u) {
return _this2.getThumbnail(u);
}); // Resolve
Promise.all(promises).then(sortAndResolve);
}
}); });
} // Process individual VTT file } // Process individual VTT file
@@ -7913,6 +8078,7 @@ var Plyr = /*#__PURE__*/function () {
this.elements = { this.elements = {
container: null, container: null,
fullscreen: null,
captions: null, captions: null,
buttons: {}, buttons: {},
display: {}, display: {},
@@ -8087,9 +8253,11 @@ var Plyr = /*#__PURE__*/function () {
tabindex: 0 tabindex: 0
}); });
wrap(this.media, this.elements.container); wrap(this.media, this.elements.container);
} // Add style hook } // Migrate custom properties from media to container (so they work 😉)
ui.migrateStyles.call(this); // Add style hook
ui.addStyleHook.call(this); // Setup media ui.addStyleHook.call(this); // Setup media
media.setup.call(this); // Listen for events if debugging media.setup.call(this); // Listen for events if debugging
@@ -8098,10 +8266,12 @@ var Plyr = /*#__PURE__*/function () {
on.call(this, this.elements.container, this.config.events.join(' '), function (event) { on.call(this, this.elements.container, this.config.events.join(' '), function (event) {
_this.debug.log("event: ".concat(event.type)); _this.debug.log("event: ".concat(event.type));
}); });
} // Setup interface } // Setup fullscreen
// If embed but not fully supported, build interface now to avoid flash of controls
this.fullscreen = new Fullscreen(this); // Setup interface
// If embed but not fully supported, build interface now to avoid flash of controls
if (this.isHTML5 || this.isEmbed && !this.supported.ui) { if (this.isHTML5 || this.isEmbed && !this.supported.ui) {
ui.build.call(this); ui.build.call(this);
} // Container listeners } // Container listeners
@@ -8109,9 +8279,7 @@ var Plyr = /*#__PURE__*/function () {
this.listeners.container(); // Global listeners this.listeners.container(); // Global listeners
this.listeners.global(); // Setup fullscreen this.listeners.global(); // Setup ads if provided
this.fullscreen = new Fullscreen(this); // Setup ads if provided
if (this.config.ads.enabled) { if (this.config.ads.enabled) {
this.ads = new Ads(this); this.ads = new Ads(this);
@@ -8120,7 +8288,7 @@ var Plyr = /*#__PURE__*/function () {
if (this.isHTML5 && this.config.autoplay) { if (this.isHTML5 && this.config.autoplay) {
setTimeout(function () { setTimeout(function () {
return _this.play(); return silencePromise(_this.play());
}, 10); }, 10);
} // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek } // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
@@ -8157,7 +8325,7 @@ var Plyr = /*#__PURE__*/function () {
this.ads.managerPromise.then(function () { this.ads.managerPromise.then(function () {
return _this2.ads.play(); return _this2.ads.play();
}).catch(function () { }).catch(function () {
return _this2.media.play(); return silencePromise(_this2.media.play());
}); });
} // Return the promise (for HTML5) } // Return the promise (for HTML5)
@@ -8810,7 +8978,7 @@ var Plyr = /*#__PURE__*/function () {
var updateStorage = true; var updateStorage = true;
if (!options.includes(quality)) { if (!options.includes(quality)) {
var value = closest(options, quality); var value = closest$1(options, quality);
this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead")); this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead"));
quality = value; // Don't update storage if quality is not supported quality = value; // Don't update storage if quality is not supported
@@ -8849,41 +9017,41 @@ var Plyr = /*#__PURE__*/function () {
this.media.loop = toggle; // Set default to be a true toggle this.media.loop = toggle; // Set default to be a true toggle
/* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle'; /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
switch (type) { switch (type) {
case 'start': case 'start':
if (this.config.loop.end && this.config.loop.end <= this.currentTime) { if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
this.config.loop.end = null; this.config.loop.end = null;
} }
this.config.loop.start = this.currentTime; this.config.loop.start = this.currentTime;
// this.config.loop.indicator.start = this.elements.display.played.value; // this.config.loop.indicator.start = this.elements.display.played.value;
break; break;
case 'end': case 'end':
if (this.config.loop.start >= this.currentTime) { if (this.config.loop.start >= this.currentTime) {
return this; return this;
} }
this.config.loop.end = this.currentTime; this.config.loop.end = this.currentTime;
// this.config.loop.indicator.end = this.elements.display.played.value; // this.config.loop.indicator.end = this.elements.display.played.value;
break; break;
case 'all': case 'all':
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
this.config.loop.indicator.start = 0;
this.config.loop.indicator.end = 100;
break;
case 'toggle':
if (this.config.loop.active) {
this.config.loop.start = 0;
this.config.loop.end = null;
} else {
this.config.loop.start = 0; this.config.loop.start = 0;
this.config.loop.end = this.duration - 2; this.config.loop.end = this.duration - 2;
} this.config.loop.indicator.start = 0;
break; this.config.loop.indicator.end = 100;
default: break;
this.config.loop.start = 0; case 'toggle':
this.config.loop.end = null; if (this.config.loop.active) {
break; this.config.loop.start = 0;
} */ this.config.loop.end = null;
} else {
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
}
break;
default:
this.config.loop.start = 0;
this.config.loop.end = null;
break;
} */
} }
/** /**
* Get current loop state * Get current loop state
@@ -8955,7 +9123,7 @@ var Plyr = /*#__PURE__*/function () {
return null; return null;
} }
return this.media.getAttribute('poster'); return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');
} }
/** /**
* Get the current aspect ratio in use * Get the current aspect ratio in use
+348 -173
View File
@@ -269,7 +269,7 @@ typeof navigator === "object" && (function (global, factory) {
(module.exports = function (key, value) { (module.exports = function (key, value) {
return sharedStore[key] || (sharedStore[key] = value !== undefined ? value : {}); return sharedStore[key] || (sharedStore[key] = value !== undefined ? value : {});
})('versions', []).push({ })('versions', []).push({
version: '3.6.4', version: '3.6.5',
mode: 'global', mode: 'global',
copyright: '© 2020 Denis Pushkarev (zloirock.ru)' copyright: '© 2020 Denis Pushkarev (zloirock.ru)'
}); });
@@ -3061,7 +3061,7 @@ typeof navigator === "object" && (function (global, factory) {
var INVALID_PORT = 'Invalid port'; var INVALID_PORT = 'Invalid port';
var ALPHA = /[A-Za-z]/; var ALPHA = /[A-Za-z]/;
var ALPHANUMERIC = /[\d+\-.A-Za-z]/; var ALPHANUMERIC = /[\d+-.A-Za-z]/;
var DIGIT = /\d/; var DIGIT = /\d/;
var HEX_START = /^(0x|0X)/; var HEX_START = /^(0x|0X)/;
var OCT = /^[0-7]+$/; var OCT = /^[0-7]+$/;
@@ -4123,6 +4123,42 @@ typeof navigator === "object" && (function (global, factory) {
return target; return target;
} }
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
function _objectWithoutProperties(source, excluded) {
if (source == null) return {};
var target = _objectWithoutPropertiesLoose(source, excluded);
var key, i;
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue;
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
function _slicedToArray(arr, i) { function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
} }
@@ -6008,7 +6044,13 @@ typeof navigator === "object" && (function (global, factory) {
defer = functionBindContext(port.postMessage, port, 1); defer = functionBindContext(port.postMessage, port, 1);
// Browsers with postMessage, skip WebWorkers // Browsers with postMessage, skip WebWorkers
// IE8 has postMessage, but it's sync & typeof its postMessage is 'object' // IE8 has postMessage, but it's sync & typeof its postMessage is 'object'
} else if (global_1.addEventListener && typeof postMessage == 'function' && !global_1.importScripts && !fails(post)) { } else if (
global_1.addEventListener &&
typeof postMessage == 'function' &&
!global_1.importScripts &&
!fails(post) &&
location.protocol !== 'file:'
) {
defer = post; defer = post;
global_1.addEventListener('message', listener, false); global_1.addEventListener('message', listener, false);
// IE8- // IE8-
@@ -6617,7 +6659,7 @@ typeof navigator === "object" && (function (global, factory) {
}; };
var isPromise = function isPromise(input) { var isPromise = function isPromise(input) {
return instanceOf$1(input, Promise); return instanceOf$1(input, Promise) && isFunction$1(input.then);
}; };
var isEmpty$1 = function isEmpty(input) { var isEmpty$1 = function isEmpty(input) {
@@ -7009,12 +7051,33 @@ typeof navigator === "object" && (function (global, factory) {
} // Element matches selector } // Element matches selector
function matches$1(element, selector) { function matches$1(element, selector) {
var _Element = Element,
prototype = _Element.prototype;
function match() { function match() {
return Array.from(document.querySelectorAll(selector)).includes(this); return Array.from(document.querySelectorAll(selector)).includes(this);
} }
var method = match; var method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;
return method.call(element, selector);
} // Closest ancestor element matching selector (also tests element itself)
function closest(element, selector) {
var _Element2 = Element,
prototype = _Element2.prototype; // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
function closestElement() {
var el = this;
do {
if (matches$1.matches(el, selector)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
}
var method = prototype.closest || closestElement;
return method.call(element, selector); return method.call(element, selector);
} // Find all elements } // Find all elements
@@ -7286,6 +7349,19 @@ typeof navigator === "object" && (function (global, factory) {
}).then(function () {}); }).then(function () {});
} }
/**
* Silence a Promise-like object.
* This is useful for avoiding non-harmful, but potentially confusing "uncaught
* play promise" rejection error messages.
* @param {Object} value An object that may or may not be `Promise`-like.
*/
function silencePromise(value) {
if (is$1.promise(value)) {
value.then(null, function () {});
}
}
function validateRatio(input) { function validateRatio(input) {
if (!is$1.array(input) && (!is$1.string(input) || !input.includes(':'))) { if (!is$1.array(input) && (!is$1.string(input) || !input.includes(':'))) {
return false; return false;
@@ -7354,8 +7430,8 @@ typeof navigator === "object" && (function (global, factory) {
var padding = 100 / w * h; var padding = 100 / w * h;
wrapper.style.paddingBottom = "".concat(padding, "%"); // For Vimeo we have an extra <div> to hide the standard controls and UI wrapper.style.paddingBottom = "".concat(padding, "%"); // For Vimeo we have an extra <div> to hide the standard controls and UI
if (this.isVimeo && this.supported.ui) { if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {
var height = 240; var height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);
var offset = (height - padding) / (height / 50); var offset = (height - padding) / (height / 50);
this.media.style.transform = "translateY(-".concat(offset, "%)"); this.media.style.transform = "translateY(-".concat(offset, "%)");
} else if (this.isHTML5) { } else if (this.isHTML5) {
@@ -7461,7 +7537,7 @@ typeof navigator === "object" && (function (global, factory) {
player.currentTime = currentTime; // Resume playing player.currentTime = currentTime; // Resume playing
if (!paused) { if (!paused) {
player.play(); silencePromise(player.play());
} }
}); // Load new source }); // Load new source
@@ -7508,7 +7584,7 @@ typeof navigator === "object" && (function (global, factory) {
}); });
} // Get the closest value in an array } // Get the closest value in an array
function closest(array, value) { function closest$1(array, value) {
if (!is$1.array(array) || !array.length) { if (!is$1.array(array) || !array.length) {
return null; return null;
} }
@@ -7625,19 +7701,19 @@ typeof navigator === "object" && (function (global, factory) {
return (current / max * 100).toFixed(2); return (current / max * 100).toFixed(2);
} // Replace all occurances of a string in a string } // Replace all occurances of a string in a string
function replaceAll() { var replaceAll = function replaceAll() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
} // Convert to title case }; // Convert to title case
function toTitleCase() { var toTitleCase = function toTitleCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
return input.toString().replace(/\w\S*/g, function (text) { return input.toString().replace(/\w\S*/g, function (text) {
return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
}); });
} // Convert string to pascalCase }; // Convert string to pascalCase
function toPascalCase() { function toPascalCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@@ -8744,39 +8820,39 @@ typeof navigator === "object" && (function (global, factory) {
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panels.loop)) { if (!is.element(this.elements.settings.panels.loop)) {
return; return;
} }
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
// Show the pane and tab // Show the pane and tab
toggleHidden(this.elements.settings.buttons.loop, false); toggleHidden(this.elements.settings.buttons.loop, false);
toggleHidden(this.elements.settings.panels.loop, false); toggleHidden(this.elements.settings.panels.loop, false);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.loop.options); const toggle = !is.empty(this.loop.options);
controls.toggleMenuButton.call(this, 'loop', toggle); controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
options.forEach(option => { options.forEach(option => {
const item = createElement('li'); const item = createElement('li');
const button = createElement( const button = createElement(
'button', 'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.loop), { extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
type: 'button', type: 'button',
class: this.config.classNames.control, class: this.config.classNames.control,
'data-plyr-loop-action': option, 'data-plyr-loop-action': option,
}), }),
i18n.get(option, this.config) i18n.get(option, this.config)
); );
if (['start', 'end'].includes(option)) { if (['start', 'end'].includes(option)) {
const badge = controls.createBadge.call(this, '00:00'); const badge = controls.createBadge.call(this, '00:00');
button.appendChild(badge); button.appendChild(badge);
} }
item.appendChild(button); item.appendChild(button);
list.appendChild(item); list.appendChild(item);
}); });
}, */ }, */
// Get current selected caption language // Get current selected caption language
// TODO: rework this to user the getter in the API? // TODO: rework this to user the getter in the API?
// Set a list of available captions languages // Set a list of available captions languages
@@ -9578,9 +9654,15 @@ typeof navigator === "object" && (function (global, factory) {
meta.set(track, { meta.set(track, {
default: track.mode === 'showing' default: track.mode === 'showing'
}); // Turn off native caption rendering to avoid double captions }); // Turn off native caption rendering to avoid double captions
// Note: mode='hidden' forces a track to download. To ensure every track
// isn't downloaded at once, only 'showing' tracks should be reassigned
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
track.mode = 'hidden'; // Add event listener for cue changes if (track.mode === 'showing') {
// eslint-disable-next-line no-param-reassign
track.mode = 'hidden';
} // Add event listener for cue changes
on.call(_this, track, 'cuechange', function () { on.call(_this, track, 'cuechange', function () {
return captions.updateCues.call(_this); return captions.updateCues.call(_this);
@@ -9604,6 +9686,8 @@ typeof navigator === "object" && (function (global, factory) {
// Toggle captions display // Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false // Used internally for the toggleCaptions method, with the passive option forced to false
toggle: function toggle(input) { toggle: function toggle(input) {
var _this2 = this;
var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
// If there's no full support // If there's no full support
@@ -9650,7 +9734,15 @@ typeof navigator === "object" && (function (global, factory) {
controls.updateSetting.call(this, 'captions'); // Trigger event (not used internally) controls.updateSetting.call(this, 'captions'); // Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
} } // Wait for the call stack to clear before setting mode='hidden'
// on the active track - forcing the browser to download it
setTimeout(function () {
if (active && _this2.captions.toggled) {
_this2.captions.currentTrackNode.mode = 'hidden';
}
});
}, },
// Set captions by track index // Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false // Used internally for the currentTrack setter with the passive option forced to false
@@ -9731,7 +9823,7 @@ typeof navigator === "object" && (function (global, factory) {
// If update is false it will also ignore tracks without metadata // If update is false it will also ignore tracks without metadata
// This is used to "freeze" the language options when captions.update is false // This is used to "freeze" the language options when captions.update is false
getTracks: function getTracks() { getTracks: function getTracks() {
var _this2 = this; var _this3 = this;
var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
// Handle media or textTracks missing or null // Handle media or textTracks missing or null
@@ -9739,20 +9831,20 @@ typeof navigator === "object" && (function (global, factory) {
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata) // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks.filter(function (track) { return tracks.filter(function (track) {
return !_this2.isHTML5 || update || _this2.captions.meta.has(track); return !_this3.isHTML5 || update || _this3.captions.meta.has(track);
}).filter(function (track) { }).filter(function (track) {
return ['captions', 'subtitles'].includes(track.kind); return ['captions', 'subtitles'].includes(track.kind);
}); });
}, },
// Match tracks based on languages and get the first // Match tracks based on languages and get the first
findTrack: function findTrack(languages) { findTrack: function findTrack(languages) {
var _this3 = this; var _this4 = this;
var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var tracks = captions.getTracks.call(this); var tracks = captions.getTracks.call(this);
var sortIsDefault = function sortIsDefault(track) { var sortIsDefault = function sortIsDefault(track) {
return Number((_this3.captions.meta.get(track) || {}).default); return Number((_this4.captions.meta.get(track) || {}).default);
}; };
var sorted = Array.from(tracks).sort(function (a, b) { var sorted = Array.from(tracks).sort(function (a, b) {
@@ -9885,7 +9977,7 @@ typeof navigator === "object" && (function (global, factory) {
// Sprite (for icons) // Sprite (for icons)
loadSprite: true, loadSprite: true,
iconPrefix: 'plyr', iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg', iconUrl: 'https://cdn.plyr.io/3.6.1/plyr.svg',
// Blank video (used to prevent errors on source change) // Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4', blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default // Quality default
@@ -9933,6 +10025,9 @@ typeof navigator === "object" && (function (global, factory) {
fallback: true, fallback: true,
// Fallback using full viewport/window // Fallback using full viewport/window
iosNative: false // Use the native fullscreen in iOS (disables custom controls) iosNative: false // Use the native fullscreen in iOS (disables custom controls)
// Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode
// Non-ancestors of the player element will be ignored
// container: null, // defaults to the player element
}, },
// Local storage // Local storage
@@ -10170,16 +10265,16 @@ typeof navigator === "object" && (function (global, factory) {
title: false, title: false,
speed: true, speed: true,
transparent: false, transparent: false,
// These settings require a pro or premium account to work // Whether the owner of the video has a Pro or Business account
sidedock: false, // (which allows us to properly hide controls without CSS hacks, etc)
controls: false, premium: false,
// Custom settings from Plyr // Custom settings from Plyr
referrerPolicy: null // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy referrerPolicy: null // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
}, },
// YouTube plugin // YouTube plugin
youtube: { youtube: {
noCookie: false, noCookie: true,
// Whether to use an alternative version of YouTube without cookies // Whether to use an alternative version of YouTube without cookies
rel: 0, rel: 0,
// No related vids // No related vids
@@ -10289,7 +10384,10 @@ typeof navigator === "object" && (function (global, factory) {
y: 0 y: 0
}; // Force the use of 'full window/browser' rather than fullscreen }; // Force the use of 'full window/browser' rather than fullscreen
this.forceFallback = player.config.fullscreen.fallback === 'force'; // Register event listeners this.forceFallback = player.config.fullscreen.fallback === 'force'; // Get the fullscreen element
// Checks container is an ancestor, defaults to null
this.player.elements.fullscreen = player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container); // Register event listeners
// Handle event (incase user presses escape etc) // Handle event (incase user presses escape etc)
on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : "".concat(this.prefix, "fullscreenchange"), function () { on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : "".concat(this.prefix, "fullscreenchange"), function () {
@@ -10462,7 +10560,7 @@ typeof navigator === "object" && (function (global, factory) {
if (browser.isIos && this.player.config.fullscreen.iosNative) { if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen(); this.target.webkitExitFullscreen();
this.player.play(); silencePromise(this.player.play());
} else if (!Fullscreen.native || this.forceFallback) { } else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(false); this.toggleFallback(false);
} else if (!this.prefix) { } else if (!this.prefix) {
@@ -10509,13 +10607,13 @@ typeof navigator === "object" && (function (global, factory) {
} }
var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")]; var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")];
return element === this.target; return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
} // Get target element } // Get target element
}, { }, {
key: "target", key: "target",
get: function get() { get: function get() {
return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container; return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen || this.player.elements.container;
} }
}], [{ }], [{
key: "native", key: "native",
@@ -10724,12 +10822,7 @@ typeof navigator === "object" && (function (global, factory) {
} // Set property synchronously to respect the call order } // Set property synchronously to respect the call order
this.media.setAttribute('poster', poster); // HTML5 uses native poster attribute this.media.setAttribute('data-poster', poster); // Wait until ui is ready
if (this.isHTML5) {
return Promise.resolve(poster);
} // Wait until ui is ready
return ready.call(this) // Load image return ready.call(this) // Load image
.then(function () { .then(function () {
@@ -10805,6 +10898,26 @@ typeof navigator === "object" && (function (global, factory) {
this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek)); this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));
} }
},
// Migrate any custom properties from the media to the parent
migrateStyles: function migrateStyles() {
var _this5 = this;
// Loop through values (as they are the keys when the object is spread 🤔)
Object.values(_objectSpread2({}, this.media.style)) // We're only fussed about Plyr specific properties
.filter(function (key) {
return !is$1.empty(key) && key.startsWith('--plyr');
}).forEach(function (key) {
// Set on the container
_this5.elements.container.style.setProperty(key, _this5.media.style.getPropertyValue(key)); // Clean up from media element
_this5.media.style.removeProperty(key);
}); // Remove attribute if empty
if (is$1.empty(this.media.style)) {
this.media.removeAttribute('style');
}
} }
}; };
@@ -10899,7 +11012,7 @@ typeof navigator === "object" && (function (global, factory) {
case 75: case 75:
// Space and K key // Space and K key
if (!repeat) { if (!repeat) {
player.togglePlay(); silencePromise(player.togglePlay());
} }
break; break;
@@ -11013,15 +11126,17 @@ typeof navigator === "object" && (function (global, factory) {
removeCurrent(); // Delay the adding of classname until the focus has changed removeCurrent(); // Delay the adding of classname until the focus has changed
// This event fires before the focusin event // This event fires before the focusin event
this.focusTimer = setTimeout(function () { if (event.type !== 'focusout') {
var focused = document.activeElement; // Ignore if current focus element isn't inside the player this.focusTimer = setTimeout(function () {
var focused = document.activeElement; // Ignore if current focus element isn't inside the player
if (!elements.container.contains(focused)) { if (!elements.container.contains(focused)) {
return; return;
} }
toggleClass(document.activeElement, player.config.classNames.tabFocus, true); toggleClass(document.activeElement, player.config.classNames.tabFocus, true);
}, 10); }, 10);
}
} // Global window & document listeners } // Global window & document listeners
}, { }, {
@@ -11039,7 +11154,7 @@ typeof navigator === "object" && (function (global, factory) {
once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection
toggleListener.call(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true); toggleListener.call(player, document.body, 'keydown focus blur focusout', this.setTabFocus, toggle, false, true);
} // Container listeners } // Container listeners
}, { }, {
@@ -11082,7 +11197,7 @@ typeof navigator === "object" && (function (global, factory) {
}); // Set a gutter for Vimeo }); // Set a gutter for Vimeo
var setGutter = function setGutter(ratio, padding, toggle) { var setGutter = function setGutter(ratio, padding, toggle) {
if (!player.isVimeo) { if (!player.isVimeo || player.config.vimeo.premium) {
return; return;
} }
@@ -11139,7 +11254,7 @@ typeof navigator === "object" && (function (global, factory) {
ratio = _setPlayerSize.ratio; // Set Vimeo gutter ratio = _setPlayerSize.ratio; // Set Vimeo gutter
setGutter(ratio, padding, isEnter); // If not using native fullscreen, we need to check for resizes of viewport setGutter(ratio, padding, isEnter); // If not using native browser fullscreen API, we need to check for resizes of viewport
if (!usingNative) { if (!usingNative) {
if (isEnter) { if (isEnter) {
@@ -11217,9 +11332,13 @@ typeof navigator === "object" && (function (global, factory) {
if (player.ended) { if (player.ended) {
_this.proxy(event, player.restart, 'restart'); _this.proxy(event, player.restart, 'restart');
_this.proxy(event, player.play, 'play'); _this.proxy(event, function () {
silencePromise(player.play());
}, 'play');
} else { } else {
_this.proxy(event, player.togglePlay, 'play'); _this.proxy(event, function () {
silencePromise(player.togglePlay());
}, 'play');
} }
}); });
} // Disable right click } // Disable right click
@@ -11317,7 +11436,9 @@ typeof navigator === "object" && (function (global, factory) {
if (elements.buttons.play) { if (elements.buttons.play) {
Array.from(elements.buttons.play).forEach(function (button) { Array.from(elements.buttons.play).forEach(function (button) {
_this3.bind(button, 'click', player.togglePlay, 'play'); _this3.bind(button, 'click', function () {
silencePromise(player.togglePlay());
}, 'play');
}); });
} // Pause } // Pause
@@ -11414,7 +11535,7 @@ typeof navigator === "object" && (function (global, factory) {
if (play && done) { if (play && done) {
seek.removeAttribute(attribute); seek.removeAttribute(attribute);
player.play(); silencePromise(player.play());
} else if (!done && player.playing) { } else if (!done && player.playing) {
seek.setAttribute(attribute, ''); seek.setAttribute(attribute, '');
player.pause(); player.pause();
@@ -11512,7 +11633,18 @@ typeof navigator === "object" && (function (global, factory) {
this.bind(elements.controls, 'mouseenter mouseleave', function (event) { this.bind(elements.controls, 'mouseenter mouseleave', function (event) {
elements.controls.hover = !player.touch && event.type === 'mouseenter'; elements.controls.hover = !player.touch && event.type === 'mouseenter';
}); // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) }); // Also update controls.hover state for any non-player children of fullscreen element (as above)
if (elements.fullscreen) {
Array.from(elements.fullscreen.children).filter(function (c) {
return !c.contains(elements.container);
}).forEach(function (child) {
_this3.bind(child, 'mouseenter mouseleave', function (event) {
elements.controls.hover = !player.touch && event.type === 'mouseenter';
});
});
} // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
@@ -11981,15 +12113,28 @@ typeof navigator === "object" && (function (global, factory) {
var _this = this; var _this = this;
var player = this; var player = this;
var config = player.config.vimeo; // Get Vimeo params for the iframe var config = player.config.vimeo;
var params = buildUrlParams(extend({}, { var premium = config.premium,
referrerPolicy = config.referrerPolicy,
frameParams = _objectWithoutProperties(config, ["premium", "referrerPolicy"]); // If the owner has a pro or premium account then we can hide controls etc
if (premium) {
Object.assign(frameParams, {
controls: false,
sidedock: false
});
} // Get Vimeo params for the iframe
var params = buildUrlParams(_objectSpread2({
loop: player.config.loop.active, loop: player.config.loop.active,
autoplay: player.autoplay, autoplay: player.autoplay,
muted: player.muted, muted: player.muted,
gesture: 'media', gesture: 'media',
playsinline: !this.config.fullscreen.iosNative playsinline: !this.config.fullscreen.iosNative
}, config)); // Get the source URL or ID }, frameParams)); // Get the source URL or ID
var source = player.media.getAttribute('src'); // Get from <div> if needed var source = player.media.getAttribute('src'); // Get from <div> if needed
@@ -12003,22 +12148,27 @@ typeof navigator === "object" && (function (global, factory) {
var src = format(player.config.urls.vimeo.iframe, id, params); var src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src); iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', ''); iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', ''); iframe.setAttribute('allow', 'autoplay,fullscreen,picture-in-picture'); // Set the referrer policy if required
iframe.setAttribute('allow', 'autoplay'); // Set the referrer policy if required
if (!is$1.empty(config.referrerPolicy)) { if (!is$1.empty(referrerPolicy)) {
iframe.setAttribute('referrerPolicy', config.referrerPolicy); iframe.setAttribute('referrerPolicy', referrerPolicy);
} // Get poster, if already set } // Inject the package
var poster = player.poster; // Inject the package var poster = player.poster;
if (premium) {
iframe.setAttribute('data-poster', poster);
player.media = replaceElement(iframe, player.media);
} else {
var wrapper = createElement('div', {
class: player.config.classNames.embedContainer,
'data-poster': poster
});
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
} // Get poster image
var wrapper = createElement('div', {
poster: poster,
class: player.config.classNames.embedContainer
});
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media); // Get poster image
fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) { fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) {
if (is$1.empty(response)) { if (is$1.empty(response)) {
@@ -12102,6 +12252,9 @@ typeof navigator === "object" && (function (global, factory) {
player.embed.setPlaybackRate(input).then(function () { player.embed.setPlaybackRate(input).then(function () {
speed = input; speed = input;
triggerEvent.call(player, player.media, 'ratechange'); triggerEvent.call(player, player.media, 'ratechange');
}).catch(function () {
// Cannot set Playback Rate, Video is probably not on Pro account
player.options.speed = [1];
}); });
} }
}); // Volume }); // Volume
@@ -12386,7 +12539,7 @@ typeof navigator === "object" && (function (global, factory) {
var container = createElement('div', { var container = createElement('div', {
id: id, id: id,
poster: poster 'data-poster': poster
}); });
player.media = replaceElement(container, player.media); // Id to poster wrapper player.media = replaceElement(container, player.media); // Id to poster wrapper
@@ -12704,14 +12857,12 @@ typeof navigator === "object" && (function (global, factory) {
class: this.config.classNames.video class: this.config.classNames.video
}); // Wrap the video in a container }); // Wrap the video in a container
wrap$1(this.media, this.elements.wrapper); // Faux poster container wrap$1(this.media, this.elements.wrapper); // Poster image container
if (this.isEmbed) { this.elements.poster = createElement('div', {
this.elements.poster = createElement('div', { class: this.config.classNames.poster
class: this.config.classNames.poster });
}); this.elements.wrapper.appendChild(this.elements.poster);
this.elements.wrapper.appendChild(this.elements.poster);
}
} }
if (this.isHTML5) { if (this.isHTML5) {
@@ -12838,6 +12989,8 @@ typeof navigator === "object" && (function (global, factory) {
* mobile devices, this initialization is done as the result of a user action. * mobile devices, this initialization is done as the result of a user action.
*/ */
value: function setupIMA() { value: function setupIMA() {
var _this4 = this;
// Create the container for our advertisements // Create the container for our advertisements
this.elements.container = createElement('div', { this.elements.container = createElement('div', {
class: this.player.config.classNames.ads class: this.player.config.classNames.ads
@@ -12850,7 +13003,16 @@ typeof navigator === "object" && (function (global, factory) {
google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); // We assume the adContainer is the video container of the plyr element that will house the ads google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); // We assume the adContainer is the video container of the plyr element that will house the ads
this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); // Request video ads to be pre-loaded this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); // Create ads loader
this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) {
return _this4.onAdsManagerLoaded(event);
}, false);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
return _this4.onAdError(error);
}, false); // Request video ads to be pre-loaded
this.requestAds(); this.requestAds();
} }
@@ -12861,21 +13023,10 @@ typeof navigator === "object" && (function (global, factory) {
}, { }, {
key: "requestAds", key: "requestAds",
value: function requestAds() { value: function requestAds() {
var _this4 = this;
var container = this.player.elements.container; var container = this.player.elements.container;
try { try {
// Create ads loader // Request video ads
this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) {
return _this4.onAdsManagerLoaded(event);
}, false);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
return _this4.onAdError(error);
}, false); // Request video ads
var request = new google.ima.AdsRequest(); var request = new google.ima.AdsRequest();
request.adTagUrl = this.tagUrl; // Specify the linear and nonlinear slot sizes. This helps the SDK request.adTagUrl = this.tagUrl; // Specify the linear and nonlinear slot sizes. This helps the SDK
// to select the correct creative if multiple are returned // to select the correct creative if multiple are returned
@@ -13054,7 +13205,13 @@ typeof navigator === "object" && (function (global, factory) {
// }; // };
// TODO: So there is still this thing where a video should only be allowed to start // TODO: So there is still this thing where a video should only be allowed to start
// playing when the IMA SDK is ready or has failed // playing when the IMA SDK is ready or has failed
this.loadAds(); if (this.player.ended) {
this.loadAds();
} else {
// The SDK won't allow new ads to be called without receiving a contentComplete()
this.loader.contentComplete();
}
break; break;
case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
@@ -13190,7 +13347,7 @@ typeof navigator === "object" && (function (global, factory) {
this.playing = false; // Play video this.playing = false; // Play video
this.player.media.play(); silencePromise(this.player.media.play());
} }
/** /**
* Pause our video * Pause our video
@@ -13247,7 +13404,9 @@ typeof navigator === "object" && (function (global, factory) {
_this11.on('loaded', resolve); _this11.on('loaded', resolve);
_this11.player.debug.log(_this11.manager); _this11.player.debug.log(_this11.manager);
}); // Now request some new advertisements }); // Now that the manager has been destroyed set it to also be un-initialized
_this11.initialized = false; // Now request some new advertisements
_this11.requestAds(); _this11.requestAds();
}).catch(function () {}); }).catch(function () {});
@@ -13542,15 +13701,10 @@ typeof navigator === "object" && (function (global, factory) {
if (is$1.empty(src)) { if (is$1.empty(src)) {
throw new Error('Missing previewThumbnails.src config attribute'); throw new Error('Missing previewThumbnails.src config attribute');
} // If string, convert into single-element list } // Resolve promise
var urls = is$1.string(src) ? [src] : src; // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails var sortAndResolve = function sortAndResolve() {
var promises = urls.map(function (u) {
return _this2.getThumbnail(u);
});
Promise.all(promises).then(function () {
// Sort smallest to biggest (e.g., [120p, 480p, 1080p]) // Sort smallest to biggest (e.g., [120p, 480p, 1080p])
_this2.thumbnails.sort(function (x, y) { _this2.thumbnails.sort(function (x, y) {
return x.height - y.height; return x.height - y.height;
@@ -13559,7 +13713,25 @@ typeof navigator === "object" && (function (global, factory) {
_this2.player.debug.log('Preview thumbnails', _this2.thumbnails); _this2.player.debug.log('Preview thumbnails', _this2.thumbnails);
resolve(); resolve();
}); }; // Via callback()
if (is$1.function(src)) {
src(function (thumbnails) {
_this2.thumbnails = thumbnails;
sortAndResolve();
});
} // VTT urls
else {
// If string, convert into single-element list
var urls = is$1.string(src) ? [src] : src; // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
var promises = urls.map(function (u) {
return _this2.getThumbnail(u);
}); // Resolve
Promise.all(promises).then(sortAndResolve);
}
}); });
} // Process individual VTT file } // Process individual VTT file
@@ -14347,6 +14519,7 @@ typeof navigator === "object" && (function (global, factory) {
this.elements = { this.elements = {
container: null, container: null,
fullscreen: null,
captions: null, captions: null,
buttons: {}, buttons: {},
display: {}, display: {},
@@ -14521,9 +14694,11 @@ typeof navigator === "object" && (function (global, factory) {
tabindex: 0 tabindex: 0
}); });
wrap$1(this.media, this.elements.container); wrap$1(this.media, this.elements.container);
} // Add style hook } // Migrate custom properties from media to container (so they work 😉)
ui.migrateStyles.call(this); // Add style hook
ui.addStyleHook.call(this); // Setup media ui.addStyleHook.call(this); // Setup media
media.setup.call(this); // Listen for events if debugging media.setup.call(this); // Listen for events if debugging
@@ -14532,10 +14707,12 @@ typeof navigator === "object" && (function (global, factory) {
on.call(this, this.elements.container, this.config.events.join(' '), function (event) { on.call(this, this.elements.container, this.config.events.join(' '), function (event) {
_this.debug.log("event: ".concat(event.type)); _this.debug.log("event: ".concat(event.type));
}); });
} // Setup interface } // Setup fullscreen
// If embed but not fully supported, build interface now to avoid flash of controls
this.fullscreen = new Fullscreen(this); // Setup interface
// If embed but not fully supported, build interface now to avoid flash of controls
if (this.isHTML5 || this.isEmbed && !this.supported.ui) { if (this.isHTML5 || this.isEmbed && !this.supported.ui) {
ui.build.call(this); ui.build.call(this);
} // Container listeners } // Container listeners
@@ -14543,9 +14720,7 @@ typeof navigator === "object" && (function (global, factory) {
this.listeners.container(); // Global listeners this.listeners.container(); // Global listeners
this.listeners.global(); // Setup fullscreen this.listeners.global(); // Setup ads if provided
this.fullscreen = new Fullscreen(this); // Setup ads if provided
if (this.config.ads.enabled) { if (this.config.ads.enabled) {
this.ads = new Ads(this); this.ads = new Ads(this);
@@ -14554,7 +14729,7 @@ typeof navigator === "object" && (function (global, factory) {
if (this.isHTML5 && this.config.autoplay) { if (this.isHTML5 && this.config.autoplay) {
setTimeout(function () { setTimeout(function () {
return _this.play(); return silencePromise(_this.play());
}, 10); }, 10);
} // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek } // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
@@ -14591,7 +14766,7 @@ typeof navigator === "object" && (function (global, factory) {
this.ads.managerPromise.then(function () { this.ads.managerPromise.then(function () {
return _this2.ads.play(); return _this2.ads.play();
}).catch(function () { }).catch(function () {
return _this2.media.play(); return silencePromise(_this2.media.play());
}); });
} // Return the promise (for HTML5) } // Return the promise (for HTML5)
@@ -15244,7 +15419,7 @@ typeof navigator === "object" && (function (global, factory) {
var updateStorage = true; var updateStorage = true;
if (!options.includes(quality)) { if (!options.includes(quality)) {
var value = closest(options, quality); var value = closest$1(options, quality);
this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead")); this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead"));
quality = value; // Don't update storage if quality is not supported quality = value; // Don't update storage if quality is not supported
@@ -15283,41 +15458,41 @@ typeof navigator === "object" && (function (global, factory) {
this.media.loop = toggle; // Set default to be a true toggle this.media.loop = toggle; // Set default to be a true toggle
/* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle'; /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
switch (type) { switch (type) {
case 'start': case 'start':
if (this.config.loop.end && this.config.loop.end <= this.currentTime) { if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
this.config.loop.end = null; this.config.loop.end = null;
} }
this.config.loop.start = this.currentTime; this.config.loop.start = this.currentTime;
// this.config.loop.indicator.start = this.elements.display.played.value; // this.config.loop.indicator.start = this.elements.display.played.value;
break; break;
case 'end': case 'end':
if (this.config.loop.start >= this.currentTime) { if (this.config.loop.start >= this.currentTime) {
return this; return this;
} }
this.config.loop.end = this.currentTime; this.config.loop.end = this.currentTime;
// this.config.loop.indicator.end = this.elements.display.played.value; // this.config.loop.indicator.end = this.elements.display.played.value;
break; break;
case 'all': case 'all':
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
this.config.loop.indicator.start = 0;
this.config.loop.indicator.end = 100;
break;
case 'toggle':
if (this.config.loop.active) {
this.config.loop.start = 0;
this.config.loop.end = null;
} else {
this.config.loop.start = 0; this.config.loop.start = 0;
this.config.loop.end = this.duration - 2; this.config.loop.end = this.duration - 2;
} this.config.loop.indicator.start = 0;
break; this.config.loop.indicator.end = 100;
default: break;
this.config.loop.start = 0; case 'toggle':
this.config.loop.end = null; if (this.config.loop.active) {
break; this.config.loop.start = 0;
} */ this.config.loop.end = null;
} else {
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
}
break;
default:
this.config.loop.start = 0;
this.config.loop.end = null;
break;
} */
} }
/** /**
* Get current loop state * Get current loop state
@@ -15389,7 +15564,7 @@ typeof navigator === "object" && (function (global, factory) {
return null; return null;
} }
return this.media.getAttribute('poster'); return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');
} }
/** /**
* Get the current aspect ratio in use * Get the current aspect ratio in use
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+348 -173
View File
@@ -263,7 +263,7 @@ var shared = createCommonjsModule(function (module) {
(module.exports = function (key, value) { (module.exports = function (key, value) {
return sharedStore[key] || (sharedStore[key] = value !== undefined ? value : {}); return sharedStore[key] || (sharedStore[key] = value !== undefined ? value : {});
})('versions', []).push({ })('versions', []).push({
version: '3.6.4', version: '3.6.5',
mode: 'global', mode: 'global',
copyright: '© 2020 Denis Pushkarev (zloirock.ru)' copyright: '© 2020 Denis Pushkarev (zloirock.ru)'
}); });
@@ -3055,7 +3055,7 @@ var INVALID_HOST = 'Invalid host';
var INVALID_PORT = 'Invalid port'; var INVALID_PORT = 'Invalid port';
var ALPHA = /[A-Za-z]/; var ALPHA = /[A-Za-z]/;
var ALPHANUMERIC = /[\d+\-.A-Za-z]/; var ALPHANUMERIC = /[\d+-.A-Za-z]/;
var DIGIT = /\d/; var DIGIT = /\d/;
var HEX_START = /^(0x|0X)/; var HEX_START = /^(0x|0X)/;
var OCT = /^[0-7]+$/; var OCT = /^[0-7]+$/;
@@ -4117,6 +4117,42 @@ function _objectSpread2(target) {
return target; return target;
} }
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
function _objectWithoutProperties(source, excluded) {
if (source == null) return {};
var target = _objectWithoutPropertiesLoose(source, excluded);
var key, i;
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue;
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
function _slicedToArray(arr, i) { function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
} }
@@ -6002,7 +6038,13 @@ if (!set$1 || !clear) {
defer = functionBindContext(port.postMessage, port, 1); defer = functionBindContext(port.postMessage, port, 1);
// Browsers with postMessage, skip WebWorkers // Browsers with postMessage, skip WebWorkers
// IE8 has postMessage, but it's sync & typeof its postMessage is 'object' // IE8 has postMessage, but it's sync & typeof its postMessage is 'object'
} else if (global_1.addEventListener && typeof postMessage == 'function' && !global_1.importScripts && !fails(post)) { } else if (
global_1.addEventListener &&
typeof postMessage == 'function' &&
!global_1.importScripts &&
!fails(post) &&
location.protocol !== 'file:'
) {
defer = post; defer = post;
global_1.addEventListener('message', listener, false); global_1.addEventListener('message', listener, false);
// IE8- // IE8-
@@ -6611,7 +6653,7 @@ var isTrack = function isTrack(input) {
}; };
var isPromise = function isPromise(input) { var isPromise = function isPromise(input) {
return instanceOf$1(input, Promise); return instanceOf$1(input, Promise) && isFunction$1(input.then);
}; };
var isEmpty$1 = function isEmpty(input) { var isEmpty$1 = function isEmpty(input) {
@@ -7003,12 +7045,33 @@ function hasClass(element, className) {
} // Element matches selector } // Element matches selector
function matches$1(element, selector) { function matches$1(element, selector) {
var _Element = Element,
prototype = _Element.prototype;
function match() { function match() {
return Array.from(document.querySelectorAll(selector)).includes(this); return Array.from(document.querySelectorAll(selector)).includes(this);
} }
var method = match; var method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;
return method.call(element, selector);
} // Closest ancestor element matching selector (also tests element itself)
function closest(element, selector) {
var _Element2 = Element,
prototype = _Element2.prototype; // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
function closestElement() {
var el = this;
do {
if (matches$1.matches(el, selector)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
}
var method = prototype.closest || closestElement;
return method.call(element, selector); return method.call(element, selector);
} // Find all elements } // Find all elements
@@ -7280,6 +7343,19 @@ function ready() {
}).then(function () {}); }).then(function () {});
} }
/**
* Silence a Promise-like object.
* This is useful for avoiding non-harmful, but potentially confusing "uncaught
* play promise" rejection error messages.
* @param {Object} value An object that may or may not be `Promise`-like.
*/
function silencePromise(value) {
if (is$1.promise(value)) {
value.then(null, function () {});
}
}
function validateRatio(input) { function validateRatio(input) {
if (!is$1.array(input) && (!is$1.string(input) || !input.includes(':'))) { if (!is$1.array(input) && (!is$1.string(input) || !input.includes(':'))) {
return false; return false;
@@ -7348,8 +7424,8 @@ function setAspectRatio(input) {
var padding = 100 / w * h; var padding = 100 / w * h;
wrapper.style.paddingBottom = "".concat(padding, "%"); // For Vimeo we have an extra <div> to hide the standard controls and UI wrapper.style.paddingBottom = "".concat(padding, "%"); // For Vimeo we have an extra <div> to hide the standard controls and UI
if (this.isVimeo && this.supported.ui) { if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {
var height = 240; var height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);
var offset = (height - padding) / (height / 50); var offset = (height - padding) / (height / 50);
this.media.style.transform = "translateY(-".concat(offset, "%)"); this.media.style.transform = "translateY(-".concat(offset, "%)");
} else if (this.isHTML5) { } else if (this.isHTML5) {
@@ -7455,7 +7531,7 @@ var html5 = {
player.currentTime = currentTime; // Resume playing player.currentTime = currentTime; // Resume playing
if (!paused) { if (!paused) {
player.play(); silencePromise(player.play());
} }
}); // Load new source }); // Load new source
@@ -7502,7 +7578,7 @@ function dedupe(array) {
}); });
} // Get the closest value in an array } // Get the closest value in an array
function closest(array, value) { function closest$1(array, value) {
if (!is$1.array(array) || !array.length) { if (!is$1.array(array) || !array.length) {
return null; return null;
} }
@@ -7619,19 +7695,19 @@ function getPercentage(current, max) {
return (current / max * 100).toFixed(2); return (current / max * 100).toFixed(2);
} // Replace all occurances of a string in a string } // Replace all occurances of a string in a string
function replaceAll() { var replaceAll = function replaceAll() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
} // Convert to title case }; // Convert to title case
function toTitleCase() { var toTitleCase = function toTitleCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
return input.toString().replace(/\w\S*/g, function (text) { return input.toString().replace(/\w\S*/g, function (text) {
return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
}); });
} // Convert string to pascalCase }; // Convert string to pascalCase
function toPascalCase() { function toPascalCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@@ -8738,39 +8814,39 @@ var controls = {
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panels.loop)) { if (!is.element(this.elements.settings.panels.loop)) {
return; return;
} }
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
// Show the pane and tab // Show the pane and tab
toggleHidden(this.elements.settings.buttons.loop, false); toggleHidden(this.elements.settings.buttons.loop, false);
toggleHidden(this.elements.settings.panels.loop, false); toggleHidden(this.elements.settings.panels.loop, false);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.loop.options); const toggle = !is.empty(this.loop.options);
controls.toggleMenuButton.call(this, 'loop', toggle); controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
options.forEach(option => { options.forEach(option => {
const item = createElement('li'); const item = createElement('li');
const button = createElement( const button = createElement(
'button', 'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.loop), { extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
type: 'button', type: 'button',
class: this.config.classNames.control, class: this.config.classNames.control,
'data-plyr-loop-action': option, 'data-plyr-loop-action': option,
}), }),
i18n.get(option, this.config) i18n.get(option, this.config)
); );
if (['start', 'end'].includes(option)) { if (['start', 'end'].includes(option)) {
const badge = controls.createBadge.call(this, '00:00'); const badge = controls.createBadge.call(this, '00:00');
button.appendChild(badge); button.appendChild(badge);
} }
item.appendChild(button); item.appendChild(button);
list.appendChild(item); list.appendChild(item);
}); });
}, */ }, */
// Get current selected caption language // Get current selected caption language
// TODO: rework this to user the getter in the API? // TODO: rework this to user the getter in the API?
// Set a list of available captions languages // Set a list of available captions languages
@@ -9572,9 +9648,15 @@ var captions = {
meta.set(track, { meta.set(track, {
default: track.mode === 'showing' default: track.mode === 'showing'
}); // Turn off native caption rendering to avoid double captions }); // Turn off native caption rendering to avoid double captions
// Note: mode='hidden' forces a track to download. To ensure every track
// isn't downloaded at once, only 'showing' tracks should be reassigned
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
track.mode = 'hidden'; // Add event listener for cue changes if (track.mode === 'showing') {
// eslint-disable-next-line no-param-reassign
track.mode = 'hidden';
} // Add event listener for cue changes
on.call(_this, track, 'cuechange', function () { on.call(_this, track, 'cuechange', function () {
return captions.updateCues.call(_this); return captions.updateCues.call(_this);
@@ -9598,6 +9680,8 @@ var captions = {
// Toggle captions display // Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false // Used internally for the toggleCaptions method, with the passive option forced to false
toggle: function toggle(input) { toggle: function toggle(input) {
var _this2 = this;
var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
// If there's no full support // If there's no full support
@@ -9644,7 +9728,15 @@ var captions = {
controls.updateSetting.call(this, 'captions'); // Trigger event (not used internally) controls.updateSetting.call(this, 'captions'); // Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
} } // Wait for the call stack to clear before setting mode='hidden'
// on the active track - forcing the browser to download it
setTimeout(function () {
if (active && _this2.captions.toggled) {
_this2.captions.currentTrackNode.mode = 'hidden';
}
});
}, },
// Set captions by track index // Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false // Used internally for the currentTrack setter with the passive option forced to false
@@ -9725,7 +9817,7 @@ var captions = {
// If update is false it will also ignore tracks without metadata // If update is false it will also ignore tracks without metadata
// This is used to "freeze" the language options when captions.update is false // This is used to "freeze" the language options when captions.update is false
getTracks: function getTracks() { getTracks: function getTracks() {
var _this2 = this; var _this3 = this;
var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
// Handle media or textTracks missing or null // Handle media or textTracks missing or null
@@ -9733,20 +9825,20 @@ var captions = {
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata) // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks.filter(function (track) { return tracks.filter(function (track) {
return !_this2.isHTML5 || update || _this2.captions.meta.has(track); return !_this3.isHTML5 || update || _this3.captions.meta.has(track);
}).filter(function (track) { }).filter(function (track) {
return ['captions', 'subtitles'].includes(track.kind); return ['captions', 'subtitles'].includes(track.kind);
}); });
}, },
// Match tracks based on languages and get the first // Match tracks based on languages and get the first
findTrack: function findTrack(languages) { findTrack: function findTrack(languages) {
var _this3 = this; var _this4 = this;
var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var tracks = captions.getTracks.call(this); var tracks = captions.getTracks.call(this);
var sortIsDefault = function sortIsDefault(track) { var sortIsDefault = function sortIsDefault(track) {
return Number((_this3.captions.meta.get(track) || {}).default); return Number((_this4.captions.meta.get(track) || {}).default);
}; };
var sorted = Array.from(tracks).sort(function (a, b) { var sorted = Array.from(tracks).sort(function (a, b) {
@@ -9879,7 +9971,7 @@ var defaults$1 = {
// Sprite (for icons) // Sprite (for icons)
loadSprite: true, loadSprite: true,
iconPrefix: 'plyr', iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg', iconUrl: 'https://cdn.plyr.io/3.6.1/plyr.svg',
// Blank video (used to prevent errors on source change) // Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4', blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default // Quality default
@@ -9927,6 +10019,9 @@ var defaults$1 = {
fallback: true, fallback: true,
// Fallback using full viewport/window // Fallback using full viewport/window
iosNative: false // Use the native fullscreen in iOS (disables custom controls) iosNative: false // Use the native fullscreen in iOS (disables custom controls)
// Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode
// Non-ancestors of the player element will be ignored
// container: null, // defaults to the player element
}, },
// Local storage // Local storage
@@ -10164,16 +10259,16 @@ var defaults$1 = {
title: false, title: false,
speed: true, speed: true,
transparent: false, transparent: false,
// These settings require a pro or premium account to work // Whether the owner of the video has a Pro or Business account
sidedock: false, // (which allows us to properly hide controls without CSS hacks, etc)
controls: false, premium: false,
// Custom settings from Plyr // Custom settings from Plyr
referrerPolicy: null // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy referrerPolicy: null // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
}, },
// YouTube plugin // YouTube plugin
youtube: { youtube: {
noCookie: false, noCookie: true,
// Whether to use an alternative version of YouTube without cookies // Whether to use an alternative version of YouTube without cookies
rel: 0, rel: 0,
// No related vids // No related vids
@@ -10283,7 +10378,10 @@ var Fullscreen = /*#__PURE__*/function () {
y: 0 y: 0
}; // Force the use of 'full window/browser' rather than fullscreen }; // Force the use of 'full window/browser' rather than fullscreen
this.forceFallback = player.config.fullscreen.fallback === 'force'; // Register event listeners this.forceFallback = player.config.fullscreen.fallback === 'force'; // Get the fullscreen element
// Checks container is an ancestor, defaults to null
this.player.elements.fullscreen = player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container); // Register event listeners
// Handle event (incase user presses escape etc) // Handle event (incase user presses escape etc)
on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : "".concat(this.prefix, "fullscreenchange"), function () { on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : "".concat(this.prefix, "fullscreenchange"), function () {
@@ -10456,7 +10554,7 @@ var Fullscreen = /*#__PURE__*/function () {
if (browser.isIos && this.player.config.fullscreen.iosNative) { if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen(); this.target.webkitExitFullscreen();
this.player.play(); silencePromise(this.player.play());
} else if (!Fullscreen.native || this.forceFallback) { } else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(false); this.toggleFallback(false);
} else if (!this.prefix) { } else if (!this.prefix) {
@@ -10503,13 +10601,13 @@ var Fullscreen = /*#__PURE__*/function () {
} }
var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")]; var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")];
return element === this.target; return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
} // Get target element } // Get target element
}, { }, {
key: "target", key: "target",
get: function get() { get: function get() {
return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container; return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen || this.player.elements.container;
} }
}], [{ }], [{
key: "native", key: "native",
@@ -10718,12 +10816,7 @@ var ui = {
} // Set property synchronously to respect the call order } // Set property synchronously to respect the call order
this.media.setAttribute('poster', poster); // HTML5 uses native poster attribute this.media.setAttribute('data-poster', poster); // Wait until ui is ready
if (this.isHTML5) {
return Promise.resolve(poster);
} // Wait until ui is ready
return ready.call(this) // Load image return ready.call(this) // Load image
.then(function () { .then(function () {
@@ -10799,6 +10892,26 @@ var ui = {
this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek)); this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));
} }
},
// Migrate any custom properties from the media to the parent
migrateStyles: function migrateStyles() {
var _this5 = this;
// Loop through values (as they are the keys when the object is spread 🤔)
Object.values(_objectSpread2({}, this.media.style)) // We're only fussed about Plyr specific properties
.filter(function (key) {
return !is$1.empty(key) && key.startsWith('--plyr');
}).forEach(function (key) {
// Set on the container
_this5.elements.container.style.setProperty(key, _this5.media.style.getPropertyValue(key)); // Clean up from media element
_this5.media.style.removeProperty(key);
}); // Remove attribute if empty
if (is$1.empty(this.media.style)) {
this.media.removeAttribute('style');
}
} }
}; };
@@ -10893,7 +11006,7 @@ var Listeners = /*#__PURE__*/function () {
case 75: case 75:
// Space and K key // Space and K key
if (!repeat) { if (!repeat) {
player.togglePlay(); silencePromise(player.togglePlay());
} }
break; break;
@@ -11007,15 +11120,17 @@ var Listeners = /*#__PURE__*/function () {
removeCurrent(); // Delay the adding of classname until the focus has changed removeCurrent(); // Delay the adding of classname until the focus has changed
// This event fires before the focusin event // This event fires before the focusin event
this.focusTimer = setTimeout(function () { if (event.type !== 'focusout') {
var focused = document.activeElement; // Ignore if current focus element isn't inside the player this.focusTimer = setTimeout(function () {
var focused = document.activeElement; // Ignore if current focus element isn't inside the player
if (!elements.container.contains(focused)) { if (!elements.container.contains(focused)) {
return; return;
} }
toggleClass(document.activeElement, player.config.classNames.tabFocus, true); toggleClass(document.activeElement, player.config.classNames.tabFocus, true);
}, 10); }, 10);
}
} // Global window & document listeners } // Global window & document listeners
}, { }, {
@@ -11033,7 +11148,7 @@ var Listeners = /*#__PURE__*/function () {
once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection
toggleListener.call(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true); toggleListener.call(player, document.body, 'keydown focus blur focusout', this.setTabFocus, toggle, false, true);
} // Container listeners } // Container listeners
}, { }, {
@@ -11076,7 +11191,7 @@ var Listeners = /*#__PURE__*/function () {
}); // Set a gutter for Vimeo }); // Set a gutter for Vimeo
var setGutter = function setGutter(ratio, padding, toggle) { var setGutter = function setGutter(ratio, padding, toggle) {
if (!player.isVimeo) { if (!player.isVimeo || player.config.vimeo.premium) {
return; return;
} }
@@ -11133,7 +11248,7 @@ var Listeners = /*#__PURE__*/function () {
ratio = _setPlayerSize.ratio; // Set Vimeo gutter ratio = _setPlayerSize.ratio; // Set Vimeo gutter
setGutter(ratio, padding, isEnter); // If not using native fullscreen, we need to check for resizes of viewport setGutter(ratio, padding, isEnter); // If not using native browser fullscreen API, we need to check for resizes of viewport
if (!usingNative) { if (!usingNative) {
if (isEnter) { if (isEnter) {
@@ -11211,9 +11326,13 @@ var Listeners = /*#__PURE__*/function () {
if (player.ended) { if (player.ended) {
_this.proxy(event, player.restart, 'restart'); _this.proxy(event, player.restart, 'restart');
_this.proxy(event, player.play, 'play'); _this.proxy(event, function () {
silencePromise(player.play());
}, 'play');
} else { } else {
_this.proxy(event, player.togglePlay, 'play'); _this.proxy(event, function () {
silencePromise(player.togglePlay());
}, 'play');
} }
}); });
} // Disable right click } // Disable right click
@@ -11311,7 +11430,9 @@ var Listeners = /*#__PURE__*/function () {
if (elements.buttons.play) { if (elements.buttons.play) {
Array.from(elements.buttons.play).forEach(function (button) { Array.from(elements.buttons.play).forEach(function (button) {
_this3.bind(button, 'click', player.togglePlay, 'play'); _this3.bind(button, 'click', function () {
silencePromise(player.togglePlay());
}, 'play');
}); });
} // Pause } // Pause
@@ -11408,7 +11529,7 @@ var Listeners = /*#__PURE__*/function () {
if (play && done) { if (play && done) {
seek.removeAttribute(attribute); seek.removeAttribute(attribute);
player.play(); silencePromise(player.play());
} else if (!done && player.playing) { } else if (!done && player.playing) {
seek.setAttribute(attribute, ''); seek.setAttribute(attribute, '');
player.pause(); player.pause();
@@ -11506,7 +11627,18 @@ var Listeners = /*#__PURE__*/function () {
this.bind(elements.controls, 'mouseenter mouseleave', function (event) { this.bind(elements.controls, 'mouseenter mouseleave', function (event) {
elements.controls.hover = !player.touch && event.type === 'mouseenter'; elements.controls.hover = !player.touch && event.type === 'mouseenter';
}); // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) }); // Also update controls.hover state for any non-player children of fullscreen element (as above)
if (elements.fullscreen) {
Array.from(elements.fullscreen.children).filter(function (c) {
return !c.contains(elements.container);
}).forEach(function (child) {
_this3.bind(child, 'mouseenter mouseleave', function (event) {
elements.controls.hover = !player.touch && event.type === 'mouseenter';
});
});
} // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
@@ -11975,15 +12107,28 @@ var vimeo = {
var _this = this; var _this = this;
var player = this; var player = this;
var config = player.config.vimeo; // Get Vimeo params for the iframe var config = player.config.vimeo;
var params = buildUrlParams(extend({}, { var premium = config.premium,
referrerPolicy = config.referrerPolicy,
frameParams = _objectWithoutProperties(config, ["premium", "referrerPolicy"]); // If the owner has a pro or premium account then we can hide controls etc
if (premium) {
Object.assign(frameParams, {
controls: false,
sidedock: false
});
} // Get Vimeo params for the iframe
var params = buildUrlParams(_objectSpread2({
loop: player.config.loop.active, loop: player.config.loop.active,
autoplay: player.autoplay, autoplay: player.autoplay,
muted: player.muted, muted: player.muted,
gesture: 'media', gesture: 'media',
playsinline: !this.config.fullscreen.iosNative playsinline: !this.config.fullscreen.iosNative
}, config)); // Get the source URL or ID }, frameParams)); // Get the source URL or ID
var source = player.media.getAttribute('src'); // Get from <div> if needed var source = player.media.getAttribute('src'); // Get from <div> if needed
@@ -11997,22 +12142,27 @@ var vimeo = {
var src = format(player.config.urls.vimeo.iframe, id, params); var src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src); iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', ''); iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', ''); iframe.setAttribute('allow', 'autoplay,fullscreen,picture-in-picture'); // Set the referrer policy if required
iframe.setAttribute('allow', 'autoplay'); // Set the referrer policy if required
if (!is$1.empty(config.referrerPolicy)) { if (!is$1.empty(referrerPolicy)) {
iframe.setAttribute('referrerPolicy', config.referrerPolicy); iframe.setAttribute('referrerPolicy', referrerPolicy);
} // Get poster, if already set } // Inject the package
var poster = player.poster; // Inject the package var poster = player.poster;
if (premium) {
iframe.setAttribute('data-poster', poster);
player.media = replaceElement(iframe, player.media);
} else {
var wrapper = createElement('div', {
class: player.config.classNames.embedContainer,
'data-poster': poster
});
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
} // Get poster image
var wrapper = createElement('div', {
poster: poster,
class: player.config.classNames.embedContainer
});
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media); // Get poster image
fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) { fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) {
if (is$1.empty(response)) { if (is$1.empty(response)) {
@@ -12096,6 +12246,9 @@ var vimeo = {
player.embed.setPlaybackRate(input).then(function () { player.embed.setPlaybackRate(input).then(function () {
speed = input; speed = input;
triggerEvent.call(player, player.media, 'ratechange'); triggerEvent.call(player, player.media, 'ratechange');
}).catch(function () {
// Cannot set Playback Rate, Video is probably not on Pro account
player.options.speed = [1];
}); });
} }
}); // Volume }); // Volume
@@ -12380,7 +12533,7 @@ var youtube = {
var container = createElement('div', { var container = createElement('div', {
id: id, id: id,
poster: poster 'data-poster': poster
}); });
player.media = replaceElement(container, player.media); // Id to poster wrapper player.media = replaceElement(container, player.media); // Id to poster wrapper
@@ -12698,14 +12851,12 @@ var media = {
class: this.config.classNames.video class: this.config.classNames.video
}); // Wrap the video in a container }); // Wrap the video in a container
wrap$1(this.media, this.elements.wrapper); // Faux poster container wrap$1(this.media, this.elements.wrapper); // Poster image container
if (this.isEmbed) { this.elements.poster = createElement('div', {
this.elements.poster = createElement('div', { class: this.config.classNames.poster
class: this.config.classNames.poster });
}); this.elements.wrapper.appendChild(this.elements.poster);
this.elements.wrapper.appendChild(this.elements.poster);
}
} }
if (this.isHTML5) { if (this.isHTML5) {
@@ -12832,6 +12983,8 @@ var Ads = /*#__PURE__*/function () {
* mobile devices, this initialization is done as the result of a user action. * mobile devices, this initialization is done as the result of a user action.
*/ */
value: function setupIMA() { value: function setupIMA() {
var _this4 = this;
// Create the container for our advertisements // Create the container for our advertisements
this.elements.container = createElement('div', { this.elements.container = createElement('div', {
class: this.player.config.classNames.ads class: this.player.config.classNames.ads
@@ -12844,7 +12997,16 @@ var Ads = /*#__PURE__*/function () {
google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); // We assume the adContainer is the video container of the plyr element that will house the ads google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); // We assume the adContainer is the video container of the plyr element that will house the ads
this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); // Request video ads to be pre-loaded this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); // Create ads loader
this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) {
return _this4.onAdsManagerLoaded(event);
}, false);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
return _this4.onAdError(error);
}, false); // Request video ads to be pre-loaded
this.requestAds(); this.requestAds();
} }
@@ -12855,21 +13017,10 @@ var Ads = /*#__PURE__*/function () {
}, { }, {
key: "requestAds", key: "requestAds",
value: function requestAds() { value: function requestAds() {
var _this4 = this;
var container = this.player.elements.container; var container = this.player.elements.container;
try { try {
// Create ads loader // Request video ads
this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) {
return _this4.onAdsManagerLoaded(event);
}, false);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
return _this4.onAdError(error);
}, false); // Request video ads
var request = new google.ima.AdsRequest(); var request = new google.ima.AdsRequest();
request.adTagUrl = this.tagUrl; // Specify the linear and nonlinear slot sizes. This helps the SDK request.adTagUrl = this.tagUrl; // Specify the linear and nonlinear slot sizes. This helps the SDK
// to select the correct creative if multiple are returned // to select the correct creative if multiple are returned
@@ -13048,7 +13199,13 @@ var Ads = /*#__PURE__*/function () {
// }; // };
// TODO: So there is still this thing where a video should only be allowed to start // TODO: So there is still this thing where a video should only be allowed to start
// playing when the IMA SDK is ready or has failed // playing when the IMA SDK is ready or has failed
this.loadAds(); if (this.player.ended) {
this.loadAds();
} else {
// The SDK won't allow new ads to be called without receiving a contentComplete()
this.loader.contentComplete();
}
break; break;
case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
@@ -13184,7 +13341,7 @@ var Ads = /*#__PURE__*/function () {
this.playing = false; // Play video this.playing = false; // Play video
this.player.media.play(); silencePromise(this.player.media.play());
} }
/** /**
* Pause our video * Pause our video
@@ -13241,7 +13398,9 @@ var Ads = /*#__PURE__*/function () {
_this11.on('loaded', resolve); _this11.on('loaded', resolve);
_this11.player.debug.log(_this11.manager); _this11.player.debug.log(_this11.manager);
}); // Now request some new advertisements }); // Now that the manager has been destroyed set it to also be un-initialized
_this11.initialized = false; // Now request some new advertisements
_this11.requestAds(); _this11.requestAds();
}).catch(function () {}); }).catch(function () {});
@@ -13536,15 +13695,10 @@ var PreviewThumbnails = /*#__PURE__*/function () {
if (is$1.empty(src)) { if (is$1.empty(src)) {
throw new Error('Missing previewThumbnails.src config attribute'); throw new Error('Missing previewThumbnails.src config attribute');
} // If string, convert into single-element list } // Resolve promise
var urls = is$1.string(src) ? [src] : src; // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails var sortAndResolve = function sortAndResolve() {
var promises = urls.map(function (u) {
return _this2.getThumbnail(u);
});
Promise.all(promises).then(function () {
// Sort smallest to biggest (e.g., [120p, 480p, 1080p]) // Sort smallest to biggest (e.g., [120p, 480p, 1080p])
_this2.thumbnails.sort(function (x, y) { _this2.thumbnails.sort(function (x, y) {
return x.height - y.height; return x.height - y.height;
@@ -13553,7 +13707,25 @@ var PreviewThumbnails = /*#__PURE__*/function () {
_this2.player.debug.log('Preview thumbnails', _this2.thumbnails); _this2.player.debug.log('Preview thumbnails', _this2.thumbnails);
resolve(); resolve();
}); }; // Via callback()
if (is$1.function(src)) {
src(function (thumbnails) {
_this2.thumbnails = thumbnails;
sortAndResolve();
});
} // VTT urls
else {
// If string, convert into single-element list
var urls = is$1.string(src) ? [src] : src; // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
var promises = urls.map(function (u) {
return _this2.getThumbnail(u);
}); // Resolve
Promise.all(promises).then(sortAndResolve);
}
}); });
} // Process individual VTT file } // Process individual VTT file
@@ -14341,6 +14513,7 @@ var Plyr = /*#__PURE__*/function () {
this.elements = { this.elements = {
container: null, container: null,
fullscreen: null,
captions: null, captions: null,
buttons: {}, buttons: {},
display: {}, display: {},
@@ -14515,9 +14688,11 @@ var Plyr = /*#__PURE__*/function () {
tabindex: 0 tabindex: 0
}); });
wrap$1(this.media, this.elements.container); wrap$1(this.media, this.elements.container);
} // Add style hook } // Migrate custom properties from media to container (so they work 😉)
ui.migrateStyles.call(this); // Add style hook
ui.addStyleHook.call(this); // Setup media ui.addStyleHook.call(this); // Setup media
media.setup.call(this); // Listen for events if debugging media.setup.call(this); // Listen for events if debugging
@@ -14526,10 +14701,12 @@ var Plyr = /*#__PURE__*/function () {
on.call(this, this.elements.container, this.config.events.join(' '), function (event) { on.call(this, this.elements.container, this.config.events.join(' '), function (event) {
_this.debug.log("event: ".concat(event.type)); _this.debug.log("event: ".concat(event.type));
}); });
} // Setup interface } // Setup fullscreen
// If embed but not fully supported, build interface now to avoid flash of controls
this.fullscreen = new Fullscreen(this); // Setup interface
// If embed but not fully supported, build interface now to avoid flash of controls
if (this.isHTML5 || this.isEmbed && !this.supported.ui) { if (this.isHTML5 || this.isEmbed && !this.supported.ui) {
ui.build.call(this); ui.build.call(this);
} // Container listeners } // Container listeners
@@ -14537,9 +14714,7 @@ var Plyr = /*#__PURE__*/function () {
this.listeners.container(); // Global listeners this.listeners.container(); // Global listeners
this.listeners.global(); // Setup fullscreen this.listeners.global(); // Setup ads if provided
this.fullscreen = new Fullscreen(this); // Setup ads if provided
if (this.config.ads.enabled) { if (this.config.ads.enabled) {
this.ads = new Ads(this); this.ads = new Ads(this);
@@ -14548,7 +14723,7 @@ var Plyr = /*#__PURE__*/function () {
if (this.isHTML5 && this.config.autoplay) { if (this.isHTML5 && this.config.autoplay) {
setTimeout(function () { setTimeout(function () {
return _this.play(); return silencePromise(_this.play());
}, 10); }, 10);
} // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek } // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
@@ -14585,7 +14760,7 @@ var Plyr = /*#__PURE__*/function () {
this.ads.managerPromise.then(function () { this.ads.managerPromise.then(function () {
return _this2.ads.play(); return _this2.ads.play();
}).catch(function () { }).catch(function () {
return _this2.media.play(); return silencePromise(_this2.media.play());
}); });
} // Return the promise (for HTML5) } // Return the promise (for HTML5)
@@ -15238,7 +15413,7 @@ var Plyr = /*#__PURE__*/function () {
var updateStorage = true; var updateStorage = true;
if (!options.includes(quality)) { if (!options.includes(quality)) {
var value = closest(options, quality); var value = closest$1(options, quality);
this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead")); this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead"));
quality = value; // Don't update storage if quality is not supported quality = value; // Don't update storage if quality is not supported
@@ -15277,41 +15452,41 @@ var Plyr = /*#__PURE__*/function () {
this.media.loop = toggle; // Set default to be a true toggle this.media.loop = toggle; // Set default to be a true toggle
/* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle'; /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
switch (type) { switch (type) {
case 'start': case 'start':
if (this.config.loop.end && this.config.loop.end <= this.currentTime) { if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
this.config.loop.end = null; this.config.loop.end = null;
} }
this.config.loop.start = this.currentTime; this.config.loop.start = this.currentTime;
// this.config.loop.indicator.start = this.elements.display.played.value; // this.config.loop.indicator.start = this.elements.display.played.value;
break; break;
case 'end': case 'end':
if (this.config.loop.start >= this.currentTime) { if (this.config.loop.start >= this.currentTime) {
return this; return this;
} }
this.config.loop.end = this.currentTime; this.config.loop.end = this.currentTime;
// this.config.loop.indicator.end = this.elements.display.played.value; // this.config.loop.indicator.end = this.elements.display.played.value;
break; break;
case 'all': case 'all':
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
this.config.loop.indicator.start = 0;
this.config.loop.indicator.end = 100;
break;
case 'toggle':
if (this.config.loop.active) {
this.config.loop.start = 0;
this.config.loop.end = null;
} else {
this.config.loop.start = 0; this.config.loop.start = 0;
this.config.loop.end = this.duration - 2; this.config.loop.end = this.duration - 2;
} this.config.loop.indicator.start = 0;
break; this.config.loop.indicator.end = 100;
default: break;
this.config.loop.start = 0; case 'toggle':
this.config.loop.end = null; if (this.config.loop.active) {
break; this.config.loop.start = 0;
} */ this.config.loop.end = null;
} else {
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
}
break;
default:
this.config.loop.start = 0;
this.config.loop.end = null;
break;
} */
} }
/** /**
* Get current loop state * Get current loop state
@@ -15383,7 +15558,7 @@ var Plyr = /*#__PURE__*/function () {
return null; return null;
} }
return this.media.getAttribute('poster'); return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');
} }
/** /**
* Get the current aspect ratio in use * Get the current aspect ratio in use
+2 -516
View File
@@ -1,522 +1,8 @@
// ========================================================================== // ==========================================================================
// Gulp build script // Gulp build script
// ========================================================================== // ==========================================================================
/* eslint no-console: "off" */
const path = require('path');
const gulp = require('gulp'); const gulp = require('gulp');
// ------------------------------------ const HubRegistry = require('gulp-hub');
// 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');
// ------------------------------------
// 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');
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');
const size = require('gulp-size');
const sourcemaps = require('gulp-sourcemaps');
const through = require('through2');
const browserSync = require('browser-sync').create();
// ------------------------------------
// 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 gulp.registry(new HubRegistry(['tasks/*.js']));
Object.values(deploy).forEach(target => {
Object.assign(target, {
publisher: publish.create({
region: target.region,
params: {
Bucket: target.bucket,
},
credentials: new aws.SharedIniFileCredentials({ profile: 'plyr' }),
}),
});
});
// Paths
const paths = {
plyr: {
// Source paths
src: {
sass: path.join(__dirname, 'src/sass/**/*.scss'),
js: path.join(__dirname, 'src/js/**/*.js'),
sprite: path.join(__dirname, 'src/sprite/*.svg'),
},
// Output paths
output: path.join(__dirname, 'dist/'),
},
demo: {
// Source paths
src: {
sass: path.join(__dirname, 'demo/src/sass/**/*.scss'),
js: path.join(__dirname, 'demo/src/js/**/*.js'),
},
// Output paths
output: path.join(__dirname, 'demo/dist/'),
// Demo
root: path.join(__dirname, 'demo/'),
},
upload: [
path.join(__dirname, `dist/*${minSuffix}.*`),
path.join(__dirname, 'dist/*.css'),
path.join(__dirname, 'dist/*.svg'),
path.join(__dirname, `demo/dist/*${minSuffix}.*`),
path.join(__dirname, 'demo/dist/*.css'),
path.join(__dirname, 'demo/dist/*.svg'),
],
};
// Task arrays
const tasks = {
css: [],
js: [],
sprite: [],
clean: 'clean',
};
// Size plugin
const sizeOptions = { showFiles: true, gzip: true };
// Clean out /dist
gulp.task(tasks.clean, done => {
const dirs = [paths.plyr.output, paths.demo.output].map(dir => path.join(dir, '**/*'));
// Don't delete the mp4
dirs.push(`!${path.join(paths.plyr.output, '**/*.mp4')}`);
del(dirs);
done();
});
// JavaScript
Object.entries(build.js).forEach(([filename, entry]) => {
const { dist, formats, namespace, polyfill, src } = entry;
formats.forEach(format => {
const name = `js:${filename}:${format}`;
const extension = format === 'es' ? 'mjs' : 'js';
tasks.js.push(name);
gulp.task(name, () =>
gulp
.src(src)
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(
rollup(
{
plugins: [
resolve(),
commonjs(),
babel({
presets: [
[
'@babel/env',
{
// debug: true,
useBuiltIns: polyfill ? 'usage' : false,
corejs: polyfill ? 3 : undefined,
},
],
],
babelrc: false,
exclude: [/\/core-js\//],
}),
],
},
{
name: namespace,
format,
},
),
)
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
.pipe(
rename({
extname: `.${extension}`,
}),
)
.pipe(gulp.dest(dist))
.pipe(filter(`**/*.${extension}`))
.pipe(terser())
.pipe(rename({ suffix: minSuffix }))
.pipe(size(sizeOptions))
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(dist)),
);
});
});
// CSS
Object.entries(build.css).forEach(([filename, entry]) => {
const { dist, src } = entry;
const name = `css:${filename}`;
tasks.css.push(name);
gulp.task(name, () =>
gulp
.src(src)
.pipe(plumber())
.pipe(sass())
.pipe(
prefix(browsers, {
cascade: false,
}),
)
.pipe(clean())
.pipe(size(sizeOptions))
.pipe(gulp.dest(dist)),
);
});
// SVG Sprites
Object.entries(build.sprite).forEach(([filename, entry]) => {
const { dist, src } = entry;
const name = `sprite:${filename}`;
tasks.sprite.push(name);
gulp.task(name, () =>
gulp
.src(src)
.pipe(plumber())
.pipe(
imagemin([
imagemin.svgo({
plugins: [{ removeViewBox: false }],
}),
]),
)
.pipe(svgstore())
.pipe(rename({ basename: path.parse(filename).name }))
.pipe(size(sizeOptions))
.pipe(gulp.dest(dist)),
);
});
// Build all JS
gulp.task('js', () => gulp.parallel(...tasks.js));
// Watch for file changes
gulp.task('watch', () => {
// Plyr core
gulp.watch(paths.plyr.src.js, gulp.parallel(...tasks.js));
gulp.watch(paths.plyr.src.sass, gulp.parallel(...tasks.css));
gulp.watch(paths.plyr.src.sprite, gulp.parallel(...tasks.sprite));
// Demo
gulp.watch(paths.demo.src.js, gulp.parallel(...tasks.js));
gulp.watch(paths.demo.src.sass, gulp.parallel(...tasks.css));
});
// Serve via browser sync
gulp.task('serve', () =>
browserSync.init({
server: {
baseDir: paths.demo.root,
},
notify: false,
watch: true,
ghostMode: false,
}),
);
// Build distribution
gulp.task('build', gulp.series(tasks.clean, gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite)));
// Default gulp task
gulp.task('default', gulp.series('build', gulp.parallel('serve', 'watch')));
// Publish a version to CDN and demo
// --------------------------------------------
// Get deployment config
let credentials = {};
try {
credentials = require('./credentials.json'); //eslint-disable-line
} catch (e) {
// Do nothing
}
// Get branch info
const branch = {
current: gitbranch.sync(),
master: 'master',
beta: 'beta',
};
const maxAge = 31536000; // 1 year
const options = {
cdn: {
headers: {
'Cache-Control': `max-age=${maxAge}`,
},
},
demo: {
uploadPath: branch.current === branch.beta ? '/beta' : null,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
},
},
symlinks(ver, filename) {
return {
headers: {
// http://stackoverflow.com/questions/2272835/amazon-s3-object-redirect
'x-amz-website-redirect-location': `/${ver}/${filename}`,
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
},
};
},
};
const regex =
'(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?';
const semver = new RegExp(`v${regex}`, 'gi');
const localPath = new RegExp('(../)?dist', 'gi');
const versionPath = `https://${deploy.cdn.domain}/${version}`;
const cdnpath = new RegExp(`${deploy.cdn.domain}/${regex}/`, 'gi');
const renameFile = rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
});
// Check we're on the correct branch to deploy
const canDeploy = () => {
const allowed = [branch.master, branch.beta];
if (!allowed.includes(branch.current)) {
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
return false;
}
return true;
};
gulp.task('version', done => {
if (!canDeploy()) {
done();
return null;
}
const { domain } = deploy.cdn;
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'];
return gulp
.src(
files.map(file => path.join(__dirname, `src/js/${file}`)),
{ base: '.' },
)
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${domain}/${version}/`))
.pipe(gulp.dest('./'));
});
// Publish version to CDN bucket
gulp.task('cdn', done => {
if (!canDeploy()) {
done();
return null;
}
const { domain, publisher } = deploy.cdn;
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`);
// Upload to CDN
return (
gulp
.src(paths.upload)
.pipe(renameFile)
// Remove min suffix from source map URL
.pipe(
replace(
/sourceMappingURL=([\w-?.]+)/,
(match, filename) => `sourceMappingURL=${filename.replace(minSuffix, '')}`,
),
)
.pipe(size(sizeOptions))
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.cdn.headers))
.pipe(publish.reporter())
);
});
// Purge the fastly cache incase any 403/404 are cached
gulp.task('purge', () => {
if (!Object.keys(credentials).includes('fastly')) {
throw new Error('Fastly credentials required to purge cache.');
}
const { fastly } = credentials;
const list = [];
return gulp
.src(paths.upload)
.pipe(
through.obj((file, enc, cb) => {
const filename = file.path.split('/').pop();
list.push(`${versionPath}/${filename.replace(minSuffix, '')}`);
cb(null);
}),
)
.on('end', () => {
const purge = new FastlyPurge(fastly.token);
list.forEach(url => {
log(`Purging ${ansi.cyan(url)}...`);
purge.url(url, (error, result) => {
if (error) {
log.error(error);
} else if (result) {
log(result);
}
});
});
});
});
// Publish to demo bucket
gulp.task('demo', done => {
if (!canDeploy()) {
done();
return null;
}
const { publisher } = deploy.demo;
const { domain } = deploy.cdn;
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`);
// Replace versioned files in readme.md
gulp.src([`${__dirname}/readme.md`])
.pipe(replace(cdnpath, `${domain}/${version}/`))
.pipe(gulp.dest(__dirname));
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
const index = `${paths.demo.root}index.html`;
const error = `${paths.demo.root}error.html`;
const pages = [index];
if (branch.current === branch.master) {
pages.push(error);
}
return gulp
.src(pages)
.pipe(replace(localPath, versionPath))
.pipe(
rename(p => {
if (options.demo.uploadPath) {
// eslint-disable-next-line no-param-reassign
p.dirname += options.demo.uploadPath;
}
}),
)
.pipe(publisher.publish(options.demo.headers))
.pipe(publish.reporter());
});
gulp.task('error', done => {
// Only update CDN for master (prod)
if (!canDeploy() || branch.current !== branch.master) {
done();
return null;
}
const { publisher } = deploy.cdn;
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
// Upload error.html to cdn
return gulp
.src(`${paths.demo.root}error.html`)
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.demo.headers))
.pipe(publish.reporter());
});
// Open the demo site to check it's ok
gulp.task('open', () => {
const { domain } = deploy.demo;
return gulp.src(__filename).pipe(
open({
uri: `https://${domain}/${branch.current === branch.beta ? 'beta' : ''}`,
}),
);
});
// Do everything
gulp.task(
'deploy',
gulp.series(
'version',
tasks.clean,
gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite),
'cdn',
'demo',
'purge',
'open',
),
);
+98 -96
View File
@@ -1,98 +1,100 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.5.10", "version": "3.6.2",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player", "description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io", "homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>", "author": "Sam Potts <sam@potts.es>",
"main": "dist/plyr.js", "main": "dist/plyr.js",
"types": "src/js/plyr.d.ts", "types": "src/js/plyr.d.ts",
"module": "dist/plyr.min.mjs", "module": "dist/plyr.min.mjs",
"jsnext:main": "dist/plyr.min.mjs", "jsnext:main": "dist/plyr.min.mjs",
"browser": "dist/plyr.min.js", "browser": "dist/plyr.min.js",
"sass": "src/sass/plyr.scss", "sass": "src/sass/plyr.scss",
"style": "dist/plyr.css", "style": "dist/plyr.css",
"keywords": [ "keywords": [
"HTML5 Video", "HTML5 Video",
"HTML5 Audio", "HTML5 Audio",
"Media Player", "Media Player",
"DASH", "DASH",
"Shaka", "Shaka",
"WordPress", "WordPress",
"HLS" "HLS"
], ],
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/sampotts/plyr.git" "url": "git://github.com/sampotts/plyr.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/sampotts/plyr/issues" "url": "https://github.com/sampotts/plyr/issues"
}, },
"browserslist": "> 1%", "browserslist": "> 1%",
"scripts": { "scripts": {
"build": "gulp build", "build": "gulp build",
"lint": "eslint src/js && npm run-script remark", "lint": "eslint src/js && npm run-script remark",
"lint:fix": "eslint --fix src/js", "lint:fix": "eslint --fix src/js",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'", "remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"deploy": "yarn lint && gulp deploy" "deploy": "yarn lint && gulp version && gulp build && gulp deploy",
}, "format": "prettier --write \"./{src,demo/src}/**/*.{js,scss}\""
"devDependencies": { },
"ansi-colors": "^4.1.1", "devDependencies": {
"aws-sdk": "^2.647.0", "ansi-colors": "^4.1.1",
"@babel/core": "^7.9.0", "autoprefixer": "^9.7.6",
"@babel/preset-env": "^7.9.0", "aws-sdk": "^2.668.0",
"babel-eslint": "^10.1.0", "@babel/core": "^7.9.6",
"browser-sync": "^2.26.7", "@babel/preset-env": "^7.9.6",
"del": "^5.1.0", "babel-eslint": "^10.1.0",
"eslint": "^6.8.0", "browser-sync": "^2.26.7",
"eslint-config-airbnb-base": "^14.1.0", "del": "^5.1.0",
"eslint-config-prettier": "^6.10.1", "eslint": "^6.8.0",
"eslint-plugin-import": "^2.20.1", "eslint-config-airbnb-base": "^14.1.0",
"eslint-plugin-simple-import-sort": "^5.0.2", "eslint-config-prettier": "^6.11.0",
"fancy-log": "^1.3.3", "eslint-plugin-import": "^2.20.2",
"fastly-purge": "^1.0.1", "eslint-plugin-simple-import-sort": "^5.0.3",
"git-branch": "^2.0.1", "fancy-log": "^1.3.3",
"gulp": "^4.0.2", "fastly-purge": "^1.0.1",
"gulp-autoprefixer": "^7.0.1", "git-branch": "^2.0.1",
"gulp-awspublish": "^4.1.1", "gulp": "^4.0.2",
"gulp-better-rollup": "^4.0.1", "gulp-awspublish": "^4.1.1",
"gulp-clean-css": "^4.3.0", "gulp-better-rollup": "^4.0.1",
"gulp-filter": "^6.0.0", "gulp-filter": "^6.0.0",
"gulp-header": "^2.0.9", "gulp-header": "^2.0.9",
"gulp-imagemin": "^7.1.0", "gulp-hub": "^4.2.0",
"gulp-open": "^3.0.1", "gulp-imagemin": "^7.1.0",
"gulp-plumber": "^1.2.1", "gulp-open": "^3.0.1",
"gulp-postcss": "^8.0.0", "gulp-plumber": "^1.2.1",
"gulp-rename": "^2.0.0", "gulp-postcss": "^8.0.0",
"gulp-replace": "^1.0.0", "gulp-rename": "^2.0.0",
"gulp-sass": "^4.0.2", "gulp-replace": "^1.0.0",
"gulp-size": "^3.0.0", "gulp-sass": "^4.1.0",
"gulp-sourcemaps": "^2.6.5", "gulp-size": "^3.0.0",
"gulp-svgstore": "^7.0.1", "gulp-sourcemaps": "^2.6.5",
"gulp-terser": "^1.2.0", "gulp-svgstore": "^7.0.1",
"postcss-custom-properties": "^9.1.1", "gulp-terser": "^1.2.0",
"prettier-eslint": "^9.0.1", "postcss-clean": "^1.1.0",
"prettier-stylelint": "^0.4.2", "postcss-custom-properties": "^9.1.1",
"remark-cli": "^7.0.1", "prettier-eslint": "^9.0.1",
"remark-validate-links": "^10.0.0", "prettier-stylelint": "^0.4.2",
"rollup": "^2.2.0", "remark-cli": "^8.0.0",
"rollup-plugin-babel": "^4.4.0", "remark-validate-links": "^10.0.0",
"rollup-plugin-commonjs": "^10.1.0", "rollup": "^2.7.6",
"rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-babel": "^4.4.0",
"stylelint": "^13.2.1", "rollup-plugin-commonjs": "^10.1.0",
"stylelint-config-prettier": "^8.0.1", "rollup-plugin-node-resolve": "^5.2.0",
"stylelint-config-recommended": "^3.0.0", "stylelint": "^13.3.3",
"stylelint-config-sass-guidelines": "^7.0.0", "stylelint-config-prettier": "^8.0.1",
"stylelint-order": "^4.0.0", "stylelint-config-recommended": "^3.0.0",
"stylelint-scss": "^3.16.0", "stylelint-config-sass-guidelines": "^7.0.0",
"stylelint-selector-bem-pattern": "^2.1.0", "stylelint-order": "^4.0.0",
"through2": "^3.0.1" "stylelint-scss": "^3.17.1",
}, "stylelint-selector-bem-pattern": "^2.1.0",
"dependencies": { "through2": "^3.0.1"
"core-js": "^3.6.4", },
"custom-event-polyfill": "^1.0.7", "dependencies": {
"loadjs": "^4.2.0", "core-js": "^3.6.5",
"rangetouch": "^2.0.1", "custom-event-polyfill": "^1.0.7",
"url-polyfill": "^1.1.8" "loadjs": "^4.2.0",
} "rangetouch": "^2.0.1",
"url-polyfill": "^1.1.8"
}
} }
+34 -34
View File
@@ -1,37 +1,37 @@
{ {
"folders": [ "folders": [
{ {
"path": "." "path": "."
}
],
"settings": {
"search.exclude": {
"**/node_modules": true,
"**/dist": true
},
// Linting
"stylelint.enable": true,
"css.validate": false,
"scss.validate": false,
"javascript.validate.enable": false,
// Formatting
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
// Trim on save
"files.trimTrailingWhitespace": true,
// Special file associations
"files.associations": {
".eslintrc": "jsonc"
},
"editor.codeActionsOnSave": {
"source.fixAll": true
}
} }
],
"settings": {
"search.exclude": {
"**/node_modules": true,
"**/dist": true
},
// Linting
"stylelint.enable": true,
"css.validate": false,
"scss.validate": false,
"javascript.validate.enable": false,
// Formatting
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
// Trim on save
"files.trimTrailingWhitespace": true,
// Special file associations
"files.associations": {
".eslintrc": "jsonc"
},
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}
} }
+328 -311
View File
@@ -8,12 +8,12 @@ import support from './support';
import { dedupe } from './utils/arrays'; import { dedupe } from './utils/arrays';
import browser from './utils/browser'; import browser from './utils/browser';
import { import {
createElement, createElement,
emptyElement, emptyElement,
getAttributesFromSelector, getAttributesFromSelector,
insertAfter, insertAfter,
removeElement, removeElement,
toggleClass, toggleClass,
} from './utils/elements'; } from './utils/elements';
import { on, triggerEvent } from './utils/events'; import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch'; import fetch from './utils/fetch';
@@ -23,368 +23,385 @@ import { getHTML } from './utils/strings';
import { parseUrl } from './utils/urls'; import { parseUrl } from './utils/urls';
const captions = { const captions = {
// Setup captions // Setup captions
setup() { setup() {
// Requires UI support // Requires UI support
if (!this.supported.ui) { if (!this.supported.ui) {
return; return;
} }
// Only Vimeo and HTML5 video supported at this point // Only Vimeo and HTML5 video supported at this point
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
// Clear menu and hide // Clear menu and hide
if ( if (
is.array(this.config.controls) && is.array(this.config.controls) &&
this.config.controls.includes('settings') && this.config.controls.includes('settings') &&
this.config.settings.includes('captions') this.config.settings.includes('captions')
) { ) {
controls.setCaptionsMenu.call(this); controls.setCaptionsMenu.call(this);
} }
return; return;
} }
// Inject the container // Inject the container
if (!is.element(this.elements.captions)) { if (!is.element(this.elements.captions)) {
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions)); this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
insertAfter(this.elements.captions, this.elements.wrapper); insertAfter(this.elements.captions, this.elements.wrapper);
} }
// Fix IE captions if CORS is used // Fix IE captions if CORS is used
// Fetch captions and inject as blobs instead (data URIs not supported!) // Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) { if (browser.isIE && window.URL) {
const elements = this.media.querySelectorAll('track'); const elements = this.media.querySelectorAll('track');
Array.from(elements).forEach(track => { Array.from(elements).forEach(track => {
const src = track.getAttribute('src'); const src = track.getAttribute('src');
const url = parseUrl(src); const url = parseUrl(src);
if ( if (
url !== null && url !== null &&
url.hostname !== window.location.href.hostname && url.hostname !== window.location.href.hostname &&
['http:', 'https:'].includes(url.protocol) ['http:', 'https:'].includes(url.protocol)
) { ) {
fetch(src, 'blob') fetch(src, 'blob')
.then(blob => { .then(blob => {
track.setAttribute('src', window.URL.createObjectURL(blob)); track.setAttribute('src', window.URL.createObjectURL(blob));
}) })
.catch(() => { .catch(() => {
removeElement(track); removeElement(track);
});
}
}); });
} }
});
}
// Get and set initial data // Get and set initial data
// The "preferred" options are not realized unless / until the wanted language has a match // The "preferred" options are not realized unless / until the wanted language has a match
// * languages: Array of user's browser languages. // * languages: Array of user's browser languages.
// * language: The language preferred by user settings or config // * language: The language preferred by user settings or config
// * active: The state preferred by user settings or config // * active: The state preferred by user settings or config
// * toggled: The real captions state // * toggled: The real captions state
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
const languages = dedupe(browserLanguages.map(language => language.split('-')[0])); const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
// Use first browser language when language is 'auto' // Use first browser language when language is 'auto'
if (language === 'auto') { if (language === 'auto') {
[language] = languages; [language] = languages;
} }
let active = this.storage.get('captions'); let active = this.storage.get('captions');
if (!is.boolean(active)) { if (!is.boolean(active)) {
({ active } = this.config.captions); ({ active } = this.config.captions);
} }
Object.assign(this.captions, { Object.assign(this.captions, {
toggled: false, toggled: false,
active, active,
language, language,
languages, languages,
});
// Watch changes to textTracks and update captions menu
if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
}
// Update available languages in list next tick (the event must not be triggered before the listeners)
setTimeout(captions.update.bind(this), 0);
},
// Update available language options in settings based on tracks
update() {
const tracks = captions.getTracks.call(this, true);
// Get the wanted language
const { active, language, meta, currentTrackNode } = this.captions;
const languageExists = Boolean(tracks.find(track => track.language === language));
// 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',
});
// Turn off native caption rendering to avoid double captions
// Note: mode='hidden' forces a track to download. To ensure every track
// isn't downloaded at once, only 'showing' tracks should be reassigned
// eslint-disable-next-line no-param-reassign
if (track.mode === 'showing') {
// eslint-disable-next-line no-param-reassign
track.mode = 'hidden';
}
// Add event listener for cue changes
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
}); });
}
// Watch changes to textTracks and update captions menu // Update language first time it matches, or if the previous matching track was removed
if (this.isHTML5) { if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; captions.setLanguage.call(this, language);
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this)); captions.toggle.call(this, active && languageExists);
} }
// Update available languages in list next tick (the event must not be triggered before the listeners) // Enable or disable captions based on track length
setTimeout(captions.update.bind(this), 0); toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
},
// Update available language options in settings based on tracks // Update available languages in list
update() { if (
const tracks = captions.getTracks.call(this, true); is.array(this.config.controls) &&
// Get the wanted language this.config.controls.includes('settings') &&
const { active, language, meta, currentTrackNode } = this.captions; this.config.settings.includes('captions')
const languageExists = Boolean(tracks.find(track => track.language === language)); ) {
controls.setCaptionsMenu.call(this);
}
},
// Handle tracks (add event listener and "pseudo"-default) // Toggle captions display
if (this.isHTML5 && this.isVideo) { // Used internally for the toggleCaptions method, with the passive option forced to false
tracks toggle(input, passive = true) {
.filter(track => !meta.get(track)) // If there's no full support
.forEach(track => { if (!this.supported.ui) {
this.debug.log('Track added', track); return;
// 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 const { toggled } = this.captions; // Current state
// eslint-disable-next-line no-param-reassign const activeClass = this.config.classNames.captions.active;
track.mode = 'hidden'; // Get the next state
// If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
// Add event listener for cue changes // Update state and trigger event
on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); if (active !== toggled) {
}); // When passive, don't override user preferences
} if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
// Update language first time it matches, or if the previous matching track was removed // Force language if the call isn't passive and there is no matching language to toggle to
if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) { if (!this.language && active && !passive) {
captions.setLanguage.call(this, language);
captions.toggle.call(this, active && languageExists);
}
// Enable or disable captions based on track length
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
// Update available languages in list
if ((is.array(this.config.controls) && this.config.controls.includes('settings'))
&& this.config.settings.includes('captions')) {
controls.setCaptionsMenu.call(this);
}
},
// Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false
toggle(input, passive = true) {
// If there's no full support
if (!this.supported.ui) {
return;
}
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;
// Update state and trigger event
if (active !== toggled) {
// When passive, don't override user preferences
if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
// Force language if the call isn't passive and there is no matching language to toggle to
if (!this.language && active && !passive) {
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
// Override user preferences to avoid switching languages if a matching track is added
this.captions.language = track.language;
// Set caption, but don't store in localStorage as user preference
captions.set.call(this, tracks.indexOf(track));
return;
}
// Toggle button if it's enabled
if (this.elements.buttons.captions) {
this.elements.buttons.captions.pressed = active;
}
// Add class hook
toggleClass(this.elements.container, activeClass, active);
this.captions.toggled = active;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
}
},
// Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false
set(index, passive = true) {
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
// Disable captions if setting to -1 // Override user preferences to avoid switching languages if a matching track is added
if (index === -1) { this.captions.language = track.language;
captions.toggle.call(this, false, passive);
return;
}
if (!is.number(index)) { // Set caption, but don't store in localStorage as user preference
this.debug.warn('Invalid caption argument', index); captions.set.call(this, tracks.indexOf(track));
return; return;
} }
if (!(index in tracks)) { // Toggle button if it's enabled
this.debug.warn('Track not found', index); if (this.elements.buttons.captions) {
return; this.elements.buttons.captions.pressed = active;
} }
if (this.captions.currentTrack !== index) { // Add class hook
this.captions.currentTrack = index; toggleClass(this.elements.container, activeClass, active);
const track = tracks[index];
const { language } = track || {};
// Store reference to node for invalidation on remove this.captions.toggled = active;
this.captions.currentTrackNode = track;
// Update settings menu // Update settings menu
controls.updateSetting.call(this, 'captions'); controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences // Trigger event (not used internally)
if (!passive) { triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
this.captions.language = language; }
this.storage.set({ language });
}
// Handle Vimeo captions // Wait for the call stack to clear before setting mode='hidden'
if (this.isVimeo) { // on the active track - forcing the browser to download it
this.embed.enableTextTrack(language); setTimeout(() => {
} if (active && this.captions.toggled) {
this.captions.currentTrackNode.mode = 'hidden';
}
});
},
// Trigger event // Set captions by track index
triggerEvent.call(this, this.media, 'languagechange'); // Used internally for the currentTrack setter with the passive option forced to false
} set(index, passive = true) {
const tracks = captions.getTracks.call(this);
// Show captions // Disable captions if setting to -1
captions.toggle.call(this, true, passive); if (index === -1) {
captions.toggle.call(this, false, passive);
return;
}
if (this.isHTML5 && this.isVideo) { if (!is.number(index)) {
// If we change the active track while a cue is already displayed we need to update it this.debug.warn('Invalid caption argument', index);
captions.updateCues.call(this); return;
} }
},
// Set captions by language if (!(index in tracks)) {
// Used internally for the language setter with the passive option forced to false this.debug.warn('Track not found', index);
setLanguage(input, passive = true) { return;
if (!is.string(input)) { }
this.debug.warn('Invalid language argument', input);
return; if (this.captions.currentTrack !== index) {
} this.captions.currentTrack = index;
// Normalize const track = tracks[index];
const language = input.toLowerCase(); const { language } = track || {};
// Store reference to node for invalidation on remove
this.captions.currentTrackNode = track;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences
if (!passive) {
this.captions.language = language; this.captions.language = language;
this.storage.set({ language });
}
// Set currentTrack // Handle Vimeo captions
const tracks = captions.getTracks.call(this); if (this.isVimeo) {
const track = captions.findTrack.call(this, [language]); this.embed.enableTextTrack(language);
captions.set.call(this, tracks.indexOf(track), passive); }
},
// Get current valid caption tracks // Trigger event
// If update is false it will also ignore tracks without metadata triggerEvent.call(this, this.media, 'languagechange');
// This is used to "freeze" the language options when captions.update is false }
getTracks(update = false) {
// Handle media or textTracks missing or null
const tracks = Array.from((this.media || {}).textTracks || []);
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
.filter(track => ['captions', 'subtitles'].includes(track.kind));
},
// Match tracks based on languages and get the first // Show captions
findTrack(languages, force = false) { captions.toggle.call(this, true, passive);
const tracks = captions.getTracks.call(this);
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 => { if (this.isHTML5 && this.isVideo) {
track = sorted.find(t => t.language === language); // If we change the active track while a cue is already displayed we need to update it
return !track; // Break iteration if there is a match captions.updateCues.call(this);
}); }
},
// If no match is found but is required, get first // Set captions by language
return track || (force ? sorted[0] : undefined); // Used internally for the language setter with the passive option forced to false
}, setLanguage(input, passive = true) {
if (!is.string(input)) {
this.debug.warn('Invalid language argument', input);
return;
}
// Normalize
const language = input.toLowerCase();
this.captions.language = language;
// Get the current track // Set currentTrack
getCurrentTrack() { const tracks = captions.getTracks.call(this);
return captions.getTracks.call(this)[this.currentTrack]; const track = captions.findTrack.call(this, [language]);
}, captions.set.call(this, tracks.indexOf(track), passive);
},
// Get UI label for track // Get current valid caption tracks
getLabel(track) { // If update is false it will also ignore tracks without metadata
let currentTrack = track; // This is used to "freeze" the language options when captions.update is false
getTracks(update = false) {
// Handle media or textTracks missing or null
const tracks = Array.from((this.media || {}).textTracks || []);
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
.filter(track => ['captions', 'subtitles'].includes(track.kind));
},
if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) { // Match tracks based on languages and get the first
currentTrack = captions.getCurrentTrack.call(this); findTrack(languages, force = false) {
} const tracks = captions.getTracks.call(this);
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
let track;
if (is.track(currentTrack)) { languages.every(language => {
if (!is.empty(currentTrack.label)) { track = sorted.find(t => t.language === language);
return currentTrack.label; return !track; // Break iteration if there is a match
} });
if (!is.empty(currentTrack.language)) { // If no match is found but is required, get first
return track.language.toUpperCase(); return track || (force ? sorted[0] : undefined);
} },
return i18n.get('enabled', this.config); // Get the current track
} getCurrentTrack() {
return captions.getTracks.call(this)[this.currentTrack];
},
return i18n.get('disabled', this.config); // Get UI label for track
}, getLabel(track) {
let currentTrack = track;
// Update captions using current track's active cues if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
// Also optional array argument in case there isn't any track (ex: vimeo) currentTrack = captions.getCurrentTrack.call(this);
updateCues(input) { }
// Requires UI
if (!this.supported.ui) {
return;
}
if (!is.element(this.elements.captions)) { if (is.track(currentTrack)) {
this.debug.warn('No captions element to render to'); if (!is.empty(currentTrack.label)) {
return; return currentTrack.label;
} }
// Only accept array or empty input if (!is.empty(currentTrack.language)) {
if (!is.nullOrUndefined(input) && !Array.isArray(input)) { return track.language.toUpperCase();
this.debug.warn('updateCues: Invalid input', input); }
return;
}
let cues = input; return i18n.get('enabled', this.config);
}
// Get cues from track return i18n.get('disabled', this.config);
if (!cues) { },
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || []) // Update captions using current track's active cues
.map(cue => cue.getCueAsHTML()) // Also optional array argument in case there isn't any track (ex: vimeo)
.map(getHTML); updateCues(input) {
} // Requires UI
if (!this.supported.ui) {
return;
}
// Set new caption text if (!is.element(this.elements.captions)) {
const content = cues.map(cueText => cueText.trim()).join('\n'); this.debug.warn('No captions element to render to');
const changed = content !== this.elements.captions.innerHTML; return;
}
if (changed) { // Only accept array or empty input
// Empty the container and create a new child element if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
emptyElement(this.elements.captions); this.debug.warn('updateCues: Invalid input', input);
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption)); return;
caption.innerHTML = content; }
this.elements.captions.appendChild(caption);
// Trigger event let cues = input;
triggerEvent.call(this, this.media, 'cuechange');
} // Get cues from track
}, if (!cues) {
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || [])
.map(cue => cue.getCueAsHTML())
.map(getHTML);
}
// Set new caption text
const content = cues.map(cueText => cueText.trim()).join('\n');
const changed = content !== this.elements.captions.innerHTML;
if (changed) {
// Empty the container and create a new child element
emptyElement(this.elements.captions);
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
caption.innerHTML = content;
this.elements.captions.appendChild(caption);
// Trigger event
triggerEvent.call(this, this.media, 'cuechange');
}
},
}; };
export default captions; export default captions;
+425 -422
View File
@@ -3,437 +3,440 @@
// ========================================================================== // ==========================================================================
const defaults = { const defaults = {
// Disable // Disable
enabled: true,
// Custom media title
title: '',
// Logging to console
debug: false,
// Auto play (if supported)
autoplay: false,
// Only allow one media playing at once (vimeo only)
autopause: true,
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
playsinline: true,
// Default time to skip when rewind/fast forward
seekTime: 10,
// Default volume
volume: 1,
muted: false,
// Pass a custom duration
duration: null,
// Display the media duration on load in the current time position
// If you have opted to display both duration and currentTime, this is ignored
displayDuration: true,
// Invert the current time to be a countdown
invertTime: true,
// Clicking the currentTime inverts it's value to show time left rather than elapsed
toggleInvert: true,
// Force an aspect ratio
// The format must be `'w:h'` (e.g. `'16:9'`)
ratio: null,
// Click video container to play/pause
clickToPlay: true,
// Auto hide the controls
hideControls: true,
// Reset to start when playback ended
resetOnEnd: false,
// Disable the standard context menu
disableContextMenu: true,
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.6.1/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default
quality: {
default: 576,
// The options to display in the UI, if available for the source media
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
forced: false,
onChange: null,
},
// Set loops
loop: {
active: false,
// start: null,
// end: null,
},
// Speed default and options to display
speed: {
selected: 1,
// The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],
},
// Keyboard shortcut settings
keyboard: {
focused: true,
global: false,
},
// Display tooltips
tooltips: {
controls: false,
seek: true,
},
// Captions settings
captions: {
active: false,
language: 'auto',
// Listen to new tracks added after Plyr is initialized.
// This is needed for streaming captions, but may result in unselectable options
update: false,
},
// Fullscreen settings
fullscreen: {
enabled: true, // Allow fullscreen?
fallback: true, // Fallback using full viewport/window
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
// Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode
// Non-ancestors of the player element will be ignored
// container: null, // defaults to the player element
},
// Local storage
storage: {
enabled: true, enabled: true,
key: 'plyr',
},
// Custom media title // Default controls
title: '', controls: [
'play-large',
// 'restart',
// 'rewind',
'play',
// 'fast-forward',
'progress',
'current-time',
// 'duration',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
// 'download',
'fullscreen',
],
settings: ['captions', 'quality', 'speed'],
// Logging to console // Localisation
debug: false, i18n: {
restart: 'Restart',
// Auto play (if supported) rewind: 'Rewind {seektime}s',
autoplay: false, play: 'Play',
pause: 'Pause',
// Only allow one media playing at once (vimeo only) fastForward: 'Forward {seektime}s',
autopause: true, seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present) played: 'Played',
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work) buffered: 'Buffered',
playsinline: true, currentTime: 'Current time',
duration: 'Duration',
// Default time to skip when rewind/fast forward volume: 'Volume',
seekTime: 10, mute: 'Mute',
unmute: 'Unmute',
// Default volume enableCaptions: 'Enable captions',
volume: 1, disableCaptions: 'Disable captions',
muted: false, download: 'Download',
enterFullscreen: 'Enter fullscreen',
// Pass a custom duration exitFullscreen: 'Exit fullscreen',
duration: null, frameTitle: 'Player for {title}',
captions: 'Captions',
// Display the media duration on load in the current time position settings: 'Settings',
// If you have opted to display both duration and currentTime, this is ignored pip: 'PIP',
displayDuration: true, menuBack: 'Go back to previous menu',
speed: 'Speed',
// Invert the current time to be a countdown normal: 'Normal',
invertTime: true, quality: 'Quality',
loop: 'Loop',
// Clicking the currentTime inverts it's value to show time left rather than elapsed start: 'Start',
toggleInvert: true, end: 'End',
all: 'All',
// Force an aspect ratio reset: 'Reset',
// The format must be `'w:h'` (e.g. `'16:9'`) disabled: 'Disabled',
ratio: null, enabled: 'Enabled',
advertisement: 'Ad',
// Click video container to play/pause qualityBadge: {
clickToPlay: true, 2160: '4K',
1440: 'HD',
// Auto hide the controls 1080: 'HD',
hideControls: true, 720: 'HD',
576: 'SD',
// Reset to start when playback ended 480: 'SD',
resetOnEnd: false,
// Disable the standard context menu
disableContextMenu: true,
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default
quality: {
default: 576,
// The options to display in the UI, if available for the source media
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
forced: false,
onChange: null,
}, },
},
// Set loops // URLs
loop: { urls: {
active: false, download: null,
// start: null,
// end: null,
},
// Speed default and options to display
speed: {
selected: 1,
// The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],
},
// Keyboard shortcut settings
keyboard: {
focused: true,
global: false,
},
// Display tooltips
tooltips: {
controls: false,
seek: true,
},
// Captions settings
captions: {
active: false,
language: 'auto',
// Listen to new tracks added after Plyr is initialized.
// This is needed for streaming captions, but may result in unselectable options
update: false,
},
// Fullscreen settings
fullscreen: {
enabled: true, // Allow fullscreen?
fallback: true, // Fallback using full viewport/window
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
},
// Local storage
storage: {
enabled: true,
key: 'plyr',
},
// Default controls
controls: [
'play-large',
// 'restart',
// 'rewind',
'play',
// 'fast-forward',
'progress',
'current-time',
// 'duration',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
// 'download',
'fullscreen',
],
settings: ['captions', 'quality', 'speed'],
// Localisation
i18n: {
restart: 'Restart',
rewind: 'Rewind {seektime}s',
play: 'Play',
pause: 'Pause',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
duration: 'Duration',
volume: 'Volume',
mute: 'Mute',
unmute: 'Unmute',
enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions',
download: 'Download',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
pip: 'PIP',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
loop: 'Loop',
start: 'Start',
end: 'End',
all: 'All',
reset: 'Reset',
disabled: 'Disabled',
enabled: 'Enabled',
advertisement: 'Ad',
qualityBadge: {
2160: '4K',
1440: 'HD',
1080: 'HD',
720: 'HD',
576: 'SD',
480: 'SD',
},
},
// URLs
urls: {
download: null,
vimeo: {
sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
api: 'https://vimeo.com/api/v2/video/{0}.json',
},
youtube: {
sdk: 'https://www.youtube.com/iframe_api',
api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
},
// Custom control listeners
listeners: {
seek: null,
play: null,
pause: null,
restart: null,
rewind: null,
fastForward: null,
mute: null,
volume: null,
captions: null,
download: null,
fullscreen: null,
pip: null,
airplay: null,
speed: null,
quality: null,
loop: null,
language: null,
},
// Events to watch and bubble
events: [
// Events to watch on HTML5 media elements and bubble
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
'ended',
'progress',
'stalled',
'playing',
'waiting',
'canplay',
'canplaythrough',
'loadstart',
'loadeddata',
'loadedmetadata',
'timeupdate',
'volumechange',
'play',
'pause',
'error',
'seeking',
'seeked',
'emptied',
'ratechange',
'cuechange',
// Custom events
'download',
'enterfullscreen',
'exitfullscreen',
'captionsenabled',
'captionsdisabled',
'languagechange',
'controlshidden',
'controlsshown',
'ready',
// YouTube
'statechange',
// Quality
'qualitychange',
// Ads
'adsloaded',
'adscontentpause',
'adscontentresume',
'adstarted',
'adsmidpoint',
'adscomplete',
'adsallcomplete',
'adsimpression',
'adsclick',
],
// Selectors
// Change these to match your template if using custom HTML
selectors: {
editable: 'input, textarea, select, [contenteditable]',
container: '.plyr',
controls: {
container: null,
wrapper: '.plyr__controls',
},
labels: '[data-plyr]',
buttons: {
play: '[data-plyr="play"]',
pause: '[data-plyr="pause"]',
restart: '[data-plyr="restart"]',
rewind: '[data-plyr="rewind"]',
fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]',
download: '[data-plyr="download"]',
fullscreen: '[data-plyr="fullscreen"]',
pip: '[data-plyr="pip"]',
airplay: '[data-plyr="airplay"]',
settings: '[data-plyr="settings"]',
loop: '[data-plyr="loop"]',
},
inputs: {
seek: '[data-plyr="seek"]',
volume: '[data-plyr="volume"]',
speed: '[data-plyr="speed"]',
language: '[data-plyr="language"]',
quality: '[data-plyr="quality"]',
},
display: {
currentTime: '.plyr__time--current',
duration: '.plyr__time--duration',
buffer: '.plyr__progress__buffer',
loop: '.plyr__progress__loop', // Used later
volume: '.plyr__volume--display',
},
progress: '.plyr__progress',
captions: '.plyr__captions',
caption: '.plyr__caption',
},
// Class hooks added to the player in different states
classNames: {
type: 'plyr--{0}',
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',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
hover: 'plyr--hover',
tooltip: 'plyr__tooltip',
cues: 'plyr__cues',
hidden: 'plyr__sr-only',
hideControls: 'plyr--hide-controls',
isIos: 'plyr--is-ios',
isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
display: {
time: 'plyr__time',
},
menu: {
value: 'plyr__menu__value',
badge: 'plyr__badge',
open: 'plyr--menu-open',
},
captions: {
enabled: 'plyr--captions-enabled',
active: 'plyr--captions-active',
},
fullscreen: {
enabled: 'plyr--fullscreen-enabled',
fallback: 'plyr--fullscreen-fallback',
},
pip: {
supported: 'plyr--pip-supported',
active: 'plyr--pip-active',
},
airplay: {
supported: 'plyr--airplay-supported',
active: 'plyr--airplay-active',
},
tabFocus: 'plyr__tab-focus',
previewThumbnails: {
// Tooltip thumbs
thumbContainer: 'plyr__preview-thumb',
thumbContainerShown: 'plyr__preview-thumb--is-shown',
imageContainer: 'plyr__preview-thumb__image-container',
timeContainer: 'plyr__preview-thumb__time-container',
// Scrubbing
scrubbingContainer: 'plyr__preview-scrubbing',
scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',
},
},
// Embed attributes
attributes: {
embed: {
provider: 'data-plyr-provider',
id: 'data-plyr-embed-id',
},
},
// Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
enabled: false,
publisherId: '',
tagUrl: '',
},
// Preview Thumbnails plugin
previewThumbnails: {
enabled: false,
src: '',
},
// Vimeo plugin
vimeo: { vimeo: {
byline: false, sdk: 'https://player.vimeo.com/api/player.js',
portrait: false, iframe: 'https://player.vimeo.com/video/{0}?{1}',
title: false, api: 'https://vimeo.com/api/v2/video/{0}.json',
speed: true,
transparent: false,
// These settings require a pro or premium account to work
sidedock: false,
controls: false,
// Custom settings from Plyr
referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
}, },
// YouTube plugin
youtube: { youtube: {
noCookie: false, // Whether to use an alternative version of YouTube without cookies sdk: 'https://www.youtube.com/iframe_api',
rel: 0, // No related vids api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
showinfo: 0, // Hide info
iv_load_policy: 3, // Hide annotations
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
}, },
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
},
// Custom control listeners
listeners: {
seek: null,
play: null,
pause: null,
restart: null,
rewind: null,
fastForward: null,
mute: null,
volume: null,
captions: null,
download: null,
fullscreen: null,
pip: null,
airplay: null,
speed: null,
quality: null,
loop: null,
language: null,
},
// Events to watch and bubble
events: [
// Events to watch on HTML5 media elements and bubble
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
'ended',
'progress',
'stalled',
'playing',
'waiting',
'canplay',
'canplaythrough',
'loadstart',
'loadeddata',
'loadedmetadata',
'timeupdate',
'volumechange',
'play',
'pause',
'error',
'seeking',
'seeked',
'emptied',
'ratechange',
'cuechange',
// Custom events
'download',
'enterfullscreen',
'exitfullscreen',
'captionsenabled',
'captionsdisabled',
'languagechange',
'controlshidden',
'controlsshown',
'ready',
// YouTube
'statechange',
// Quality
'qualitychange',
// Ads
'adsloaded',
'adscontentpause',
'adscontentresume',
'adstarted',
'adsmidpoint',
'adscomplete',
'adsallcomplete',
'adsimpression',
'adsclick',
],
// Selectors
// Change these to match your template if using custom HTML
selectors: {
editable: 'input, textarea, select, [contenteditable]',
container: '.plyr',
controls: {
container: null,
wrapper: '.plyr__controls',
},
labels: '[data-plyr]',
buttons: {
play: '[data-plyr="play"]',
pause: '[data-plyr="pause"]',
restart: '[data-plyr="restart"]',
rewind: '[data-plyr="rewind"]',
fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]',
download: '[data-plyr="download"]',
fullscreen: '[data-plyr="fullscreen"]',
pip: '[data-plyr="pip"]',
airplay: '[data-plyr="airplay"]',
settings: '[data-plyr="settings"]',
loop: '[data-plyr="loop"]',
},
inputs: {
seek: '[data-plyr="seek"]',
volume: '[data-plyr="volume"]',
speed: '[data-plyr="speed"]',
language: '[data-plyr="language"]',
quality: '[data-plyr="quality"]',
},
display: {
currentTime: '.plyr__time--current',
duration: '.plyr__time--duration',
buffer: '.plyr__progress__buffer',
loop: '.plyr__progress__loop', // Used later
volume: '.plyr__volume--display',
},
progress: '.plyr__progress',
captions: '.plyr__captions',
caption: '.plyr__caption',
},
// Class hooks added to the player in different states
classNames: {
type: 'plyr--{0}',
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',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
hover: 'plyr--hover',
tooltip: 'plyr__tooltip',
cues: 'plyr__cues',
hidden: 'plyr__sr-only',
hideControls: 'plyr--hide-controls',
isIos: 'plyr--is-ios',
isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
display: {
time: 'plyr__time',
},
menu: {
value: 'plyr__menu__value',
badge: 'plyr__badge',
open: 'plyr--menu-open',
},
captions: {
enabled: 'plyr--captions-enabled',
active: 'plyr--captions-active',
},
fullscreen: {
enabled: 'plyr--fullscreen-enabled',
fallback: 'plyr--fullscreen-fallback',
},
pip: {
supported: 'plyr--pip-supported',
active: 'plyr--pip-active',
},
airplay: {
supported: 'plyr--airplay-supported',
active: 'plyr--airplay-active',
},
tabFocus: 'plyr__tab-focus',
previewThumbnails: {
// Tooltip thumbs
thumbContainer: 'plyr__preview-thumb',
thumbContainerShown: 'plyr__preview-thumb--is-shown',
imageContainer: 'plyr__preview-thumb__image-container',
timeContainer: 'plyr__preview-thumb__time-container',
// Scrubbing
scrubbingContainer: 'plyr__preview-scrubbing',
scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',
},
},
// Embed attributes
attributes: {
embed: {
provider: 'data-plyr-provider',
id: 'data-plyr-embed-id',
},
},
// Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
enabled: false,
publisherId: '',
tagUrl: '',
},
// Preview Thumbnails plugin
previewThumbnails: {
enabled: false,
src: '',
},
// Vimeo plugin
vimeo: {
byline: false,
portrait: false,
title: false,
speed: true,
transparent: false,
// Whether the owner of the video has a Pro or Business account
// (which allows us to properly hide controls without CSS hacks, etc)
premium: false,
// Custom settings from Plyr
referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
},
// YouTube plugin
youtube: {
noCookie: true, // Whether to use an alternative version of YouTube without cookies
rel: 0, // No related vids
showinfo: 0, // Hide info
iv_load_policy: 3, // Hide annotations
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
},
}; };
export default defaults; export default defaults;
+2 -2
View File
@@ -3,8 +3,8 @@
// ========================================================================== // ==========================================================================
export const pip = { export const pip = {
active: 'picture-in-picture', active: 'picture-in-picture',
inactive: 'inline', inactive: 'inline',
}; };
export default { pip }; export default { pip };
+14 -14
View File
@@ -3,14 +3,14 @@
// ========================================================================== // ==========================================================================
export const providers = { export const providers = {
html5: 'html5', html5: 'html5',
youtube: 'youtube', youtube: 'youtube',
vimeo: 'vimeo', vimeo: 'vimeo',
}; };
export const types = { export const types = {
audio: 'audio', audio: 'audio',
video: 'video', video: 'video',
}; };
/** /**
@@ -18,17 +18,17 @@ export const types = {
* @param {String} url * @param {String} url
*/ */
export function getProviderByUrl(url) { export function getProviderByUrl(url) {
// YouTube // YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) { if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube; return providers.youtube;
} }
// Vimeo // Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) { if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo; return providers.vimeo;
} }
return null; return null;
} }
export default { providers, types }; export default { providers, types };
+17 -17
View File
@@ -5,26 +5,26 @@
const noop = () => {}; const noop = () => {};
export default class Console { export default class Console {
constructor(enabled = false) { constructor(enabled = false) {
this.enabled = window.console && enabled; this.enabled = window.console && enabled;
if (this.enabled) { if (this.enabled) {
this.log('Debugging enabled'); this.log('Debugging enabled');
}
} }
}
get log() { get log() {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.log, console) : noop; return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
} }
get warn() { get warn() {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop; return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
} }
get error() { get error() {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.error, console) : noop; return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
} }
} }
+1580 -1584
View File
File diff suppressed because it is too large Load Diff
+263 -258
View File
@@ -5,288 +5,293 @@
// ========================================================================== // ==========================================================================
import browser from './utils/browser'; import browser from './utils/browser';
import { getElements, hasClass, toggleClass } from './utils/elements'; import { closest,getElements, hasClass, toggleClass } from './utils/elements';
import { on, triggerEvent } from './utils/events'; import { on, triggerEvent } from './utils/events';
import is from './utils/is'; import is from './utils/is';
import { silencePromise } from './utils/promise';
class Fullscreen { class Fullscreen {
constructor(player) { constructor(player) {
// Keep reference to parent // Keep reference to parent
this.player = player; this.player = player;
// Get prefix // Get prefix
this.prefix = Fullscreen.prefix; this.prefix = Fullscreen.prefix;
this.property = Fullscreen.property; this.property = Fullscreen.property;
// Scroll position // Scroll position
this.scrollPosition = { x: 0, y: 0 }; this.scrollPosition = { x: 0, y: 0 };
// Force the use of 'full window/browser' rather than fullscreen // Force the use of 'full window/browser' rather than fullscreen
this.forceFallback = player.config.fullscreen.fallback === 'force'; this.forceFallback = player.config.fullscreen.fallback === 'force';
// Register event listeners // Get the fullscreen element
// Handle event (incase user presses escape etc) // Checks container is an ancestor, defaults to null
on.call( this.player.elements.fullscreen =
this.player, player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);
document,
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
// TODO: Filter for target??
this.onChange();
},
);
// Fullscreen toggle on double click // Register event listeners
on.call(this.player, this.player.elements.container, 'dblclick', event => { // Handle event (incase user presses escape etc)
// Ignore double click in controls on.call(
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) { this.player,
return; document,
} this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
this.toggle(); // TODO: Filter for target??
});
// Tap focus when in fullscreen
on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));
// Update the UI
this.update();
}
// Determine if native supported
static get native() {
return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
}
// If we're actually using native
get usingNative() {
return Fullscreen.native && !this.forceFallback;
}
// Get the prefix for handlers
static get prefix() {
// No prefix
if (is.function(document.exitFullscreen)) {
return '';
}
// Check for fullscreen support by vendor prefix
let value = '';
const prefixes = ['webkit', 'moz', 'ms'];
prefixes.some(pre => {
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
value = pre;
return true;
}
return false;
});
return value;
}
static get property() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
}
// Determine if fullscreen is enabled
get enabled() {
return (
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
this.player.config.fullscreen.enabled &&
this.player.supported.ui &&
this.player.isVideo
);
}
// Get active state
get active() {
if (!this.enabled) {
return false;
}
// Fallback using classname
if (!Fullscreen.native || this.forceFallback) {
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
return element === this.target;
}
// Get target element
get target() {
return browser.isIos && this.player.config.fullscreen.iosNative
? this.player.media
: this.player.elements.container;
}
onChange() {
if (!this.enabled) {
return;
}
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
button.pressed = this.active;
}
// Trigger an event
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
}
toggleFallback(toggle = false) {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
}
// Toggle scroll
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Force full viewport on iPhone X+
if (browser.isIos) {
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter(part => part.trim() !== property)
.join(',');
}
}
// Toggle button and fire events
this.onChange(); this.onChange();
},
);
// Fullscreen toggle on double click
on.call(this.player, this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;
}
this.toggle();
});
// Tap focus when in fullscreen
on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));
// Update the UI
this.update();
}
// Determine if native supported
static get native() {
return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
}
// If we're actually using native
get usingNative() {
return Fullscreen.native && !this.forceFallback;
}
// Get the prefix for handlers
static get prefix() {
// No prefix
if (is.function(document.exitFullscreen)) {
return '';
} }
// Trap focus inside container // Check for fullscreen support by vendor prefix
trapFocus(event) { let value = '';
// Bail if iOS, not active, not the tab key const prefixes = ['webkit', 'moz', 'ms'];
if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element prefixes.some(pre => {
const focused = document.activeElement; if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
const focusable = getElements.call( value = pre;
this.player, return true;
'a[href], button:not(:disabled), input:not(:disabled), [tabindex]', }
);
const [first] = focusable;
const last = focusable[focusable.length - 1];
if (focused === last && !event.shiftKey) { return false;
// Move focus to first element that can be tabbed if Shift isn't used });
first.focus();
event.preventDefault(); return value;
} else if (focused === first && event.shiftKey) { }
// Move focus to last element that can be tabbed if Shift is used
last.focus(); static get property() {
event.preventDefault(); return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
} }
// Determine if fullscreen is enabled
get enabled() {
return (
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
this.player.config.fullscreen.enabled &&
this.player.supported.ui &&
this.player.isVideo
);
}
// Get active state
get active() {
if (!this.enabled) {
return false;
} }
// Update UI // Fallback using classname
update() { if (!Fullscreen.native || this.forceFallback) {
if (this.enabled) { return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
let mode;
if (this.forceFallback) {
mode = 'Fallback (forced)';
} else if (Fullscreen.native) {
mode = 'Native';
} else {
mode = 'Fallback';
}
this.player.debug.log(`${mode} fullscreen enabled`);
} else {
this.player.debug.log('Fullscreen not supported and fallback disabled');
}
// Add styling hook to show button
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
} }
// Make an element fullscreen const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
enter() {
if (!this.enabled) {
return;
}
// iOS native fullscreen doesn't need the request step return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
if (browser.isIos && this.player.config.fullscreen.iosNative) { }
this.target.webkitEnterFullscreen();
} else if (!Fullscreen.native || this.forceFallback) { // Get target element
this.toggleFallback(true); get target() {
} else if (!this.prefix) { return browser.isIos && this.player.config.fullscreen.iosNative
this.target.requestFullscreen({ navigationUI: 'hide' }); ? this.player.media
} else if (!is.empty(this.prefix)) { : this.player.elements.fullscreen || this.player.elements.container;
this.target[`${this.prefix}Request${this.property}`](); }
}
onChange() {
if (!this.enabled) {
return;
} }
// Bail from fullscreen // Update toggle button
exit() { const button = this.player.elements.buttons.fullscreen;
if (!this.enabled) { if (is.element(button)) {
return; button.pressed = this.active;
}
// iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen();
this.player.play();
} else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`]();
}
} }
// Toggle state // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up
toggle() { const target = this.target === this.player.media ? this.target : this.player.elements.container;
if (!this.active) { // Trigger an event
this.enter(); triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
} else { }
this.exit();
} toggleFallback(toggle = false) {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
} }
// Toggle scroll
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Force full viewport on iPhone X+
if (browser.isIos) {
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter(part => part.trim() !== property)
.join(',');
}
}
// Toggle button and fire events
this.onChange();
}
// Trap focus inside container
trapFocus(event) {
// Bail if iOS, not active, not the tab key
if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = document.activeElement;
const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');
const [first] = focusable;
const last = focusable[focusable.length - 1];
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
}
// Update UI
update() {
if (this.enabled) {
let mode;
if (this.forceFallback) {
mode = 'Fallback (forced)';
} else if (Fullscreen.native) {
mode = 'Native';
} else {
mode = 'Fallback';
}
this.player.debug.log(`${mode} fullscreen enabled`);
} else {
this.player.debug.log('Fullscreen not supported and fallback disabled');
}
// Add styling hook to show button
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
}
// Make an element fullscreen
enter() {
if (!this.enabled) {
return;
}
// iOS native fullscreen doesn't need the request step
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitEnterFullscreen();
} else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(true);
} else if (!this.prefix) {
this.target.requestFullscreen({ navigationUI: 'hide' });
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
}
// Bail from fullscreen
exit() {
if (!this.enabled) {
return;
}
// iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen();
silencePromise(this.player.play());
} else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`]();
}
}
// Toggle state
toggle() {
if (!this.active) {
this.enter();
} else {
this.exit();
}
}
} }
export default Fullscreen; export default Fullscreen;
+123 -122
View File
@@ -6,141 +6,142 @@ import support from './support';
import { removeElement } from './utils/elements'; import { removeElement } from './utils/elements';
import { triggerEvent } from './utils/events'; import { triggerEvent } from './utils/events';
import is from './utils/is'; import is from './utils/is';
import { silencePromise } from './utils/promise';
import { setAspectRatio } from './utils/style'; import { setAspectRatio } from './utils/style';
const html5 = { const html5 = {
getSources() { getSources() {
if (!this.isHTML5) { if (!this.isHTML5) {
return []; return [];
}
const sources = Array.from(this.media.querySelectorAll('source'));
// Filter out unsupported sources (if type is specified)
return sources.filter(source => {
const type = source.getAttribute('type');
if (is.empty(type)) {
return true;
}
return support.mime.call(this, type);
});
},
// Get quality levels
getQualityOptions() {
// Whether we're forcing all options (e.g. for streaming)
if (this.config.quality.forced) {
return this.config.quality.options;
}
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map(source => Number(source.getAttribute('size')))
.filter(Boolean);
},
setup() {
if (!this.isHTML5) {
return;
}
const player = this;
// Set speed options from config
player.options.speed = player.config.speed.options;
// Set aspect ratio if fixed
if (!is.empty(this.config.ratio)) {
setAspectRatio.call(player);
}
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
// Get sources
const sources = html5.getSources.call(player);
const source = sources.find(s => s.getAttribute('src') === player.source);
// Return size, if match is found
return source && Number(source.getAttribute('size'));
},
set(input) {
if (player.quality === input) {
return;
} }
const sources = Array.from(this.media.querySelectorAll('source')); // If we're using an an external handler...
if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
player.config.quality.onChange(input);
} else {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find(s => Number(s.getAttribute('size')) === input);
// Filter out unsupported sources (if type is specified) // No matching source found
return sources.filter(source => { if (!source) {
const type = source.getAttribute('type');
if (is.empty(type)) {
return true;
}
return support.mime.call(this, type);
});
},
// Get quality levels
getQualityOptions() {
// Whether we're forcing all options (e.g. for streaming)
if (this.config.quality.forced) {
return this.config.quality.options;
}
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map(source => Number(source.getAttribute('size')))
.filter(Boolean);
},
setup() {
if (!this.isHTML5) {
return; return;
}
// Get current state
const { currentTime, paused, preload, readyState, playbackRate } = player.media;
// Set new source
player.media.src = source.getAttribute('src');
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
player.speed = playbackRate;
player.currentTime = currentTime;
// Resume playing
if (!paused) {
silencePromise(player.play());
}
});
// Load new source
player.media.load();
}
} }
const player = this; // Trigger change event
triggerEvent.call(player, player.media, 'qualitychange', false, {
// Set speed options from config quality: input,
player.options.speed = player.config.speed.options;
// Set aspect ratio if fixed
if (!is.empty(this.config.ratio)) {
setAspectRatio.call(player);
}
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
// Get sources
const sources = html5.getSources.call(player);
const source = sources.find(s => s.getAttribute('src') === player.source);
// Return size, if match is found
return source && Number(source.getAttribute('size'));
},
set(input) {
if (player.quality === input) {
return;
}
// If we're using an an external handler...
if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
player.config.quality.onChange(input);
} else {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find(s => Number(s.getAttribute('size')) === input);
// No matching source found
if (!source) {
return;
}
// Get current state
const { currentTime, paused, preload, readyState, playbackRate } = player.media;
// Set new source
player.media.src = source.getAttribute('src');
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
player.speed = playbackRate;
player.currentTime = currentTime;
// Resume playing
if (!paused) {
player.play();
}
});
// Load new source
player.media.load();
}
}
// Trigger change event
triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input,
});
},
}); });
}, },
});
},
// Cancel current network requests // Cancel current network requests
// See https://github.com/sampotts/plyr/issues/174
cancelRequests() {
if (!this.isHTML5) {
return;
}
// Remove child sources
removeElement(html5.getSources.call(this));
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
this.media.setAttribute('src', this.config.blankVideo);
// Load the new empty source
// This will cancel existing requests
// See https://github.com/sampotts/plyr/issues/174 // See https://github.com/sampotts/plyr/issues/174
cancelRequests() { this.media.load();
if (!this.isHTML5) {
return;
}
// Remove child sources // Debugging
removeElement(html5.getSources.call(this)); this.debug.log('Cancelled network requests');
},
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
this.media.setAttribute('src', this.config.blankVideo);
// Load the new empty source
// This will cancel existing requests
// See https://github.com/sampotts/plyr/issues/174
this.media.load();
// Debugging
this.debug.log('Cancelled network requests');
},
}; };
export default html5; export default html5;
+793 -760
View File
File diff suppressed because it is too large Load Diff
+38 -40
View File
@@ -8,54 +8,52 @@ import youtube from './plugins/youtube';
import { createElement, toggleClass, wrap } from './utils/elements'; import { createElement, toggleClass, wrap } from './utils/elements';
const media = { const media = {
// Setup media // Setup media
setup() { setup() {
// If there's no media, bail // If there's no media, bail
if (!this.media) { if (!this.media) {
this.debug.warn('No media element found!'); this.debug.warn('No media element found!');
return; return;
} }
// Add type class // Add type class
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
// Add provider class // Add provider class
toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true); toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
// Add video class for embeds // Add video class for embeds
// This will require changes if audio embeds are added // This will require changes if audio embeds are added
if (this.isEmbed) { if (this.isEmbed) {
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true); toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
} }
// Inject the player wrapper // Inject the player wrapper
if (this.isVideo) { if (this.isVideo) {
// Create the wrapper div // Create the wrapper div
this.elements.wrapper = createElement('div', { this.elements.wrapper = createElement('div', {
class: this.config.classNames.video, class: this.config.classNames.video,
}); });
// Wrap the video in a container // Wrap the video in a container
wrap(this.media, this.elements.wrapper); wrap(this.media, this.elements.wrapper);
// Faux poster container // Poster image container
if (this.isEmbed) { this.elements.poster = createElement('div', {
this.elements.poster = createElement('div', { class: this.config.classNames.poster,
class: this.config.classNames.poster, });
});
this.elements.wrapper.appendChild(this.elements.poster); this.elements.wrapper.appendChild(this.elements.poster);
} }
}
if (this.isHTML5) { if (this.isHTML5) {
html5.setup.call(this); html5.setup.call(this);
} else if (this.isYouTube) { } else if (this.isYouTube) {
youtube.setup.call(this); youtube.setup.call(this);
} else if (this.isVimeo) { } else if (this.isVimeo) {
vimeo.setup.call(this); vimeo.setup.call(this);
} }
}, },
}; };
export default media; export default media;
+571 -563
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+334 -322
View File
@@ -10,393 +10,405 @@ import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch'; import fetch from '../utils/fetch';
import is from '../utils/is'; import is from '../utils/is';
import loadScript from '../utils/load-script'; import loadScript from '../utils/load-script';
import { extend } from '../utils/objects';
import { format, stripHTML } from '../utils/strings'; import { format, stripHTML } from '../utils/strings';
import { setAspectRatio } from '../utils/style'; import { setAspectRatio } from '../utils/style';
import { buildUrlParams } from '../utils/urls'; import { buildUrlParams } from '../utils/urls';
// Parse Vimeo ID from URL // Parse Vimeo ID from URL
function parseId(url) { function parseId(url) {
if (is.empty(url)) { if (is.empty(url)) {
return null; return null;
} }
if (is.number(Number(url))) { if (is.number(Number(url))) {
return url; return url;
} }
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/; const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url; return url.match(regex) ? RegExp.$2 : url;
} }
// Set playback state and trigger change (only on actual change) // Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) { function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) { if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true; this.embed.hasPlayed = true;
} }
if (this.media.paused === play) { if (this.media.paused === play) {
this.media.paused = !play; this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause'); triggerEvent.call(this, this.media, play ? 'play' : 'pause');
} }
} }
const vimeo = { const vimeo = {
setup() { setup() {
const player = this; const player = this;
// Add embed class for responsive // Add embed class for responsive
toggleClass(player.elements.wrapper, player.config.classNames.embed, true); toggleClass(player.elements.wrapper, player.config.classNames.embed, true);
// Set speed options from config // Set speed options from config
player.options.speed = player.config.speed.options; player.options.speed = player.config.speed.options;
// Set intial ratio // Set intial ratio
setAspectRatio.call(player); setAspectRatio.call(player);
// Load the SDK if not already // Load the SDK if not already
if (!is.object(window.Vimeo)) { if (!is.object(window.Vimeo)) {
loadScript(player.config.urls.vimeo.sdk) loadScript(player.config.urls.vimeo.sdk)
.then(() => { .then(() => {
vimeo.ready.call(player); vimeo.ready.call(player);
}) })
.catch(error => { .catch(error => {
player.debug.warn('Vimeo SDK (player.js) failed to load', error); player.debug.warn('Vimeo SDK (player.js) failed to load', error);
});
} else {
vimeo.ready.call(player);
}
},
// API Ready
ready() {
const player = this;
const config = player.config.vimeo;
// Get Vimeo params for the iframe
const params = buildUrlParams(
extend(
{},
{
loop: player.config.loop.active,
autoplay: player.autoplay,
muted: player.muted,
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
},
config,
),
);
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id);
}
const id = parseId(source);
// Build an iframe
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
// Set the referrer policy if required
if (!is.empty(config.referrerPolicy)) {
iframe.setAttribute('referrerPolicy', config.referrerPolicy);
}
// Get poster, if already set
const { poster } = player;
// Inject the package
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
// Get poster image
fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (is.empty(response)) {
return;
}
// Get the URL for thumbnail
const url = new URL(response[0].thumbnail_large);
// Get original image
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set and show poster
ui.setPoster.call(player, url.href).catch(() => {});
}); });
} else {
vimeo.ready.call(player);
}
},
// Setup instance // API Ready
// https://github.com/vimeo/player.js ready() {
player.embed = new window.Vimeo.Player(iframe, { const player = this;
autopause: player.config.autopause, const config = player.config.vimeo;
muted: player.muted, const { premium, referrerPolicy, ...frameParams } = config;
});
player.media.paused = true; // If the owner has a pro or premium account then we can hide controls etc
player.media.currentTime = 0; if (premium) {
Object.assign(frameParams, {
controls: false,
sidedock: false,
});
}
// Disable native text track rendering // Get Vimeo params for the iframe
if (player.supported.ui) { const params = buildUrlParams({
player.embed.disableTextTrack(); loop: player.config.loop.active,
} autoplay: player.autoplay,
muted: player.muted,
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
...frameParams,
});
// Create a faux HTML5 API using the Vimeo API // Get the source URL or ID
player.media.play = () => { let source = player.media.getAttribute('src');
assurePlaybackState.call(player, true);
return player.embed.play();
};
player.media.pause = () => { // Get from <div> if needed
assurePlaybackState.call(player, false); if (is.empty(source)) {
return player.embed.pause(); source = player.media.getAttribute(player.config.attributes.embed.id);
}; }
player.media.stop = () => { const id = parseId(source);
player.pause(); // Build an iframe
player.currentTime = 0; const iframe = createElement('iframe');
}; const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allow', 'autoplay,fullscreen,picture-in-picture');
// Seeking // Set the referrer policy if required
let { currentTime } = player.media; if (!is.empty(referrerPolicy)) {
Object.defineProperty(player.media, 'currentTime', { iframe.setAttribute('referrerPolicy', referrerPolicy);
get() { }
return currentTime;
},
set(time) {
// Vimeo will automatically play on seek if the video hasn't been played before
// Get current paused state and volume etc // Inject the package
const { embed, media, paused, volume } = player; const { poster } = player;
const restorePause = paused && !embed.hasPlayed; if (premium) {
iframe.setAttribute('data-poster', poster);
player.media = replaceElement(iframe, player.media);
} else {
const wrapper = createElement('div', { class: player.config.classNames.embedContainer, 'data-poster': poster });
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
}
// Set seeking state and trigger event // Get poster image
media.seeking = true; fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
triggerEvent.call(player, media, 'seeking'); if (is.empty(response)) {
return;
}
// If paused, mute until seek is complete // Get the URL for thumbnail
Promise.resolve(restorePause && embed.setVolume(0)) const url = new URL(response[0].thumbnail_large);
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
.then(() => restorePause && embed.pause())
// Restore volume
.then(() => restorePause && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
},
});
// Playback speed // Get original image
let speed = player.config.speed.selected; url.pathname = `${url.pathname.split('_')[0]}.jpg`;
Object.defineProperty(player.media, 'playbackRate', {
get() {
return speed;
},
set(input) {
player.embed.setPlaybackRate(input).then(() => {
speed = input;
triggerEvent.call(player, player.media, 'ratechange');
});
},
});
// Volume // Set and show poster
let { volume } = player.config; ui.setPoster.call(player, url.href).catch(() => {});
Object.defineProperty(player.media, 'volume', { });
get() {
return volume;
},
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Muted // Setup instance
let { muted } = player.config; // https://github.com/vimeo/player.js
Object.defineProperty(player.media, 'muted', { player.embed = new window.Vimeo.Player(iframe, {
get() { autopause: player.config.autopause,
return muted; muted: player.muted,
}, });
set(input) {
const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => { player.media.paused = true;
muted = toggle; player.media.currentTime = 0;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Loop // Disable native text track rendering
let { loop } = player.config; if (player.supported.ui) {
Object.defineProperty(player.media, 'loop', { player.embed.disableTextTrack();
get() { }
return loop;
},
set(input) {
const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => { // Create a faux HTML5 API using the Vimeo API
loop = toggle; player.media.play = () => {
}); assurePlaybackState.call(player, true);
}, return player.embed.play();
}); };
// Source player.media.pause = () => {
let currentSrc; assurePlaybackState.call(player, false);
return player.embed.pause();
};
player.media.stop = () => {
player.pause();
player.currentTime = 0;
};
// Seeking
let { currentTime } = player.media;
Object.defineProperty(player.media, 'currentTime', {
get() {
return currentTime;
},
set(time) {
// Vimeo will automatically play on seek if the video hasn't been played before
// Get current paused state and volume etc
const { embed, media, paused, volume } = player;
const restorePause = paused && !embed.hasPlayed;
// Set seeking state and trigger event
media.seeking = true;
triggerEvent.call(player, media, 'seeking');
// If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0))
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
.then(() => restorePause && embed.pause())
// Restore volume
.then(() => restorePause && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
},
});
// Playback speed
let speed = player.config.speed.selected;
Object.defineProperty(player.media, 'playbackRate', {
get() {
return speed;
},
set(input) {
player.embed player.embed
.getVideoUrl() .setPlaybackRate(input)
.then(value => { .then(() => {
currentSrc = value; speed = input;
controls.setDownloadUrl.call(player); triggerEvent.call(player, player.media, 'ratechange');
}) })
.catch(error => { .catch(() => {
this.debug.warn(error); // Cannot set Playback Rate, Video is probably not on Pro account
}); player.options.speed = [1];
});
},
});
Object.defineProperty(player.media, 'currentSrc', { // Volume
get() { let { volume } = player.config;
return currentSrc; Object.defineProperty(player.media, 'volume', {
}, get() {
return volume;
},
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
triggerEvent.call(player, player.media, 'volumechange');
}); });
},
});
// Ended // Muted
Object.defineProperty(player.media, 'ended', { let { muted } = player.config;
get() { Object.defineProperty(player.media, 'muted', {
return player.currentTime === player.duration; get() {
}, return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle;
triggerEvent.call(player, player.media, 'volumechange');
}); });
},
});
// Set aspect ratio based on video size // Loop
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { let { loop } = player.config;
const [width, height] = dimensions; Object.defineProperty(player.media, 'loop', {
player.embed.ratio = [width, height]; get() {
setAspectRatio.call(this); return loop;
},
set(input) {
const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => {
loop = toggle;
}); });
},
});
// Set autopause // Source
player.embed.setAutopause(player.config.autopause).then(state => { let currentSrc;
player.config.autopause = state; player.embed
}); .getVideoUrl()
.then(value => {
currentSrc = value;
controls.setDownloadUrl.call(player);
})
.catch(error => {
this.debug.warn(error);
});
// Get title Object.defineProperty(player.media, 'currentSrc', {
player.embed.getVideoTitle().then(title => { get() {
player.config.title = title; return currentSrc;
ui.setTitle.call(this); },
}); });
// Get current time // Ended
player.embed.getCurrentTime().then(value => { Object.defineProperty(player.media, 'ended', {
currentTime = value; get() {
triggerEvent.call(player, player.media, 'timeupdate'); return player.currentTime === player.duration;
}); },
});
// Get duration // Set aspect ratio based on video size
player.embed.getDuration().then(value => { Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
player.media.duration = value; const [width, height] = dimensions;
triggerEvent.call(player, player.media, 'durationchange'); player.embed.ratio = [width, height];
}); setAspectRatio.call(this);
});
// Get captions // Set autopause
player.embed.getTextTracks().then(tracks => { player.embed.setAutopause(player.config.autopause).then(state => {
player.media.textTracks = tracks; player.config.autopause = state;
captions.setup.call(player); });
});
player.embed.on('cuechange', ({ cues = [] }) => { // Get title
const strippedCues = cues.map(cue => stripHTML(cue.text)); player.embed.getVideoTitle().then(title => {
captions.updateCues.call(player, strippedCues); player.config.title = title;
}); ui.setTitle.call(this);
});
player.embed.on('loaded', () => { // Get current time
// Assure state and events are updated on autoplay player.embed.getCurrentTime().then(value => {
player.embed.getPaused().then(paused => { currentTime = value;
assurePlaybackState.call(player, !paused); triggerEvent.call(player, player.media, 'timeupdate');
if (!paused) { });
triggerEvent.call(player, player.media, 'playing');
}
});
if (is.element(player.embed.element) && player.supported.ui) { // Get duration
const frame = player.embed.element; player.embed.getDuration().then(value => {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
});
// Fix keyboard focus issues // Get captions
// https://github.com/sampotts/plyr/issues/317 player.embed.getTextTracks().then(tracks => {
frame.setAttribute('tabindex', -1); player.media.textTracks = tracks;
} captions.setup.call(player);
}); });
player.embed.on('bufferstart', () => { player.embed.on('cuechange', ({ cues = [] }) => {
triggerEvent.call(player, player.media, 'waiting'); const strippedCues = cues.map(cue => stripHTML(cue.text));
}); captions.updateCues.call(player, strippedCues);
});
player.embed.on('bufferend', () => { player.embed.on('loaded', () => {
triggerEvent.call(player, player.media, 'playing'); // Assure state and events are updated on autoplay
}); player.embed.getPaused().then(paused => {
assurePlaybackState.call(player, !paused);
if (!paused) {
triggerEvent.call(player, player.media, 'playing');
}
});
player.embed.on('play', () => { if (is.element(player.embed.element) && player.supported.ui) {
assurePlaybackState.call(player, true); const frame = player.embed.element;
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('pause', () => { // Fix keyboard focus issues
assurePlaybackState.call(player, false); // https://github.com/sampotts/plyr/issues/317
}); frame.setAttribute('tabindex', -1);
}
});
player.embed.on('timeupdate', data => { player.embed.on('bufferstart', () => {
player.media.seeking = false; triggerEvent.call(player, player.media, 'waiting');
currentTime = data.seconds; });
triggerEvent.call(player, player.media, 'timeupdate');
});
player.embed.on('progress', data => { player.embed.on('bufferend', () => {
player.media.buffered = data.percent; triggerEvent.call(player, player.media, 'playing');
triggerEvent.call(player, player.media, 'progress'); });
// Check all loaded player.embed.on('play', () => {
if (parseInt(data.percent, 10) === 1) { assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'canplaythrough'); triggerEvent.call(player, player.media, 'playing');
} });
// Get duration as if we do it before load, it gives an incorrect value player.embed.on('pause', () => {
// https://github.com/sampotts/plyr/issues/891 assurePlaybackState.call(player, false);
player.embed.getDuration().then(value => { });
if (value !== player.media.duration) {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => { player.embed.on('timeupdate', data => {
player.media.seeking = false; player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked'); currentTime = data.seconds;
}); triggerEvent.call(player, player.media, 'timeupdate');
});
player.embed.on('ended', () => { player.embed.on('progress', data => {
player.media.paused = true; player.media.buffered = data.percent;
triggerEvent.call(player, player.media, 'ended'); triggerEvent.call(player, player.media, 'progress');
});
player.embed.on('error', detail => { // Check all loaded
player.media.error = detail; if (parseInt(data.percent, 10) === 1) {
triggerEvent.call(player, player.media, 'error'); triggerEvent.call(player, player.media, 'canplaythrough');
}); }
// Rebuild UI // Get duration as if we do it before load, it gives an incorrect value
setTimeout(() => ui.build.call(player), 0); // https://github.com/sampotts/plyr/issues/891
}, player.embed.getDuration().then(value => {
if (value !== player.media.duration) {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => {
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
});
player.embed.on('ended', () => {
player.media.paused = true;
triggerEvent.call(player, player.media, 'ended');
});
player.embed.on('error', detail => {
player.media.error = detail;
triggerEvent.call(player, player.media, 'error');
});
// Rebuild UI
setTimeout(() => ui.build.call(player), 0);
},
}; };
export default vimeo; export default vimeo;
+390 -390
View File
@@ -15,426 +15,426 @@ import { setAspectRatio } from '../utils/style';
// Parse YouTube ID from URL // Parse YouTube ID from URL
function parseId(url) { function parseId(url) {
if (is.empty(url)) { if (is.empty(url)) {
return null; return null;
} }
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url; return url.match(regex) ? RegExp.$2 : url;
} }
// Set playback state and trigger change (only on actual change) // Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) { function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) { if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true; this.embed.hasPlayed = true;
} }
if (this.media.paused === play) { if (this.media.paused === play) {
this.media.paused = !play; this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause'); triggerEvent.call(this, this.media, play ? 'play' : 'pause');
} }
} }
function getHost(config) { function getHost(config) {
if (config.noCookie) { if (config.noCookie) {
return 'https://www.youtube-nocookie.com'; return 'https://www.youtube-nocookie.com';
} }
if (window.location.protocol === 'http:') { if (window.location.protocol === 'http:') {
return 'http://www.youtube.com'; return 'http://www.youtube.com';
} }
// Use YouTube's default // Use YouTube's default
return undefined; return undefined;
} }
const youtube = { const youtube = {
setup() { setup() {
// Add embed class for responsive // Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true); toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Setup API // Setup API
if (is.object(window.YT) && is.function(window.YT.Player)) { if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this); youtube.ready.call(this);
} else { } else {
// Reference current global callback // Reference current global callback
const callback = window.onYouTubeIframeAPIReady; const callback = window.onYouTubeIframeAPIReady;
// Set callback to process queue // Set callback to process queue
window.onYouTubeIframeAPIReady = () => { window.onYouTubeIframeAPIReady = () => {
// Call global callback if set // Call global callback if set
if (is.function(callback)) { if (is.function(callback)) {
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 youtube.ready.call(this);
getTitle(videoId) { };
const url = format(this.config.urls.youtube.api, videoId);
fetch(url) // Load the SDK
.then(data => { loadScript(this.config.urls.youtube.sdk).catch(error => {
if (is.object(data)) { this.debug.warn('YouTube API failed to load', error);
const { title, height, width } = data; });
}
},
// Set title // Get the media title
this.config.title = title; getTitle(videoId) {
ui.setTitle.call(this); const url = format(this.config.urls.youtube.api, videoId);
// Set aspect ratio fetch(url)
this.embed.ratio = [width, height]; .then(data => {
} if (is.object(data)) {
const { title, height, width } = data;
setAspectRatio.call(this); // Set title
}) this.config.title = title;
.catch(() => { ui.setTitle.call(this);
// Set aspect ratio
setAspectRatio.call(this);
});
},
// API ready // Set aspect ratio
ready() { this.embed.ratio = [width, height];
const player = this; }
// Ignore already setup (race condition)
const currentId = player.media && player.media.getAttribute('id'); setAspectRatio.call(this);
if (!is.empty(currentId) && currentId.startsWith('youtube-')) { })
.catch(() => {
// Set aspect ratio
setAspectRatio.call(this);
});
},
// API ready
ready() {
const player = this;
// Ignore already setup (race condition)
const currentId = player.media && player.media.getAttribute('id');
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// 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, 'data-poster': poster });
player.media = replaceElement(container, player.media);
// Id to poster wrapper
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(src => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!src.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
})
.catch(() => {});
const config = player.config.youtube;
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(id, {
videoId,
host: getHost(config),
playerVars: extend(
{},
{
autoplay: player.config.autoplay ? 1 : 0, // Autoplay
hl: player.config.hl, // iframe interface language
controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
disablekb: 1, // Disable keyboard as we handle it
playsinline: !player.config.fullscreen.iosNative ? 1 : 0, // Allow iOS inline playback
// Captions are flaky on YouTube
cc_load_policy: player.captions.active ? 1 : 0,
cc_lang_pref: player.config.captions.language,
// Tracking for stats
widget_referrer: window ? window.location.href : null,
},
config,
),
events: {
onError(event) {
// YouTube may fire onError twice, so only handle it once
if (!player.media.error) {
const code = event.data;
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occured';
player.media.error = { code, message };
triggerEvent.call(player, player.media, 'error');
}
},
onPlaybackRateChange(event) {
// Get the instance
const instance = event.target;
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Bail if onReady has already been called. See issue #1108
if (is.function(player.media.play)) {
return; return;
} }
// Get the instance
const instance = event.target;
// Get the source URL or ID // Get the title
let source = player.media.getAttribute('src'); youtube.getTitle.call(player, videoId);
// Get from <div> if needed // Create a faux HTML5 API using the YouTube API
if (is.empty(source)) { player.media.play = () => {
source = player.media.getAttribute(this.config.attributes.embed.id); assurePlaybackState.call(player, true);
} instance.playVideo();
};
// Replace the <iframe> with a <div> due to YouTube API issues player.media.pause = () => {
const videoId = parseId(source); assurePlaybackState.call(player, false);
const id = generateId(player.provider); instance.pauseVideo();
// 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 player.media.stop = () => {
const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`; instance.stopVideo();
};
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) player.media.duration = instance.getDuration();
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded player.media.paused = true;
.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(src => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!src.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
})
.catch(() => {});
const config = player.config.youtube; // Seeking
player.media.currentTime = 0;
// Setup instance Object.defineProperty(player.media, 'currentTime', {
// https://developers.google.com/youtube/iframe_api_reference get() {
player.embed = new window.YT.Player(id, { return Number(instance.getCurrentTime());
videoId,
host: getHost(config),
playerVars: extend(
{},
{
autoplay: player.config.autoplay ? 1 : 0, // Autoplay
hl: player.config.hl, // iframe interface language
controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
disablekb: 1, // Disable keyboard as we handle it
playsinline: !player.config.fullscreen.iosNative ? 1 : 0, // Allow iOS inline playback
// Captions are flaky on YouTube
cc_load_policy: player.captions.active ? 1 : 0,
cc_lang_pref: player.config.captions.language,
// Tracking for stats
widget_referrer: window ? window.location.href : null,
},
config,
),
events: {
onError(event) {
// YouTube may fire onError twice, so only handle it once
if (!player.media.error) {
const code = event.data;
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occured';
player.media.error = { code, message };
triggerEvent.call(player, player.media, 'error');
}
},
onPlaybackRateChange(event) {
// Get the instance
const instance = event.target;
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Bail if onReady has already been called. See issue #1108
if (is.function(player.media.play)) {
return;
}
// Get the instance
const instance = event.target;
// Get the title
youtube.getTitle.call(player, videoId);
// Create a faux HTML5 API using the YouTube API
player.media.play = () => {
assurePlaybackState.call(player, true);
instance.playVideo();
};
player.media.pause = () => {
assurePlaybackState.call(player, false);
instance.pauseVideo();
};
player.media.stop = () => {
instance.stopVideo();
};
player.media.duration = instance.getDuration();
player.media.paused = true;
// Seeking
player.media.currentTime = 0;
Object.defineProperty(player.media, 'currentTime', {
get() {
return Number(instance.getCurrentTime());
},
set(time) {
// If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused && !player.embed.hasPlayed) {
player.embed.mute();
}
// Set seeking state and trigger event
player.media.seeking = true;
triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
},
});
// Playback speed
Object.defineProperty(player.media, 'playbackRate', {
get() {
return instance.getPlaybackRate();
},
set(input) {
instance.setPlaybackRate(input);
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
volume = input;
instance.setVolume(volume * 100);
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Source
Object.defineProperty(player.media, 'currentSrc', {
get() {
return instance.getVideoUrl();
},
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
});
// Get available speeds
const speeds = instance.getAvailablePlaybackRates();
// Filter based on config
player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));
// Set the tabindex to avoid focus entering iframe
if (player.supported.ui) {
player.media.setAttribute('tabindex', -1);
}
triggerEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
// Setup buffering
player.timers.buffering = setInterval(() => {
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
// Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
player.media.lastBuffered = player.media.buffered;
// Bail if we're at 100%
if (player.media.buffered === 1) {
clearInterval(player.timers.buffering);
// Trigger event
triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
// Rebuild UI
setTimeout(() => ui.build.call(player), 50);
},
onStateChange(event) {
// Get the instance
const instance = event.target;
// Reset timer
clearInterval(player.timers.playing);
const seeked = player.media.seeking && [1, 2].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
// -1 Unstarted
// 0 Ended
// 1 Playing
// 2 Paused
// 3 Buffering
// 5 Video cued
switch (event.data) {
case -1:
// Update scrubber
triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
triggerEvent.call(player, player.media, 'progress');
break;
case 0:
assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
// YouTube needs a call to `stopVideo` before playing again
instance.stopVideo();
instance.playVideo();
} else {
triggerEvent.call(player, player.media, 'ended');
}
break;
case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
// https://github.com/sampotts/plyr/issues/374
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
triggerEvent.call(player, player.media, 'durationchange');
}
}
break;
case 2:
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.muted) {
player.embed.unMute();
}
assurePlaybackState.call(player, false);
break;
case 3:
// Trigger waiting event to add loading classes to container as the video buffers.
triggerEvent.call(player, player.media, 'waiting');
break;
default:
break;
}
triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},
}, },
}); set(time) {
}, // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused && !player.embed.hasPlayed) {
player.embed.mute();
}
// Set seeking state and trigger event
player.media.seeking = true;
triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
},
});
// Playback speed
Object.defineProperty(player.media, 'playbackRate', {
get() {
return instance.getPlaybackRate();
},
set(input) {
instance.setPlaybackRate(input);
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
volume = input;
instance.setVolume(volume * 100);
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Source
Object.defineProperty(player.media, 'currentSrc', {
get() {
return instance.getVideoUrl();
},
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
});
// Get available speeds
const speeds = instance.getAvailablePlaybackRates();
// Filter based on config
player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));
// Set the tabindex to avoid focus entering iframe
if (player.supported.ui) {
player.media.setAttribute('tabindex', -1);
}
triggerEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
// Setup buffering
player.timers.buffering = setInterval(() => {
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
// Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
player.media.lastBuffered = player.media.buffered;
// Bail if we're at 100%
if (player.media.buffered === 1) {
clearInterval(player.timers.buffering);
// Trigger event
triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
// Rebuild UI
setTimeout(() => ui.build.call(player), 50);
},
onStateChange(event) {
// Get the instance
const instance = event.target;
// Reset timer
clearInterval(player.timers.playing);
const seeked = player.media.seeking && [1, 2].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
// -1 Unstarted
// 0 Ended
// 1 Playing
// 2 Paused
// 3 Buffering
// 5 Video cued
switch (event.data) {
case -1:
// Update scrubber
triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
triggerEvent.call(player, player.media, 'progress');
break;
case 0:
assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
// YouTube needs a call to `stopVideo` before playing again
instance.stopVideo();
instance.playVideo();
} else {
triggerEvent.call(player, player.media, 'ended');
}
break;
case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
// https://github.com/sampotts/plyr/issues/374
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
triggerEvent.call(player, player.media, 'durationchange');
}
}
break;
case 2:
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.muted) {
player.embed.unMute();
}
assurePlaybackState.call(player, false);
break;
case 3:
// Trigger waiting event to add loading classes to container as the video buffers.
triggerEvent.call(player, player.media, 'waiting');
break;
default:
break;
}
triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},
},
});
},
}; };
export default youtube; export default youtube;
+500 -489
View File
File diff suppressed because it is too large Load Diff
+1068 -1058
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
// ========================================================================== // ==========================================================================
// Plyr Polyfilled Build // Plyr Polyfilled Build
// plyr.js v3.5.10 // plyr.js v3.6.1
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
+122 -122
View File
@@ -13,146 +13,146 @@ import is from './utils/is';
import { getDeep } from './utils/objects'; import { getDeep } from './utils/objects';
const source = { const source = {
// Add elements to HTML5 media (source, tracks, etc) // Add elements to HTML5 media (source, tracks, etc)
insertElements(type, attributes) { insertElements(type, attributes) {
if (is.string(attributes)) { if (is.string(attributes)) {
insertElement(type, this.media, { insertElement(type, this.media, {
src: attributes, src: attributes,
}); });
} else if (is.array(attributes)) { } else if (is.array(attributes)) {
attributes.forEach(attribute => { attributes.forEach(attribute => {
insertElement(type, this.media, attribute); insertElement(type, this.media, attribute);
}); });
} }
}, },
// Update source // Update source
// Sources are not checked for support so be careful // Sources are not checked for support so be careful
change(input) { change(input) {
if (!getDeep(input, 'sources.length')) { if (!getDeep(input, 'sources.length')) {
this.debug.warn('Invalid source format'); this.debug.warn('Invalid source format');
return; return;
}
// Cancel current network requests
html5.cancelRequests.call(this);
// Destroy instance and re-setup
this.destroy.call(
this,
() => {
// Reset quality options
this.options.quality = [];
// Remove elements
removeElement(this.media);
this.media = null;
// Reset class name
if (is.element(this.elements.container)) {
this.elements.container.removeAttribute('class');
} }
// Cancel current network requests // Set the type and provider
html5.cancelRequests.call(this); const { sources, type } = input;
const [{ provider = providers.html5, src }] = sources;
const tagName = provider === 'html5' ? type : 'div';
const attributes = provider === 'html5' ? {} : { src };
// Destroy instance and re-setup Object.assign(this, {
this.destroy.call( provider,
this, type,
() => { // Check for support
// Reset quality options supported: support.check(type, provider, this.config.playsinline),
this.options.quality = []; // Create new element
media: createElement(tagName, attributes),
});
// Remove elements // Inject the new element
removeElement(this.media); this.elements.container.appendChild(this.media);
this.media = null;
// Reset class name // Autoplay the new source?
if (is.element(this.elements.container)) { if (is.boolean(input.autoplay)) {
this.elements.container.removeAttribute('class'); this.config.autoplay = input.autoplay;
} }
// Set the type and provider // Set attributes for audio and video
const { sources, type } = input; if (this.isHTML5) {
const [{ provider = providers.html5, src }] = sources; if (this.config.crossorigin) {
const tagName = provider === 'html5' ? type : 'div'; this.media.setAttribute('crossorigin', '');
const attributes = provider === 'html5' ? {} : { src }; }
if (this.config.autoplay) {
this.media.setAttribute('autoplay', '');
}
if (!is.empty(input.poster)) {
this.poster = input.poster;
}
if (this.config.loop.active) {
this.media.setAttribute('loop', '');
}
if (this.config.muted) {
this.media.setAttribute('muted', '');
}
if (this.config.playsinline) {
this.media.setAttribute('playsinline', '');
}
}
Object.assign(this, { // Restore class hook
provider, ui.addStyleHook.call(this);
type,
// Check for support
supported: support.check(type, provider, this.config.playsinline),
// Create new element
media: createElement(tagName, attributes),
});
// Inject the new element // Set new sources for html5
this.elements.container.appendChild(this.media); if (this.isHTML5) {
source.insertElements.call(this, 'source', sources);
}
// Autoplay the new source? // Set video title
if (is.boolean(input.autoplay)) { this.config.title = input.title;
this.config.autoplay = input.autoplay;
}
// Set attributes for audio and video // Set up from scratch
if (this.isHTML5) { media.setup.call(this);
if (this.config.crossorigin) {
this.media.setAttribute('crossorigin', '');
}
if (this.config.autoplay) {
this.media.setAttribute('autoplay', '');
}
if (!is.empty(input.poster)) {
this.poster = input.poster;
}
if (this.config.loop.active) {
this.media.setAttribute('loop', '');
}
if (this.config.muted) {
this.media.setAttribute('muted', '');
}
if (this.config.playsinline) {
this.media.setAttribute('playsinline', '');
}
}
// Restore class hook // HTML5 stuff
ui.addStyleHook.call(this); if (this.isHTML5) {
// Setup captions
if (Object.keys(input).includes('tracks')) {
source.insertElements.call(this, 'track', input.tracks);
}
}
// Set new sources for html5 // If HTML5 or embed but not fully supported, setupInterface and call ready now
if (this.isHTML5) { if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {
source.insertElements.call(this, 'source', sources); // Setup interface
} ui.build.call(this);
}
// Set video title // Load HTML5 sources
this.config.title = input.title; if (this.isHTML5) {
this.media.load();
}
// Set up from scratch // Update previewThumbnails config & reload plugin
media.setup.call(this); if (!is.empty(input.previewThumbnails)) {
Object.assign(this.config.previewThumbnails, input.previewThumbnails);
// HTML5 stuff // Cleanup previewThumbnails plugin if it was loaded
if (this.isHTML5) { if (this.previewThumbnails && this.previewThumbnails.loaded) {
// Setup captions this.previewThumbnails.destroy();
if (Object.keys(input).includes('tracks')) { this.previewThumbnails = null;
source.insertElements.call(this, 'track', input.tracks); }
}
}
// If HTML5 or embed but not fully supported, setupInterface and call ready now // Create new instance if it is still enabled
if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) { if (this.config.previewThumbnails.enabled) {
// Setup interface this.previewThumbnails = new PreviewThumbnails(this);
ui.build.call(this); }
} }
// Load HTML5 sources // Update the fullscreen support
if (this.isHTML5) { this.fullscreen.update();
this.media.load(); },
} true,
);
// Update previewThumbnails config & reload plugin },
if (!is.empty(input.previewThumbnails)) {
Object.assign(this.config.previewThumbnails, input.previewThumbnails);
// Cleanup previewThumbnails plugin if it was loaded
if (this.previewThumbnails && this.previewThumbnails.loaded) {
this.previewThumbnails.destroy();
this.previewThumbnails = null;
}
// Create new instance if it is still enabled
if (this.config.previewThumbnails.enabled) {
this.previewThumbnails = new PreviewThumbnails(this);
}
}
// Update the fullscreen support
this.fullscreen.update();
},
true,
);
},
}; };
export default source; export default source;
+56 -56
View File
@@ -6,72 +6,72 @@ import is from './utils/is';
import { extend } from './utils/objects'; import { extend } from './utils/objects';
class Storage { class Storage {
constructor(player) { constructor(player) {
this.enabled = player.config.storage.enabled; this.enabled = player.config.storage.enabled;
this.key = player.config.storage.key; this.key = player.config.storage.key;
}
// Check for actual support (see if we can use it)
static get supported() {
try {
if (!('localStorage' in window)) {
return false;
}
const test = '___test';
// Try to use it (it might be disabled, e.g. user is in private mode)
// see: https://github.com/sampotts/plyr/issues/131
window.localStorage.setItem(test, test);
window.localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
get(key) {
if (!Storage.supported || !this.enabled) {
return null;
} }
// Check for actual support (see if we can use it) const store = window.localStorage.getItem(this.key);
static get supported() {
try {
if (!('localStorage' in window)) {
return false;
}
const test = '___test'; if (is.empty(store)) {
return null;
// Try to use it (it might be disabled, e.g. user is in private mode)
// see: https://github.com/sampotts/plyr/issues/131
window.localStorage.setItem(test, test);
window.localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
} }
get(key) { const json = JSON.parse(store);
if (!Storage.supported || !this.enabled) {
return null;
}
const store = window.localStorage.getItem(this.key); return is.string(key) && key.length ? json[key] : json;
}
if (is.empty(store)) { set(object) {
return null; // Bail if we don't have localStorage support or it's disabled
} if (!Storage.supported || !this.enabled) {
return;
const json = JSON.parse(store);
return is.string(key) && key.length ? json[key] : json;
} }
set(object) { // Can only store objectst
// Bail if we don't have localStorage support or it's disabled if (!is.object(object)) {
if (!Storage.supported || !this.enabled) { return;
return;
}
// Can only store objectst
if (!is.object(object)) {
return;
}
// Get current storage
let storage = this.get();
// Default to empty object
if (is.empty(storage)) {
storage = {};
}
// Update the working copy of the values
extend(storage, object);
// Update storage
window.localStorage.setItem(this.key, JSON.stringify(storage));
} }
// Get current storage
let storage = this.get();
// Default to empty object
if (is.empty(storage)) {
storage = {};
}
// Update the working copy of the values
extend(storage, object);
// Update storage
window.localStorage.setItem(this.key, JSON.stringify(storage));
}
} }
export default Storage; export default Storage;
+82 -82
View File
@@ -9,110 +9,110 @@ import is from './utils/is';
// Default codecs for checking mimetype support // Default codecs for checking mimetype support
const defaultCodecs = { const defaultCodecs = {
'audio/ogg': 'vorbis', 'audio/ogg': 'vorbis',
'audio/wav': '1', 'audio/wav': '1',
'video/webm': 'vp8, vorbis', 'video/webm': 'vp8, vorbis',
'video/mp4': 'avc1.42E01E, mp4a.40.2', 'video/mp4': 'avc1.42E01E, mp4a.40.2',
'video/ogg': 'theora', 'video/ogg': 'theora',
}; };
// Check for feature support // Check for feature support
const support = { const support = {
// Basic support // Basic support
audio: 'canPlayType' in document.createElement('audio'), audio: 'canPlayType' in document.createElement('audio'),
video: 'canPlayType' in document.createElement('video'), video: 'canPlayType' in document.createElement('video'),
// Check for support // Check for support
// Basic functionality vs full UI // Basic functionality vs full UI
check(type, provider, playsinline) { check(type, provider, playsinline) {
const canPlayInline = browser.isIPhone && playsinline && support.playsinline; const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
const api = support[type] || provider !== 'html5'; const api = support[type] || provider !== 'html5';
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline); const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
return { return {
api, api,
ui, ui,
}; };
}, },
// Picture-in-picture support // Picture-in-picture support
// Safari & Chrome only currently // Safari & Chrome only currently
pip: (() => { pip: (() => {
if (browser.isIPhone) { if (browser.isIPhone) {
return false; return false;
} }
// Safari // Safari
// https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
if (is.function(createElement('video').webkitSetPresentationMode)) { if (is.function(createElement('video').webkitSetPresentationMode)) {
return true; return true;
} }
// Chrome // Chrome
// https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) { if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
return true; return true;
} }
return false; return false;
})(), })(),
// Airplay support // Airplay support
// Safari only currently // Safari only currently
airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent), airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
// Inline playback support // Inline playback support
// https://webkit.org/blog/6784/new-video-policies-for-ios/ // https://webkit.org/blog/6784/new-video-policies-for-ios/
playsinline: 'playsInline' in document.createElement('video'), playsinline: 'playsInline' in document.createElement('video'),
// Check for mime type support against a player instance // Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html // Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplayer.com/test/h5mt.html // Related: http://www.leanbackplayer.com/test/h5mt.html
mime(input) { mime(input) {
if (is.empty(input)) { if (is.empty(input)) {
return false; return false;
} }
const [mediaType] = input.split('/'); const [mediaType] = input.split('/');
let type = input; let type = input;
// Verify we're using HTML5 and there's no media type mismatch // Verify we're using HTML5 and there's no media type mismatch
if (!this.isHTML5 || mediaType !== this.type) { if (!this.isHTML5 || mediaType !== this.type) {
return false; return false;
} }
// Add codec if required // Add codec if required
if (Object.keys(defaultCodecs).includes(type)) { if (Object.keys(defaultCodecs).includes(type)) {
type += `; codecs="${defaultCodecs[input]}"`; type += `; codecs="${defaultCodecs[input]}"`;
} }
try { try {
return Boolean(type && this.media.canPlayType(type).replace(/no/, '')); return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
} catch (e) { } catch (e) {
return false; return false;
} }
}, },
// Check for textTracks support // Check for textTracks support
textTracks: 'textTracks' in document.createElement('video'), textTracks: 'textTracks' in document.createElement('video'),
// <input type="range"> Sliders // <input type="range"> Sliders
rangeInput: (() => { rangeInput: (() => {
const range = document.createElement('input'); const range = document.createElement('input');
range.type = 'range'; range.type = 'range';
return range.type === 'range'; return range.type === 'range';
})(), })(),
// Touch // Touch
// NOTE: Remember a device can be mouse + touch enabled so we check on first touch event // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
touch: 'ontouchstart' in document.documentElement, touch: 'ontouchstart' in document.documentElement,
// Detect transitions support // Detect transitions support
transitions: transitionEndEvent !== false, transitions: transitionEndEvent !== false,
// Reduced motion iOS & MacOS setting // Reduced motion iOS & MacOS setting
// https://webkit.org/blog/7551/responsive-design-for-motion/ // https://webkit.org/blog/7551/responsive-design-for-motion/
reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches, reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,
}; };
export default support; export default support;
+220 -210
View File
@@ -13,267 +13,277 @@ import is from './utils/is';
import loadImage from './utils/load-image'; import loadImage from './utils/load-image';
const ui = { const ui = {
addStyleHook() { addStyleHook() {
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
}, },
// Toggle native HTML5 media controls // Toggle native HTML5 media controls
toggleNativeControls(toggle = false) { toggleNativeControls(toggle = false) {
if (toggle && this.isHTML5) { if (toggle && this.isHTML5) {
this.media.setAttribute('controls', ''); this.media.setAttribute('controls', '');
} else { } else {
this.media.removeAttribute('controls'); this.media.removeAttribute('controls');
} }
}, },
// Setup the UI // Setup the UI
build() { build() {
// Re-attach media element listeners // Re-attach media element listeners
// TODO: Use event bubbling? // TODO: Use event bubbling?
this.listeners.media(); this.listeners.media();
// Don't setup interface if no support // Don't setup interface if no support
if (!this.supported.ui) { if (!this.supported.ui) {
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`); this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
// Restore native controls // Restore native controls
ui.toggleNativeControls.call(this, true); ui.toggleNativeControls.call(this, true);
// Bail // Bail
return; return;
} }
// Inject custom controls if not present // Inject custom controls if not present
if (!is.element(this.elements.controls)) { if (!is.element(this.elements.controls)) {
// Inject custom controls // Inject custom controls
controls.inject.call(this); controls.inject.call(this);
// Re-attach control listeners // Re-attach control listeners
this.listeners.controls(); this.listeners.controls();
} }
// Remove native controls // Remove native controls
ui.toggleNativeControls.call(this); ui.toggleNativeControls.call(this);
// Setup captions for HTML5 // Setup captions for HTML5
if (this.isHTML5) { if (this.isHTML5) {
captions.setup.call(this); captions.setup.call(this);
} }
// Reset volume // Reset volume
this.volume = null; this.volume = null;
// Reset mute state // Reset mute state
this.muted = null; this.muted = null;
// Reset loop state // Reset loop state
this.loop = null; this.loop = null;
// Reset quality setting // Reset quality setting
this.quality = null; this.quality = null;
// Reset speed // Reset speed
this.speed = null; this.speed = null;
// Reset volume display // Reset volume display
controls.updateVolume.call(this); controls.updateVolume.call(this);
// Reset time display // Reset time display
controls.timeUpdate.call(this); controls.timeUpdate.call(this);
// Update the UI // Update the UI
ui.checkPlaying.call(this); ui.checkPlaying.call(this);
// Check for picture-in-picture support // Check for picture-in-picture support
toggleClass( toggleClass(
this.elements.container, this.elements.container,
this.config.classNames.pip.supported, this.config.classNames.pip.supported,
support.pip && this.isHTML5 && this.isVideo, support.pip && this.isHTML5 && this.isVideo,
); );
// Check for airplay support // Check for airplay support
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class // Add iOS class
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class // Add touch class
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls // Ready for API calls
this.ready = true; this.ready = true;
// Ready event at end of execution stack // Ready event at end of execution stack
setTimeout(() => { setTimeout(() => {
triggerEvent.call(this, this.media, 'ready'); triggerEvent.call(this, this.media, 'ready');
}, 0); }, 0);
// Set the title // Set the title
ui.setTitle.call(this); ui.setTitle.call(this);
// Assure the poster image is set, if the property was added before the element was created // Assure the poster image is set, if the property was added before the element was created
if (this.poster) { if (this.poster) {
ui.setPoster.call(this, this.poster, false).catch(() => {}); ui.setPoster.call(this, this.poster, false).catch(() => {});
} }
// Manually set the duration if user has overridden it. // Manually set the duration if user has overridden it.
// The event listeners for it doesn't get called if preload is disabled (#701) // The event listeners for it doesn't get called if preload is disabled (#701)
if (this.config.duration) { if (this.config.duration) {
controls.durationUpdate.call(this); controls.durationUpdate.call(this);
} }
}, },
// Setup aria attribute for play and iframe title // Setup aria attribute for play and iframe title
setTitle() { setTitle() {
// Find the current text // Find the current text
let label = i18n.get('play', this.config); let label = i18n.get('play', this.config);
// If there's a media title set, use that for the label // If there's a media title set, use that for the label
if (is.string(this.config.title) && !is.empty(this.config.title)) { if (is.string(this.config.title) && !is.empty(this.config.title)) {
label += `, ${this.config.title}`; label += `, ${this.config.title}`;
} }
// If there's a play button, set label // If there's a play button, set label
Array.from(this.elements.buttons.play || []).forEach(button => { Array.from(this.elements.buttons.play || []).forEach(button => {
button.setAttribute('aria-label', label); button.setAttribute('aria-label', label);
}); });
// Set iframe title // Set iframe title
// https://github.com/sampotts/plyr/issues/124 // https://github.com/sampotts/plyr/issues/124
if (this.isEmbed) { if (this.isEmbed) {
const iframe = getElement.call(this, 'iframe'); const iframe = getElement.call(this, 'iframe');
if (!is.element(iframe)) { if (!is.element(iframe)) {
return; return;
} }
// Default to media type // Default to media type
const title = !is.empty(this.config.title) ? this.config.title : 'video'; const title = !is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config); const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', format.replace('{title}', title)); iframe.setAttribute('title', format.replace('{title}', title));
} }
}, },
// Toggle poster // Toggle poster
togglePoster(enable) { togglePoster(enable) {
toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
}, },
// Set the poster image (async) // Set the poster image (async)
// Used internally for the poster setter, with the passive option forced to false // Used internally for the poster setter, with the passive option forced to false
setPoster(poster, passive = true) { setPoster(poster, passive = true) {
// Don't override if call is passive // Don't override if call is passive
if (passive && this.poster) { if (passive && this.poster) {
return Promise.reject(new Error('Poster already set')); return Promise.reject(new Error('Poster already set'));
} }
// Set property synchronously to respect the call order // Set property synchronously to respect the call order
this.media.setAttribute('poster', poster); this.media.setAttribute('data-poster', poster);
// HTML5 uses native poster attribute // Wait until ui is ready
if (this.isHTML5) { return (
return Promise.resolve(poster); ready
} .call(this)
// Load image
.then(() => loadImage(poster))
.catch(err => {
// Hide poster on error unless it's been set by another call
if (poster === this.poster) {
ui.togglePoster.call(this, false);
}
// Rethrow
throw err;
})
.then(() => {
// Prevent race conditions
if (poster !== this.poster) {
throw new Error('setPoster cancelled by later call to setPoster');
}
})
.then(() => {
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
// Wait until ui is ready ui.togglePoster.call(this, true);
return (
ready
.call(this)
// Load image
.then(() => loadImage(poster))
.catch(err => {
// Hide poster on error unless it's been set by another call
if (poster === this.poster) {
ui.togglePoster.call(this, false);
}
// Rethrow
throw err;
})
.then(() => {
// Prevent race conditions
if (poster !== this.poster) {
throw new Error('setPoster cancelled by later call to setPoster');
}
})
.then(() => {
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
ui.togglePoster.call(this, true); return poster;
})
);
},
return poster; // Check playing state
}) checkPlaying(event) {
); // Class hooks
}, toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Check playing state // Set state
checkPlaying(event) { Array.from(this.elements.buttons.play || []).forEach(target => {
// Class hooks Object.assign(target, { pressed: this.playing });
toggleClass(this.elements.container, this.config.classNames.playing, this.playing); target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
toggleClass(this.elements.container, this.config.classNames.paused, this.paused); });
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set state // Only update controls on non timeupdate events
Array.from(this.elements.buttons.play || []).forEach(target => { if (is.event(event) && event.type === 'timeupdate') {
Object.assign(target, { pressed: this.playing }); return;
target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config)); }
});
// Only update controls on non timeupdate events // Toggle controls
if (is.event(event) && event.type === 'timeupdate') { ui.toggleControls.call(this);
return; },
}
// Toggle controls // Check if media is loading
checkLoading(event) {
this.loading = ['stalled', 'waiting'].includes(event.type);
// Clear timer
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);
// Update controls visibility
ui.toggleControls.call(this); ui.toggleControls.call(this);
}, },
this.loading ? 250 : 0,
);
},
// Check if media is loading // Toggle controls based on state and `force` argument
checkLoading(event) { toggleControls(force) {
this.loading = ['stalled', 'waiting'].includes(event.type); const { controls: controlsElement } = this.elements;
// Clear timer if (controlsElement && this.config.hideControls) {
clearTimeout(this.timers.loading); // 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();
// Timer to prevent flicker when seeking // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
this.timers.loading = setTimeout( this.toggleControls(
() => { Boolean(
// Update progress bar loading class state force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,
toggleClass(this.elements.container, this.config.classNames.loading, this.loading); ),
);
}
},
// Update controls visibility // Migrate any custom properties from the media to the parent
ui.toggleControls.call(this); migrateStyles() {
}, // Loop through values (as they are the keys when the object is spread 🤔)
this.loading ? 250 : 0, Object.values({ ...this.media.style })
); // We're only fussed about Plyr specific properties
}, .filter(key => !is.empty(key) && key.startsWith('--plyr'))
.forEach(key => {
// Set on the container
this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));
// Toggle controls based on state and `force` argument // Clean up from media element
toggleControls(force) { this.media.style.removeProperty(key);
const { controls: controlsElement } = this.elements; });
if (controlsElement && this.config.hideControls) { // Remove attribute if empty
// 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.) if (is.empty(this.media.style)) {
const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now(); this.media.removeAttribute('style');
}
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide },
this.toggleControls(
Boolean(
force ||
this.loading ||
this.paused ||
controlsElement.pressed ||
controlsElement.hover ||
recentTouchSeek,
),
);
}
},
}; };
export default ui; export default ui;
+21 -21
View File
@@ -5,34 +5,34 @@
import is from './is'; import is from './is';
export const transitionEndEvent = (() => { export const transitionEndEvent = (() => {
const element = document.createElement('span'); const element = document.createElement('span');
const events = { const events = {
WebkitTransition: 'webkitTransitionEnd', WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend', MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend', OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend', transition: 'transitionend',
}; };
const type = Object.keys(events).find(event => element.style[event] !== undefined); const type = Object.keys(events).find(event => element.style[event] !== undefined);
return is.string(type) ? events[type] : false; return is.string(type) ? events[type] : false;
})(); })();
// Force repaint of element // Force repaint of element
export function repaint(element, delay) { export function repaint(element, delay) {
setTimeout(() => { setTimeout(() => {
try { try {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
element.hidden = true; element.hidden = true;
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
element.offsetHeight; element.offsetHeight;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
element.hidden = false; element.hidden = false;
} catch (e) { } catch (e) {
// Do nothing // Do nothing
} }
}, delay); }, delay);
} }
+8 -8
View File
@@ -6,18 +6,18 @@ import is from './is';
// Remove duplicates in an array // Remove duplicates in an array
export function dedupe(array) { export function dedupe(array) {
if (!is.array(array)) { if (!is.array(array)) {
return array; return array;
} }
return array.filter((item, index) => array.indexOf(item) === index); return array.filter((item, index) => array.indexOf(item) === index);
} }
// Get the closest value in an array // Get the closest value in an array
export function closest(array, value) { export function closest(array, value) {
if (!is.array(array) || !array.length) { if (!is.array(array) || !array.length) {
return null; return null;
} }
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev)); return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
} }
+5 -5
View File
@@ -4,11 +4,11 @@
// ========================================================================== // ==========================================================================
const browser = { const browser = {
isIE: /* @cc_on!@ */ false || !!document.documentMode, isIE: /* @cc_on!@ */ false || !!document.documentMode,
isEdge: window.navigator.userAgent.includes('Edge'), isEdge: window.navigator.userAgent.includes('Edge'),
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent), isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform), isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform), isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
}; };
export default browser; export default browser;
+181 -161
View File
@@ -7,257 +7,277 @@ import { extend } from './objects';
// Wrap an element // Wrap an element
export function wrap(elements, wrapper) { export function wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary. // Convert `elements` to an array, if necessary.
const targets = elements.length ? elements : [elements]; const targets = elements.length ? elements : [elements];
// Loops backwards to prevent having to clone the wrapper on the // Loops backwards to prevent having to clone the wrapper on the
// first element (see `child` below). // first element (see `child` below).
Array.from(targets) Array.from(targets)
.reverse() .reverse()
.forEach((element, index) => { .forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper; const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling. // Cache the current parent and sibling.
const parent = element.parentNode; const parent = element.parentNode;
const sibling = element.nextSibling; const sibling = element.nextSibling;
// Wrap the element (is automatically removed from its current // Wrap the element (is automatically removed from its current
// parent). // parent).
child.appendChild(element); child.appendChild(element);
// If the element had a sibling, insert the wrapper before // If the element had a sibling, insert the wrapper before
// the sibling to maintain the HTML structure; otherwise, just // the sibling to maintain the HTML structure; otherwise, just
// append it to the parent. // append it to the parent.
if (sibling) { if (sibling) {
parent.insertBefore(child, sibling); parent.insertBefore(child, sibling);
} else { } else {
parent.appendChild(child); parent.appendChild(child);
} }
}); });
} }
// Set attributes // Set attributes
export function setAttributes(element, attributes) { export function setAttributes(element, attributes) {
if (!is.element(element) || is.empty(attributes)) { if (!is.element(element) || is.empty(attributes)) {
return; return;
} }
// Assume null and undefined attributes should be left out, // Assume null and undefined attributes should be left out,
// Setting them would otherwise convert them to "null" and "undefined" // Setting them would otherwise convert them to "null" and "undefined"
Object.entries(attributes) Object.entries(attributes)
.filter(([, value]) => !is.nullOrUndefined(value)) .filter(([, value]) => !is.nullOrUndefined(value))
.forEach(([key, value]) => element.setAttribute(key, value)); .forEach(([key, value]) => element.setAttribute(key, value));
} }
// Create a DocumentFragment // Create a DocumentFragment
export function createElement(type, attributes, text) { export function createElement(type, attributes, text) {
// Create a new <element> // Create a new <element>
const element = document.createElement(type); const element = document.createElement(type);
// Set all passed attributes // Set all passed attributes
if (is.object(attributes)) { if (is.object(attributes)) {
setAttributes(element, attributes); setAttributes(element, attributes);
} }
// Add text node // Add text node
if (is.string(text)) { if (is.string(text)) {
element.innerText = text; element.innerText = text;
} }
// Return built element // Return built element
return element; return element;
} }
// Inaert an element after another // Inaert an element after another
export function insertAfter(element, target) { export function insertAfter(element, target) {
if (!is.element(element) || !is.element(target)) { if (!is.element(element) || !is.element(target)) {
return; return;
} }
target.parentNode.insertBefore(element, target.nextSibling); target.parentNode.insertBefore(element, target.nextSibling);
} }
// Insert a DocumentFragment // Insert a DocumentFragment
export function insertElement(type, parent, attributes, text) { export function insertElement(type, parent, attributes, text) {
if (!is.element(parent)) { if (!is.element(parent)) {
return; return;
} }
parent.appendChild(createElement(type, attributes, text)); parent.appendChild(createElement(type, attributes, text));
} }
// Remove element(s) // Remove element(s)
export function removeElement(element) { export function removeElement(element) {
if (is.nodeList(element) || is.array(element)) { if (is.nodeList(element) || is.array(element)) {
Array.from(element).forEach(removeElement); Array.from(element).forEach(removeElement);
return; return;
} }
if (!is.element(element) || !is.element(element.parentNode)) { if (!is.element(element) || !is.element(element.parentNode)) {
return; return;
} }
element.parentNode.removeChild(element); element.parentNode.removeChild(element);
} }
// Remove all child elements // Remove all child elements
export function emptyElement(element) { export function emptyElement(element) {
if (!is.element(element)) { if (!is.element(element)) {
return; return;
} }
let { length } = element.childNodes; let { length } = element.childNodes;
while (length > 0) { while (length > 0) {
element.removeChild(element.lastChild); element.removeChild(element.lastChild);
length -= 1; length -= 1;
} }
} }
// Replace element // Replace element
export function replaceElement(newChild, oldChild) { export function replaceElement(newChild, oldChild) {
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
return null; return null;
} }
oldChild.parentNode.replaceChild(newChild, oldChild); oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild; return newChild;
} }
// Get an attribute object from a string selector // Get an attribute object from a string selector
export function getAttributesFromSelector(sel, existingAttributes) { export function getAttributesFromSelector(sel, existingAttributes) {
// For example: // For example:
// '.test' to { class: 'test' } // '.test' to { class: 'test' }
// '#test' to { id: 'test' } // '#test' to { id: 'test' }
// '[data-test="test"]' to { 'data-test': 'test' } // '[data-test="test"]' to { 'data-test': 'test' }
if (!is.string(sel) || is.empty(sel)) { if (!is.string(sel) || is.empty(sel)) {
return {}; return {};
} }
const attributes = {}; const attributes = {};
const existing = extend({}, existingAttributes); const existing = extend({}, existingAttributes);
sel.split(',').forEach(s => { sel.split(',').forEach(s => {
// Remove whitespace // Remove whitespace
const selector = s.trim(); const selector = s.trim();
const className = selector.replace('.', ''); const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, ''); const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value // Get the parts and value
const parts = stripped.split('='); const parts = stripped.split('=');
const [key] = parts; const [key] = parts;
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character // Get the first character
const start = selector.charAt(0); const start = selector.charAt(0);
switch (start) { switch (start) {
case '.': case '.':
// Add to existing classname // Add to existing classname
if (is.string(existing.class)) { if (is.string(existing.class)) {
attributes.class = `${existing.class} ${className}`; attributes.class = `${existing.class} ${className}`;
} else { } else {
attributes.class = className; attributes.class = className;
}
break;
case '#':
// ID selector
attributes.id = selector.replace('#', '');
break;
case '[':
// Attribute selector
attributes[key] = value;
break;
default:
break;
} }
}); break;
return extend(existing, attributes); case '#':
// ID selector
attributes.id = selector.replace('#', '');
break;
case '[':
// Attribute selector
attributes[key] = value;
break;
default:
break;
}
});
return extend(existing, attributes);
} }
// Toggle hidden // Toggle hidden
export function toggleHidden(element, hidden) { export function toggleHidden(element, hidden) {
if (!is.element(element)) { if (!is.element(element)) {
return; return;
} }
let hide = hidden; let hide = hidden;
if (!is.boolean(hide)) { if (!is.boolean(hide)) {
hide = !element.hidden; hide = !element.hidden;
} }
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
element.hidden = hide; element.hidden = hide;
} }
// Mirror Element.classList.toggle, with IE compatibility for "force" argument // Mirror Element.classList.toggle, with IE compatibility for "force" argument
export function toggleClass(element, className, force) { export function toggleClass(element, className, force) {
if (is.nodeList(element)) { if (is.nodeList(element)) {
return Array.from(element).map(e => toggleClass(e, className, force)); return Array.from(element).map(e => toggleClass(e, className, force));
}
if (is.element(element)) {
let method = 'toggle';
if (typeof force !== 'undefined') {
method = force ? 'add' : 'remove';
} }
if (is.element(element)) { element.classList[method](className);
let method = 'toggle'; return element.classList.contains(className);
if (typeof force !== 'undefined') { }
method = force ? 'add' : 'remove';
}
element.classList[method](className); return false;
return element.classList.contains(className);
}
return false;
} }
// Has class name // Has class name
export function hasClass(element, className) { export function hasClass(element, className) {
return is.element(element) && element.classList.contains(className); return is.element(element) && element.classList.contains(className);
} }
// Element matches selector // Element matches selector
export function matches(element, selector) { export function matches(element, selector) {
const prototype = { Element }; const { prototype } = Element;
function match() { function match() {
return Array.from(document.querySelectorAll(selector)).includes(this); return Array.from(document.querySelectorAll(selector)).includes(this);
} }
const method = const method =
prototype.matches || prototype.matches ||
prototype.webkitMatchesSelector || prototype.webkitMatchesSelector ||
prototype.mozMatchesSelector || prototype.mozMatchesSelector ||
prototype.msMatchesSelector || prototype.msMatchesSelector ||
match; match;
return method.call(element, selector); return method.call(element, selector);
}
// Closest ancestor element matching selector (also tests element itself)
export function closest(element, selector) {
const { prototype } = Element;
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
function closestElement() {
let el = this;
do {
if (matches.matches(el, selector)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
}
const method = prototype.closest || closestElement;
return method.call(element, selector);
} }
// Find all elements // Find all elements
export function getElements(selector) { export function getElements(selector) {
return this.elements.container.querySelectorAll(selector); return this.elements.container.querySelectorAll(selector);
} }
// Find a single element // Find a single element
export function getElement(selector) { export function getElement(selector) {
return this.elements.container.querySelector(selector); return this.elements.container.querySelector(selector);
} }
// Set focus and tab focus class // Set focus and tab focus class
export function setFocus(element = null, tabFocus = false) { export function setFocus(element = null, tabFocus = false) {
if (!is.element(element)) { if (!is.element(element)) {
return; return;
} }
// Set regular focus // Set regular focus
element.focus({ preventScroll: true }); element.focus({ preventScroll: true });
// If we want to mimic keyboard focus via tab // If we want to mimic keyboard focus via tab
if (tabFocus) { if (tabFocus) {
toggleClass(element, this.config.classNames.tabFocus); toggleClass(element, this.config.classNames.tabFocus);
} }
} }
+71 -71
View File
@@ -8,110 +8,110 @@ import is from './is';
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
// https://www.youtube.com/watch?v=NPM6172J22g // https://www.youtube.com/watch?v=NPM6172J22g
const supportsPassiveListeners = (() => { const supportsPassiveListeners = (() => {
// Test via a getter in the options object to see if the passive property is accessed // Test via a getter in the options object to see if the passive property is accessed
let supported = false; let supported = false;
try { try {
const options = Object.defineProperty({}, 'passive', { const options = Object.defineProperty({}, 'passive', {
get() { get() {
supported = true; supported = true;
return null; return null;
}, },
}); });
window.addEventListener('test', null, options); window.addEventListener('test', null, options);
window.removeEventListener('test', null, options); window.removeEventListener('test', null, options);
} catch (e) { } catch (e) {
// Do nothing // Do nothing
} }
return supported; return supported;
})(); })();
// Toggle event listener // Toggle event listener
export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) { export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {
// Bail if no element, event, or callback // Bail if no element, event, or callback
if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) { if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {
return; return;
}
// 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;
// If passive events listeners are supported
if (supportsPassiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive,
// Whether the listener is a capturing listener or not
capture,
};
}
// If a single node is passed, bind the event listener
events.forEach(type => {
if (this && this.eventListeners && toggle) {
// Cache event listener
this.eventListeners.push({ element, type, callback, options });
} }
// Allow multiple events element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
const events = event.split(' '); });
// Build options
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;
// If passive events listeners are supported
if (supportsPassiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive,
// Whether the listener is a capturing listener or not
capture,
};
}
// If a single node is passed, bind the event listener
events.forEach(type => {
if (this && this.eventListeners && toggle) {
// Cache event listener
this.eventListeners.push({ element, type, callback, options });
}
element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
});
} }
// Bind event handler // Bind event handler
export function on(element, events = '', callback, passive = true, capture = false) { export function on(element, events = '', callback, passive = true, capture = false) {
toggleListener.call(this, element, events, callback, true, passive, capture); toggleListener.call(this, element, events, callback, true, passive, capture);
} }
// Unbind event handler // Unbind event handler
export function off(element, events = '', callback, passive = true, capture = false) { export function off(element, events = '', callback, passive = true, capture = false) {
toggleListener.call(this, element, events, callback, false, passive, capture); toggleListener.call(this, element, events, callback, false, passive, capture);
} }
// Bind once-only event handler // Bind once-only event handler
export function once(element, events = '', callback, passive = true, capture = false) { export function once(element, events = '', callback, passive = true, capture = false) {
const onceCallback = (...args) => { const onceCallback = (...args) => {
off(element, events, onceCallback, passive, capture); off(element, events, onceCallback, passive, capture);
callback.apply(this, args); callback.apply(this, args);
}; };
toggleListener.call(this, element, events, onceCallback, true, passive, capture); toggleListener.call(this, element, events, onceCallback, true, passive, capture);
} }
// Trigger event // Trigger event
export function triggerEvent(element, type = '', bubbles = false, detail = {}) { export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element // Bail if no element
if (!is.element(element) || is.empty(type)) { if (!is.element(element) || is.empty(type)) {
return; return;
} }
// Create and dispatch the event // Create and dispatch the event
const event = new CustomEvent(type, { const event = new CustomEvent(type, {
bubbles, bubbles,
detail: { ...detail, plyr: this,}, detail: { ...detail, plyr: this },
}); });
// Dispatch the event // Dispatch the event
element.dispatchEvent(event); element.dispatchEvent(event);
} }
// Unbind all cached event listeners // Unbind all cached event listeners
export function unbindListeners() { export function unbindListeners() {
if (this && this.eventListeners) { if (this && this.eventListeners) {
this.eventListeners.forEach(item => { this.eventListeners.forEach(item => {
const { element, type, callback, options } = item; const { element, type, callback, options } = item;
element.removeEventListener(type, callback, options); element.removeEventListener(type, callback, options);
}); });
this.eventListeners = []; this.eventListeners = [];
} }
} }
// Run method when / if player is ready // Run method when / if player is ready
export function ready() { export function ready() {
return new Promise(resolve => return new Promise(resolve =>
this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve), this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve),
).then(() => {}); ).then(() => {});
} }
+32 -32
View File
@@ -4,39 +4,39 @@
// ========================================================================== // ==========================================================================
export default function fetch(url, responseType = 'text') { export default function fetch(url, responseType = 'text') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const request = new XMLHttpRequest(); const request = new XMLHttpRequest();
// Check for CORS support // Check for CORS support
if (!('withCredentials' in request)) { if (!('withCredentials' in request)) {
return; return;
} }
request.addEventListener('load', () => { request.addEventListener('load', () => {
if (responseType === 'text') { if (responseType === 'text') {
try { try {
resolve(JSON.parse(request.responseText)); resolve(JSON.parse(request.responseText));
} catch (e) { } catch (e) {
resolve(request.responseText); resolve(request.responseText);
} }
} else { } else {
resolve(request.response); resolve(request.response);
}
});
request.addEventListener('error', () => {
throw new Error(request.status);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
} }
}); });
request.addEventListener('error', () => {
throw new Error(request.status);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
}
});
} }
+25 -25
View File
@@ -8,40 +8,40 @@ import { replaceAll } from './strings';
// Skip i18n for abbreviations and brand names // Skip i18n for abbreviations and brand names
const resources = { const resources = {
pip: 'PIP', pip: 'PIP',
airplay: 'AirPlay', airplay: 'AirPlay',
html5: 'HTML5', html5: 'HTML5',
vimeo: 'Vimeo', vimeo: 'Vimeo',
youtube: 'YouTube', youtube: 'YouTube',
}; };
const i18n = { const i18n = {
get(key = '', config = {}) { get(key = '', config = {}) {
if (is.empty(key) || is.empty(config)) { if (is.empty(key) || is.empty(config)) {
return ''; return '';
} }
let string = getDeep(config.i18n, key); let string = getDeep(config.i18n, key);
if (is.empty(string)) { if (is.empty(string)) {
if (Object.keys(resources).includes(key)) { if (Object.keys(resources).includes(key)) {
return resources[key]; return resources[key];
} }
return ''; return '';
} }
const replace = { const replace = {
'{seektime}': config.seekTime, '{seektime}': config.seekTime,
'{title}': config.title, '{title}': config.title,
}; };
Object.entries(replace).forEach(([k, v]) => { Object.entries(replace).forEach(([k, v]) => {
string = replaceAll(string, k, v); string = replaceAll(string, k, v);
}); });
return string; return string;
}, },
}; };
export default i18n; export default i18n;
+40 -40
View File
@@ -19,54 +19,54 @@ const isEvent = input => instanceOf(input, Event);
const isKeyboardEvent = input => instanceOf(input, KeyboardEvent); const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);
const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind)); const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));
const isPromise = input => instanceOf(input, Promise); const isPromise = input => instanceOf(input, Promise) && isFunction(input.then);
const isEmpty = input => const isEmpty = input =>
isNullOrUndefined(input) || isNullOrUndefined(input) ||
((isString(input) || isArray(input) || isNodeList(input)) && !input.length) || ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||
(isObject(input) && !Object.keys(input).length); (isObject(input) && !Object.keys(input).length);
const isUrl = input => { const isUrl = input => {
// Accept a URL object // Accept a URL object
if (instanceOf(input, window.URL)) { if (instanceOf(input, window.URL)) {
return true; return true;
} }
// Must be string from here // Must be string from here
if (!isString(input)) { if (!isString(input)) {
return false; return false;
} }
// Add the protocol if required // Add the protocol if required
let string = input; let string = input;
if (!input.startsWith('http://') || !input.startsWith('https://')) { if (!input.startsWith('http://') || !input.startsWith('https://')) {
string = `http://${input}`; string = `http://${input}`;
} }
try { try {
return !isEmpty(new URL(string).hostname); return !isEmpty(new URL(string).hostname);
} catch (e) { } catch (e) {
return false; return false;
} }
}; };
export default { export default {
nullOrUndefined: isNullOrUndefined, nullOrUndefined: isNullOrUndefined,
object: isObject, object: isObject,
number: isNumber, number: isNumber,
string: isString, string: isString,
boolean: isBoolean, boolean: isBoolean,
function: isFunction, function: isFunction,
array: isArray, array: isArray,
weakMap: isWeakMap, weakMap: isWeakMap,
nodeList: isNodeList, nodeList: isNodeList,
element: isElement, element: isElement,
textNode: isTextNode, textNode: isTextNode,
event: isEvent, event: isEvent,
keyboardEvent: isKeyboardEvent, keyboardEvent: isKeyboardEvent,
cue: isCue, cue: isCue,
track: isTrack, track: isTrack,
promise: isPromise, promise: isPromise,
url: isUrl, url: isUrl,
empty: isEmpty, empty: isEmpty,
}; };
+9 -9
View File
@@ -5,15 +5,15 @@
// ========================================================================== // ==========================================================================
export default function loadImage(src, minWidth = 1) { export default function loadImage(src, minWidth = 1) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const image = new Image(); const image = new Image();
const handler = () => { const handler = () => {
delete image.onload; delete image.onload;
delete image.onerror; delete image.onerror;
(image.naturalWidth >= minWidth ? resolve : reject)(image); (image.naturalWidth >= minWidth ? resolve : reject)(image);
}; };
Object.assign(image, { onload: handler, onerror: handler, src }); Object.assign(image, { onload: handler, onerror: handler, src });
}); });
} }
+5 -5
View File
@@ -5,10 +5,10 @@
import loadjs from 'loadjs'; import loadjs from 'loadjs';
export default function loadScript(url) { export default function loadScript(url) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
loadjs(url, { loadjs(url, {
success: resolve, success: resolve,
error: reject, error: reject,
});
}); });
});
} }
+55 -55
View File
@@ -8,68 +8,68 @@ import is from './is';
// Load an external SVG sprite // Load an external SVG sprite
export default function loadSprite(url, id) { export default function loadSprite(url, id) {
if (!is.string(url)) { if (!is.string(url)) {
return; return;
}
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
if (hasId && exists()) {
return;
} }
const prefix = 'cache'; // Inject the SVG to the body
const hasId = is.string(id); document.body.insertAdjacentElement('afterbegin', container);
let isCached = false; };
const exists = () => document.getElementById(id) !== null;
const update = (container, data) => { // Only load once if ID set
// eslint-disable-next-line no-param-reassign if (!hasId || !exists()) {
container.innerHTML = data; const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
container.setAttribute('hidden', '');
// Check again incase of race condition if (hasId) {
if (hasId && exists()) { container.setAttribute('id', id);
return; }
// Check in cache
if (useStorage) {
const cached = window.localStorage.getItem(`${prefix}-${id}`);
isCached = cached !== null;
if (isCached) {
const data = JSON.parse(cached);
update(container, data.content);
}
}
// Get the sprite
fetch(url)
.then(result => {
if (is.empty(result)) {
return;
} }
// Inject the SVG to the body
document.body.insertAdjacentElement('afterbegin', container);
};
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
container.setAttribute('hidden', '');
if (hasId) {
container.setAttribute('id', id);
}
// Check in cache
if (useStorage) { if (useStorage) {
const cached = window.localStorage.getItem(`${prefix}-${id}`); window.localStorage.setItem(
isCached = cached !== null; `${prefix}-${id}`,
JSON.stringify({
if (isCached) { content: result,
const data = JSON.parse(cached); }),
update(container, data.content); );
}
} }
// Get the sprite update(container, result);
fetch(url) })
.then(result => { .catch(() => {});
if (is.empty(result)) { }
return;
}
if (useStorage) {
window.localStorage.setItem(
`${prefix}-${id}`,
JSON.stringify({
content: result,
}),
);
}
update(container, result);
})
.catch(() => {});
}
} }
+1 -1
View File
@@ -11,7 +11,7 @@
* @type Number * @type Number
*/ */
export function clamp(input = 0, min = 0, max = 255) { export function clamp(input = 0, min = 0, max = 255) {
return Math.min(Math.max(input, min), max); return Math.min(Math.max(input, min), max);
} }
export default { clamp }; export default { clamp };
+23 -23
View File
@@ -6,37 +6,37 @@ import is from './is';
// Clone nested objects // Clone nested objects
export function cloneDeep(object) { export function cloneDeep(object) {
return JSON.parse(JSON.stringify(object)); return JSON.parse(JSON.stringify(object));
} }
// Get a nested value in an object // Get a nested value in an object
export function getDeep(object, path) { export function getDeep(object, path) {
return path.split('.').reduce((obj, key) => obj && obj[key], object); return path.split('.').reduce((obj, key) => obj && obj[key], object);
} }
// Deep extend destination object with N more objects // Deep extend destination object with N more objects
export function extend(target = {}, ...sources) { export function extend(target = {}, ...sources) {
if (!sources.length) { if (!sources.length) {
return target; return target;
}
const source = sources.shift();
if (!is.object(source)) {
return target;
}
Object.keys(source).forEach(key => {
if (is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, { [key]: {} });
}
extend(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
} }
});
const source = sources.shift(); return extend(target, ...sources);
if (!is.object(source)) {
return target;
}
Object.keys(source).forEach(key => {
if (is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, { [key]: {} });
}
extend(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
});
return extend(target, ...sources);
} }
+14
View File
@@ -0,0 +1,14 @@
import is from './is';
/**
* Silence a Promise-like object.
* This is useful for avoiding non-harmful, but potentially confusing "uncaught
* play promise" rejection error messages.
* @param {Object} value An object that may or may not be `Promise`-like.
*/
export function silencePromise(value) {
if (is.promise(value)) {
value.then(null, () => {});
}
}
export default { silencePromise };

Some files were not shown because too many files have changed in this diff Show More