Compare commits

..

No commits in common. "master" and "develop" have entirely different histories.

60 changed files with 14049 additions and 14454 deletions

View File

@ -1 +0,0 @@
19.7.0

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
16

View File

@ -1,58 +1,3 @@
# Changelog
### v3.7.8
- Feat: Minor demo style tweaks
- Fix: Minor style fixes related to backgrounds and border radii (🚨 Requires a SCSS/CSS update 🚨)
### v3.7.7
- Fix (Accessibility): Dont set tabindex on parent container
- Fix (Accessibility): Add `role="timer"` to time elements
- Fix (Accessibility): Leverage native `:focus-visible` in CSS, instead of a custom solution (🚨 Requires a SCSS/CSS update 🚨)
### v3.7.6
- Fix: Revert postinstall script
### v3.7.5
- Fix: Replace `pnpm` with `npm` in scripts to fix build issues
### v3.7.4
- Fix: Fixed event key with space (thanks @royeden!)
- Fix: Changing Vimeo function call from `setVolume` to `setMuted` to fix iOS issue (issue #2624) (thanks @HandreMelo and Andre Fernandes Cristofolini Melo!)
- Fix: Call preview-thumbnails listeners() function on load (thanks @mogzol!)
- Fix: Fullscreen improvements for iOS & iPadOS
- Feat: Remove need for iOS-specific styling (please update [volume.scss](https://github.com/sampotts/plyr/blob/master/src/sass/components/volume.scss))
### v3.7.3
- Fix: force nowrap in progress tooltips (related: #2549) (thanks @raad-altaie!)
- Feat(i18n): Make captions autodetect text direction (#2540) (thanks @ebraminio!)
- Fix: fixed menu border radius bug (#2548) (thanks @raad-altaie!)
- Chore: navigator.platform is deprecated (#2530) (thanks @stamat!)
- Feat: Added configurable property to elements for re-use (#2489) (thanks @NoirHusky!)
- Docs: Replace example video ID with one that still works (#2518) (thanks @luvejo!)
- Fix: Improve accessibility on control buttons with aria-pressed (#2523) (thanks @emilkarl!)
- Fix: Fix for calc() in newer Dart Sass versions (#2519) (thanks @ckhicks!)
- Fix: simplify logic for isFunction assertion method
- Chore: update types to include string for controls
- Chore: upgrade packages
- Chore: use `.node-version` instead of `.nvmrc`
### v3.7.2
- Fix: Add `@babel/plugin-proposal-optional-chaining` to transform optional chaining in build output
### v3.7.1
- Feat: Minor styling improvements to the preview thumbnails (🚨 Requires a SCSS/CSS update 🚨)
- Fix: Fix invalid CSS @charset rule in Sass files (thanks @Hashen110!)
- Chore: Replace deprecated KeyboardEvent `keyCode` references to use `key` instead (thanks @Hashen110!)
- Various other code clean up and typo fixes (thanks @Hashen110!)
## v3.7.0
- Feat: Add markers support (🚨 Requires a SCSS/CSS update 🚨) (thanks @ForeverSc and @fengshuo!)
@ -659,10 +604,10 @@ Because we're using the fancy new ES6 syntax, you will need to polyfill for vint
- Vimeo controls fix (fixes #697)
- SVG4everybody compatibility fix
- Allow Plyr.setup event listeners to be set up as separate event listeners (<https://github.com/sampotts/plyr/pull/703>)
- Added title to the layer html template (for custom controls) (<https://github.com/sampotts/plyr/pull/649>)
- Target is null bug fix (<https://github.com/sampotts/plyr/pull/617>)
- fix #684 memory leaks issues after destroy (<https://github.com/sampotts/plyr/pull/700>)
- Allow Plyr.setup event listeners to be set up as separate event listeners (https://github.com/sampotts/plyr/pull/703)
- Added title to the layer html template (for custom controls) (https://github.com/sampotts/plyr/pull/649)
- Target is null bug fix (https://github.com/sampotts/plyr/pull/617)
- fix #684 memory leaks issues after destroy (https://github.com/sampotts/plyr/pull/700)
### v2.0.16
@ -682,8 +627,8 @@ Because we're using the fancy new ES6 syntax, you will need to polyfill for vint
### v2.0.12
- Ability to set custom `blankUrl` for source changes (<https://github.com/sampotts/plyr/pull/504>)
- Ability to set caption button listener (<https://github.com/sampotts/plyr/pull/468>)
- Ability to set custom `blankUrl` for source changes (https://github.com/sampotts/plyr/pull/504)
- Ability to set caption button listener (https://github.com/sampotts/plyr/pull/468)
### v2.0.11

107
README.md
View File

@ -7,7 +7,7 @@ Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vi
[![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)
[![Screenshot of Plyr](https://cdn.plyr.io/static/screenshot.webp)](https://plyr.io)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io)
# Features
@ -32,7 +32,7 @@ Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vi
- 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required
- 💁‍♀️ **Sass** - to include in your build processes
## Demos
### Demos
You can try Plyr in Codepen using our minimal templates: [HTML5 video](https://codepen.io/pen?template=bKeqpr), [HTML5 audio](https://codepen.io/pen?template=rKLywR), [YouTube](https://codepen.io/pen?template=GGqbbJ), [Vimeo](https://codepen.io/pen?template=bKeXNq). For Streaming we also have example integrations with: [Dash.js](https://codepen.io/pen?template=GRoogML), [Hls.js](https://codepen.io/pen?template=oyLKQb) and [Shaka Player](https://codepen.io/pen?template=ZRpzZO)
@ -117,7 +117,7 @@ Or the `<div>` non progressively enhanced method:
You can use Plyr as an ES6 module as follows:
```js
```javascript
import Plyr from 'plyr';
const player = new Plyr('#player');
@ -132,18 +132,18 @@ Alternatively you can include the `plyr.js` script before the closing `</body>`
</script>
```
See [initialising](#initializing) for more information on advanced setups.
See [initialising](#initialising) for more information on advanced setups.
You can use our CDN (provided by [Cloudflare](https://www.cloudflare.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills separately 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 separately as part of your application but to make life easier you can use the polyfilled build.
```html
<script src="https://cdn.plyr.io/3.7.8/plyr.js"></script>
<script src="https://cdn.plyr.io/3.7.0/plyr.js"></script>
```
...or...
```html
<script src="https://cdn.plyr.io/3.7.8/plyr.polyfilled.js"></script>
<script src="https://cdn.plyr.io/3.7.0/plyr.polyfilled.js"></script>
```
## CSS
@ -154,24 +154,16 @@ Include the `plyr.css` stylesheet into your `<head>`.
<link rel="stylesheet" href="path/to/plyr.css" />
```
If you want to use our CDN (provided by [Cloudflare](https://www.cloudflare.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
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.0/plyr.css" />
```
## SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Cloudflare](https://www.cloudflare.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.7.8/plyr.svg`.
### Self hosting
If you don't want to create a build system to include Plyr as an npm module, you can use the pre-built files. You have a few options:
- Download the files from the CDN links above, they're already minified.
- Download the files from [unpkg](https://unpkg.com/browse/plyr/dist/) or similar services.
- Build the project yourself using `npm i && npm run build`, which installs the dependencies and spits out a build to `dist`.
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.7.0/plyr.svg`.
# Ads
@ -197,10 +189,11 @@ Here's a list of the properties and what they are used for:
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `--plyr-color-main` | The primary UI color. | ![#f03c15](https://place-hold.it/15/00b3ff/000000?text=+) `#00b3ff` |
| `--plyr-video-background` | The background color of video and poster wrappers for using alpha channel videos and poster images. | `rgba(0, 0, 0, 1)` |
| `--plyr-focus-visible-color` | The color used for the focus styles when an element is `:focus-visible` (keyboard focused). | `--plyr-color-main` |
| `--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://place-hold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-badge-text-color` | The text color for badges. | ![#ffffff](https://place-hold.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://place-hold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-control-icon-size` | The size of the icons used in the controls. | `18px` |
@ -317,7 +310,7 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
## JavaScript
### Initializing
### Initialising
You can specify a range of arguments for the constructor to use:
@ -331,17 +324,17 @@ _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element
Passing a CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector):
```js
```javascript
const player = new Plyr('#player');
```
Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement):
```js
```javascript
const player = new Plyr(document.getElementById('player'));
```
```js
```javascript
const player = new Plyr(document.querySelector('.js-player'));
```
@ -351,13 +344,13 @@ The HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<
You have two choices here. You can either use a simple array loop to map the constructor:
```js
```javascript
const players = Array.from(document.querySelectorAll('.js-player')).map((p) => new Plyr(p));
```
...or use a static method where you can pass a [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement), or a [JQuery](https://jquery.com) object:
```js
```javascript
const players = Plyr.setup('.js-player');
```
@ -367,7 +360,7 @@ Both options will also return an array of instances in the order of they were in
The second argument for the constructor is the [options](#options) object:
```js
```javascript
const player = new Plyr('#player', {
title: 'Example Title',
});
@ -382,7 +375,7 @@ Options can be passed as an object to the constructor as above or as JSON in `da
Note the single quotes encapsulating the JSON and double quotes on the object keys. Only string values need double quotes.
| Option | Type | Default | Description |
| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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 |
| `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. |
@ -394,7 +387,6 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `blankVideo` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
| `autoplay`&sup2; | Boolean | `false` | Autoplay the media on load. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
| `autopause`&sup1; | Boolean | `true` | Only allow one player playing at once. |
| `playsinline`&sup3; | Boolean | `true` | Allow inline playback on iOS. Note this has no effect on iPadOS. |
| `seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind. |
| `volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player. |
| `muted` | Boolean | `false` | Whether to start playback muted. If the `muted` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
@ -410,7 +402,7 @@ 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. |
| `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 non-selectable language options). |
| `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) - note this has no effect on iPadOS. `container`: A selector for an ancestor of the player element, allows contextual content to remain visual in fullscreen mode. Non-ancestors are ignored. |
| `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. |
| `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, 4] }` | `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. |
@ -427,14 +419,9 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
1. Vimeo only
2. Autoplay is generally not recommended as it is seen as a negative user experience. It is also disabled in many browsers. Before raising issues, do your homework. More info can be found here:
- <https://webkit.org/blog/6784/new-video-policies-for-ios/>
- <https://developers.google.com/web/updates/2017/09/autoplay-policy-changes>
- <https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/>
3. YouTube does not support programatically toggling the native fullscreen player via it's API. This means on iOS you have two options, neither being perfect:
- Use the fallback/faux fullscreen option which covers the whole viewport (this is the default)
- Set `playsinline` to `false` and/or `fullscreen.iosNative` to `true` - either option hides the fullscreen toggle in the UI (because of the above API issue) and means iOS will play the video in it's native player.
- https://webkit.org/blog/6784/new-video-policies-for-ios/
- https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
- https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/
# API
@ -444,7 +431,7 @@ There are methods, setters and getters on a Plyr object.
The easiest way to access the Plyr object is to set the return value from your call to the constructor to a variable. For example:
```js
```javascript
const player = new Plyr('#player', {
/* options */
});
@ -452,7 +439,7 @@ const player = new Plyr('#player', {
You can also access the object through any events:
```js
```javascript
element.addEventListener('ready', (event) => {
const player = event.detail.plyr;
});
@ -462,7 +449,7 @@ element.addEventListener('ready', (event) => {
Example method use:
```js
```javascript
player.play(); // Start playback
player.fullscreen.enter(); // Enter fullscreen
```
@ -497,14 +484,14 @@ player.fullscreen.enter(); // Enter fullscreen
Example setters:
```js
```javascript
player.volume = 0.5; // Sets volume at 50%
player.currentTime = 10; // Seeks to 10 seconds
```
Example getters:
```js
```javascript
player.volume; // 0.5;
player.currentTime; // 10
player.fullscreen.active; // false;
@ -548,7 +535,7 @@ This allows changing the player source and type on the fly.
Video example:
```js
```javascript
player.source = {
type: 'video',
title: 'Example title',
@ -588,7 +575,7 @@ player.source = {
Audio example:
```js
```javascript
player.source = {
type: 'audio',
title: 'Example title',
@ -607,7 +594,7 @@ player.source = {
YouTube example:
```js
```javascript
player.source = {
type: 'video',
sources: [
@ -621,12 +608,12 @@ player.source = {
Vimeo example
```js
```javascript
player.source = {
type: 'video',
sources: [
{
src: '76979871',
src: '143418951',
provider: 'vimeo',
},
],
@ -652,7 +639,7 @@ You can listen for events on the target element you setup Plyr on (see example u
reference to the instance, you can use the `on()` API method or `addEventListener()`. Access to the API can be obtained this way through the `event.detail.plyr`
property. Here's an example:
```js
```javascript
player.on('ready', (event) => {
const instance = event.detail.plyr;
});
@ -774,20 +761,21 @@ Plyr uses ES6 which isn't supported in all browsers quite yet. This means some f
You can use the static method to check for support. For example
```js
const supported = Plyr.supported('video', 'html5');
```javascript
const supported = Plyr.supported('video', 'html5', true);
```
The arguments are:
- Media type (`'audio' | 'video'`)
- Provider (`'html5' | 'youtube' | 'vimeo'`)
- Media type (`audio` or `video`)
- Provider (`html5`, `youtube` or `vimeo`)
- Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
## Disable support programmatically
The `enabled` option can be used to disable certain User Agents. For example, if you don't want to use Plyr for smartphones, you could use:
```js
```javascript
{
enabled: !/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
}
@ -857,8 +845,6 @@ Plyr costs money to run, not only my time. I donate my time for free as I enjoy
- [pressakey.com | Blog-Magazin für Videospiele](https://pressakey.com)
- [STROLLÿN: Work with a View](https://strollyn.com)
- [CFDA Runway360](https://runway360.cfda.com/)
- [NKLAV | Filmmaker](https://nklav.com)
- [GDI.JS.ORG - Google Drive Index](https://gitlab.com/GoogleDriveIndex/Google-Drive-Index)
If you want to be added to the list, open a pull request. It'd be awesome to see how you're using Plyr 😎
@ -869,8 +855,13 @@ If you want to be added to the list, open a pull request. It'd be awesome to see
# Thanks
- [Cloudflare](https://www.cloudflare.com/) and [Fastly](https://www.fastly.com/) for providing the CDN services.
- [Sentry](https://sentry.io/) for error logging service on the demo website.
[![Fastly](https://cdn.plyr.io/static/fastly-logo.png)](https://www.fastly.com/)
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.
[![Sentry](https://cdn.plyr.io/static/sentry-logo-black.svg)](https://sentry.io/)
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
## Contributors

View File

@ -2,7 +2,7 @@
"version": "0.2",
"ignorePaths": ["package.json", "dist/*", "demo/node_modules/*"],
"dictionaryDefinitions": [],
"dictionaries": ["en-gb", "softwareTerms", "html", "css", "typescript"],
"dictionaries": [],
"words": [
"autopause",
"autoplay",
@ -13,11 +13,9 @@
"fastly",
"fullscreen",
"gordita",
"loadjs",
"magazin",
"menuitemradio",
"noupe",
"otransitionend",
"playsinline",
"plyr",
"rutheneum",

View File

@ -66,46 +66,42 @@
<h1>Pl<span>a</span>y<span>e</span>r</h1>
<p>
A simple, accessible and customisable media player for
<button type="button" class="link" data-source="video">
<button type="button" class="faux-link" data-source="video">
<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>
Video</button
></path></svg
>Video</button
>,
<button type="button" class="link" data-source="audio">
<button type="button" class="faux-link" data-source="audio">
<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>
Audio</button
></path></svg
>Audio</button
>,
<button type="button" class="link" data-source="youtube">
<button type="button" class="faux-link" data-source="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
></path></svg
>YouTube
</button>
and
<button type="button" class="link" data-source="vimeo">
<button type="button" class="faux-link" data-source="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
></path></svg
>Vimeo
</button>
</p>
@ -181,10 +177,7 @@
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"
class="link"
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank"
>View From A Blue Moon</a
>
&copy; Brainfarm
@ -198,7 +191,7 @@
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" class="link"
<a href="http://www.kishibashi.com/" target="_blank"
>Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;</a
>
&copy; Kishi Bashi
@ -222,7 +215,7 @@
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/40648169" target="_blank" class="link">Toob “Wavaphon” Music Video</a>
<a href="https://vimeo.com/40648169" target="_blank">Toob “Wavaphon” Music Video</a>
on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
@ -255,7 +248,7 @@
<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"
target="_blank"
class="link js-shr"
class="js-shr"
>tweet it</a
>
👍

View File

@ -4,6 +4,7 @@
// Please see README.md in the root or github.com/sampotts/plyr
// ==========================================================================
import './tab-focus';
import 'custom-event-polyfill';
import 'url-polyfill';
@ -12,13 +13,13 @@ import Shr from 'shr-buttons';
import Plyr from '../../../src/js/plyr';
import sources from './sources';
import toggleClass from './toggle-class';
(() => {
const production = 'plyr.io';
const isProduction = window.location.host.includes(production);
// Sentry for demo site (https://plyr.io) only
if (isProduction) {
if (window.location.host === production) {
Sentry.init({
dsn: 'https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555',
whitelistUrls: [production].map((d) => new RegExp(`https://(([a-z0-9])+(.))*${d}`)),
@ -52,10 +53,10 @@ import sources from './sources';
captions: {
active: true,
},
/* ads: {
enabled: isProduction,
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'],
@ -106,10 +107,10 @@ import sources from './sources';
function render(type) {
// Remove active classes
Array.from(buttons).forEach((button) => button.parentElement.classList.toggle('active', false));
Array.from(buttons).forEach((button) => toggleClass(button.parentElement, 'active', false));
// Set active on parent
document.querySelector(`[data-source="${type}"]`).classList.toggle('active', true);
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach((cite) => {

31
demo/src/js/tab-focus.js Normal file
View File

@ -0,0 +1,31 @@
// Setup tab focus
const container = document.getElementById('container');
const tabClassName = 'tab-focus';
// Remove class on blur
document.addEventListener('focusout', (event) => {
if (!event.target.classList || container.contains(event.target)) {
return;
}
event.target.classList.remove(tabClassName);
});
// Add classname to tabbed elements
document.addEventListener('keydown', (event) => {
if (event.keyCode !== 9) {
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;
}
focused.classList.add(tabClassName);
}, 10);
});

View File

@ -0,0 +1,5 @@
// Toggle class on an element
const toggleClass = (element, className = '', toggle = false) =>
element && element.classList[toggle ? 'add' : 'remove'](className);
export default toggleClass;

View File

@ -1,9 +1,7 @@
@charset "UTF-8";
// ==========================================================================
// Plyr.io Demo Page
// ==========================================================================
@charset 'UTF-8';
@import '../../../../src/sass/lib/css-vars';
$css-vars-use-native: true;

View File

@ -1,8 +1,7 @@
@charset "UTF-8";
// ==========================================================================
// Plyr.io Error Page
// ==========================================================================
@charset 'UTF-8';
// Settings
@import '../settings/colors';

View File

@ -7,7 +7,8 @@
.button__count {
align-items: center;
border: 0;
border-radius: $border-radius-medium;
border-radius: $border-radius-base;
box-shadow: 0 1px 1px rgba(#000, 0.1);
display: inline-flex;
padding: ($spacing-base * 0.75);
position: relative;
@ -18,49 +19,45 @@
// Buttons
.button {
--shadow-color: 0deg 0% 20%;
align-items: center;
background-color: $color-button-background;
background-image: linear-gradient(0deg, transparent, rgba(255, 255, 255, 0.05));
border: 1px solid darken($color-button-background, 5);
box-shadow: 0 0.8px 1px hsl(var(--shadow-color) / 0.05), 0 1.3px 1.6px -1px hsl(var(--shadow-color) / 0.06),
0 2.8px 3.4px -2px hsl(var(--shadow-color) / 0.07);
background: $color-button-background;
color: $color-button-text;
display: inline-flex;
font-weight: $font-weight-bold;
gap: 0.25rem;
padding-left: ($spacing-base * 1.25);
padding-right: ($spacing-base * 1.25);
text-decoration: none;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
&:hover,
&:focus {
background: $color-button-background-hover;
border-color: darken($color-button-background, 7);
// Remove the underline/border
&::after {
display: none !important;
display: none;
}
}
&:hover {
box-shadow: 0 2px 2px rgba(#000, 0.1);
}
&:focus {
outline: 0;
}
&:focus-visible {
@include focus-visible($color-button-background);
&.tab-focus {
@include tab-focus;
}
&:active {
box-shadow: none;
top: 1px;
}
}
.icon {
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
// Button group
.button--with-count {
display: inline-flex;
.button .icon {
flex-shrink: 0;
}
}
@ -69,21 +66,19 @@
.button__count {
animation: fade-in 0.2s ease;
background: $color-button-count-background;
border: 1px solid $color-gray-100;
color: $color-button-count-text;
margin-left: ($spacing-base * 0.75);
&::before {
background-color: $color-button-count-background;
border: inherit;
border-width: 0 0 1px 1px;
border: $arrow-size solid transparent;
border-left-width: 0;
border-right-color: $color-button-count-background;
content: '';
display: block;
height: 8px;
height: 0;
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%) translateX(50%) translateX(-1px) rotate(45deg);
width: 8px;
transform: translateY(-50%);
width: 0;
}
}

View File

@ -2,9 +2,15 @@
// Links
// ==========================================================================
.link {
align-items: center;
border-bottom: 1px dashed currentColor;
// Make a <button> look like an <a>
button.faux-link {
@extend a; // stylelint-disable-line
@include cancel-button-styles;
}
// Links
a {
border-bottom: 1px dotted currentColor;
color: $color-link;
position: relative;
text-decoration: none;
@ -32,8 +38,8 @@
}
}
&:focus-visible {
@include focus-visible($color-link);
&.tab-focus {
@include tab-focus;
}
&.no-border::after {

View File

@ -6,10 +6,8 @@
// Example players
.plyr {
--shadow-color: 197deg 32% 65%;
border-radius: $border-radius-2x-large;
box-shadow: 0 0.5px 0.6px hsl(var(--shadow-color) / 0.36), 0 1.7px 1.9px -0.8px hsl(var(--shadow-color) / 0.36),
0 4.3px 4.8px -1.7px hsl(var(--shadow-color) / 0.36), -0.1px 10.6px 11.9px -2.5px hsl(var(--shadow-color) / 0.36);
border-radius: $border-radius-large;
box-shadow: 0 2px 15px rgba(#000, 0.1);
margin: $spacing-base auto;
&.plyr--audio {
@ -19,7 +17,6 @@
.plyr__video-wrapper::after {
border: 1px solid rgba(#000, 0.15);
border-bottom-color: rgba(#000, 0.25);
border-radius: inherit;
bottom: 0;
content: '';

View File

@ -37,7 +37,6 @@ main {
aside {
align-items: center;
background: #fff;
box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.05);
display: flex;
flex-shrink: 0;
justify-content: center;
@ -59,8 +58,8 @@ aside {
a {
color: $color-twitter;
&:focus-visible {
@include focus-visible($color-twitter);
&.tab-focus {
@include tab-focus($color-twitter);
}
}
}

View File

@ -4,11 +4,30 @@
@use 'sass:math';
// Convert a <button> into an <a>
// ---------------------------------------
@mixin cancel-button-styles() {
background: transparent;
border: 0;
border-radius: 0;
cursor: pointer;
font: inherit;
line-height: $line-height-base;
margin: 0;
padding: 0;
position: relative;
text-align: inherit;
text-shadow: inherit;
user-select: text;
vertical-align: baseline;
width: auto;
}
// Nicer focus styles
// ---------------------------------------
@mixin focus-visible($color: $focus-default-color) {
outline: 2px dashed $color;
outline-offset: 2px;
@mixin tab-focus($color: $tab-focus-default-color) {
box-shadow: 0 0 0 3px rgba($color, 0.35);
outline: 0;
}
// Use rems for font sizing

View File

@ -9,7 +9,3 @@
*::before {
box-sizing: border-box;
}
button {
all: unset;
}

View File

@ -39,4 +39,4 @@ $color-button-count-background: #fff;
$color-button-count-text: $color-gray-600;
// Focus
$focus-default-color: $color-brand-primary;
$tab-focus-default-color: #fff;

View File

@ -6,9 +6,8 @@
$arrow-size: 5px;
// Radii
$border-radius-small: 4px;
$border-radius-medium: 6px;
$border-radius-2x-large: 12px;
$border-radius-base: 4px;
$border-radius-large: 8px;
// Background
$page-background: linear-gradient(to left top, $color-background-from, $color-background-to);

View File

@ -1,4 +0,0 @@
*:focus-visible {
outline: 2px dotted $color-brand-primary;
outline-offset: 2px;
}

View File

@ -1,6 +1,6 @@
{
"name": "plyr",
"version": "3.7.8",
"version": "3.7.0",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>",
@ -34,25 +34,24 @@
"lint": "eslint src/js && npm run remark && stylelint **/*.scss",
"lint:fix": "eslint --fix src/js && stylelint **/*.scss --fix",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"deploy": "npm run lint && gulp version && gulp build && gulp deploy",
"deploy": "yarn lint && gulp version && gulp build && gulp deploy",
"format": "prettier --write \"./{src,demo/src}/**/*.{js,scss}\"",
"spellcheck": "cspell \"**/*.{js,md,scss,json}\" --no-must-find-files",
"start": "gulp"
},
"devDependencies": {
"@babel/core": "^7.20.2",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
"@babel/preset-env": "^7.20.2",
"@babel/core": "^7.17.9",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/preset-env": "^7.16.11",
"@sampotts/eslint-config": "1.1.7",
"autoprefixer": "^10.4.13",
"aws-sdk": "^2.1256.0",
"autoprefixer": "^10.4.4",
"aws-sdk": "^2.1116.0",
"babel-eslint": "^10.1.0",
"browser-sync": "^2.27.10",
"colorette": "2.0.19",
"cspell": "^6.14.2",
"cssnano": "^5.1.14",
"del": "^6.1.1",
"browser-sync": "^2.27.9",
"colorette": "2.0.16",
"cspell": "^5.19.7",
"cssnano": "^5.1.7",
"del": "^6.0.0",
"eslint": "^7.23.0",
"fancy-log": "^2.0.0",
"git-branch": "^2.0.1",
@ -74,25 +73,25 @@
"gulp-sourcemaps": "^3.0.0",
"gulp-svgstore": "^9.0.0",
"gulp-terser": "^2.1.0",
"postcss": "^8.4.19",
"postcss-custom-properties": "^12.1.9",
"postcss-scss": "^4.0.5",
"postcss": "^8.4.12",
"postcss-custom-properties": "^12.1.7",
"postcss-scss": "^4.0.3",
"prettier-eslint": "^12.0.0",
"prettier-stylelint": "^0.4.2",
"remark-cli": "^11.0.0",
"remark-validate-links": "^12.1.0",
"rollup": "^3.3.0",
"remark-cli": "^10.0.1",
"remark-validate-links": "^11.0.2",
"rollup": "^2.70.2",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"sass": "^1.56.1",
"stylelint": "^14.15.0",
"stylelint-config-prettier": "^9.0.4",
"sass": "^1.50.0",
"stylelint": "^14.7.1",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-sass-guidelines": "^9.0.1",
"stylelint-selector-bem-pattern": "^2.1.1"
},
"dependencies": {
"core-js": "^3.26.1",
"core-js": "^3.22.0",
"custom-event-polyfill": "^1.0.7",
"loadjs": "^4.2.0",
"rangetouch": "^2.0.1",

13882
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,6 @@ const captions = {
// Inject the container
if (!is.element(this.elements.captions)) {
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
this.elements.captions.setAttribute('dir', 'auto');
insertAfter(this.elements.captions, this.elements.wrapper);
}

View File

@ -18,7 +18,8 @@ const defaults = {
// Only allow one media playing at once (vimeo only)
autopause: true,
// Allow inline playback on iOS
// 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
@ -60,7 +61,7 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',
iconUrl: 'https://cdn.plyr.io/3.7.0/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@ -352,6 +353,7 @@ const defaults = {
marker: 'plyr__progress__marker',
hidden: 'plyr__sr-only',
hideControls: 'plyr--hide-controls',
isIos: 'plyr--is-ios',
isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
@ -379,6 +381,7 @@ const defaults = {
supported: 'plyr--airplay-supported',
active: 'plyr--airplay-active',
},
tabFocus: 'plyr__tab-focus',
previewThumbnails: {
// Tooltip thumbs
thumbContainer: 'plyr__preview-thumb',

38
src/js/controls.js vendored
View File

@ -383,7 +383,6 @@ const controls = {
extend(attributes, {
class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),
'aria-label': i18n.get(type, this.config),
role: 'timer',
}),
'00:00',
);
@ -405,7 +404,7 @@ const controls = {
'keydown keyup',
(event) => {
// We only care about space and ⬆️ ⬇️️ ➡️
if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {
if (![32, 38, 39, 40].includes(event.which)) {
return;
}
@ -421,13 +420,13 @@ const controls = {
const isRadioButton = matches(menuItem, '[role="menuitemradio"]');
// Show the respective menu
if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {
if (!isRadioButton && [32, 39].includes(event.which)) {
controls.showMenuPanel.call(this, type, true);
} else {
let target;
if (event.key !== ' ') {
if (event.key === 'ArrowDown' || (isRadioButton && event.key === 'ArrowRight')) {
if (event.which !== 32) {
if (event.which === 40 || (isRadioButton && event.which === 39)) {
target = menuItem.nextElementSibling;
if (!is.element(target)) {
@ -451,7 +450,9 @@ const controls = {
// Enter will fire a `click` event but we still need to manage focus
// So we bind to keyup which fires after and set focus here
on.call(this, menuItem, 'keyup', (event) => {
if (event.key !== 'Return') return;
if (event.which !== 13) {
return;
}
controls.focusFirstMenuItem.call(this, null, true);
});
@ -505,7 +506,7 @@ const controls = {
menuItem,
'click keyup',
(event) => {
if (is.keyboardEvent(event) && event.key !== ' ') {
if (is.keyboardEvent(event) && event.which !== 32) {
return;
}
@ -677,7 +678,7 @@ const controls = {
}
// WebKit only
if (!browser.isWebKit && !browser.isIPadOS) {
if (!browser.isWebkit) {
return;
}
@ -1106,7 +1107,7 @@ const controls = {
},
// Focus the first menu item in a given (or visible) menu
focusFirstMenuItem(pane, focusVisible = false) {
focusFirstMenuItem(pane, tabFocus = false) {
if (this.elements.settings.popup.hidden) {
return;
}
@ -1119,7 +1120,7 @@ const controls = {
const firstItem = target.querySelector('[role^="menuitem"]');
setFocus.call(this, firstItem, focusVisible);
setFocus.call(this, firstItem, tabFocus);
},
// Show/hide menu
@ -1138,7 +1139,7 @@ const controls = {
if (is.boolean(input)) {
show = input;
} else if (is.keyboardEvent(input) && input.key === 'Escape') {
} else if (is.keyboardEvent(input) && input.which === 27) {
show = false;
} else if (is.event(input)) {
// If Plyr is in a shadowDOM, the event target is set to the component, instead of the
@ -1196,7 +1197,7 @@ const controls = {
},
// Show a panel in the menu
showMenuPanel(type = '', focusVisible = false) {
showMenuPanel(type = '', tabFocus = false) {
const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);
// Nothing to show, bail
@ -1247,7 +1248,7 @@ const controls = {
toggleHidden(target, false);
// Focus the first item
controls.focusFirstMenuItem.call(this, target, focusVisible);
controls.focusFirstMenuItem.call(this, target, tabFocus);
},
// Set the download URL
@ -1386,7 +1387,7 @@ const controls = {
// Volume range control
// Ignored on iOS as it's handled globally
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {
if (control === 'volume' && !browser.isIos) {
// Set the attributes
const attributes = {
max: 1,
@ -1526,7 +1527,10 @@ const controls = {
pane,
'keydown',
(event) => {
if (event.key !== 'ArrowLeft') return;
// We only care about <-
if (event.which !== 37) {
return;
}
// Prevent seek
event.preventDefault();
@ -1716,17 +1720,13 @@ const controls = {
if (!is.empty(this.elements.buttons)) {
const addProperty = (button) => {
const className = this.config.classNames.controlPressed;
button.setAttribute('aria-pressed', 'false');
Object.defineProperty(button, 'pressed', {
configurable: true,
enumerable: true,
get() {
return hasClass(button, className);
},
set(pressed = false) {
toggleClass(button, className, pressed);
button.setAttribute('aria-pressed', pressed ? 'true' : 'false');
},
});
};

View File

@ -57,10 +57,12 @@ class Fullscreen {
// Update the UI
this.update();
// this.toggle = this.toggle.bind(this);
}
// Determine if native supported
static get nativeSupported() {
static get native() {
return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
@ -70,14 +72,16 @@ class Fullscreen {
}
// If we're actually using native
get useNative() {
return Fullscreen.nativeSupported && !this.forceFallback;
get usingNative() {
return Fullscreen.native && !this.forceFallback;
}
// Get the prefix for handlers
static get prefix() {
// No prefix
if (is.function(document.exitFullscreen)) return '';
if (is.function(document.exitFullscreen)) {
return '';
}
// Check for fullscreen support by vendor prefix
let value = '';
@ -99,30 +103,24 @@ class Fullscreen {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
}
// Determine if fullscreen is supported
get supported() {
return [
// Fullscreen is enabled in config
this.player.config.fullscreen.enabled,
// Must be a video
this.player.isVideo,
// Either native is supported or fallback enabled
Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,
// YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline
// must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback
!this.player.isYouTube ||
Fullscreen.nativeSupported ||
!browser.isIos ||
(this.player.config.playsinline && !this.player.config.fullscreen.iosNative),
].every(Boolean);
// 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.supported) return false;
if (!this.enabled) {
return false;
}
// Fallback using classname
if (!Fullscreen.nativeSupported || this.forceFallback) {
if (!Fullscreen.native || this.forceFallback) {
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
@ -137,11 +135,13 @@ class Fullscreen {
get target() {
return browser.isIos && this.player.config.fullscreen.iosNative
? this.player.media
: this.player.elements.fullscreen ?? this.player.elements.container;
: this.player.elements.fullscreen || this.player.elements.container;
}
onChange = () => {
if (!this.supported) return;
if (!this.enabled) {
return;
}
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
@ -159,8 +159,8 @@ class Fullscreen {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX ?? 0,
y: window.scrollY ?? 0,
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
@ -188,7 +188,10 @@ class Fullscreen {
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) viewport.content += `,${property}`;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
@ -203,8 +206,10 @@ class Fullscreen {
// Trap focus inside container
trapFocus = (event) => {
// Bail if iOS/iPadOS, not active, not the tab key
if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;
// 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;
@ -225,12 +230,16 @@ class Fullscreen {
// Update UI
update = () => {
if (this.supported) {
if (this.enabled) {
let mode;
if (this.forceFallback) mode = 'Fallback (forced)';
else if (Fullscreen.nativeSupported) mode = 'Native';
else mode = 'Fallback';
if (this.forceFallback) {
mode = 'Fallback (forced)';
} else if (Fullscreen.native) {
mode = 'Native';
} else {
mode = 'Fallback';
}
this.player.debug.log(`${mode} fullscreen enabled`);
} else {
@ -238,12 +247,14 @@ class Fullscreen {
}
// Add styling hook to show button
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
};
// Make an element fullscreen
enter = () => {
if (!this.supported) return;
if (!this.enabled) {
return;
}
// iOS native fullscreen doesn't need the request step
if (browser.isIos && this.player.config.fullscreen.iosNative) {
@ -252,7 +263,7 @@ class Fullscreen {
} else {
this.target.webkitEnterFullscreen();
}
} else if (!Fullscreen.nativeSupported || this.forceFallback) {
} else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(true);
} else if (!this.prefix) {
this.target.requestFullscreen({ navigationUI: 'hide' });
@ -263,17 +274,15 @@ class Fullscreen {
// Bail from fullscreen
exit = () => {
if (!this.supported) return;
if (!this.enabled) {
return;
}
// iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) {
if (this.player.isVimeo) {
this.player.embed.exitFullscreen();
} else {
this.target.webkitEnterFullscreen();
}
this.target.webkitExitFullscreen();
silencePromise(this.player.play());
} else if (!Fullscreen.nativeSupported || this.forceFallback) {
} else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
@ -285,8 +294,11 @@ class Fullscreen {
// Toggle state
toggle = () => {
if (!this.active) this.enter();
else this.exit();
if (!this.active) {
this.enter();
} else {
this.exit();
}
};
}

View File

@ -21,6 +21,7 @@ class Listeners {
this.handleKey = this.handleKey.bind(this);
this.toggleMenu = this.toggleMenu.bind(this);
this.setTabFocus = this.setTabFocus.bind(this);
this.firstTouch = this.firstTouch.bind(this);
}
@ -28,25 +29,25 @@ class Listeners {
handleKey(event) {
const { player } = this;
const { elements } = player;
const { key, type, altKey, ctrlKey, metaKey, shiftKey } = event;
const pressed = type === 'keydown';
const repeat = pressed && key === this.lastKey;
const code = event.keyCode ? event.keyCode : event.which;
const pressed = event.type === 'keydown';
const repeat = pressed && code === this.lastKey;
// Bail if a modifier key is set
if (altKey || ctrlKey || metaKey || shiftKey) {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
// If the event is bubbled from the media element
// Firefox doesn't get the key for whatever reason
if (!key) {
// Firefox doesn't get the keycode for whatever reason
if (!is.number(code)) {
return;
}
// Seek by increment
const seekByIncrement = (increment) => {
// Seek by the number keys
const seekByKey = () => {
// Divide the max duration into 10th's and times by the number value
player.currentTime = (player.duration / 10) * increment;
player.currentTime = (player.duration / 10) * (code - 48);
};
// Handle the key on keydown
@ -64,114 +65,113 @@ class Listeners {
return;
}
if (event.key === ' ' && matches(focused, 'button, [role^="menuitem"]')) {
if (event.which === 32 && matches(focused, 'button, [role^="menuitem"]')) {
return;
}
}
// Which keys should we prevent default
const preventDefault = [
' ',
'ArrowLeft',
'ArrowUp',
'ArrowRight',
'ArrowDown',
// Which keycodes should we prevent default
const preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79];
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'c',
'f',
'k',
'l',
'm',
];
// If the key is found prevent default (e.g. prevent scrolling for arrows)
if (preventDefault.includes(key)) {
// If the code is found prevent default (e.g. prevent scrolling for arrows)
if (preventDefault.includes(code)) {
event.preventDefault();
event.stopPropagation();
}
switch (key) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
switch (code) {
case 48:
case 49:
case 50:
case 51:
case 52:
case 53:
case 54:
case 55:
case 56:
case 57:
// 0-9
if (!repeat) {
seekByIncrement(parseInt(key, 10));
seekByKey();
}
break;
case ' ':
case 'k':
case 32:
case 75:
// Space and K key
if (!repeat) {
silencePromise(player.togglePlay());
}
break;
case 'ArrowUp':
case 38:
// Arrow up
player.increaseVolume(0.1);
break;
case 'ArrowDown':
case 40:
// Arrow down
player.decreaseVolume(0.1);
break;
case 'm':
case 77:
// M key
if (!repeat) {
player.muted = !player.muted;
}
break;
case 'ArrowRight':
case 39:
// Arrow forward
player.forward();
break;
case 'ArrowLeft':
case 37:
// Arrow back
player.rewind();
break;
case 'f':
case 70:
// F key
player.fullscreen.toggle();
break;
case 'c':
case 67:
// C key
if (!repeat) {
player.toggleCaptions();
}
break;
case 'l':
case 76:
// L key
player.loop = !player.loop;
break;
/* case 73:
this.setLoop('start');
break;
case 76:
this.setLoop();
break;
case 79:
this.setLoop('end');
break; */
default:
break;
}
// Escape is handle natively when in full screen
// So we only need to worry about non native
if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {
if (code === 27 && !player.fullscreen.usingNative && player.fullscreen.active) {
player.fullscreen.toggle();
}
// Store last key for next cycle
this.lastKey = key;
// Store last code for next cycle
this.lastKey = code;
} else {
this.lastKey = null;
}
@ -193,6 +193,56 @@ class Listeners {
toggleClass(elements.container, player.config.classNames.isTouch, true);
};
setTabFocus = (event) => {
const { player } = this;
const { elements } = player;
clearTimeout(this.focusTimer);
// Ignore any key other than tab
if (event.type === 'keydown' && event.which !== 9) {
return;
}
// Store reference to event timeStamp
if (event.type === 'keydown') {
this.lastKeyDown = event.timeStamp;
}
// Remove current classes
const removeCurrent = () => {
const className = player.config.classNames.tabFocus;
const current = getElements.call(player, `.${className}`);
toggleClass(current, className, false);
};
// Determine if a key was pressed to trigger this event
const wasKeyDown = event.timeStamp - this.lastKeyDown <= 20;
// Ignore focus events if a key was pressed prior
if (event.type === 'focus' && !wasKeyDown) {
return;
}
// Remove all current
removeCurrent();
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
if (event.type !== 'focusout') {
this.focusTimer = setTimeout(() => {
const focused = document.activeElement;
// Ignore if current focus element isn't inside the player
if (!elements.container.contains(focused)) {
return;
}
toggleClass(document.activeElement, player.config.classNames.tabFocus, true);
}, 10);
}
};
// Global window & document listeners
global = (toggle = true) => {
const { player } = this;
@ -207,6 +257,9 @@ class Listeners {
// Detect touch by events
once.call(player, document.body, 'touchstart', this.firstTouch);
// Tab focus detection
toggleListener.call(player, document.body, 'keydown focus blur focusout', this.setTabFocus, toggle, false, true);
};
// Container listeners
@ -608,12 +661,15 @@ class Listeners {
elements.buttons.settings,
'keyup',
(event) => {
if (![' ', 'Enter'].includes(event.key)) {
const code = event.which;
// We only care about space and return
if (![13, 32].includes(code)) {
return;
}
// Because return triggers a click anyway, all we need to do is set focus
if (event.key === 'Enter') {
if (code === 13) {
controls.focusFirstMenuItem.call(player, null, true);
return;
}
@ -633,7 +689,7 @@ class Listeners {
// Escape closes menu
this.bind(elements.settings.menu, 'keydown', (event) => {
if (event.key === 'Escape') {
if (event.which === 27) {
controls.toggleMenu.call(player, event);
}
});
@ -648,9 +704,10 @@ class Listeners {
// Pause while seeking
this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', (event) => {
const seek = event.currentTarget;
const code = event.keyCode ? event.keyCode : event.which;
const attribute = 'play-on-seeked';
if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {
if (is.keyboardEvent(event) && code !== 39 && code !== 37) {
return;
}
@ -742,7 +799,7 @@ class Listeners {
});
// Polyfill for lower fill in <input type="range"> for webkit
if (browser.isWebKit) {
if (browser.isWebkit) {
Array.from(getElements.call(player, 'input[type="range"]')).forEach((element) => {
this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));
});
@ -826,7 +883,7 @@ class Listeners {
elements.inputs.volume,
'wheel',
(event) => {
// Detect "natural" scroll - supported on OS X Safari only
// Detect "natural" scroll - suppored on OS X Safari only
// Other browsers on OS X will be inverted until support improves
const inverted = event.webkitDirectionInvertedFromDevice;
// Get delta from event. Invert if `inverted` is true

View File

@ -240,7 +240,7 @@ class Ads {
/**
* This method is called whenever the ads are ready inside the AdDisplayContainer
* @param {Event} event - adsManagerLoadedEvent
* @param {Event} adsManagerLoadedEvent
*/
onAdsManagerLoaded = (event) => {
// Load could occur after a source change (race condition)
@ -581,7 +581,6 @@ class Ads {
/**
* Handles callbacks after an ad event was invoked
* @param {String} event - Event type
* @param args
*/
trigger = (event, ...args) => {
const handlers = this.events[event];

View File

@ -2,7 +2,6 @@ import { createElement } from '../utils/elements';
import { once } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import { clamp } from '../utils/numbers';
import { formatTime } from '../utils/time';
// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg"
@ -110,7 +109,9 @@ class PreviewThumbnails {
this.player.elements.display.seekTooltip.hidden = this.enabled;
}
if (!this.enabled) return;
if (!this.enabled) {
return;
}
this.getThumbnails().then(() => {
if (!this.enabled) {
@ -123,9 +124,6 @@ class PreviewThumbnails {
// Check to see if thumb container size was specified manually in CSS
this.determineContainerAutoSizing();
// Set up listeners
this.listeners();
this.loaded = true;
});
};
@ -207,12 +205,18 @@ class PreviewThumbnails {
};
startMove = (event) => {
if (!this.loaded) return;
if (!this.loaded) {
return;
}
if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;
if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) {
return;
}
// Wait until media has a duration
if (!this.player.media.duration) return;
if (!this.player.media.duration) {
return;
}
if (event.type === 'touchmove') {
// Calculate seek hover position as approx video seconds
@ -387,7 +391,7 @@ class PreviewThumbnails {
}
});
// Only proceed if either thumb num or thumbfilename has changed
// Only proceed if either thumbnum or thumbfilename has changed
if (thumbNum !== this.showingThumb) {
this.showingThumb = thumbNum;
this.loadImage(qualityIndex);
@ -558,7 +562,11 @@ class PreviewThumbnails {
};
get currentImageContainer() {
return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;
if (this.mouseDown) {
return this.elements.scrubbing.container;
}
return this.elements.thumb.imageContainer;
}
get usingSprites() {
@ -591,7 +599,11 @@ class PreviewThumbnails {
}
get currentImageElement() {
return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;
if (this.mouseDown) {
return this.currentScrubbingImageElement;
}
return this.currentThumbnailImageElement;
}
set currentImageElement(element) {
@ -631,39 +643,46 @@ class PreviewThumbnails {
// Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS
setThumbContainerSizeAndPos = () => {
const { imageContainer } = this.elements.thumb;
if (!this.sizeSpecifiedInCSS) {
const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);
imageContainer.style.height = `${this.thumbContainerHeight}px`;
imageContainer.style.width = `${thumbWidth}px`;
} else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {
const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);
imageContainer.style.width = `${thumbWidth}px`;
} else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {
const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);
imageContainer.style.height = `${thumbHeight}px`;
this.elements.thumb.imageContainer.style.height = `${this.thumbContainerHeight}px`;
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
} else if (
this.elements.thumb.imageContainer.clientHeight > 20 &&
this.elements.thumb.imageContainer.clientWidth < 20
) {
const thumbWidth = Math.floor(this.elements.thumb.imageContainer.clientHeight * this.thumbAspectRatio);
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
} else if (
this.elements.thumb.imageContainer.clientHeight < 20 &&
this.elements.thumb.imageContainer.clientWidth > 20
) {
const thumbHeight = Math.floor(this.elements.thumb.imageContainer.clientWidth / this.thumbAspectRatio);
this.elements.thumb.imageContainer.style.height = `${thumbHeight}px`;
}
this.setThumbContainerPos();
};
setThumbContainerPos = () => {
const scrubberRect = this.player.elements.progress.getBoundingClientRect();
const containerRect = this.player.elements.container.getBoundingClientRect();
const seekbarRect = this.player.elements.progress.getBoundingClientRect();
const plyrRect = this.player.elements.container.getBoundingClientRect();
const { container } = this.elements.thumb;
// Find the lowest and highest desired left-position, so we don't slide out the side of the video container
const min = containerRect.left - scrubberRect.left + 10;
const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;
const minVal = plyrRect.left - seekbarRect.left + 10;
const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10;
// Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;
const clamped = clamp(position, min, max);
let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2;
// Move the popover position
container.style.left = `${clamped}px`;
if (previewPos < minVal) {
previewPos = minVal;
}
// The arrow can follow the cursor
container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);
if (previewPos > maxVal) {
previewPos = maxVal;
}
container.style.left = `${previewPos}px`;
};
// Can't use 100% width, in case the video is a different aspect ratio to the video container
@ -678,7 +697,9 @@ class PreviewThumbnails {
// Sprites need to be offset to the correct location
setImageSizeAndOffset = (previewImage, frame) => {
if (!this.usingSprites) return;
if (!this.usingSprites) {
return;
}
// Find difference between height and preview container height
const multiplier = this.thumbContainerHeight / frame.h;

View File

@ -113,7 +113,7 @@ const vimeo = {
autoplay: player.autoplay,
muted: player.muted,
gesture: 'media',
playsinline: player.config.playsinline,
playsinline: !this.config.fullscreen.iosNative,
// hash has to be added to iframe-URL
...hashParam,
...frameParams,
@ -265,7 +265,7 @@ const vimeo = {
set(input) {
const toggle = is.boolean(input) ? input : false;
player.embed.setMuted(toggle ? true : player.config.muted).then(() => {
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle;
triggerEvent.call(player, player.media, 'volumechange');
});

View File

@ -131,7 +131,7 @@ const youtube = {
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) // Highest quality and un-padded
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))
@ -161,7 +161,7 @@ const youtube = {
// Disable keyboard as we handle it
disablekb: 1,
// Allow iOS inline playback
playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,
playsinline: !player.config.fullscreen.iosNative ? 1 : 0,
// Captions are flaky on YouTube
cc_load_policy: player.captions.active ? 1 : 0,
cc_lang_pref: player.config.captions.language,
@ -183,7 +183,7 @@ const youtube = {
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 occurred';
}[code] || 'An unknown error occured';
player.media.error = { code, message };

27
src/js/plyr.d.ts vendored
View File

@ -212,7 +212,7 @@ declare class Plyr {
airplay(): void;
/**
* Sets the preview thumbnails for the current source.
* Sets the preview thubmnails for the current source.
*/
setPreviewThumbnails(source: Plyr.PreviewThumbnailsOptions): void;
@ -272,8 +272,8 @@ declare namespace Plyr {
controlsshown: PlyrEvent;
ready: PlyrEvent;
};
// For retrocompatibility, we keep StandardEvent
type StandardEvent = keyof Plyr.StandardEventMap;
// For retrocompatibility, we keep StandadEvent
type StandadEvent = keyof Plyr.StandardEventMap;
type Html5EventMap = {
loadstart: PlyrEvent;
loadeddata: PlyrEvent;
@ -341,7 +341,7 @@ declare namespace Plyr {
* id (the unique id for the player), seektime (the seektime step in seconds), and title (the media title). See CONTROLS.md for more info on how the html needs to be structured.
* Defaults to ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']
*/
controls?: string | string[] | ((id: string, seektime: number, title: string) => unknown) | Element;
controls?: string[] | ((id: string, seektime: number, title: string) => unknown) | Element;
/**
* If you're using the default controls are used then you can specify which settings to show in the menu
@ -459,7 +459,7 @@ declare namespace Plyr {
* 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?: { [key: string]: (error: PlyrEvent) => void };
listeners?: {[key: string]: (error: PlyrEvent) => void};
/**
* active: Toggles if captions should be active by default. language: Sets the default language to load (if available). 'auto' uses the browser language.
@ -523,11 +523,6 @@ declare namespace Plyr {
* Media Metadata Options.
*/
mediaMetadata?: MediaMetadataOptions;
/**
* Markers Options
*/
markers?: MarkersOptions;
}
interface QualityOptions {
@ -599,16 +594,6 @@ declare namespace Plyr {
artwork?: MediaMetadataArtwork[];
}
interface MarkersPoints {
time: number;
label: string;
}
interface MarkersOptions {
enabled: boolean;
points: MarkersPoints[];
}
export interface Elements {
buttons: {
airplay?: HTMLButtonElement;
@ -700,7 +685,7 @@ declare namespace Plyr {
}
interface PlyrEvent extends CustomEvent {
readonly detail: { readonly plyr: Plyr };
readonly detail: {readonly plyr: Plyr};
}
enum YoutubeState {

View File

@ -1,6 +1,6 @@
// ==========================================================================
// Plyr
// plyr.js v3.7.8
// plyr.js v3.7.0
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
@ -246,7 +246,7 @@ class Plyr {
}
// Check for support again but with type
this.supported = support.check(this.type, this.provider);
this.supported = support.check(this.type, this.provider, this.config.playsinline);
// If no support for even API, bail
if (!this.supported.api) {
@ -267,7 +267,7 @@ class Plyr {
// Wrap media
if (!is.element(this.elements.container)) {
this.elements.container = createElement('div');
this.elements.container = createElement('div', { tabindex: 0 });
wrap(this.media, this.elements.container);
}
@ -649,7 +649,7 @@ class Plyr {
/**
* Set playback speed
* @param {Number} input - the speed of playback (0.5-2.0)
* @param {Number} speed - the speed of playback (0.5-2.0)
*/
set speed(input) {
let speed = null;
@ -933,7 +933,8 @@ class Plyr {
* @param {Boolean} input - Whether to autoplay or not
*/
set autoplay(input) {
this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;
const toggle = is.boolean(input) ? input : this.config.autoplay;
this.config.autoplay = toggle;
}
/**
@ -953,7 +954,7 @@ class Plyr {
/**
* Set the caption track by index
* @param {Number} input - Caption index
* @param {Number} - Caption index
*/
set currentTrack(input) {
captions.set.call(this, input, false);
@ -971,7 +972,7 @@ class Plyr {
/**
* Set the wanted language for captions
* Since tracks can be added later it won't update the actual caption track until there is a matching track
* @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)
* @param {String} - Two character ISO language code (e.g. EN, FR, PT, etc)
*/
set language(input) {
captions.setLanguage.call(this, input, false);
@ -1032,7 +1033,7 @@ class Plyr {
}
/**
* Sets the preview thumbnails for the current source
* Sets the preview thubmnails for the current source
*/
setPreviewThumbnails(thumbnailSource) {
if (this.previewThumbnails && this.previewThumbnails.loaded) {
@ -1239,9 +1240,10 @@ class Plyr {
* Check for support
* @param {String} type - Player type (audio/video)
* @param {String} provider - Provider (html5/youtube/vimeo)
* @param {Boolean} inline - Where player has `playsinline` sttribute
*/
static supported(type, provider) {
return support.check(type, provider);
static supported(type, provider, inline) {
return support.check(type, provider, inline);
}
/**

View File

@ -1,6 +1,6 @@
// ==========================================================================
// Plyr Polyfilled Build
// plyr.js v3.7.8
// plyr.js v3.7.0
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================

View File

@ -24,9 +24,10 @@ const support = {
// Check for support
// Basic functionality vs full UI
check(type, provider) {
check(type, provider, playsinline) {
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
const api = support[type] || provider !== 'html5';
const ui = api && support.rangeInput;
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
return {
api,
@ -37,9 +38,6 @@ const support = {
// Picture-in-picture support
// Safari & Chrome only currently
pip: (() => {
// While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them
// It will throw the following error when trying to enter picture-in-picture
// `NotSupportedError: The Picture-in-Picture mode is not supported.`
if (browser.isIPhone) {
return false;
}

View File

@ -5,6 +5,7 @@
import captions from './captions';
import controls from './controls';
import support from './support';
import browser from './utils/browser';
import { getElement, toggleClass } from './utils/elements';
import { ready, triggerEvent } from './utils/events';
import i18n from './utils/i18n';
@ -97,6 +98,9 @@ const ui = {
// Check for airplay support
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);

View File

@ -3,19 +3,14 @@
// Unfortunately, due to mixed support, UA sniffing is required
// ==========================================================================
const isIE = Boolean(window.document.documentMode);
const isEdge = /Edge/g.test(navigator.userAgent);
const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);
const isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
// navigator.platform may be deprecated but this check is still required
const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
const isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
export default {
isIE,
isEdge,
isWebKit,
isIPhone,
isIPadOS,
isIos,
const browser = {
isIE: Boolean(window.document.documentMode),
isEdge: window.navigator.userAgent.includes('Edge'),
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
isIos:
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) ||
/(iPad|iPhone|iPod)/gi.test(navigator.platform),
};
export default browser;

View File

@ -37,7 +37,9 @@ export function wrap(elements, wrapper) {
// Set attributes
export function setAttributes(element, attributes) {
if (!is.element(element) || is.empty(attributes)) return;
if (!is.element(element) || is.empty(attributes)) {
return;
}
// Assume null and undefined attributes should be left out,
// Setting them would otherwise convert them to "null" and "undefined"
@ -65,16 +67,20 @@ export function createElement(type, attributes, text) {
return element;
}
// Insert an element after another
// Inaert an element after another
export function insertAfter(element, target) {
if (!is.element(element) || !is.element(target)) return;
if (!is.element(element) || !is.element(target)) {
return;
}
target.parentNode.insertBefore(element, target.nextSibling);
}
// Insert a DocumentFragment
export function insertElement(type, parent, attributes, text) {
if (!is.element(parent)) return;
if (!is.element(parent)) {
return;
}
parent.appendChild(createElement(type, attributes, text));
}
@ -95,7 +101,9 @@ export function removeElement(element) {
// Remove all child elements
export function emptyElement(element) {
if (!is.element(element)) return;
if (!is.element(element)) {
return;
}
let { length } = element.childNodes;
@ -107,7 +115,9 @@ export function emptyElement(element) {
// Replace element
export function replaceElement(newChild, oldChild) {
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
return null;
}
oldChild.parentNode.replaceChild(newChild, oldChild);
@ -121,7 +131,9 @@ export function getAttributesFromSelector(sel, existingAttributes) {
// '#test' to { id: 'test' }
// '[data-test="test"]' to { 'data-test': 'test' }
if (!is.string(sel) || is.empty(sel)) return {};
if (!is.string(sel) || is.empty(sel)) {
return {};
}
const attributes = {};
const existing = extend({}, existingAttributes);
@ -169,7 +181,9 @@ export function getAttributesFromSelector(sel, existingAttributes) {
// Toggle hidden
export function toggleHidden(element, hidden) {
if (!is.element(element)) return;
if (!is.element(element)) {
return;
}
let hide = hidden;
@ -254,9 +268,16 @@ export function getElement(selector) {
}
// Set focus and tab focus class
export function setFocus(element = null, focusVisible = false) {
if (!is.element(element)) return;
export function setFocus(element = null, tabFocus = false) {
if (!is.element(element)) {
return;
}
// Set regular focus
element.focus({ preventScroll: true, focusVisible });
element.focus({ preventScroll: true });
// If we want to mimic keyboard focus via tab
if (tabFocus) {
toggleClass(element, this.config.classNames.tabFocus);
}
}

View File

@ -9,7 +9,7 @@ const isObject = (input) => getConstructor(input) === Object;
const isNumber = (input) => getConstructor(input) === Number && !Number.isNaN(input);
const isString = (input) => getConstructor(input) === String;
const isBoolean = (input) => getConstructor(input) === Boolean;
const isFunction = (input) => typeof input === 'function';
const isFunction = (input) => getConstructor(input) === Function;
const isArray = (input) => Array.isArray(input);
const isWeakMap = (input) => instanceOf(input, WeakMap);
const isNodeList = (input) => instanceOf(input, NodeList);

View File

@ -7,7 +7,7 @@
* @param {Number} input
* @param {Number} min The lower boundary of the output range
* @param {Number} max The upper boundary of the output range
* @returns A number within the bounds of min and max
* @returns A number in the range [min, max]
* @type Number
*/
export function clamp(input = 0, min = 0, max = 255) {

View File

@ -11,9 +11,11 @@ export function generateId(prefix) {
// Format string
export function format(input, ...args) {
if (is.empty(input)) return input;
if (is.empty(input)) {
return input;
}
return input.toString().replace(/{(\d+)}/g, (_, i) => args[i].toString());
return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString());
}
// Get percentage
@ -25,7 +27,7 @@ export function getPercentage(current, max) {
return ((current / max) * 100).toFixed(2);
}
// Replace all occurrences of a string in a string
// Replace all occurances of a string in a string
export const replaceAll = (input = '', find = '', replace = '') =>
input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());

View File

@ -28,8 +28,8 @@
}
// Tab focus
&:focus-visible {
@include plyr-focus-visible;
&.plyr__tab-focus {
@include plyr-tab-focus;
}
}

View File

@ -26,7 +26,7 @@
&__container {
animation: plyr-popup 0.2s ease;
background: $plyr-menu-background;
border-radius: $plyr-menu-radius;
border-radius: 4px;
bottom: 100%;
box-shadow: $plyr-menu-shadow;
color: $plyr-menu-color;
@ -100,7 +100,7 @@
right: calc((#{$plyr-control-padding} * 1.5) - #{$plyr-menu-item-arrow-size});
}
&:focus-visible::after,
&.plyr__tab-focus::after,
&:hover::after {
border-left-color: currentColor;
}
@ -132,7 +132,7 @@
top: 100%;
}
&:focus-visible::after,
&.plyr__tab-focus::after,
&:hover::after {
border-right-color: currentColor;
}
@ -181,7 +181,7 @@
}
}
&:focus-visible::before,
&.plyr__tab-focus::before,
&:hover::before {
background: rgba($plyr-color-gray-900, 0.1);
}
@ -192,7 +192,7 @@
align-items: center;
display: flex;
margin-left: auto;
margin-right: calc((#{$plyr-control-padding} - 2px) * -1);
margin-right: calc((#{$plyr-control-padding} - 2) * -1);
overflow: hidden;
padding-left: calc(#{$plyr-control-padding} * 3.5);
pointer-events: none;

View File

@ -27,6 +27,7 @@ $plyr-progress-offset: $plyr-range-thumb-height;
left: 0;
max-width: 120px;
overflow-wrap: break-word;
white-space: normal;
}
}

View File

@ -83,17 +83,17 @@
outline: 0;
}
&:focus-visible {
&.plyr__tab-focus {
&::-webkit-slider-runnable-track {
@include plyr-focus-visible;
@include plyr-tab-focus;
}
&::-moz-range-track {
@include plyr-focus-visible;
@include plyr-tab-focus;
}
&::-ms-track {
@include plyr-focus-visible;
@include plyr-tab-focus;
}
}
}

View File

@ -42,7 +42,7 @@
// Displaying
.plyr .plyr__control:hover .plyr__tooltip,
.plyr .plyr__control:focus-visible .plyr__tooltip,
.plyr .plyr__control.plyr__tab-focus .plyr__tooltip,
.plyr__tooltip--visible {
opacity: 1;
transform: translate(-50%, 0) scale(1);
@ -82,7 +82,7 @@
.plyr__controls > .plyr__control:first-child + .plyr__control,
.plyr__controls > .plyr__control:last-child {
&:hover .plyr__tooltip,
&:focus-visible .plyr__tooltip,
&.plyr__tab-focus .plyr__tooltip,
.plyr__tooltip--visible {
transform: translate(0, 0) scale(1);
}

View File

@ -5,14 +5,21 @@
.plyr__volume {
align-items: center;
display: flex;
max-width: 110px;
min-width: 80px;
position: relative;
width: 20%;
input[type='range'] {
margin-left: calc(#{$plyr-control-spacing} / 2);
margin-right: calc(#{$plyr-control-spacing} / 2);
max-width: 90px;
min-width: 60px;
position: relative;
z-index: 2;
}
}
// Auto size on iOS as there's no slider
.plyr--is-ios .plyr__volume {
min-width: 0;
width: auto;
}

View File

@ -4,8 +4,8 @@
// Nicer focus styles
// ---------------------------------------
@mixin plyr-focus-visible($color: $plyr-focus-visible-color) {
outline: 2px dashed $color;
@mixin plyr-tab-focus($color: $plyr-tab-focus-color) {
outline: $color dotted 3px;
outline-offset: 2px;
}

View File

@ -32,7 +32,7 @@
bottom: calc(#{$plyr-preview-arrow-size} * -1);
content: '';
height: 0;
left: calc(50% + var(--preview-arrow-offset));
left: 50%;
position: absolute;
transform: translateX(-50%);
width: 0;
@ -46,27 +46,15 @@
position: relative;
z-index: 0;
img,
&::after {
height: 100%;
img {
height: 100%; // Non sprite images are 100%. Sprites will have their size applied by JavaScript
left: 0;
max-height: none;
max-width: none;
position: absolute;
top: 0;
width: 100%;
}
&::after {
border-radius: inherit;
box-shadow: inset 0 0 0 1px rgba(#000, 15%);
content: '';
pointer-events: none;
}
img {
// Non sprite images are 100%. Sprites will have their size applied by JavaScript
max-height: none;
max-width: none;
}
}
// Seek time text

View File

@ -4,7 +4,7 @@
$plyr-preview-padding: $plyr-tooltip-padding !default;
$plyr-preview-background: $plyr-tooltip-background !default;
$plyr-preview-radius: $plyr-menu-radius !default;
$plyr-preview-radius: 6px !default;
$plyr-preview-shadow: $plyr-tooltip-shadow !default;
$plyr-preview-arrow-size: $plyr-tooltip-arrow-size !default;
$plyr-preview-image-background: $plyr-color-gray-200 !default;

View File

@ -1,11 +1,9 @@
@charset "UTF-8";
// ==========================================================================
// Plyr styles
// https://github.com/sampotts/plyr
// TODO: Review use of BEM classnames
// ==========================================================================
@charset 'UTF-8';
@import 'lib/css-vars';
$css-vars-use-native: true;

View File

@ -6,7 +6,7 @@ $plyr-control-icon-size: var(--plyr-control-icon-size, 18px) !default;
$plyr-control-spacing: var(--plyr-control-spacing, 10px) !default;
$plyr-control-padding: calc(#{$plyr-control-spacing} * 0.7);
$plyr-control-padding: var(--plyr-control-padding, $plyr-control-padding) !default;
$plyr-control-radius: var(--plyr-control-radius, 4px) !default;
$plyr-control-radius: var(--plyr-control-radius, 3px) !default;
$plyr-control-toggle-checked-background: var(
--plyr-control-toggle-checked-background,
var(--plyr-color-main, $plyr-color-main)

View File

@ -2,4 +2,4 @@
// Cosmetic
// ==========================================================================
$plyr-focus-visible-color: var(--plyr-focus-visible-color, var(--plyr-color-main, $plyr-color-main)) !default;
$plyr-tab-focus-color: var(--plyr-tab-focus-color, var(--plyr-color-main, $plyr-color-main)) !default;

View File

@ -3,7 +3,7 @@
// ==========================================================================
$plyr-menu-background: var(--plyr-menu-background, rgba(#fff, 0.9)) !default;
$plyr-menu-radius: var(--plyr-menu-radius, 8px) !default;
$plyr-menu-radius: var(--plyr-menu-radius, 4px) !default;
$plyr-menu-color: var(--plyr-menu-color, $plyr-color-gray-700) !default;
$plyr-menu-shadow: var(--plyr-menu-shadow, 0 1px 2px rgba(#000, 0.15)) !default;
$plyr-menu-arrow-size: var(--plyr-menu-arrow-size, 4px) !default;

View File

@ -2,10 +2,10 @@
// Tooltips
// ==========================================================================
$plyr-tooltip-background: var(--plyr-tooltip-background, #fff) !default;
$plyr-tooltip-background: var(--plyr-tooltip-background, rgba(#fff, 0.9)) !default;
$plyr-tooltip-color: var(--plyr-tooltip-color, $plyr-color-gray-700) !default;
$plyr-tooltip-padding: calc(#{$plyr-control-spacing} / 2);
$plyr-tooltip-padding: var(--plyr-tooltip-padding, $plyr-tooltip-padding) !default;
$plyr-tooltip-arrow-size: var(--plyr-tooltip-arrow-size, 4px) !default;
$plyr-tooltip-radius: var(--plyr-tooltip-radius, 5px) !default;
$plyr-tooltip-radius: var(--plyr-tooltip-radius, 3px) !default;
$plyr-tooltip-shadow: var(--plyr-tooltip-shadow, 0 1px 2px rgba(0, 0, 0, 0.15)) !default;

View File

@ -11,6 +11,7 @@
@include plyr-fullscreen-active;
bottom: 0;
display: block;
left: 0;
position: fixed;
right: 0;

View File

@ -17,7 +17,7 @@
// Control elements
.plyr--audio .plyr__control {
&:focus-visible,
&.plyr__tab-focus,
&:hover,
&[aria-expanded='true'] {
background: $plyr-audio-control-background-hover;

View File

@ -6,6 +6,7 @@
// Container
.plyr--video {
background: var(--plyr-video-background, $plyr-video-background);
overflow: hidden;
&.plyr--menu-open {
@ -15,7 +16,6 @@
.plyr__video-wrapper {
background: var(--plyr-video-background, $plyr-video-background);
border-radius: inherit;
height: 100%;
margin: auto;
overflow: hidden;
@ -87,7 +87,8 @@ $embed-padding: (math.div(100, 16) * 9);
// Control elements
.plyr--video .plyr__control {
&:focus-visible,
// Hover and tab focus
&.plyr__tab-focus,
&:hover,
&[aria-expanded='true'] {
background: $plyr-video-control-background-hover;

View File

@ -118,7 +118,7 @@ Object.entries(build.js).forEach(([filename, entry]) => {
},
],
],
plugins: ['@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-optional-chaining'],
plugins: ['@babel/plugin-proposal-class-properties'],
babelrc: false,
exclude: [/\/core-js\//],
}),

13416
yarn.lock Normal file

File diff suppressed because it is too large Load Diff