Compare commits
128 Commits
v3.0.0-bet
...
v3.2.0
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
c90f1bdf08 | |||
6a9be8d16b | |||
58c2c52c95 | |||
73a39769d4 | |||
7221e26eca | |||
98adb8e784 | |||
a59dcb2f53 | |||
d21b58e1c9 | |||
d6e84cbabb | |||
fcccf1d479 | |||
211db12a3d | |||
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,
|
||||||
|
@ -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"
|
||||||
|
86
changelog.md
86
changelog.md
@ -1,3 +1,87 @@
|
|||||||
|
## 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 +151,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
|
||||||
|
|
||||||
|
209
controls.md
209
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,78 @@ 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">00:00</div>
|
||||||
"</progress>",
|
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
|
||||||
"<span class='plyr__tooltip'>00:00</span>",
|
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
|
||||||
"</span>",
|
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
|
||||||
"<span class='plyr__time'>",
|
<span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span>
|
||||||
"<span class='plyr__sr-only'>Current time</span>",
|
<span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span>
|
||||||
"<span class='plyr__time--current'>00:00</span>",
|
</button>
|
||||||
"</span>",
|
<div class="plyr__volume">
|
||||||
"<span class='plyr__time'>",
|
<label for="plyr-volume-{id}" class="plyr__sr-only">Volume</label>
|
||||||
"<span class='plyr__sr-only'>Duration</span>",
|
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" id="plyr-volume-{id}">
|
||||||
"<span class='plyr__time--duration'>00:00</span>",
|
</div>
|
||||||
"</span>",
|
<button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions">
|
||||||
"<button type='button' data-plyr='mute'>",
|
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
|
||||||
"<svg class='icon--muted'><use xlink:href='#plyr-muted'></use></svg>",
|
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-captions-off"></use></svg>
|
||||||
"<svg><use xlink:href='#plyr-volume'></use></svg>",
|
<span class="label--pressed plyr__tooltip" role="tooltip">Disable captions</span>
|
||||||
"<span class='plyr__sr-only'>Toggle Mute</span>",
|
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enable captions</span>
|
||||||
"</button>",
|
</button>
|
||||||
"<span class='plyr__volume'>",
|
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Enter fullscreen" data-plyr="fullscreen">
|
||||||
"<label for='volume{id}' class='plyr__sr-only'>Volume</label>",
|
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg>
|
||||||
"<input id='volume{id}' class='plyr__volume--input' type='range' min='0' max='10' value='5' data-plyr='volume'>",
|
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg>
|
||||||
"<progress class='plyr__volume--display' max='10' value='0' role='presentation'></progress>",
|
<span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span>
|
||||||
"</span>",
|
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enter fullscreen</span>
|
||||||
"<button type='button' data-plyr='captions'>",
|
</button>
|
||||||
"<svg class='icon--captions-on'><use xlink:href='#plyr-captions-on'></use></svg>",
|
</div>
|
||||||
"<svg><use xlink:href='#plyr-captions-off'></use></svg>",
|
`;
|
||||||
"<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
4123
demo/dist/demo.js
vendored
4123
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,11 +6,16 @@
|
|||||||
<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-light.woff2">
|
|
||||||
<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">
|
||||||
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2">
|
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2">
|
||||||
</head>
|
</head>
|
||||||
|
@ -27,10 +27,9 @@
|
|||||||
<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-light.woff2">
|
|
||||||
<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">
|
||||||
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2">
|
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2">
|
||||||
</head>
|
</head>
|
||||||
@ -67,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>
|
||||||
@ -94,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>
|
||||||
@ -113,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>
|
||||||
@ -164,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"
|
||||||
|
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
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
|
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', () => {
|
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,30 @@ 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',
|
||||||
|
], */
|
||||||
captions: {
|
captions: {
|
||||||
active: true,
|
active: true,
|
||||||
},
|
},
|
||||||
@ -53,10 +82,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 +119,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: [
|
||||||
{
|
{
|
||||||
@ -220,11 +268,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 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 +287,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';
|
||||||
|
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
.button,
|
.button,
|
||||||
.button__count {
|
.button__count {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #fff;
|
background: $color-button-background;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: $border-radius-base;
|
border-radius: $border-radius-base;
|
||||||
box-shadow: 0 1px 1px rgba(#000, 0.1);
|
box-shadow: 0 1px 1px rgba(#000, 0.1);
|
||||||
color: $gray;
|
color: $color-button-text;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
padding: ($spacing-base * 0.75);
|
padding: ($spacing-base * 0.75);
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -71,7 +71,7 @@
|
|||||||
&::before {
|
&::before {
|
||||||
border: $arrow-size solid transparent;
|
border: $arrow-size solid transparent;
|
||||||
border-left-width: 0;
|
border-left-width: 0;
|
||||||
border-right-color: #fff;
|
border-right-color: $color-button-background;
|
||||||
content: '';
|
content: '';
|
||||||
height: 0;
|
height: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -28,6 +28,7 @@ body {
|
|||||||
|
|
||||||
main {
|
main {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
padding-bottom: 1px; // Collapsing margins
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,5 +24,9 @@ $color-vimeo: #19b7ed;
|
|||||||
$color-link: #fff;
|
$color-link: #fff;
|
||||||
$color-background: $color-brand-primary;
|
$color-background: $color-brand-primary;
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
$color-button-background: #fff;
|
||||||
|
$color-button-text: $gray;
|
||||||
|
|
||||||
// Focus
|
// Focus
|
||||||
$tab-focus-default-color: #fff;
|
$tab-focus-default-color: #fff;
|
||||||
|
@ -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;
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@include font-size($font-size-h1);
|
@include font-size($font-size-h1);
|
||||||
font-weight: $font-weight-light;
|
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
7928
dist/plyr.js
vendored
7928
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
12965
dist/plyr.polyfilled.js
vendored
Normal file
12965
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
76
gulpfile.js
76
gulpfile.js
@ -9,6 +9,7 @@ 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');
|
||||||
@ -24,8 +25,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 +40,8 @@ try {
|
|||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minSuffix = '.min';
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
const root = __dirname;
|
const root = __dirname;
|
||||||
const paths = {
|
const paths = {
|
||||||
@ -68,8 +70,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,6 +127,7 @@ 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, () =>
|
gulp.task(name, () =>
|
||||||
gulp
|
gulp
|
||||||
@ -135,15 +141,19 @@ const build = {
|
|||||||
resolve(),
|
resolve(),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
babel(babelrc),
|
babel(babelrc),
|
||||||
uglify({}, minify),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(size(sizeOptions))
|
|
||||||
.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)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -266,6 +276,23 @@ 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',
|
||||||
|
];
|
||||||
|
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 +304,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 +335,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`;
|
||||||
@ -389,6 +407,6 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
|||||||
|
|
||||||
// Do everything
|
// Do everything
|
||||||
gulp.task('publish', () => {
|
gulp.task('publish', () => {
|
||||||
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo');
|
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
52
package.json
52
package.json
@ -1,50 +1,53 @@
|
|||||||
{
|
{
|
||||||
"name": "plyr",
|
"name": "plyr",
|
||||||
"version": "3.0.0-beta.12",
|
"version": "3.2.0",
|
||||||
"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.0",
|
||||||
"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.6.1",
|
||||||
"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.11.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.3",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-open": "^2.1.0",
|
"gulp-filter": "^5.1.0",
|
||||||
|
"gulp-open": "^3.0.1",
|
||||||
"gulp-rename": "^1.2.2",
|
"gulp-rename": "^1.2.2",
|
||||||
"gulp-replace": "^0.6.1",
|
"gulp-replace": "^0.6.1",
|
||||||
"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.1",
|
||||||
"gulp-util": "^3.0.8",
|
"gulp-util": "^3.0.8",
|
||||||
|
"prettier-eslint": "^8.8.1",
|
||||||
|
"prettier-stylelint": "^0.4.2",
|
||||||
"rollup-plugin-babel": "^3.0.3",
|
"rollup-plugin-babel": "^3.0.3",
|
||||||
"rollup-plugin-commonjs": "^8.3.0",
|
"rollup-plugin-commonjs": "^9.1.0",
|
||||||
"rollup-plugin-node-resolve": "^3.0.2",
|
"rollup-plugin-node-resolve": "^3.3.0",
|
||||||
"rollup-plugin-uglify": "^3.0.0",
|
|
||||||
"run-sequence": "^2.2.1",
|
"run-sequence": "^2.2.1",
|
||||||
"stylelint": "^8.4.0",
|
"stylelint": "^9.2.0",
|
||||||
"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.0.0",
|
||||||
"stylelint-selector-bem-pattern": "^2.0.0",
|
"stylelint-selector-bem-pattern": "^2.0.0"
|
||||||
"uglify-es": "^3.3.9"
|
|
||||||
},
|
},
|
||||||
"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 +65,10 @@
|
|||||||
"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.24.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
181
readme.md
181
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
|
||||||
@ -35,40 +35,25 @@ Oh and yes, it works with Bootstrap.
|
|||||||
|
|
||||||
Check out the [changelog](changelog.md) to see what's new with Plyr.
|
Check out the [changelog](changelog.md) to see what's new with Plyr.
|
||||||
|
|
||||||
## CMS plugins
|
## Plugins & Components
|
||||||
|
|
||||||
### [WordPress](https://wordpress.org/plugins/plyr/)
|
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
|
||||||
|
|
||||||
Created and maintained by Ryan Anthony Drake ([@iamryandrake](https://github.com/iamryandrake))
|
| Type | Maintainer | Link |
|
||||||
|
| --------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||||
### [Neos](https://packagist.org/packages/jonnitto/plyr)
|
| WordPress | Ryan Anthony Drake ([@iamryandrake](https://github.com/iamryandrake)) | [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) |
|
||||||
Created and maintained by Jon Uhlmann ([@jonnitto](https://github.com/jonnitto))
|
| 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) |
|
||||||
### [Kirby](https://github.com/dpschen/kirby-plyrtag)
|
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
|
||||||
|
|
||||||
Created and maintained by Dominik Pschenitschni ([@dpschen](https://github.com/dpschen))
|
|
||||||
|
|
||||||
## Using package managers
|
|
||||||
|
|
||||||
You can grab the source using one of the following package managers.
|
|
||||||
|
|
||||||
### npm
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install plyr
|
|
||||||
```
|
|
||||||
|
|
||||||
[https://www.npmjs.com/package/plyr](https://www.npmjs.com/package/plyr)
|
|
||||||
|
|
||||||
## Quick setup
|
## Quick setup
|
||||||
|
|
||||||
Here's a quick run through on getting up and running. There's also a [demo on Codepen](http://codepen.io/sampotts/pen/jARJYp).
|
Here's a quick run through on getting up and running. There's also a [demo on Codepen](http://codepen.io/sampotts/pen/jARJYp). You can grab all of the source with [NPM](https://www.npmjs.com/package/plyr) using `npm install plyr`.
|
||||||
|
|
||||||
### 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
|
||||||
|
|
||||||
@ -131,18 +116,19 @@ 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>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See [initialising](#initialising) for more information on advanced setups.
|
||||||
|
|
||||||
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
|
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://cdn.plyr.io/3.0.0-beta.12/plyr.js"></script>
|
<script src="https://cdn.plyr.io/3.2.0/plyr.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
|
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
|
||||||
@ -158,13 +144,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.12/plyr.css">
|
<link rel="stylesheet" href="https://cdn.plyr.io/3.2.0/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.12/plyr.svg`.
|
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.2.0/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
|
||||||
|
|
||||||
@ -210,10 +206,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.
|
||||||
|
|
||||||
Here's some examples
|
Here's some examples
|
||||||
|
|
||||||
@ -237,7 +234,13 @@ 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
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const players = Array.from(document.querySelectorAll('.js-player')).map(player => new Plyr(player));
|
||||||
|
```
|
||||||
|
|
||||||
|
The second argument for the constructor is the [options](#options) object:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const player = new Plyr('#player', {
|
const player = new Plyr('#player', {
|
||||||
@ -258,10 +261,10 @@ Options can be passed as an object to the constructor as above or as JSON in `da
|
|||||||
Note the single quotes encapsulating the JSON and double quotes on the object keys. Only string values need double quotes.
|
Note the single quotes encapsulating the JSON and double quotes on the object keys. Only string values need double quotes.
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
| -------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. |
|
| `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. |
|
||||||
| `debug` | Boolean | `false` | Display debugging information in the console |
|
| `debug` | Boolean | `false` | Display debugging information in the console |
|
||||||
| `controls` | Function or Array | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return a string of HTML for the controls. Three arguments will be passed to your function; id (the unique id for the player), seektime (the seektime step in seconds), and title (the media title). See [controls.md](controls.md) for more info on how the html needs to be structured. |
|
| `controls` | Array, Function or Element | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; `id` (the unique id for the player), `seektime` (the seektime step in seconds), and `title` (the media title). See [controls.md](controls.md) for more info on how the html needs to be structured. |
|
||||||
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If you're using the default controls are used then you can specify which settings to show in the menu |
|
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If you're using the default controls are used then you can specify which settings to show in the menu |
|
||||||
| `i18n` | Object | See [defaults.js](/src/js/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
|
| `i18n` | Object | See [defaults.js](/src/js/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
|
||||||
| `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. |
|
| `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. |
|
||||||
@ -285,12 +288,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
|
||||||
|
|
||||||
@ -318,14 +322,15 @@ element.addEventListener('ready', event => {
|
|||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
|
|
||||||
Methods are not chainable. An example use of a method:
|
Example method use:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
player.play();
|
player.play(); // Start playback
|
||||||
|
player.fullscreen.enter(); // Enter fullscreen
|
||||||
```
|
```
|
||||||
|
|
||||||
| Method | Parameters | Description |
|
| Method | Parameters | Description |
|
||||||
| ------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
|
| ------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||||
| `play()`¹ | - | Start playback. |
|
| `play()`¹ | - | Start playback. |
|
||||||
| `pause()` | - | Pause playback. |
|
| `pause()` | - | Pause playback. |
|
||||||
| `togglePlay(toggle)` | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. |
|
| `togglePlay(toggle)` | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. |
|
||||||
@ -336,7 +341,9 @@ player.play();
|
|||||||
| `increaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
|
| `increaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
|
||||||
| `decreaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
|
| `decreaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
|
||||||
| `toggleCaptions(toggle)` | Boolean | Toggle captions display. If no parameter is passed, it will toggle based on current status. |
|
| `toggleCaptions(toggle)` | Boolean | Toggle captions display. If no parameter is passed, it will toggle based on current status. |
|
||||||
| `toggleFullscreen(event)` | Event | Toggle fullscreen. Fullscreen can only be initiated by a user event. Exit is possible without user input. |
|
| `fullscreen.enter()` | - | Enter fullscreen. If fullscreen is not supported, a fallback "full window/viewport" is used instead. |
|
||||||
|
| `fullscreen.exit()` | - | Exit 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 based on the specified boolean. |
|
||||||
| `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. |
|
||||||
@ -348,39 +355,45 @@ player.play();
|
|||||||
|
|
||||||
### Getters and Setters
|
### Getters and Setters
|
||||||
|
|
||||||
An example setter:
|
Example setters:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
player.volume = 0.5;
|
player.volume = 0.5; // Sets volume at 50%
|
||||||
|
player.currentTime = 10; // Seeks to 10 seconds
|
||||||
```
|
```
|
||||||
|
|
||||||
An example getter:
|
Example getters:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
player.volume; // returns 0.5;
|
player.volume; // 0.5;
|
||||||
|
player.currentTime; // 10
|
||||||
|
player.fullscreen.active; // false;
|
||||||
```
|
```
|
||||||
|
|
||||||
| Property | Getter | Setter | Description |
|
| Property | Getter | Setter | Description |
|
||||||
| --------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `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. |
|
| `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. |
|
||||||
| `ended` | ✔ | - | Returns a boolean indicating if the current player has finished playback. |
|
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
|
||||||
| `currentTime` | ✔ | ✔ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
|
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
|
||||||
| `seeking` | ✔ | - | Returns a boolean indicating if the current player is seeking. |
|
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
|
||||||
| `duration` | ✔ | - | Returns the duration for the current media. |
|
| `seeking` | ✓ | - | Returns a boolean indicating if the current player is seeking. |
|
||||||
| `volume` | ✔ | ✔ | Gets or sets the volume for the player. The setter accepts a float between 0 and 1. |
|
| `duration` | ✓ | - | Returns the duration for the current media. |
|
||||||
| `muted` | ✔ | ✔ | Gets or sets the muted state of the player. The setter accepts a boolean. |
|
| `volume` | ✓ | ✓ | Gets or sets the volume for the player. The setter accepts a float between 0 and 1. |
|
||||||
| `hasAudio` | ✔ | - | Returns a boolean indicating if the current media has an audio track. |
|
| `muted` | ✓ | ✓ | Gets or sets the muted state of the player. The setter accepts a boolean. |
|
||||||
| `speed` | ✔ | ✔ | Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5. |
|
| `hasAudio` | ✓ | - | Returns a boolean indicating if the current media has an audio track. |
|
||||||
| `quality`¹ | ✔ | ✔ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
|
| `speed` | ✓ | ✓ | Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5. |
|
||||||
| `loop` | ✔ | ✔ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
|
| `quality`¹ | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
|
||||||
| `source` | ✔ | ✔ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
|
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
|
||||||
| `poster`² | ✔ | ✔ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
|
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
|
||||||
| `autoplay` | ✔ | ✔ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
|
| `poster`² | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
|
||||||
| `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. |
|
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
|
||||||
| `pip` | ✔ | ✔ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |
|
| `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.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
|
||||||
|
| `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |
|
||||||
|
|
||||||
1. YouTube only. HTML5 will follow.
|
1. YouTube only. HTML5 will follow.
|
||||||
2. HTML5 only
|
2. HTML5 only
|
||||||
@ -596,14 +609,14 @@ 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))
|
||||||
@ -694,7 +707,7 @@ 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.
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Plyr Captions
|
// Plyr Captions
|
||||||
|
// TODO: Create as class
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import controls from './controls';
|
import controls from './controls';
|
||||||
|
import i18n from './i18n';
|
||||||
|
|
||||||
const captions = {
|
const captions = {
|
||||||
// Setup captions
|
// Setup captions
|
||||||
@ -39,7 +41,7 @@ const captions = {
|
|||||||
// Only Vimeo and HTML5 video supported at this point
|
// Only Vimeo and HTML5 video supported at this point
|
||||||
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
|
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
|
||||||
// Clear menu and hide
|
// Clear menu and hide
|
||||||
if (this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
|
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
|
||||||
controls.setCaptionsMenu.call(this);
|
controls.setCaptionsMenu.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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);
|
||||||
|
|
||||||
@ -68,7 +101,7 @@ const captions = {
|
|||||||
captions.show.call(this);
|
captions.show.call(this);
|
||||||
|
|
||||||
// Set available languages in list
|
// Set available languages in list
|
||||||
if (this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
|
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
|
||||||
controls.setCaptionsMenu.call(this);
|
controls.setCaptionsMenu.call(this);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -78,7 +111,7 @@ const captions = {
|
|||||||
// Setup HTML5 track rendering
|
// Setup HTML5 track rendering
|
||||||
if (this.isHTML5 && this.isVideo) {
|
if (this.isHTML5 && this.isVideo) {
|
||||||
captions.getTracks.call(this).forEach(track => {
|
captions.getTracks.call(this).forEach(track => {
|
||||||
// Remove previous bindings
|
// Show track
|
||||||
utils.on(track, 'cuechange', event => captions.setCue.call(this, event));
|
utils.on(track, 'cuechange', event => captions.setCue.call(this, event));
|
||||||
|
|
||||||
// Turn off native caption rendering to avoid double captions
|
// Turn off native caption rendering to avoid double captions
|
||||||
@ -117,14 +150,57 @@ 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
|
||||||
setCue(input) {
|
setCue(input) {
|
||||||
// Get the track from the event if needed
|
// Get the track from the event if needed
|
||||||
const track = utils.is.event(input) ? input.target : input;
|
const track = utils.is.event(input) ? input.target : input;
|
||||||
const active = track.activeCues[0];
|
const { activeCues } = track;
|
||||||
|
const active = activeCues.length && activeCues[0];
|
||||||
const currentTrack = captions.getCurrentTrack.call(this);
|
const currentTrack = captions.getCurrentTrack.call(this);
|
||||||
|
|
||||||
// Only display current track
|
// Only display current track
|
||||||
|
286
src/js/controls.js
vendored
286
src/js/controls.js
vendored
@ -5,7 +5,9 @@
|
|||||||
import support from './support';
|
import support from './support';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
|
import i18n from './i18n';
|
||||||
import captions from './captions';
|
import captions from './captions';
|
||||||
|
import html5 from './html5';
|
||||||
|
|
||||||
// Sniff out the browser
|
// Sniff out the browser
|
||||||
const browser = utils.getBrowser();
|
const browser = utils.getBrowser();
|
||||||
@ -50,7 +52,7 @@ const controls = {
|
|||||||
icon,
|
icon,
|
||||||
utils.extend(attributes, {
|
utils.extend(attributes, {
|
||||||
role: 'presentation',
|
role: 'presentation',
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create the <use> to reference sprite
|
// Create the <use> to reference sprite
|
||||||
@ -74,7 +76,7 @@ const controls = {
|
|||||||
|
|
||||||
// Create hidden text label
|
// Create hidden text label
|
||||||
createLabel(type, attr) {
|
createLabel(type, attr) {
|
||||||
let text = this.config.i18n[type];
|
let text = i18n.get(type, this.config);
|
||||||
const attributes = Object.assign({}, attr);
|
const attributes = Object.assign({}, attr);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -115,8 +117,8 @@ const controls = {
|
|||||||
{
|
{
|
||||||
class: this.config.classNames.menu.badge,
|
class: this.config.classNames.menu.badge,
|
||||||
},
|
},
|
||||||
text
|
text,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return badge;
|
return badge;
|
||||||
@ -126,7 +128,7 @@ const controls = {
|
|||||||
createButton(buttonType, attr) {
|
createButton(buttonType, attr) {
|
||||||
const button = utils.createElement('button');
|
const button = utils.createElement('button');
|
||||||
const attributes = Object.assign({}, attr);
|
const attributes = Object.assign({}, attr);
|
||||||
let type = buttonType;
|
let type = utils.toCamelCase(buttonType);
|
||||||
|
|
||||||
let toggle = false;
|
let toggle = false;
|
||||||
let label;
|
let label;
|
||||||
@ -147,7 +149,7 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Large play button
|
// Large play button
|
||||||
switch (type) {
|
switch (buttonType) {
|
||||||
case 'play':
|
case 'play':
|
||||||
toggle = true;
|
toggle = true;
|
||||||
label = 'play';
|
label = 'play';
|
||||||
@ -189,7 +191,7 @@ const controls = {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
label = type;
|
label = type;
|
||||||
icon = type;
|
icon = buttonType;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup toggle icon and labels
|
// Setup toggle icon and labels
|
||||||
@ -204,7 +206,7 @@ const controls = {
|
|||||||
|
|
||||||
// Add aria attributes
|
// Add aria attributes
|
||||||
attributes['aria-pressed'] = false;
|
attributes['aria-pressed'] = false;
|
||||||
attributes['aria-label'] = this.config.i18n[label];
|
attributes['aria-label'] = i18n.get(label, this.config);
|
||||||
} else {
|
} else {
|
||||||
button.appendChild(controls.createIcon.call(this, icon));
|
button.appendChild(controls.createIcon.call(this, icon));
|
||||||
button.appendChild(controls.createLabel.call(this, label));
|
button.appendChild(controls.createLabel.call(this, label));
|
||||||
@ -215,7 +217,16 @@ const controls = {
|
|||||||
|
|
||||||
utils.setAttributes(button, attributes);
|
utils.setAttributes(button, attributes);
|
||||||
|
|
||||||
|
// We have multiple play buttons
|
||||||
|
if (type === 'play') {
|
||||||
|
if (!utils.is.array(this.elements.buttons[type])) {
|
||||||
|
this.elements.buttons[type] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.elements.buttons[type].push(button);
|
||||||
|
} else {
|
||||||
this.elements.buttons[type] = button;
|
this.elements.buttons[type] = button;
|
||||||
|
}
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
},
|
},
|
||||||
@ -229,7 +240,7 @@ const controls = {
|
|||||||
for: attributes.id,
|
for: attributes.id,
|
||||||
class: this.config.classNames.hidden,
|
class: this.config.classNames.hidden,
|
||||||
},
|
},
|
||||||
this.config.i18n[type]
|
i18n.get(type, this.config),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Seek input
|
// Seek input
|
||||||
@ -245,8 +256,8 @@ const controls = {
|
|||||||
value: 0,
|
value: 0,
|
||||||
autocomplete: 'off',
|
autocomplete: 'off',
|
||||||
},
|
},
|
||||||
attributes
|
attributes,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.elements.inputs[type] = input;
|
this.elements.inputs[type] = input;
|
||||||
@ -271,8 +282,8 @@ const controls = {
|
|||||||
max: 100,
|
max: 100,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
attributes
|
attributes,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create the label inside
|
// Create the label inside
|
||||||
@ -282,11 +293,11 @@ const controls = {
|
|||||||
let suffix = '';
|
let suffix = '';
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'played':
|
case 'played':
|
||||||
suffix = this.config.i18n.played;
|
suffix = i18n.get('played', this.config);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'buffer':
|
case 'buffer':
|
||||||
suffix = this.config.i18n.buffered;
|
suffix = i18n.get('buffered', this.config);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -313,8 +324,8 @@ const controls = {
|
|||||||
{
|
{
|
||||||
class: this.config.classNames.hidden,
|
class: this.config.classNames.hidden,
|
||||||
},
|
},
|
||||||
this.config.i18n[type]
|
i18n.get(type, this.config),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.display[type]), '00:00'));
|
container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.display[type]), '00:00'));
|
||||||
@ -340,7 +351,7 @@ const controls = {
|
|||||||
value,
|
value,
|
||||||
checked,
|
checked,
|
||||||
class: 'plyr__sr-only',
|
class: 'plyr__sr-only',
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const faux = utils.createElement('span', { 'aria-hidden': true });
|
const faux = utils.createElement('span', { 'aria-hidden': true });
|
||||||
@ -374,6 +385,16 @@ const controls = {
|
|||||||
const clientRect = this.elements.inputs.seek.getBoundingClientRect();
|
const clientRect = this.elements.inputs.seek.getBoundingClientRect();
|
||||||
const visible = `${this.config.classNames.tooltip}--visible`;
|
const visible = `${this.config.classNames.tooltip}--visible`;
|
||||||
|
|
||||||
|
const toggle = toggle => {
|
||||||
|
utils.toggleClass(this.elements.display.seekTooltip, visible, toggle);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hide on touch
|
||||||
|
if (this.touch) {
|
||||||
|
toggle(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Determine percentage, if already visible
|
// Determine percentage, if already visible
|
||||||
if (utils.is.event(event)) {
|
if (utils.is.event(event)) {
|
||||||
percent = 100 / clientRect.width * (event.pageX - clientRect.left);
|
percent = 100 / clientRect.width * (event.pageX - clientRect.left);
|
||||||
@ -402,7 +423,7 @@ const controls = {
|
|||||||
'mouseenter',
|
'mouseenter',
|
||||||
'mouseleave',
|
'mouseleave',
|
||||||
].includes(event.type)) {
|
].includes(event.type)) {
|
||||||
utils.toggleClass(this.elements.display.seekTooltip, visible, event.type === 'mouseenter');
|
toggle(event.type === 'mouseenter');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -415,23 +436,29 @@ const controls = {
|
|||||||
utils.toggleHidden(pane, !toggle);
|
utils.toggleHidden(pane, !toggle);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set the YouTube quality menu
|
// Set the quality menu
|
||||||
// TODO: Support for HTML5
|
// TODO: Vimeo support
|
||||||
setQualityMenu(options) {
|
setQualityMenu(options) {
|
||||||
|
// Menu required
|
||||||
|
if (!utils.is.element(this.elements.settings.panes.quality)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const type = 'quality';
|
const type = 'quality';
|
||||||
const list = this.elements.settings.panes.quality.querySelector('ul');
|
const list = this.elements.settings.panes.quality.querySelector('ul');
|
||||||
|
|
||||||
// Set options if passed and filter based on config
|
// Set options if passed and filter based on config
|
||||||
if (utils.is.array(options)) {
|
if (utils.is.array(options)) {
|
||||||
this.options.quality = options.filter(quality => this.config.quality.options.includes(quality));
|
this.options.quality = options.filter(quality => this.config.quality.options.includes(quality));
|
||||||
} else {
|
|
||||||
this.options.quality = this.config.quality.options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle the pane and tab
|
// Toggle the pane and tab
|
||||||
const toggle = !utils.is.empty(this.options.quality) && this.isYouTube;
|
const toggle = !utils.is.empty(this.options.quality) && this.options.quality.length > 1;
|
||||||
controls.toggleTab.call(this, type, toggle);
|
controls.toggleTab.call(this, type, toggle);
|
||||||
|
|
||||||
|
// Check if we need to toggle the parent
|
||||||
|
controls.checkMenu.call(this);
|
||||||
|
|
||||||
// If we're hiding, nothing more to do
|
// If we're hiding, nothing more to do
|
||||||
if (!toggle) {
|
if (!toggle) {
|
||||||
return;
|
return;
|
||||||
@ -445,20 +472,18 @@ const controls = {
|
|||||||
let label = '';
|
let label = '';
|
||||||
|
|
||||||
switch (quality) {
|
switch (quality) {
|
||||||
case 'hd2160':
|
case 2160:
|
||||||
label = '4K';
|
label = '4K';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'hd1440':
|
case 1440:
|
||||||
label = 'WQHD';
|
case 1080:
|
||||||
break;
|
case 720:
|
||||||
|
|
||||||
case 'hd1080':
|
|
||||||
label = 'HD';
|
label = 'HD';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'hd720':
|
case 576:
|
||||||
label = 'HD';
|
label = 'SD';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -472,9 +497,16 @@ const controls = {
|
|||||||
return controls.createBadge.call(this, label);
|
return controls.createBadge.call(this, label);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.options.quality.forEach(quality =>
|
// Sort options by the config and then render options
|
||||||
controls.createMenuItem.call(this, quality, list, type, controls.getLabel.call(this, 'quality', quality), getBadge(quality))
|
this.options.quality
|
||||||
);
|
.sort((a, b) => {
|
||||||
|
const sorting = this.config.quality.options;
|
||||||
|
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
|
||||||
|
})
|
||||||
|
.forEach(quality => {
|
||||||
|
const label = controls.getLabel.call(this, 'quality', quality);
|
||||||
|
controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality));
|
||||||
|
});
|
||||||
|
|
||||||
controls.updateSetting.call(this, type, list);
|
controls.updateSetting.call(this, type, list);
|
||||||
},
|
},
|
||||||
@ -484,34 +516,17 @@ const controls = {
|
|||||||
getLabel(setting, value) {
|
getLabel(setting, value) {
|
||||||
switch (setting) {
|
switch (setting) {
|
||||||
case 'speed':
|
case 'speed':
|
||||||
return value === 1 ? 'Normal' : `${value}×`;
|
return value === 1 ? i18n.get('normal', this.config) : `${value}×`;
|
||||||
|
|
||||||
case 'quality':
|
case 'quality':
|
||||||
switch (value) {
|
if (utils.is.number(value)) {
|
||||||
case 'hd2160':
|
return `${value}p`;
|
||||||
return '2160P';
|
|
||||||
case 'hd1440':
|
|
||||||
return '1440P';
|
|
||||||
case 'hd1080':
|
|
||||||
return '1080P';
|
|
||||||
case 'hd720':
|
|
||||||
return '720P';
|
|
||||||
case 'large':
|
|
||||||
return '480P';
|
|
||||||
case 'medium':
|
|
||||||
return '360P';
|
|
||||||
case 'small':
|
|
||||||
return '240P';
|
|
||||||
case 'tiny':
|
|
||||||
return 'Tiny';
|
|
||||||
case 'default':
|
|
||||||
return 'Auto';
|
|
||||||
default:
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return utils.toTitleCase(value);
|
||||||
|
|
||||||
case 'captions':
|
case 'captions':
|
||||||
return controls.getLanguage.call(this);
|
return captions.getLabel.call(this);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
@ -519,18 +534,27 @@ const controls = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Update the selected setting
|
// Update the selected setting
|
||||||
updateSetting(setting, container) {
|
updateSetting(setting, container, input) {
|
||||||
const pane = this.elements.settings.panes[setting];
|
const pane = this.elements.settings.panes[setting];
|
||||||
let value = null;
|
let value = null;
|
||||||
let list = container;
|
let list = container;
|
||||||
|
|
||||||
switch (setting) {
|
switch (setting) {
|
||||||
case 'captions':
|
case 'captions':
|
||||||
value = this.captions.active ? this.captions.language : '';
|
if (this.captions.active) {
|
||||||
|
if (this.options.captions.length > 2 || !this.options.captions.some(lang => lang === 'enabled')) {
|
||||||
|
value = this.captions.language;
|
||||||
|
} else {
|
||||||
|
value = 'enabled';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = '';
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
value = this[setting];
|
value = !utils.is.empty(input) ? input : this[setting];
|
||||||
|
|
||||||
// Get default
|
// Get default
|
||||||
if (utils.is.empty(value)) {
|
if (utils.is.empty(value)) {
|
||||||
@ -538,7 +562,7 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unsupported value
|
// Unsupported value
|
||||||
if (!this.options[setting].includes(value)) {
|
if (!utils.is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
|
||||||
this.debug.warn(`Unsupported value of '${value}' for ${setting}`);
|
this.debug.warn(`Unsupported value of '${value}' for ${setting}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -557,23 +581,30 @@ const controls = {
|
|||||||
list = pane && pane.querySelector('ul');
|
list = pane && pane.querySelector('ul');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the label
|
// If there's no list it means it's not been rendered...
|
||||||
if (!utils.is.empty(value)) {
|
if (!utils.is.element(list)) {
|
||||||
const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`);
|
return;
|
||||||
label.innerHTML = controls.getLabel.call(this, setting, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the radio option
|
// Update the label
|
||||||
|
const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`);
|
||||||
|
label.innerHTML = controls.getLabel.call(this, setting, value);
|
||||||
|
|
||||||
|
// Find the radio option and check it
|
||||||
const target = list && list.querySelector(`input[value="${value}"]`);
|
const target = list && list.querySelector(`input[value="${value}"]`);
|
||||||
|
|
||||||
if (utils.is.element(target)) {
|
if (utils.is.element(target)) {
|
||||||
// Check it
|
|
||||||
target.checked = true;
|
target.checked = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set the looping options
|
// Set the looping options
|
||||||
/* setLoopMenu() {
|
/* setLoopMenu() {
|
||||||
|
// Menu required
|
||||||
|
if (!utils.is.element(this.elements.settings.panes.loop)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const options = ['start', 'end', 'all', 'reset'];
|
const options = ['start', 'end', 'all', 'reset'];
|
||||||
const list = this.elements.settings.panes.loop.querySelector('ul');
|
const list = this.elements.settings.panes.loop.querySelector('ul');
|
||||||
|
|
||||||
@ -598,7 +629,7 @@ const controls = {
|
|||||||
class: this.config.classNames.control,
|
class: this.config.classNames.control,
|
||||||
'data-plyr-loop-action': option,
|
'data-plyr-loop-action': option,
|
||||||
}),
|
}),
|
||||||
this.config.i18n[option]
|
i18n.get(option, this.config)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (['start', 'end'].includes(option)) {
|
if (['start', 'end'].includes(option)) {
|
||||||
@ -613,25 +644,7 @@ const controls = {
|
|||||||
|
|
||||||
// Get current selected caption language
|
// Get current selected caption language
|
||||||
// TODO: rework this to user the getter in the API?
|
// TODO: rework this to user the getter in the API?
|
||||||
getLanguage() {
|
|
||||||
if (!this.supported.ui) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!support.textTracks || !captions.getTracks.call(this).length) {
|
|
||||||
return this.config.i18n.none;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.captions.active) {
|
|
||||||
const currentTrack = captions.getCurrentTrack.call(this);
|
|
||||||
|
|
||||||
if (utils.is.track(currentTrack)) {
|
|
||||||
return currentTrack.label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.config.i18n.disabled;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Set a list of available captions languages
|
// Set a list of available captions languages
|
||||||
setCaptionsMenu() {
|
setCaptionsMenu() {
|
||||||
@ -640,27 +653,30 @@ const controls = {
|
|||||||
const list = this.elements.settings.panes.captions.querySelector('ul');
|
const list = this.elements.settings.panes.captions.querySelector('ul');
|
||||||
|
|
||||||
// Toggle the pane and tab
|
// Toggle the pane and tab
|
||||||
const hasTracks = captions.getTracks.call(this).length;
|
const toggle = captions.getTracks.call(this).length;
|
||||||
controls.toggleTab.call(this, type, hasTracks);
|
controls.toggleTab.call(this, type, toggle);
|
||||||
|
|
||||||
// Empty the menu
|
// Empty the menu
|
||||||
utils.emptyElement(list);
|
utils.emptyElement(list);
|
||||||
|
|
||||||
|
// Check if we need to toggle the parent
|
||||||
|
controls.checkMenu.call(this);
|
||||||
|
|
||||||
// If there's no captions, bail
|
// If there's no captions, bail
|
||||||
if (!hasTracks) {
|
if (!toggle) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-map the tracks into just the data we need
|
// Re-map the tracks into just the data we need
|
||||||
const tracks = captions.getTracks.call(this).map(track => ({
|
const tracks = captions.getTracks.call(this).map(track => ({
|
||||||
language: track.language,
|
language: !utils.is.empty(track.language) ? track.language : 'enabled',
|
||||||
label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(),
|
label: captions.getLabel.call(this, track),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Add the "None" option to turn off captions
|
// Add the "Disabled" option to turn off captions
|
||||||
tracks.unshift({
|
tracks.unshift({
|
||||||
language: '',
|
language: '',
|
||||||
label: this.config.i18n.none,
|
label: i18n.get('disabled', this.config),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate options
|
// Generate options
|
||||||
@ -670,21 +686,36 @@ const controls = {
|
|||||||
track.language,
|
track.language,
|
||||||
list,
|
list,
|
||||||
'language',
|
'language',
|
||||||
track.label || track.language,
|
track.label,
|
||||||
controls.createBadge.call(this, track.language.toUpperCase()),
|
track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null,
|
||||||
track.language.toLowerCase() === this.captions.language.toLowerCase()
|
track.language.toLowerCase() === this.captions.language.toLowerCase(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Store reference
|
||||||
|
this.options.captions = tracks.map(track => track.language);
|
||||||
|
|
||||||
controls.updateSetting.call(this, type, list);
|
controls.updateSetting.call(this, type, list);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set a list of available captions languages
|
// Set a list of available captions languages
|
||||||
setSpeedMenu() {
|
setSpeedMenu(options) {
|
||||||
|
// Do nothing if not selected
|
||||||
|
if (!this.config.controls.includes('settings') || !this.config.settings.includes('speed')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu required
|
||||||
|
if (!utils.is.element(this.elements.settings.panes.speed)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const type = 'speed';
|
const type = 'speed';
|
||||||
|
|
||||||
// Set the default speeds
|
// Set the speed options
|
||||||
if (!utils.is.object(this.options.speed) || !Object.keys(this.options.speed).length) {
|
if (utils.is.array(options)) {
|
||||||
|
this.options.speed = options;
|
||||||
|
} else if (this.isHTML5 || this.isVimeo) {
|
||||||
this.options.speed = [
|
this.options.speed = [
|
||||||
0.5,
|
0.5,
|
||||||
0.75,
|
0.75,
|
||||||
@ -700,9 +731,12 @@ const controls = {
|
|||||||
this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
|
this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
|
||||||
|
|
||||||
// Toggle the pane and tab
|
// Toggle the pane and tab
|
||||||
const toggle = !utils.is.empty(this.options.speed);
|
const toggle = !utils.is.empty(this.options.speed) && this.options.speed.length > 1;
|
||||||
controls.toggleTab.call(this, type, toggle);
|
controls.toggleTab.call(this, type, toggle);
|
||||||
|
|
||||||
|
// Check if we need to toggle the parent
|
||||||
|
controls.checkMenu.call(this);
|
||||||
|
|
||||||
// If we're hiding, nothing more to do
|
// If we're hiding, nothing more to do
|
||||||
if (!toggle) {
|
if (!toggle) {
|
||||||
return;
|
return;
|
||||||
@ -719,15 +753,32 @@ const controls = {
|
|||||||
utils.emptyElement(list);
|
utils.emptyElement(list);
|
||||||
|
|
||||||
// Create items
|
// Create items
|
||||||
this.options.speed.forEach(speed => controls.createMenuItem.call(this, speed, list, type, controls.getLabel.call(this, 'speed', speed)));
|
this.options.speed.forEach(speed => {
|
||||||
|
const label = controls.getLabel.call(this, 'speed', speed);
|
||||||
|
controls.createMenuItem.call(this, speed, list, type, label);
|
||||||
|
});
|
||||||
|
|
||||||
controls.updateSetting.call(this, type, list);
|
controls.updateSetting.call(this, type, list);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Check if we need to hide/show the settings menu
|
||||||
|
checkMenu() {
|
||||||
|
const { tabs } = this.elements.settings;
|
||||||
|
const visible = !utils.is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden);
|
||||||
|
|
||||||
|
utils.toggleHidden(this.elements.settings.menu, !visible);
|
||||||
|
},
|
||||||
|
|
||||||
// Show/hide menu
|
// Show/hide menu
|
||||||
toggleMenu(event) {
|
toggleMenu(event) {
|
||||||
const { form } = this.elements.settings;
|
const { form } = this.elements.settings;
|
||||||
const button = this.elements.buttons.settings;
|
const button = this.elements.buttons.settings;
|
||||||
|
|
||||||
|
// Menu and button are required
|
||||||
|
if (!utils.is.element(form) || !utils.is.element(button)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.getAttribute('aria-hidden') === 'true';
|
const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.getAttribute('aria-hidden') === 'true';
|
||||||
|
|
||||||
if (utils.is.event(event)) {
|
if (utils.is.event(event)) {
|
||||||
@ -893,7 +944,6 @@ const controls = {
|
|||||||
// Play/Pause button
|
// Play/Pause button
|
||||||
if (this.config.controls.includes('play')) {
|
if (this.config.controls.includes('play')) {
|
||||||
container.appendChild(controls.createButton.call(this, 'play'));
|
container.appendChild(controls.createButton.call(this, 'play'));
|
||||||
// container.appendChild(controls.createButton.call(this, 'pause'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast forward button
|
// Fast forward button
|
||||||
@ -925,7 +975,7 @@ const controls = {
|
|||||||
role: 'tooltip',
|
role: 'tooltip',
|
||||||
class: this.config.classNames.tooltip,
|
class: this.config.classNames.tooltip,
|
||||||
},
|
},
|
||||||
'00:00'
|
'00:00',
|
||||||
);
|
);
|
||||||
|
|
||||||
progress.appendChild(tooltip);
|
progress.appendChild(tooltip);
|
||||||
@ -970,7 +1020,7 @@ const controls = {
|
|||||||
'volume',
|
'volume',
|
||||||
utils.extend(attributes, {
|
utils.extend(attributes, {
|
||||||
id: `plyr-volume-${data.id}`,
|
id: `plyr-volume-${data.id}`,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
volume.appendChild(range.label);
|
volume.appendChild(range.label);
|
||||||
volume.appendChild(range.input);
|
volume.appendChild(range.input);
|
||||||
@ -989,6 +1039,7 @@ const controls = {
|
|||||||
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
||||||
const menu = utils.createElement('div', {
|
const menu = utils.createElement('div', {
|
||||||
class: 'plyr__menu',
|
class: 'plyr__menu',
|
||||||
|
hidden: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
menu.appendChild(
|
menu.appendChild(
|
||||||
@ -997,7 +1048,7 @@ const controls = {
|
|||||||
'aria-haspopup': true,
|
'aria-haspopup': true,
|
||||||
'aria-controls': `plyr-settings-${data.id}`,
|
'aria-controls': `plyr-settings-${data.id}`,
|
||||||
'aria-expanded': false,
|
'aria-expanded': false,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const form = utils.createElement('form', {
|
const form = utils.createElement('form', {
|
||||||
@ -1040,7 +1091,7 @@ const controls = {
|
|||||||
'aria-controls': `plyr-settings-${data.id}-${type}`,
|
'aria-controls': `plyr-settings-${data.id}-${type}`,
|
||||||
'aria-expanded': false,
|
'aria-expanded': false,
|
||||||
}),
|
}),
|
||||||
this.config.i18n[type]
|
i18n.get(type, this.config),
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = utils.createElement('span', {
|
const value = utils.createElement('span', {
|
||||||
@ -1080,7 +1131,7 @@ const controls = {
|
|||||||
'aria-controls': `plyr-settings-${data.id}-home`,
|
'aria-controls': `plyr-settings-${data.id}-home`,
|
||||||
'aria-expanded': false,
|
'aria-expanded': false,
|
||||||
},
|
},
|
||||||
this.config.i18n[type]
|
i18n.get(type, this.config),
|
||||||
);
|
);
|
||||||
|
|
||||||
pane.appendChild(back);
|
pane.appendChild(back);
|
||||||
@ -1123,10 +1174,12 @@ const controls = {
|
|||||||
|
|
||||||
this.elements.controls = container;
|
this.elements.controls = container;
|
||||||
|
|
||||||
if (this.config.controls.includes('settings') && this.config.settings.includes('speed')) {
|
if (this.isHTML5) {
|
||||||
controls.setSpeedMenu.call(this);
|
controls.setQualityMenu.call(this, html5.getQualityOptions.call(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
controls.setSpeedMenu.call(this);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1147,9 +1200,10 @@ const controls = {
|
|||||||
|
|
||||||
// Null by default
|
// Null by default
|
||||||
let container = null;
|
let container = null;
|
||||||
|
this.elements.controls = null;
|
||||||
|
|
||||||
// HTML passed as the option
|
// HTML or Element passed as the option
|
||||||
if (utils.is.string(this.config.controls)) {
|
if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) {
|
||||||
container = this.config.controls;
|
container = this.config.controls;
|
||||||
} else if (utils.is.function(this.config.controls)) {
|
} else if (utils.is.function(this.config.controls)) {
|
||||||
// A custom function to build controls
|
// A custom function to build controls
|
||||||
@ -1166,7 +1220,7 @@ const controls = {
|
|||||||
seektime: this.config.seekTime,
|
seektime: this.config.seekTime,
|
||||||
speed: this.speed,
|
speed: this.speed,
|
||||||
quality: this.quality,
|
quality: this.quality,
|
||||||
captions: controls.getLanguage.call(this),
|
captions: captions.getLabel.call(this),
|
||||||
// TODO: Looping
|
// TODO: Looping
|
||||||
// loop: 'None',
|
// loop: 'None',
|
||||||
});
|
});
|
||||||
@ -1188,12 +1242,12 @@ const controls = {
|
|||||||
// Inject controls HTML
|
// Inject controls HTML
|
||||||
if (utils.is.element(container)) {
|
if (utils.is.element(container)) {
|
||||||
target.appendChild(container);
|
target.appendChild(container);
|
||||||
} else {
|
} else if (container) {
|
||||||
target.insertAdjacentHTML('beforeend', container);
|
target.insertAdjacentHTML('beforeend', container);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the elements if need be
|
// Find the elements if need be
|
||||||
if (utils.is.element(this.elements.controls)) {
|
if (!utils.is.element(this.elements.controls)) {
|
||||||
utils.findElements.call(this);
|
utils.findElements.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1212,7 +1266,7 @@ const controls = {
|
|||||||
this.config.selectors.labels,
|
this.config.selectors.labels,
|
||||||
' .',
|
' .',
|
||||||
this.config.classNames.hidden,
|
this.config.classNames.hidden,
|
||||||
].join('')
|
].join(''),
|
||||||
);
|
);
|
||||||
|
|
||||||
Array.from(labels).forEach(label => {
|
Array.from(labels).forEach(label => {
|
||||||
|
@ -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.12/plyr.svg',
|
iconUrl: 'https://cdn.plyr.io/3.2.0/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,13 +115,14 @@ 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
|
||||||
fullscreen: {
|
fullscreen: {
|
||||||
enabled: true, // Allow fullscreen?
|
enabled: true, // Allow fullscreen?
|
||||||
fallback: true, // Fallback for vintage browsers
|
fallback: true, // Fallback for vintage browsers
|
||||||
|
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Local storage
|
// Local storage
|
||||||
@ -131,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',
|
||||||
@ -154,7 +160,7 @@ const defaults = {
|
|||||||
rewind: 'Rewind {seektime} secs',
|
rewind: 'Rewind {seektime} secs',
|
||||||
play: 'Play',
|
play: 'Play',
|
||||||
pause: 'Pause',
|
pause: 'Pause',
|
||||||
forward: 'Forward {seektime} secs',
|
fastForward: 'Forward {seektime} secs',
|
||||||
seek: 'Seek',
|
seek: 'Seek',
|
||||||
played: 'Played',
|
played: 'Played',
|
||||||
buffered: 'Buffered',
|
buffered: 'Buffered',
|
||||||
@ -171,15 +177,16 @@ 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
|
||||||
@ -202,7 +209,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,
|
||||||
@ -258,7 +265,7 @@ const defaults = {
|
|||||||
// Ads
|
// Ads
|
||||||
'adsloaded',
|
'adsloaded',
|
||||||
'adscontentpause',
|
'adscontentpause',
|
||||||
'adsconentresume',
|
'adscontentresume',
|
||||||
'adstarted',
|
'adstarted',
|
||||||
'adsmidpoint',
|
'adsmidpoint',
|
||||||
'adscomplete',
|
'adscomplete',
|
||||||
@ -282,7 +289,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"]',
|
||||||
@ -372,9 +379,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,127 +1,213 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Plyr fullscreen API
|
// Fullscreen wrapper
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
|
|
||||||
// Determine the prefix
|
const browser = utils.getBrowser();
|
||||||
const prefix = (() => {
|
|
||||||
let value = false;
|
|
||||||
|
|
||||||
if (utils.is.function(document.cancelFullScreen)) {
|
function onChange() {
|
||||||
value = '';
|
if (!this.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update toggle button
|
||||||
|
const button = this.player.elements.buttons.fullscreen;
|
||||||
|
if (utils.is.element(button)) {
|
||||||
|
utils.toggleState(button, this.active);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger an event
|
||||||
|
utils.dispatchEvent(this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
||||||
|
|
||||||
|
// Trap focus in container
|
||||||
|
if (!browser.isIos) {
|
||||||
|
utils.trapFocus.call(this.player, this.target, this.active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFallback(toggle = false) {
|
||||||
|
// Store or restore scroll position
|
||||||
|
if (toggle) {
|
||||||
|
this.scrollPosition = {
|
||||||
|
x: window.scrollX || 0,
|
||||||
|
y: window.scrollY || 0,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
|
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle scroll
|
||||||
|
document.body.style.overflow = toggle ? 'hidden' : '';
|
||||||
|
|
||||||
|
// Toggle class hook
|
||||||
|
utils.toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
|
||||||
|
|
||||||
|
// Toggle button and fire events
|
||||||
|
onChange.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fullscreen {
|
||||||
|
constructor(player) {
|
||||||
|
// Keep reference to parent
|
||||||
|
this.player = player;
|
||||||
|
|
||||||
|
// Get prefix
|
||||||
|
this.prefix = Fullscreen.prefix;
|
||||||
|
this.name = Fullscreen.name;
|
||||||
|
|
||||||
|
// Scroll position
|
||||||
|
this.scrollPosition = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
// Register event listeners
|
||||||
|
// Handle event (incase user presses escape etc)
|
||||||
|
utils.on(document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {
|
||||||
|
// TODO: Filter for target??
|
||||||
|
onChange.call(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fullscreen toggle on double click
|
||||||
|
utils.on(this.player.elements.container, 'dblclick', event => {
|
||||||
|
// Ignore double click in controls
|
||||||
|
if (this.player.elements.controls.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the UI
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if native supported
|
||||||
|
static get native() {
|
||||||
|
return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the prefix for handlers
|
||||||
|
static get prefix() {
|
||||||
|
// No prefix
|
||||||
|
if (utils.is.function(document.exitFullscreen)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for fullscreen support by vendor prefix
|
// Check for fullscreen support by vendor prefix
|
||||||
[
|
let value = '';
|
||||||
|
const prefixes = [
|
||||||
'webkit',
|
'webkit',
|
||||||
'o',
|
|
||||||
'moz',
|
'moz',
|
||||||
'ms',
|
'ms',
|
||||||
'khtml',
|
];
|
||||||
].some(pre => {
|
|
||||||
if (utils.is.function(document[`${pre}CancelFullScreen`])) {
|
prefixes.some(pre => {
|
||||||
|
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) && document.msFullscreenEnabled) {
|
|
||||||
// Special case for MS (when isn't it?)
|
|
||||||
value = 'ms';
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
})();
|
}
|
||||||
|
|
||||||
// Fullscreen API
|
static get name() {
|
||||||
const fullscreen = {
|
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
|
||||||
// Get the prefix
|
}
|
||||||
prefix,
|
|
||||||
|
|
||||||
// Check if we can use it
|
// Determine if fullscreen is enabled
|
||||||
enabled: document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled,
|
get enabled() {
|
||||||
|
return (
|
||||||
|
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
|
||||||
|
this.player.config.fullscreen.enabled &&
|
||||||
|
this.player.supported.ui &&
|
||||||
|
this.player.isVideo
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Yet again Microsoft awesomeness,
|
// Get active state
|
||||||
// Sometimes the prefix is 'ms', sometimes 'MS' to keep you on your toes
|
get active() {
|
||||||
eventType: prefix === 'ms' ? 'MSFullscreenChange' : `${prefix}fullscreenchange`,
|
if (!this.enabled) {
|
||||||
|
|
||||||
// Is an element fullscreen
|
|
||||||
isFullScreen(element) {
|
|
||||||
if (!fullscreen.enabled) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = utils.is.nullOrUndefined(element) ? document.body : element;
|
// Fallback using classname
|
||||||
|
if (!Fullscreen.native) {
|
||||||
switch (prefix) {
|
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
||||||
case '':
|
}
|
||||||
return document.fullscreenElement === target;
|
|
||||||
|
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.name}Element`];
|
||||||
case 'moz':
|
|
||||||
return document.mozFullScreenElement === target;
|
return element === this.target;
|
||||||
|
}
|
||||||
default:
|
|
||||||
return document[`${prefix}FullscreenElement`] === target;
|
// Get target element
|
||||||
|
get target() {
|
||||||
|
return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
update() {
|
||||||
|
if (this.enabled) {
|
||||||
|
this.player.debug.log(`${Fullscreen.native ? 'Native' : 'Fallback'} fullscreen enabled`);
|
||||||
|
} else {
|
||||||
|
this.player.debug.log('Fullscreen not supported and fallback disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add styling hook to show button
|
||||||
|
utils.toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
// Make an element fullscreen
|
// Make an element fullscreen
|
||||||
requestFullScreen(element) {
|
enter() {
|
||||||
if (!fullscreen.enabled) {
|
if (!this.enabled) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = utils.is.nullOrUndefined(element) ? document.body : element;
|
|
||||||
|
|
||||||
return !prefix.length ? target.requestFullScreen() : target[prefix + (prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')]();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Bail from fullscreen
|
|
||||||
cancelFullScreen() {
|
|
||||||
if (!fullscreen.enabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !prefix.length ? document.cancelFullScreen() : document[prefix + (prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')]();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get the current element
|
|
||||||
element() {
|
|
||||||
if (!fullscreen.enabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !prefix.length ? document.fullscreenElement : document[`${prefix}FullscreenElement`];
|
|
||||||
},
|
|
||||||
|
|
||||||
// Setup fullscreen
|
|
||||||
setup() {
|
|
||||||
if (!this.supported.ui || this.isAudio || !this.config.fullscreen.enabled) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for native support
|
// iOS native fullscreen doesn't need the request step
|
||||||
const nativeSupport = fullscreen.enabled;
|
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||||
|
if (this.player.playing) {
|
||||||
|
this.target.webkitEnterFullscreen();
|
||||||
|
}
|
||||||
|
} else if (!Fullscreen.native) {
|
||||||
|
toggleFallback.call(this, true);
|
||||||
|
} else if (!this.prefix) {
|
||||||
|
this.target.requestFullscreen();
|
||||||
|
} else if (!utils.is.empty(this.prefix)) {
|
||||||
|
this.target[`${this.prefix}Request${this.name}`]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (nativeSupport || (this.config.fullscreen.fallback && !utils.inFrame())) {
|
// Bail from fullscreen
|
||||||
this.debug.log(`${nativeSupport ? 'Native' : 'Fallback'} fullscreen enabled`);
|
exit() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add styling hook to show button
|
// iOS native fullscreen
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.fullscreen.enabled, true);
|
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||||
} else {
|
this.target.webkitExitFullscreen();
|
||||||
this.debug.log('Fullscreen not supported and fallback disabled');
|
this.player.play();
|
||||||
|
} else if (!Fullscreen.native) {
|
||||||
|
toggleFallback.call(this, false);
|
||||||
|
} else if (!this.prefix) {
|
||||||
|
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
||||||
|
} else if (!utils.is.empty(this.prefix)) {
|
||||||
|
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
|
||||||
|
document[`${this.prefix}${action}${this.name}`]();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle state
|
// Toggle state
|
||||||
if (this.elements.buttons && this.elements.buttons.fullscreen) {
|
toggle() {
|
||||||
utils.toggleState(this.elements.buttons.fullscreen, false);
|
if (!this.active) {
|
||||||
|
this.enter();
|
||||||
|
} else {
|
||||||
|
this.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trap focus in container
|
export default Fullscreen;
|
||||||
utils.trapFocus.call(this);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default fullscreen;
|
|
||||||
|
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,28 +2,28 @@
|
|||||||
// Plyr Event Listeners
|
// Plyr Event Listeners
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import support from './support';
|
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import controls from './controls';
|
import controls from './controls';
|
||||||
import fullscreen from './fullscreen';
|
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
|
|
||||||
// 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) {
|
||||||
@ -39,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
|
||||||
@ -74,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,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.toggleFullscreen();
|
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:
|
||||||
@ -171,110 +171,142 @@ 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 (!fullscreen.enabled && this.fullscreen.active && code === 27) {
|
if (!this.player.fullscreen.enabled && this.player.fullscreen.active && code === 27) {
|
||||||
this.toggleFullscreen();
|
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
|
// Toggle controls visibility based on mouse movement
|
||||||
if (this.config.hideControls) {
|
if (this.player.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, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => {
|
||||||
this.toggleControls(event);
|
this.player.toggleControls(event);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle user exiting fullscreen by escaping etc
|
|
||||||
if (fullscreen.enabled) {
|
|
||||||
utils.on(document, fullscreen.eventType, event => {
|
|
||||||
this.toggleFullscreen(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fullscreen toggle on double click
|
|
||||||
utils.on(this.elements.container, 'dblclick', event => {
|
|
||||||
this.toggleFullscreen(event);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
// 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', event => ui.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 loadedmetadata', event => ui.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.showPosterOnEnd) {
|
||||||
// Restart
|
// Restart
|
||||||
this.restart();
|
this.player.restart();
|
||||||
|
|
||||||
// Re-load media
|
// Re-load media
|
||||||
this.media.load();
|
this.player.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', event => ui.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 => ui.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', event => ui.checkPlaying.call(this.player, event));
|
||||||
|
|
||||||
// Loading
|
// Loading state
|
||||||
utils.on(this.media, 'stalled 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
|
// Check if media failed to load
|
||||||
// utils.on(this.media, 'play', event => ui.checkFailed.call(this, event));
|
// utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.player, event));
|
||||||
|
|
||||||
|
// If autoplay, then load advertisement if required
|
||||||
|
// 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)) {
|
||||||
@ -284,78 +316,81 @@ 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.media,
|
||||||
'contextmenu',
|
'contextmenu',
|
||||||
event => {
|
event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 => {
|
||||||
@ -363,193 +398,200 @@ 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.toggleFullscreen();
|
'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();
|
||||||
|
|
||||||
// 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', () => {
|
},
|
||||||
this.quality = event.target.value;
|
'language',
|
||||||
});
|
);
|
||||||
} else if (utils.matches(event.target, this.config.selectors.inputs.speed)) {
|
} else if (utils.matches(event.target, this.player.config.selectors.inputs.quality)) {
|
||||||
proxy(event, 'speed', () => {
|
proxy(
|
||||||
this.speed = parseFloat(event.target.value);
|
event,
|
||||||
});
|
() => {
|
||||||
|
this.player.quality = event.target.value;
|
||||||
|
},
|
||||||
|
'quality',
|
||||||
|
);
|
||||||
|
} else if (utils.matches(event.target, this.player.config.selectors.inputs.speed)) {
|
||||||
|
proxy(
|
||||||
|
event,
|
||||||
|
() => {
|
||||||
|
this.player.speed = parseFloat(event.target.value);
|
||||||
|
},
|
||||||
|
'speed',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
controls.showTab.call(this, event);
|
controls.showTab.call(this.player, event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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 => {
|
||||||
|
this.player.currentTime = event.target.value / event.target.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);
|
ui.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
|
// Toggle controls visibility based on mouse movement
|
||||||
if (this.config.hideControls) {
|
if (this.player.config.hideControls) {
|
||||||
// Watch for cursor over controls so they don't hide when trying to interact
|
// Watch for cursor over controls so they don't hide when trying to interact
|
||||||
utils.on(this.elements.controls, 'mouseenter mouseleave', event => {
|
on(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
||||||
this.elements.controls.hover = event.type === 'mouseenter';
|
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch for cursor over controls so they don't hide when trying to interact
|
// Watch for cursor over controls so they don't hide when trying to interact
|
||||||
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);
|
this.player.toggleControls(event);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
@ -559,10 +601,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -570,22 +612,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();
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
'volume',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default listeners;
|
// Reset on destroy
|
||||||
|
clear() {
|
||||||
|
this.global(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Listeners;
|
||||||
|
@ -6,6 +6,7 @@ import support from './support';
|
|||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import youtube from './plugins/youtube';
|
import youtube from './plugins/youtube';
|
||||||
import vimeo from './plugins/vimeo';
|
import vimeo from './plugins/vimeo';
|
||||||
|
import html5 from './html5';
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
|
|
||||||
// Sniff out the browser
|
// Sniff out the browser
|
||||||
@ -46,7 +47,7 @@ const media = {
|
|||||||
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
||||||
|
|
||||||
// Add touch class
|
// Add touch class
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, support.touch);
|
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject the player wrapper
|
// Inject the player wrapper
|
||||||
@ -75,32 +76,10 @@ const media = {
|
|||||||
}
|
}
|
||||||
} else if (this.isHTML5) {
|
} else if (this.isHTML5) {
|
||||||
ui.setTitle.call(this);
|
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
|
|
||||||
Array.from(this.media.querySelectorAll('source')).forEach(utils.removeElement);
|
|
||||||
|
|
||||||
// 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;
|
||||||
|
@ -7,22 +7,7 @@
|
|||||||
/* global google */
|
/* global google */
|
||||||
|
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
import i18n from '../i18n';
|
||||||
// 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 {
|
||||||
/**
|
/**
|
||||||
@ -32,39 +17,10 @@ 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.enabled = player.isHTML5 && player.isVideo && player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length;
|
||||||
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 +32,77 @@ 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.api)
|
||||||
|
.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 +115,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 +148,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
|
||||||
@ -161,8 +161,6 @@ class Ads {
|
|||||||
request.forceNonLinearFullSlot = false;
|
request.forceNonLinearFullSlot = false;
|
||||||
|
|
||||||
this.loader.requestAds(request);
|
this.loader.requestAds(request);
|
||||||
|
|
||||||
this.handleEventListeners('ADS_LOADER_LOADED');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.onAdError(e);
|
this.onAdError(e);
|
||||||
}
|
}
|
||||||
@ -174,25 +172,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 +200,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,6 +222,7 @@ class Ads {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Get skippable state
|
// Get skippable state
|
||||||
// TODO: Skip button
|
// TODO: Skip button
|
||||||
@ -241,7 +241,7 @@ class Ads {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Resolve our adsManager
|
// Resolve our adsManager
|
||||||
this.handleEventListeners('ADS_MANAGER_LOADED');
|
this.trigger('loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -259,17 +259,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 +288,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 +322,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 +334,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 +344,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 +362,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 +387,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 +402,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 +415,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 +440,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 +464,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 +487,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 +498,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 +507,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 +540,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 +560,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);
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import utils from './../utils';
|
import utils from './../utils';
|
||||||
import captions from './../captions';
|
import captions from './../captions';
|
||||||
|
import controls from './../controls';
|
||||||
import ui from './../ui';
|
import ui from './../ui';
|
||||||
|
|
||||||
const vimeo = {
|
const vimeo = {
|
||||||
@ -16,8 +17,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.api)
|
||||||
|
.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 +35,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
|
||||||
@ -49,6 +59,7 @@ const vimeo = {
|
|||||||
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);
|
||||||
|
|
||||||
@ -82,6 +93,11 @@ const vimeo = {
|
|||||||
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(() => {
|
player.embed.play().then(() => {
|
||||||
@ -96,10 +112,8 @@ const vimeo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
player.media.stop = () => {
|
player.media.stop = () => {
|
||||||
player.embed.stop().then(() => {
|
player.pause();
|
||||||
player.media.paused = true;
|
|
||||||
player.currentTime = 0;
|
player.currentTime = 0;
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Seeking
|
// Seeking
|
||||||
@ -136,9 +150,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 +212,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;
|
||||||
@ -311,7 +339,7 @@ const vimeo = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Rebuild UI
|
// Rebuild UI
|
||||||
window.setTimeout(() => ui.build.call(player), 0);
|
setTimeout(() => ui.build.call(player), 0);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,6 +6,64 @@ import utils from './../utils';
|
|||||||
import controls from './../controls';
|
import controls from './../controls';
|
||||||
import ui from './../ui';
|
import ui from './../ui';
|
||||||
|
|
||||||
|
// 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)));
|
||||||
|
}
|
||||||
|
|
||||||
const youtube = {
|
const youtube = {
|
||||||
setup() {
|
setup() {
|
||||||
// Add embed class for responsive
|
// Add embed class for responsive
|
||||||
@ -19,7 +77,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.api).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...
|
||||||
@ -166,14 +226,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
|
||||||
@ -194,17 +250,14 @@ 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 = () => {
|
||||||
instance.playVideo();
|
instance.playVideo();
|
||||||
player.media.paused = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
player.media.pause = () => {
|
player.media.pause = () => {
|
||||||
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,6 +270,9 @@ const youtube = {
|
|||||||
return Number(instance.getCurrentTime());
|
return Number(instance.getCurrentTime());
|
||||||
},
|
},
|
||||||
set(time) {
|
set(time) {
|
||||||
|
// Vimeo will automatically play on seek
|
||||||
|
const { paused } = player.media;
|
||||||
|
|
||||||
// Set seeking flag
|
// Set seeking flag
|
||||||
player.media.seeking = true;
|
player.media.seeking = true;
|
||||||
|
|
||||||
@ -225,6 +281,11 @@ const youtube = {
|
|||||||
|
|
||||||
// Seek after events sent
|
// Seek after events sent
|
||||||
instance.seekTo(time);
|
instance.seekTo(time);
|
||||||
|
|
||||||
|
// Restore pause state
|
||||||
|
if (paused) {
|
||||||
|
player.pause();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -241,15 +302,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 +370,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 +387,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 +395,14 @@ 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);
|
||||||
|
|
||||||
// Handle events
|
// Handle events
|
||||||
// -1 Unstarted
|
// -1 Unstarted
|
||||||
@ -348,6 +412,16 @@ 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;
|
player.media.paused = true;
|
||||||
|
|
||||||
@ -378,7 +452,7 @@ const youtube = {
|
|||||||
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,7 +465,7 @@ const youtube = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get quality
|
// Get quality
|
||||||
controls.setQualityMenu.call(player, instance.getAvailableQualityLevels());
|
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
339
src/js/plyr.js
339
src/js/plyr.js
@ -1,6 +1,6 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Plyr
|
// Plyr
|
||||||
// plyr.js v3.0.0-beta.12
|
// plyr.js v3.2.0
|
||||||
// https://github.com/sampotts/plyr
|
// https://github.com/sampotts/plyr
|
||||||
// License: The MIT License (MIT)
|
// License: The MIT License (MIT)
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@ -11,13 +11,13 @@ import support from './support';
|
|||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
|
|
||||||
import Console from './console';
|
import Console from './console';
|
||||||
|
import Fullscreen from './fullscreen';
|
||||||
|
import Listeners from './listeners';
|
||||||
import Storage from './storage';
|
import Storage from './storage';
|
||||||
import Ads from './plugins/ads';
|
import Ads from './plugins/ads';
|
||||||
|
|
||||||
import captions from './captions';
|
import captions from './captions';
|
||||||
import controls from './controls';
|
import controls from './controls';
|
||||||
import fullscreen from './fullscreen';
|
|
||||||
import listeners from './listeners';
|
|
||||||
import media from './media';
|
import media from './media';
|
||||||
import source from './source';
|
import source from './source';
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
@ -26,12 +26,6 @@ import ui from './ui';
|
|||||||
// TODO: Use a WeakMap for private globals
|
// TODO: Use a WeakMap for private globals
|
||||||
// const globals = new WeakMap();
|
// const globals = new WeakMap();
|
||||||
|
|
||||||
// Globals
|
|
||||||
let scrollPosition = {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Plyr instance
|
// Plyr instance
|
||||||
class Plyr {
|
class Plyr {
|
||||||
constructor(target, options) {
|
constructor(target, options) {
|
||||||
@ -42,6 +36,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;
|
||||||
|
|
||||||
@ -60,7 +57,7 @@ class Plyr {
|
|||||||
this.config = utils.extend(
|
this.config = utils.extend(
|
||||||
{},
|
{},
|
||||||
defaults,
|
defaults,
|
||||||
options,
|
options || {},
|
||||||
(() => {
|
(() => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(this.media.getAttribute('data-plyr-config'));
|
return JSON.parse(this.media.getAttribute('data-plyr-config'));
|
||||||
@ -100,6 +97,7 @@ class Plyr {
|
|||||||
this.options = {
|
this.options = {
|
||||||
speed: [],
|
speed: [],
|
||||||
quality: [],
|
quality: [],
|
||||||
|
captions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debugging
|
// Debugging
|
||||||
@ -136,7 +134,17 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache original element state for .destroy()
|
// Cache original element state for .destroy()
|
||||||
this.elements.original = this.media.cloneNode(true);
|
// TODO: Investigate a better solution as I suspect this causes reported double load issues?
|
||||||
|
setTimeout(() => {
|
||||||
|
const clone = this.media.cloneNode(true);
|
||||||
|
|
||||||
|
// Prevent the clone autoplaying
|
||||||
|
if (clone.getAttribute('autoplay')) {
|
||||||
|
clone.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.elements.original = clone;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
// 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
|
||||||
@ -177,12 +185,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
|
||||||
@ -216,7 +229,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;
|
||||||
@ -232,11 +245,8 @@ class Plyr {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup local storage for user settings
|
|
||||||
this.storage = new Storage(this);
|
|
||||||
|
|
||||||
// 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) {
|
||||||
@ -244,6 +254,12 @@ class Plyr {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create listeners
|
||||||
|
this.listeners = new Listeners(this);
|
||||||
|
|
||||||
|
// Setup local storage for user settings
|
||||||
|
this.storage = new Storage(this);
|
||||||
|
|
||||||
// Store reference
|
// Store reference
|
||||||
this.media.plyr = this;
|
this.media.plyr = this;
|
||||||
|
|
||||||
@ -256,9 +272,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);
|
||||||
|
|
||||||
@ -278,8 +291,22 @@ class Plyr {
|
|||||||
ui.build.call(this);
|
ui.build.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Container listeners
|
||||||
|
this.listeners.container();
|
||||||
|
|
||||||
|
// Global listeners
|
||||||
|
this.listeners.global();
|
||||||
|
|
||||||
|
// Setup fullscreen
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
@ -290,34 +317,37 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If ads are enabled, wait for them first
|
||||||
|
/* if (this.ads.enabled && !this.ads.initialized) {
|
||||||
|
return this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play());
|
||||||
|
} */
|
||||||
|
|
||||||
// Return the promise (for HTML5)
|
// Return the promise (for HTML5)
|
||||||
return this.media.play();
|
return this.media.play();
|
||||||
}
|
}
|
||||||
@ -326,7 +356,7 @@ class Plyr {
|
|||||||
* Pause the media
|
* Pause the media
|
||||||
*/
|
*/
|
||||||
pause() {
|
pause() {
|
||||||
if (!this.playing) {
|
if (!this.playing || !utils.is.function(this.media.pause)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,21 +367,21 @@ class Plyr {
|
|||||||
* Get paused state
|
* Get paused state
|
||||||
*/
|
*/
|
||||||
get paused() {
|
get paused() {
|
||||||
return this.media.paused;
|
return Boolean(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 && (this.isHTML5 ? this.media.readyState > 2 : true));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get ended state
|
* Get ended state
|
||||||
*/
|
*/
|
||||||
get ended() {
|
get ended() {
|
||||||
return this.media.ended;
|
return Boolean(this.media.ended);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -373,8 +403,11 @@ class Plyr {
|
|||||||
* Stop playback
|
* Stop playback
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop() {
|
||||||
this.restart();
|
if (this.isHTML5) {
|
||||||
this.pause();
|
this.media.load();
|
||||||
|
} else if (utils.is.function(this.media.stop)) {
|
||||||
|
this.media.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -419,7 +452,7 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set
|
// Set
|
||||||
this.media.currentTime = targetTime.toFixed(4);
|
this.media.currentTime = parseFloat(targetTime.toFixed(4));
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
this.debug.log(`Seeking to ${this.currentTime} seconds`);
|
this.debug.log(`Seeking to ${this.currentTime} seconds`);
|
||||||
@ -432,11 +465,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -447,7 +501,7 @@ class Plyr {
|
|||||||
const fauxDuration = parseInt(this.config.duration, 10);
|
const fauxDuration = parseInt(this.config.duration, 10);
|
||||||
|
|
||||||
// True duration
|
// True duration
|
||||||
const realDuration = Number(this.media.duration);
|
const realDuration = this.media ? Number(this.media.duration) : 0;
|
||||||
|
|
||||||
// If custom duration is funky, use regular duration
|
// If custom duration is funky, use regular duration
|
||||||
return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration;
|
return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration;
|
||||||
@ -491,8 +545,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -501,7 +555,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -550,7 +604,7 @@ class Plyr {
|
|||||||
* Get current muted state
|
* Get current muted state
|
||||||
*/
|
*/
|
||||||
get muted() {
|
get muted() {
|
||||||
return this.media.muted;
|
return Boolean(this.media.muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -567,12 +621,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;
|
||||||
@ -613,34 +671,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;
|
||||||
|
|
||||||
@ -713,7 +780,7 @@ class Plyr {
|
|||||||
* Get current loop state
|
* Get current loop state
|
||||||
*/
|
*/
|
||||||
get loop() {
|
get loop() {
|
||||||
return this.media.loop;
|
return Boolean(this.media.loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -770,7 +837,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -784,7 +851,7 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
@ -814,17 +881,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;
|
||||||
@ -850,62 +929,6 @@ class Plyr {
|
|||||||
return this.captions.language;
|
return this.captions.language;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle fullscreen playback
|
|
||||||
* Requires user input event
|
|
||||||
* @param {event} event
|
|
||||||
*/
|
|
||||||
toggleFullscreen(event) {
|
|
||||||
// Video only
|
|
||||||
if (this.isAudio) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for native support
|
|
||||||
if (fullscreen.enabled) {
|
|
||||||
if (utils.is.event(event) && event.type === fullscreen.eventType) {
|
|
||||||
// If it's a fullscreen change event, update the state
|
|
||||||
this.fullscreen.active = fullscreen.isFullScreen(this.elements.container);
|
|
||||||
} else {
|
|
||||||
// Else it's a user request to enter or exit
|
|
||||||
if (!this.fullscreen.active) {
|
|
||||||
fullscreen.requestFullScreen(this.elements.container);
|
|
||||||
} else {
|
|
||||||
fullscreen.cancelFullScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Otherwise, it's a simple toggle
|
|
||||||
this.fullscreen.active = !this.fullscreen.active;
|
|
||||||
|
|
||||||
// Add class hook
|
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.fullscreen.fallback, this.fullscreen.active);
|
|
||||||
|
|
||||||
// Make sure we don't lose scroll position
|
|
||||||
if (this.fullscreen.active) {
|
|
||||||
scrollPosition = {
|
|
||||||
x: window.pageXOffset || 0,
|
|
||||||
y: window.pageYOffset || 0,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
window.scrollTo(scrollPosition.x, scrollPosition.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind/unbind escape key
|
|
||||||
document.body.style.overflow = this.fullscreen.active ? 'hidden' : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set button state
|
|
||||||
if (utils.is.element(this.elements.buttons.fullscreen)) {
|
|
||||||
utils.toggleState(this.elements.buttons.fullscreen, this.fullscreen.active);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger an event
|
|
||||||
utils.dispatchEvent.call(this, this.media, this.fullscreen.active ? 'enterfullscreen' : 'exitfullscreen');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle picture-in-picture playback on WebKit/MacOS
|
* Toggle picture-in-picture playback on WebKit/MacOS
|
||||||
* TODO: update player with state, support, enabled
|
* TODO: update player with state, support, enabled
|
||||||
@ -976,26 +999,32 @@ class Plyr {
|
|||||||
// Is the enter fullscreen event
|
// Is the enter fullscreen event
|
||||||
isEnterFullscreen = toggle.type === 'enterfullscreen';
|
isEnterFullscreen = toggle.type === 'enterfullscreen';
|
||||||
|
|
||||||
// Whether to show controls
|
// Events that show the controls
|
||||||
show = [
|
const showEvents = [
|
||||||
'mouseenter',
|
|
||||||
'mousemove',
|
|
||||||
'touchstart',
|
'touchstart',
|
||||||
'touchmove',
|
'touchmove',
|
||||||
'focusin',
|
'mouseenter',
|
||||||
].includes(toggle.type);
|
|
||||||
|
|
||||||
// Delay hiding on move events
|
|
||||||
if ([
|
|
||||||
'mousemove',
|
'mousemove',
|
||||||
|
'focusin',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Events that delay hiding
|
||||||
|
const delayEvents = [
|
||||||
'touchmove',
|
'touchmove',
|
||||||
'touchend',
|
'touchend',
|
||||||
].includes(toggle.type)) {
|
'mousemove',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Whether to show controls
|
||||||
|
show = showEvents.includes(toggle.type);
|
||||||
|
|
||||||
|
// Delay hiding on move events
|
||||||
|
if (delayEvents.includes(toggle.type)) {
|
||||||
delay = 2000;
|
delay = 2000;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delay a little more for keyboard users
|
// Delay a little more for keyboard users
|
||||||
if (toggle.type === 'focusin') {
|
if (!this.touch && toggle.type === 'focusin') {
|
||||||
delay = 3000;
|
delay = 3000;
|
||||||
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
|
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
|
||||||
}
|
}
|
||||||
@ -1005,7 +1034,7 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear timer on every call
|
// Clear timer on every call
|
||||||
window.clearTimeout(this.timers.controls);
|
clearTimeout(this.timers.controls);
|
||||||
|
|
||||||
// If the mouse is not over the controls, set a timeout to hide them
|
// If the mouse is not over the controls, set a timeout to hide them
|
||||||
if (show || this.paused || this.loading) {
|
if (show || this.paused || this.loading) {
|
||||||
@ -1023,7 +1052,7 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delay for hiding on touch
|
// Delay for hiding on touch
|
||||||
if (support.touch) {
|
if (this.touch) {
|
||||||
delay = 3000;
|
delay = 3000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1031,14 +1060,11 @@ class Plyr {
|
|||||||
// If toggle is false or if we're playing (regardless of toggle),
|
// If toggle is false or if we're playing (regardless of toggle),
|
||||||
// then set the timer to hide the controls
|
// then set the timer to hide the controls
|
||||||
if (!show || this.playing) {
|
if (!show || this.playing) {
|
||||||
this.timers.controls = window.setTimeout(() => {
|
this.timers.controls = setTimeout(() => {
|
||||||
/* this.debug.warn({
|
// We need controls of course...
|
||||||
pressed: this.elements.controls.pressed,
|
if (!utils.is.element(this.elements.controls)) {
|
||||||
hover: this.elements.controls.pressed,
|
return;
|
||||||
playing: this.playing,
|
}
|
||||||
paused: this.paused,
|
|
||||||
loading: this.loading,
|
|
||||||
}); */
|
|
||||||
|
|
||||||
// If the mouse is over the controls (and not entering fullscreen), bail
|
// If the mouse is over the controls (and not entering fullscreen), bail
|
||||||
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
|
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
|
||||||
@ -1091,6 +1117,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 = '';
|
||||||
@ -1101,12 +1131,8 @@ class Plyr {
|
|||||||
// If it's a soft destroy, make minimal changes
|
// If it's a soft destroy, make minimal changes
|
||||||
if (soft) {
|
if (soft) {
|
||||||
if (Object.keys(this.elements).length) {
|
if (Object.keys(this.elements).length) {
|
||||||
// Remove buttons
|
// Remove elements
|
||||||
if (this.elements.buttons && this.elements.buttons.play) {
|
utils.removeElement(this.elements.buttons.play);
|
||||||
Array.from(this.elements.buttons.play).forEach(button => utils.removeElement(button));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove others
|
|
||||||
utils.removeElement(this.elements.captions);
|
utils.removeElement(this.elements.captions);
|
||||||
utils.removeElement(this.elements.controls);
|
utils.removeElement(this.elements.controls);
|
||||||
utils.removeElement(this.elements.wrapper);
|
utils.removeElement(this.elements.wrapper);
|
||||||
@ -1123,6 +1149,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);
|
||||||
|
|
||||||
@ -1134,15 +1163,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);
|
||||||
|
|
||||||
@ -1153,11 +1194,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1174,7 +1215,7 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vimeo does not always return
|
// Vimeo does not always return
|
||||||
window.setTimeout(done, 200);
|
setTimeout(done, 200);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
14
src/js/plyr.polyfilled.js
Normal file
14
src/js/plyr.polyfilled.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Plyr Polyfilled Build
|
||||||
|
// plyr.js v3.2.0
|
||||||
|
// https://github.com/sampotts/plyr
|
||||||
|
// License: The MIT License (MIT)
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import 'babel-polyfill';
|
||||||
|
|
||||||
|
import 'custom-event-polyfill';
|
||||||
|
|
||||||
|
import Plyr from './plyr';
|
||||||
|
|
||||||
|
export default Plyr;
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import { providers } from './types';
|
import { providers } from './types';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
|
import html5 from './html5';
|
||||||
import media from './media';
|
import media from './media';
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
import support from './support';
|
import support from './support';
|
||||||
@ -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}`) {
|
||||||
@ -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', '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,6 +138,9 @@ const source = {
|
|||||||
// Setup interface
|
// Setup interface
|
||||||
ui.build.call(this);
|
ui.build.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the fullscreen support
|
||||||
|
this.fullscreen.update();
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
@ -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) {
|
||||||
|
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) {
|
||||||
@ -147,7 +148,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
|
||||||
|
53
src/js/ui.js
53
src/js/ui.js
@ -5,8 +5,7 @@
|
|||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import captions from './captions';
|
import captions from './captions';
|
||||||
import controls from './controls';
|
import controls from './controls';
|
||||||
import fullscreen from './fullscreen';
|
import i18n from './i18n';
|
||||||
import listeners from './listeners';
|
|
||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
addStyleHook() {
|
addStyleHook() {
|
||||||
@ -26,19 +25,13 @@ 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) {
|
||||||
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
|
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
|
||||||
|
|
||||||
// Remove controls
|
|
||||||
utils.removeElement.call(this, 'controls');
|
|
||||||
|
|
||||||
// Remove large play
|
|
||||||
utils.removeElement.call(this, 'buttons.play');
|
|
||||||
|
|
||||||
// Restore native controls
|
// Restore native controls
|
||||||
ui.toggleNativeControls.call(this, true);
|
ui.toggleNativeControls.call(this, true);
|
||||||
|
|
||||||
@ -52,7 +45,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 there's no controls, bail
|
||||||
@ -63,9 +56,6 @@ const ui = {
|
|||||||
// Remove native controls
|
// Remove native controls
|
||||||
ui.toggleNativeControls.call(this);
|
ui.toggleNativeControls.call(this);
|
||||||
|
|
||||||
// Setup fullscreen
|
|
||||||
fullscreen.setup.call(this);
|
|
||||||
|
|
||||||
// Captions
|
// Captions
|
||||||
captions.setup.call(this);
|
captions.setup.call(this);
|
||||||
|
|
||||||
@ -81,8 +71,11 @@ 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
|
||||||
|
ui.updateVolume.call(this);
|
||||||
|
|
||||||
// Reset time display
|
// Reset time display
|
||||||
ui.timeUpdate.call(this);
|
ui.timeUpdate.call(this);
|
||||||
@ -94,7 +87,9 @@ const ui = {
|
|||||||
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);
|
||||||
@ -103,7 +98,7 @@ const ui = {
|
|||||||
// 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)) {
|
||||||
@ -132,7 +127,7 @@ 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';
|
||||||
|
|
||||||
iframe.setAttribute('title', this.config.i18n.frameTitle.replace('{title}', title));
|
iframe.setAttribute('title', i18n.get('frameTitle', this.config));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -142,10 +137,8 @@ const ui = {
|
|||||||
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.stopped, this.paused);
|
||||||
|
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle controls
|
// Toggle controls
|
||||||
this.toggleControls(!this.playing);
|
this.toggleControls(!this.playing);
|
||||||
@ -266,21 +259,7 @@ const ui = {
|
|||||||
// Check buffer status
|
// Check buffer status
|
||||||
case 'playing':
|
case 'playing':
|
||||||
case 'progress':
|
case 'progress':
|
||||||
value = (() => {
|
ui.setProgress.call(this, this.elements.display.buffer, this.buffered * 100);
|
||||||
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;
|
break;
|
||||||
|
|
||||||
|
226
src/js/utils.js
226
src/js/utils.js
@ -2,6 +2,8 @@
|
|||||||
// Plyr utils
|
// Plyr utils
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
|
import loadjs from 'loadjs';
|
||||||
|
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import { providers } from './types';
|
import { providers } from './types';
|
||||||
|
|
||||||
@ -83,7 +85,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 +96,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 +112,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);
|
||||||
@ -114,55 +124,13 @@ const utils = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 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 +143,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 +158,8 @@ 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()) {
|
||||||
// Create container
|
// Create container
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
utils.toggleHidden(container, true);
|
utils.toggleHidden(container, true);
|
||||||
@ -200,7 +175,7 @@ const utils = {
|
|||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -222,7 +197,7 @@ const utils = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSprite.call(container, result);
|
injectSprite.call(container, result);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
@ -233,15 +208,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.
|
||||||
@ -306,12 +272,15 @@ const utils = {
|
|||||||
// Remove an element
|
// Remove an element
|
||||||
removeElement(element) {
|
removeElement(element) {
|
||||||
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
|
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
|
||||||
return null;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utils.is.nodeList(element) || utils.is.array(element)) {
|
||||||
|
Array.from(element).forEach(utils.removeElement);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
element.parentNode.removeChild(element);
|
element.parentNode.removeChild(element);
|
||||||
|
|
||||||
return element;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Remove all child elements
|
// Remove all child elements
|
||||||
@ -341,8 +310,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);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -469,7 +441,7 @@ const utils = {
|
|||||||
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
|
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
|
||||||
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
|
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
|
||||||
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
|
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
|
||||||
forward: utils.getElement.call(this, this.config.selectors.buttons.forward),
|
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
|
||||||
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
|
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
|
||||||
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
|
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
|
||||||
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
|
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
|
||||||
@ -525,17 +497,18 @@ const utils = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Trap focus inside container
|
// Trap focus inside container
|
||||||
trapFocus() {
|
trapFocus(element = null, toggle = false) {
|
||||||
|
if (!utils.is.element(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
|
const focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
|
||||||
const first = focusable[0];
|
const first = focusable[0];
|
||||||
const last = focusable[focusable.length - 1];
|
const last = focusable[focusable.length - 1];
|
||||||
|
|
||||||
utils.on(
|
const trap = event => {
|
||||||
this.elements.container,
|
|
||||||
'keydown',
|
|
||||||
event => {
|
|
||||||
// Bail if not tab key or not fullscreen
|
// Bail if not tab key or not fullscreen
|
||||||
if (event.key !== 'Tab' || event.keyCode !== 9 || !this.fullscreen.active) {
|
if (event.key !== 'Tab' || event.keyCode !== 9) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -551,20 +524,24 @@ const utils = {
|
|||||||
last.focus();
|
last.focus();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
false,
|
|
||||||
);
|
if (toggle) {
|
||||||
|
utils.on(this.elements.container, 'keydown', trap, false);
|
||||||
|
} else {
|
||||||
|
utils.off(this.elements.container, 'keydown', trap, false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 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 elements
|
// Bail if no elemetns, event, or callback
|
||||||
if (utils.is.nullOrUndefined(elements)) {
|
if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a nodelist is passed, call itself on each node
|
// If a nodelist is passed, call itself on each node
|
||||||
if (utils.is.nodeList(elements)) {
|
if (utils.is.nodeList(elements) || utils.is.array(elements)) {
|
||||||
// Create listener for each node
|
// Create listener for each node
|
||||||
Array.from(elements).forEach(element => {
|
Array.from(elements).forEach(element => {
|
||||||
if (element instanceof Node) {
|
if (element instanceof Node) {
|
||||||
@ -579,16 +556,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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -599,25 +576,25 @@ 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: utils.is.plyr(this) ? this : null,
|
||||||
}),
|
}),
|
||||||
@ -630,6 +607,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;
|
||||||
@ -648,6 +631,7 @@ const utils = {
|
|||||||
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);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -688,6 +672,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) {
|
||||||
@ -715,6 +737,24 @@ 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);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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
|
||||||
@ -831,7 +871,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,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
// Ignore focus
|
// Ignore focus
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
// --------------------------------------------------------------
|
// --------------------------------------------------------------
|
||||||
|
|
||||||
.plyr__badge {
|
.plyr__badge {
|
||||||
background: $plyr-menu-color;
|
background: $plyr-badge-bg;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
color: $plyr-menu-bg;
|
color: $plyr-badge-color;
|
||||||
font-size: $plyr-font-size-badge;
|
font-size: $plyr-font-size-badge;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 3px 4px;
|
padding: 3px 4px;
|
||||||
|
@ -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
|
||||||
$padding: ((100 / 16) * 9);
|
$embed-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,6 +20,17 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the full custom UI is supported
|
||||||
|
.plyr--full-ui .plyr__video-embed {
|
||||||
|
$height: 240;
|
||||||
|
$offset: to-percentage(($height - $embed-padding) / ($height / 50));
|
||||||
|
|
||||||
|
// To allow mouse events to be captured if full support
|
||||||
|
iframe {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
// Vimeo hack
|
// Vimeo hack
|
||||||
> div {
|
> div {
|
||||||
@ -30,7 +39,3 @@
|
|||||||
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;
|
||||||
@ -59,6 +59,14 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: $plyr-control-padding;
|
padding: $plyr-control-padding;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options
|
// Options
|
||||||
@ -66,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%;
|
||||||
@ -76,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 {
|
||||||
@ -100,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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
@import 'settings/cosmetics';
|
@import 'settings/cosmetics';
|
||||||
@import 'settings/type';
|
@import 'settings/type';
|
||||||
|
|
||||||
|
@import 'settings/badges';
|
||||||
@import 'settings/captions';
|
@import 'settings/captions';
|
||||||
@import 'settings/controls';
|
@import 'settings/controls';
|
||||||
@import 'settings/helpers';
|
@import 'settings/helpers';
|
||||||
|
6
src/sass/settings/badges.scss
Normal file
6
src/sass/settings/badges.scss
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Badges
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
$plyr-badge-bg: $plyr-color-fiord !default;
|
||||||
|
$plyr-badge-color: #fff !default;
|
@ -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;
|
||||||
|
Reference in New Issue
Block a user