fix: fullscreen improvements for iOS & iPadOS
This commit is contained in:
parent
5731245f4f
commit
62436d8e8e
67
README.md
67
README.md
@ -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
|
- 🤟 **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
|
||||||
|
|
||||||
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)
|
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:
|
You can use Plyr as an ES6 module as follows:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
import Plyr from 'plyr';
|
import Plyr from 'plyr';
|
||||||
|
|
||||||
const player = new Plyr('#player');
|
const player = new Plyr('#player');
|
||||||
@ -132,7 +132,7 @@ Alternatively you can include the `plyr.js` script before the closing `</body>`
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
See [initialising](#initialising) for more information on advanced setups.
|
See [initialising](#initializing) 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 [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.
|
||||||
|
|
||||||
@ -310,7 +310,7 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
|
|||||||
|
|
||||||
## JavaScript
|
## JavaScript
|
||||||
|
|
||||||
### Initialising
|
### Initializing
|
||||||
|
|
||||||
You can specify a range of arguments for the constructor to use:
|
You can specify a range of arguments for the constructor to use:
|
||||||
|
|
||||||
@ -324,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):
|
Passing a CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector):
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
const player = new Plyr('#player');
|
const player = new Plyr('#player');
|
||||||
```
|
```
|
||||||
|
|
||||||
Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement):
|
Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement):
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
const player = new Plyr(document.getElementById('player'));
|
const player = new Plyr(document.getElementById('player'));
|
||||||
```
|
```
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
const player = new Plyr(document.querySelector('.js-player'));
|
const player = new Plyr(document.querySelector('.js-player'));
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -344,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:
|
You have two choices here. You can either use a simple array loop to map the constructor:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
const players = Array.from(document.querySelectorAll('.js-player')).map((p) => new Plyr(p));
|
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:
|
...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:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
const players = Plyr.setup('.js-player');
|
const players = Plyr.setup('.js-player');
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -360,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:
|
The second argument for the constructor is the [options](#options) object:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
const player = new Plyr('#player', {
|
const player = new Plyr('#player', {
|
||||||
title: 'Example Title',
|
title: 'Example Title',
|
||||||
});
|
});
|
||||||
@ -375,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.
|
Note the single quotes encapsulating the JSON and double quotes on the object keys. Only string values need double quotes.
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| 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. |
|
| `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. |
|
||||||
@ -387,6 +387,7 @@ 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. |
|
| `blankVideo` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
|
||||||
| `autoplay`² | Boolean | `false` | Autoplay the media on load. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
|
| `autoplay`² | Boolean | `false` | Autoplay the media on load. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
|
||||||
| `autopause`¹ | Boolean | `true` | Only allow one player playing at once. |
|
| `autopause`¹ | Boolean | `true` | Only allow one player playing at once. |
|
||||||
|
| `playsinline`³ | 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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
||||||
@ -402,7 +403,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. |
|
| `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 non-selectable 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 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). `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) - 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. |
|
||||||
| `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, 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. |
|
| `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. |
|
||||||
@ -419,9 +420,14 @@ 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/>
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
# API
|
# API
|
||||||
|
|
||||||
@ -431,7 +437,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:
|
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:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
const player = new Plyr('#player', {
|
const player = new Plyr('#player', {
|
||||||
/* options */
|
/* options */
|
||||||
});
|
});
|
||||||
@ -439,7 +445,7 @@ const player = new Plyr('#player', {
|
|||||||
|
|
||||||
You can also access the object through any events:
|
You can also access the object through any events:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
element.addEventListener('ready', (event) => {
|
element.addEventListener('ready', (event) => {
|
||||||
const player = event.detail.plyr;
|
const player = event.detail.plyr;
|
||||||
});
|
});
|
||||||
@ -449,7 +455,7 @@ element.addEventListener('ready', (event) => {
|
|||||||
|
|
||||||
Example method use:
|
Example method use:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
player.play(); // Start playback
|
player.play(); // Start playback
|
||||||
player.fullscreen.enter(); // Enter fullscreen
|
player.fullscreen.enter(); // Enter fullscreen
|
||||||
```
|
```
|
||||||
@ -484,14 +490,14 @@ player.fullscreen.enter(); // Enter fullscreen
|
|||||||
|
|
||||||
Example setters:
|
Example setters:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
player.volume = 0.5; // Sets volume at 50%
|
player.volume = 0.5; // Sets volume at 50%
|
||||||
player.currentTime = 10; // Seeks to 10 seconds
|
player.currentTime = 10; // Seeks to 10 seconds
|
||||||
```
|
```
|
||||||
|
|
||||||
Example getters:
|
Example getters:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
player.volume; // 0.5;
|
player.volume; // 0.5;
|
||||||
player.currentTime; // 10
|
player.currentTime; // 10
|
||||||
player.fullscreen.active; // false;
|
player.fullscreen.active; // false;
|
||||||
@ -535,7 +541,7 @@ This allows changing the player source and type on the fly.
|
|||||||
|
|
||||||
Video example:
|
Video example:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
player.source = {
|
player.source = {
|
||||||
type: 'video',
|
type: 'video',
|
||||||
title: 'Example title',
|
title: 'Example title',
|
||||||
@ -575,7 +581,7 @@ player.source = {
|
|||||||
|
|
||||||
Audio example:
|
Audio example:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
player.source = {
|
player.source = {
|
||||||
type: 'audio',
|
type: 'audio',
|
||||||
title: 'Example title',
|
title: 'Example title',
|
||||||
@ -594,7 +600,7 @@ player.source = {
|
|||||||
|
|
||||||
YouTube example:
|
YouTube example:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
player.source = {
|
player.source = {
|
||||||
type: 'video',
|
type: 'video',
|
||||||
sources: [
|
sources: [
|
||||||
@ -608,7 +614,7 @@ player.source = {
|
|||||||
|
|
||||||
Vimeo example
|
Vimeo example
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
player.source = {
|
player.source = {
|
||||||
type: 'video',
|
type: 'video',
|
||||||
sources: [
|
sources: [
|
||||||
@ -639,7 +645,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`
|
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:
|
property. Here's an example:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
player.on('ready', (event) => {
|
player.on('ready', (event) => {
|
||||||
const instance = event.detail.plyr;
|
const instance = event.detail.plyr;
|
||||||
});
|
});
|
||||||
@ -761,21 +767,20 @@ 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
|
You can use the static method to check for support. For example
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
const supported = Plyr.supported('video', 'html5', true);
|
const supported = Plyr.supported('video', 'html5');
|
||||||
```
|
```
|
||||||
|
|
||||||
The arguments are:
|
The arguments are:
|
||||||
|
|
||||||
- Media type (`audio` or `video`)
|
- Media type (`'audio' | 'video'`)
|
||||||
- Provider (`html5`, `youtube` or `vimeo`)
|
- Provider (`'html5' | 'youtube' | 'vimeo'`)
|
||||||
- Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
|
|
||||||
|
|
||||||
## Disable support programmatically
|
## 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:
|
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:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
{
|
{
|
||||||
enabled: !/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
enabled: !/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,7 @@ const defaults = {
|
|||||||
// Only allow one media playing at once (vimeo only)
|
// Only allow one media playing at once (vimeo only)
|
||||||
autopause: true,
|
autopause: true,
|
||||||
|
|
||||||
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
|
// Allow inline playback on iOS
|
||||||
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
|
|
||||||
playsinline: true,
|
playsinline: true,
|
||||||
|
|
||||||
// Default time to skip when rewind/fast forward
|
// Default time to skip when rewind/fast forward
|
||||||
@ -353,7 +352,6 @@ const defaults = {
|
|||||||
marker: 'plyr__progress__marker',
|
marker: 'plyr__progress__marker',
|
||||||
hidden: 'plyr__sr-only',
|
hidden: 'plyr__sr-only',
|
||||||
hideControls: 'plyr--hide-controls',
|
hideControls: 'plyr--hide-controls',
|
||||||
isIos: 'plyr--is-ios',
|
|
||||||
isTouch: 'plyr--is-touch',
|
isTouch: 'plyr--is-touch',
|
||||||
uiSupported: 'plyr--full-ui',
|
uiSupported: 'plyr--full-ui',
|
||||||
noTransition: 'plyr--no-transition',
|
noTransition: 'plyr--no-transition',
|
||||||
|
4
src/js/controls.js
vendored
4
src/js/controls.js
vendored
@ -676,7 +676,7 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WebKit only
|
// WebKit only
|
||||||
if (!browser.isWebkit) {
|
if (!browser.isWebKit && !browser.isIPadOS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1385,7 +1385,7 @@ const controls = {
|
|||||||
// Volume range control
|
// Volume range control
|
||||||
// Ignored on iOS as it's handled globally
|
// 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
|
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
|
||||||
if (control === 'volume' && !browser.isIos) {
|
if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {
|
||||||
// Set the attributes
|
// Set the attributes
|
||||||
const attributes = {
|
const attributes = {
|
||||||
max: 1,
|
max: 1,
|
||||||
|
@ -57,12 +57,10 @@ class Fullscreen {
|
|||||||
|
|
||||||
// Update the UI
|
// Update the UI
|
||||||
this.update();
|
this.update();
|
||||||
|
|
||||||
// this.toggle = this.toggle.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if native supported
|
// Determine if native supported
|
||||||
static get native() {
|
static get nativeSupported() {
|
||||||
return !!(
|
return !!(
|
||||||
document.fullscreenEnabled ||
|
document.fullscreenEnabled ||
|
||||||
document.webkitFullscreenEnabled ||
|
document.webkitFullscreenEnabled ||
|
||||||
@ -72,16 +70,14 @@ class Fullscreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we're actually using native
|
// If we're actually using native
|
||||||
get usingNative() {
|
get useNative() {
|
||||||
return Fullscreen.native && !this.forceFallback;
|
return Fullscreen.nativeSupported && !this.forceFallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the prefix for handlers
|
// Get the prefix for handlers
|
||||||
static get prefix() {
|
static get prefix() {
|
||||||
// No prefix
|
// No prefix
|
||||||
if (is.function(document.exitFullscreen)) {
|
if (is.function(document.exitFullscreen)) return '';
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for fullscreen support by vendor prefix
|
// Check for fullscreen support by vendor prefix
|
||||||
let value = '';
|
let value = '';
|
||||||
@ -103,24 +99,30 @@ class Fullscreen {
|
|||||||
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
|
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if fullscreen is enabled
|
// Determine if fullscreen is supported
|
||||||
get enabled() {
|
get supported() {
|
||||||
return (
|
return [
|
||||||
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
|
// Fullscreen is enabled in config
|
||||||
this.player.config.fullscreen.enabled &&
|
this.player.config.fullscreen.enabled,
|
||||||
this.player.supported.ui &&
|
// Must be a video
|
||||||
this.player.isVideo
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get active state
|
// Get active state
|
||||||
get active() {
|
get active() {
|
||||||
if (!this.enabled) {
|
if (!this.supported) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback using classname
|
// Fallback using classname
|
||||||
if (!Fullscreen.native || this.forceFallback) {
|
if (!Fullscreen.nativeSupported || this.forceFallback) {
|
||||||
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,13 +137,11 @@ class Fullscreen {
|
|||||||
get target() {
|
get target() {
|
||||||
return browser.isIos && this.player.config.fullscreen.iosNative
|
return browser.isIos && this.player.config.fullscreen.iosNative
|
||||||
? this.player.media
|
? this.player.media
|
||||||
: this.player.elements.fullscreen || this.player.elements.container;
|
: this.player.elements.fullscreen ?? this.player.elements.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange = () => {
|
onChange = () => {
|
||||||
if (!this.enabled) {
|
if (!this.supported) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update toggle button
|
// Update toggle button
|
||||||
const button = this.player.elements.buttons.fullscreen;
|
const button = this.player.elements.buttons.fullscreen;
|
||||||
@ -159,8 +159,8 @@ class Fullscreen {
|
|||||||
// Store or restore scroll position
|
// Store or restore scroll position
|
||||||
if (toggle) {
|
if (toggle) {
|
||||||
this.scrollPosition = {
|
this.scrollPosition = {
|
||||||
x: window.scrollX || 0,
|
x: window.scrollX ?? 0,
|
||||||
y: window.scrollY || 0,
|
y: window.scrollY ?? 0,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
|
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
|
||||||
@ -188,10 +188,7 @@ class Fullscreen {
|
|||||||
|
|
||||||
if (toggle) {
|
if (toggle) {
|
||||||
this.cleanupViewport = !hasProperty;
|
this.cleanupViewport = !hasProperty;
|
||||||
|
if (!hasProperty) viewport.content += `,${property}`;
|
||||||
if (!hasProperty) {
|
|
||||||
viewport.content += `,${property}`;
|
|
||||||
}
|
|
||||||
} else if (this.cleanupViewport) {
|
} else if (this.cleanupViewport) {
|
||||||
viewport.content = viewport.content
|
viewport.content = viewport.content
|
||||||
.split(',')
|
.split(',')
|
||||||
@ -206,10 +203,8 @@ class Fullscreen {
|
|||||||
|
|
||||||
// Trap focus inside container
|
// Trap focus inside container
|
||||||
trapFocus = (event) => {
|
trapFocus = (event) => {
|
||||||
// Bail if iOS, not active, not the tab key
|
// Bail if iOS/iPadOS, not active, not the tab key
|
||||||
if (browser.isIos || !this.active || event.key !== 'Tab') {
|
if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current focused element
|
// Get the current focused element
|
||||||
const focused = document.activeElement;
|
const focused = document.activeElement;
|
||||||
@ -230,16 +225,12 @@ class Fullscreen {
|
|||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
update = () => {
|
update = () => {
|
||||||
if (this.enabled) {
|
if (this.supported) {
|
||||||
let mode;
|
let mode;
|
||||||
|
|
||||||
if (this.forceFallback) {
|
if (this.forceFallback) mode = 'Fallback (forced)';
|
||||||
mode = 'Fallback (forced)';
|
else if (Fullscreen.nativeSupported) mode = 'Native';
|
||||||
} else if (Fullscreen.native) {
|
else mode = 'Fallback';
|
||||||
mode = 'Native';
|
|
||||||
} else {
|
|
||||||
mode = 'Fallback';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.player.debug.log(`${mode} fullscreen enabled`);
|
this.player.debug.log(`${mode} fullscreen enabled`);
|
||||||
} else {
|
} else {
|
||||||
@ -247,14 +238,12 @@ class Fullscreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add styling hook to show button
|
// Add styling hook to show button
|
||||||
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
|
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Make an element fullscreen
|
// Make an element fullscreen
|
||||||
enter = () => {
|
enter = () => {
|
||||||
if (!this.enabled) {
|
if (!this.supported) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// iOS native fullscreen doesn't need the request step
|
// iOS native fullscreen doesn't need the request step
|
||||||
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||||
@ -263,7 +252,7 @@ class Fullscreen {
|
|||||||
} else {
|
} else {
|
||||||
this.target.webkitEnterFullscreen();
|
this.target.webkitEnterFullscreen();
|
||||||
}
|
}
|
||||||
} else if (!Fullscreen.native || this.forceFallback) {
|
} else if (!Fullscreen.nativeSupported || this.forceFallback) {
|
||||||
this.toggleFallback(true);
|
this.toggleFallback(true);
|
||||||
} else if (!this.prefix) {
|
} else if (!this.prefix) {
|
||||||
this.target.requestFullscreen({ navigationUI: 'hide' });
|
this.target.requestFullscreen({ navigationUI: 'hide' });
|
||||||
@ -274,15 +263,17 @@ class Fullscreen {
|
|||||||
|
|
||||||
// Bail from fullscreen
|
// Bail from fullscreen
|
||||||
exit = () => {
|
exit = () => {
|
||||||
if (!this.enabled) {
|
if (!this.supported) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// iOS native fullscreen
|
// iOS native fullscreen
|
||||||
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||||
this.target.webkitExitFullscreen();
|
if (this.player.isVimeo) {
|
||||||
|
this.player.embed.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
this.target.webkitEnterFullscreen();
|
||||||
|
}
|
||||||
silencePromise(this.player.play());
|
silencePromise(this.player.play());
|
||||||
} else if (!Fullscreen.native || this.forceFallback) {
|
} else if (!Fullscreen.nativeSupported || this.forceFallback) {
|
||||||
this.toggleFallback(false);
|
this.toggleFallback(false);
|
||||||
} else if (!this.prefix) {
|
} else if (!this.prefix) {
|
||||||
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
||||||
@ -294,11 +285,8 @@ class Fullscreen {
|
|||||||
|
|
||||||
// Toggle state
|
// Toggle state
|
||||||
toggle = () => {
|
toggle = () => {
|
||||||
if (!this.active) {
|
if (!this.active) this.enter();
|
||||||
this.enter();
|
else this.exit();
|
||||||
} else {
|
|
||||||
this.exit();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -797,7 +797,7 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Polyfill for lower fill in <input type="range"> for webkit
|
// 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) => {
|
Array.from(getElements.call(player, 'input[type="range"]')).forEach((element) => {
|
||||||
this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));
|
this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));
|
||||||
});
|
});
|
||||||
|
@ -113,7 +113,7 @@ const vimeo = {
|
|||||||
autoplay: player.autoplay,
|
autoplay: player.autoplay,
|
||||||
muted: player.muted,
|
muted: player.muted,
|
||||||
gesture: 'media',
|
gesture: 'media',
|
||||||
playsinline: !this.config.fullscreen.iosNative,
|
playsinline: player.config.playsinline,
|
||||||
// hash has to be added to iframe-URL
|
// hash has to be added to iframe-URL
|
||||||
...hashParam,
|
...hashParam,
|
||||||
...frameParams,
|
...frameParams,
|
||||||
|
@ -131,7 +131,7 @@ const youtube = {
|
|||||||
const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
|
const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
|
||||||
|
|
||||||
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
||||||
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
|
loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded
|
||||||
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
|
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
|
||||||
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
|
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
|
||||||
.then((image) => ui.setPoster.call(player, image.src))
|
.then((image) => ui.setPoster.call(player, image.src))
|
||||||
@ -161,7 +161,7 @@ const youtube = {
|
|||||||
// Disable keyboard as we handle it
|
// Disable keyboard as we handle it
|
||||||
disablekb: 1,
|
disablekb: 1,
|
||||||
// Allow iOS inline playback
|
// Allow iOS inline playback
|
||||||
playsinline: !player.config.fullscreen.iosNative ? 1 : 0,
|
playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,
|
||||||
// Captions are flaky on YouTube
|
// Captions are flaky on YouTube
|
||||||
cc_load_policy: player.captions.active ? 1 : 0,
|
cc_load_policy: player.captions.active ? 1 : 0,
|
||||||
cc_lang_pref: player.config.captions.language,
|
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.',
|
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.',
|
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.',
|
150: 'The owner of the requested video does not allow it to be played in embedded players.',
|
||||||
}[code] || 'An unknown error occured';
|
}[code] || 'An unknown error occurred';
|
||||||
|
|
||||||
player.media.error = { code, message };
|
player.media.error = { code, message };
|
||||||
|
|
||||||
|
@ -246,7 +246,7 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for support again but with type
|
// Check for support again but with type
|
||||||
this.supported = support.check(this.type, this.provider, this.config.playsinline);
|
this.supported = support.check(this.type, this.provider);
|
||||||
|
|
||||||
// If no support for even API, bail
|
// If no support for even API, bail
|
||||||
if (!this.supported.api) {
|
if (!this.supported.api) {
|
||||||
@ -1032,7 +1032,7 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the preview thubmnails for the current source
|
* Sets the preview thumbnails for the current source
|
||||||
*/
|
*/
|
||||||
setPreviewThumbnails(thumbnailSource) {
|
setPreviewThumbnails(thumbnailSource) {
|
||||||
if (this.previewThumbnails && this.previewThumbnails.loaded) {
|
if (this.previewThumbnails && this.previewThumbnails.loaded) {
|
||||||
@ -1239,10 +1239,9 @@ class Plyr {
|
|||||||
* Check for support
|
* Check for support
|
||||||
* @param {String} type - Player type (audio/video)
|
* @param {String} type - Player type (audio/video)
|
||||||
* @param {String} provider - Provider (html5/youtube/vimeo)
|
* @param {String} provider - Provider (html5/youtube/vimeo)
|
||||||
* @param {Boolean} inline - Where player has `playsinline` sttribute
|
|
||||||
*/
|
*/
|
||||||
static supported(type, provider, inline) {
|
static supported(type, provider) {
|
||||||
return support.check(type, provider, inline);
|
return support.check(type, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,10 +24,9 @@ const support = {
|
|||||||
|
|
||||||
// Check for support
|
// Check for support
|
||||||
// Basic functionality vs full UI
|
// Basic functionality vs full UI
|
||||||
check(type, provider, playsinline) {
|
check(type, provider) {
|
||||||
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;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
api,
|
api,
|
||||||
|
@ -98,9 +98,6 @@ const ui = {
|
|||||||
// 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
|
|
||||||
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);
|
||||||
|
|
||||||
|
@ -3,12 +3,19 @@
|
|||||||
// Unfortunately, due to mixed support, UA sniffing is required
|
// Unfortunately, due to mixed support, UA sniffing is required
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
const browser = {
|
const isIE = Boolean(window.document.documentMode);
|
||||||
isIE: Boolean(window.document.documentMode),
|
const isEdge = /Edge/g.test(navigator.userAgent);
|
||||||
isEdge: /Edge/g.test(navigator.userAgent),
|
const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);
|
||||||
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent),
|
const isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
|
||||||
isIPhone: /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1,
|
// navigator.platform may be deprecated but this check is still required
|
||||||
isIos: /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1,
|
const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
|
||||||
};
|
const isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
|
||||||
|
|
||||||
export default browser;
|
export default {
|
||||||
|
isIE,
|
||||||
|
isEdge,
|
||||||
|
isWebKit,
|
||||||
|
isIPhone,
|
||||||
|
isIPadOS,
|
||||||
|
isIos,
|
||||||
|
};
|
||||||
|
@ -67,7 +67,7 @@ export function createElement(type, attributes, text) {
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inaert an element after another
|
// Insert 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;
|
||||||
|
@ -13,7 +13,7 @@ export function generateId(prefix) {
|
|||||||
export function format(input, ...args) {
|
export function format(input, ...args) {
|
||||||
if (is.empty(input)) return input;
|
if (is.empty(input)) return input;
|
||||||
|
|
||||||
return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString());
|
return input.toString().replace(/{(\d+)}/g, (_, i) => args[i].toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get percentage
|
// Get percentage
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
@include plyr-fullscreen-active;
|
@include plyr-fullscreen-active;
|
||||||
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
display: block;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user