Compare commits
204 Commits
v3.0.0-bet
...
v3.3.9
Author | SHA1 | Date | |
---|---|---|---|
d70a787af1 | |||
69bb0917ad | |||
6f256d09b2 | |||
e9684c2021 | |||
bf91a0e73f | |||
14b6309aef | |||
6391ced99f | |||
fac8a185ba | |||
c69aa8a42b | |||
f34bf22125 | |||
f0be913dc3 | |||
cd51788b98 | |||
edd67b0da3 | |||
d733454d7f | |||
41f9a87e0e | |||
f4858f0c62 | |||
121093ae71 | |||
aa8fc313a9 | |||
723298a07b | |||
f8c89e3e95 | |||
333435a9c2 | |||
3ab2295fe7 | |||
c41bb657ac | |||
55bbf64f2b | |||
3bba65f2c2 | |||
1bab0d07b5 | |||
602353f4d9 | |||
51814249af | |||
37c5fbfe16 | |||
d7356726a1 | |||
4db6bf7a2e | |||
28826f6402 | |||
c845558d96 | |||
16c3a7d9e5 | |||
90d5b48845 | |||
d1acc4abb3 | |||
797b70998f | |||
4a01027da0 | |||
7ca2169790 | |||
054f522aa9 | |||
f2fc3f5ea5 | |||
765c01e83d | |||
33a11fb53a | |||
d1d41ca49a | |||
c06e0ee5e9 | |||
83f80ccc40 | |||
069065ea3a | |||
1672e78041 | |||
34401de3d0 | |||
f687b81b70 | |||
bbb11e611e | |||
90919411e9 | |||
1491b017a0 | |||
1655150092 | |||
ceb6c9a100 | |||
00bbce08fb | |||
91a4b86860 | |||
5aece6fa06 | |||
a70b94afe2 | |||
9ebc2719d3 | |||
b46aae1833 | |||
30e6a40865 | |||
403df36af6 | |||
5ca769807e | |||
72a71a605b | |||
24d833a5d1 | |||
44b30380f7 | |||
261cd086c7 | |||
9e19b526b9 | |||
6c617a0ef1 | |||
a812650fea | |||
fec7a77d6f | |||
971e261067 | |||
27407ba021 | |||
ef8e58ede4 | |||
f13260c10a | |||
e1183d6049 | |||
f1b275aedc | |||
b647af256c | |||
d2e9ed3467 | |||
5b39986835 | |||
a97b08e8ea | |||
56d1be9447 | |||
a241cb5215 | |||
042b1a8294 | |||
6d79b8cd4c | |||
88d766aeae | |||
119b471b84 | |||
7f079e0ec3 | |||
46fe3eecff | |||
3061a701d5 | |||
e45109e1d7 | |||
e138e6d51e | |||
aef1363b04 | |||
766dd03d81 | |||
ab393651ec | |||
ffd265d0ae | |||
72155472dd | |||
9b7170834e | |||
3e57a87bf7 | |||
a15d1c9f1c | |||
a095a64f90 | |||
2374d6b1c4 | |||
5ed3ff9084 | |||
385be55510 | |||
3082d0d128 | |||
f7e242f054 | |||
2874505004 | |||
ed9e0c13d7 | |||
10be94fa99 | |||
ee79c46145 | |||
384010a2c0 | |||
1e47019122 | |||
536c65e82c | |||
cdf14932ec | |||
3b20dbd9fd | |||
e4d975af00 | |||
2782a00e7c | |||
91d192dd7c | |||
b1e3abc795 | |||
3395e8df90 | |||
cce143a7da | |||
d593005b32 | |||
7be9d5d4d3 | |||
d7141d5ed7 | |||
0d0ece94d3 | |||
1c06f6d06d | |||
dda8e30b92 | |||
c4e2e24643 | |||
e020a105a3 | |||
2b7fe9a4f9 | |||
951df64b7f | |||
0976afe282 | |||
7b1e4abda7 | |||
0cf75eed3f | |||
d96957d086 | |||
1a032ea498 | |||
5d079da1b8 | |||
9c1bc6ab08 | |||
3d2ba8c009 | |||
e872ce3f77 | |||
b77756da04 | |||
9b23e13ce8 | |||
5eafe9baff | |||
c251c94131 | |||
17041efc71 | |||
05b8e8a6e0 | |||
f998b996fa | |||
958b47c435 | |||
a27248d3b6 | |||
1b1f7be7ff | |||
59d4a27240 | |||
75e9f3c2e3 | |||
7132eccf50 | |||
e953c6398c | |||
bb7eea27e5 | |||
595c5e95bc | |||
43e6dcd41d | |||
b06c8ae43f | |||
c7ea13c0c7 | |||
0f8c6e147b | |||
e566365288 | |||
a06e0f5890 | |||
3bccc0da01 | |||
a0173d991e | |||
600f0eb8a3 | |||
5db73b1327 | |||
5cb1628cd8 | |||
c74b75e8e1 | |||
0538476d6f | |||
5ebe18d081 | |||
207adde36d | |||
1b13ddaa54 | |||
9981c349be | |||
b3365a7373 | |||
9a0c1c830d | |||
ef27ba16f4 | |||
e206edc1f6 | |||
c734bc4957 | |||
572b8a7aca | |||
eebae4a227 | |||
e0562752ea | |||
6a2ca534d2 | |||
7adc2bc6c8 | |||
ba8d7831a7 | |||
69ffcbad27 | |||
819f7d1080 | |||
409b588458 | |||
e90a603d57 | |||
6f061621ad | |||
0300610108 | |||
2fba5f152c | |||
317b08c703 | |||
e6db374a72 | |||
bfb550b8d0 | |||
174234c166 | |||
24b4220de5 | |||
f1895a4cce | |||
c2a6306d46 | |||
7ac732f45b | |||
a59dcb2f53 | |||
ab7f277a1b | |||
ce1d5a60d6 | |||
d5a1a7ca1c |
@ -8,6 +8,7 @@
|
|||||||
"globals": { "Plyr": false, "jQuery": false },
|
"globals": { "Plyr": false, "jQuery": false },
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-const-assign": 1,
|
"no-const-assign": 1,
|
||||||
|
"no-shadow": 0,
|
||||||
"no-this-before-super": 1,
|
"no-this-before-super": 1,
|
||||||
"no-undef": 1,
|
"no-undef": 1,
|
||||||
"no-unreachable": 1,
|
"no-unreachable": 1,
|
||||||
@ -31,6 +32,7 @@
|
|||||||
"message": "Use local parameter instead."
|
"message": "Use local parameter instead."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"no-param-reassign": [2, { "props": false }],
|
||||||
"array-bracket-newline": [2, { "minItems": 2 }],
|
"array-bracket-newline": [2, { "minItems": 2 }],
|
||||||
"array-element-newline": [2, { "minItems": 2 }]
|
"array-element-newline": [2, { "minItems": 2 }]
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"],
|
"plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"],
|
||||||
"extends": ["stylelint-config-sass-guidelines", "stylelint-config-prettier"],
|
"extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"selector-class-pattern": null,
|
"selector-class-pattern": null,
|
||||||
"selector-no-qualifying-type": [
|
"selector-no-qualifying-type": [
|
||||||
@ -9,6 +9,7 @@
|
|||||||
"ignore": ["attribute", "class"]
|
"ignore": ["attribute", "class"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"string-no-newline": null,
|
||||||
"indentation": 4,
|
"indentation": 4,
|
||||||
"string-quotes": "single",
|
"string-quotes": "single",
|
||||||
"max-nesting-depth": 2,
|
"max-nesting-depth": 2,
|
||||||
|
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Chrome against localhost",
|
||||||
|
"url": "http://localhost/dev/plyr/demo",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -4,13 +4,14 @@
|
|||||||
"plyr.css": "src/sass/plyr.scss"
|
"plyr.css": "src/sass/plyr.scss"
|
||||||
},
|
},
|
||||||
"js": {
|
"js": {
|
||||||
"plyr.js": "src/js/plyr.js"
|
"plyr.js": "src/js/plyr.js",
|
||||||
|
"plyr.polyfilled.js": "src/js/plyr.polyfilled.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"demo": {
|
"demo": {
|
||||||
"sass": {
|
"sass": {
|
||||||
"demo.css": "demo/src/sass/bundles/demo.scss",
|
"demo.css": "demo/src/sass/bundles/demo.scss",
|
||||||
"error.css": "demo/src/sass/bundles/error.csss"
|
"error.css": "demo/src/sass/bundles/error.scss"
|
||||||
},
|
},
|
||||||
"js": {
|
"js": {
|
||||||
"demo.js": "demo/src/js/demo.js"
|
"demo.js": "demo/src/js/demo.js"
|
||||||
|
173
changelog.md
173
changelog.md
@ -1,3 +1,174 @@
|
|||||||
|
# v3.3.9
|
||||||
|
|
||||||
|
Again, more changes from @friday!
|
||||||
|
|
||||||
|
* Restore window reference in `utils.is.cue()`
|
||||||
|
* Fix InvalidStateError and IE11 issues
|
||||||
|
* Respect storage being disabled for storage getter
|
||||||
|
|
||||||
|
# v3.3.8
|
||||||
|
|
||||||
|
Many changes here thanks to @friday:
|
||||||
|
|
||||||
|
* Added missing URL polyfill
|
||||||
|
* Pause while seeking to mimic default HTML5 behaviour
|
||||||
|
* Add `seeked` event listener to update progress (fixes #966)
|
||||||
|
* Trigger seeked event in youtube plugin if either playing or paused (fixes #921)
|
||||||
|
* Fix for YouTube and Vimeo autoplays on seek (fixes #876)
|
||||||
|
* Toggle controls improvements
|
||||||
|
* Cleanup unused code
|
||||||
|
* Poster image loading improvements
|
||||||
|
* Fix for seek tooltip vs click accuracy
|
||||||
|
|
||||||
|
# v3.3.7
|
||||||
|
|
||||||
|
* Poster fixes (thanks @friday)
|
||||||
|
* Grid tweak
|
||||||
|
|
||||||
|
# v3.3.6
|
||||||
|
|
||||||
|
* Vimeo fixes for mute state
|
||||||
|
* Vimeo ID fix (fixes #945)
|
||||||
|
* Use `<div>` for poster container
|
||||||
|
* Tooltip fixes for unicode languages (fixes #943)
|
||||||
|
|
||||||
|
# v3.3.5
|
||||||
|
|
||||||
|
* Removed `.load()` call as it breaks HLS (see #870)
|
||||||
|
|
||||||
|
# v3.3.4
|
||||||
|
|
||||||
|
* Fix for controls sometimes not showing while video is playing
|
||||||
|
* Fixed logic for show home tab on option select
|
||||||
|
|
||||||
|
# v3.3.3
|
||||||
|
|
||||||
|
* Reverted change to show home tab on option select due to usability regression
|
||||||
|
|
||||||
|
# v3.3.2
|
||||||
|
|
||||||
|
* Fix for ads running in audio
|
||||||
|
* Fix for setting poster on source change
|
||||||
|
|
||||||
|
## v3.3.0
|
||||||
|
|
||||||
|
* Now using a custom poster image element to hide the YouTube play button and give more control over when the poster image shows
|
||||||
|
* Renamed `showPosterOnEnd` to `resetOnEnd` as it makes more sense and now works for all players and does not reload media
|
||||||
|
* Fix for same domain SVG URLs (raised by Jochem in Slack)
|
||||||
|
* [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/Window/URL) is polyfill now required
|
||||||
|
* Added pause className (fixes #941)
|
||||||
|
* Button height set in CSS (auto) (fixes #928)
|
||||||
|
* Don't autoplay cloned original media (fixes #936)
|
||||||
|
* Return to the home menu pane after selecting an option
|
||||||
|
|
||||||
|
## v3.2.4
|
||||||
|
|
||||||
|
* Fix issue wher player never reports as ready if controls is empty array
|
||||||
|
* Fix issue where screen reader labels were removed from time displays
|
||||||
|
* Fix issue where custom controls placeholders were not populated
|
||||||
|
* Custom controls HTML example updated
|
||||||
|
* Fix for aria-label being set to the initial state on toggle buttons, overriding the inner labels
|
||||||
|
* Fix for hidden mute button on iOS (not functional for Vimeo due to API limitations) (fixes #656)
|
||||||
|
|
||||||
|
## v3.2.3
|
||||||
|
|
||||||
|
* Fix for iOS 9 throwing error for `name` property in fullscreen API (fixes #908)
|
||||||
|
|
||||||
|
## v3.2.2
|
||||||
|
|
||||||
|
* Fix for regression in 3.2.1 resulting in hidden buffer display (fixes #920)
|
||||||
|
* Cleaned up incorrect use of `aria-hidden` attribute
|
||||||
|
|
||||||
|
## v3.2.1
|
||||||
|
|
||||||
|
* Accessibility improvements for the controls (part of #905 fixes)
|
||||||
|
* Fix for context menu showing on YouTube (thanks Anthony Recenello in Slack)
|
||||||
|
* Vimeo fix for their API not returning the right duration until playback begins (fixes #891)
|
||||||
|
|
||||||
|
## v3.2.0
|
||||||
|
|
||||||
|
* Fullscreen fixes (thanks @friday)
|
||||||
|
* Menu fix for if speed not in config
|
||||||
|
* Menu z-index fix (thanks @danielsarin)
|
||||||
|
* i18n fix for missing "Normal" string (thanks @danielsarin)
|
||||||
|
* Safer check for active caption (thanks @Antonio-Laguna)
|
||||||
|
* Add custom property fallback (thanks @friday)
|
||||||
|
* Fixed bug for captions with no srclang and labels and improved logic (fixes #875)
|
||||||
|
* Fix for `playing` false positive (fixes #898)
|
||||||
|
* Fix for IE issue with navigator.language (thanks @nicolasthy) (fixes #893)
|
||||||
|
* Fix for Vimeo controls missing on iOS (thanks @verde-io) (fixes #807)
|
||||||
|
* Fix for double vimeo caption rendering (fixes #877)
|
||||||
|
|
||||||
|
## v3.1.0
|
||||||
|
|
||||||
|
* Styling fixes
|
||||||
|
|
||||||
|
## v3.1.0-beta.2
|
||||||
|
|
||||||
|
* YouTube playback speed fixes
|
||||||
|
|
||||||
|
## v3.1.0-beta.1
|
||||||
|
|
||||||
|
* HTML5 quality selection
|
||||||
|
* Improvements to the YouTube quality selection
|
||||||
|
|
||||||
|
## v3.0.11
|
||||||
|
|
||||||
|
* Muted and autoplay fixes
|
||||||
|
* Small bug fixes from Sentry logs
|
||||||
|
|
||||||
|
## v3.0.10
|
||||||
|
|
||||||
|
* Docs fix
|
||||||
|
* Package upgrades
|
||||||
|
|
||||||
|
## v3.0.9
|
||||||
|
|
||||||
|
* Demo fix
|
||||||
|
* Fix Vimeo regression
|
||||||
|
|
||||||
|
## v3.0.8
|
||||||
|
|
||||||
|
* Vimeo hotfix for private videos
|
||||||
|
|
||||||
|
## v3.0.7
|
||||||
|
|
||||||
|
* Fix for keyboard shortcut error with fast forward
|
||||||
|
* Fix for Vimeo trying to set playback rate when not allowed
|
||||||
|
|
||||||
|
## v3.0.6
|
||||||
|
|
||||||
|
* Improved the logic for the custom handlers preventing default handlers
|
||||||
|
|
||||||
|
## v3.0.5
|
||||||
|
|
||||||
|
* Removed console messages
|
||||||
|
|
||||||
|
## v3.0.4
|
||||||
|
|
||||||
|
* Fixes for fullscreen not working inside iframes
|
||||||
|
* Fixes for custom handlers being able to prevent default
|
||||||
|
* Fixes for controls not hiding/showing correctly on Mobile Safari
|
||||||
|
|
||||||
|
## v3.0.3
|
||||||
|
|
||||||
|
* Vimeo offset tweak (fixes #826)
|
||||||
|
* Fix for .stop() method (fixes #819)
|
||||||
|
* Check for array for speed options (fixes #817)
|
||||||
|
* Restore as float (fixes #828)
|
||||||
|
* Fix for Firefox fullscreen oddness (Fixes #821)
|
||||||
|
* Improve Sprite checking (fixes #827)
|
||||||
|
* Fix fast-forward control (thanks @saadshahd)
|
||||||
|
* Fix the options link in the readme (thanks @DanielRuf)
|
||||||
|
|
||||||
|
## v3.0.2
|
||||||
|
|
||||||
|
* Fix for Safari not firing error events when trying to load blocked scripts
|
||||||
|
|
||||||
|
## v3.0.1
|
||||||
|
|
||||||
|
* Fix for trying to accessing local storage when it's blocked
|
||||||
|
|
||||||
# v3.0.0
|
# v3.0.0
|
||||||
|
|
||||||
This is a massive release. A _mostly_ complete rewrite in ES6. What started out as a few changes quickly snowballed. There's many breaking changes so be careful upgrading.
|
This is a massive release. A _mostly_ complete rewrite in ES6. What started out as a few changes quickly snowballed. There's many breaking changes so be careful upgrading.
|
||||||
@ -67,7 +238,7 @@ You gotta break eggs to make an omelette. Sadly, there's quite a few breaking ch
|
|||||||
|
|
||||||
### Polyfilling
|
### Polyfilling
|
||||||
|
|
||||||
Because we're using the fancy new ES6 syntax, you will need to polyfill for vintage browsers if you want to use Plyr and still support them. Luckily there's a decent service for this that makes it painless, [polyfill.io](https://polyfill.io).
|
Because we're using the fancy new ES6 syntax, you will need to polyfill for vintage browsers if you want to use Plyr and still support them. Luckily there's a decent service for this that makes it painless, [polyfill.io](https://polyfill.io). Alternatively, you can use the prebuilt polyfilled build but bear in mind this is 20kb larger. I'd suggest working our your own polyfill strategy.
|
||||||
|
|
||||||
## v2.0.18
|
## v2.0.18
|
||||||
|
|
||||||
|
210
controls.md
210
controls.md
@ -1,27 +1,73 @@
|
|||||||
# Controls
|
# Controls
|
||||||
|
|
||||||
This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs.
|
This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. You can pass the following to the `controls` option:
|
||||||
|
|
||||||
## Internationalization using default controls
|
* `Array` of options (this builds the default controls based on your choices)
|
||||||
|
* `String` containing the desired HTML
|
||||||
|
* `Function` that will be executed and should return one of the above
|
||||||
|
|
||||||
|
## Using default controls
|
||||||
|
|
||||||
|
If you want to use the standard controls as they are, you don't need to pass any options. If you want to turn on off controls, here's the full list:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
controls: [
|
||||||
|
'play-large', // The large play button in the center
|
||||||
|
'restart', // Restart playback
|
||||||
|
'rewind', // Rewind by the seek time (default 10 seconds)
|
||||||
|
'play', // Play/pause playback
|
||||||
|
'fast-forward', // Fast forward by the seek time (default 10 seconds)
|
||||||
|
'progress', // The progress bar and scrubber for playback and buffering
|
||||||
|
'current-time', // The current time of playback
|
||||||
|
'duration', // The full duration of the media
|
||||||
|
'mute', // Toggle mute
|
||||||
|
'volume', // Volume control
|
||||||
|
'captions', // Toggle captions
|
||||||
|
'settings', // Settings menu
|
||||||
|
'pip', // Picture-in-picture (currently Safari only)
|
||||||
|
'airplay', // Airplay (currently Safari only)
|
||||||
|
'fullscreen', // Toggle fullscreen
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Internationalization using default controls
|
||||||
|
|
||||||
You can provide an `i18n` object as one of your options when initialising the plugin which we be used when rendering the controls.
|
You can provide an `i18n` object as one of your options when initialising the plugin which we be used when rendering the controls.
|
||||||
|
|
||||||
### Example
|
#### Example
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
i18n: {
|
i18n: {
|
||||||
restart: "Restart",
|
restart: 'Restart',
|
||||||
rewind: "Rewind {seektime} secs",
|
rewind: 'Rewind {seektime} secs',
|
||||||
play: "Play",
|
play: 'Play',
|
||||||
pause: "Pause",
|
pause: 'Pause',
|
||||||
forward: "Forward {seektime} secs",
|
fastForward: 'Forward {seektime} secs',
|
||||||
buffered: "buffered",
|
seek: 'Seek',
|
||||||
currentTime: "Current time",
|
played: 'Played',
|
||||||
duration: "Duration",
|
buffered: 'Buffered',
|
||||||
volume: "Volume",
|
currentTime: 'Current time',
|
||||||
toggleMute: "Toggle Mute",
|
duration: 'Duration',
|
||||||
toggleCaptions: "Toggle Captions",
|
volume: 'Volume',
|
||||||
toggleFullscreen: "Toggle Fullscreen"
|
mute: 'Mute',
|
||||||
|
unmute: 'Unmute',
|
||||||
|
enableCaptions: 'Enable captions',
|
||||||
|
disableCaptions: 'Disable captions',
|
||||||
|
enterFullscreen: 'Enter fullscreen',
|
||||||
|
exitFullscreen: 'Exit fullscreen',
|
||||||
|
frameTitle: 'Player for {title}',
|
||||||
|
captions: 'Captions',
|
||||||
|
settings: 'Settings',
|
||||||
|
speed: 'Speed',
|
||||||
|
normal: 'Normal',
|
||||||
|
quality: 'Quality',
|
||||||
|
loop: 'Loop',
|
||||||
|
start: 'Start',
|
||||||
|
end: 'End',
|
||||||
|
all: 'All',
|
||||||
|
reset: 'Reset',
|
||||||
|
disabled: 'Disabled',
|
||||||
|
advertisement: 'Ad',
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -29,85 +75,79 @@ Note: `{seektime}` will be replaced with your configured seek time or the defaul
|
|||||||
|
|
||||||
## Using custom HTML
|
## Using custom HTML
|
||||||
|
|
||||||
You can specify the HTML for the controls using the `html` option.
|
You can specify the HTML as a `String` or your `Function` return for the controls using the `controls` option.
|
||||||
|
|
||||||
The classes and data attributes used in your template should match the `selectors` option.
|
The classes and data attributes used in your template should match the `selectors` option if you change any.
|
||||||
|
|
||||||
You need to add several placeholders to your html template that are replaced when rendering:
|
You need to add several placeholders to your HTML template that are replaced when rendering:
|
||||||
|
|
||||||
- `{id}` - the dynamically generated ID for the player (for form controls)
|
* `{id}` - the dynamically generated ID for the player (for form controls)
|
||||||
- `{seektime}` - the seek time specified in options for fast forward and rewind
|
* `{seektime}` - the seek time specified in options for fast forward and rewind
|
||||||
- `{title}` - the title of your media, if specified
|
* `{title}` - the title of your media, if specified
|
||||||
|
|
||||||
You can include only the controls you need when specifying custom html.
|
### Limitations
|
||||||
|
|
||||||
|
* Currently the settings menus are not supported with custom controls HTML
|
||||||
|
* AirPlay and PiP buttons can be added but you will have to manage feature detection
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
This is an example `html` option with all controls.
|
Here's an example of custom controls markup (this is just all default controls shown).
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var controls = ["<div class='plyr__controls'>",
|
const controls = `
|
||||||
"<button type='button' data-plyr='restart'>",
|
<div class="plyr__controls">
|
||||||
"<svg><use xlink:href='#plyr-restart'></use></svg>",
|
<button type="button" class="plyr__control" data-plyr="restart">
|
||||||
"<span class='plyr__sr-only'>Restart</span>",
|
<svg role="presentation"><use xlink:href="#plyr-restart"></use></svg>
|
||||||
"</button>",
|
<span class="plyr__tooltip" role="tooltip">Restart</span>
|
||||||
"<button type='button' data-plyr='rewind'>",
|
</button>
|
||||||
"<svg><use xlink:href='#plyr-rewind'></use></svg>",
|
<button type="button" class="plyr__control" data-plyr="rewind">
|
||||||
"<span class='plyr__sr-only'>Rewind {seektime} secs</span>",
|
<svg role="presentation"><use xlink:href="#plyr-rewind"></use></svg>
|
||||||
"</button>",
|
<span class="plyr__tooltip" role="tooltip">Rewind {seektime} secs</span>
|
||||||
"<button type='button' data-plyr='play'>",
|
</button>
|
||||||
"<svg><use xlink:href='#plyr-play'></use></svg>",
|
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Play, {title}" data-plyr="play">
|
||||||
"<span class='plyr__sr-only'>Play</span>",
|
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-pause"></use></svg>
|
||||||
"</button>",
|
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-play"></use></svg>
|
||||||
"<button type='button' data-plyr='pause'>",
|
<span class="label--pressed plyr__tooltip" role="tooltip">Pause</span>
|
||||||
"<svg><use xlink:href='#plyr-pause'></use></svg>",
|
<span class="label--not-pressed plyr__tooltip" role="tooltip">Play</span>
|
||||||
"<span class='plyr__sr-only'>Pause</span>",
|
</button>
|
||||||
"</button>",
|
<button type="button" class="plyr__control" data-plyr="fast-forward">
|
||||||
"<button type='button' data-plyr='fast-forward'>",
|
<svg role="presentation"><use xlink:href="#plyr-fast-forward"></use></svg>
|
||||||
"<svg><use xlink:href='#plyr-fast-forward'></use></svg>",
|
<span class="plyr__tooltip" role="tooltip">Forward {seektime} secs</span>
|
||||||
"<span class='plyr__sr-only'>Forward {seektime} secs</span>",
|
</button>
|
||||||
"</button>",
|
<div class="plyr__progress">
|
||||||
"<span class='plyr__progress'>",
|
<label for="plyr-seek-{id}" class="plyr__sr-only">Seek</label>
|
||||||
"<label for='seek{id}' class='plyr__sr-only'>Seek</label>",
|
<input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" id="plyr-seek-{id}">
|
||||||
"<input id='seek{id}' class='plyr__progress--seek' type='range' min='0' max='100' step='0.1' value='0' data-plyr='seek'>",
|
<progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress>
|
||||||
"<progress class='plyr__progress--played' max='100' value='0' role='presentation'></progress>",
|
<span role="tooltip" class="plyr__tooltip">00:00</span>
|
||||||
"<progress class='plyr__progress--buffer' max='100' value='0'>",
|
</div>
|
||||||
"<span>0</span>% buffered",
|
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
|
||||||
"</progress>",
|
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
|
||||||
"<span class='plyr__tooltip'>00:00</span>",
|
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
|
||||||
"</span>",
|
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
|
||||||
"<span class='plyr__time'>",
|
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
|
||||||
"<span class='plyr__sr-only'>Current time</span>",
|
<span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span>
|
||||||
"<span class='plyr__time--current'>00:00</span>",
|
<span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span>
|
||||||
"</span>",
|
</button>
|
||||||
"<span class='plyr__time'>",
|
<div class="plyr__volume">
|
||||||
"<span class='plyr__sr-only'>Duration</span>",
|
<label for="plyr-volume-{id}" class="plyr__sr-only">Volume</label>
|
||||||
"<span class='plyr__time--duration'>00:00</span>",
|
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" id="plyr-volume-{id}">
|
||||||
"</span>",
|
</div>
|
||||||
"<button type='button' data-plyr='mute'>",
|
<button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions">
|
||||||
"<svg class='icon--muted'><use xlink:href='#plyr-muted'></use></svg>",
|
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
|
||||||
"<svg><use xlink:href='#plyr-volume'></use></svg>",
|
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-captions-off"></use></svg>
|
||||||
"<span class='plyr__sr-only'>Toggle Mute</span>",
|
<span class="label--pressed plyr__tooltip" role="tooltip">Disable captions</span>
|
||||||
"</button>",
|
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enable captions</span>
|
||||||
"<span class='plyr__volume'>",
|
</button>
|
||||||
"<label for='volume{id}' class='plyr__sr-only'>Volume</label>",
|
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Enter fullscreen" data-plyr="fullscreen">
|
||||||
"<input id='volume{id}' class='plyr__volume--input' type='range' min='0' max='10' value='5' data-plyr='volume'>",
|
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg>
|
||||||
"<progress class='plyr__volume--display' max='10' value='0' role='presentation'></progress>",
|
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg>
|
||||||
"</span>",
|
<span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span>
|
||||||
"<button type='button' data-plyr='captions'>",
|
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enter fullscreen</span>
|
||||||
"<svg class='icon--captions-on'><use xlink:href='#plyr-captions-on'></use></svg>",
|
</button>
|
||||||
"<svg><use xlink:href='#plyr-captions-off'></use></svg>",
|
</div>
|
||||||
"<span class='plyr__sr-only'>Toggle Captions</span>",
|
`;
|
||||||
"</button>",
|
|
||||||
"<button type='button' data-plyr='fullscreen'>",
|
|
||||||
"<svg class='icon--exit-fullscreen'><use xlink:href='#plyr-exit-fullscreen'></use></svg>",
|
|
||||||
"<svg><use xlink:href='#plyr-enter-fullscreen'></use></svg>",
|
|
||||||
"<span class='plyr__sr-only'>Toggle Fullscreen</span>",
|
|
||||||
"</button>",
|
|
||||||
"</div>"].join("");
|
|
||||||
|
|
||||||
// Setup the player
|
// Setup the player
|
||||||
plyr.setup('.js-player', {
|
const player = new Plyr('#player', { controls });
|
||||||
html: controls
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
2
demo/dist/demo.css
vendored
2
demo/dist/demo.css
vendored
File diff suppressed because one or more lines are too long
4291
demo/dist/demo.js
vendored
4291
demo/dist/demo.js
vendored
File diff suppressed because it is too large
Load Diff
2
demo/dist/demo.js.map
vendored
2
demo/dist/demo.js.map
vendored
File diff suppressed because one or more lines are too long
2
demo/dist/demo.min.js
vendored
Normal file
2
demo/dist/demo.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
demo/dist/demo.min.js.map
vendored
Normal file
1
demo/dist/demo.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
demo/dist/error.css
vendored
Normal file
1
demo/dist/error.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -6,8 +6,14 @@
|
|||||||
<title>Doh. Looks like something went wrong.</title>
|
<title>Doh. Looks like something went wrong.</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<!-- Icons -->
|
||||||
|
<link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico">
|
||||||
|
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32">
|
||||||
|
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png">
|
||||||
|
|
||||||
<!-- Docs styles -->
|
<!-- Docs styles -->
|
||||||
<link rel="stylesheet" href="dist/error.css">
|
<link rel="stylesheet" href="dist/error.css?v=2">
|
||||||
|
|
||||||
<!-- Preload -->
|
<!-- Preload -->
|
||||||
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
|
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
|
||||||
<!-- Docs styles -->
|
<!-- Docs styles -->
|
||||||
<link rel="stylesheet" href="dist/demo.css">
|
<link rel="stylesheet" href="dist/demo.css?v=2">
|
||||||
|
|
||||||
<!-- Preload -->
|
<!-- Preload -->
|
||||||
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
|
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
|
||||||
@ -66,7 +66,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>Premium video monitization from
|
<p>Premium video monitization from
|
||||||
<a href="https://vi.ai/publisher-video-monetization/" target="_blank" class="no-border">
|
<a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
|
||||||
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi">
|
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi">
|
||||||
<span class="sr-only">ai.vi</span>
|
<span class="sr-only">ai.vi</span>
|
||||||
</a>
|
</a>
|
||||||
@ -93,16 +93,18 @@
|
|||||||
<main>
|
<main>
|
||||||
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
|
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
|
||||||
<!-- Video files -->
|
<!-- Video files -->
|
||||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" type="video/mp4">
|
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
|
||||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.webm" type="video/webm">
|
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
|
||||||
|
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
|
||||||
|
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4" type="video/mp4" size="1440">
|
||||||
|
|
||||||
<!-- Text track file -->
|
<!-- Caption files -->
|
||||||
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
|
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
|
||||||
default>
|
default>
|
||||||
<track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
|
<track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
|
||||||
|
|
||||||
<!-- Fallback for browsers that don't support the <video> element -->
|
<!-- Fallback for browsers that don't support the <video> element -->
|
||||||
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" download>Download</a>
|
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
@ -112,7 +114,7 @@
|
|||||||
<title>HTML5</title>
|
<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>
|
<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>
|
</svg>
|
||||||
<a href="http://viewfromabluemoon.com/" target="_blank">View From A Blue Moon</a> © Brainfarm
|
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank">View From A Blue Moon</a> © Brainfarm
|
||||||
</small>
|
</small>
|
||||||
</li>
|
</li>
|
||||||
<li class="plyr__cite plyr__cite--audio" hidden>
|
<li class="plyr__cite plyr__cite--audio" hidden>
|
||||||
@ -139,7 +141,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="plyr__cite plyr__cite--vimeo" hidden>
|
<li class="plyr__cite plyr__cite--vimeo" hidden>
|
||||||
<small>
|
<small>
|
||||||
<a href="https://vimeo.com/ondemand/viewfromabluemoon4k" target="_blank">View From A Blue Moon</a> on
|
<a href="https://vimeo.com/76979871" target="_blank">The New Vimeo Player</a> on
|
||||||
<span class="color--vimeo">
|
<span class="color--vimeo">
|
||||||
<svg class="icon" role="presentation">
|
<svg class="icon" role="presentation">
|
||||||
<title>Vimeo</title>
|
<title>Vimeo</title>
|
||||||
@ -163,25 +165,26 @@
|
|||||||
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"></path>
|
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<p>If you think Plyr's good,
|
<p>If you think Plyr's good,
|
||||||
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&url=http%3A%2F%2Fplyr.io&via=Sam_Potts"
|
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&url=http%3A%2F%2Fplyr.io&via=Sam_Potts"
|
||||||
target="_blank" data-shr-network="twitter">tweet it</a>
|
target="_blank" data-shr-network="twitter">tweet it</a>
|
||||||
</p>
|
</p>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Polyfills -->
|
<!-- Polyfills -->
|
||||||
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent"></script>
|
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<!-- Plyr core script -->
|
<!-- Plyr core script -->
|
||||||
<script src="../dist/plyr.js"></script>
|
<script src="../dist/plyr.js" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<!-- Sharing libary (https://shr.one) -->
|
<!-- Sharing libary (https://shr.one) -->
|
||||||
<script src="https://cdn.shr.one/1.0.1/shr.js"></script>
|
<script src="https://cdn.shr.one/1.0.1/shr.js" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<!-- Rangetouch to fix <input type="range"> on touch devices (see https://rangetouch.com) -->
|
<!-- Rangetouch to fix <input type="range"> on touch devices (see https://rangetouch.com) -->
|
||||||
<script src="https://cdn.rangetouch.com/1.0.1/rangetouch.js" async></script>
|
<script src="https://cdn.rangetouch.com/1.0.1/rangetouch.js" async crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<!-- Docs script -->
|
<!-- Docs script -->
|
||||||
<script src="dist/demo.js"></script>
|
<script src="dist/demo.js" crossorigin="anonymous"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -4,7 +4,19 @@
|
|||||||
// Please see readme.md in the root or github.com/sampotts/plyr
|
// Please see readme.md in the root or github.com/sampotts/plyr
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
import Raven from 'raven-js';
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const isLive = window.location.host === 'plyr.io';
|
||||||
|
|
||||||
|
// Raven / Sentry
|
||||||
|
// For demo site (https://plyr.io) only
|
||||||
|
if (isLive) {
|
||||||
|
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
Raven.context(() => {
|
||||||
if (window.shr) {
|
if (window.shr) {
|
||||||
window.shr.setup({
|
window.shr.setup({
|
||||||
count: {
|
count: {
|
||||||
@ -29,7 +41,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Delay the adding of classname until the focus has changed
|
// Delay the adding of classname until the focus has changed
|
||||||
// This event fires before the focusin event
|
// This event fires before the focusin event
|
||||||
window.setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.activeElement.classList.add(tabClassName);
|
document.activeElement.classList.add(tabClassName);
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
@ -38,13 +50,63 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const player = new Plyr('#player', {
|
const player = new Plyr('#player', {
|
||||||
debug: true,
|
debug: true,
|
||||||
title: 'View From A Blue Moon',
|
title: 'View From A Blue Moon',
|
||||||
// iconUrl: '../dist/plyr.svg',
|
iconUrl: '../dist/plyr.svg',
|
||||||
keyboard: {
|
keyboard: {
|
||||||
global: true,
|
global: true,
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
controls: true,
|
controls: true,
|
||||||
},
|
},
|
||||||
|
/* controls: [
|
||||||
|
'play-large',
|
||||||
|
'restart',
|
||||||
|
'rewind',
|
||||||
|
'play',
|
||||||
|
'fast-forward',
|
||||||
|
'progress',
|
||||||
|
'current-time',
|
||||||
|
'duration',
|
||||||
|
'mute',
|
||||||
|
'volume',
|
||||||
|
'captions',
|
||||||
|
'settings',
|
||||||
|
'pip',
|
||||||
|
'airplay',
|
||||||
|
'fullscreen',
|
||||||
|
], */
|
||||||
|
/* i18n: {
|
||||||
|
restart: '重新開始',
|
||||||
|
rewind: '快退{seektime}秒',
|
||||||
|
play: '播放',
|
||||||
|
pause: '暫停',
|
||||||
|
fastForward: '快進{seektime}秒',
|
||||||
|
seek: '尋求',
|
||||||
|
played: '發揮',
|
||||||
|
buffered: '緩衝的',
|
||||||
|
currentTime: '當前時間戳',
|
||||||
|
duration: '長短',
|
||||||
|
volume: '音量',
|
||||||
|
mute: '靜音',
|
||||||
|
unmute: '取消靜音',
|
||||||
|
enableCaptions: '開啟字幕',
|
||||||
|
disableCaptions: '關閉字幕',
|
||||||
|
enterFullscreen: '進入全螢幕',
|
||||||
|
exitFullscreen: '退出全螢幕',
|
||||||
|
frameTitle: '球員為{title}',
|
||||||
|
captions: '字幕',
|
||||||
|
settings: '設定',
|
||||||
|
speed: '速度',
|
||||||
|
normal: '正常',
|
||||||
|
quality: '質量',
|
||||||
|
loop: '循環',
|
||||||
|
start: 'Start',
|
||||||
|
end: 'End',
|
||||||
|
all: 'All',
|
||||||
|
reset: '重啟',
|
||||||
|
disabled: '殘',
|
||||||
|
enabled: '啟用',
|
||||||
|
advertisement: '廣告',
|
||||||
|
}, */
|
||||||
captions: {
|
captions: {
|
||||||
active: true,
|
active: true,
|
||||||
},
|
},
|
||||||
@ -53,10 +115,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
},
|
},
|
||||||
ads: {
|
ads: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
publisherId: '918848828995742',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Expose for testing
|
// Expose for tinkering in the console
|
||||||
window.player = player;
|
window.player = player;
|
||||||
|
|
||||||
// Setup type toggle
|
// Setup type toggle
|
||||||
@ -89,10 +152,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
player.source = {
|
player.source = {
|
||||||
type: 'video',
|
type: 'video',
|
||||||
title: 'View From A Blue Moon',
|
title: 'View From A Blue Moon',
|
||||||
sources: [{
|
sources: [
|
||||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4',
|
{
|
||||||
|
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
}],
|
size: 576,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
|
||||||
|
type: 'video/mp4',
|
||||||
|
size: 720,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
|
||||||
|
type: 'video/mp4',
|
||||||
|
size: 1080,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
|
||||||
|
type: 'video/mp4',
|
||||||
|
size: 1440,
|
||||||
|
},
|
||||||
|
],
|
||||||
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
|
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
|
||||||
tracks: [
|
tracks: [
|
||||||
{
|
{
|
||||||
@ -134,7 +215,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
case types.youtube:
|
case types.youtube:
|
||||||
player.source = {
|
player.source = {
|
||||||
type: 'video',
|
type: 'video',
|
||||||
title: 'View From A Blue Moon',
|
|
||||||
sources: [{
|
sources: [{
|
||||||
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
||||||
provider: 'youtube',
|
provider: 'youtube',
|
||||||
@ -219,12 +299,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
newSource(currentType, true);
|
newSource(currentType, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Google analytics
|
// Google analytics
|
||||||
// For demo site (https://plyr.io) only
|
// For demo site (https://plyr.io) only
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
if (window.location.host === 'plyr.io') {
|
if (isLive) {
|
||||||
(function(i, s, o, g, r, a, m) {
|
(function(i, s, o, g, r, a, m) {
|
||||||
i.GoogleAnalyticsObject = r;
|
i.GoogleAnalyticsObject = r;
|
||||||
i[r] =
|
i[r] =
|
||||||
@ -238,8 +319,9 @@ if (window.location.host === 'plyr.io') {
|
|||||||
a.async = 1;
|
a.async = 1;
|
||||||
a.src = g;
|
a.src = g;
|
||||||
m.parentNode.insertBefore(a, m);
|
m.parentNode.insertBefore(a, m);
|
||||||
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
|
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
|
||||||
window.ga('create', 'UA-40881672-11', 'auto');
|
window.ga('create', 'UA-40881672-11', 'auto');
|
||||||
window.ga('send', 'pageview');
|
window.ga('send', 'pageview');
|
||||||
}
|
}
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
})();
|
||||||
|
@ -3,12 +3,6 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@charset 'UTF-8';
|
@charset 'UTF-8';
|
||||||
|
|
||||||
// Libs
|
|
||||||
@import '../lib/fontface';
|
|
||||||
@import '../lib/mixins';
|
|
||||||
@import '../lib/normalize';
|
|
||||||
@import '../lib/reset';
|
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
@import '../settings/colors';
|
@import '../settings/colors';
|
||||||
@import '../settings/cosmetic';
|
@import '../settings/cosmetic';
|
||||||
@ -17,6 +11,12 @@
|
|||||||
@import '../settings/spacing';
|
@import '../settings/spacing';
|
||||||
@import '../settings/type';
|
@import '../settings/type';
|
||||||
|
|
||||||
|
// Libs
|
||||||
|
@import '../lib/fontface';
|
||||||
|
@import '../lib/mixins';
|
||||||
|
@import '../lib/normalize';
|
||||||
|
@import '../lib/reset';
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
@import '../layout/error';
|
@import '../layout/error';
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ video {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Style full supported player
|
// Style full supported player
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
// Layout
|
// Layout
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
$container-max-width: 1280px;
|
$container-max-width: 1260px;
|
||||||
|
@ -16,3 +16,4 @@ $plyr-font-size-captions-base: $plyr-font-size-base;
|
|||||||
$plyr-font-size-captions-small: $plyr-font-size-small;
|
$plyr-font-size-captions-small: $plyr-font-size-small;
|
||||||
$plyr-font-size-captions-medium: 18px;
|
$plyr-font-size-captions-medium: 18px;
|
||||||
$plyr-font-size-captions-large: 21px;
|
$plyr-font-size-captions-large: 21px;
|
||||||
|
$plyr-font-size-menu: $plyr-font-size-base;
|
||||||
|
@ -6,5 +6,6 @@ h1 {
|
|||||||
@include font-size($font-size-h1);
|
@include font-size($font-size-h1);
|
||||||
font-weight: $font-weight-bold;
|
font-weight: $font-weight-bold;
|
||||||
letter-spacing: $letter-spacing-headings;
|
letter-spacing: $letter-spacing-headings;
|
||||||
margin: 0 0 ($spacing-base / 2);
|
line-height: 1.2;
|
||||||
|
margin: 0 0 $spacing-base;
|
||||||
}
|
}
|
||||||
|
2
dist/plyr.css
vendored
2
dist/plyr.css
vendored
File diff suppressed because one or more lines are too long
8161
dist/plyr.js
vendored
8161
dist/plyr.js
vendored
File diff suppressed because one or more lines are too long
2
dist/plyr.js.map
vendored
2
dist/plyr.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/plyr.min.js
vendored
Normal file
2
dist/plyr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/plyr.min.js.map
vendored
Normal file
1
dist/plyr.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
13570
dist/plyr.polyfilled.js
vendored
Normal file
13570
dist/plyr.polyfilled.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
dist/plyr.polyfilled.js.map
vendored
Normal file
1
dist/plyr.polyfilled.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/plyr.polyfilled.min.js
vendored
Normal file
2
dist/plyr.polyfilled.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/plyr.polyfilled.min.js.map
vendored
Normal file
1
dist/plyr.polyfilled.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
93
gulpfile.js
93
gulpfile.js
@ -9,9 +9,11 @@ const path = require('path');
|
|||||||
const gulp = require('gulp');
|
const gulp = require('gulp');
|
||||||
const gutil = require('gulp-util');
|
const gutil = require('gulp-util');
|
||||||
const concat = require('gulp-concat');
|
const concat = require('gulp-concat');
|
||||||
|
const filter = require('gulp-filter');
|
||||||
const sass = require('gulp-sass');
|
const sass = require('gulp-sass');
|
||||||
const cleancss = require('gulp-clean-css');
|
const cleancss = require('gulp-clean-css');
|
||||||
const run = require('run-sequence');
|
const run = require('run-sequence');
|
||||||
|
const header = require('gulp-header');
|
||||||
const prefix = require('gulp-autoprefixer');
|
const prefix = require('gulp-autoprefixer');
|
||||||
const gitbranch = require('git-branch');
|
const gitbranch = require('git-branch');
|
||||||
const svgstore = require('gulp-svgstore');
|
const svgstore = require('gulp-svgstore');
|
||||||
@ -24,8 +26,7 @@ const size = require('gulp-size');
|
|||||||
const rollup = require('gulp-better-rollup');
|
const rollup = require('gulp-better-rollup');
|
||||||
const babel = require('rollup-plugin-babel');
|
const babel = require('rollup-plugin-babel');
|
||||||
const sourcemaps = require('gulp-sourcemaps');
|
const sourcemaps = require('gulp-sourcemaps');
|
||||||
const uglify = require('rollup-plugin-uglify');
|
const uglify = require('gulp-uglify-es').default;
|
||||||
const { minify } = require('uglify-es');
|
|
||||||
const commonjs = require('rollup-plugin-commonjs');
|
const commonjs = require('rollup-plugin-commonjs');
|
||||||
const resolve = require('rollup-plugin-node-resolve');
|
const resolve = require('rollup-plugin-node-resolve');
|
||||||
|
|
||||||
@ -40,6 +41,8 @@ try {
|
|||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minSuffix = '.min';
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
const root = __dirname;
|
const root = __dirname;
|
||||||
const paths = {
|
const paths = {
|
||||||
@ -68,8 +71,11 @@ const paths = {
|
|||||||
root: path.join(root, 'demo/'),
|
root: path.join(root, 'demo/'),
|
||||||
},
|
},
|
||||||
upload: [
|
upload: [
|
||||||
path.join(root, 'dist/**'),
|
path.join(root, `dist/*${minSuffix}.*`),
|
||||||
path.join(root, 'demo/dist/**'),
|
path.join(root, 'dist/*.css'),
|
||||||
|
path.join(root, 'dist/*.svg'),
|
||||||
|
path.join(root, `demo/dist/*${minSuffix}.*`),
|
||||||
|
path.join(root, 'demo/dist/*.css'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -122,8 +128,9 @@ const build = {
|
|||||||
Object.keys(files).forEach(key => {
|
Object.keys(files).forEach(key => {
|
||||||
const name = `js:${key}`;
|
const name = `js:${key}`;
|
||||||
tasks.js.push(name);
|
tasks.js.push(name);
|
||||||
|
const { output } = paths[bundle];
|
||||||
|
|
||||||
gulp.task(name, () =>
|
return gulp.task(name, () =>
|
||||||
gulp
|
gulp
|
||||||
.src(bundles[bundle].js[key])
|
.src(bundles[bundle].js[key])
|
||||||
.pipe(sourcemaps.init())
|
.pipe(sourcemaps.init())
|
||||||
@ -135,15 +142,20 @@ const build = {
|
|||||||
resolve(),
|
resolve(),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
babel(babelrc),
|
babel(babelrc),
|
||||||
uglify({}, minify),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(size(sizeOptions))
|
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
|
||||||
.pipe(sourcemaps.write(''))
|
.pipe(sourcemaps.write(''))
|
||||||
.pipe(gulp.dest(paths[bundle].output)),
|
.pipe(gulp.dest(output))
|
||||||
|
.pipe(filter('**/*.js'))
|
||||||
|
.pipe(uglify())
|
||||||
|
.pipe(size(sizeOptions))
|
||||||
|
.pipe(rename({ suffix: minSuffix }))
|
||||||
|
.pipe(sourcemaps.write(''))
|
||||||
|
.pipe(gulp.dest(output)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -152,7 +164,7 @@ const build = {
|
|||||||
const name = `sass:${key}`;
|
const name = `sass:${key}`;
|
||||||
tasks.sass.push(name);
|
tasks.sass.push(name);
|
||||||
|
|
||||||
gulp.task(name, () =>
|
return gulp.task(name, () =>
|
||||||
gulp
|
gulp
|
||||||
.src(bundles[bundle].sass[key])
|
.src(bundles[bundle].sass[key])
|
||||||
.pipe(sass())
|
.pipe(sass())
|
||||||
@ -170,7 +182,7 @@ const build = {
|
|||||||
tasks.sprite.push(name);
|
tasks.sprite.push(name);
|
||||||
|
|
||||||
// Process Icons
|
// Process Icons
|
||||||
gulp.task(name, () =>
|
return gulp.task(name, () =>
|
||||||
gulp
|
gulp
|
||||||
.src(paths[bundle].src.sprite)
|
.src(paths[bundle].src.sprite)
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -229,11 +241,11 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
|||||||
const branch = {
|
const branch = {
|
||||||
current: gitbranch.sync(),
|
current: gitbranch.sync(),
|
||||||
master: 'master',
|
master: 'master',
|
||||||
beta: 'beta',
|
develop: 'develop',
|
||||||
};
|
};
|
||||||
const allowed = [
|
const allowed = [
|
||||||
branch.master,
|
branch.master,
|
||||||
branch.beta,
|
branch.develop,
|
||||||
];
|
];
|
||||||
|
|
||||||
const maxAge = 31536000; // 1 year
|
const maxAge = 31536000; // 1 year
|
||||||
@ -245,7 +257,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
demo: {
|
demo: {
|
||||||
uploadPath: branch.current === branch.beta ? 'beta/' : null,
|
uploadPath: branch.current === branch.develop ? 'beta/' : null,
|
||||||
headers: {
|
headers: {
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
|
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
|
||||||
Vary: 'Accept-Encoding',
|
Vary: 'Accept-Encoding',
|
||||||
@ -266,6 +278,24 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
|||||||
const semver = new RegExp(`v${regex}`, 'gi');
|
const semver = new RegExp(`v${regex}`, 'gi');
|
||||||
const localPath = new RegExp('(../)?dist', 'gi');
|
const localPath = new RegExp('(../)?dist', 'gi');
|
||||||
const versionPath = `https://${aws.cdn.domain}/${version}`;
|
const versionPath = `https://${aws.cdn.domain}/${version}`;
|
||||||
|
const cdnpath = new RegExp(`${aws.cdn.domain}/${regex}/`, 'gi');
|
||||||
|
|
||||||
|
gulp.task('version', () => {
|
||||||
|
console.log(`Updating versions to '${version}'...`);
|
||||||
|
|
||||||
|
// Replace versioned URLs in source
|
||||||
|
const files = [
|
||||||
|
'plyr.js',
|
||||||
|
'plyr.polyfilled.js',
|
||||||
|
'defaults.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
return gulp
|
||||||
|
.src(files.map(file => path.join(root, `src/js/${file}`)))
|
||||||
|
.pipe(replace(semver, `v${version}`))
|
||||||
|
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
|
||||||
|
.pipe(gulp.dest(path.join(root, 'src/js/')));
|
||||||
|
});
|
||||||
|
|
||||||
// Publish version to CDN bucket
|
// Publish version to CDN bucket
|
||||||
gulp.task('cdn', () => {
|
gulp.task('cdn', () => {
|
||||||
@ -277,22 +307,26 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
|||||||
console.log(`Uploading '${version}' to ${aws.cdn.domain}...`);
|
console.log(`Uploading '${version}' to ${aws.cdn.domain}...`);
|
||||||
|
|
||||||
// Upload to CDN
|
// Upload to CDN
|
||||||
return gulp
|
return (
|
||||||
|
gulp
|
||||||
.src(paths.upload)
|
.src(paths.upload)
|
||||||
|
.pipe(
|
||||||
|
rename(p => {
|
||||||
|
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
|
||||||
|
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
// Remove min suffix from source map URL
|
||||||
|
.pipe(replace(/sourceMappingURL=([\w-?.]+)/, (match, p1) => `sourceMappingURL=${p1.replace(minSuffix, '')}`))
|
||||||
.pipe(
|
.pipe(
|
||||||
size({
|
size({
|
||||||
showFiles: true,
|
showFiles: true,
|
||||||
gzip: true,
|
gzip: true,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.pipe(
|
|
||||||
rename(p => {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
p.dirname = p.dirname.replace('.', version);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.pipe(replace(localPath, versionPath))
|
.pipe(replace(localPath, versionPath))
|
||||||
.pipe(s3(aws.cdn, options.cdn));
|
.pipe(s3(aws.cdn, options.cdn))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Publish to demo bucket
|
// Publish to demo bucket
|
||||||
@ -304,25 +338,12 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
|||||||
|
|
||||||
console.log(`Uploading '${version}' demo to ${aws.demo.domain}...`);
|
console.log(`Uploading '${version}' demo to ${aws.demo.domain}...`);
|
||||||
|
|
||||||
const cdnpath = new RegExp(`${aws.cdn.domain}/${regex}/`, 'gi');
|
|
||||||
|
|
||||||
// Replace versioned files in readme.md
|
// Replace versioned files in readme.md
|
||||||
gulp
|
gulp
|
||||||
.src([`${root}/readme.md`])
|
.src([`${root}/readme.md`])
|
||||||
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
|
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
|
||||||
.pipe(gulp.dest(root));
|
.pipe(gulp.dest(root));
|
||||||
|
|
||||||
// Replace versioned URLs in source
|
|
||||||
const files = [
|
|
||||||
'plyr.js',
|
|
||||||
'defaults.js',
|
|
||||||
];
|
|
||||||
gulp
|
|
||||||
.src(files.map(file => path.join(root, `src/js/${file}`)))
|
|
||||||
.pipe(replace(semver, `v${version}`))
|
|
||||||
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
|
|
||||||
.pipe(gulp.dest(path.join(root, 'src/js/')));
|
|
||||||
|
|
||||||
// Replace local file paths with remote paths in demo HTML
|
// Replace local file paths with remote paths in demo HTML
|
||||||
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
|
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
|
||||||
const index = `${paths.demo.root}index.html`;
|
const index = `${paths.demo.root}index.html`;
|
||||||
@ -388,7 +409,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Do everything
|
// Do everything
|
||||||
gulp.task('publish', () => {
|
gulp.task('publish', callback => {
|
||||||
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo');
|
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo', callback);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
66
package.json
66
package.json
@ -1,50 +1,56 @@
|
|||||||
{
|
{
|
||||||
"name": "plyr",
|
"name": "plyr",
|
||||||
"version": "3.0.0-beta.13",
|
"version": "3.3.9",
|
||||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||||
"homepage": "https://plyr.io",
|
"homepage": "https://plyr.io",
|
||||||
"main": "./dist/plyr.js",
|
"main": "./dist/plyr.js",
|
||||||
|
"browser": "./dist/plyr.min.js",
|
||||||
"sass": "./src/sass/plyr.scss",
|
"sass": "./src/sass/plyr.scss",
|
||||||
"style": "./dist/plyr.css",
|
"style": "./dist/plyr.css",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.26.0",
|
"babel-core": "^6.26.3",
|
||||||
"babel-eslint": "^8.2.1",
|
"babel-eslint": "^8.2.3",
|
||||||
"babel-plugin-external-helpers": "^6.22.0",
|
"babel-plugin-external-helpers": "^6.22.0",
|
||||||
"babel-preset-env": "^1.6.1",
|
"babel-preset-env": "^1.7.0",
|
||||||
"del": "^3.0.0",
|
"del": "^3.0.0",
|
||||||
"eslint": "^4.17.0",
|
"eslint": "^4.19.1",
|
||||||
"eslint-config-airbnb-base": "^12.1.0",
|
"eslint-config-airbnb-base": "^12.1.0",
|
||||||
"eslint-config-prettier": "^2.9.0",
|
"eslint-config-prettier": "^2.9.0",
|
||||||
"eslint-plugin-import": "^2.8.0",
|
"eslint-plugin-import": "^2.12.0",
|
||||||
"git-branch": "^1.0.0",
|
"git-branch": "^2.0.1",
|
||||||
"gulp": "^3.9.1",
|
"gulp": "^3.9.1",
|
||||||
"gulp-autoprefixer": "^4.1.0",
|
"gulp-autoprefixer": "^5.0.0",
|
||||||
"gulp-better-rollup": "^3.0.0",
|
"gulp-better-rollup": "^3.1.0",
|
||||||
"gulp-clean-css": "^3.9.2",
|
"gulp-clean-css": "^3.9.4",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-open": "^2.1.0",
|
"gulp-filter": "^5.1.0",
|
||||||
"gulp-rename": "^1.2.2",
|
"gulp-header": "^2.0.5",
|
||||||
"gulp-replace": "^0.6.1",
|
"gulp-open": "^3.0.1",
|
||||||
|
"gulp-postcss": "^7.0.1",
|
||||||
|
"gulp-rename": "^1.2.3",
|
||||||
|
"gulp-replace": "^1.0.0",
|
||||||
"gulp-s3": "^0.11.0",
|
"gulp-s3": "^0.11.0",
|
||||||
"gulp-sass": "^3.1.0",
|
"gulp-sass": "^4.0.1",
|
||||||
"gulp-size": "^3.0.0",
|
"gulp-size": "^3.0.0",
|
||||||
"gulp-sourcemaps": "^2.6.4",
|
"gulp-sourcemaps": "^2.6.4",
|
||||||
"gulp-svgmin": "^1.2.4",
|
"gulp-svgmin": "^1.2.4",
|
||||||
"gulp-svgstore": "^6.1.1",
|
"gulp-svgstore": "^6.1.1",
|
||||||
|
"gulp-uglify-es": "^1.0.4",
|
||||||
"gulp-util": "^3.0.8",
|
"gulp-util": "^3.0.8",
|
||||||
"rollup-plugin-babel": "^3.0.3",
|
"postcss-custom-properties": "^7.0.0",
|
||||||
"rollup-plugin-commonjs": "^8.3.0",
|
"prettier-eslint": "^8.8.1",
|
||||||
"rollup-plugin-node-resolve": "^3.0.2",
|
"prettier-stylelint": "^0.4.2",
|
||||||
"rollup-plugin-uglify": "^3.0.0",
|
"rollup-plugin-babel": "^3.0.4",
|
||||||
|
"rollup-plugin-commonjs": "^9.1.3",
|
||||||
|
"rollup-plugin-node-resolve": "^3.3.0",
|
||||||
"run-sequence": "^2.2.1",
|
"run-sequence": "^2.2.1",
|
||||||
"stylelint": "^8.4.0",
|
"stylelint": "^9.2.1",
|
||||||
"stylelint-config-prettier": "^2.0.0",
|
"stylelint-config-prettier": "^3.2.0",
|
||||||
"stylelint-config-sass-guidelines": "^4.1.0",
|
"stylelint-config-recommended": "^2.1.0",
|
||||||
"stylelint-config-standard": "^18.0.0",
|
"stylelint-config-sass-guidelines": "^5.0.0",
|
||||||
"stylelint-order": "^0.8.0",
|
"stylelint-order": "^0.8.1",
|
||||||
"stylelint-scss": "^2.3.0",
|
"stylelint-scss": "^3.1.0",
|
||||||
"stylelint-selector-bem-pattern": "^2.0.0",
|
"stylelint-selector-bem-pattern": "^2.0.0"
|
||||||
"uglify-es": "^3.3.10"
|
|
||||||
},
|
},
|
||||||
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
|
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
|
||||||
"repository": {
|
"repository": {
|
||||||
@ -62,5 +68,11 @@
|
|||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "Sam Potts <sam@potts.es>",
|
"author": "Sam Potts <sam@potts.es>",
|
||||||
"dependencies": {}
|
"dependencies": {
|
||||||
|
"babel-polyfill": "^6.26.0",
|
||||||
|
"custom-event-polyfill": "^0.3.0",
|
||||||
|
"loadjs": "^3.5.4",
|
||||||
|
"raven-js": "^3.25.2",
|
||||||
|
"url-polyfill": "^1.0.13"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
123
readme.md
123
readme.md
@ -1,30 +1,30 @@
|
|||||||
---
|
|
||||||
Beware: This version is currently in beta and not production-ready
|
|
||||||
---
|
|
||||||
|
|
||||||
# Plyr
|
# Plyr
|
||||||
|
|
||||||
A simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo media player that supports [_modern_](#browser-support) browsers.
|
A simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo media player that supports [_modern_](#browser-support) browsers.
|
||||||
|
|
||||||
[Checkout the demo](https://plyr.io) - [Donate to support Plyr](#donate) - [Chat on Slack](https://bit.ly/plyr-slack)
|
[Checkout the demo](https://plyr.io) - [Donate to support Plyr](#donate) - [Chat on Slack](https://bit.ly/plyr-chat)
|
||||||
|
|
||||||
[](https://plyr.io)
|
[](https://plyr.io)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* **Accessible** - full support for VTT captions and screen readers
|
* **Accessible** - full support for VTT captions and screen readers
|
||||||
* **Lightweight** - just 18KB minified and gzipped
|
|
||||||
* **[Customisable](#html)** - make the player look how you want with the markup you want
|
* **[Customisable](#html)** - make the player look how you want with the markup you want
|
||||||
* **Semantic** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
|
* **Good HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
|
||||||
`<span>` or `<a href="#">` button hacks
|
`<span>` or `<a href="#">` button hacks
|
||||||
* **Responsive** - works with any screen size
|
* **Responsive** - works with any screen size
|
||||||
* **HTML Video & Audio** - support for both formats
|
* **HTML Video & Audio** - support for both formats
|
||||||
* **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback
|
* **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback
|
||||||
|
* **[Monetization](#ads)** - make money from your videos
|
||||||
* **[Streaming](#streaming)** - support for hls.js, Shaka and dash.js streaming playback
|
* **[Streaming](#streaming)** - support for hls.js, Shaka and dash.js streaming playback
|
||||||
* **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
|
* **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
|
||||||
* **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
|
* **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
|
||||||
* **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
|
* **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
|
||||||
* **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
|
* **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
|
||||||
|
* **Picture-in-Picture** - supports Safari's picture-in-picture mode
|
||||||
|
* **Playsinline** - supports the `playsinline` attribute
|
||||||
|
* **Speed controls** - adjust speed on the fly
|
||||||
|
* **Multiple captions** - support for multiple caption tracks
|
||||||
* **i18n support** - support for internationalization of controls
|
* **i18n support** - support for internationalization of controls
|
||||||
* **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
|
* **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
|
||||||
* **SASS** - to include in your build processes
|
* **SASS** - to include in your build processes
|
||||||
@ -40,8 +40,8 @@ Check out the [changelog](changelog.md) to see what's new with Plyr.
|
|||||||
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
|
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
|
||||||
|
|
||||||
| Type | Maintainer | Link |
|
| Type | Maintainer | Link |
|
||||||
| --------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
| --------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||||
| WordPress | Ryan Anthony Drake ([@iamryandrake](https://github.com/iamryandrake)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
|
| WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
|
||||||
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
|
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
|
||||||
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
|
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
|
||||||
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
|
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
|
||||||
@ -53,8 +53,7 @@ Here's a quick run through on getting up and running. There's also a [demo on Co
|
|||||||
|
|
||||||
### HTML
|
### HTML
|
||||||
|
|
||||||
Plyr extends upon the standard HTML5 markup so that's all you need for those types. More info on advanced HTML markup can be found under
|
Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) markup so that's all you need for those types.
|
||||||
[initialising](#initialising).
|
|
||||||
|
|
||||||
#### HTML5 Video
|
#### HTML5 Video
|
||||||
|
|
||||||
@ -117,21 +116,26 @@ Or the `<div>` non progressively enhanced method:
|
|||||||
|
|
||||||
### JavaScript
|
### JavaScript
|
||||||
|
|
||||||
Include the `plyr.js` script before the closing `</body>` tag and then call `plyr.setup()`. More info on `setup()` can be found under
|
Include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below.
|
||||||
[initialising](#initialising).
|
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="path/to/plyr.js"></script>
|
<script src="path/to/plyr.js"></script>
|
||||||
<script>const player = new Plyr('#player');</script>
|
<script>const player = new Plyr('#player');</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
|
See [initialising](#initialising) for more information on advanced setups.
|
||||||
|
|
||||||
|
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://cdn.plyr.io/3.0.0-beta.13/plyr.js"></script>
|
<script src="https://cdn.plyr.io/3.3.9/plyr.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
|
...or...
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.plyr.io/3.3.9/plyr.polyfilled.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
### CSS
|
### CSS
|
||||||
|
|
||||||
@ -144,13 +148,23 @@ Include the `plyr.css` stylsheet into your `<head>`
|
|||||||
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
|
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.0.0-beta.13/plyr.css">
|
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.9/plyr.css">
|
||||||
```
|
```
|
||||||
|
|
||||||
### SVG Sprite
|
### SVG Sprite
|
||||||
|
|
||||||
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
|
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
|
||||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.0.0-beta.13/plyr.svg`.
|
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.9/plyr.svg`.
|
||||||
|
|
||||||
|
## Ads
|
||||||
|
|
||||||
|
Plyr has partnered up with [vi.ai](http://vi.ai/publisher-video-monetization/?aid=plyrio) to offer monetization options for your videos. Getting setup is easy:
|
||||||
|
|
||||||
|
* [Sign up for a vi.ai account](http://vi.ai/publisher-video-monetization/?aid=plyrio)
|
||||||
|
* Grab your publisher ID from the code snippet
|
||||||
|
* Enable ads in the [config options](#options) and enter your publisher ID
|
||||||
|
|
||||||
|
Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues.
|
||||||
|
|
||||||
## Advanced
|
## Advanced
|
||||||
|
|
||||||
@ -196,10 +210,11 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
|
|||||||
You can specify a range of arguments for the constructor to use:
|
You can specify a range of arguments for the constructor to use:
|
||||||
|
|
||||||
* A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
|
* A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
|
||||||
* A [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
|
* A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
|
||||||
* A [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or Array of [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) -
|
* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
|
||||||
the first element will be used
|
* A [jQuery](https://jquery.com) object
|
||||||
* A [jQuery](https://jquery.com) object - if multiple are passed, the first element will be used
|
|
||||||
|
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
|
||||||
|
|
||||||
Here's some examples
|
Here's some examples
|
||||||
|
|
||||||
@ -215,15 +230,33 @@ Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElemen
|
|||||||
const player = new Plyr(document.getElementById('player'));
|
const player = new Plyr(document.getElementById('player'));
|
||||||
```
|
```
|
||||||
|
|
||||||
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList):
|
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) (see note below):
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const player = new Plyr(document.querySelectorAll('.js-player'));
|
const player = new Plyr(document.querySelectorAll('.js-player'));
|
||||||
```
|
```
|
||||||
|
|
||||||
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds
|
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds.
|
||||||
|
|
||||||
The second argument for the constructor is the [#options](options) object:
|
##### Setting up multiple players
|
||||||
|
|
||||||
|
You have two choices here. You can either use a simple array loop to map the constructor:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p));
|
||||||
|
```
|
||||||
|
|
||||||
|
...or use a static method where you can pass a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of elements:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const players = Plyr.setup('.js-player');
|
||||||
|
```
|
||||||
|
|
||||||
|
Both options will also return an array of instances in the order of they were in the DOM for the string selector or the source NodeList or Array.
|
||||||
|
|
||||||
|
##### Passing options
|
||||||
|
|
||||||
|
The second argument for the constructor is the [options](#options) object:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const player = new Plyr('#player', {
|
const player = new Plyr('#player', {
|
||||||
@ -231,7 +264,7 @@ const player = new Plyr('#player', {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
The constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
|
In all cases, the constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
|
||||||
|
|
||||||
#### Options
|
#### Options
|
||||||
|
|
||||||
@ -262,7 +295,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
|||||||
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
|
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
|
||||||
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
|
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
|
||||||
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
|
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
|
||||||
| `showPosterOnEnd` | Boolean | false | This will restore and _reload_ HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution. |
|
| `resetOnEnd` | Boolean | false | Reset the playback to the start once playback is complete. |
|
||||||
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
|
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
|
||||||
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
|
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
|
||||||
| `duration` | Number | `null` | Specify a custom duration for media. |
|
| `duration` | Number | `null` | Specify a custom duration for media. |
|
||||||
@ -271,12 +304,13 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
|||||||
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
|
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
|
||||||
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
|
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
|
||||||
| `captions` | Object | `{ active: false, language: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). |
|
| `captions` | Object | `{ active: false, language: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). |
|
||||||
| `fullscreen` | Object | `{ enabled: true, fallback: true }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. |
|
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
|
||||||
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
|
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
|
||||||
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
|
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
|
||||||
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. |
|
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. |
|
||||||
| `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. |
|
| `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. |
|
||||||
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
|
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
|
||||||
|
| `ads` | Object | `{ enabled: false, publisherId: '' }` | `enabled`: Whether to enable vi.ai ads. `publisherId`: Your unique vi.ai publisher ID. |
|
||||||
|
|
||||||
1. Vimeo only
|
1. Vimeo only
|
||||||
|
|
||||||
@ -327,7 +361,7 @@ player.fullscreen.enter(); // Enter fullscreen
|
|||||||
| `fullscreen.exit()` | - | Exit fullscreen. |
|
| `fullscreen.exit()` | - | Exit fullscreen. |
|
||||||
| `fullscreen.toggle()` | - | Toggle fullscreen. |
|
| `fullscreen.toggle()` | - | Toggle fullscreen. |
|
||||||
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
|
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
|
||||||
| `toggleControls(toggle)` | Boolean | Toggle the controls based on the specified boolean. |
|
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
|
||||||
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
|
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
|
||||||
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
|
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
|
||||||
| `supports(type)` | String | Check support for a mime type. |
|
| `supports(type)` | String | Check support for a mime type. |
|
||||||
@ -356,9 +390,11 @@ player.fullscreen.active; // false;
|
|||||||
| -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. |
|
| `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. |
|
||||||
| `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. |
|
| `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. |
|
||||||
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
|
|
||||||
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
|
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
|
||||||
|
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
|
||||||
|
| `stopped` | ✓ | - | Returns a boolean indicating if the current player is stopped. |
|
||||||
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
|
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
|
||||||
|
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
|
||||||
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
|
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
|
||||||
| `seeking` | ✓ | - | Returns a boolean indicating if the current player is seeking. |
|
| `seeking` | ✓ | - | Returns a boolean indicating if the current player is seeking. |
|
||||||
| `duration` | ✓ | - | Returns the duration for the current media. |
|
| `duration` | ✓ | - | Returns the duration for the current media. |
|
||||||
@ -369,7 +405,7 @@ player.fullscreen.active; // false;
|
|||||||
| `quality`¹ | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
|
| `quality`¹ | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
|
||||||
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
|
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
|
||||||
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
|
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
|
||||||
| `poster`² | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
|
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
|
||||||
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
|
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
|
||||||
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. |
|
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. |
|
||||||
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
|
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
|
||||||
@ -580,6 +616,8 @@ Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's
|
|||||||
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
|
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
|
||||||
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
|
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
|
||||||
|
|
||||||
|
_Note_: These need updating to use the new v3 syntax but would still work.
|
||||||
|
|
||||||
## Fullscreen
|
## Fullscreen
|
||||||
|
|
||||||
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
|
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
|
||||||
@ -589,18 +627,19 @@ Fullscreen in Plyr is supported by all browsers that [currently support it](http
|
|||||||
Plyr supports the last 2 versions of most _modern_ browsers.
|
Plyr supports the last 2 versions of most _modern_ browsers.
|
||||||
|
|
||||||
| Browser | Supported |
|
| Browser | Supported |
|
||||||
| ------------- | --------- |
|
| ------------- | ------------- |
|
||||||
| Safari | ✓ |
|
| Safari | ✓ |
|
||||||
| Mobile Safari | ✓¹ |
|
| Mobile Safari | ✓¹ |
|
||||||
| Firefox | ✓ |
|
| Firefox | ✓ |
|
||||||
| Chrome | ✓ |
|
| Chrome | ✓ |
|
||||||
| Opera | ✓ |
|
| Opera | ✓ |
|
||||||
| Edge | ✓ |
|
| Edge | ✓ |
|
||||||
| IE11 | ✓ |
|
| IE11 | ✓³ |
|
||||||
| IE10 | ✓² |
|
| IE10 | ✓²³ |
|
||||||
|
|
||||||
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
|
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
|
||||||
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options))
|
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)).
|
||||||
|
3. Polyfills required. See below.
|
||||||
|
|
||||||
### Polyfills
|
### Polyfills
|
||||||
|
|
||||||
@ -649,8 +688,10 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
|
|||||||
|
|
||||||
## Donate
|
## Donate
|
||||||
|
|
||||||
Plyr costs money to run, not only my time - I donate that for free but domains, hosting and more. Any help is appreciated...
|
Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated...
|
||||||
[Donate to support Plyr](https://www.paypal.me/pottsy/20usd)
|
|
||||||
|
* [Donate via Patron](https://www.patreon.com/plyr)
|
||||||
|
* [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
|
||||||
|
|
||||||
## Mentions
|
## Mentions
|
||||||
|
|
||||||
@ -688,10 +729,14 @@ Credit to the PayPal HTML5 Video player from which Plyr's caption functionality
|
|||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|
||||||
[](https://www.fastly.com/)
|
[](https://www.fastly.com/)
|
||||||
|
|
||||||
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.
|
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.
|
||||||
|
|
||||||
|
[](https://sentry.io/)
|
||||||
|
|
||||||
|
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
|
||||||
|
|
||||||
## Copyright and License
|
## Copyright and License
|
||||||
|
|
||||||
[The MIT license](license.md)
|
[The MIT license](license.md)
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Plyr Captions
|
// Plyr Captions
|
||||||
|
// TODO: Create as class
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
|
import controls from './controls';
|
||||||
|
import i18n from './i18n';
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import controls from './controls';
|
|
||||||
|
|
||||||
const captions = {
|
const captions = {
|
||||||
// Setup captions
|
// Setup captions
|
||||||
@ -56,11 +58,42 @@ const captions = {
|
|||||||
// Set the class hook
|
// Set the class hook
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this)));
|
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this)));
|
||||||
|
|
||||||
|
// Get tracks
|
||||||
|
const tracks = captions.getTracks.call(this);
|
||||||
|
|
||||||
// If no caption file exists, hide container for caption text
|
// If no caption file exists, hide container for caption text
|
||||||
if (utils.is.empty(captions.getTracks.call(this))) {
|
if (utils.is.empty(tracks)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get browser info
|
||||||
|
const browser = utils.getBrowser();
|
||||||
|
|
||||||
|
// Fix IE captions if CORS is used
|
||||||
|
// Fetch captions and inject as blobs instead (data URIs not supported!)
|
||||||
|
if (browser.isIE && window.URL) {
|
||||||
|
const elements = this.media.querySelectorAll('track');
|
||||||
|
|
||||||
|
Array.from(elements).forEach(track => {
|
||||||
|
const src = track.getAttribute('src');
|
||||||
|
const href = utils.parseUrl(src);
|
||||||
|
|
||||||
|
if (href.hostname !== window.location.href.hostname && [
|
||||||
|
'http:',
|
||||||
|
'https:',
|
||||||
|
].includes(href.protocol)) {
|
||||||
|
utils
|
||||||
|
.fetch(src, 'blob')
|
||||||
|
.then(blob => {
|
||||||
|
track.setAttribute('src', window.URL.createObjectURL(blob));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
utils.removeElement(track);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Set language
|
// Set language
|
||||||
captions.setLanguage.call(this);
|
captions.setLanguage.call(this);
|
||||||
|
|
||||||
@ -117,7 +150,49 @@ const captions = {
|
|||||||
|
|
||||||
// Get the current track for the current language
|
// Get the current track for the current language
|
||||||
getCurrentTrack() {
|
getCurrentTrack() {
|
||||||
return captions.getTracks.call(this).find(track => track.language.toLowerCase() === this.language);
|
const tracks = captions.getTracks.call(this);
|
||||||
|
|
||||||
|
if (!tracks.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get track based on current language
|
||||||
|
let track = tracks.find(track => track.language.toLowerCase() === this.language);
|
||||||
|
|
||||||
|
// Get the <track> with default attribute
|
||||||
|
if (!track) {
|
||||||
|
track = utils.getElement.call(this, 'track[default]');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first track
|
||||||
|
if (!track) {
|
||||||
|
[track] = tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
return track;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get UI label for track
|
||||||
|
getLabel(track) {
|
||||||
|
let currentTrack = track;
|
||||||
|
|
||||||
|
if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) {
|
||||||
|
currentTrack = captions.getCurrentTrack.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utils.is.track(currentTrack)) {
|
||||||
|
if (!utils.is.empty(currentTrack.label)) {
|
||||||
|
return currentTrack.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!utils.is.empty(currentTrack.language)) {
|
||||||
|
return track.language.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n.get('enabled', this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n.get('disabled', this.config);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Display active caption if it contains text
|
// Display active caption if it contains text
|
||||||
@ -161,7 +236,7 @@ const captions = {
|
|||||||
|
|
||||||
// Set the span content
|
// Set the span content
|
||||||
if (utils.is.string(caption)) {
|
if (utils.is.string(caption)) {
|
||||||
content.textContent = caption.trim();
|
content.innerText = caption.trim();
|
||||||
} else {
|
} else {
|
||||||
content.appendChild(caption);
|
content.appendChild(caption);
|
||||||
}
|
}
|
||||||
@ -175,11 +250,6 @@ const captions = {
|
|||||||
|
|
||||||
// Display captions container and button (for initialization)
|
// Display captions container and button (for initialization)
|
||||||
show() {
|
show() {
|
||||||
// If there's no caption toggle, bail
|
|
||||||
if (!utils.is.element(this.elements.buttons.captions)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to load the value from storage
|
// Try to load the value from storage
|
||||||
let active = this.storage.get('captions');
|
let active = this.storage.get('captions');
|
||||||
|
|
||||||
|
629
src/js/controls.js
vendored
629
src/js/controls.js
vendored
File diff suppressed because it is too large
Load Diff
@ -47,8 +47,8 @@ const defaults = {
|
|||||||
// Auto hide the controls
|
// Auto hide the controls
|
||||||
hideControls: true,
|
hideControls: true,
|
||||||
|
|
||||||
// Revert to poster on finish (HTML5 - will cause reload)
|
// Reset to start when playback ended
|
||||||
showPosterOnEnd: false,
|
resetOnEnd: false,
|
||||||
|
|
||||||
// Disable the standard context menu
|
// Disable the standard context menu
|
||||||
disableContextMenu: true,
|
disableContextMenu: true,
|
||||||
@ -56,24 +56,26 @@ const defaults = {
|
|||||||
// Sprite (for icons)
|
// Sprite (for icons)
|
||||||
loadSprite: true,
|
loadSprite: true,
|
||||||
iconPrefix: 'plyr',
|
iconPrefix: 'plyr',
|
||||||
iconUrl: 'https://cdn.plyr.io/3.0.0-beta.13/plyr.svg',
|
iconUrl: 'https://cdn.plyr.io/3.3.9/plyr.svg',
|
||||||
|
|
||||||
// Blank video (used to prevent errors on source change)
|
// Blank video (used to prevent errors on source change)
|
||||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||||
|
|
||||||
// Quality default
|
// Quality default
|
||||||
quality: {
|
quality: {
|
||||||
default: 'default',
|
default: 576,
|
||||||
options: [
|
options: [
|
||||||
'hd2160',
|
4320,
|
||||||
'hd1440',
|
2880,
|
||||||
'hd1080',
|
2160,
|
||||||
'hd720',
|
1440,
|
||||||
'large',
|
1080,
|
||||||
'medium',
|
720,
|
||||||
'small',
|
576,
|
||||||
'tiny',
|
480,
|
||||||
'default',
|
360,
|
||||||
|
240,
|
||||||
|
'default', // YouTube's "auto"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -113,7 +115,7 @@ const defaults = {
|
|||||||
// Captions settings
|
// Captions settings
|
||||||
captions: {
|
captions: {
|
||||||
active: false,
|
active: false,
|
||||||
language: window.navigator.language.split('-')[0],
|
language: (navigator.language || navigator.userLanguage).split('-')[0],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fullscreen settings
|
// Fullscreen settings
|
||||||
@ -132,7 +134,10 @@ const defaults = {
|
|||||||
// Default controls
|
// Default controls
|
||||||
controls: [
|
controls: [
|
||||||
'play-large',
|
'play-large',
|
||||||
|
// 'restart',
|
||||||
|
// 'rewind',
|
||||||
'play',
|
'play',
|
||||||
|
// 'fast-forward',
|
||||||
'progress',
|
'progress',
|
||||||
'current-time',
|
'current-time',
|
||||||
'mute',
|
'mute',
|
||||||
@ -152,10 +157,10 @@ const defaults = {
|
|||||||
// Localisation
|
// Localisation
|
||||||
i18n: {
|
i18n: {
|
||||||
restart: 'Restart',
|
restart: 'Restart',
|
||||||
rewind: 'Rewind {seektime} secs',
|
rewind: 'Rewind {seektime}s',
|
||||||
play: 'Play',
|
play: 'Play',
|
||||||
pause: 'Pause',
|
pause: 'Pause',
|
||||||
forward: 'Forward {seektime} secs',
|
fastForward: 'Forward {seektime}s',
|
||||||
seek: 'Seek',
|
seek: 'Seek',
|
||||||
played: 'Played',
|
played: 'Played',
|
||||||
buffered: 'Buffered',
|
buffered: 'Buffered',
|
||||||
@ -172,27 +177,31 @@ const defaults = {
|
|||||||
captions: 'Captions',
|
captions: 'Captions',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
speed: 'Speed',
|
speed: 'Speed',
|
||||||
|
normal: 'Normal',
|
||||||
quality: 'Quality',
|
quality: 'Quality',
|
||||||
loop: 'Loop',
|
loop: 'Loop',
|
||||||
start: 'Start',
|
start: 'Start',
|
||||||
end: 'End',
|
end: 'End',
|
||||||
all: 'All',
|
all: 'All',
|
||||||
reset: 'Reset',
|
reset: 'Reset',
|
||||||
none: 'None',
|
|
||||||
disabled: 'Disabled',
|
disabled: 'Disabled',
|
||||||
advertisment: 'Ad',
|
enabled: 'Enabled',
|
||||||
|
advertisement: 'Ad',
|
||||||
},
|
},
|
||||||
|
|
||||||
// URLs
|
// URLs
|
||||||
urls: {
|
urls: {
|
||||||
vimeo: {
|
vimeo: {
|
||||||
api: 'https://player.vimeo.com/api/player.js',
|
sdk: 'https://player.vimeo.com/api/player.js',
|
||||||
|
iframe: 'https://player.vimeo.com/video/{0}?{1}',
|
||||||
|
api: 'https://vimeo.com/api/v2/video/{0}.json',
|
||||||
},
|
},
|
||||||
youtube: {
|
youtube: {
|
||||||
api: 'https://www.youtube.com/iframe_api',
|
sdk: 'https://www.youtube.com/iframe_api',
|
||||||
|
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
|
||||||
},
|
},
|
||||||
googleIMA: {
|
googleIMA: {
|
||||||
api: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -203,7 +212,7 @@ const defaults = {
|
|||||||
pause: null,
|
pause: null,
|
||||||
restart: null,
|
restart: null,
|
||||||
rewind: null,
|
rewind: null,
|
||||||
forward: null,
|
fastForward: null,
|
||||||
mute: null,
|
mute: null,
|
||||||
volume: null,
|
volume: null,
|
||||||
captions: null,
|
captions: null,
|
||||||
@ -259,7 +268,7 @@ const defaults = {
|
|||||||
// Ads
|
// Ads
|
||||||
'adsloaded',
|
'adsloaded',
|
||||||
'adscontentpause',
|
'adscontentpause',
|
||||||
'adsconentresume',
|
'adscontentresume',
|
||||||
'adstarted',
|
'adstarted',
|
||||||
'adsmidpoint',
|
'adsmidpoint',
|
||||||
'adscomplete',
|
'adscomplete',
|
||||||
@ -283,7 +292,7 @@ const defaults = {
|
|||||||
pause: '[data-plyr="pause"]',
|
pause: '[data-plyr="pause"]',
|
||||||
restart: '[data-plyr="restart"]',
|
restart: '[data-plyr="restart"]',
|
||||||
rewind: '[data-plyr="rewind"]',
|
rewind: '[data-plyr="rewind"]',
|
||||||
forward: '[data-plyr="fast-forward"]',
|
fastForward: '[data-plyr="fast-forward"]',
|
||||||
mute: '[data-plyr="mute"]',
|
mute: '[data-plyr="mute"]',
|
||||||
captions: '[data-plyr="captions"]',
|
captions: '[data-plyr="captions"]',
|
||||||
fullscreen: '[data-plyr="fullscreen"]',
|
fullscreen: '[data-plyr="fullscreen"]',
|
||||||
@ -316,16 +325,19 @@ const defaults = {
|
|||||||
|
|
||||||
// Class hooks added to the player in different states
|
// Class hooks added to the player in different states
|
||||||
classNames: {
|
classNames: {
|
||||||
video: 'plyr__video-wrapper',
|
|
||||||
embed: 'plyr__video-embed',
|
|
||||||
ads: 'plyr__ads',
|
|
||||||
control: 'plyr__control',
|
|
||||||
type: 'plyr--{0}',
|
type: 'plyr--{0}',
|
||||||
provider: 'plyr--{0}',
|
provider: 'plyr--{0}',
|
||||||
stopped: 'plyr--stopped',
|
video: 'plyr__video-wrapper',
|
||||||
|
embed: 'plyr__video-embed',
|
||||||
|
embedContainer: 'plyr__video-embed__container',
|
||||||
|
poster: 'plyr__poster',
|
||||||
|
posterEnabled: 'plyr__poster-enabled',
|
||||||
|
ads: 'plyr__ads',
|
||||||
|
control: 'plyr__control',
|
||||||
playing: 'plyr--playing',
|
playing: 'plyr--playing',
|
||||||
|
paused: 'plyr--paused',
|
||||||
|
stopped: 'plyr--stopped',
|
||||||
loading: 'plyr--loading',
|
loading: 'plyr--loading',
|
||||||
error: 'plyr--has-error',
|
|
||||||
hover: 'plyr--hover',
|
hover: 'plyr--hover',
|
||||||
tooltip: 'plyr__tooltip',
|
tooltip: 'plyr__tooltip',
|
||||||
cues: 'plyr__cues',
|
cues: 'plyr__cues',
|
||||||
@ -373,9 +385,10 @@ const defaults = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Advertisements plugin
|
// Advertisements plugin
|
||||||
// Tag is not required as publisher is determined by vi.ai using the domain
|
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
|
||||||
ads: {
|
ads: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
publisherId: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Fullscreen wrapper
|
// Fullscreen wrapper
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
@ -18,7 +19,7 @@ function onChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trigger an event
|
// Trigger an event
|
||||||
utils.dispatchEvent(this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
utils.dispatchEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
||||||
|
|
||||||
// Trap focus in container
|
// Trap focus in container
|
||||||
if (!browser.isIos) {
|
if (!browser.isIos) {
|
||||||
@ -54,6 +55,7 @@ class Fullscreen {
|
|||||||
|
|
||||||
// Get prefix
|
// Get prefix
|
||||||
this.prefix = Fullscreen.prefix;
|
this.prefix = Fullscreen.prefix;
|
||||||
|
this.property = Fullscreen.property;
|
||||||
|
|
||||||
// Scroll position
|
// Scroll position
|
||||||
this.scrollPosition = { x: 0, y: 0 };
|
this.scrollPosition = { x: 0, y: 0 };
|
||||||
@ -66,13 +68,15 @@ class Fullscreen {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fullscreen toggle on double click
|
// Fullscreen toggle on double click
|
||||||
utils.on(this.player.elements.container, 'dblclick', () => {
|
utils.on(this.player.elements.container, 'dblclick', event => {
|
||||||
|
// Ignore double click in controls
|
||||||
|
if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.toggle();
|
this.toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prevent double click on controls bubbling up
|
|
||||||
utils.on(this.player.elements.controls, 'dblclick', event => event.stopPropagation());
|
|
||||||
|
|
||||||
// Update the UI
|
// Update the UI
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
@ -85,8 +89,8 @@ class Fullscreen {
|
|||||||
// Get the prefix for handlers
|
// Get the prefix for handlers
|
||||||
static get prefix() {
|
static get prefix() {
|
||||||
// No prefix
|
// No prefix
|
||||||
if (utils.is.function(document.cancelFullScreen)) {
|
if (utils.is.function(document.exitFullscreen)) {
|
||||||
return false;
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for fullscreen support by vendor prefix
|
// Check for fullscreen support by vendor prefix
|
||||||
@ -98,12 +102,9 @@ class Fullscreen {
|
|||||||
];
|
];
|
||||||
|
|
||||||
prefixes.some(pre => {
|
prefixes.some(pre => {
|
||||||
if (utils.is.function(document[`${pre}CancelFullScreen`])) {
|
if (utils.is.function(document[`${pre}ExitFullscreen`]) || utils.is.function(document[`${pre}CancelFullScreen`])) {
|
||||||
value = pre;
|
value = pre;
|
||||||
return true;
|
return true;
|
||||||
} else if (utils.is.function(document.msExitFullscreen)) {
|
|
||||||
value = 'ms';
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -112,11 +113,18 @@ class Fullscreen {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get property() {
|
||||||
|
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
|
||||||
|
}
|
||||||
|
|
||||||
// Determine if fullscreen is enabled
|
// Determine if fullscreen is enabled
|
||||||
get enabled() {
|
get enabled() {
|
||||||
const fallback = this.player.config.fullscreen.fallback && !utils.inFrame();
|
return (
|
||||||
|
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
|
||||||
return (Fullscreen.native || fallback) && this.player.config.fullscreen.enabled && this.player.supported.ui && this.player.isVideo;
|
this.player.config.fullscreen.enabled &&
|
||||||
|
this.player.supported.ui &&
|
||||||
|
this.player.isVideo
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get active state
|
// Get active state
|
||||||
@ -130,7 +138,7 @@ class Fullscreen {
|
|||||||
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}FullscreenElement`];
|
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
|
||||||
|
|
||||||
return element === this.target;
|
return element === this.target;
|
||||||
}
|
}
|
||||||
@ -166,9 +174,9 @@ class Fullscreen {
|
|||||||
} else if (!Fullscreen.native) {
|
} else if (!Fullscreen.native) {
|
||||||
toggleFallback.call(this, true);
|
toggleFallback.call(this, true);
|
||||||
} else if (!this.prefix) {
|
} else if (!this.prefix) {
|
||||||
this.target.requestFullScreen();
|
this.target.requestFullscreen();
|
||||||
} else if (!utils.is.empty(this.prefix)) {
|
} else if (!utils.is.empty(this.prefix)) {
|
||||||
this.target[`${this.prefix}${this.prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen'}`]();
|
this.target[`${this.prefix}Request${this.property}`]();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,9 +193,10 @@ class Fullscreen {
|
|||||||
} else if (!Fullscreen.native) {
|
} else if (!Fullscreen.native) {
|
||||||
toggleFallback.call(this, false);
|
toggleFallback.call(this, false);
|
||||||
} else if (!this.prefix) {
|
} else if (!this.prefix) {
|
||||||
document.cancelFullScreen();
|
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
||||||
} else if (!utils.is.empty(this.prefix)) {
|
} else if (!utils.is.empty(this.prefix)) {
|
||||||
document[`${this.prefix}${this.prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen'}`]();
|
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
|
||||||
|
document[`${this.prefix}${action}${this.property}`]();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
146
src/js/html5.js
Normal file
146
src/js/html5.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Plyr HTML5 helpers
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import support from './support';
|
||||||
|
import utils from './utils';
|
||||||
|
|
||||||
|
const html5 = {
|
||||||
|
getSources() {
|
||||||
|
if (!this.isHTML5) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.media.querySelectorAll('source');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get quality levels
|
||||||
|
getQualityOptions() {
|
||||||
|
if (!this.isHTML5) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get sources
|
||||||
|
const sources = html5.getSources.call(this);
|
||||||
|
|
||||||
|
if (utils.is.empty(sources)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get <source> with size attribute
|
||||||
|
const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size')));
|
||||||
|
|
||||||
|
// If none, bail
|
||||||
|
if (utils.is.empty(sizes)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce to unique list
|
||||||
|
return utils.dedupe(sizes.map(source => Number(source.getAttribute('size'))));
|
||||||
|
},
|
||||||
|
|
||||||
|
extend() {
|
||||||
|
if (!this.isHTML5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = this;
|
||||||
|
|
||||||
|
// Quality
|
||||||
|
Object.defineProperty(player.media, 'quality', {
|
||||||
|
get() {
|
||||||
|
// Get sources
|
||||||
|
const sources = html5.getSources.call(player);
|
||||||
|
|
||||||
|
if (utils.is.empty(sources)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
|
||||||
|
|
||||||
|
if (utils.is.empty(matches)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(matches[0].getAttribute('size'));
|
||||||
|
},
|
||||||
|
set(input) {
|
||||||
|
// Get sources
|
||||||
|
const sources = html5.getSources.call(player);
|
||||||
|
|
||||||
|
if (utils.is.empty(sources)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get matches for requested size
|
||||||
|
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
|
||||||
|
|
||||||
|
// No matches for requested size
|
||||||
|
if (utils.is.empty(matches)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get supported sources
|
||||||
|
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
|
||||||
|
|
||||||
|
// No supported sources
|
||||||
|
if (utils.is.empty(supported)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger change event
|
||||||
|
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
|
||||||
|
quality: input,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get current state
|
||||||
|
const { currentTime, playing } = player;
|
||||||
|
|
||||||
|
// Set new source
|
||||||
|
player.media.src = supported[0].getAttribute('src');
|
||||||
|
|
||||||
|
// Load new source
|
||||||
|
player.media.load();
|
||||||
|
|
||||||
|
// Resume playing
|
||||||
|
if (playing) {
|
||||||
|
player.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore time
|
||||||
|
player.currentTime = currentTime;
|
||||||
|
|
||||||
|
// Trigger change event
|
||||||
|
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
|
||||||
|
quality: input,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cancel current network requests
|
||||||
|
// See https://github.com/sampotts/plyr/issues/174
|
||||||
|
cancelRequests() {
|
||||||
|
if (!this.isHTML5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove child sources
|
||||||
|
utils.removeElement(html5.getSources());
|
||||||
|
|
||||||
|
// Set blank video src attribute
|
||||||
|
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
|
||||||
|
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
|
||||||
|
this.media.setAttribute('src', this.config.blankVideo);
|
||||||
|
|
||||||
|
// Load the new empty source
|
||||||
|
// This will cancel existing requests
|
||||||
|
// See https://github.com/sampotts/plyr/issues/174
|
||||||
|
this.media.load();
|
||||||
|
|
||||||
|
// Debugging
|
||||||
|
this.debug.log('Cancelled network requests');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default html5;
|
31
src/js/i18n.js
Normal file
31
src/js/i18n.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Plyr internationalization
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import utils from './utils';
|
||||||
|
|
||||||
|
const i18n = {
|
||||||
|
get(key = '', config = {}) {
|
||||||
|
if (utils.is.empty(key) || utils.is.empty(config) || !Object.keys(config.i18n).includes(key)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let string = config.i18n[key];
|
||||||
|
|
||||||
|
const replace = {
|
||||||
|
'{seektime}': config.seekTime,
|
||||||
|
'{title}': config.title,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(replace).forEach(([
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
]) => {
|
||||||
|
string = utils.replaceAll(string, key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return string;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default i18n;
|
@ -2,27 +2,28 @@
|
|||||||
// Plyr Event Listeners
|
// Plyr Event Listeners
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import support from './support';
|
|
||||||
import utils from './utils';
|
|
||||||
import controls from './controls';
|
import controls from './controls';
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
|
import utils from './utils';
|
||||||
|
|
||||||
// Sniff out the browser
|
// Sniff out the browser
|
||||||
const browser = utils.getBrowser();
|
const browser = utils.getBrowser();
|
||||||
|
|
||||||
const listeners = {
|
class Listeners {
|
||||||
// Global listeners
|
constructor(player) {
|
||||||
global() {
|
this.player = player;
|
||||||
let last = null;
|
this.lastKey = null;
|
||||||
|
|
||||||
// Get the key code for an event
|
this.handleKey = this.handleKey.bind(this);
|
||||||
const getKeyCode = event => (event.keyCode ? event.keyCode : event.which);
|
this.toggleMenu = this.toggleMenu.bind(this);
|
||||||
|
this.firstTouch = this.firstTouch.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle key press
|
// Handle key presses
|
||||||
const handleKey = event => {
|
handleKey(event) {
|
||||||
const code = getKeyCode(event);
|
const code = event.keyCode ? event.keyCode : event.which;
|
||||||
const pressed = event.type === 'keydown';
|
const pressed = event.type === 'keydown';
|
||||||
const repeat = pressed && code === last;
|
const repeat = pressed && code === this.lastKey;
|
||||||
|
|
||||||
// Bail if a modifier key is set
|
// Bail if a modifier key is set
|
||||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||||
@ -38,7 +39,7 @@ const listeners = {
|
|||||||
// Seek by the number keys
|
// Seek by the number keys
|
||||||
const seekByKey = () => {
|
const seekByKey = () => {
|
||||||
// Divide the max duration into 10th's and times by the number value
|
// Divide the max duration into 10th's and times by the number value
|
||||||
this.currentTime = this.duration / 10 * (code - 48);
|
this.player.currentTime = this.player.duration / 10 * (code - 48);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle the key on keydown
|
// Handle the key on keydown
|
||||||
@ -73,7 +74,7 @@ const listeners = {
|
|||||||
// and if the focused element is not editable (e.g. text input)
|
// and if the focused element is not editable (e.g. text input)
|
||||||
// and any that accept key input http://webaim.org/techniques/keyboard/
|
// and any that accept key input http://webaim.org/techniques/keyboard/
|
||||||
const focused = utils.getFocusElement();
|
const focused = utils.getFocusElement();
|
||||||
if (utils.is.element(focused) && utils.matches(focused, this.config.selectors.editable)) {
|
if (utils.is.element(focused) && utils.matches(focused, this.player.config.selectors.editable)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,52 +105,52 @@ const listeners = {
|
|||||||
case 75:
|
case 75:
|
||||||
// Space and K key
|
// Space and K key
|
||||||
if (!repeat) {
|
if (!repeat) {
|
||||||
this.togglePlay();
|
this.player.togglePlay();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 38:
|
case 38:
|
||||||
// Arrow up
|
// Arrow up
|
||||||
this.increaseVolume(0.1);
|
this.player.increaseVolume(0.1);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 40:
|
case 40:
|
||||||
// Arrow down
|
// Arrow down
|
||||||
this.decreaseVolume(0.1);
|
this.player.decreaseVolume(0.1);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 77:
|
case 77:
|
||||||
// M key
|
// M key
|
||||||
if (!repeat) {
|
if (!repeat) {
|
||||||
this.muted = !this.muted;
|
this.player.muted = !this.player.muted;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 39:
|
case 39:
|
||||||
// Arrow forward
|
// Arrow forward
|
||||||
this.forward();
|
this.player.forward();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 37:
|
case 37:
|
||||||
// Arrow back
|
// Arrow back
|
||||||
this.rewind();
|
this.player.rewind();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 70:
|
case 70:
|
||||||
// F key
|
// F key
|
||||||
this.fullscreen.toggle();
|
this.player.fullscreen.toggle();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 67:
|
case 67:
|
||||||
// C key
|
// C key
|
||||||
if (!repeat) {
|
if (!repeat) {
|
||||||
this.toggleCaptions();
|
this.player.toggleCaptions();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 76:
|
case 76:
|
||||||
// L key
|
// L key
|
||||||
this.loop = !this.loop;
|
this.player.loop = !this.player.loop;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
/* case 73:
|
/* case 73:
|
||||||
@ -170,98 +171,159 @@ const listeners = {
|
|||||||
|
|
||||||
// Escape is handle natively when in full screen
|
// Escape is handle natively when in full screen
|
||||||
// So we only need to worry about non native
|
// So we only need to worry about non native
|
||||||
if (!this.fullscreen.enabled && this.fullscreen.active && code === 27) {
|
if (!this.player.fullscreen.enabled && this.player.fullscreen.active && code === 27) {
|
||||||
this.fullscreen.toggle();
|
this.player.fullscreen.toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store last code for next cycle
|
// Store last code for next cycle
|
||||||
last = code;
|
this.lastKey = code;
|
||||||
} else {
|
} else {
|
||||||
last = null;
|
this.lastKey = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
// Toggle menu
|
||||||
|
toggleMenu(event) {
|
||||||
|
controls.toggleMenu.call(this.player, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device is touch enabled
|
||||||
|
firstTouch() {
|
||||||
|
this.player.touch = true;
|
||||||
|
|
||||||
|
// Add touch class
|
||||||
|
utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
utils.off(document.body, 'touchstart', this.firstTouch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global window & document listeners
|
||||||
|
global(toggle = true) {
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
if (this.config.keyboard.global) {
|
if (this.player.config.keyboard.global) {
|
||||||
utils.on(window, 'keydown keyup', handleKey, false);
|
utils.toggleListener(window, 'keydown keyup', this.handleKey, toggle, false);
|
||||||
} else if (this.config.keyboard.focused) {
|
}
|
||||||
utils.on(this.elements.container, 'keydown keyup', handleKey, false);
|
|
||||||
|
// Click anywhere closes menu
|
||||||
|
utils.toggleListener(document.body, 'click', this.toggleMenu, toggle);
|
||||||
|
|
||||||
|
// Detect touch by events
|
||||||
|
utils.on(document.body, 'touchstart', this.firstTouch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container listeners
|
||||||
|
container() {
|
||||||
|
// Keyboard shortcuts
|
||||||
|
if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) {
|
||||||
|
utils.on(this.player.elements.container, 'keydown keyup', this.handleKey, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect tab focus
|
// Detect tab focus
|
||||||
// Remove class on blur/focusout
|
// Remove class on blur/focusout
|
||||||
utils.on(this.elements.container, 'focusout', event => {
|
utils.on(this.player.elements.container, 'focusout', event => {
|
||||||
utils.toggleClass(event.target, this.config.classNames.tabFocus, false);
|
utils.toggleClass(event.target, this.player.config.classNames.tabFocus, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add classname to tabbed elements
|
// Add classname to tabbed elements
|
||||||
utils.on(this.elements.container, 'keydown', event => {
|
utils.on(this.player.elements.container, 'keydown', event => {
|
||||||
if (event.keyCode !== 9) {
|
if (event.keyCode !== 9) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delay the adding of classname until the focus has changed
|
// Delay the adding of classname until the focus has changed
|
||||||
// This event fires before the focusin event
|
// This event fires before the focusin event
|
||||||
window.setTimeout(() => {
|
setTimeout(() => {
|
||||||
utils.toggleClass(utils.getFocusElement(), this.config.classNames.tabFocus, true);
|
utils.toggleClass(utils.getFocusElement(), this.player.config.classNames.tabFocus, true);
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle controls visibility based on mouse movement
|
|
||||||
if (this.config.hideControls) {
|
|
||||||
// Toggle controls on mouse events and entering fullscreen
|
// Toggle controls on mouse events and entering fullscreen
|
||||||
utils.on(this.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => {
|
utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {
|
||||||
this.toggleControls(event);
|
const { controls } = this.player.elements;
|
||||||
|
|
||||||
|
// Remove button states for fullscreen
|
||||||
|
if (event.type === 'enterfullscreen') {
|
||||||
|
controls.pressed = false;
|
||||||
|
controls.hover = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show, then hide after a timeout unless another control event occurs
|
||||||
|
const show = [
|
||||||
|
'touchstart',
|
||||||
|
'touchmove',
|
||||||
|
'mousemove',
|
||||||
|
].includes(event.type);
|
||||||
|
|
||||||
|
let delay = 0;
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
ui.toggleControls.call(this.player, true);
|
||||||
|
// Use longer timeout for touch devices
|
||||||
|
delay = this.player.touch ? 3000 : 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear timer
|
||||||
|
clearTimeout(this.player.timers.controls);
|
||||||
|
// Timer to prevent flicker when seeking
|
||||||
|
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
// Listen for media events
|
// Listen for media events
|
||||||
media() {
|
media() {
|
||||||
// Time change on media
|
// Time change on media
|
||||||
utils.on(this.media, 'timeupdate seeking', event => ui.timeUpdate.call(this, event));
|
utils.on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event));
|
||||||
|
|
||||||
// Display duration
|
// Display duration
|
||||||
utils.on(this.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this, event));
|
utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event));
|
||||||
|
|
||||||
// Check for audio tracks on load
|
// Check for audio tracks on load
|
||||||
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
|
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
|
||||||
utils.on(this.media, 'loadeddata', () => {
|
utils.on(this.player.media, 'loadeddata', () => {
|
||||||
utils.toggleHidden(this.elements.volume, !this.hasAudio);
|
utils.toggleHidden(this.player.elements.volume, !this.player.hasAudio);
|
||||||
utils.toggleHidden(this.elements.buttons.mute, !this.hasAudio);
|
utils.toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle the media finishing
|
// Handle the media finishing
|
||||||
utils.on(this.media, 'ended', () => {
|
utils.on(this.player.media, 'ended', () => {
|
||||||
// Show poster on end
|
// Show poster on end
|
||||||
if (this.isHTML5 && this.isVideo && this.config.showPosterOnEnd) {
|
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
|
||||||
// Restart
|
// Restart
|
||||||
this.restart();
|
this.player.restart();
|
||||||
|
|
||||||
// Re-load media
|
|
||||||
this.media.load();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check for buffer progress
|
// Check for buffer progress
|
||||||
utils.on(this.media, 'progress playing', event => ui.updateProgress.call(this, event));
|
utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event));
|
||||||
|
|
||||||
// Handle native mute
|
// Handle volume changes
|
||||||
utils.on(this.media, 'volumechange', event => ui.updateVolume.call(this, event));
|
utils.on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event));
|
||||||
|
|
||||||
// Handle native play/pause
|
// Handle play/pause
|
||||||
utils.on(this.media, 'playing play pause ended', event => ui.checkPlaying.call(this, event));
|
utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event));
|
||||||
|
|
||||||
// Loading
|
// Loading state
|
||||||
utils.on(this.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this, event));
|
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
|
||||||
|
|
||||||
// Check if media failed to load
|
// If autoplay, then load advertisement if required
|
||||||
// utils.on(this.media, 'play', event => ui.checkFailed.call(this, event));
|
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
|
||||||
|
utils.on(this.player.media, 'playing', () => {
|
||||||
|
if (!this.player.ads) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If ads are enabled, wait for them first
|
||||||
|
if (this.player.ads.enabled && !this.player.ads.initialized) {
|
||||||
|
// Wait for manager response
|
||||||
|
this.player.ads.managerPromise.then(() => this.player.ads.play()).catch(() => this.player.play());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Click video
|
// Click video
|
||||||
if (this.supported.ui && this.config.clickToPlay && !this.isAudio) {
|
if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) {
|
||||||
// Re-fetch the wrapper
|
// Re-fetch the wrapper
|
||||||
const wrapper = utils.getElement.call(this, `.${this.config.classNames.video}`);
|
const wrapper = utils.getElement.call(this.player, `.${this.player.config.classNames.video}`);
|
||||||
|
|
||||||
// Bail if there's no wrapper (this should never happen)
|
// Bail if there's no wrapper (this should never happen)
|
||||||
if (!utils.is.element(wrapper)) {
|
if (!utils.is.element(wrapper)) {
|
||||||
@ -271,25 +333,25 @@ const listeners = {
|
|||||||
// On click play, pause ore restart
|
// On click play, pause ore restart
|
||||||
utils.on(wrapper, 'click', () => {
|
utils.on(wrapper, 'click', () => {
|
||||||
// Touch devices will just show controls (if we're hiding controls)
|
// Touch devices will just show controls (if we're hiding controls)
|
||||||
if (this.config.hideControls && support.touch && !this.paused) {
|
if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.paused) {
|
if (this.player.paused) {
|
||||||
this.play();
|
this.player.play();
|
||||||
} else if (this.ended) {
|
} else if (this.player.ended) {
|
||||||
this.restart();
|
this.player.restart();
|
||||||
this.play();
|
this.player.play();
|
||||||
} else {
|
} else {
|
||||||
this.pause();
|
this.player.pause();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable right click
|
// Disable right click
|
||||||
if (this.supported.ui && this.config.disableContextMenu) {
|
if (this.player.supported.ui && this.player.config.disableContextMenu) {
|
||||||
utils.on(
|
utils.on(
|
||||||
this.media,
|
this.player.elements.wrapper,
|
||||||
'contextmenu',
|
'contextmenu',
|
||||||
event => {
|
event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -299,50 +361,53 @@ const listeners = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Volume change
|
// Volume change
|
||||||
utils.on(this.media, 'volumechange', () => {
|
utils.on(this.player.media, 'volumechange', () => {
|
||||||
// Save to storage
|
// Save to storage
|
||||||
this.storage.set({ volume: this.volume, muted: this.muted });
|
this.player.storage.set({ volume: this.player.volume, muted: this.player.muted });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Speed change
|
// Speed change
|
||||||
utils.on(this.media, 'ratechange', () => {
|
utils.on(this.player.media, 'ratechange', () => {
|
||||||
// Update UI
|
// Update UI
|
||||||
controls.updateSetting.call(this, 'speed');
|
controls.updateSetting.call(this.player, 'speed');
|
||||||
|
|
||||||
// Save to storage
|
// Save to storage
|
||||||
this.storage.set({ speed: this.speed });
|
this.player.storage.set({ speed: this.player.speed });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quality request
|
||||||
|
utils.on(this.player.media, 'qualityrequested', event => {
|
||||||
|
// Save to storage
|
||||||
|
this.player.storage.set({ quality: event.detail.quality });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quality change
|
// Quality change
|
||||||
utils.on(this.media, 'qualitychange', () => {
|
utils.on(this.player.media, 'qualitychange', event => {
|
||||||
// Update UI
|
// Update UI
|
||||||
controls.updateSetting.call(this, 'quality');
|
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
|
||||||
|
|
||||||
// Save to storage
|
|
||||||
this.storage.set({ quality: this.quality });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Caption language change
|
// Caption language change
|
||||||
utils.on(this.media, 'languagechange', () => {
|
utils.on(this.player.media, 'languagechange', () => {
|
||||||
// Update UI
|
// Update UI
|
||||||
controls.updateSetting.call(this, 'captions');
|
controls.updateSetting.call(this.player, 'captions');
|
||||||
|
|
||||||
// Save to storage
|
// Save to storage
|
||||||
this.storage.set({ language: this.language });
|
this.player.storage.set({ language: this.player.language });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Captions toggle
|
// Captions toggle
|
||||||
utils.on(this.media, 'captionsenabled captionsdisabled', () => {
|
utils.on(this.player.media, 'captionsenabled captionsdisabled', () => {
|
||||||
// Update UI
|
// Update UI
|
||||||
controls.updateSetting.call(this, 'captions');
|
controls.updateSetting.call(this.player, 'captions');
|
||||||
|
|
||||||
// Save to storage
|
// Save to storage
|
||||||
this.storage.set({ captions: this.captions.active });
|
this.player.storage.set({ captions: this.player.captions.active });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Proxy events to container
|
// Proxy events to container
|
||||||
// Bubble up key events for Edge
|
// Bubble up key events for Edge
|
||||||
utils.on(this.media, this.config.events.concat([
|
utils.on(this.player.media, this.player.config.events.concat([
|
||||||
'keyup',
|
'keyup',
|
||||||
'keydown',
|
'keydown',
|
||||||
]).join(' '), event => {
|
]).join(' '), event => {
|
||||||
@ -350,193 +415,272 @@ const listeners = {
|
|||||||
|
|
||||||
// Get error details from media
|
// Get error details from media
|
||||||
if (event.type === 'error') {
|
if (event.type === 'error') {
|
||||||
detail = this.media.error;
|
detail = this.player.media.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.dispatchEvent.call(this, this.elements.container, event.type, true, detail);
|
utils.dispatchEvent.call(this.player, this.player.elements.container, event.type, true, detail);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
// Listen for control events
|
// Listen for control events
|
||||||
controls() {
|
controls() {
|
||||||
// IE doesn't support input event, so we fallback to change
|
// IE doesn't support input event, so we fallback to change
|
||||||
const inputEvent = browser.isIE ? 'change' : 'input';
|
const inputEvent = browser.isIE ? 'change' : 'input';
|
||||||
|
|
||||||
// Trigger custom and default handlers
|
// Run default and custom handlers
|
||||||
const proxy = (event, handlerKey, defaultHandler) => {
|
const proxy = (event, defaultHandler, customHandlerKey) => {
|
||||||
const customHandler = this.config.listeners[handlerKey];
|
const customHandler = this.player.config.listeners[customHandlerKey];
|
||||||
|
const hasCustomHandler = utils.is.function(customHandler);
|
||||||
|
let returned = true;
|
||||||
|
|
||||||
// Execute custom handler
|
// Execute custom handler
|
||||||
if (utils.is.function(customHandler)) {
|
if (hasCustomHandler) {
|
||||||
customHandler.call(this, event);
|
returned = customHandler.call(this.player, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only call default handler if not prevented in custom handler
|
// Only call default handler if not prevented in custom handler
|
||||||
if (!event.defaultPrevented && utils.is.function(defaultHandler)) {
|
if (returned && utils.is.function(defaultHandler)) {
|
||||||
defaultHandler.call(this, event);
|
defaultHandler.call(this.player, event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Trigger custom and default handlers
|
||||||
|
const on = (element, type, defaultHandler, customHandlerKey, passive = true) => {
|
||||||
|
const customHandler = this.player.config.listeners[customHandlerKey];
|
||||||
|
const hasCustomHandler = utils.is.function(customHandler);
|
||||||
|
|
||||||
|
utils.on(element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);
|
||||||
|
};
|
||||||
|
|
||||||
// Play/pause toggle
|
// Play/pause toggle
|
||||||
utils.on(this.elements.buttons.play, 'click', event =>
|
on(this.player.elements.buttons.play, 'click', this.player.togglePlay, 'play');
|
||||||
proxy(event, 'play', () => {
|
|
||||||
this.togglePlay();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pause
|
// Pause
|
||||||
utils.on(this.elements.buttons.restart, 'click', event =>
|
on(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
|
||||||
proxy(event, 'restart', () => {
|
|
||||||
this.restart();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Rewind
|
// Rewind
|
||||||
utils.on(this.elements.buttons.rewind, 'click', event =>
|
on(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
|
||||||
proxy(event, 'rewind', () => {
|
|
||||||
this.rewind();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Rewind
|
// Rewind
|
||||||
utils.on(this.elements.buttons.forward, 'click', event =>
|
on(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward');
|
||||||
proxy(event, 'forward', () => {
|
|
||||||
this.forward();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mute toggle
|
// Mute toggle
|
||||||
utils.on(this.elements.buttons.mute, 'click', event =>
|
on(
|
||||||
proxy(event, 'mute', () => {
|
this.player.elements.buttons.mute,
|
||||||
this.muted = !this.muted;
|
'click',
|
||||||
}),
|
() => {
|
||||||
|
this.player.muted = !this.player.muted;
|
||||||
|
},
|
||||||
|
'mute',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Captions toggle
|
// Captions toggle
|
||||||
utils.on(this.elements.buttons.captions, 'click', event =>
|
on(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions);
|
||||||
proxy(event, 'captions', () => {
|
|
||||||
this.toggleCaptions();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fullscreen toggle
|
// Fullscreen toggle
|
||||||
utils.on(this.elements.buttons.fullscreen, 'click', event =>
|
on(
|
||||||
proxy(event, 'fullscreen', () => {
|
this.player.elements.buttons.fullscreen,
|
||||||
this.fullscreen.toggle();
|
'click',
|
||||||
}),
|
() => {
|
||||||
|
this.player.fullscreen.toggle();
|
||||||
|
},
|
||||||
|
'fullscreen',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Picture-in-Picture
|
// Picture-in-Picture
|
||||||
utils.on(this.elements.buttons.pip, 'click', event =>
|
on(
|
||||||
proxy(event, 'pip', () => {
|
this.player.elements.buttons.pip,
|
||||||
this.pip = 'toggle';
|
'click',
|
||||||
}),
|
() => {
|
||||||
|
this.player.pip = 'toggle';
|
||||||
|
},
|
||||||
|
'pip',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Airplay
|
// Airplay
|
||||||
utils.on(this.elements.buttons.airplay, 'click', event =>
|
on(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay');
|
||||||
proxy(event, 'airplay', () => {
|
|
||||||
this.airplay();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Settings menu
|
// Settings menu
|
||||||
utils.on(this.elements.buttons.settings, 'click', event => {
|
on(this.player.elements.buttons.settings, 'click', event => {
|
||||||
controls.toggleMenu.call(this, event);
|
controls.toggleMenu.call(this.player, event);
|
||||||
});
|
|
||||||
|
|
||||||
// Click anywhere closes menu
|
|
||||||
utils.on(document.documentElement, 'click', event => {
|
|
||||||
controls.toggleMenu.call(this, event);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Settings menu
|
// Settings menu
|
||||||
utils.on(this.elements.settings.form, 'click', event => {
|
on(this.player.elements.settings.form, 'click', event => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
// Go back to home tab on click
|
||||||
|
const showHomeTab = () => {
|
||||||
|
const id = `plyr-settings-${this.player.id}-home`;
|
||||||
|
controls.showTab.call(this.player, id);
|
||||||
|
};
|
||||||
|
|
||||||
// Settings menu items - use event delegation as items are added/removed
|
// Settings menu items - use event delegation as items are added/removed
|
||||||
if (utils.matches(event.target, this.config.selectors.inputs.language)) {
|
if (utils.matches(event.target, this.player.config.selectors.inputs.language)) {
|
||||||
proxy(event, 'language', () => {
|
proxy(
|
||||||
this.language = event.target.value;
|
event,
|
||||||
});
|
() => {
|
||||||
} else if (utils.matches(event.target, this.config.selectors.inputs.quality)) {
|
this.player.language = event.target.value;
|
||||||
proxy(event, 'quality', () => {
|
showHomeTab();
|
||||||
this.quality = event.target.value;
|
},
|
||||||
});
|
'language',
|
||||||
} else if (utils.matches(event.target, this.config.selectors.inputs.speed)) {
|
);
|
||||||
proxy(event, 'speed', () => {
|
} else if (utils.matches(event.target, this.player.config.selectors.inputs.quality)) {
|
||||||
this.speed = parseFloat(event.target.value);
|
proxy(
|
||||||
});
|
event,
|
||||||
|
() => {
|
||||||
|
this.player.quality = event.target.value;
|
||||||
|
showHomeTab();
|
||||||
|
},
|
||||||
|
'quality',
|
||||||
|
);
|
||||||
|
} else if (utils.matches(event.target, this.player.config.selectors.inputs.speed)) {
|
||||||
|
proxy(
|
||||||
|
event,
|
||||||
|
() => {
|
||||||
|
this.player.speed = parseFloat(event.target.value);
|
||||||
|
showHomeTab();
|
||||||
|
},
|
||||||
|
'speed',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
controls.showTab.call(this, event);
|
const tab = event.target;
|
||||||
|
controls.showTab.call(this.player, tab.getAttribute('aria-controls'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set range input alternative "value", which matches the tooltip time (#954)
|
||||||
|
on(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
|
||||||
|
const clientRect = this.player.elements.progress.getBoundingClientRect();
|
||||||
|
const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
|
||||||
|
event.currentTarget.setAttribute('seek-value', percent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pause while seeking
|
||||||
|
on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
|
||||||
|
const seek = event.currentTarget;
|
||||||
|
|
||||||
|
// Was playing before?
|
||||||
|
const play = seek.hasAttribute('play-on-seeked');
|
||||||
|
|
||||||
|
// Done seeking
|
||||||
|
const done = [
|
||||||
|
'mouseup',
|
||||||
|
'touchend',
|
||||||
|
'keyup',
|
||||||
|
].includes(event.type);
|
||||||
|
|
||||||
|
// If we're done seeking and it was playing, resume playback
|
||||||
|
if (play && done) {
|
||||||
|
seek.removeAttribute('play-on-seeked');
|
||||||
|
this.player.play();
|
||||||
|
} else if (!done && this.player.playing) {
|
||||||
|
seek.setAttribute('play-on-seeked', '');
|
||||||
|
this.player.pause();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seek
|
// Seek
|
||||||
utils.on(this.elements.inputs.seek, inputEvent, event =>
|
on(
|
||||||
proxy(event, 'seek', () => {
|
this.player.elements.inputs.seek,
|
||||||
this.currentTime = event.target.value / event.target.max * this.duration;
|
inputEvent,
|
||||||
}),
|
event => {
|
||||||
|
const seek = event.currentTarget;
|
||||||
|
|
||||||
|
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
|
||||||
|
let seekTo = seek.getAttribute('seek-value');
|
||||||
|
|
||||||
|
if (utils.is.empty(seekTo)) {
|
||||||
|
seekTo = seek.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
seek.removeAttribute('seek-value');
|
||||||
|
|
||||||
|
this.player.currentTime = seekTo / seek.max * this.player.duration;
|
||||||
|
},
|
||||||
|
'seek',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Current time invert
|
// Current time invert
|
||||||
// Only if one time element is used for both currentTime and duration
|
// Only if one time element is used for both currentTime and duration
|
||||||
if (this.config.toggleInvert && !utils.is.element(this.elements.display.duration)) {
|
if (this.player.config.toggleInvert && !utils.is.element(this.player.elements.display.duration)) {
|
||||||
utils.on(this.elements.display.currentTime, 'click', () => {
|
on(this.player.elements.display.currentTime, 'click', () => {
|
||||||
// Do nothing if we're at the start
|
// Do nothing if we're at the start
|
||||||
if (this.currentTime === 0) {
|
if (this.player.currentTime === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.config.invertTime = !this.config.invertTime;
|
this.player.config.invertTime = !this.player.config.invertTime;
|
||||||
ui.timeUpdate.call(this);
|
|
||||||
|
controls.timeUpdate.call(this.player);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Volume
|
// Volume
|
||||||
utils.on(this.elements.inputs.volume, inputEvent, event =>
|
on(
|
||||||
proxy(event, 'volume', () => {
|
this.player.elements.inputs.volume,
|
||||||
this.volume = event.target.value;
|
inputEvent,
|
||||||
}),
|
event => {
|
||||||
|
this.player.volume = event.target.value;
|
||||||
|
},
|
||||||
|
'volume',
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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) {
|
||||||
utils.on(utils.getElements.call(this, 'input[type="range"]'), 'input', event => {
|
on(utils.getElements.call(this.player, 'input[type="range"]'), 'input', event => {
|
||||||
controls.updateRangeFill.call(this, event.target);
|
controls.updateRangeFill.call(this.player, event.target);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seek tooltip
|
// Seek tooltip
|
||||||
utils.on(this.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this, event));
|
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
|
||||||
|
|
||||||
// Toggle controls visibility based on mouse movement
|
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
|
||||||
if (this.config.hideControls) {
|
on(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
||||||
// Watch for cursor over controls so they don't hide when trying to interact
|
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
|
||||||
utils.on(this.elements.controls, 'mouseenter mouseleave', event => {
|
|
||||||
this.elements.controls.hover = event.type === 'mouseenter';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch for cursor over controls so they don't hide when trying to interact
|
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
|
||||||
utils.on(this.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
||||||
this.elements.controls.pressed = [
|
this.player.elements.controls.pressed = [
|
||||||
'mousedown',
|
'mousedown',
|
||||||
'touchstart',
|
'touchstart',
|
||||||
].includes(event.type);
|
].includes(event.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Focus in/out on controls
|
// Focus in/out on controls
|
||||||
utils.on(this.elements.controls, 'focusin focusout', event => {
|
on(this.player.elements.controls, 'focusin focusout', event => {
|
||||||
this.toggleControls(event);
|
const { config, elements, timers } = this.player;
|
||||||
});
|
|
||||||
|
// Skip transition to prevent focus from scrolling the parent element
|
||||||
|
utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
ui.toggleControls.call(this.player, event.type === 'focusin');
|
||||||
|
|
||||||
|
// If focusin, hide again after delay
|
||||||
|
if (event.type === 'focusin') {
|
||||||
|
// Restore transition
|
||||||
|
setTimeout(() => {
|
||||||
|
utils.toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Delay a little more for keyboard users
|
||||||
|
const delay = this.touch ? 3000 : 4000;
|
||||||
|
|
||||||
|
// Clear timer
|
||||||
|
clearTimeout(timers.controls);
|
||||||
|
// Hide
|
||||||
|
timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Mouse wheel for volume
|
// Mouse wheel for volume
|
||||||
utils.on(
|
on(
|
||||||
this.elements.inputs.volume,
|
this.player.elements.inputs.volume,
|
||||||
'wheel',
|
'wheel',
|
||||||
event =>
|
event => {
|
||||||
proxy(event, 'volume', () => {
|
|
||||||
// Detect "natural" scroll - suppored 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
|
// Other browsers on OS X will be inverted until support improves
|
||||||
const inverted = event.webkitDirectionInvertedFromDevice;
|
const inverted = event.webkitDirectionInvertedFromDevice;
|
||||||
@ -546,10 +690,10 @@ const listeners = {
|
|||||||
// Scroll down (or up on natural) to decrease
|
// Scroll down (or up on natural) to decrease
|
||||||
if (event.deltaY < 0 || event.deltaX > 0) {
|
if (event.deltaY < 0 || event.deltaX > 0) {
|
||||||
if (inverted) {
|
if (inverted) {
|
||||||
this.decreaseVolume(step);
|
this.player.decreaseVolume(step);
|
||||||
direction = -1;
|
direction = -1;
|
||||||
} else {
|
} else {
|
||||||
this.increaseVolume(step);
|
this.player.increaseVolume(step);
|
||||||
direction = 1;
|
direction = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -557,22 +701,28 @@ const listeners = {
|
|||||||
// Scroll up (or down on natural) to increase
|
// Scroll up (or down on natural) to increase
|
||||||
if (event.deltaY > 0 || event.deltaX < 0) {
|
if (event.deltaY > 0 || event.deltaX < 0) {
|
||||||
if (inverted) {
|
if (inverted) {
|
||||||
this.increaseVolume(step);
|
this.player.increaseVolume(step);
|
||||||
direction = 1;
|
direction = 1;
|
||||||
} else {
|
} else {
|
||||||
this.decreaseVolume(step);
|
this.player.decreaseVolume(step);
|
||||||
direction = -1;
|
direction = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't break page scrolling at max and min
|
// Don't break page scrolling at max and min
|
||||||
if ((direction === 1 && this.media.volume < 1) || (direction === -1 && this.media.volume > 0)) {
|
if ((direction === 1 && this.player.media.volume < 1) || (direction === -1 && this.player.media.volume > 0)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
'volume',
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default listeners;
|
// Reset on destroy
|
||||||
|
clear() {
|
||||||
|
this.global(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Listeners;
|
||||||
|
@ -2,14 +2,10 @@
|
|||||||
// Plyr Media
|
// Plyr Media
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import support from './support';
|
import html5 from './html5';
|
||||||
import utils from './utils';
|
|
||||||
import youtube from './plugins/youtube';
|
|
||||||
import vimeo from './plugins/vimeo';
|
import vimeo from './plugins/vimeo';
|
||||||
import ui from './ui';
|
import youtube from './plugins/youtube';
|
||||||
|
import utils from './utils';
|
||||||
// Sniff out the browser
|
|
||||||
const browser = utils.getBrowser();
|
|
||||||
|
|
||||||
const media = {
|
const media = {
|
||||||
// Setup media
|
// Setup media
|
||||||
@ -32,23 +28,6 @@ const media = {
|
|||||||
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
|
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.supported.ui) {
|
|
||||||
// Check for picture-in-picture support
|
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
|
|
||||||
|
|
||||||
// Check for airplay support
|
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
|
||||||
|
|
||||||
// If there's no autoplay attribute, assume the video is stopped and add state class
|
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.config.autoplay);
|
|
||||||
|
|
||||||
// Add iOS class
|
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
|
||||||
|
|
||||||
// Add touch class
|
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, support.touch);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject the player wrapper
|
// Inject the player wrapper
|
||||||
if (this.isVideo) {
|
if (this.isVideo) {
|
||||||
// Create the wrapper div
|
// Create the wrapper div
|
||||||
@ -58,6 +37,13 @@ const media = {
|
|||||||
|
|
||||||
// Wrap the video in a container
|
// Wrap the video in a container
|
||||||
utils.wrap(this.media, this.elements.wrapper);
|
utils.wrap(this.media, this.elements.wrapper);
|
||||||
|
|
||||||
|
// Faux poster container
|
||||||
|
this.elements.poster = utils.createElement('div', {
|
||||||
|
class: this.config.classNames.poster,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.elements.wrapper.appendChild(this.elements.poster);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isEmbed) {
|
if (this.isEmbed) {
|
||||||
@ -74,33 +60,9 @@ const media = {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if (this.isHTML5) {
|
} else if (this.isHTML5) {
|
||||||
ui.setTitle.call(this);
|
html5.extend.call(this);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Cancel current network requests
|
|
||||||
// See https://github.com/sampotts/plyr/issues/174
|
|
||||||
cancelRequests() {
|
|
||||||
if (!this.isHTML5) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove child sources
|
|
||||||
utils.removeElement(this.media.querySelectorAll('source'));
|
|
||||||
|
|
||||||
// Set blank video src attribute
|
|
||||||
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
|
|
||||||
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
|
|
||||||
this.media.setAttribute('src', this.config.blankVideo);
|
|
||||||
|
|
||||||
// Load the new empty source
|
|
||||||
// This will cancel existing requests
|
|
||||||
// See https://github.com/sampotts/plyr/issues/174
|
|
||||||
this.media.load();
|
|
||||||
|
|
||||||
// Debugging
|
|
||||||
this.debug.log('Cancelled network requests');
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default media;
|
export default media;
|
||||||
|
@ -6,24 +6,9 @@
|
|||||||
|
|
||||||
/* global google */
|
/* global google */
|
||||||
|
|
||||||
|
import i18n from '../i18n';
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
|
||||||
// Build the default tag URL
|
|
||||||
const getTagUrl = () => {
|
|
||||||
const params = {
|
|
||||||
AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
|
|
||||||
AV_CHANNELID: '5a0458dc28a06145e4519d21',
|
|
||||||
AV_URL: '127.0.0.1:3000',
|
|
||||||
cb: 1,
|
|
||||||
AV_WIDTH: 640,
|
|
||||||
AV_HEIGHT: 480,
|
|
||||||
};
|
|
||||||
|
|
||||||
const base = 'https://go.aniview.com/api/adserver6/vast/';
|
|
||||||
|
|
||||||
return `${base}?${utils.buildUrlParams(params)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Ads {
|
class Ads {
|
||||||
/**
|
/**
|
||||||
* Ads constructor.
|
* Ads constructor.
|
||||||
@ -32,39 +17,9 @@ class Ads {
|
|||||||
*/
|
*/
|
||||||
constructor(player) {
|
constructor(player) {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.enabled = player.config.ads.enabled;
|
this.publisherId = player.config.ads.publisherId;
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
this.blocked = false;
|
|
||||||
this.enabled = utils.is.url(player.config.ads.tag);
|
|
||||||
|
|
||||||
// Check if a tag URL is provided.
|
|
||||||
if (!this.enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the Google IMA3 SDK is loaded or load it ourselves
|
|
||||||
if (!utils.is.object(window.google)) {
|
|
||||||
utils.loadScript(
|
|
||||||
player.config.urls.googleIMA.api,
|
|
||||||
() => {
|
|
||||||
this.ready();
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
// Script failed to load or is blocked
|
|
||||||
this.blocked = true;
|
|
||||||
this.player.debug.log('Ads error: Google IMA SDK failed to load');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.ready();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ads instance ready.
|
|
||||||
*/
|
|
||||||
ready() {
|
|
||||||
this.elements = {
|
this.elements = {
|
||||||
container: null,
|
container: null,
|
||||||
displayContainer: null,
|
displayContainer: null,
|
||||||
@ -76,32 +31,81 @@ class Ads {
|
|||||||
this.safetyTimer = null;
|
this.safetyTimer = null;
|
||||||
this.countdownTimer = null;
|
this.countdownTimer = null;
|
||||||
|
|
||||||
// Set listeners on the Plyr instance
|
// Setup a promise to resolve when the IMA manager is ready
|
||||||
this.listeners();
|
this.managerPromise = new Promise((resolve, reject) => {
|
||||||
|
// The ad is loaded and ready
|
||||||
|
this.on('loaded', resolve);
|
||||||
|
|
||||||
|
// Ads failed
|
||||||
|
this.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
get enabled() {
|
||||||
|
return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the IMA SDK
|
||||||
|
*/
|
||||||
|
load() {
|
||||||
|
if (this.enabled) {
|
||||||
|
// Check if the Google IMA3 SDK is loaded or load it ourselves
|
||||||
|
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
|
||||||
|
utils
|
||||||
|
.loadScript(this.player.config.urls.googleIMA.sdk)
|
||||||
|
.then(() => {
|
||||||
|
this.ready();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Script failed to load or is blocked
|
||||||
|
this.trigger('error', new Error('Google IMA SDK failed to load'));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.ready();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ads instance ready
|
||||||
|
*/
|
||||||
|
ready() {
|
||||||
// Start ticking our safety timer. If the whole advertisement
|
// Start ticking our safety timer. If the whole advertisement
|
||||||
// thing doesn't resolve within our set time; we bail
|
// thing doesn't resolve within our set time; we bail
|
||||||
this.startSafetyTimer(12000, 'ready()');
|
this.startSafetyTimer(12000, 'ready()');
|
||||||
|
|
||||||
// Setup a simple promise to resolve if the IMA loader is ready
|
|
||||||
this.loaderPromise = new Promise(resolve => {
|
|
||||||
this.on('ADS_LOADER_LOADED', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup a promise to resolve if the IMA manager is ready
|
|
||||||
this.managerPromise = new Promise(resolve => {
|
|
||||||
this.on('ADS_MANAGER_LOADED', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear the safety timer
|
// Clear the safety timer
|
||||||
this.managerPromise.then(() => {
|
this.managerPromise.then(() => {
|
||||||
this.clearSafetyTimer('onAdsManagerLoaded()');
|
this.clearSafetyTimer('onAdsManagerLoaded()');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set listeners on the Plyr instance
|
||||||
|
this.listeners();
|
||||||
|
|
||||||
// Setup the IMA SDK
|
// Setup the IMA SDK
|
||||||
this.setupIMA();
|
this.setupIMA();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the default tag URL
|
||||||
|
get tagUrl() {
|
||||||
|
const params = {
|
||||||
|
AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
|
||||||
|
AV_CHANNELID: '5a0458dc28a06145e4519d21',
|
||||||
|
AV_URL: location.hostname,
|
||||||
|
cb: Date.now(),
|
||||||
|
AV_WIDTH: 640,
|
||||||
|
AV_HEIGHT: 480,
|
||||||
|
AV_CDIM2: this.publisherId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const base = 'https://go.aniview.com/api/adserver6/vast/';
|
||||||
|
|
||||||
|
return `${base}?${utils.buildUrlParams(params)}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In order for the SDK to display ads for our video, we need to tell it where to put them,
|
* In order for the SDK to display ads for our video, we need to tell it where to put them,
|
||||||
* so here we define our ad container. This div is set up to render on top of the video player.
|
* so here we define our ad container. This div is set up to render on top of the video player.
|
||||||
@ -114,7 +118,6 @@ class Ads {
|
|||||||
// Create the container for our advertisements
|
// Create the container for our advertisements
|
||||||
this.elements.container = utils.createElement('div', {
|
this.elements.container = utils.createElement('div', {
|
||||||
class: this.player.config.classNames.ads,
|
class: this.player.config.classNames.ads,
|
||||||
hidden: '',
|
|
||||||
});
|
});
|
||||||
this.player.elements.container.appendChild(this.elements.container);
|
this.player.elements.container.appendChild(this.elements.container);
|
||||||
|
|
||||||
@ -148,7 +151,7 @@ class Ads {
|
|||||||
|
|
||||||
// Request video ads
|
// Request video ads
|
||||||
const request = new google.ima.AdsRequest();
|
const request = new google.ima.AdsRequest();
|
||||||
request.adTagUrl = getTagUrl();
|
request.adTagUrl = this.tagUrl;
|
||||||
|
|
||||||
// Specify the linear and nonlinear slot sizes. This helps the SDK
|
// Specify the linear and nonlinear slot sizes. This helps the SDK
|
||||||
// to select the correct creative if multiple are returned
|
// to select the correct creative if multiple are returned
|
||||||
@ -160,9 +163,10 @@ class Ads {
|
|||||||
// We only overlay ads as we only support video.
|
// We only overlay ads as we only support video.
|
||||||
request.forceNonLinearFullSlot = false;
|
request.forceNonLinearFullSlot = false;
|
||||||
|
|
||||||
this.loader.requestAds(request);
|
// Mute based on current state
|
||||||
|
request.setAdWillPlayMuted(!this.player.muted);
|
||||||
|
|
||||||
this.handleEventListeners('ADS_LOADER_LOADED');
|
this.loader.requestAds(request);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.onAdError(e);
|
this.onAdError(e);
|
||||||
}
|
}
|
||||||
@ -174,25 +178,25 @@ class Ads {
|
|||||||
*/
|
*/
|
||||||
pollCountdown(start = false) {
|
pollCountdown(start = false) {
|
||||||
if (!start) {
|
if (!start) {
|
||||||
window.clearInterval(this.countdownTimer);
|
clearInterval(this.countdownTimer);
|
||||||
this.elements.container.removeAttribute('data-badge-text');
|
this.elements.container.removeAttribute('data-badge-text');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
const time = utils.formatTime(this.manager.getRemainingTime());
|
const time = utils.formatTime(Math.max(this.manager.getRemainingTime(), 0));
|
||||||
const label = `${this.player.config.i18n.advertisment} - ${time}`;
|
const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;
|
||||||
this.elements.container.setAttribute('data-badge-text', label);
|
this.elements.container.setAttribute('data-badge-text', label);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.countdownTimer = window.setInterval(update, 100);
|
this.countdownTimer = setInterval(update, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called whenever the ads are ready inside the AdDisplayContainer
|
* This method is called whenever the ads are ready inside the AdDisplayContainer
|
||||||
* @param {Event} adsManagerLoadedEvent
|
* @param {Event} adsManagerLoadedEvent
|
||||||
*/
|
*/
|
||||||
onAdsManagerLoaded(adsManagerLoadedEvent) {
|
onAdsManagerLoaded(event) {
|
||||||
// Get the ads manager
|
// Get the ads manager
|
||||||
const settings = new google.ima.AdsRenderingSettings();
|
const settings = new google.ima.AdsRenderingSettings();
|
||||||
|
|
||||||
@ -202,17 +206,18 @@ class Ads {
|
|||||||
|
|
||||||
// The SDK is polling currentTime on the contentPlayback. And needs a duration
|
// The SDK is polling currentTime on the contentPlayback. And needs a duration
|
||||||
// so it can determine when to start the mid- and post-roll
|
// so it can determine when to start the mid- and post-roll
|
||||||
this.manager = adsManagerLoadedEvent.getAdsManager(this.player, settings);
|
this.manager = event.getAdsManager(this.player, settings);
|
||||||
|
|
||||||
// Get the cue points for any mid-rolls by filtering out the pre- and post-roll
|
// Get the cue points for any mid-rolls by filtering out the pre- and post-roll
|
||||||
this.cuePoints = this.manager.getCuePoints();
|
this.cuePoints = this.manager.getCuePoints();
|
||||||
|
|
||||||
// Add advertisement cue's within the time line if available
|
// Add advertisement cue's within the time line if available
|
||||||
|
if (!utils.is.empty(this.cuePoints)) {
|
||||||
this.cuePoints.forEach(cuePoint => {
|
this.cuePoints.forEach(cuePoint => {
|
||||||
if (cuePoint !== 0 && cuePoint !== -1) {
|
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
|
||||||
const seekElement = this.player.elements.progress;
|
const seekElement = this.player.elements.progress;
|
||||||
|
|
||||||
if (seekElement) {
|
if (utils.is.element(seekElement)) {
|
||||||
const cuePercentage = 100 / this.player.duration * cuePoint;
|
const cuePercentage = 100 / this.player.duration * cuePoint;
|
||||||
const cue = utils.createElement('span', {
|
const cue = utils.createElement('span', {
|
||||||
class: this.player.config.classNames.cues,
|
class: this.player.config.classNames.cues,
|
||||||
@ -223,10 +228,11 @@ class Ads {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Get skippable state
|
// Get skippable state
|
||||||
// TODO: Skip button
|
// TODO: Skip button
|
||||||
// this.manager.getAdSkippableState();
|
// this.player.debug.warn(this.manager.getAdSkippableState());
|
||||||
|
|
||||||
// Set volume to match player
|
// Set volume to match player
|
||||||
this.manager.setVolume(this.player.volume);
|
this.manager.setVolume(this.player.volume);
|
||||||
@ -241,7 +247,7 @@ class Ads {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Resolve our adsManager
|
// Resolve our adsManager
|
||||||
this.handleEventListeners('ADS_MANAGER_LOADED');
|
this.trigger('loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -259,17 +265,18 @@ class Ads {
|
|||||||
|
|
||||||
// Proxy event
|
// Proxy event
|
||||||
const dispatchEvent = type => {
|
const dispatchEvent = type => {
|
||||||
utils.dispatchEvent.call(this.player, this.player.media, `ads${type}`);
|
const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
|
||||||
|
utils.dispatchEvent.call(this.player, this.player.media, event);
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case google.ima.AdEvent.Type.LOADED:
|
case google.ima.AdEvent.Type.LOADED:
|
||||||
// This is the first event sent for an ad - it is possible to determine whether the
|
// This is the first event sent for an ad - it is possible to determine whether the
|
||||||
// ad is a video ad or an overlay
|
// ad is a video ad or an overlay
|
||||||
this.handleEventListeners('LOADED');
|
this.trigger('loaded');
|
||||||
|
|
||||||
// Bubble event
|
// Bubble event
|
||||||
dispatchEvent('loaded');
|
dispatchEvent(event.type);
|
||||||
|
|
||||||
// Start countdown
|
// Start countdown
|
||||||
this.pollCountdown(true);
|
this.pollCountdown(true);
|
||||||
@ -287,10 +294,9 @@ class Ads {
|
|||||||
case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
|
case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
|
||||||
// All ads for the current videos are done. We can now request new advertisements
|
// All ads for the current videos are done. We can now request new advertisements
|
||||||
// in case the video is re-played
|
// in case the video is re-played
|
||||||
this.handleEventListeners('ALL_ADS_COMPLETED');
|
|
||||||
|
|
||||||
// Fire event
|
// Fire event
|
||||||
dispatchEvent('allcomplete');
|
dispatchEvent(event.type);
|
||||||
|
|
||||||
// TODO: Example for what happens when a next video in a playlist would be loaded.
|
// TODO: Example for what happens when a next video in a playlist would be loaded.
|
||||||
// So here we load a new video when all ads are done.
|
// So here we load a new video when all ads are done.
|
||||||
@ -322,9 +328,8 @@ class Ads {
|
|||||||
// This event indicates the ad has started - the video player can adjust the UI,
|
// This event indicates the ad has started - the video player can adjust the UI,
|
||||||
// for example display a pause button and remaining time. Fired when content should
|
// for example display a pause button and remaining time. Fired when content should
|
||||||
// be paused. This usually happens right before an ad is about to cover the content
|
// be paused. This usually happens right before an ad is about to cover the content
|
||||||
this.handleEventListeners('CONTENT_PAUSE_REQUESTED');
|
|
||||||
|
|
||||||
dispatchEvent('contentpause');
|
dispatchEvent(event.type);
|
||||||
|
|
||||||
this.pauseContent();
|
this.pauseContent();
|
||||||
|
|
||||||
@ -335,9 +340,8 @@ class Ads {
|
|||||||
// appropriate UI actions, such as removing the timer for remaining time detection.
|
// appropriate UI actions, such as removing the timer for remaining time detection.
|
||||||
// Fired when content should be resumed. This usually happens when an ad finishes
|
// Fired when content should be resumed. This usually happens when an ad finishes
|
||||||
// or collapses
|
// or collapses
|
||||||
this.handleEventListeners('CONTENT_RESUME_REQUESTED');
|
|
||||||
|
|
||||||
dispatchEvent('contentresume');
|
dispatchEvent(event.type);
|
||||||
|
|
||||||
this.pollCountdown();
|
this.pollCountdown();
|
||||||
|
|
||||||
@ -346,23 +350,11 @@ class Ads {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case google.ima.AdEvent.Type.STARTED:
|
case google.ima.AdEvent.Type.STARTED:
|
||||||
dispatchEvent('started');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case google.ima.AdEvent.Type.MIDPOINT:
|
case google.ima.AdEvent.Type.MIDPOINT:
|
||||||
dispatchEvent('midpoint');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case google.ima.AdEvent.Type.COMPLETE:
|
case google.ima.AdEvent.Type.COMPLETE:
|
||||||
dispatchEvent('complete');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case google.ima.AdEvent.Type.IMPRESSION:
|
case google.ima.AdEvent.Type.IMPRESSION:
|
||||||
dispatchEvent('impression');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case google.ima.AdEvent.Type.CLICK:
|
case google.ima.AdEvent.Type.CLICK:
|
||||||
dispatchEvent('click');
|
dispatchEvent(event.type);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -376,7 +368,7 @@ class Ads {
|
|||||||
*/
|
*/
|
||||||
onAdError(event) {
|
onAdError(event) {
|
||||||
this.cancel();
|
this.cancel();
|
||||||
this.player.debug.log('Ads error', event);
|
this.player.debug.warn('Ads error', event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -401,6 +393,10 @@ class Ads {
|
|||||||
this.player.on('seeked', () => {
|
this.player.on('seeked', () => {
|
||||||
const seekedTime = this.player.currentTime;
|
const seekedTime = this.player.currentTime;
|
||||||
|
|
||||||
|
if (utils.is.empty(this.cuePoints)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.cuePoints.forEach((cuePoint, index) => {
|
this.cuePoints.forEach((cuePoint, index) => {
|
||||||
if (time < cuePoint && cuePoint < seekedTime) {
|
if (time < cuePoint && cuePoint < seekedTime) {
|
||||||
this.manager.discardAdBreak();
|
this.manager.discardAdBreak();
|
||||||
@ -412,7 +408,9 @@ class Ads {
|
|||||||
// Listen to the resizing of the window. And resize ad accordingly
|
// Listen to the resizing of the window. And resize ad accordingly
|
||||||
// TODO: eventually implement ResizeObserver
|
// TODO: eventually implement ResizeObserver
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
|
if (this.manager) {
|
||||||
this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
|
this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,11 +421,12 @@ class Ads {
|
|||||||
const { container } = this.player.elements;
|
const { container } = this.player.elements;
|
||||||
|
|
||||||
if (!this.managerPromise) {
|
if (!this.managerPromise) {
|
||||||
return;
|
this.resumeContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play the requested advertisement whenever the adsManager is ready
|
// Play the requested advertisement whenever the adsManager is ready
|
||||||
this.managerPromise.then(() => {
|
this.managerPromise
|
||||||
|
.then(() => {
|
||||||
// Initialize the container. Must be done via a user action on mobile devices
|
// Initialize the container. Must be done via a user action on mobile devices
|
||||||
this.elements.displayContainer.initialize();
|
this.elements.displayContainer.initialize();
|
||||||
|
|
||||||
@ -447,15 +446,16 @@ class Ads {
|
|||||||
// VAST response
|
// VAST response
|
||||||
this.onAdError(adError);
|
this.onAdError(adError);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resume our video.
|
* Resume our video
|
||||||
*/
|
*/
|
||||||
resumeContent() {
|
resumeContent() {
|
||||||
// Hide our ad container
|
// Hide the advertisement container
|
||||||
utils.toggleHidden(this.elements.container, true);
|
this.elements.container.style.zIndex = '';
|
||||||
|
|
||||||
// Ad is stopped
|
// Ad is stopped
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
@ -470,8 +470,8 @@ class Ads {
|
|||||||
* Pause our video
|
* Pause our video
|
||||||
*/
|
*/
|
||||||
pauseContent() {
|
pauseContent() {
|
||||||
// Show our ad container.
|
// Show the advertisement container
|
||||||
utils.toggleHidden(this.elements.container, false);
|
this.elements.container.style.zIndex = 3;
|
||||||
|
|
||||||
// Ad is playing.
|
// Ad is playing.
|
||||||
this.playing = true;
|
this.playing = true;
|
||||||
@ -493,7 +493,7 @@ class Ads {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tell our instance that we're done for now
|
// Tell our instance that we're done for now
|
||||||
this.handleEventListeners('ERROR');
|
this.trigger('error');
|
||||||
|
|
||||||
// Re-create our adsManager
|
// Re-create our adsManager
|
||||||
this.loadAds();
|
this.loadAds();
|
||||||
@ -504,7 +504,8 @@ class Ads {
|
|||||||
*/
|
*/
|
||||||
loadAds() {
|
loadAds() {
|
||||||
// Tell our adsManager to go bye bye
|
// Tell our adsManager to go bye bye
|
||||||
this.managerPromise.then(() => {
|
this.managerPromise
|
||||||
|
.then(() => {
|
||||||
// Destroy our adsManager
|
// Destroy our adsManager
|
||||||
if (this.manager) {
|
if (this.manager) {
|
||||||
this.manager.destroy();
|
this.manager.destroy();
|
||||||
@ -512,22 +513,29 @@ class Ads {
|
|||||||
|
|
||||||
// Re-set our adsManager promises
|
// Re-set our adsManager promises
|
||||||
this.managerPromise = new Promise(resolve => {
|
this.managerPromise = new Promise(resolve => {
|
||||||
this.on('ADS_MANAGER_LOADED', () => resolve());
|
this.on('loaded', resolve);
|
||||||
this.player.debug.log(this.manager);
|
this.player.debug.log(this.manager);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Now request some new advertisements
|
// Now request some new advertisements
|
||||||
this.requestAds();
|
this.requestAds();
|
||||||
});
|
})
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles callbacks after an ad event was invoked
|
* Handles callbacks after an ad event was invoked
|
||||||
* @param {string} event - Event type
|
* @param {string} event - Event type
|
||||||
*/
|
*/
|
||||||
handleEventListeners(event) {
|
trigger(event, ...args) {
|
||||||
if (utils.is.function(this.events[event])) {
|
const handlers = this.events[event];
|
||||||
this.events[event].call(this);
|
|
||||||
|
if (utils.is.array(handlers)) {
|
||||||
|
handlers.forEach(handler => {
|
||||||
|
if (utils.is.function(handler)) {
|
||||||
|
handler.apply(this, args);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -538,7 +546,12 @@ class Ads {
|
|||||||
* @return {Ads}
|
* @return {Ads}
|
||||||
*/
|
*/
|
||||||
on(event, callback) {
|
on(event, callback) {
|
||||||
this.events[event] = callback;
|
if (!utils.is.array(this.events[event])) {
|
||||||
|
this.events[event] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.events[event].push(callback);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -553,7 +566,7 @@ class Ads {
|
|||||||
startSafetyTimer(time, from) {
|
startSafetyTimer(time, from) {
|
||||||
this.player.debug.log(`Safety timer invoked from: ${from}`);
|
this.player.debug.log(`Safety timer invoked from: ${from}`);
|
||||||
|
|
||||||
this.safetyTimer = window.setTimeout(() => {
|
this.safetyTimer = setTimeout(() => {
|
||||||
this.cancel();
|
this.cancel();
|
||||||
this.clearSafetyTimer('startSafetyTimer()');
|
this.clearSafetyTimer('startSafetyTimer()');
|
||||||
}, time);
|
}, time);
|
||||||
|
@ -2,9 +2,18 @@
|
|||||||
// Vimeo plugin
|
// Vimeo plugin
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import utils from './../utils';
|
|
||||||
import captions from './../captions';
|
import captions from './../captions';
|
||||||
|
import controls from './../controls';
|
||||||
import ui from './../ui';
|
import ui from './../ui';
|
||||||
|
import utils from './../utils';
|
||||||
|
|
||||||
|
// Set playback state and trigger change (only on actual change)
|
||||||
|
function assurePlaybackState(play) {
|
||||||
|
if (this.media.paused === play) {
|
||||||
|
this.media.paused = !play;
|
||||||
|
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const vimeo = {
|
const vimeo = {
|
||||||
setup() {
|
setup() {
|
||||||
@ -16,8 +25,13 @@ const vimeo = {
|
|||||||
|
|
||||||
// Load the API if not already
|
// Load the API if not already
|
||||||
if (!utils.is.object(window.Vimeo)) {
|
if (!utils.is.object(window.Vimeo)) {
|
||||||
utils.loadScript(this.config.urls.vimeo.api, () => {
|
utils
|
||||||
|
.loadScript(this.config.urls.vimeo.sdk)
|
||||||
|
.then(() => {
|
||||||
vimeo.ready.call(this);
|
vimeo.ready.call(this);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.debug.warn('Vimeo API failed to load', error);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
vimeo.ready.call(this);
|
vimeo.ready.call(this);
|
||||||
@ -29,10 +43,14 @@ const vimeo = {
|
|||||||
setAspectRatio(input) {
|
setAspectRatio(input) {
|
||||||
const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':');
|
const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':');
|
||||||
const padding = 100 / ratio[0] * ratio[1];
|
const padding = 100 / ratio[0] * ratio[1];
|
||||||
const height = 200;
|
|
||||||
const offset = (height - padding) / (height / 50);
|
|
||||||
this.elements.wrapper.style.paddingBottom = `${padding}%`;
|
this.elements.wrapper.style.paddingBottom = `${padding}%`;
|
||||||
|
|
||||||
|
if (this.supported.ui) {
|
||||||
|
const height = 240;
|
||||||
|
const offset = (height - padding) / (height / 50);
|
||||||
|
|
||||||
this.media.style.transform = `translateY(-${offset}%)`;
|
this.media.style.transform = `translateY(-${offset}%)`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// API Ready
|
// API Ready
|
||||||
@ -43,12 +61,14 @@ const vimeo = {
|
|||||||
const options = {
|
const options = {
|
||||||
loop: player.config.loop.active,
|
loop: player.config.loop.active,
|
||||||
autoplay: player.autoplay,
|
autoplay: player.autoplay,
|
||||||
|
// muted: player.muted,
|
||||||
byline: false,
|
byline: false,
|
||||||
portrait: false,
|
portrait: false,
|
||||||
title: false,
|
title: false,
|
||||||
speed: true,
|
speed: true,
|
||||||
transparent: 0,
|
transparent: 0,
|
||||||
gesture: 'media',
|
gesture: 'media',
|
||||||
|
playsinline: !this.config.fullscreen.iosNative,
|
||||||
};
|
};
|
||||||
const params = utils.buildUrlParams(options);
|
const params = utils.buildUrlParams(options);
|
||||||
|
|
||||||
@ -57,49 +77,69 @@ const vimeo = {
|
|||||||
|
|
||||||
// Get from <div> if needed
|
// Get from <div> if needed
|
||||||
if (utils.is.empty(source)) {
|
if (utils.is.empty(source)) {
|
||||||
source = player.media.getAttribute(this.config.attributes.embed.id);
|
source = player.media.getAttribute(player.config.attributes.embed.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = utils.parseVimeoId(source);
|
const id = utils.parseVimeoId(source);
|
||||||
|
|
||||||
// Build an iframe
|
// Build an iframe
|
||||||
const iframe = utils.createElement('iframe');
|
const iframe = utils.createElement('iframe');
|
||||||
const src = `https://player.vimeo.com/video/${id}?${params}`;
|
const src = utils.format(player.config.urls.vimeo.iframe, id, params);
|
||||||
iframe.setAttribute('src', src);
|
iframe.setAttribute('src', src);
|
||||||
iframe.setAttribute('allowfullscreen', '');
|
iframe.setAttribute('allowfullscreen', '');
|
||||||
iframe.setAttribute('allowtransparency', '');
|
iframe.setAttribute('allowtransparency', '');
|
||||||
iframe.setAttribute('allow', 'autoplay');
|
iframe.setAttribute('allow', 'autoplay');
|
||||||
|
|
||||||
// Inject the package
|
// Inject the package
|
||||||
const wrapper = utils.createElement('div');
|
const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
|
||||||
wrapper.appendChild(iframe);
|
wrapper.appendChild(iframe);
|
||||||
player.media = utils.replaceElement(wrapper, player.media);
|
player.media = utils.replaceElement(wrapper, player.media);
|
||||||
|
|
||||||
|
// Get poster image
|
||||||
|
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
|
||||||
|
if (utils.is.empty(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the URL for thumbnail
|
||||||
|
const url = new URL(response[0].thumbnail_large);
|
||||||
|
|
||||||
|
// Get original image
|
||||||
|
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
|
||||||
|
|
||||||
|
// Set and show poster
|
||||||
|
ui.setPoster.call(player, url.href);
|
||||||
|
});
|
||||||
|
|
||||||
// Setup instance
|
// Setup instance
|
||||||
// https://github.com/vimeo/player.js
|
// https://github.com/vimeo/player.js
|
||||||
player.embed = new window.Vimeo.Player(iframe);
|
player.embed = new window.Vimeo.Player(iframe, {
|
||||||
|
autopause: player.config.autopause,
|
||||||
|
muted: player.muted,
|
||||||
|
});
|
||||||
|
|
||||||
player.media.paused = true;
|
player.media.paused = true;
|
||||||
player.media.currentTime = 0;
|
player.media.currentTime = 0;
|
||||||
|
|
||||||
|
// Disable native text track rendering
|
||||||
|
if (player.supported.ui) {
|
||||||
|
player.embed.disableTextTrack();
|
||||||
|
}
|
||||||
|
|
||||||
// Create a faux HTML5 API using the Vimeo API
|
// Create a faux HTML5 API using the Vimeo API
|
||||||
player.media.play = () => {
|
player.media.play = () => {
|
||||||
player.embed.play().then(() => {
|
assurePlaybackState.call(player, true);
|
||||||
player.media.paused = false;
|
return player.embed.play();
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
player.media.pause = () => {
|
player.media.pause = () => {
|
||||||
player.embed.pause().then(() => {
|
assurePlaybackState.call(player, false);
|
||||||
player.media.paused = true;
|
return player.embed.pause();
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
player.media.stop = () => {
|
player.media.stop = () => {
|
||||||
player.embed.stop().then(() => {
|
player.pause();
|
||||||
player.media.paused = true;
|
|
||||||
player.currentTime = 0;
|
player.currentTime = 0;
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Seeking
|
// Seeking
|
||||||
@ -109,23 +149,26 @@ const vimeo = {
|
|||||||
return currentTime;
|
return currentTime;
|
||||||
},
|
},
|
||||||
set(time) {
|
set(time) {
|
||||||
// Get current paused state
|
// Vimeo will automatically play on seek if the video hasn't been played before
|
||||||
// Vimeo will automatically play on seek
|
|
||||||
const { paused } = player.media;
|
|
||||||
|
|
||||||
// Set seeking flag
|
// Get current paused state and volume etc
|
||||||
player.media.seeking = true;
|
const { embed, media, paused, volume } = player;
|
||||||
|
|
||||||
// Trigger seeking
|
// Set seeking state and trigger event
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeking');
|
media.seeking = true;
|
||||||
|
utils.dispatchEvent.call(player, media, 'seeking');
|
||||||
|
|
||||||
// Seek after events
|
// If paused, mute until seek is complete
|
||||||
player.embed.setCurrentTime(time);
|
Promise.resolve(paused && embed.setVolume(0))
|
||||||
|
// Seek
|
||||||
// Restore pause state
|
.then(() => embed.setCurrentTime(time))
|
||||||
if (paused) {
|
// Restore paused
|
||||||
player.pause();
|
.then(() => paused && embed.pause())
|
||||||
}
|
// Restore volume
|
||||||
|
.then(() => paused && embed.setVolume(volume))
|
||||||
|
.catch(() => {
|
||||||
|
// Do nothing
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -136,9 +179,17 @@ const vimeo = {
|
|||||||
return speed;
|
return speed;
|
||||||
},
|
},
|
||||||
set(input) {
|
set(input) {
|
||||||
player.embed.setPlaybackRate(input).then(() => {
|
player.embed
|
||||||
|
.setPlaybackRate(input)
|
||||||
|
.then(() => {
|
||||||
speed = input;
|
speed = input;
|
||||||
utils.dispatchEvent.call(player, player.media, 'ratechange');
|
utils.dispatchEvent.call(player, player.media, 'ratechange');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Hide menu item (and menu if empty)
|
||||||
|
if (error.name === 'Error') {
|
||||||
|
controls.setSpeedMenu.call(player, []);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -190,9 +241,15 @@ const vimeo = {
|
|||||||
|
|
||||||
// Source
|
// Source
|
||||||
let currentSrc;
|
let currentSrc;
|
||||||
player.embed.getVideoUrl().then(value => {
|
player.embed
|
||||||
|
.getVideoUrl()
|
||||||
|
.then(value => {
|
||||||
currentSrc = value;
|
currentSrc = value;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.debug.warn(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(player.media, 'currentSrc', {
|
Object.defineProperty(player.media, 'currentSrc', {
|
||||||
get() {
|
get() {
|
||||||
return currentSrc;
|
return currentSrc;
|
||||||
@ -265,17 +322,12 @@ const vimeo = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('play', () => {
|
player.embed.on('play', () => {
|
||||||
// Only fire play if paused before
|
assurePlaybackState.call(player, true);
|
||||||
if (player.media.paused) {
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'play');
|
|
||||||
}
|
|
||||||
player.media.paused = false;
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
utils.dispatchEvent.call(player, player.media, 'playing');
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('pause', () => {
|
player.embed.on('pause', () => {
|
||||||
player.media.paused = true;
|
assurePlaybackState.call(player, false);
|
||||||
utils.dispatchEvent.call(player, player.media, 'pause');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('timeupdate', data => {
|
player.embed.on('timeupdate', data => {
|
||||||
@ -292,12 +344,20 @@ const vimeo = {
|
|||||||
if (parseInt(data.percent, 10) === 1) {
|
if (parseInt(data.percent, 10) === 1) {
|
||||||
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
|
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get duration as if we do it before load, it gives an incorrect value
|
||||||
|
// https://github.com/sampotts/plyr/issues/891
|
||||||
|
player.embed.getDuration().then(value => {
|
||||||
|
if (value !== player.media.duration) {
|
||||||
|
player.media.duration = value;
|
||||||
|
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('seeked', () => {
|
player.embed.on('seeked', () => {
|
||||||
player.media.seeking = false;
|
player.media.seeking = false;
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeked');
|
utils.dispatchEvent.call(player, player.media, 'seeked');
|
||||||
utils.dispatchEvent.call(player, player.media, 'play');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('ended', () => {
|
player.embed.on('ended', () => {
|
||||||
@ -311,7 +371,7 @@ const vimeo = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Rebuild UI
|
// Rebuild UI
|
||||||
window.setTimeout(() => ui.build.call(player), 0);
|
setTimeout(() => ui.build.call(player), 0);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,9 +2,75 @@
|
|||||||
// YouTube plugin
|
// YouTube plugin
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import utils from './../utils';
|
|
||||||
import controls from './../controls';
|
import controls from './../controls';
|
||||||
import ui from './../ui';
|
import ui from './../ui';
|
||||||
|
import utils from './../utils';
|
||||||
|
|
||||||
|
// Standardise YouTube quality unit
|
||||||
|
function mapQualityUnit(input) {
|
||||||
|
switch (input) {
|
||||||
|
case 'hd2160':
|
||||||
|
return 2160;
|
||||||
|
|
||||||
|
case 2160:
|
||||||
|
return 'hd2160';
|
||||||
|
|
||||||
|
case 'hd1440':
|
||||||
|
return 1440;
|
||||||
|
|
||||||
|
case 1440:
|
||||||
|
return 'hd1440';
|
||||||
|
|
||||||
|
case 'hd1080':
|
||||||
|
return 1080;
|
||||||
|
|
||||||
|
case 1080:
|
||||||
|
return 'hd1080';
|
||||||
|
|
||||||
|
case 'hd720':
|
||||||
|
return 720;
|
||||||
|
|
||||||
|
case 720:
|
||||||
|
return 'hd720';
|
||||||
|
|
||||||
|
case 'large':
|
||||||
|
return 480;
|
||||||
|
|
||||||
|
case 480:
|
||||||
|
return 'large';
|
||||||
|
|
||||||
|
case 'medium':
|
||||||
|
return 360;
|
||||||
|
|
||||||
|
case 360:
|
||||||
|
return 'medium';
|
||||||
|
|
||||||
|
case 'small':
|
||||||
|
return 240;
|
||||||
|
|
||||||
|
case 240:
|
||||||
|
return 'small';
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapQualityUnits(levels) {
|
||||||
|
if (utils.is.empty(levels)) {
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set playback state and trigger change (only on actual change)
|
||||||
|
function assurePlaybackState(play) {
|
||||||
|
if (this.media.paused === play) {
|
||||||
|
this.media.paused = !play;
|
||||||
|
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const youtube = {
|
const youtube = {
|
||||||
setup() {
|
setup() {
|
||||||
@ -19,7 +85,9 @@ const youtube = {
|
|||||||
youtube.ready.call(this);
|
youtube.ready.call(this);
|
||||||
} else {
|
} else {
|
||||||
// Load the API
|
// Load the API
|
||||||
utils.loadScript(this.config.urls.youtube.api);
|
utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
|
||||||
|
this.debug.warn('YouTube API failed to load', error);
|
||||||
|
});
|
||||||
|
|
||||||
// Setup callback for the API
|
// Setup callback for the API
|
||||||
// YouTube has it's own system of course...
|
// YouTube has it's own system of course...
|
||||||
@ -57,7 +125,7 @@ const youtube = {
|
|||||||
// Or via Google API
|
// Or via Google API
|
||||||
const key = this.config.keys.google;
|
const key = this.config.keys.google;
|
||||||
if (utils.is.string(key) && !utils.is.empty(key)) {
|
if (utils.is.string(key) && !utils.is.empty(key)) {
|
||||||
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`;
|
const url = utils.format(this.config.urls.youtube.api, videoId, key);
|
||||||
|
|
||||||
utils
|
utils
|
||||||
.fetch(url)
|
.fetch(url)
|
||||||
@ -101,6 +169,21 @@ const youtube = {
|
|||||||
const container = utils.createElement('div', { id });
|
const container = utils.createElement('div', { id });
|
||||||
player.media = utils.replaceElement(container, player.media);
|
player.media = utils.replaceElement(container, player.media);
|
||||||
|
|
||||||
|
// Set poster image
|
||||||
|
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
|
||||||
|
|
||||||
|
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
||||||
|
utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
|
||||||
|
.catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
|
||||||
|
.catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
|
||||||
|
.then(image => ui.setPoster.call(player, image.src))
|
||||||
|
.then(posterSrc => {
|
||||||
|
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
|
||||||
|
if (!posterSrc.includes('maxres')) {
|
||||||
|
player.elements.poster.style.backgroundSize = 'cover';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Setup instance
|
// Setup instance
|
||||||
// https://developers.google.com/youtube/iframe_api_reference
|
// https://developers.google.com/youtube/iframe_api_reference
|
||||||
player.embed = new window.YT.Player(id, {
|
player.embed = new window.YT.Player(id, {
|
||||||
@ -166,14 +249,10 @@ const youtube = {
|
|||||||
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'error');
|
utils.dispatchEvent.call(player, player.media, 'error');
|
||||||
},
|
},
|
||||||
onPlaybackQualityChange(event) {
|
onPlaybackQualityChange() {
|
||||||
// Get the instance
|
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
|
||||||
const instance = event.target;
|
quality: player.media.quality,
|
||||||
|
});
|
||||||
// Get current quality
|
|
||||||
player.media.quality = instance.getPlaybackQuality();
|
|
||||||
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'qualitychange');
|
|
||||||
},
|
},
|
||||||
onPlaybackRateChange(event) {
|
onPlaybackRateChange(event) {
|
||||||
// Get the instance
|
// Get the instance
|
||||||
@ -193,18 +272,17 @@ const youtube = {
|
|||||||
|
|
||||||
// Create a faux HTML5 API using the YouTube API
|
// Create a faux HTML5 API using the YouTube API
|
||||||
player.media.play = () => {
|
player.media.play = () => {
|
||||||
|
assurePlaybackState.call(player, true);
|
||||||
instance.playVideo();
|
instance.playVideo();
|
||||||
player.media.paused = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
player.media.pause = () => {
|
player.media.pause = () => {
|
||||||
|
assurePlaybackState.call(player, false);
|
||||||
instance.pauseVideo();
|
instance.pauseVideo();
|
||||||
player.media.paused = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
player.media.stop = () => {
|
player.media.stop = () => {
|
||||||
instance.stopVideo();
|
instance.stopVideo();
|
||||||
player.media.paused = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
player.media.duration = instance.getDuration();
|
player.media.duration = instance.getDuration();
|
||||||
@ -217,10 +295,13 @@ const youtube = {
|
|||||||
return Number(instance.getCurrentTime());
|
return Number(instance.getCurrentTime());
|
||||||
},
|
},
|
||||||
set(time) {
|
set(time) {
|
||||||
// Set seeking flag
|
// If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
|
||||||
player.media.seeking = true;
|
if (player.paused) {
|
||||||
|
player.embed.mute();
|
||||||
|
}
|
||||||
|
|
||||||
// Trigger seeking
|
// Set seeking state and trigger event
|
||||||
|
player.media.seeking = true;
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeking');
|
utils.dispatchEvent.call(player, player.media, 'seeking');
|
||||||
|
|
||||||
// Seek after events sent
|
// Seek after events sent
|
||||||
@ -241,15 +322,18 @@ const youtube = {
|
|||||||
// Quality
|
// Quality
|
||||||
Object.defineProperty(player.media, 'quality', {
|
Object.defineProperty(player.media, 'quality', {
|
||||||
get() {
|
get() {
|
||||||
return instance.getPlaybackQuality();
|
return mapQualityUnit(instance.getPlaybackQuality());
|
||||||
},
|
},
|
||||||
set(input) {
|
set(input) {
|
||||||
|
const quality = input;
|
||||||
|
|
||||||
|
// Set via API
|
||||||
|
instance.setPlaybackQuality(mapQualityUnit(quality));
|
||||||
|
|
||||||
// Trigger request event
|
// Trigger request event
|
||||||
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
|
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
|
||||||
quality: input,
|
quality,
|
||||||
});
|
});
|
||||||
|
|
||||||
instance.setPlaybackQuality(input);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -306,10 +390,10 @@ const youtube = {
|
|||||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
||||||
|
|
||||||
// Reset timer
|
// Reset timer
|
||||||
window.clearInterval(player.timers.buffering);
|
clearInterval(player.timers.buffering);
|
||||||
|
|
||||||
// Setup buffering
|
// Setup buffering
|
||||||
player.timers.buffering = window.setInterval(() => {
|
player.timers.buffering = setInterval(() => {
|
||||||
// Get loaded % from YouTube
|
// Get loaded % from YouTube
|
||||||
player.media.buffered = instance.getVideoLoadedFraction();
|
player.media.buffered = instance.getVideoLoadedFraction();
|
||||||
|
|
||||||
@ -323,7 +407,7 @@ const youtube = {
|
|||||||
|
|
||||||
// Bail if we're at 100%
|
// Bail if we're at 100%
|
||||||
if (player.media.buffered === 1) {
|
if (player.media.buffered === 1) {
|
||||||
window.clearInterval(player.timers.buffering);
|
clearInterval(player.timers.buffering);
|
||||||
|
|
||||||
// Trigger event
|
// Trigger event
|
||||||
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
|
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
|
||||||
@ -331,14 +415,25 @@ const youtube = {
|
|||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
// Rebuild UI
|
// Rebuild UI
|
||||||
window.setTimeout(() => ui.build.call(player), 50);
|
setTimeout(() => ui.build.call(player), 50);
|
||||||
},
|
},
|
||||||
onStateChange(event) {
|
onStateChange(event) {
|
||||||
// Get the instance
|
// Get the instance
|
||||||
const instance = event.target;
|
const instance = event.target;
|
||||||
|
|
||||||
// Reset timer
|
// Reset timer
|
||||||
window.clearInterval(player.timers.playing);
|
clearInterval(player.timers.playing);
|
||||||
|
|
||||||
|
const seeked = player.media.seeking && [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
].includes(event.data);
|
||||||
|
|
||||||
|
if (seeked) {
|
||||||
|
// Unset seeking and fire seeked event
|
||||||
|
player.media.seeking = false;
|
||||||
|
utils.dispatchEvent.call(player, player.media, 'seeked');
|
||||||
|
}
|
||||||
|
|
||||||
// Handle events
|
// Handle events
|
||||||
// -1 Unstarted
|
// -1 Unstarted
|
||||||
@ -348,8 +443,18 @@ const youtube = {
|
|||||||
// 3 Buffering
|
// 3 Buffering
|
||||||
// 5 Video cued
|
// 5 Video cued
|
||||||
switch (event.data) {
|
switch (event.data) {
|
||||||
|
case -1:
|
||||||
|
// Update scrubber
|
||||||
|
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
||||||
|
|
||||||
|
// Get loaded % from YouTube
|
||||||
|
player.media.buffered = instance.getVideoLoadedFraction();
|
||||||
|
utils.dispatchEvent.call(player, player.media, 'progress');
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
case 0:
|
case 0:
|
||||||
player.media.paused = true;
|
assurePlaybackState.call(player, false);
|
||||||
|
|
||||||
// YouTube doesn't support loop for a single video, so mimick it.
|
// YouTube doesn't support loop for a single video, so mimick it.
|
||||||
if (player.media.loop) {
|
if (player.media.loop) {
|
||||||
@ -363,22 +468,16 @@ const youtube = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
// If we were seeking, fire seeked event
|
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
|
||||||
if (player.media.seeking) {
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeked');
|
|
||||||
}
|
|
||||||
player.media.seeking = false;
|
|
||||||
|
|
||||||
// Only fire play if paused before
|
|
||||||
if (player.media.paused) {
|
if (player.media.paused) {
|
||||||
utils.dispatchEvent.call(player, player.media, 'play');
|
player.media.pause();
|
||||||
}
|
} else {
|
||||||
player.media.paused = false;
|
assurePlaybackState.call(player, true);
|
||||||
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
utils.dispatchEvent.call(player, player.media, 'playing');
|
||||||
|
|
||||||
// Poll to get playback progress
|
// Poll to get playback progress
|
||||||
player.timers.playing = window.setInterval(() => {
|
player.timers.playing = setInterval(() => {
|
||||||
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
@ -391,14 +490,17 @@ const youtube = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get quality
|
// Get quality
|
||||||
controls.setQualityMenu.call(player, instance.getAvailableQualityLevels());
|
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
player.media.paused = true;
|
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
|
||||||
|
if (!player.muted) {
|
||||||
utils.dispatchEvent.call(player, player.media, 'pause');
|
player.embed.unMute();
|
||||||
|
}
|
||||||
|
assurePlaybackState.call(player, false);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
419
src/js/plyr.js
419
src/js/plyr.js
@ -1,26 +1,24 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Plyr
|
// Plyr
|
||||||
// plyr.js v3.0.0-beta.13
|
// plyr.js v3.3.9
|
||||||
// https://github.com/sampotts/plyr
|
// https://github.com/sampotts/plyr
|
||||||
// License: The MIT License (MIT)
|
// License: The MIT License (MIT)
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import { providers, types } from './types';
|
|
||||||
import defaults from './defaults';
|
|
||||||
import support from './support';
|
|
||||||
import utils from './utils';
|
|
||||||
|
|
||||||
import Console from './console';
|
|
||||||
import Fullscreen from './fullscreen';
|
|
||||||
import Storage from './storage';
|
|
||||||
import Ads from './plugins/ads';
|
|
||||||
|
|
||||||
import captions from './captions';
|
import captions from './captions';
|
||||||
|
import Console from './console';
|
||||||
import controls from './controls';
|
import controls from './controls';
|
||||||
import listeners from './listeners';
|
import defaults from './defaults';
|
||||||
|
import Fullscreen from './fullscreen';
|
||||||
|
import Listeners from './listeners';
|
||||||
import media from './media';
|
import media from './media';
|
||||||
|
import Ads from './plugins/ads';
|
||||||
import source from './source';
|
import source from './source';
|
||||||
|
import Storage from './storage';
|
||||||
|
import support from './support';
|
||||||
|
import { providers, types } from './types';
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
|
import utils from './utils';
|
||||||
|
|
||||||
// Private properties
|
// Private properties
|
||||||
// TODO: Use a WeakMap for private globals
|
// TODO: Use a WeakMap for private globals
|
||||||
@ -36,6 +34,9 @@ class Plyr {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.failed = false;
|
this.failed = false;
|
||||||
|
|
||||||
|
// Touch device
|
||||||
|
this.touch = support.touch;
|
||||||
|
|
||||||
// Set the media element
|
// Set the media element
|
||||||
this.media = target;
|
this.media = target;
|
||||||
|
|
||||||
@ -54,7 +55,8 @@ class Plyr {
|
|||||||
this.config = utils.extend(
|
this.config = utils.extend(
|
||||||
{},
|
{},
|
||||||
defaults,
|
defaults,
|
||||||
options,
|
Plyr.defaults,
|
||||||
|
options || {},
|
||||||
(() => {
|
(() => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(this.media.getAttribute('data-plyr-config'));
|
return JSON.parse(this.media.getAttribute('data-plyr-config'));
|
||||||
@ -94,6 +96,7 @@ class Plyr {
|
|||||||
this.options = {
|
this.options = {
|
||||||
speed: [],
|
speed: [],
|
||||||
quality: [],
|
quality: [],
|
||||||
|
captions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debugging
|
// Debugging
|
||||||
@ -130,7 +133,9 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache original element state for .destroy()
|
// Cache original element state for .destroy()
|
||||||
this.elements.original = this.media.cloneNode(true);
|
const clone = this.media.cloneNode(true);
|
||||||
|
clone.autoplay = false;
|
||||||
|
this.elements.original = clone;
|
||||||
|
|
||||||
// Set media type based on tag or data attribute
|
// Set media type based on tag or data attribute
|
||||||
// Supported: video, audio, vimeo, youtube
|
// Supported: video, audio, vimeo, youtube
|
||||||
@ -171,12 +176,17 @@ class Plyr {
|
|||||||
if (truthy.includes(params.autoplay)) {
|
if (truthy.includes(params.autoplay)) {
|
||||||
this.config.autoplay = true;
|
this.config.autoplay = true;
|
||||||
}
|
}
|
||||||
if (truthy.includes(params.playsinline)) {
|
|
||||||
this.config.inline = true;
|
|
||||||
}
|
|
||||||
if (truthy.includes(params.loop)) {
|
if (truthy.includes(params.loop)) {
|
||||||
this.config.loop.active = true;
|
this.config.loop.active = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: replace fullscreen.iosNative with this playsinline config option
|
||||||
|
// YouTube requires the playsinline in the URL
|
||||||
|
if (this.isYouTube) {
|
||||||
|
this.config.playsinline = truthy.includes(params.playsinline);
|
||||||
|
} else {
|
||||||
|
this.config.playsinline = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// <div> with attributes
|
// <div> with attributes
|
||||||
@ -210,7 +220,7 @@ class Plyr {
|
|||||||
this.config.autoplay = true;
|
this.config.autoplay = true;
|
||||||
}
|
}
|
||||||
if (this.media.hasAttribute('playsinline')) {
|
if (this.media.hasAttribute('playsinline')) {
|
||||||
this.config.inline = true;
|
this.config.playsinline = true;
|
||||||
}
|
}
|
||||||
if (this.media.hasAttribute('muted')) {
|
if (this.media.hasAttribute('muted')) {
|
||||||
this.config.muted = true;
|
this.config.muted = true;
|
||||||
@ -227,7 +237,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.inline);
|
this.supported = support.check(this.type, this.provider, this.config.playsinline);
|
||||||
|
|
||||||
// If no support for even API, bail
|
// If no support for even API, bail
|
||||||
if (!this.supported.api) {
|
if (!this.supported.api) {
|
||||||
@ -235,6 +245,9 @@ class Plyr {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create listeners
|
||||||
|
this.listeners = new Listeners(this);
|
||||||
|
|
||||||
// Setup local storage for user settings
|
// Setup local storage for user settings
|
||||||
this.storage = new Storage(this);
|
this.storage = new Storage(this);
|
||||||
|
|
||||||
@ -250,9 +263,6 @@ class Plyr {
|
|||||||
// Allow focus to be captured
|
// Allow focus to be captured
|
||||||
this.elements.container.setAttribute('tabindex', 0);
|
this.elements.container.setAttribute('tabindex', 0);
|
||||||
|
|
||||||
// Global listeners
|
|
||||||
listeners.global.call(this);
|
|
||||||
|
|
||||||
// Add style hook
|
// Add style hook
|
||||||
ui.addStyleHook.call(this);
|
ui.addStyleHook.call(this);
|
||||||
|
|
||||||
@ -272,11 +282,22 @@ class Plyr {
|
|||||||
ui.build.call(this);
|
ui.build.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Container listeners
|
||||||
|
this.listeners.container();
|
||||||
|
|
||||||
|
// Global listeners
|
||||||
|
this.listeners.global();
|
||||||
|
|
||||||
// Setup fullscreen
|
// Setup fullscreen
|
||||||
this.fullscreen = new Fullscreen(this);
|
this.fullscreen = new Fullscreen(this);
|
||||||
|
|
||||||
// Setup ads if provided
|
// Setup ads if provided
|
||||||
this.ads = new Ads(this);
|
this.ads = new Ads(this);
|
||||||
|
|
||||||
|
// Autoplay if required
|
||||||
|
if (this.config.autoplay) {
|
||||||
|
this.play();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
@ -287,31 +308,29 @@ class Plyr {
|
|||||||
* Types and provider helpers
|
* Types and provider helpers
|
||||||
*/
|
*/
|
||||||
get isHTML5() {
|
get isHTML5() {
|
||||||
return this.provider === providers.html5;
|
return Boolean(this.provider === providers.html5);
|
||||||
}
|
}
|
||||||
get isEmbed() {
|
get isEmbed() {
|
||||||
return this.isYouTube || this.isVimeo;
|
return Boolean(this.isYouTube || this.isVimeo);
|
||||||
}
|
}
|
||||||
get isYouTube() {
|
get isYouTube() {
|
||||||
return this.provider === providers.youtube;
|
return Boolean(this.provider === providers.youtube);
|
||||||
}
|
}
|
||||||
get isVimeo() {
|
get isVimeo() {
|
||||||
return this.provider === providers.vimeo;
|
return Boolean(this.provider === providers.vimeo);
|
||||||
}
|
}
|
||||||
get isVideo() {
|
get isVideo() {
|
||||||
return this.type === types.video;
|
return Boolean(this.type === types.video);
|
||||||
}
|
}
|
||||||
get isAudio() {
|
get isAudio() {
|
||||||
return this.type === types.audio;
|
return Boolean(this.type === types.audio);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play the media, or play the advertisement (if they are not blocked)
|
* Play the media, or play the advertisement (if they are not blocked)
|
||||||
*/
|
*/
|
||||||
play() {
|
play() {
|
||||||
// TODO: Always return a promise?
|
if (!utils.is.function(this.media.play)) {
|
||||||
if (this.ads.enabled && !this.ads.initialized && !this.ads.blocked) {
|
|
||||||
this.ads.play();
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,32 +342,39 @@ class Plyr {
|
|||||||
* Pause the media
|
* Pause the media
|
||||||
*/
|
*/
|
||||||
pause() {
|
pause() {
|
||||||
if (!this.playing) {
|
if (!this.playing || !utils.is.function(this.media.pause)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.media.pause();
|
this.media.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get paused state
|
|
||||||
*/
|
|
||||||
get paused() {
|
|
||||||
return this.media.paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get playing state
|
* Get playing state
|
||||||
*/
|
*/
|
||||||
get playing() {
|
get playing() {
|
||||||
return !this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true);
|
return Boolean(this.ready && !this.paused && !this.ended);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get paused state
|
||||||
|
*/
|
||||||
|
get paused() {
|
||||||
|
return Boolean(this.media.paused);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stopped state
|
||||||
|
*/
|
||||||
|
get stopped() {
|
||||||
|
return Boolean(this.paused && this.currentTime === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get ended state
|
* Get ended state
|
||||||
*/
|
*/
|
||||||
get ended() {
|
get ended() {
|
||||||
return this.media.ended;
|
return Boolean(this.media.ended);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -370,8 +396,12 @@ class Plyr {
|
|||||||
* Stop playback
|
* Stop playback
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop() {
|
||||||
this.restart();
|
if (this.isHTML5) {
|
||||||
this.pause();
|
this.pause();
|
||||||
|
this.restart();
|
||||||
|
} else if (utils.is.function(this.media.stop)) {
|
||||||
|
this.media.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -402,21 +432,16 @@ class Plyr {
|
|||||||
* @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
|
* @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
|
||||||
*/
|
*/
|
||||||
set currentTime(input) {
|
set currentTime(input) {
|
||||||
let targetTime = 0;
|
// Bail if media duration isn't available yet
|
||||||
|
if (!this.duration) {
|
||||||
if (utils.is.number(input)) {
|
return;
|
||||||
targetTime = input;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalise targetTime
|
// Validate input
|
||||||
if (targetTime < 0) {
|
const inputIsValid = utils.is.number(input) && input > 0;
|
||||||
targetTime = 0;
|
|
||||||
} else if (targetTime > this.duration) {
|
|
||||||
targetTime = this.duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set
|
// Set
|
||||||
this.media.currentTime = targetTime.toFixed(4);
|
this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
this.debug.log(`Seeking to ${this.currentTime} seconds`);
|
this.debug.log(`Seeking to ${this.currentTime} seconds`);
|
||||||
@ -429,11 +454,32 @@ class Plyr {
|
|||||||
return Number(this.media.currentTime);
|
return Number(this.media.currentTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get buffered
|
||||||
|
*/
|
||||||
|
get buffered() {
|
||||||
|
const { buffered } = this.media;
|
||||||
|
|
||||||
|
// YouTube / Vimeo return a float between 0-1
|
||||||
|
if (utils.is.number(buffered)) {
|
||||||
|
return buffered;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML5
|
||||||
|
// TODO: Handle buffered chunks of the media
|
||||||
|
// (i.e. seek to another section buffers only that section)
|
||||||
|
if (buffered && buffered.length && this.duration > 0) {
|
||||||
|
return buffered.end(0) / this.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get seeking status
|
* Get seeking status
|
||||||
*/
|
*/
|
||||||
get seeking() {
|
get seeking() {
|
||||||
return this.media.seeking;
|
return Boolean(this.media.seeking);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -441,13 +487,13 @@ class Plyr {
|
|||||||
*/
|
*/
|
||||||
get duration() {
|
get duration() {
|
||||||
// Faux duration set via config
|
// Faux duration set via config
|
||||||
const fauxDuration = parseInt(this.config.duration, 10);
|
const fauxDuration = parseFloat(this.config.duration);
|
||||||
|
|
||||||
// True duration
|
// Media duration can be NaN before the media has loaded
|
||||||
const realDuration = Number(this.media.duration);
|
const duration = (this.media || {}).duration || 0;
|
||||||
|
|
||||||
// If custom duration is funky, use regular duration
|
// If config duration is funky, use regular duration
|
||||||
return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration;
|
return fauxDuration || duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -488,8 +534,8 @@ class Plyr {
|
|||||||
// Set the player volume
|
// Set the player volume
|
||||||
this.media.volume = volume;
|
this.media.volume = volume;
|
||||||
|
|
||||||
// If muted, and we're increasing volume, reset muted state
|
// If muted, and we're increasing volume manually, reset muted state
|
||||||
if (this.muted && volume > 0) {
|
if (!utils.is.empty(value) && this.muted && volume > 0) {
|
||||||
this.muted = false;
|
this.muted = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -498,7 +544,7 @@ class Plyr {
|
|||||||
* Get the current player volume
|
* Get the current player volume
|
||||||
*/
|
*/
|
||||||
get volume() {
|
get volume() {
|
||||||
return this.media.volume;
|
return Number(this.media.volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -547,7 +593,7 @@ class Plyr {
|
|||||||
* Get current muted state
|
* Get current muted state
|
||||||
*/
|
*/
|
||||||
get muted() {
|
get muted() {
|
||||||
return this.media.muted;
|
return Boolean(this.media.muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -564,12 +610,16 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get audio tracks
|
// Get audio tracks
|
||||||
return this.media.mozHasAudio || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);
|
return (
|
||||||
|
Boolean(this.media.mozHasAudio) ||
|
||||||
|
Boolean(this.media.webkitAudioDecodedByteCount) ||
|
||||||
|
Boolean(this.media.audioTracks && this.media.audioTracks.length)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set playback speed
|
* Set playback speed
|
||||||
* @param {decimal} speed - the speed of playback (0.5-2.0)
|
* @param {number} speed - the speed of playback (0.5-2.0)
|
||||||
*/
|
*/
|
||||||
set speed(input) {
|
set speed(input) {
|
||||||
let speed = null;
|
let speed = null;
|
||||||
@ -610,34 +660,43 @@ class Plyr {
|
|||||||
* Get current playback speed
|
* Get current playback speed
|
||||||
*/
|
*/
|
||||||
get speed() {
|
get speed() {
|
||||||
return this.media.playbackRate;
|
return Number(this.media.playbackRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set playback quality
|
* Set playback quality
|
||||||
* Currently YouTube only
|
* Currently HTML5 & YouTube only
|
||||||
* @param {string} input - Quality level
|
* @param {number} input - Quality level
|
||||||
*/
|
*/
|
||||||
set quality(input) {
|
set quality(input) {
|
||||||
let quality = null;
|
let quality = null;
|
||||||
|
|
||||||
if (utils.is.string(input)) {
|
if (!utils.is.empty(input)) {
|
||||||
quality = input;
|
quality = Number(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!utils.is.string(quality)) {
|
if (!utils.is.number(quality) || quality === 0) {
|
||||||
quality = this.storage.get('quality');
|
quality = this.storage.get('quality');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!utils.is.string(quality)) {
|
if (!utils.is.number(quality)) {
|
||||||
quality = this.config.quality.selected;
|
quality = this.config.quality.selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.options.quality.includes(quality)) {
|
if (!utils.is.number(quality)) {
|
||||||
this.debug.warn(`Unsupported quality option (${quality})`);
|
quality = this.config.quality.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.options.quality.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.options.quality.includes(quality)) {
|
||||||
|
const closest = utils.closest(this.options.quality, quality);
|
||||||
|
this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`);
|
||||||
|
quality = closest;
|
||||||
|
}
|
||||||
|
|
||||||
// Update config
|
// Update config
|
||||||
this.config.quality.selected = quality;
|
this.config.quality.selected = quality;
|
||||||
|
|
||||||
@ -710,7 +769,7 @@ class Plyr {
|
|||||||
* Get current loop state
|
* Get current loop state
|
||||||
*/
|
*/
|
||||||
get loop() {
|
get loop() {
|
||||||
return this.media.loop;
|
return Boolean(this.media.loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -729,25 +788,23 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the poster image for a HTML5 video
|
* Set the poster image for a video
|
||||||
* @param {input} - the URL for the new poster image
|
* @param {input} - the URL for the new poster image
|
||||||
*/
|
*/
|
||||||
set poster(input) {
|
set poster(input) {
|
||||||
if (!this.isHTML5 || !this.isVideo) {
|
if (!this.isVideo) {
|
||||||
this.debug.warn('Poster can only be set on HTML5 video');
|
this.debug.warn('Poster can only be set for video');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (utils.is.string(input)) {
|
ui.setPoster.call(this, input);
|
||||||
this.media.setAttribute('poster', input);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current poster image
|
* Get the current poster image
|
||||||
*/
|
*/
|
||||||
get poster() {
|
get poster() {
|
||||||
if (!this.isHTML5 || !this.isVideo) {
|
if (!this.isVideo) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -767,7 +824,7 @@ class Plyr {
|
|||||||
* Get the current autoplay state
|
* Get the current autoplay state
|
||||||
*/
|
*/
|
||||||
get autoplay() {
|
get autoplay() {
|
||||||
return this.config.autoplay;
|
return Boolean(this.config.autoplay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -775,13 +832,13 @@ class Plyr {
|
|||||||
* @param {boolean} input - Whether to enable captions
|
* @param {boolean} input - Whether to enable captions
|
||||||
*/
|
*/
|
||||||
toggleCaptions(input) {
|
toggleCaptions(input) {
|
||||||
// If there's no full support, or there's no caption toggle
|
// If there's no full support
|
||||||
if (!this.supported.ui || !utils.is.element(this.elements.buttons.captions)) {
|
if (!this.supported.ui) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the method is called without parameter, toggle based on current value
|
// If the method is called without parameter, toggle based on current value
|
||||||
const show = utils.is.boolean(input) ? input : this.elements.container.className.indexOf(this.config.classNames.captions.active) === -1;
|
const show = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
|
||||||
|
|
||||||
// Nothing to change...
|
// Nothing to change...
|
||||||
if (this.captions.active === show) {
|
if (this.captions.active === show) {
|
||||||
@ -811,17 +868,29 @@ class Plyr {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle captions based on input
|
|
||||||
this.toggleCaptions(!utils.is.empty(input));
|
|
||||||
|
|
||||||
// If empty string is passed, assume disable captions
|
// If empty string is passed, assume disable captions
|
||||||
if (utils.is.empty(input)) {
|
if (utils.is.empty(input)) {
|
||||||
|
this.toggleCaptions(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize
|
// Normalize
|
||||||
const language = input.toLowerCase();
|
const language = input.toLowerCase();
|
||||||
|
|
||||||
|
// Check for support
|
||||||
|
if (!this.options.captions.includes(language)) {
|
||||||
|
this.debug.warn(`Unsupported language option: ${language}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure captions are enabled
|
||||||
|
this.toggleCaptions(true);
|
||||||
|
|
||||||
|
// Enabled only
|
||||||
|
if (language === 'enabled') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If nothing to change, bail
|
// If nothing to change, bail
|
||||||
if (this.language === language) {
|
if (this.language === language) {
|
||||||
return;
|
return;
|
||||||
@ -894,116 +963,32 @@ class Plyr {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle the player controls
|
* Toggle the player controls
|
||||||
* @param {boolean} toggle - Whether to show the controls
|
* @param {boolean} [toggle] - Whether to show the controls
|
||||||
*/
|
*/
|
||||||
toggleControls(toggle) {
|
toggleControls(toggle) {
|
||||||
// We need controls of course...
|
// Don't toggle if missing UI support or if it's audio
|
||||||
if (!utils.is.element(this.elements.controls)) {
|
if (this.supported.ui && !this.isAudio) {
|
||||||
return;
|
// Get state before change
|
||||||
}
|
const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
|
||||||
|
|
||||||
// Don't hide if no UI support or it's audio
|
// Negate the argument if not undefined since adding the class to hides the controls
|
||||||
if (!this.supported.ui || this.isAudio) {
|
const force = typeof toggle === 'undefined' ? undefined : !toggle;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let delay = 0;
|
// Apply and get updated state
|
||||||
let show = toggle;
|
const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
|
||||||
let isEnterFullscreen = false;
|
|
||||||
|
|
||||||
// Get toggle state if not set
|
// Close menu
|
||||||
if (!utils.is.boolean(toggle)) {
|
if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
||||||
if (utils.is.event(toggle)) {
|
|
||||||
// Is the enter fullscreen event
|
|
||||||
isEnterFullscreen = toggle.type === 'enterfullscreen';
|
|
||||||
|
|
||||||
// Whether to show controls
|
|
||||||
show = [
|
|
||||||
'mouseenter',
|
|
||||||
'mousemove',
|
|
||||||
'touchstart',
|
|
||||||
'touchmove',
|
|
||||||
'focusin',
|
|
||||||
].includes(toggle.type);
|
|
||||||
|
|
||||||
// Delay hiding on move events
|
|
||||||
if ([
|
|
||||||
'mousemove',
|
|
||||||
'touchmove',
|
|
||||||
'touchend',
|
|
||||||
].includes(toggle.type)) {
|
|
||||||
delay = 2000;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delay a little more for keyboard users
|
|
||||||
if (toggle.type === 'focusin') {
|
|
||||||
delay = 3000;
|
|
||||||
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
show = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear timer on every call
|
|
||||||
window.clearTimeout(this.timers.controls);
|
|
||||||
|
|
||||||
// If the mouse is not over the controls, set a timeout to hide them
|
|
||||||
if (show || this.paused || this.loading) {
|
|
||||||
// Check if controls toggled
|
|
||||||
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false);
|
|
||||||
|
|
||||||
// Trigger event
|
|
||||||
if (toggled) {
|
|
||||||
utils.dispatchEvent.call(this, this.media, 'controlsshown');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always show controls when paused or if touch
|
|
||||||
if (this.paused || this.loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delay for hiding on touch
|
|
||||||
if (support.touch) {
|
|
||||||
delay = 3000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If toggle is false or if we're playing (regardless of toggle),
|
|
||||||
// then set the timer to hide the controls
|
|
||||||
if (!show || this.playing) {
|
|
||||||
this.timers.controls = window.setTimeout(() => {
|
|
||||||
/* this.debug.warn({
|
|
||||||
pressed: this.elements.controls.pressed,
|
|
||||||
hover: this.elements.controls.pressed,
|
|
||||||
playing: this.playing,
|
|
||||||
paused: this.paused,
|
|
||||||
loading: this.loading,
|
|
||||||
}); */
|
|
||||||
|
|
||||||
// If the mouse is over the controls (and not entering fullscreen), bail
|
|
||||||
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore transition behaviour
|
|
||||||
if (!utils.hasClass(this.elements.container, this.config.classNames.hideControls)) {
|
|
||||||
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if controls toggled
|
|
||||||
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, true);
|
|
||||||
|
|
||||||
// Trigger event and close menu
|
|
||||||
if (toggled) {
|
|
||||||
utils.dispatchEvent.call(this, this.media, 'controlshidden');
|
|
||||||
|
|
||||||
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
|
||||||
controls.toggleMenu.call(this, false);
|
controls.toggleMenu.call(this, false);
|
||||||
}
|
}
|
||||||
|
// Trigger event on change
|
||||||
|
if (hiding !== isHidden) {
|
||||||
|
const eventName = hiding ? 'controlshidden' : 'controlsshown';
|
||||||
|
utils.dispatchEvent.call(this, this.media, eventName);
|
||||||
}
|
}
|
||||||
}, delay);
|
return !hiding;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1032,6 +1017,10 @@ class Plyr {
|
|||||||
* @param {boolean} soft - Whether it's a soft destroy (for source changes etc)
|
* @param {boolean} soft - Whether it's a soft destroy (for source changes etc)
|
||||||
*/
|
*/
|
||||||
destroy(callback, soft = false) {
|
destroy(callback, soft = false) {
|
||||||
|
if (!this.ready) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const done = () => {
|
const done = () => {
|
||||||
// Reset overflow (incase destroyed while in fullscreen)
|
// Reset overflow (incase destroyed while in fullscreen)
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
@ -1060,6 +1049,9 @@ class Plyr {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Unbind listeners
|
||||||
|
this.listeners.clear();
|
||||||
|
|
||||||
// Replace the container with the original element provided
|
// Replace the container with the original element provided
|
||||||
utils.replaceElement(this.elements.original, this.elements.container);
|
utils.replaceElement(this.elements.original, this.elements.container);
|
||||||
|
|
||||||
@ -1071,15 +1063,27 @@ class Plyr {
|
|||||||
callback.call(this.elements.original);
|
callback.call(this.elements.original);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear for GC
|
// Reset state
|
||||||
|
this.ready = false;
|
||||||
|
|
||||||
|
// Clear for garbage collection
|
||||||
|
setTimeout(() => {
|
||||||
this.elements = null;
|
this.elements = null;
|
||||||
|
this.media = null;
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Stop playback
|
||||||
|
this.stop();
|
||||||
|
|
||||||
// Type specific stuff
|
// Type specific stuff
|
||||||
switch (`${this.provider}:${this.type}`) {
|
switch (`${this.provider}:${this.type}`) {
|
||||||
case 'html5:video':
|
case 'html5:video':
|
||||||
case 'html5:audio':
|
case 'html5:audio':
|
||||||
|
// Clear timeout
|
||||||
|
clearTimeout(this.timers.loading);
|
||||||
|
|
||||||
// Restore native video controls
|
// Restore native video controls
|
||||||
ui.toggleNativeControls.call(this, true);
|
ui.toggleNativeControls.call(this, true);
|
||||||
|
|
||||||
@ -1090,11 +1094,11 @@ class Plyr {
|
|||||||
|
|
||||||
case 'youtube:video':
|
case 'youtube:video':
|
||||||
// Clear timers
|
// Clear timers
|
||||||
window.clearInterval(this.timers.buffering);
|
clearInterval(this.timers.buffering);
|
||||||
window.clearInterval(this.timers.playing);
|
clearInterval(this.timers.playing);
|
||||||
|
|
||||||
// Destroy YouTube API
|
// Destroy YouTube API
|
||||||
if (this.embed !== null) {
|
if (this.embed !== null && utils.is.function(this.embed.destroy)) {
|
||||||
this.embed.destroy();
|
this.embed.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1111,7 +1115,7 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vimeo does not always return
|
// Vimeo does not always return
|
||||||
window.setTimeout(done, 200);
|
setTimeout(done, 200);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -1146,6 +1150,31 @@ class Plyr {
|
|||||||
static loadSprite(url, id) {
|
static loadSprite(url, id) {
|
||||||
return utils.loadSprite(url, id);
|
return utils.loadSprite(url, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup multiple instances
|
||||||
|
* @param {*} selector
|
||||||
|
* @param {object} options
|
||||||
|
*/
|
||||||
|
static setup(selector, options = {}) {
|
||||||
|
let targets = null;
|
||||||
|
|
||||||
|
if (utils.is.string(selector)) {
|
||||||
|
targets = Array.from(document.querySelectorAll(selector));
|
||||||
|
} else if (utils.is.nodeList(selector)) {
|
||||||
|
targets = Array.from(selector);
|
||||||
|
} else if (utils.is.array(selector)) {
|
||||||
|
targets = selector.filter(i => utils.is.element(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utils.is.empty(targets)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets.map(t => new Plyr(t, options));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Plyr.defaults = utils.cloneDeep(defaults);
|
||||||
|
|
||||||
export default Plyr;
|
export default Plyr;
|
||||||
|
13
src/js/plyr.polyfilled.js
Normal file
13
src/js/plyr.polyfilled.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Plyr Polyfilled Build
|
||||||
|
// plyr.js v3.3.9
|
||||||
|
// https://github.com/sampotts/plyr
|
||||||
|
// License: The MIT License (MIT)
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import 'babel-polyfill';
|
||||||
|
import 'custom-event-polyfill';
|
||||||
|
import 'url-polyfill';
|
||||||
|
import Plyr from './plyr';
|
||||||
|
|
||||||
|
export default Plyr;
|
@ -2,11 +2,12 @@
|
|||||||
// Plyr source update
|
// Plyr source update
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import { providers } from './types';
|
import html5 from './html5';
|
||||||
import utils from './utils';
|
|
||||||
import media from './media';
|
import media from './media';
|
||||||
import ui from './ui';
|
|
||||||
import support from './support';
|
import support from './support';
|
||||||
|
import { providers } from './types';
|
||||||
|
import ui from './ui';
|
||||||
|
import utils from './utils';
|
||||||
|
|
||||||
const source = {
|
const source = {
|
||||||
// Add elements to HTML5 media (source, tracks, etc)
|
// Add elements to HTML5 media (source, tracks, etc)
|
||||||
@ -31,13 +32,14 @@ const source = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cancel current network requests
|
// Cancel current network requests
|
||||||
media.cancelRequests.call(this);
|
html5.cancelRequests.call(this);
|
||||||
|
|
||||||
// Destroy instance and re-setup
|
// Destroy instance and re-setup
|
||||||
this.destroy.call(
|
this.destroy.call(
|
||||||
this,
|
this,
|
||||||
() => {
|
() => {
|
||||||
// TODO: Reset menus here
|
// Reset quality options
|
||||||
|
this.options.quality = [];
|
||||||
|
|
||||||
// Remove elements
|
// Remove elements
|
||||||
utils.removeElement(this.media);
|
utils.removeElement(this.media);
|
||||||
@ -53,7 +55,7 @@ const source = {
|
|||||||
this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5;
|
this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5;
|
||||||
|
|
||||||
// Check for support
|
// Check for support
|
||||||
this.supported = support.check(this.type, this.provider, this.config.inline);
|
this.supported = support.check(this.type, this.provider, this.config.playsinline);
|
||||||
|
|
||||||
// Create new markup
|
// Create new markup
|
||||||
switch (`${this.provider}:${this.type}`) {
|
switch (`${this.provider}:${this.type}`) {
|
||||||
@ -92,8 +94,8 @@ const source = {
|
|||||||
if (this.config.autoplay) {
|
if (this.config.autoplay) {
|
||||||
this.media.setAttribute('autoplay', '');
|
this.media.setAttribute('autoplay', '');
|
||||||
}
|
}
|
||||||
if ('poster' in input) {
|
if (!utils.is.empty(input.poster)) {
|
||||||
this.media.setAttribute('poster', input.poster);
|
this.poster = input.poster;
|
||||||
}
|
}
|
||||||
if (this.config.loop.active) {
|
if (this.config.loop.active) {
|
||||||
this.media.setAttribute('loop', '');
|
this.media.setAttribute('loop', '');
|
||||||
@ -101,7 +103,7 @@ const source = {
|
|||||||
if (this.config.muted) {
|
if (this.config.muted) {
|
||||||
this.media.setAttribute('muted', '');
|
this.media.setAttribute('muted', '');
|
||||||
}
|
}
|
||||||
if (this.config.inline) {
|
if (this.config.playsinline) {
|
||||||
this.media.setAttribute('playsinline', '');
|
this.media.setAttribute('playsinline', '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ class Storage {
|
|||||||
|
|
||||||
// Check for actual support (see if we can use it)
|
// Check for actual support (see if we can use it)
|
||||||
static get supported() {
|
static get supported() {
|
||||||
|
try {
|
||||||
if (!('localStorage' in window)) {
|
if (!('localStorage' in window)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -20,9 +21,9 @@ class Storage {
|
|||||||
|
|
||||||
// Try to use it (it might be disabled, e.g. user is in private mode)
|
// Try to use it (it might be disabled, e.g. user is in private mode)
|
||||||
// see: https://github.com/sampotts/plyr/issues/131
|
// see: https://github.com/sampotts/plyr/issues/131
|
||||||
try {
|
|
||||||
window.localStorage.setItem(test, test);
|
window.localStorage.setItem(test, test);
|
||||||
window.localStorage.removeItem(test);
|
window.localStorage.removeItem(test);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
@ -30,9 +31,13 @@ class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(key) {
|
get(key) {
|
||||||
|
if (!Storage.supported || !this.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const store = window.localStorage.getItem(this.key);
|
const store = window.localStorage.getItem(this.key);
|
||||||
|
|
||||||
if (!Storage.supported || utils.is.empty(store)) {
|
if (utils.is.empty(store)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,16 +12,16 @@ const support = {
|
|||||||
|
|
||||||
// Check for support
|
// Check for support
|
||||||
// Basic functionality vs full UI
|
// Basic functionality vs full UI
|
||||||
check(type, provider, inline) {
|
check(type, provider, playsinline) {
|
||||||
let api = false;
|
let api = false;
|
||||||
let ui = false;
|
let ui = false;
|
||||||
const browser = utils.getBrowser();
|
const browser = utils.getBrowser();
|
||||||
const playsInline = browser.isIPhone && inline && support.inline;
|
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
|
||||||
|
|
||||||
switch (`${provider}:${type}`) {
|
switch (`${provider}:${type}`) {
|
||||||
case 'html5:video':
|
case 'html5:video':
|
||||||
api = support.video;
|
api = support.video;
|
||||||
ui = api && support.rangeInput && (!browser.isIPhone || playsInline);
|
ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'html5:audio':
|
case 'html5:audio':
|
||||||
@ -30,13 +30,9 @@ const support = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'youtube:video':
|
case 'youtube:video':
|
||||||
api = true;
|
|
||||||
ui = support.rangeInput && (!browser.isIPhone || playsInline);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'vimeo:video':
|
case 'vimeo:video':
|
||||||
api = true;
|
api = true;
|
||||||
ui = support.rangeInput && !browser.isIPhone;
|
ui = support.rangeInput && (!browser.isIPhone || canPlayInline);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -63,7 +59,7 @@ const support = {
|
|||||||
|
|
||||||
// Inline playback support
|
// Inline playback support
|
||||||
// https://webkit.org/blog/6784/new-video-policies-for-ios/
|
// https://webkit.org/blog/6784/new-video-policies-for-ios/
|
||||||
inline: 'playsInline' in document.createElement('video'),
|
playsinline: 'playsInline' in document.createElement('video'),
|
||||||
|
|
||||||
// Check for mime type support against a player instance
|
// Check for mime type support against a player instance
|
||||||
// Credits: http://diveintohtml5.info/everything.html
|
// Credits: http://diveintohtml5.info/everything.html
|
||||||
@ -77,6 +73,11 @@ const support = {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check directly if codecs specified
|
||||||
|
if (type.includes('codecs=')) {
|
||||||
|
return media.canPlayType(type).replace(/no/, '');
|
||||||
|
}
|
||||||
|
|
||||||
// Type specific checks
|
// Type specific checks
|
||||||
if (this.isVideo) {
|
if (this.isVideo) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -132,6 +133,7 @@ const support = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
window.addEventListener('test', null, options);
|
window.addEventListener('test', null, options);
|
||||||
|
window.removeEventListener('test', null, options);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
@ -147,7 +149,7 @@ const support = {
|
|||||||
})(),
|
})(),
|
||||||
|
|
||||||
// Touch
|
// Touch
|
||||||
// Remember a device can be moust + touch enabled
|
// NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
|
||||||
touch: 'ontouchstart' in document.documentElement,
|
touch: 'ontouchstart' in document.documentElement,
|
||||||
|
|
||||||
// Detect transitions support
|
// Detect transitions support
|
||||||
|
284
src/js/ui.js
284
src/js/ui.js
@ -2,10 +2,14 @@
|
|||||||
// Plyr UI
|
// Plyr UI
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import utils from './utils';
|
|
||||||
import captions from './captions';
|
import captions from './captions';
|
||||||
import controls from './controls';
|
import controls from './controls';
|
||||||
import listeners from './listeners';
|
import i18n from './i18n';
|
||||||
|
import support from './support';
|
||||||
|
import utils from './utils';
|
||||||
|
|
||||||
|
// Sniff out the browser
|
||||||
|
const browser = utils.getBrowser();
|
||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
addStyleHook() {
|
addStyleHook() {
|
||||||
@ -25,8 +29,8 @@ const ui = {
|
|||||||
// Setup the UI
|
// Setup the UI
|
||||||
build() {
|
build() {
|
||||||
// Re-attach media element listeners
|
// Re-attach media element listeners
|
||||||
// TODO: Use event bubbling
|
// TODO: Use event bubbling?
|
||||||
listeners.media.call(this);
|
this.listeners.media();
|
||||||
|
|
||||||
// Don't setup interface if no support
|
// Don't setup interface if no support
|
||||||
if (!this.supported.ui) {
|
if (!this.supported.ui) {
|
||||||
@ -45,12 +49,7 @@ const ui = {
|
|||||||
controls.inject.call(this);
|
controls.inject.call(this);
|
||||||
|
|
||||||
// Re-attach control listeners
|
// Re-attach control listeners
|
||||||
listeners.controls.call(this);
|
this.listeners.controls();
|
||||||
}
|
|
||||||
|
|
||||||
// If there's no controls, bail
|
|
||||||
if (!utils.is.element(this.elements.controls)) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove native controls
|
// Remove native controls
|
||||||
@ -71,29 +70,51 @@ const ui = {
|
|||||||
// Reset loop state
|
// Reset loop state
|
||||||
this.loop = null;
|
this.loop = null;
|
||||||
|
|
||||||
// Reset quality options
|
// Reset quality setting
|
||||||
this.options.quality = [];
|
this.quality = null;
|
||||||
|
|
||||||
|
// Reset volume display
|
||||||
|
controls.updateVolume.call(this);
|
||||||
|
|
||||||
// Reset time display
|
// Reset time display
|
||||||
ui.timeUpdate.call(this);
|
controls.timeUpdate.call(this);
|
||||||
|
|
||||||
// Update the UI
|
// Update the UI
|
||||||
ui.checkPlaying.call(this);
|
ui.checkPlaying.call(this);
|
||||||
|
|
||||||
|
// Check for picture-in-picture support
|
||||||
|
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
|
||||||
|
|
||||||
|
// Check for airplay support
|
||||||
|
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
||||||
|
|
||||||
|
// Add iOS class
|
||||||
|
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
||||||
|
|
||||||
|
// Add touch class
|
||||||
|
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
||||||
|
|
||||||
// Ready for API calls
|
// Ready for API calls
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
|
|
||||||
// Ready event at end of execution stack
|
// Ready event at end of execution stack
|
||||||
|
setTimeout(() => {
|
||||||
utils.dispatchEvent.call(this, this.media, 'ready');
|
utils.dispatchEvent.call(this, this.media, 'ready');
|
||||||
|
}, 0);
|
||||||
|
|
||||||
// Set the title
|
// Set the title
|
||||||
ui.setTitle.call(this);
|
ui.setTitle.call(this);
|
||||||
|
|
||||||
|
// Assure the poster image is set, if the property was added before the element was created
|
||||||
|
if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) {
|
||||||
|
ui.setPoster.call(this, this.poster);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Setup aria attribute for play and iframe title
|
// Setup aria attribute for play and iframe title
|
||||||
setTitle() {
|
setTitle() {
|
||||||
// Find the current text
|
// Find the current text
|
||||||
let label = this.config.i18n.play;
|
let label = i18n.get('play', this.config);
|
||||||
|
|
||||||
// If there's a media title set, use that for the label
|
// If there's a media title set, use that for the label
|
||||||
if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) {
|
if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) {
|
||||||
@ -121,24 +142,64 @@ const ui = {
|
|||||||
|
|
||||||
// Default to media type
|
// Default to media type
|
||||||
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
|
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
|
||||||
|
const format = i18n.get('frameTitle', this.config);
|
||||||
|
|
||||||
iframe.setAttribute('title', this.config.i18n.frameTitle.replace('{title}', title));
|
iframe.setAttribute('title', format.replace('{title}', title));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Toggle poster
|
||||||
|
togglePoster(enable) {
|
||||||
|
utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Set the poster image (async)
|
||||||
|
setPoster(poster) {
|
||||||
|
// Set property regardless of validity
|
||||||
|
this.media.setAttribute('poster', poster);
|
||||||
|
|
||||||
|
// Bail if element is missing
|
||||||
|
if (!utils.is.element(this.elements.poster)) {
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the image, and set poster if successful
|
||||||
|
const loadPromise = utils.loadImage(poster)
|
||||||
|
.then(() => {
|
||||||
|
this.elements.poster.style.backgroundImage = `url('${poster}')`;
|
||||||
|
Object.assign(this.elements.poster.style, {
|
||||||
|
backgroundImage: `url('${poster}')`,
|
||||||
|
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
|
||||||
|
backgroundSize: '',
|
||||||
|
});
|
||||||
|
ui.togglePoster.call(this, true);
|
||||||
|
return poster;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video)
|
||||||
|
loadPromise.catch(() => ui.togglePoster.call(this, false));
|
||||||
|
|
||||||
|
// Return the promise so the caller can use it as well
|
||||||
|
return loadPromise;
|
||||||
|
},
|
||||||
|
|
||||||
// Check playing state
|
// Check playing state
|
||||||
checkPlaying() {
|
checkPlaying(event) {
|
||||||
// Class hooks
|
// Class hooks
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
|
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused);
|
utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
|
||||||
|
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
|
||||||
|
|
||||||
// Set aria state
|
// Set ARIA state
|
||||||
if (utils.is.nodeList(this.elements.buttons.play)) {
|
utils.toggleState(this.elements.buttons.play, this.playing);
|
||||||
Array.from(this.elements.buttons.play).forEach(button => utils.toggleState(button, this.playing));
|
|
||||||
|
// Only update controls on non timeupdate events
|
||||||
|
if (utils.is.event(event) && event.type === 'timeupdate') {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle controls
|
// Toggle controls
|
||||||
this.toggleControls(!this.playing);
|
ui.toggleControls.call(this);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check if media is loading
|
// Check if media is loading
|
||||||
@ -153,185 +214,22 @@ const ui = {
|
|||||||
|
|
||||||
// Timer to prevent flicker when seeking
|
// Timer to prevent flicker when seeking
|
||||||
this.timers.loading = setTimeout(() => {
|
this.timers.loading = setTimeout(() => {
|
||||||
// Toggle container class hook
|
// Update progress bar loading class state
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||||
|
|
||||||
// Show controls if loading, hide if done
|
// Update controls visibility
|
||||||
this.toggleControls(this.loading);
|
ui.toggleControls.call(this);
|
||||||
}, this.loading ? 250 : 0);
|
}, this.loading ? 250 : 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check if media failed to load
|
// Toggle controls based on state and `force` argument
|
||||||
checkFailed() {
|
toggleControls(force) {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState
|
const { controls } = this.elements;
|
||||||
this.failed = this.media.networkState === 3;
|
|
||||||
|
|
||||||
if (this.failed) {
|
if (controls && this.config.hideControls) {
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, false);
|
// Show controls if force, loading, paused, or button interaction, otherwise hide
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.error, true);
|
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear timer
|
|
||||||
clearTimeout(this.timers.failed);
|
|
||||||
|
|
||||||
// Timer to prevent flicker when seeking
|
|
||||||
this.timers.loading = setTimeout(() => {
|
|
||||||
// Toggle container class hook
|
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
|
||||||
|
|
||||||
// Show controls if loading, hide if done
|
|
||||||
this.toggleControls(this.loading);
|
|
||||||
}, this.loading ? 250 : 0);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update volume UI and storage
|
|
||||||
updateVolume() {
|
|
||||||
if (!this.supported.ui) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update range
|
|
||||||
if (utils.is.element(this.elements.inputs.volume)) {
|
|
||||||
ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update mute state
|
|
||||||
if (utils.is.element(this.elements.buttons.mute)) {
|
|
||||||
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update seek value and lower fill
|
|
||||||
setRange(target, value = 0) {
|
|
||||||
if (!utils.is.element(target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
target.value = value;
|
|
||||||
|
|
||||||
// Webkit range fill
|
|
||||||
controls.updateRangeFill.call(this, target);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Set <progress> value
|
|
||||||
setProgress(target, input) {
|
|
||||||
const value = utils.is.number(input) ? input : 0;
|
|
||||||
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
|
|
||||||
|
|
||||||
// Update value and label
|
|
||||||
if (utils.is.element(progress)) {
|
|
||||||
progress.value = value;
|
|
||||||
|
|
||||||
// Update text label inside
|
|
||||||
const label = progress.getElementsByTagName('span')[0];
|
|
||||||
if (utils.is.element(label)) {
|
|
||||||
label.childNodes[0].nodeValue = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update <progress> elements
|
|
||||||
updateProgress(event) {
|
|
||||||
if (!this.supported.ui || !utils.is.event(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = 0;
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
switch (event.type) {
|
|
||||||
// Video playing
|
|
||||||
case 'timeupdate':
|
|
||||||
case 'seeking':
|
|
||||||
value = utils.getPercentage(this.currentTime, this.duration);
|
|
||||||
|
|
||||||
// Set seek range value only if it's a 'natural' time event
|
|
||||||
if (event.type === 'timeupdate') {
|
|
||||||
ui.setRange.call(this, this.elements.inputs.seek, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Check buffer status
|
|
||||||
case 'playing':
|
|
||||||
case 'progress':
|
|
||||||
value = (() => {
|
|
||||||
const { buffered } = this.media;
|
|
||||||
|
|
||||||
if (buffered && buffered.length) {
|
|
||||||
// HTML5
|
|
||||||
return utils.getPercentage(buffered.end(0), this.duration);
|
|
||||||
} else if (utils.is.number(buffered)) {
|
|
||||||
// YouTube returns between 0 and 1
|
|
||||||
return buffered * 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
})();
|
|
||||||
|
|
||||||
ui.setProgress.call(this, this.elements.display.buffer, value);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update the displayed time
|
|
||||||
updateTimeDisplay(target = null, time = 0, inverted = false) {
|
|
||||||
// Bail if there's no element to display or the value isn't a number
|
|
||||||
if (!utils.is.element(target) || !utils.is.number(time)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always display hours if duration is over an hour
|
|
||||||
const displayHours = utils.getHours(this.duration) > 0;
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
target.textContent = utils.formatTime(time, displayHours, inverted);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Handle time change event
|
|
||||||
timeUpdate(event) {
|
|
||||||
// Only invert if only one time element is displayed and used for both duration and currentTime
|
|
||||||
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
|
|
||||||
|
|
||||||
// Duration
|
|
||||||
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
|
|
||||||
|
|
||||||
// Ignore updates while seeking
|
|
||||||
if (event && event.type === 'timeupdate' && this.media.seeking) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playing progress
|
|
||||||
ui.updateProgress.call(this, event);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Show the duration on metadataloaded
|
|
||||||
durationUpdate() {
|
|
||||||
if (!this.supported.ui) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's a spot to display duration
|
|
||||||
const hasDuration = utils.is.element(this.elements.display.duration);
|
|
||||||
|
|
||||||
// If there's only one time display, display duration there
|
|
||||||
if (!hasDuration && this.config.displayDuration && this.paused) {
|
|
||||||
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's a duration element, update content
|
|
||||||
if (hasDuration) {
|
|
||||||
ui.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the tooltip (if visible)
|
|
||||||
controls.updateSeekTooltip.call(this);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
348
src/js/utils.js
348
src/js/utils.js
@ -2,15 +2,14 @@
|
|||||||
// Plyr utils
|
// Plyr utils
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
|
import loadjs from 'loadjs';
|
||||||
|
import Storage from './storage';
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import { providers } from './types';
|
import { providers } from './types';
|
||||||
|
|
||||||
const utils = {
|
const utils = {
|
||||||
// Check variable types
|
// Check variable types
|
||||||
is: {
|
is: {
|
||||||
plyr(input) {
|
|
||||||
return this.instanceof(input, window.Plyr);
|
|
||||||
},
|
|
||||||
object(input) {
|
object(input) {
|
||||||
return this.getConstructor(input) === Object;
|
return this.getConstructor(input) === Object;
|
||||||
},
|
},
|
||||||
@ -30,19 +29,19 @@ const utils = {
|
|||||||
return !this.nullOrUndefined(input) && Array.isArray(input);
|
return !this.nullOrUndefined(input) && Array.isArray(input);
|
||||||
},
|
},
|
||||||
weakMap(input) {
|
weakMap(input) {
|
||||||
return this.instanceof(input, window.WeakMap);
|
return this.instanceof(input, WeakMap);
|
||||||
},
|
},
|
||||||
nodeList(input) {
|
nodeList(input) {
|
||||||
return this.instanceof(input, window.NodeList);
|
return this.instanceof(input, NodeList);
|
||||||
},
|
},
|
||||||
element(input) {
|
element(input) {
|
||||||
return this.instanceof(input, window.Element);
|
return this.instanceof(input, Element);
|
||||||
},
|
},
|
||||||
textNode(input) {
|
textNode(input) {
|
||||||
return this.getConstructor(input) === Text;
|
return this.getConstructor(input) === Text;
|
||||||
},
|
},
|
||||||
event(input) {
|
event(input) {
|
||||||
return this.instanceof(input, window.Event);
|
return this.instanceof(input, Event);
|
||||||
},
|
},
|
||||||
cue(input) {
|
cue(input) {
|
||||||
return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue);
|
return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue);
|
||||||
@ -83,7 +82,7 @@ const utils = {
|
|||||||
|
|
||||||
// Fetch wrapper
|
// Fetch wrapper
|
||||||
// Using XHR to avoid issues with older browsers
|
// Using XHR to avoid issues with older browsers
|
||||||
fetch(url) {
|
fetch(url, responseType = 'text') {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const request = new XMLHttpRequest();
|
const request = new XMLHttpRequest();
|
||||||
@ -94,11 +93,15 @@ const utils = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
request.addEventListener('load', () => {
|
request.addEventListener('load', () => {
|
||||||
|
if (responseType === 'text') {
|
||||||
try {
|
try {
|
||||||
resolve(JSON.parse(request.responseText));
|
resolve(JSON.parse(request.responseText));
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
resolve(request.responseText);
|
resolve(request.responseText);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
resolve(request.response);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
request.addEventListener('error', () => {
|
request.addEventListener('error', () => {
|
||||||
@ -106,6 +109,10 @@ const utils = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
request.open('GET', url, true);
|
request.open('GET', url, true);
|
||||||
|
|
||||||
|
// Set the required response type
|
||||||
|
request.responseType = responseType;
|
||||||
|
|
||||||
request.send();
|
request.send();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
@ -113,56 +120,29 @@ const utils = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Load image avoiding xhr/fetch CORS issues
|
||||||
|
// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded.
|
||||||
|
// By default it checks if it is at least 1px, but you can add a second argument to change this.
|
||||||
|
loadImage(src, minWidth = 1) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const image = new Image();
|
||||||
|
const handler = () => {
|
||||||
|
delete image.onload;
|
||||||
|
delete image.onerror;
|
||||||
|
(image.naturalWidth >= minWidth ? resolve : reject)(image);
|
||||||
|
};
|
||||||
|
Object.assign(image, {onload: handler, onerror: handler, src});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Load an external script
|
// Load an external script
|
||||||
loadScript(url, callback, error) {
|
loadScript(url) {
|
||||||
const current = document.querySelector(`script[src="${url}"]`);
|
return new Promise((resolve, reject) => {
|
||||||
|
loadjs(url, {
|
||||||
// Check script is not already referenced, if so wait for load
|
success: resolve,
|
||||||
if (current !== null) {
|
error: reject,
|
||||||
current.callbacks = current.callbacks || [];
|
});
|
||||||
current.callbacks.push(callback);
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the element
|
|
||||||
const element = document.createElement('script');
|
|
||||||
|
|
||||||
// Callback queue
|
|
||||||
element.callbacks = element.callbacks || [];
|
|
||||||
element.callbacks.push(callback);
|
|
||||||
|
|
||||||
// Error queue
|
|
||||||
element.errors = element.errors || [];
|
|
||||||
element.errors.push(error);
|
|
||||||
|
|
||||||
// Bind callback
|
|
||||||
if (utils.is.function(callback)) {
|
|
||||||
element.addEventListener(
|
|
||||||
'load',
|
|
||||||
event => {
|
|
||||||
element.callbacks.forEach(cb => cb.call(null, event));
|
|
||||||
element.callbacks = null;
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind error handling
|
|
||||||
element.addEventListener(
|
|
||||||
'error',
|
|
||||||
event => {
|
|
||||||
element.errors.forEach(err => err.call(null, event));
|
|
||||||
element.errors = null;
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set the URL after binding callback
|
|
||||||
element.src = url;
|
|
||||||
|
|
||||||
// Inject
|
|
||||||
const first = document.getElementsByTagName('script')[0];
|
|
||||||
first.parentNode.insertBefore(element, first);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Load an external SVG sprite
|
// Load an external SVG sprite
|
||||||
@ -175,7 +155,14 @@ const utils = {
|
|||||||
const hasId = utils.is.string(id);
|
const hasId = utils.is.string(id);
|
||||||
let isCached = false;
|
let isCached = false;
|
||||||
|
|
||||||
function updateSprite(data) {
|
const exists = () => document.querySelectorAll(`#${id}`).length;
|
||||||
|
|
||||||
|
function injectSprite(data) {
|
||||||
|
// Check again incase of race condition
|
||||||
|
if (hasId && exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Inject content
|
// Inject content
|
||||||
this.innerHTML = data;
|
this.innerHTML = data;
|
||||||
|
|
||||||
@ -183,8 +170,10 @@ const utils = {
|
|||||||
document.body.insertBefore(this, document.body.childNodes[0]);
|
document.body.insertBefore(this, document.body.childNodes[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only load once
|
// Only load once if ID set
|
||||||
if (!hasId || !document.querySelectorAll(`#${id}`).length) {
|
if (!hasId || !exists()) {
|
||||||
|
const useStorage = Storage.supported;
|
||||||
|
|
||||||
// Create container
|
// Create container
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
utils.toggleHidden(container, true);
|
utils.toggleHidden(container, true);
|
||||||
@ -194,13 +183,13 @@ const utils = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check in cache
|
// Check in cache
|
||||||
if (support.storage) {
|
if (useStorage) {
|
||||||
const cached = window.localStorage.getItem(prefix + id);
|
const cached = window.localStorage.getItem(prefix + id);
|
||||||
isCached = cached !== null;
|
isCached = cached !== null;
|
||||||
|
|
||||||
if (isCached) {
|
if (isCached) {
|
||||||
const data = JSON.parse(cached);
|
const data = JSON.parse(cached);
|
||||||
updateSprite.call(container, data.content);
|
injectSprite.call(container, data.content);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -213,7 +202,7 @@ const utils = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (support.storage) {
|
if (useStorage) {
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
prefix + id,
|
prefix + id,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@ -222,7 +211,7 @@ const utils = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSprite.call(container, result);
|
injectSprite.call(container, result);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
@ -233,15 +222,6 @@ const utils = {
|
|||||||
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
|
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Determine if we're in an iframe
|
|
||||||
inFrame() {
|
|
||||||
try {
|
|
||||||
return window.self !== window.top;
|
|
||||||
} catch (e) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Wrap an element
|
// Wrap an element
|
||||||
wrap(elements, wrapper) {
|
wrap(elements, wrapper) {
|
||||||
// Convert `elements` to an array, if necessary.
|
// Convert `elements` to an array, if necessary.
|
||||||
@ -285,7 +265,7 @@ const utils = {
|
|||||||
|
|
||||||
// Add text node
|
// Add text node
|
||||||
if (utils.is.string(text)) {
|
if (utils.is.string(text)) {
|
||||||
element.textContent = text;
|
element.innerText = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return built element
|
// Return built element
|
||||||
@ -303,14 +283,14 @@ const utils = {
|
|||||||
parent.appendChild(utils.createElement(type, attributes, text));
|
parent.appendChild(utils.createElement(type, attributes, text));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Remove an element
|
// Remove element(s)
|
||||||
removeElement(element) {
|
removeElement(element) {
|
||||||
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
|
if (utils.is.nodeList(element) || utils.is.array(element)) {
|
||||||
|
Array.from(element).forEach(utils.removeElement);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (utils.is.nodeList(element) || utils.is.array(element)) {
|
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
|
||||||
Array.from(element).forEach(utils.removeElement);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,8 +324,11 @@ const utils = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(attributes).forEach(key => {
|
Object.entries(attributes).forEach(([
|
||||||
element.setAttribute(key, attributes[key]);
|
key,
|
||||||
|
value,
|
||||||
|
]) => {
|
||||||
|
element.setAttribute(key, value);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -406,14 +389,35 @@ const utils = {
|
|||||||
return attributes;
|
return attributes;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Toggle class on an element
|
// Toggle hidden
|
||||||
toggleClass(element, className, toggle) {
|
toggleHidden(element, hidden) {
|
||||||
|
if (!utils.is.element(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hide = hidden;
|
||||||
|
|
||||||
|
if (!utils.is.boolean(hide)) {
|
||||||
|
hide = !element.hasAttribute('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hide) {
|
||||||
|
element.setAttribute('hidden', '');
|
||||||
|
} else {
|
||||||
|
element.removeAttribute('hidden');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
|
||||||
|
toggleClass(element, className, force) {
|
||||||
if (utils.is.element(element)) {
|
if (utils.is.element(element)) {
|
||||||
const contains = element.classList.contains(className);
|
let method = 'toggle';
|
||||||
|
if (typeof force !== 'undefined') {
|
||||||
|
method = force ? 'add' : 'remove';
|
||||||
|
}
|
||||||
|
|
||||||
element.classList[toggle ? 'add' : 'remove'](className);
|
element.classList[method](className);
|
||||||
|
return element.classList.contains(className);
|
||||||
return (toggle && !contains) || (!toggle && contains);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -424,19 +428,6 @@ const utils = {
|
|||||||
return utils.is.element(element) && element.classList.contains(className);
|
return utils.is.element(element) && element.classList.contains(className);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Toggle hidden attribute on an element
|
|
||||||
toggleHidden(element, toggle) {
|
|
||||||
if (!utils.is.element(element)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toggle) {
|
|
||||||
element.setAttribute('hidden', '');
|
|
||||||
} else {
|
|
||||||
element.removeAttribute('hidden');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Element matches selector
|
// Element matches selector
|
||||||
matches(element, selector) {
|
matches(element, selector) {
|
||||||
const prototype = { Element };
|
const prototype = { Element };
|
||||||
@ -460,60 +451,6 @@ const utils = {
|
|||||||
return this.elements.container.querySelector(selector);
|
return this.elements.container.querySelector(selector);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Find the UI controls and store references in custom controls
|
|
||||||
// TODO: Allow settings menus with custom controls
|
|
||||||
findElements() {
|
|
||||||
try {
|
|
||||||
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
this.elements.buttons = {
|
|
||||||
play: utils.getElements.call(this, this.config.selectors.buttons.play),
|
|
||||||
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
|
|
||||||
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
|
|
||||||
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
|
|
||||||
forward: utils.getElement.call(this, this.config.selectors.buttons.forward),
|
|
||||||
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
|
|
||||||
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
|
|
||||||
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
|
|
||||||
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
|
|
||||||
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
|
|
||||||
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Progress
|
|
||||||
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
|
|
||||||
|
|
||||||
// Inputs
|
|
||||||
this.elements.inputs = {
|
|
||||||
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
|
|
||||||
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Display
|
|
||||||
this.elements.display = {
|
|
||||||
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
|
|
||||||
duration: utils.getElement.call(this, this.config.selectors.display.duration),
|
|
||||||
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Seek tooltip
|
|
||||||
if (utils.is.element(this.elements.progress)) {
|
|
||||||
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
// Log it
|
|
||||||
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
|
|
||||||
|
|
||||||
// Restore native video controls
|
|
||||||
this.toggleNativeControls(true);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get the focused element
|
// Get the focused element
|
||||||
getFocusElement() {
|
getFocusElement() {
|
||||||
let focused = document.activeElement;
|
let focused = document.activeElement;
|
||||||
@ -565,7 +502,7 @@ const utils = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Toggle event listener
|
// Toggle event listener
|
||||||
toggleListener(elements, event, callback, toggle, passive, capture) {
|
toggleListener(elements, event, callback, toggle = false, passive = true, capture = false) {
|
||||||
// Bail if no elemetns, event, or callback
|
// Bail if no elemetns, event, or callback
|
||||||
if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
|
if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
|
||||||
return;
|
return;
|
||||||
@ -587,16 +524,16 @@ const utils = {
|
|||||||
const events = event.split(' ');
|
const events = event.split(' ');
|
||||||
|
|
||||||
// Build options
|
// Build options
|
||||||
// Default to just capture boolean
|
// Default to just the capture boolean for browsers with no passive listener support
|
||||||
let options = utils.is.boolean(capture) ? capture : false;
|
let options = capture;
|
||||||
|
|
||||||
// If passive events listeners are supported
|
// If passive events listeners are supported
|
||||||
if (support.passiveListeners) {
|
if (support.passiveListeners) {
|
||||||
options = {
|
options = {
|
||||||
// Whether the listener can be passive (i.e. default never prevented)
|
// Whether the listener can be passive (i.e. default never prevented)
|
||||||
passive: utils.is.boolean(passive) ? passive : true,
|
passive,
|
||||||
// Whether the listener is a capturing listener or not
|
// Whether the listener is a capturing listener or not
|
||||||
capture: utils.is.boolean(capture) ? capture : false,
|
capture,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,27 +544,27 @@ const utils = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Bind event handler
|
// Bind event handler
|
||||||
on(element, events, callback, passive, capture) {
|
on(element, events = '', callback, passive = true, capture = false) {
|
||||||
utils.toggleListener(element, events, callback, true, passive, capture);
|
utils.toggleListener(element, events, callback, true, passive, capture);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Unbind event handler
|
// Unbind event handler
|
||||||
off(element, events, callback, passive, capture) {
|
off(element, events = '', callback, passive = true, capture = false) {
|
||||||
utils.toggleListener(element, events, callback, false, passive, capture);
|
utils.toggleListener(element, events, callback, false, passive, capture);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Trigger event
|
// Trigger event
|
||||||
dispatchEvent(element, type, bubbles, detail) {
|
dispatchEvent(element, type = '', bubbles = false, detail = {}) {
|
||||||
// Bail if no element
|
// Bail if no element
|
||||||
if (!element || !type) {
|
if (!utils.is.element(element) || utils.is.empty(type)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and dispatch the event
|
// Create and dispatch the event
|
||||||
const event = new CustomEvent(type, {
|
const event = new CustomEvent(type, {
|
||||||
bubbles: utils.is.boolean(bubbles) ? bubbles : false,
|
bubbles,
|
||||||
detail: Object.assign({}, detail, {
|
detail: Object.assign({}, detail, {
|
||||||
plyr: utils.is.plyr(this) ? this : null,
|
plyr: this,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -638,6 +575,12 @@ const utils = {
|
|||||||
// Toggle aria-pressed state on a toggle button
|
// Toggle aria-pressed state on a toggle button
|
||||||
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
|
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
|
||||||
toggleState(element, input) {
|
toggleState(element, input) {
|
||||||
|
// If multiple elements passed
|
||||||
|
if (utils.is.array(element) || utils.is.nodeList(element)) {
|
||||||
|
Array.from(element).forEach(target => utils.toggleState(target, input));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Bail if no target
|
// Bail if no target
|
||||||
if (!utils.is.element(element)) {
|
if (!utils.is.element(element)) {
|
||||||
return;
|
return;
|
||||||
@ -651,11 +594,21 @@ const utils = {
|
|||||||
element.setAttribute('aria-pressed', state);
|
element.setAttribute('aria-pressed', state);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Format string
|
||||||
|
format(input, ...args) {
|
||||||
|
if (utils.is.empty(input)) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : ''));
|
||||||
|
},
|
||||||
|
|
||||||
// Get percentage
|
// Get percentage
|
||||||
getPercentage(current, max) {
|
getPercentage(current, max) {
|
||||||
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
|
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (current / max * 100).toFixed(2);
|
return (current / max * 100).toFixed(2);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -696,6 +649,44 @@ const utils = {
|
|||||||
return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
|
return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Replace all occurances of a string in a string
|
||||||
|
replaceAll(input = '', find = '', replace = '') {
|
||||||
|
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
|
||||||
|
},
|
||||||
|
|
||||||
|
// Convert to title case
|
||||||
|
toTitleCase(input = '') {
|
||||||
|
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
|
||||||
|
},
|
||||||
|
|
||||||
|
// Convert string to pascalCase
|
||||||
|
toPascalCase(input = '') {
|
||||||
|
let string = input.toString();
|
||||||
|
|
||||||
|
// Convert kebab case
|
||||||
|
string = utils.replaceAll(string, '-', ' ');
|
||||||
|
|
||||||
|
// Convert snake case
|
||||||
|
string = utils.replaceAll(string, '_', ' ');
|
||||||
|
|
||||||
|
// Convert to title case
|
||||||
|
string = utils.toTitleCase(string);
|
||||||
|
|
||||||
|
// Convert to pascal case
|
||||||
|
return utils.replaceAll(string, ' ', '');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Convert string to pascalCase
|
||||||
|
toCamelCase(input = '') {
|
||||||
|
let string = input.toString();
|
||||||
|
|
||||||
|
// Convert to pascal case
|
||||||
|
string = utils.toPascalCase(string);
|
||||||
|
|
||||||
|
// Convert first character to lowercase
|
||||||
|
return string.charAt(0).toLowerCase() + string.slice(1);
|
||||||
|
},
|
||||||
|
|
||||||
// Deep extend destination object with N more objects
|
// Deep extend destination object with N more objects
|
||||||
extend(target = {}, ...sources) {
|
extend(target = {}, ...sources) {
|
||||||
if (!sources.length) {
|
if (!sources.length) {
|
||||||
@ -723,6 +714,29 @@ const utils = {
|
|||||||
return utils.extend(target, ...sources);
|
return utils.extend(target, ...sources);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Remove duplicates in an array
|
||||||
|
dedupe(array) {
|
||||||
|
if (!utils.is.array(array)) {
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array.filter((item, index) => array.indexOf(item) === index);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Clone nested objects
|
||||||
|
cloneDeep(object) {
|
||||||
|
return JSON.parse(JSON.stringify(object));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get the closest value in an array
|
||||||
|
closest(array, value) {
|
||||||
|
if (!utils.is.array(array) || !array.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
|
||||||
|
},
|
||||||
|
|
||||||
// Get the provider for a given URL
|
// Get the provider for a given URL
|
||||||
getProviderByUrl(url) {
|
getProviderByUrl(url) {
|
||||||
// YouTube
|
// YouTube
|
||||||
@ -731,7 +745,7 @@ const utils = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vimeo
|
// Vimeo
|
||||||
if (/^https?:\/\/player.vimeo.com\/video\/\d{8,}(?=\b|\/)/.test(url)) {
|
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
|
||||||
return providers.vimeo;
|
return providers.vimeo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -839,7 +853,7 @@ const utils = {
|
|||||||
|
|
||||||
// Force repaint of element
|
// Force repaint of element
|
||||||
repaint(element) {
|
repaint(element) {
|
||||||
window.setTimeout(() => {
|
setTimeout(() => {
|
||||||
utils.toggleHidden(element, true);
|
utils.toggleHidden(element, true);
|
||||||
element.offsetHeight; // eslint-disable-line
|
element.offsetHeight; // eslint-disable-line
|
||||||
utils.toggleHidden(element, false);
|
utils.toggleHidden(element, false);
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
font-family: $plyr-font-family;
|
font-family: $plyr-font-family;
|
||||||
|
font-variant-numeric: tabular-nums; // Force monosace-esque number widths
|
||||||
font-weight: $plyr-font-weight-regular;
|
font-weight: $plyr-font-weight-regular;
|
||||||
line-height: $plyr-line-height;
|
line-height: $plyr-line-height;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@ -25,6 +26,12 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
// Ignore focus
|
// Ignore focus
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
@ -3,14 +3,12 @@
|
|||||||
// YouTube, Vimeo, etc
|
// YouTube, Vimeo, etc
|
||||||
// --------------------------------------------------------------
|
// --------------------------------------------------------------
|
||||||
|
|
||||||
.plyr__video-embed {
|
// Default to 16:9 ratio but this is set by JavaScript based on config
|
||||||
// Default to 16:9 ratio but this is set by JavaScript based on config
|
$embed-padding: ((100 / 16) * 9);
|
||||||
$padding: ((100 / 16) * 9);
|
|
||||||
$height: 200;
|
|
||||||
$offset: to-percentage(($height - $padding) / ($height / 50));
|
|
||||||
|
|
||||||
|
.plyr__video-embed {
|
||||||
height: 0;
|
height: 0;
|
||||||
padding-bottom: to-percentage($padding);
|
padding-bottom: to-percentage($embed-padding);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
@ -22,15 +20,22 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Vimeo hack
|
// If the full custom UI is supported
|
||||||
> div {
|
.plyr--full-ui .plyr__video-embed {
|
||||||
|
$height: 240;
|
||||||
|
$offset: to-percentage(($height - $embed-padding) / ($height / 50));
|
||||||
|
|
||||||
|
// To allow mouse events to be captured if full support
|
||||||
|
iframe {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only used for Vimeo
|
||||||
|
> .plyr__video-embed__container {
|
||||||
padding-bottom: to-percentage($height);
|
padding-bottom: to-percentage($height);
|
||||||
position: relative;
|
position: relative;
|
||||||
transform: translateY(-$offset);
|
transform: translateY(-$offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// To allow mouse events to be captured if full support
|
|
||||||
.plyr--full-ui .plyr__video-embed iframe {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
right: -3px;
|
right: -3px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
z-index: 1;
|
z-index: 3;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -74,6 +74,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
color: $plyr-menu-color;
|
color: $plyr-menu-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
font-size: $plyr-font-size-menu;
|
||||||
padding: ceil($plyr-control-padding / 2) ($plyr-control-padding * 2);
|
padding: ceil($plyr-control-padding / 2) ($plyr-control-padding * 2);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -84,7 +85,6 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
transition: border-color 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--forward {
|
&--forward {
|
||||||
@ -108,7 +108,6 @@
|
|||||||
margin-bottom: floor($plyr-control-padding / 2);
|
margin-bottom: floor($plyr-control-padding / 2);
|
||||||
padding-left: ceil($plyr-control-padding * 4);
|
padding-left: ceil($plyr-control-padding * 4);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
width: calc(100% - #{$horizontal-padding});
|
width: calc(100% - #{$horizontal-padding});
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
23
src/sass/components/poster.scss
Normal file
23
src/sass/components/poster.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// --------------------------------------------------------------
|
||||||
|
// Faux poster overlay
|
||||||
|
// --------------------------------------------------------------
|
||||||
|
|
||||||
|
.plyr__poster {
|
||||||
|
background-color: #000;
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plyr--stopped.plyr__poster-enabled .plyr__poster {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
@ -6,10 +6,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin-right: $plyr-range-thumb-height;
|
||||||
|
left: $plyr-range-thumb-height / 2;
|
||||||
|
|
||||||
input[type='range'] {
|
input[type='range'] {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
// Offset the range thumb in order to be able to calculate the relative progress (#954)
|
||||||
|
width: calc(100% + #{$plyr-range-thumb-height}) !important;
|
||||||
|
margin: 0 -#{$plyr-range-thumb-height / 2} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seek tooltip to show time
|
// Seek tooltip to show time
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
&::-webkit-slider-runnable-track {
|
&::-webkit-slider-runnable-track {
|
||||||
@include plyr-range-track();
|
@include plyr-range-track();
|
||||||
background-image: linear-gradient(to right, currentColor var(--value), transparent var(--value));
|
background-image: linear-gradient(to right, currentColor var(--value, 0%), transparent var(--value, 0%));
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-slider-thumb {
|
&::-webkit-slider-thumb {
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
transform: translate(-50%, 10px) scale(0.8);
|
transform: translate(-50%, 10px) scale(0.8);
|
||||||
transform-origin: 50% 100%;
|
transform-origin: 50% 100%;
|
||||||
transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;
|
transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;
|
||||||
|
white-space: nowrap;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
// The background triangle
|
// The background triangle
|
||||||
|
@ -23,7 +23,12 @@
|
|||||||
// Hide sound controls on iOS
|
// Hide sound controls on iOS
|
||||||
// It's not supported to change volume using JavaScript:
|
// It's not supported to change volume using JavaScript:
|
||||||
// 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
|
||||||
.plyr--is-ios .plyr__volume,
|
.plyr--is-ios .plyr__volume {
|
||||||
.plyr--is-ios [data-plyr='mute'] {
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vimeo has no toggle mute method so hide mute button
|
||||||
|
// https://github.com/vimeo/player.js/issues/236#issuecomment-384663183
|
||||||
|
.plyr--is-ios.plyr--vimeo [data-plyr='mute'] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,27 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Advertisments
|
// Advertisements
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
.plyr__ads {
|
.plyr__ads {
|
||||||
|
border-radius: inherit;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
overflow: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 3; // Above the controls
|
z-index: -1; // Hide it by default
|
||||||
|
|
||||||
|
// Make sure the inner container is big enough for the ad creative.
|
||||||
|
> div,
|
||||||
|
> div iframe {
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The countdown label
|
||||||
&::after {
|
&::after {
|
||||||
background: rgba($plyr-color-gunmetal, 0.8);
|
background: rgba($plyr-color-gunmetal, 0.8);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
@ -32,13 +32,13 @@
|
|||||||
@import 'components/embed';
|
@import 'components/embed';
|
||||||
@import 'components/menus';
|
@import 'components/menus';
|
||||||
@import 'components/progress';
|
@import 'components/progress';
|
||||||
|
@import 'components/poster';
|
||||||
@import 'components/sliders';
|
@import 'components/sliders';
|
||||||
@import 'components/times';
|
@import 'components/times';
|
||||||
@import 'components/tooltips';
|
@import 'components/tooltips';
|
||||||
@import 'components/video';
|
@import 'components/video';
|
||||||
@import 'components/volume';
|
@import 'components/volume';
|
||||||
|
|
||||||
@import 'states/error';
|
|
||||||
@import 'states/fullscreen';
|
@import 'states/fullscreen';
|
||||||
|
|
||||||
@import 'plugins/ads';
|
@import 'plugins/ads';
|
||||||
|
@ -8,8 +8,9 @@ $plyr-font-size-small: 14px !default;
|
|||||||
$plyr-font-size-large: 18px !default;
|
$plyr-font-size-large: 18px !default;
|
||||||
$plyr-font-size-xlarge: 21px !default;
|
$plyr-font-size-xlarge: 21px !default;
|
||||||
|
|
||||||
$plyr-font-size-time: 14px !default;
|
$plyr-font-size-time: $plyr-font-size-small !default;
|
||||||
$plyr-font-size-badge: 9px !default;
|
$plyr-font-size-badge: 9px !default;
|
||||||
|
$plyr-font-size-menu: $plyr-font-size-small !default;
|
||||||
|
|
||||||
$plyr-font-weight-regular: 500 !default;
|
$plyr-font-weight-regular: 500 !default;
|
||||||
$plyr-font-weight-bold: 600 !default;
|
$plyr-font-weight-bold: 600 !default;
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
// --------------------------------------------------------------
|
|
||||||
// Error state
|
|
||||||
// --------------------------------------------------------------
|
|
||||||
|
|
||||||
.plyr--has-error {
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
align-items: center;
|
|
||||||
background: rgba(#000, 90%);
|
|
||||||
color: #fff;
|
|
||||||
content: attr(data-plyr-error);
|
|
||||||
display: flex;
|
|
||||||
font-size: $plyr-font-size-base;
|
|
||||||
height: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
text-shadow: 0 1px 1px rgba(#000, 10%);
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,15 +2,6 @@
|
|||||||
// Hiding content nicely
|
// Hiding content nicely
|
||||||
// --------------------------------------------------------------
|
// --------------------------------------------------------------
|
||||||
|
|
||||||
// Attributes
|
|
||||||
.plyr--full-ui [hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plyr--full-ui [aria-hidden='true'] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Screen reader only elements
|
// Screen reader only elements
|
||||||
.plyr__sr-only {
|
.plyr__sr-only {
|
||||||
clip: rect(1px, 1px, 1px, 1px);
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
|
Reference in New Issue
Block a user