Compare commits

...

70 Commits

Author SHA1 Message Date
Sam Potts 91a4b86860 Small bug fixes 2018-05-06 01:03:38 +10:00
Sam Potts 5aece6fa06 Merge 2018-05-06 00:50:02 +10:00
Sam Potts a70b94afe2 Merge branch 'master' of github.com:sampotts/plyr 2018-05-06 00:49:22 +10:00
Sam Potts 9ebc2719d3 v3.3.0 2018-05-06 00:49:12 +10:00
Sam Potts b46aae1833 Merge pull request #939 from Billybobbonnet/patch-1
Added 480p to SD labels
2018-05-03 20:28:03 +10:00
Antoine Cordelois 30e6a40865 Added 480p to SD labels 2018-05-03 11:42:09 +02:00
Sam Potts 5ca769807e Merge pull request #923 from friday/922
Only add hideControls class if config.hideControls is truthy
2018-04-27 20:07:18 +10:00
Sam Potts 72a71a605b Fix for default timestamp 2018-04-27 20:06:14 +10:00
Sam Potts 261cd086c7 Update readme.md 2018-04-27 12:44:58 +10:00
Albin Larsson 9e19b526b9 Only add hideControls class if config.hideControls is truthy 2018-04-26 17:51:14 +02:00
Sam Potts 6c617a0ef1 Readme fix 2018-04-27 01:12:04 +10:00
Sam Potts a812650fea v3.2.4 2018-04-27 00:47:51 +10:00
Sam Potts fec7a77d6f v3.2.3 2018-04-25 20:02:36 +10:00
Sam Potts 971e261067 Fix for iOS 9 throwing error for name property in fullscreen API (fixes #908) 2018-04-25 19:59:22 +10:00
Sam Potts 27407ba021 v3.2.2 2018-04-25 19:46:39 +10:00
Sam Potts ef8e58ede4 Fix for hidden buffer and incorrect use of aria-hidden 2018-04-25 19:40:23 +10:00
Sam Potts f1b275aedc v3.2.1 2018-04-23 00:53:54 +10:00
Sam Potts b647af256c More a11y stuff and context menu fix 2018-04-23 00:01:19 +10:00
Sam Potts d2e9ed3467 Merge 2018-04-18 18:34:59 +10:00
Sam Potts 5b39986835 Merge branch 'master' of github.com:sampotts/plyr 2018-04-18 18:29:50 +10:00
Sam Potts a97b08e8ea ARIA and Vimeo fixes 2018-04-18 18:29:43 +10:00
Sam Potts 56d1be9447 Merge pull request #903 from friday/901
Show captions even if toggle button is omitted from controls
2018-04-18 08:49:05 +10:00
Sam Potts a241cb5215 Merge pull request #904 from friday/881
Fullscreen aria-pressed event listened fix for Chrome
2018-04-18 08:48:08 +10:00
Albin Larsson 042b1a8294 Fullscreen aria-pressed event listened fix for Chrome 2018-04-17 20:28:47 +02:00
Albin Larsson 6d79b8cd4c Don't require captions toggle button to be enabled in order to show captions 2018-04-17 18:59:19 +02:00
Sam Potts 88d766aeae v3.2.0 2018-04-17 23:54:38 +10:00
Sam Potts 119b471b84 More bug fixes 2018-04-17 23:51:23 +10:00
Sam Potts 7f079e0ec3 Fix for playing false positive (fixes #898) 2018-04-17 22:52:46 +10:00
Sam Potts 46fe3eecff Fixed bug for captions with no srclang and labels and improved logic (fixes #875) 2018-04-17 22:49:28 +10:00
Sam Potts 3061a701d5 PR merge 2018-04-14 14:58:09 +10:00
Sam Potts e45109e1d7 Merge branch 'master' of github.com:sampotts/plyr 2018-04-14 14:48:20 +10:00
Sam Potts e138e6d51e Merge pull request #895 from nicolasthy/patch-1
Fix IE10 split error
2018-04-14 14:47:57 +10:00
Nicolas Thiry aef1363b04 Fix IE10 with default captions.language 2018-04-13 14:44:05 +02:00
Nicolas Thiry 766dd03d81 Fix IE10 split error
On IE10, Plyr throws the error `Unable to get property 'split' of undefined or null reference`. This fixes the case when `window.navigator.language` is null and can't use the `split()` function.
2018-04-12 22:12:12 +02:00
Sam Potts ab393651ec Merge branch 'master' of github.com:sampotts/plyr 2018-04-11 23:44:44 +10:00
Sam Potts ffd265d0ae Merge pull request #888 from Antonio-Laguna/master
Safer check for active caption
2018-04-11 23:42:40 +10:00
Antonio Laguna 72155472dd Safer check for active caption 2018-04-11 15:39:12 +02:00
Sam Potts 9b7170834e Merge pull request #887 from danielsarin/use-i18n-for-normal-speed
Add i18n support for "Normal" value in speed options
2018-04-11 22:43:47 +10:00
Daniel Sarin 3e57a87bf7 Add i18n support for "Normal" value in speed options 2018-04-11 15:39:23 +03:00
Sam Potts a15d1c9f1c Merge pull request #886 from danielsarin/increate-menu-z-index
Increase menu container z-index to be higher than controls
2018-04-11 22:23:46 +10:00
Daniel Sarin a095a64f90 Increase menu container z-index to be higher than controls 2018-04-11 15:13:34 +03:00
Sam Potts 2374d6b1c4 Merge branch 'master' of github.com:sampotts/plyr 2018-04-11 21:52:36 +10:00
Sam Potts 5ed3ff9084 Restore paused state after seek 2018-04-11 21:52:31 +10:00
Sam Potts 385be55510 Merge pull request #874 from friday/873
Fixes issue leaving fullscreen in Chrome using button
2018-04-10 17:21:15 +10:00
Albin Larsson 3082d0d128 Fixes #873 Can't leave fullscreen in Chrome (using button) 2018-04-05 20:29:01 +02:00
Sam Potts f7e242f054 Merge pull request #871 from friday/867
Fix #867: Add custom property fallback
2018-04-05 09:19:46 +10:00
Sam Potts 2874505004 Merge pull request #868 from friday/null-no-controls
Fix string "null" being appended after the video if controls argument is empty.
2018-04-05 09:19:05 +10:00
Albin Larsson ed9e0c13d7 Fix #867: Add custom property fallback 2018-04-05 00:33:09 +02:00
Albin Larsson 10be94fa99 Fix 'null' being appended after the video if controls is empty array 2018-04-04 21:33:14 +02:00
Sam Potts ee79c46145 Merge branch 'master' of github.com:sampotts/plyr 2018-04-04 16:50:06 +10:00
Sam Potts 384010a2c0 Style fixes 2018-04-04 16:50:00 +10:00
Sam Potts 1e47019122 Merge pull request #863 from friday/data-plyr-config-no-options
Fix loading data-plyr-config when initiating Plyr without any options
2018-04-04 11:33:30 +10:00
Albin Larsson 536c65e82c Fix loading data-plyr-config when initiating Plyr without any options 2018-04-04 03:16:25 +02:00
Sam Potts cdf14932ec Changelog 2018-04-03 23:03:16 +10:00
Sam Potts 3b20dbd9fd v3.1.0 2018-04-03 22:57:34 +10:00
Sam Potts e4d975af00 Styling fixes 2018-04-03 22:56:19 +10:00
Sam Potts 2782a00e7c v3.1.0-beta.2 2018-04-03 22:31:55 +10:00
Sam Potts 91d192dd7c YouTube speed menu fix 2018-04-03 22:30:29 +10:00
Sam Potts b1e3abc795 v3.1.0-beta.1 2018-04-02 22:52:02 +10:00
Sam Potts 3395e8df90 HTML5 quality selection 2018-04-02 22:40:03 +10:00
Sam Potts cce143a7da v3.0.11 2018-03-30 23:14:07 +11:00
Sam Potts d593005b32 Muted and autoplay fixes, small bug fixes 2018-03-30 23:09:17 +11:00
Sam Potts 7be9d5d4d3 v3.0.10 2018-03-30 00:16:42 +11:00
Sam Potts d7141d5ed7 Controls docs, package upgrades 2018-03-30 00:11:48 +11:00
Sam Potts 0d0ece94d3 Fix regression 2018-03-29 20:20:37 +11:00
Sam Potts 1c06f6d06d Vimeo hotfix 2018-03-29 19:35:02 +11:00
Sam Potts dda8e30b92 Merge branch 'master' of github.com:sampotts/plyr 2018-03-28 22:45:18 +11:00
Sam Potts c4e2e24643 Bug fixes 2018-03-28 22:45:11 +11:00
Sam Potts e020a105a3 Update readme.md 2018-03-28 00:55:55 +11:00
Sam Potts 2b7fe9a4f9 v3.0.6 2018-03-28 00:17:15 +11:00
56 changed files with 8742 additions and 5755 deletions
+1
View File
@@ -9,6 +9,7 @@
"ignore": ["attribute", "class"]
}
],
"string-no-newline": null,
"indentation": 4,
"string-quotes": "single",
"max-nesting-depth": 2,
+15
View 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}"
}
]
}
+1 -1
View File
@@ -11,7 +11,7 @@
"demo": {
"sass": {
"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": {
"demo.js": "demo/src/js/demo.js"
+108 -3
View File
@@ -1,4 +1,109 @@
# v3.0.3
# v3.3.2
* Fix for ads running in audio
* Fix for setting poster on source change
## v3.3.0
* Now using a custom poster image element to hide the YouTube play button and give more control over when the poster image shows
* Renamed `showPosterOnEnd` to `resetOnEnd` as it makes more sense and now works for all players and does not reload media
* Fix for same domain SVG URLs (raised by Jochem in Slack)
* [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/Window/URL) is polyfill now required
* Added pause className (fixes #941)
* Button height set in CSS (auto) (fixes #928)
* Don't autoplay cloned original media (fixes #936)
* Return to the home menu pane after selecting an option
## v3.2.4
* Fix issue wher player never reports as ready if controls is empty array
* Fix issue where screen reader labels were removed from time displays
* Fix issue where custom controls placeholders were not populated
* Custom controls HTML example updated
* Fix for aria-label being set to the initial state on toggle buttons, overriding the inner labels
* Fix for hidden mute button on iOS (not functional for Vimeo due to API limitations) (fixes #656)
## v3.2.3
* Fix for iOS 9 throwing error for `name` property in fullscreen API (fixes #908)
## v3.2.2
* Fix for regression in 3.2.1 resulting in hidden buffer display (fixes #920)
* Cleaned up incorrect use of `aria-hidden` attribute
## v3.2.1
* Accessibility improvements for the controls (part of #905 fixes)
* Fix for context menu showing on YouTube (thanks Anthony Recenello in Slack)
* Vimeo fix for their API not returning the right duration until playback begins (fixes #891)
## v3.2.0
* Fullscreen fixes (thanks @friday)
* Menu fix for if speed not in config
* Menu z-index fix (thanks @danielsarin)
* i18n fix for missing "Normal" string (thanks @danielsarin)
* Safer check for active caption (thanks @Antonio-Laguna)
* Add custom property fallback (thanks @friday)
* Fixed bug for captions with no srclang and labels and improved logic (fixes #875)
* Fix for `playing` false positive (fixes #898)
* Fix for IE issue with navigator.language (thanks @nicolasthy) (fixes #893)
* Fix for Vimeo controls missing on iOS (thanks @verde-io) (fixes #807)
* Fix for double vimeo caption rendering (fixes #877)
## v3.1.0
* Styling fixes
## v3.1.0-beta.2
* YouTube playback speed fixes
## v3.1.0-beta.1
* HTML5 quality selection
* Improvements to the YouTube quality selection
## v3.0.11
* Muted and autoplay fixes
* Small bug fixes from Sentry logs
## v3.0.10
* Docs fix
* Package upgrades
## v3.0.9
* Demo fix
* Fix Vimeo regression
## v3.0.8
* Vimeo hotfix for private videos
## v3.0.7
* Fix for keyboard shortcut error with fast forward
* Fix for Vimeo trying to set playback rate when not allowed
## v3.0.6
* Improved the logic for the custom handlers preventing default handlers
## v3.0.5
* Removed console messages
## v3.0.4
* Fixes for fullscreen not working inside iframes
* Fixes for custom handlers being able to prevent default
* Fixes for controls not hiding/showing correctly on Mobile Safari
## v3.0.3
* Vimeo offset tweak (fixes #826)
* Fix for .stop() method (fixes #819)
@@ -9,11 +114,11 @@
* Fix fast-forward control (thanks @saadshahd)
* Fix the options link in the readme (thanks @DanielRuf)
# v3.0.2
## v3.0.2
* Fix for Safari not firing error events when trying to load blocked scripts
# v3.0.1
## v3.0.1
* Fix for trying to accessing local storage when it's blocked
+125 -85
View File
@@ -1,27 +1,73 @@
# 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.
### Example
#### Example
```javascript
i18n: {
restart: "Restart",
rewind: "Rewind {seektime} secs",
play: "Play",
pause: "Pause",
forward: "Forward {seektime} secs",
buffered: "buffered",
currentTime: "Current time",
duration: "Duration",
volume: "Volume",
toggleMute: "Toggle Mute",
toggleCaptions: "Toggle Captions",
toggleFullscreen: "Toggle Fullscreen"
restart: 'Restart',
rewind: 'Rewind {seektime} secs',
play: 'Play',
pause: 'Pause',
fastForward: 'Forward {seektime} secs',
seek: 'Seek',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
duration: 'Duration',
volume: 'Volume',
mute: 'Mute',
unmute: 'Unmute',
enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
loop: 'Loop',
start: 'Start',
end: 'End',
all: 'All',
reset: 'Reset',
disabled: 'Disabled',
advertisement: 'Ad',
}
```
@@ -29,85 +75,79 @@ Note: `{seektime}` will be replaced with your configured seek time or the defaul
## Using custom HTML
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)
- `{seektime}` - the seek time specified in options for fast forward and rewind
- `{title}` - the title of your media, if specified
* `{id}` - the dynamically generated ID for the player (for form controls)
* `{seektime}` - the seek time specified in options for fast forward and rewind
* `{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
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
var controls = ["<div class='plyr__controls'>",
"<button type='button' data-plyr='restart'>",
"<svg><use xlink:href='#plyr-restart'></use></svg>",
"<span class='plyr__sr-only'>Restart</span>",
"</button>",
"<button type='button' data-plyr='rewind'>",
"<svg><use xlink:href='#plyr-rewind'></use></svg>",
"<span class='plyr__sr-only'>Rewind {seektime} secs</span>",
"</button>",
"<button type='button' data-plyr='play'>",
"<svg><use xlink:href='#plyr-play'></use></svg>",
"<span class='plyr__sr-only'>Play</span>",
"</button>",
"<button type='button' data-plyr='pause'>",
"<svg><use xlink:href='#plyr-pause'></use></svg>",
"<span class='plyr__sr-only'>Pause</span>",
"</button>",
"<button type='button' data-plyr='fast-forward'>",
"<svg><use xlink:href='#plyr-fast-forward'></use></svg>",
"<span class='plyr__sr-only'>Forward {seektime} secs</span>",
"</button>",
"<span class='plyr__progress'>",
"<label for='seek{id}' class='plyr__sr-only'>Seek</label>",
"<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--played' max='100' value='0' role='presentation'></progress>",
"<progress class='plyr__progress--buffer' max='100' value='0'>",
"<span>0</span>% buffered",
"</progress>",
"<span class='plyr__tooltip'>00:00</span>",
"</span>",
"<span class='plyr__time'>",
"<span class='plyr__sr-only'>Current time</span>",
"<span class='plyr__time--current'>00:00</span>",
"</span>",
"<span class='plyr__time'>",
"<span class='plyr__sr-only'>Duration</span>",
"<span class='plyr__time--duration'>00:00</span>",
"</span>",
"<button type='button' data-plyr='mute'>",
"<svg class='icon--muted'><use xlink:href='#plyr-muted'></use></svg>",
"<svg><use xlink:href='#plyr-volume'></use></svg>",
"<span class='plyr__sr-only'>Toggle Mute</span>",
"</button>",
"<span class='plyr__volume'>",
"<label for='volume{id}' class='plyr__sr-only'>Volume</label>",
"<input id='volume{id}' class='plyr__volume--input' type='range' min='0' max='10' value='5' data-plyr='volume'>",
"<progress class='plyr__volume--display' max='10' value='0' role='presentation'></progress>",
"</span>",
"<button type='button' data-plyr='captions'>",
"<svg class='icon--captions-on'><use xlink:href='#plyr-captions-on'></use></svg>",
"<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("");
const controls = `
<div class="plyr__controls">
<button type="button" class="plyr__control" data-plyr="restart">
<svg role="presentation"><use xlink:href="#plyr-restart"></use></svg>
<span class="plyr__tooltip" role="tooltip">Restart</span>
</button>
<button type="button" class="plyr__control" data-plyr="rewind">
<svg role="presentation"><use xlink:href="#plyr-rewind"></use></svg>
<span class="plyr__tooltip" role="tooltip">Rewind {seektime} secs</span>
</button>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Play, {title}" data-plyr="play">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-pause"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-play"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Pause</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Play</span>
</button>
<button type="button" class="plyr__control" data-plyr="fast-forward">
<svg role="presentation"><use xlink:href="#plyr-fast-forward"></use></svg>
<span class="plyr__tooltip" role="tooltip">Forward {seektime} secs</span>
</button>
<div class="plyr__progress">
<label for="plyr-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}">
<progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress>
<span role="tooltip" class="plyr__tooltip">00:00</span>
</div>
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span>
</button>
<div class="plyr__volume">
<label for="plyr-volume-{id}" class="plyr__sr-only">Volume</label>
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" id="plyr-volume-{id}">
</div>
<button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-captions-off"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Disable captions</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enable captions</span>
</button>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Enter fullscreen" data-plyr="fullscreen">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enter fullscreen</span>
</button>
</div>
`;
// Setup the player
plyr.setup('.js-player', {
html: controls
});
const player = new Plyr('#player', { controls });
```
+1 -1
View File
File diff suppressed because one or more lines are too long
+381 -257
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+7 -1
View File
@@ -6,8 +6,14 @@
<title>Doh. Looks like something went wrong.</title>
<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 -->
<link rel="stylesheet" href="dist/error.css">
<link rel="stylesheet" href="dist/error.css?v=2">
<!-- Preload -->
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
+11 -8
View File
@@ -27,7 +27,7 @@
<meta name="twitter:card" content="summary_large_image">
<!-- Docs styles -->
<link rel="stylesheet" href="dist/demo.css">
<link rel="stylesheet" href="dist/demo.css?v=2">
<!-- Preload -->
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
@@ -93,16 +93,18 @@
<main>
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
<!-- Video files -->
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" type="video/mp4">
<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-576p.mp4" type="video/mp4" size="576">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
<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"
default>
<track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
<!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-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>
<ul>
@@ -112,7 +114,7 @@
<title>HTML5</title>
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
</svg>
<a href="http://viewfromabluemoon.com/" target="_blank">View From A Blue Moon</a> &copy; Brainfarm
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank">View From A Blue Moon</a> &copy; Brainfarm
</small>
</li>
<li class="plyr__cite plyr__cite--audio" hidden>
@@ -139,7 +141,7 @@
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/ondemand/viewfromabluemoon4k" target="_blank">View From A Blue Moon</a> on&nbsp;
<a href="https://vimeo.com/76979871" target="_blank">The New Vimeo Player</a> on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
@@ -169,7 +171,8 @@
</aside>
<!-- Polyfills -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent" crossorigin="anonymous"></script>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"
crossorigin="anonymous"></script>
<!-- Plyr core script -->
<script src="../dist/plyr.js" crossorigin="anonymous"></script>
+24 -6
View File
@@ -65,6 +65,7 @@ import Raven from 'raven-js';
'fast-forward',
'progress',
'current-time',
'duration',
'mute',
'volume',
'captions',
@@ -80,7 +81,7 @@ import Raven from 'raven-js';
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
// enabled: true,
enabled: true,
publisherId: '918848828995742',
},
});
@@ -118,10 +119,28 @@ import Raven from 'raven-js';
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4',
type: 'video/mp4',
}],
sources: [
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.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',
tracks: [
{
@@ -163,7 +182,6 @@ import Raven from 'raven-js';
case types.youtube:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
+6 -6
View File
@@ -3,12 +3,6 @@
// ==========================================================================
@charset 'UTF-8';
// Libs
@import '../lib/fontface';
@import '../lib/mixins';
@import '../lib/normalize';
@import '../lib/reset';
// Settings
@import '../settings/colors';
@import '../settings/cosmetic';
@@ -17,6 +11,12 @@
@import '../settings/spacing';
@import '../settings/type';
// Libs
@import '../lib/fontface';
@import '../lib/mixins';
@import '../lib/normalize';
@import '../lib/reset';
// Layout
@import '../layout/error';
+1
View File
@@ -29,6 +29,7 @@ video {
position: absolute;
right: 0;
top: 0;
z-index: 3;
}
// Style full supported player
+1
View File
@@ -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-medium: 18px;
$plyr-font-size-captions-large: 21px;
$plyr-font-size-menu: $plyr-font-size-base;
+2 -1
View File
@@ -6,5 +6,6 @@ h1 {
@include font-size($font-size-h1);
font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-headings;
margin: 0 0 ($spacing-base / 2);
line-height: 1.2;
margin: 0 0 $spacing-base;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+2577 -2260
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2644 -2479
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+7 -6
View File
@@ -129,7 +129,7 @@ const build = {
tasks.js.push(name);
const { output } = paths[bundle];
gulp.task(name, () =>
return gulp.task(name, () =>
gulp
.src(bundles[bundle].js[key])
.pipe(sourcemaps.init())
@@ -162,7 +162,7 @@ const build = {
const name = `sass:${key}`;
tasks.sass.push(name);
gulp.task(name, () =>
return gulp.task(name, () =>
gulp
.src(bundles[bundle].sass[key])
.pipe(sass())
@@ -180,7 +180,7 @@ const build = {
tasks.sprite.push(name);
// Process Icons
gulp.task(name, () =>
return gulp.task(name, () =>
gulp
.src(paths[bundle].src.sprite)
.pipe(
@@ -287,7 +287,8 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
'plyr.polyfilled.js',
'defaults.js',
];
gulp
return gulp
.src(files.map(file => path.join(root, `src/js/${file}`)))
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
@@ -406,7 +407,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
});
// Do everything
gulp.task('publish', () => {
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo');
gulp.task('publish', callback => {
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo', callback);
});
}
+18 -17
View File
@@ -1,6 +1,6 @@
{
"name": "plyr",
"version": "3.0.5",
"version": "3.3.2",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"main": "./dist/plyr.js",
@@ -8,27 +8,27 @@
"sass": "./src/sass/plyr.scss",
"style": "./dist/plyr.css",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.2",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.6.1",
"del": "^3.0.0",
"eslint": "^4.19.0",
"eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.9.0",
"eslint-plugin-import": "^2.11.0",
"git-branch": "^2.0.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^5.0.0",
"gulp-better-rollup": "^3.0.0",
"gulp-clean-css": "^3.9.3",
"gulp-better-rollup": "^3.1.0",
"gulp-clean-css": "^3.9.4",
"gulp-concat": "^2.6.1",
"gulp-filter": "^5.1.0",
"gulp-open": "^3.0.0",
"gulp-open": "^3.0.1",
"gulp-rename": "^1.2.2",
"gulp-replace": "^0.6.1",
"gulp-s3": "^0.11.0",
"gulp-sass": "^3.1.0",
"gulp-sass": "^4.0.1",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.4",
"gulp-svgmin": "^1.2.4",
@@ -37,16 +37,16 @@
"gulp-util": "^3.0.8",
"prettier-eslint": "^8.8.1",
"prettier-stylelint": "^0.4.2",
"rollup-plugin-babel": "^3.0.3",
"rollup-plugin-commonjs": "^8.4.1",
"rollup-plugin-node-resolve": "^3.2.0",
"rollup-plugin-babel": "^3.0.4",
"rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^3.3.0",
"run-sequence": "^2.2.1",
"stylelint": "^9.1.3",
"stylelint-config-prettier": "^3.0.4",
"stylelint": "^9.2.0",
"stylelint-config-prettier": "^3.2.0",
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.0.0",
"stylelint-order": "^0.8.1",
"stylelint-scss": "^2.5.0",
"stylelint-scss": "^3.1.0",
"stylelint-selector-bem-pattern": "^2.0.0"
},
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
@@ -68,7 +68,8 @@
"dependencies": {
"babel-polyfill": "^6.26.0",
"custom-event-polyfill": "^0.3.0",
"loadjs": "^3.5.2",
"raven-js": "^3.23.3"
"loadjs": "^3.5.4",
"npm": "^6.0.0",
"raven-js": "^3.24.2"
}
}
+38 -28
View File
@@ -2,7 +2,7 @@
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)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io)
@@ -39,13 +39,13 @@ Check out the [changelog](changelog.md) to see what's new with Plyr.
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
| Type | Maintainer | Link |
| --------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| 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) |
| 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 | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
| Type | Maintainer | Link |
| --------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
| 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 | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
## Quick setup
@@ -128,7 +128,7 @@ 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:
```html
<script src="https://cdn.plyr.io/3.0.5/plyr.js"></script>
<script src="https://cdn.plyr.io/3.3.2/plyr.js"></script>
```
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
@@ -144,13 +144,13 @@ 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:
```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.0.5/plyr.css">
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.2/plyr.css">
```
### 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
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.0.5/plyr.svg`.
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.2/plyr.svg`.
## Ads
@@ -279,7 +279,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
| `showPosterOnEnd` | Boolean | false | This will restore and _reload_ HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution. |
| `resetOnEnd` | Boolean | false | Reset the playback to the start once playback is complete. |
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
| `duration` | Number | `null` | Specify a custom duration for media. |
@@ -374,8 +374,9 @@ player.fullscreen.active; // false;
| -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. |
| `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. |
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `stopped` | ✓ | - | Returns a boolean indicating if the current player is stopped. |
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
@@ -388,7 +389,7 @@ player.fullscreen.active; // false;
| `quality`&sup1; | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
| `poster`&sup2; | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
| `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. |
@@ -599,6 +600,8 @@ Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
_Note_: These need updating to use the new v3 syntax but would still work.
## Fullscreen
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
@@ -607,19 +610,20 @@ Fullscreen in Plyr is supported by all browsers that [currently support it](http
Plyr supports the last 2 versions of most _modern_ browsers.
| Browser | Supported |
| ------------- | --------- |
| Safari | ✓ |
| Mobile Safari | ✓&sup1; |
| Firefox | ✓ |
| Chrome | ✓ |
| Opera | ✓ |
| Edge | ✓ |
| IE11 | ✓ |
| IE10 | ✓&sup2; |
| Browser | Supported |
| ------------- | ------------- |
| Safari | ✓ |
| Mobile Safari | ✓&sup1; |
| Firefox | ✓ |
| Chrome | ✓ |
| Opera | ✓ |
| Edge | ✓ |
| IE11 | ✓&sup3; |
| IE10 | ✓&sup2;&sup3; |
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options))
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)).
3. Polyfills required. See below.
### Polyfills
@@ -668,8 +672,10 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
## Donate
Plyr costs money to run, not only my time - I donate that for free but domains, hosting and more. Any help is appreciated...
[Donate to support Plyr](https://www.paypal.me/pottsy/20usd)
Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated...
* [Donate via Patron](https://www.patreon.com/plyr)
* [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
## Mentions
@@ -707,10 +713,14 @@ Credit to the PayPal HTML5 Video player from which Plyr's caption functionality
## Thanks
[![Fastly](https://cdn.plyr.io/static/demo/fastly-logo.png)](https://www.fastly.com/)
[![Fastly](https://cdn.plyr.io/static/fastly-logo.png)](https://www.fastly.com/)
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.
[![Sentry](https://cdn.plyr.io/static/sentry-logo-black.svg)](https://sentry.io/)
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
## Copyright and License
[The MIT license](license.md)
+46 -7
View File
@@ -3,9 +3,10 @@
// TODO: Create as class
// ==========================================================================
import controls from './controls';
import i18n from './i18n';
import support from './support';
import utils from './utils';
import controls from './controls';
const captions = {
// Setup captions
@@ -46,6 +47,7 @@ const captions = {
return;
}
// Inject the container
if (!utils.is.element(this.elements.captions)) {
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
@@ -148,7 +150,49 @@ const captions = {
// Get the current track for the current language
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
@@ -206,11 +250,6 @@ const captions = {
// Display captions container and button (for initialization)
show() {
// If there's no caption toggle, bail
if (!utils.is.element(this.elements.buttons.captions)) {
return;
}
// Try to load the value from storage
let active = this.storage.get('captions');
+237 -139
View File
@@ -2,11 +2,12 @@
// Plyr controls
// ==========================================================================
import support from './support';
import utils from './utils';
import ui from './ui';
import i18n from './i18n';
import captions from './captions';
import html5 from './html5';
import i18n from './i18n';
import support from './support';
import ui from './ui';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
@@ -14,11 +15,6 @@ const browser = utils.getBrowser();
const controls = {
// Webkit polyfill for lower fill range
updateRangeFill(target) {
// WebKit only
if (!browser.isWebkit) {
return;
}
// Get range from event if event passed
const range = utils.is.event(target) ? target.target : target;
@@ -27,23 +23,88 @@ const controls = {
return;
}
// Set aria value for https://github.com/sampotts/plyr/issues/905
range.setAttribute('aria-valuenow', range.value);
// WebKit only
if (!browser.isWebkit) {
return;
}
// Set CSS custom property
range.style.setProperty('--value', `${range.value / range.max * 100}%`);
},
// Get icon URL
getIconUrl() {
const url = new URL(this.config.iconUrl, window.location);
const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody);
return {
url: this.config.iconUrl,
absolute: this.config.iconUrl.indexOf('http') === 0 || (browser.isIE && !window.svg4everybody),
cors,
};
},
// Find the UI controls and store references in custom controls
// TODO: Allow settings menus with custom controls
findElements() {
try {
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
// Buttons
this.elements.buttons = {
play: utils.getElements.call(this, this.config.selectors.buttons.play),
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
};
// Progress
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
// Inputs
this.elements.inputs = {
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
};
// Display
this.elements.display = {
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
duration: utils.getElement.call(this, this.config.selectors.display.duration),
};
// Seek tooltip
if (utils.is.element(this.elements.progress)) {
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
}
return true;
} catch (error) {
// Log it
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
// Restore native video controls
this.toggleNativeControls(true);
return false;
}
},
// Create <svg> icon
createIcon(type, attributes) {
const namespace = 'http://www.w3.org/2000/svg';
const iconUrl = controls.getIconUrl.call(this);
const iconPath = `${!iconUrl.absolute ? iconUrl.url : ''}#${this.config.iconPrefix}`;
const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
// Create <svg>
const icon = document.createElementNS(namespace, 'svg');
@@ -51,6 +112,7 @@ const controls = {
icon,
utils.extend(attributes, {
role: 'presentation',
focusable: 'false',
}),
);
@@ -205,7 +267,6 @@ const controls = {
// Add aria attributes
attributes['aria-pressed'] = false;
attributes['aria-label'] = i18n.get(label, this.config);
} else {
button.appendChild(controls.createIcon.call(this, icon));
button.appendChild(controls.createLabel.call(this, label));
@@ -237,6 +298,7 @@ const controls = {
'label',
{
for: attributes.id,
id: `${attributes.id}-label`,
class: this.config.classNames.hidden,
},
i18n.get(type, this.config),
@@ -254,6 +316,12 @@ const controls = {
step: 0.01,
value: 0,
autocomplete: 'off',
// A11y fixes for https://github.com/sampotts/plyr/issues/905
role: 'slider',
'aria-labelledby': `${attributes.id}-label`,
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-valuenow': 0,
},
attributes,
),
@@ -280,6 +348,8 @@ const controls = {
min: 0,
max: 100,
value: 0,
role: 'presentation',
'aria-hidden': true,
},
attributes,
),
@@ -313,22 +383,14 @@ const controls = {
// Create time display
createTime(type) {
const container = utils.createElement('div', {
class: 'plyr__time',
});
const attributes = utils.getAttributesFromSelector(this.config.selectors.display[type]);
container.appendChild(
utils.createElement(
'span',
{
class: this.config.classNames.hidden,
},
i18n.get(type, this.config),
),
);
container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.display[type]), '00:00'));
const container = utils.createElement('div', utils.extend(attributes, {
class: `plyr__time ${attributes.class}`,
'aria-label': i18n.get(type, this.config),
}), '00:00');
// Reference for updates
this.elements.display[type] = container;
return container;
@@ -353,7 +415,7 @@ const controls = {
}),
);
const faux = utils.createElement('span', { 'aria-hidden': true });
const faux = utils.createElement('span', { hidden: '' });
label.appendChild(radio);
label.appendChild(faux);
@@ -428,15 +490,11 @@ const controls = {
// Hide/show a tab
toggleTab(setting, toggle) {
const tab = this.elements.settings.tabs[setting];
const pane = this.elements.settings.panes[setting];
utils.toggleHidden(tab, !toggle);
utils.toggleHidden(pane, !toggle);
utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
},
// Set the YouTube quality menu
// TODO: Support for HTML5
// Set the quality menu
// TODO: Vimeo support
setQualityMenu(options) {
// Menu required
if (!utils.is.element(this.elements.settings.panes.quality)) {
@@ -449,14 +507,15 @@ const controls = {
// Set options if passed and filter based on config
if (utils.is.array(options)) {
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
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);
// Check if we need to toggle the parent
controls.checkMenu.call(this);
// If we're hiding, nothing more to do
if (!toggle) {
return;
@@ -470,20 +529,19 @@ const controls = {
let label = '';
switch (quality) {
case 'hd2160':
case 2160:
label = '4K';
break;
case 'hd1440':
label = 'WQHD';
break;
case 'hd1080':
case 1440:
case 1080:
case 720:
label = 'HD';
break;
case 'hd720':
label = 'HD';
case 576:
case 480:
label = 'SD';
break;
default:
@@ -497,9 +555,16 @@ const controls = {
return controls.createBadge.call(this, label);
};
this.options.quality.forEach(quality =>
controls.createMenuItem.call(this, quality, list, type, controls.getLabel.call(this, 'quality', quality), getBadge(quality)),
);
// Sort options by the config and then render options
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);
},
@@ -509,34 +574,17 @@ const controls = {
getLabel(setting, value) {
switch (setting) {
case 'speed':
return value === 1 ? 'Normal' : `${value}&times;`;
return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;
case 'quality':
switch (value) {
case 'hd2160':
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;
if (utils.is.number(value)) {
return `${value}p`;
}
return utils.toTitleCase(value);
case 'captions':
return controls.getLanguage.call(this);
return captions.getLabel.call(this);
default:
return null;
@@ -544,18 +592,27 @@ const controls = {
},
// Update the selected setting
updateSetting(setting, container) {
updateSetting(setting, container, input) {
const pane = this.elements.settings.panes[setting];
let value = null;
let list = container;
switch (setting) {
case 'captions':
value = this.captions.active ? this.captions.language : i18n.get('disabled', this.config);
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;
default:
value = this[setting];
value = !utils.is.empty(input) ? input : this[setting];
// Get default
if (utils.is.empty(value)) {
@@ -563,7 +620,7 @@ const controls = {
}
// 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}`);
return;
}
@@ -582,17 +639,19 @@ const controls = {
list = pane && pane.querySelector('ul');
}
// Update the label
if (!utils.is.empty(value)) {
const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`);
label.innerHTML = controls.getLabel.call(this, setting, value);
// If there's no list it means it's not been rendered...
if (!utils.is.element(list)) {
return;
}
// 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}"]`);
if (utils.is.element(target)) {
// Check it
target.checked = true;
}
},
@@ -643,21 +702,6 @@ const controls = {
// Get current selected caption language
// 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 && this.captions.active) {
const currentTrack = captions.getCurrentTrack.call(this);
if (utils.is.track(currentTrack)) {
return currentTrack.label;
}
}
return i18n.get('disabled', this.config);
},
// Set a list of available captions languages
setCaptionsMenu() {
@@ -666,21 +710,24 @@ const controls = {
const list = this.elements.settings.panes.captions.querySelector('ul');
// Toggle the pane and tab
const hasTracks = captions.getTracks.call(this).length;
controls.toggleTab.call(this, type, hasTracks);
const toggle = captions.getTracks.call(this).length;
controls.toggleTab.call(this, type, toggle);
// Empty the menu
utils.emptyElement(list);
// Check if we need to toggle the parent
controls.checkMenu.call(this);
// If there's no captions, bail
if (!hasTracks) {
if (!toggle) {
return;
}
// Re-map the tracks into just the data we need
const tracks = captions.getTracks.call(this).map(track => ({
language: track.language,
label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(),
language: !utils.is.empty(track.language) ? track.language : 'enabled',
label: captions.getLabel.call(this, track),
}));
// Add the "Disabled" option to turn off captions
@@ -696,17 +743,25 @@ const controls = {
track.language,
list,
'language',
track.label || track.language,
controls.createBadge.call(this, track.language.toUpperCase()),
track.label,
track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null,
track.language.toLowerCase() === this.captions.language.toLowerCase(),
);
});
// Store reference
this.options.captions = tracks.map(track => track.language);
controls.updateSetting.call(this, type, list);
},
// 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;
@@ -714,8 +769,10 @@ const controls = {
const type = 'speed';
// Set the default speeds
if (!utils.is.array(this.options.speed) || !this.options.speed.length) {
// Set the speed options
if (utils.is.array(options)) {
this.options.speed = options;
} else if (this.isHTML5 || this.isVimeo) {
this.options.speed = [
0.5,
0.75,
@@ -731,9 +788,12 @@ const controls = {
this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
// 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);
// Check if we need to toggle the parent
controls.checkMenu.call(this);
// If we're hiding, nothing more to do
if (!toggle) {
return;
@@ -742,19 +802,26 @@ const controls = {
// Get the list to populate
const list = this.elements.settings.panes.speed.querySelector('ul');
// Show the pane and tab
utils.toggleHidden(this.elements.settings.tabs.speed, false);
utils.toggleHidden(this.elements.settings.panes.speed, false);
// Empty the menu
utils.emptyElement(list);
// 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);
},
// 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
toggleMenu(event) {
const { form } = this.elements.settings;
@@ -765,7 +832,7 @@ const controls = {
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.hasAttribute('hidden');
if (utils.is.event(event)) {
const isMenuItem = utils.is.element(form) && form.contains(event.target);
@@ -790,7 +857,7 @@ const controls = {
}
if (utils.is.element(form)) {
form.setAttribute('aria-hidden', !show);
utils.toggleHidden(form, !show);
utils.toggleClass(this.elements.container, this.config.classNames.menu.open, show);
if (show) {
@@ -806,7 +873,7 @@ const controls = {
const clone = tab.cloneNode(true);
clone.style.position = 'absolute';
clone.style.opacity = 0;
clone.setAttribute('aria-hidden', false);
clone.removeAttribute('hidden');
// Prevent input's being unchecked due to the name being identical
Array.from(clone.querySelectorAll('input[name]')).forEach(input => {
@@ -831,11 +898,9 @@ const controls = {
},
// Toggle Menu
showTab(event) {
showTab(target = '') {
const { menu } = this.elements.settings;
const tab = event.target;
const show = tab.getAttribute('aria-expanded') === 'false';
const pane = document.getElementById(tab.getAttribute('aria-controls'));
const pane = document.getElementById(target);
// Nothing to show, bail
if (!utils.is.element(pane)) {
@@ -850,7 +915,7 @@ const controls = {
// Hide all other tabs
// Get other tabs
const current = menu.querySelector('[role="tabpanel"][aria-hidden="false"]');
const current = menu.querySelector('[role="tabpanel"]:not([hidden])');
const container = current.parentNode;
// Set other toggles to be expanded false
@@ -894,12 +959,16 @@ const controls = {
}
// Set attributes on current tab
current.setAttribute('aria-hidden', true);
utils.toggleHidden(current, true);
current.setAttribute('tabindex', -1);
// Set attributes on target
pane.setAttribute('aria-hidden', !show);
tab.setAttribute('aria-expanded', show);
utils.toggleHidden(pane, false);
const tabs = utils.getElements.call(this, `[aria-controls="${target}"]`);
Array.from(tabs).forEach(tab => {
tab.setAttribute('aria-expanded', true);
});
pane.removeAttribute('tabindex');
// Focus the first item
@@ -1025,6 +1094,7 @@ const controls = {
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
const menu = utils.createElement('div', {
class: 'plyr__menu',
hidden: '',
});
menu.appendChild(
@@ -1039,7 +1109,7 @@ const controls = {
const form = utils.createElement('form', {
class: 'plyr__menu__container',
id: `plyr-settings-${data.id}`,
'aria-hidden': true,
hidden: '',
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
role: 'tablist',
tabindex: -1,
@@ -1049,7 +1119,6 @@ const controls = {
const home = utils.createElement('div', {
id: `plyr-settings-${data.id}-home`,
'aria-hidden': false,
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
role: 'tabpanel',
});
@@ -1100,11 +1169,10 @@ const controls = {
this.config.settings.forEach(type => {
const pane = utils.createElement('div', {
id: `plyr-settings-${data.id}-${type}`,
'aria-hidden': true,
hidden: '',
'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`,
role: 'tabpanel',
tabindex: -1,
hidden: '',
});
const back = utils.createElement(
@@ -1159,10 +1227,12 @@ const controls = {
this.elements.controls = container;
if (this.config.controls.includes('settings') && this.config.settings.includes('speed')) {
controls.setSpeedMenu.call(this);
if (this.isHTML5) {
controls.setQualityMenu.call(this, html5.getQualityOptions.call(this));
}
controls.setSpeedMenu.call(this);
return container;
},
@@ -1173,7 +1243,7 @@ const controls = {
const icon = controls.getIconUrl.call(this);
// Only load external sprite using AJAX
if (icon.absolute) {
if (icon.cors) {
utils.loadSprite(icon.url, 'sprite-plyr');
}
}
@@ -1185,17 +1255,21 @@ const controls = {
let container = null;
this.elements.controls = null;
// HTML or Element passed as the option
// Set template properties
const props = {
id: this.id,
seektime: this.config.seekTime,
title: this.config.title,
};
let update = true;
if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) {
// String or HTMLElement passed as the option
container = this.config.controls;
} else if (utils.is.function(this.config.controls)) {
// A custom function to build controls
// The function can return a HTMLElement or String
container = this.config.controls({
id: this.id,
seektime: this.config.seekTime,
title: this.config.title,
});
container = this.config.controls.call(this, props);
} else {
// Create controls
container = controls.create.call(this, {
@@ -1203,10 +1277,34 @@ const controls = {
seektime: this.config.seekTime,
speed: this.speed,
quality: this.quality,
captions: controls.getLanguage.call(this),
captions: captions.getLabel.call(this),
// TODO: Looping
// loop: 'None',
});
update = false;
}
// Replace props with their value
const replace = input => {
let result = input;
Object.entries(props).forEach(([
key,
value,
]) => {
result = utils.replaceAll(result, `{${key}}`, value);
});
return result;
};
// Update markup
if (update) {
if (utils.is.string(this.config.controls)) {
container = replace(container);
} else if (utils.is.element(container)) {
container.innerHTML = replace(container.innerHTML);
}
}
// Controls container
@@ -1225,13 +1323,13 @@ const controls = {
// Inject controls HTML
if (utils.is.element(container)) {
target.appendChild(container);
} else {
} else if (container) {
target.insertAdjacentHTML('beforeend', container);
}
// Find the elements if need be
if (!utils.is.element(this.elements.controls)) {
utils.findElements.call(this);
controls.findElements.call(this);
}
// Edge sometimes doesn't finish the paint so force a redraw
+28 -18
View File
@@ -47,8 +47,8 @@ const defaults = {
// Auto hide the controls
hideControls: true,
// Revert to poster on finish (HTML5 - will cause reload)
showPosterOnEnd: false,
// Reset to start when playback ended
resetOnEnd: false,
// Disable the standard context menu
disableContextMenu: true,
@@ -56,24 +56,26 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.0.5/plyr.svg',
iconUrl: 'https://cdn.plyr.io/3.3.2/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default
quality: {
default: 'default',
default: 576,
options: [
'hd2160',
'hd1440',
'hd1080',
'hd720',
'large',
'medium',
'small',
'tiny',
'default',
4320,
2880,
2160,
1440,
1080,
720,
576,
480,
360,
240,
'default', // YouTube's "auto"
],
},
@@ -113,7 +115,7 @@ const defaults = {
// Captions settings
captions: {
active: false,
language: window.navigator.language.split('-')[0],
language: (navigator.language || navigator.userLanguage).split('-')[0],
},
// Fullscreen settings
@@ -175,6 +177,7 @@ const defaults = {
captions: 'Captions',
settings: 'Settings',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
loop: 'Loop',
start: 'Start',
@@ -182,19 +185,24 @@ const defaults = {
all: 'All',
reset: 'Reset',
disabled: 'Disabled',
enabled: 'Enabled',
advertisement: 'Ad',
},
// URLs
urls: {
vimeo: {
api: 'https://player.vimeo.com/api/player.js',
sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
api: 'https://vimeo.com/api/v2/video/{0}.json',
},
youtube: {
api: 'https://www.youtube.com/iframe_api',
sdk: 'https://www.youtube.com/iframe_api',
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
poster: 'https://img.youtube.com/vi/{0}/maxresdefault.jpg,https://img.youtube.com/vi/{0}/hqdefault.jpg',
},
googleIMA: {
api: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
},
@@ -320,12 +328,14 @@ const defaults = {
classNames: {
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
poster: 'plyr__poster',
ads: 'plyr__ads',
control: 'plyr__control',
type: 'plyr--{0}',
provider: 'plyr--{0}',
stopped: 'plyr--stopped',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
error: 'plyr--has-error',
hover: 'plyr--hover',
+13 -11
View File
@@ -55,7 +55,7 @@ class Fullscreen {
// Get prefix
this.prefix = Fullscreen.prefix;
this.name = Fullscreen.name;
this.property = Fullscreen.property;
// Scroll position
this.scrollPosition = { x: 0, y: 0 };
@@ -68,13 +68,15 @@ class Fullscreen {
});
// Fullscreen toggle on double click
utils.on(this.player.elements.container, 'dblclick', () => {
utils.on(this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls
if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;
}
this.toggle();
});
// Prevent double click on controls bubbling up
utils.on(this.player.elements.controls, 'dblclick', event => event.stopPropagation());
// Update the UI
this.update();
}
@@ -88,7 +90,7 @@ class Fullscreen {
static get prefix() {
// No prefix
if (utils.is.function(document.exitFullscreen)) {
return false;
return '';
}
// Check for fullscreen support by vendor prefix
@@ -111,7 +113,7 @@ class Fullscreen {
return value;
}
static get name() {
static get property() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
}
@@ -136,7 +138,7 @@ class Fullscreen {
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.name}Element`];
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
return element === this.target;
}
@@ -174,7 +176,7 @@ class Fullscreen {
} else if (!this.prefix) {
this.target.requestFullscreen();
} else if (!utils.is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.name}`]();
this.target[`${this.prefix}Request${this.property}`]();
}
}
@@ -191,10 +193,10 @@ class Fullscreen {
} else if (!Fullscreen.native) {
toggleFallback.call(this, false);
} else if (!this.prefix) {
document.cancelFullScreen();
(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}`]();
document[`${this.prefix}${action}${this.property}`]();
}
}
+146
View 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;
+46 -22
View File
@@ -2,10 +2,9 @@
// Plyr Event Listeners
// ==========================================================================
import support from './support';
import utils from './utils';
import controls from './controls';
import ui from './ui';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
@@ -129,7 +128,7 @@ class Listeners {
case 39:
// Arrow forward
this.player.fastForward();
this.player.forward();
break;
case 37:
@@ -254,7 +253,7 @@ class Listeners {
utils.on(this.player.media, 'timeupdate seeking', event => ui.timeUpdate.call(this.player, event));
// Display duration
utils.on(this.player.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this.player, event));
utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => ui.durationUpdate.call(this.player, event));
// Check for audio tracks on load
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
@@ -266,30 +265,41 @@ class Listeners {
// Handle the media finishing
utils.on(this.player.media, 'ended', () => {
// Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.showPosterOnEnd) {
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
// Restart
this.player.restart();
// Re-load media
this.player.media.load();
}
});
// Check for buffer progress
utils.on(this.player.media, 'progress playing', event => ui.updateProgress.call(this.player, event));
// Handle native mute
// Handle volume changes
utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event));
// Handle native play/pause
utils.on(this.player.media, 'playing play pause ended emptied', event => ui.checkPlaying.call(this.player, event));
// Handle play/pause
utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event));
// Loading
// Loading state
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
// Check if media failed to load
// 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
if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) {
// Re-fetch the wrapper
@@ -321,7 +331,7 @@ class Listeners {
// Disable right click
if (this.player.supported.ui && this.player.config.disableContextMenu) {
utils.on(
this.player.media,
this.player.elements.wrapper,
'contextmenu',
event => {
event.preventDefault();
@@ -345,13 +355,16 @@ class Listeners {
this.player.storage.set({ speed: this.player.speed });
});
// Quality change
utils.on(this.player.media, 'qualitychange', () => {
// Update UI
controls.updateSetting.call(this.player, 'quality');
// Quality request
utils.on(this.player.media, 'qualityrequested', event => {
// Save to storage
this.player.storage.set({ quality: this.player.quality });
this.player.storage.set({ quality: event.detail.quality });
});
// Quality change
utils.on(this.player.media, 'qualitychange', event => {
// Update UI
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
});
// Caption language change
@@ -398,14 +411,15 @@ class Listeners {
const proxy = (event, defaultHandler, customHandlerKey) => {
const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = utils.is.function(customHandler);
let returned = true;
// Execute custom handler
if (hasCustomHandler) {
customHandler.call(this.player, event);
returned = customHandler.call(this.player, event);
}
// 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.player, event);
}
};
@@ -475,12 +489,19 @@ class Listeners {
on(this.player.elements.settings.form, 'click', event => {
event.stopPropagation();
// Go back to home tab on click
const showHomeTab = () => {
const id = `plyr-settings-${this.player.id}-home`;
controls.showTab.call(this.player, id);
};
// Settings menu items - use event delegation as items are added/removed
if (utils.matches(event.target, this.player.config.selectors.inputs.language)) {
proxy(
event,
() => {
this.player.language = event.target.value;
showHomeTab();
},
'language',
);
@@ -489,6 +510,7 @@ class Listeners {
event,
() => {
this.player.quality = event.target.value;
showHomeTab();
},
'quality',
);
@@ -497,11 +519,13 @@ class Listeners {
event,
() => {
this.player.speed = parseFloat(event.target.value);
showHomeTab();
},
'speed',
);
} else {
controls.showTab.call(this.player, event);
const tab = event.target;
controls.showTab.call(this.player, tab.getAttribute('aria-controls'));
}
});
+11 -49
View File
@@ -2,14 +2,10 @@
// Plyr Media
// ==========================================================================
import support from './support';
import utils from './utils';
import youtube from './plugins/youtube';
import html5 from './html5';
import vimeo from './plugins/vimeo';
import ui from './ui';
// Sniff out the browser
const browser = utils.getBrowser();
import youtube from './plugins/youtube';
import utils from './utils';
const media = {
// Setup media
@@ -32,23 +28,6 @@ const media = {
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
}
if (this.supported.ui) {
// Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
// Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// If there's no autoplay attribute, assume the video is stopped and add state class
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.config.autoplay);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
}
// Inject the player wrapper
if (this.isVideo) {
// Create the wrapper div
@@ -58,6 +37,13 @@ const media = {
// Wrap the video in a container
utils.wrap(this.media, this.elements.wrapper);
// Faux poster container
this.elements.poster = utils.createElement('span', {
class: this.config.classNames.poster,
});
this.elements.wrapper.appendChild(this.elements.poster);
}
if (this.isEmbed) {
@@ -74,33 +60,9 @@ const media = {
break;
}
} else if (this.isHTML5) {
ui.setTitle.call(this);
html5.extend.call(this);
}
},
// Cancel current network requests
// See https://github.com/sampotts/plyr/issues/174
cancelRequests() {
if (!this.isHTML5) {
return;
}
// Remove child sources
utils.removeElement(this.media.querySelectorAll('source'));
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
this.media.setAttribute('src', this.config.blankVideo);
// Load the new empty source
// This will cancel existing requests
// See https://github.com/sampotts/plyr/issues/174
this.media.load();
// Debugging
this.debug.log('Cancelled network requests');
},
};
export default media;
+31 -17
View File
@@ -6,8 +6,8 @@
/* global google */
import utils from '../utils';
import i18n from '../i18n';
import utils from '../utils';
class Ads {
/**
@@ -18,7 +18,6 @@ class Ads {
constructor(player) {
this.player = player;
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.initialized = false;
this.elements = {
@@ -44,6 +43,10 @@ class Ads {
this.load();
}
get enabled() {
return this.player.isHTML5 && this.player.isVideo && this.player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length;
}
/**
* Load the IMA SDK
*/
@@ -52,7 +55,7 @@ class Ads {
// 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)
.loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => {
this.ready();
})
@@ -160,6 +163,9 @@ class Ads {
// We only overlay ads as we only support video.
request.forceNonLinearFullSlot = false;
// Mute based on current state
request.setAdWillPlayMuted(!this.player.muted);
this.loader.requestAds(request);
} catch (e) {
this.onAdError(e);
@@ -206,25 +212,27 @@ class Ads {
this.cuePoints = this.manager.getCuePoints();
// Add advertisement cue's within the time line if available
this.cuePoints.forEach(cuePoint => {
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
const seekElement = this.player.elements.progress;
if (!utils.is.empty(this.cuePoints)) {
this.cuePoints.forEach(cuePoint => {
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
const seekElement = this.player.elements.progress;
if (seekElement) {
const cuePercentage = 100 / this.player.duration * cuePoint;
const cue = utils.createElement('span', {
class: this.player.config.classNames.cues,
});
if (utils.is.element(seekElement)) {
const cuePercentage = 100 / this.player.duration * cuePoint;
const cue = utils.createElement('span', {
class: this.player.config.classNames.cues,
});
cue.style.left = `${cuePercentage.toString()}%`;
seekElement.appendChild(cue);
cue.style.left = `${cuePercentage.toString()}%`;
seekElement.appendChild(cue);
}
}
}
});
});
}
// Get skippable state
// TODO: Skip button
// this.manager.getAdSkippableState();
// this.player.debug.warn(this.manager.getAdSkippableState());
// Set volume to match player
this.manager.setVolume(this.player.volume);
@@ -385,6 +393,10 @@ class Ads {
this.player.on('seeked', () => {
const seekedTime = this.player.currentTime;
if (utils.is.empty(this.cuePoints)) {
return;
}
this.cuePoints.forEach((cuePoint, index) => {
if (time < cuePoint && cuePoint < seekedTime) {
this.manager.discardAdBreak();
@@ -396,7 +408,9 @@ class Ads {
// Listen to the resizing of the window. And resize ad accordingly
// TODO: eventually implement ResizeObserver
window.addEventListener('resize', () => {
this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
if (this.manager) {
this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
}
});
}
+70 -15
View File
@@ -2,9 +2,10 @@
// Vimeo plugin
// ==========================================================================
import utils from './../utils';
import captions from './../captions';
import controls from './../controls';
import ui from './../ui';
import utils from './../utils';
const vimeo = {
setup() {
@@ -17,7 +18,7 @@ const vimeo = {
// Load the API if not already
if (!utils.is.object(window.Vimeo)) {
utils
.loadScript(this.config.urls.vimeo.api)
.loadScript(this.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(this);
})
@@ -34,10 +35,14 @@ const vimeo = {
setAspectRatio(input) {
const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':');
const padding = 100 / ratio[0] * ratio[1];
const height = 240;
const offset = (height - padding) / (height / 50);
this.elements.wrapper.style.paddingBottom = `${padding}%`;
this.media.style.transform = `translateY(-${offset}%)`;
if (this.supported.ui) {
const height = 240;
const offset = (height - padding) / (height / 50);
this.media.style.transform = `translateY(-${offset}%)`;
}
},
// API Ready
@@ -54,6 +59,7 @@ const vimeo = {
speed: true,
transparent: 0,
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
};
const params = utils.buildUrlParams(options);
@@ -62,14 +68,14 @@ const vimeo = {
// Get from <div> if needed
if (utils.is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
source = player.media.getAttribute(player.config.attributes.embed.id);
}
const id = utils.parseVimeoId(source);
// Build an iframe
const iframe = utils.createElement('iframe');
const src = `https://player.vimeo.com/video/${id}?${params}`;
const src = utils.format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
@@ -80,6 +86,25 @@ const vimeo = {
wrapper.appendChild(iframe);
player.media = utils.replaceElement(wrapper, player.media);
// Get poster image
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (utils.is.empty(response)) {
return;
}
// Get the URL for thumbnail
const url = new URL(response[0].thumbnail_large);
// Get original image
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set attribute
player.media.setAttribute('poster', url.href);
// Update
ui.setPoster.call(player);
});
// Setup instance
// https://github.com/vimeo/player.js
player.embed = new window.Vimeo.Player(iframe);
@@ -87,6 +112,11 @@ const vimeo = {
player.media.paused = true;
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
player.media.play = () => {
player.embed.play().then(() => {
@@ -123,7 +153,9 @@ const vimeo = {
utils.dispatchEvent.call(player, player.media, 'seeking');
// Seek after events
player.embed.setCurrentTime(time);
player.embed.setCurrentTime(time).catch(() => {
// Do nothing
});
// Restore pause state
if (paused) {
@@ -139,10 +171,18 @@ const vimeo = {
return speed;
},
set(input) {
player.embed.setPlaybackRate(input).then(() => {
speed = input;
utils.dispatchEvent.call(player, player.media, 'ratechange');
});
player.embed
.setPlaybackRate(input)
.then(() => {
speed = input;
utils.dispatchEvent.call(player, player.media, 'ratechange');
})
.catch(error => {
// Hide menu item (and menu if empty)
if (error.name === 'Error') {
controls.setSpeedMenu.call(player, []);
}
});
},
});
@@ -193,9 +233,15 @@ const vimeo = {
// Source
let currentSrc;
player.embed.getVideoUrl().then(value => {
currentSrc = value;
});
player.embed
.getVideoUrl()
.then(value => {
currentSrc = value;
})
.catch(error => {
this.debug.warn(error);
});
Object.defineProperty(player.media, 'currentSrc', {
get() {
return currentSrc;
@@ -295,6 +341,15 @@ const vimeo = {
if (parseInt(data.percent, 10) === 1) {
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
}
// Get duration as if we do it before load, it gives an incorrect value
// https://github.com/sampotts/plyr/issues/891
player.embed.getDuration().then(value => {
if (value !== player.media.duration) {
player.media.duration = value;
utils.dispatchEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => {
+84 -16
View File
@@ -2,9 +2,67 @@
// YouTube plugin
// ==========================================================================
import utils from './../utils';
import controls from './../controls';
import ui from './../ui';
import utils from './../utils';
// Standardise YouTube quality unit
function mapQualityUnit(input) {
switch (input) {
case 'hd2160':
return 2160;
case 2160:
return 'hd2160';
case 'hd1440':
return 1440;
case 1440:
return 'hd1440';
case 'hd1080':
return 1080;
case 1080:
return 'hd1080';
case 'hd720':
return 720;
case 720:
return 'hd720';
case 'large':
return 480;
case 480:
return 'large';
case 'medium':
return 360;
case 360:
return 'medium';
case 'small':
return 240;
case 240:
return 'small';
default:
return 'default';
}
}
function mapQualityUnits(levels) {
if (utils.is.empty(levels)) {
return levels;
}
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
}
const youtube = {
setup() {
@@ -19,7 +77,7 @@ const youtube = {
youtube.ready.call(this);
} else {
// Load the API
utils.loadScript(this.config.urls.youtube.api).catch(error => {
utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
@@ -59,7 +117,7 @@ const youtube = {
// Or via Google API
const key = this.config.keys.google;
if (utils.is.string(key) && !utils.is.empty(key)) {
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`;
const url = utils.format(this.config.urls.youtube.api, videoId, key);
utils
.fetch(url)
@@ -103,6 +161,9 @@ const youtube = {
const container = utils.createElement('div', { id });
player.media = utils.replaceElement(container, player.media);
// Set poster image
player.media.setAttribute('poster', utils.format(player.config.urls.youtube.poster, videoId));
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(id, {
@@ -168,14 +229,10 @@ const youtube = {
utils.dispatchEvent.call(player, player.media, 'error');
},
onPlaybackQualityChange(event) {
// Get the instance
const instance = event.target;
// Get current quality
player.media.quality = instance.getPlaybackQuality();
utils.dispatchEvent.call(player, player.media, 'qualitychange');
onPlaybackQualityChange() {
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
quality: player.media.quality,
});
},
onPlaybackRateChange(event) {
// Get the instance
@@ -216,6 +273,9 @@ const youtube = {
return Number(instance.getCurrentTime());
},
set(time) {
// Vimeo will automatically play on seek
const { paused } = player.media;
// Set seeking flag
player.media.seeking = true;
@@ -224,6 +284,11 @@ const youtube = {
// Seek after events sent
instance.seekTo(time);
// Restore pause state
if (paused) {
player.pause();
}
},
});
@@ -240,15 +305,18 @@ const youtube = {
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
return instance.getPlaybackQuality();
return mapQualityUnit(instance.getPlaybackQuality());
},
set(input) {
const quality = input;
// Set via API
instance.setPlaybackQuality(mapQualityUnit(quality));
// Trigger request event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality: input,
quality,
});
instance.setPlaybackQuality(input);
},
});
@@ -400,7 +468,7 @@ const youtube = {
}
// Get quality
controls.setQualityMenu.call(player, instance.getAvailableQualityLevels());
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
break;
+91 -51
View File
@@ -1,26 +1,24 @@
// ==========================================================================
// Plyr
// plyr.js v3.0.5
// plyr.js v3.3.2
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import { providers, types } from './types';
import defaults from './defaults';
import support from './support';
import utils from './utils';
import captions from './captions';
import Console from './console';
import controls from './controls';
import defaults from './defaults';
import Fullscreen from './fullscreen';
import Listeners from './listeners';
import Storage from './storage';
import Ads from './plugins/ads';
import captions from './captions';
import controls from './controls';
import media from './media';
import Ads from './plugins/ads';
import source from './source';
import Storage from './storage';
import support from './support';
import { providers, types } from './types';
import ui from './ui';
import utils from './utils';
// Private properties
// TODO: Use a WeakMap for private globals
@@ -57,7 +55,7 @@ class Plyr {
this.config = utils.extend(
{},
defaults,
options,
options || {},
(() => {
try {
return JSON.parse(this.media.getAttribute('data-plyr-config'));
@@ -97,6 +95,7 @@ class Plyr {
this.options = {
speed: [],
quality: [],
captions: [],
};
// Debugging
@@ -133,7 +132,9 @@ class Plyr {
}
// Cache original element state for .destroy()
this.elements.original = this.media.cloneNode(true);
const clone = this.media.cloneNode(true);
clone.autoplay = false;
this.elements.original = clone;
// Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube
@@ -174,12 +175,17 @@ class Plyr {
if (truthy.includes(params.autoplay)) {
this.config.autoplay = true;
}
if (truthy.includes(params.playsinline)) {
this.config.inline = true;
}
if (truthy.includes(params.loop)) {
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 {
// <div> with attributes
@@ -213,7 +219,7 @@ class Plyr {
this.config.autoplay = true;
}
if (this.media.hasAttribute('playsinline')) {
this.config.inline = true;
this.config.playsinline = true;
}
if (this.media.hasAttribute('muted')) {
this.config.muted = true;
@@ -230,7 +236,7 @@ class Plyr {
}
// 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 (!this.supported.api) {
@@ -286,6 +292,11 @@ class Plyr {
// Setup ads if provided
this.ads = new Ads(this);
// Autoplay if required
if (this.config.autoplay) {
this.play();
}
}
// ---------------------------------------
@@ -322,11 +333,6 @@ class Plyr {
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 this.media.play();
}
@@ -342,6 +348,13 @@ class Plyr {
this.media.pause();
}
/**
* Get playing state
*/
get playing() {
return Boolean(this.ready && !this.paused && !this.ended);
}
/**
* Get paused state
*/
@@ -350,10 +363,10 @@ class Plyr {
}
/**
* Get playing state
* Get stopped state
*/
get playing() {
return Boolean(!this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true));
get stopped() {
return Boolean(this.paused && this.currentTime === 0);
}
/**
@@ -384,7 +397,7 @@ class Plyr {
stop() {
if (this.isHTML5) {
this.media.load();
} else {
} else if (utils.is.function(this.media.stop)) {
this.media.stop();
}
}
@@ -431,7 +444,7 @@ class Plyr {
}
// Set
this.media.currentTime = parseFloat(targetTime.toFixed(4));
this.media.currentTime = targetTime;
// Logging
this.debug.log(`Seeking to ${this.currentTime} seconds`);
@@ -477,7 +490,7 @@ class Plyr {
*/
get duration() {
// Faux duration set via config
const fauxDuration = parseInt(this.config.duration, 10);
const fauxDuration = parseFloat(this.config.duration);
// True duration
const realDuration = this.media ? Number(this.media.duration) : 0;
@@ -524,8 +537,8 @@ class Plyr {
// Set the player volume
this.media.volume = volume;
// If muted, and we're increasing volume, reset muted state
if (this.muted && volume > 0) {
// If muted, and we're increasing volume manually, reset muted state
if (!utils.is.empty(value) && this.muted && volume > 0) {
this.muted = false;
}
}
@@ -655,29 +668,38 @@ class Plyr {
/**
* Set playback quality
* Currently YouTube only
* @param {string} input - Quality level
* Currently HTML5 & YouTube only
* @param {number} input - Quality level
*/
set quality(input) {
let quality = null;
if (utils.is.string(input)) {
quality = input;
if (!utils.is.empty(input)) {
quality = Number(input);
}
if (!utils.is.string(quality)) {
if (!utils.is.number(quality) || quality === 0) {
quality = this.storage.get('quality');
}
if (!utils.is.string(quality)) {
if (!utils.is.number(quality)) {
quality = this.config.quality.selected;
}
if (!this.options.quality.includes(quality)) {
this.debug.warn(`Unsupported quality option (${quality})`);
if (!utils.is.number(quality)) {
quality = this.config.quality.default;
}
if (!this.options.quality.length) {
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
this.config.quality.selected = quality;
@@ -769,17 +791,18 @@ class Plyr {
}
/**
* Set the poster image for a HTML5 video
* Set the poster image for a video
* @param {input} - the URL for the new poster image
*/
set poster(input) {
if (!this.isHTML5 || !this.isVideo) {
this.debug.warn('Poster can only be set on HTML5 video');
if (!this.isVideo) {
this.debug.warn('Poster can only be set for video');
return;
}
if (utils.is.string(input)) {
this.media.setAttribute('poster', input);
ui.setPoster.call(this);
}
}
@@ -787,7 +810,7 @@ class Plyr {
* Get the current poster image
*/
get poster() {
if (!this.isHTML5 || !this.isVideo) {
if (!this.isVideo) {
return null;
}
@@ -815,13 +838,13 @@ class Plyr {
* @param {boolean} input - Whether to enable captions
*/
toggleCaptions(input) {
// If there's no full support, or there's no caption toggle
if (!this.supported.ui || !utils.is.element(this.elements.buttons.captions)) {
// If there's no full support
if (!this.supported.ui) {
return;
}
// 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...
if (this.captions.active === show) {
@@ -851,17 +874,29 @@ class Plyr {
return;
}
// Toggle captions based on input
this.toggleCaptions(!utils.is.empty(input));
// If empty string is passed, assume disable captions
if (utils.is.empty(input)) {
this.toggleCaptions(false);
return;
}
// Normalize
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 (this.language === language) {
return;
@@ -1019,6 +1054,11 @@ class Plyr {
// then set the timer to hide the controls
if (!show || this.playing) {
this.timers.controls = setTimeout(() => {
// We need controls of course...
if (!utils.is.element(this.elements.controls)) {
return;
}
// If the mouse is over the controls (and not entering fullscreen), bail
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
return;
@@ -1029,8 +1069,8 @@ class Plyr {
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
}
// Check if controls toggled
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, true);
// Set hideControls class
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, this.config.hideControls);
// Trigger event and close menu
if (toggled) {
+1 -3
View File
@@ -1,14 +1,12 @@
// ==========================================================================
// Plyr Polyfilled Build
// plyr.js v3.0.5
// plyr.js v3.3.2
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import 'babel-polyfill';
import 'custom-event-polyfill';
import Plyr from './plyr';
export default Plyr;
+11 -9
View File
@@ -2,11 +2,12 @@
// Plyr source update
// ==========================================================================
import { providers } from './types';
import utils from './utils';
import html5 from './html5';
import media from './media';
import ui from './ui';
import support from './support';
import { providers } from './types';
import ui from './ui';
import utils from './utils';
const source = {
// Add elements to HTML5 media (source, tracks, etc)
@@ -31,13 +32,14 @@ const source = {
}
// Cancel current network requests
media.cancelRequests.call(this);
html5.cancelRequests.call(this);
// Destroy instance and re-setup
this.destroy.call(
this,
() => {
// TODO: Reset menus here
// Reset quality options
this.options.quality = [];
// Remove elements
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;
// 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
switch (`${this.provider}:${this.type}`) {
@@ -92,8 +94,8 @@ const source = {
if (this.config.autoplay) {
this.media.setAttribute('autoplay', '');
}
if ('poster' in input) {
this.media.setAttribute('poster', input.poster);
if (!utils.is.empty(input.poster)) {
this.poster = input.poster;
}
if (this.config.loop.active) {
this.media.setAttribute('loop', '');
@@ -101,7 +103,7 @@ const source = {
if (this.config.muted) {
this.media.setAttribute('muted', '');
}
if (this.config.inline) {
if (this.config.playsinline) {
this.media.setAttribute('playsinline', '');
}
}
+10 -5
View File
@@ -12,16 +12,16 @@ const support = {
// Check for support
// Basic functionality vs full UI
check(type, provider, inline) {
check(type, provider, playsinline) {
let api = false;
let ui = false;
const browser = utils.getBrowser();
const playsInline = browser.isIPhone && inline && support.inline;
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
switch (`${provider}:${type}`) {
case 'html5:video':
api = support.video;
ui = api && support.rangeInput && (!browser.isIPhone || playsInline);
ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline);
break;
case 'html5:audio':
@@ -32,7 +32,7 @@ const support = {
case 'youtube:video':
case 'vimeo:video':
api = true;
ui = support.rangeInput && (!browser.isIPhone || playsInline);
ui = support.rangeInput && (!browser.isIPhone || canPlayInline);
break;
default:
@@ -59,7 +59,7 @@ const support = {
// Inline playback support
// 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
// Credits: http://diveintohtml5.info/everything.html
@@ -73,6 +73,11 @@ const support = {
return false;
}
// Check directly if codecs specified
if (type.includes('codecs=')) {
return media.canPlayType(type).replace(/no/, '');
}
// Type specific checks
if (this.isVideo) {
switch (type) {
+42 -12
View File
@@ -2,10 +2,14 @@
// Plyr UI
// ==========================================================================
import utils from './utils';
import captions from './captions';
import controls from './controls';
import i18n from './i18n';
import support from './support';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
const ui = {
addStyleHook() {
@@ -48,11 +52,6 @@ const ui = {
this.listeners.controls();
}
// If there's no controls, bail
if (!utils.is.element(this.elements.controls)) {
return;
}
// Remove native controls
ui.toggleNativeControls.call(this);
@@ -71,8 +70,11 @@ const ui = {
// Reset loop state
this.loop = null;
// Reset quality options
this.options.quality = [];
// Reset quality setting
this.quality = null;
// Reset volume display
ui.updateVolume.call(this);
// Reset time display
ui.timeUpdate.call(this);
@@ -80,6 +82,18 @@ const ui = {
// Update the UI
ui.checkPlaying.call(this);
// Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
// Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls
this.ready = true;
@@ -90,6 +104,9 @@ const ui = {
// Set the title
ui.setTitle.call(this);
// Set the poster image
ui.setPoster.call(this);
},
// Setup aria attribute for play and iframe title
@@ -123,16 +140,29 @@ const ui = {
// Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', i18n.get('frameTitle', this.config));
iframe.setAttribute('title', format.replace('{title}', title));
}
},
// Set the poster image
setPoster() {
if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) {
return;
}
// Set the inline style
const posters = this.poster.split(',');
this.elements.poster.style.backgroundImage = posters.map(p => `url('${p}')`).join(',');
},
// Check playing state
checkPlaying() {
// Class hooks
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused);
utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set ARIA state
utils.toggleState(this.elements.buttons.play, this.playing);
@@ -274,10 +304,10 @@ const ui = {
}
// Always display hours if duration is over an hour
const displayHours = utils.getHours(this.duration) > 0;
const forceHours = utils.getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign
target.textContent = utils.formatTime(time, displayHours, inverted);
target.textContent = utils.formatTime(time, forceHours, inverted);
},
// Handle time change event
+53 -75
View File
@@ -3,7 +3,6 @@
// ==========================================================================
import loadjs from 'loadjs';
import support from './support';
import { providers } from './types';
@@ -269,14 +268,14 @@ const utils = {
parent.appendChild(utils.createElement(type, attributes, text));
},
// Remove an element
// Remove element(s)
removeElement(element) {
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
if (utils.is.nodeList(element) || utils.is.array(element)) {
Array.from(element).forEach(utils.removeElement);
return;
}
if (utils.is.nodeList(element) || utils.is.array(element)) {
Array.from(element).forEach(utils.removeElement);
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
return;
}
@@ -375,6 +374,25 @@ const utils = {
return attributes;
},
// Toggle hidden
toggleHidden(element, hidden) {
if (!utils.is.element(element)) {
return;
}
let hide = hidden;
if (!utils.is.boolean(hide)) {
hide = !element.hasAttribute('hidden');
}
if (hide) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
},
// Toggle class on an element
toggleClass(element, className, toggle) {
if (utils.is.element(element)) {
@@ -393,19 +411,6 @@ const utils = {
return utils.is.element(element) && element.classList.contains(className);
},
// Toggle hidden attribute on an element
toggleHidden(element, toggle) {
if (!utils.is.element(element)) {
return;
}
if (toggle) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
},
// Element matches selector
matches(element, selector) {
const prototype = { Element };
@@ -429,60 +434,6 @@ const utils = {
return this.elements.container.querySelector(selector);
},
// Find the UI controls and store references in custom controls
// TODO: Allow settings menus with custom controls
findElements() {
try {
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
// Buttons
this.elements.buttons = {
play: utils.getElements.call(this, this.config.selectors.buttons.play),
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
};
// Progress
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
// Inputs
this.elements.inputs = {
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
};
// Display
this.elements.display = {
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
duration: utils.getElement.call(this, this.config.selectors.display.duration),
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
};
// Seek tooltip
if (utils.is.element(this.elements.progress)) {
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
}
return true;
} catch (error) {
// Log it
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
// Restore native video controls
this.toggleNativeControls(true);
return false;
}
},
// Get the focused element
getFocusElement() {
let focused = document.activeElement;
@@ -586,15 +537,15 @@ const utils = {
},
// Trigger event
dispatchEvent(element, type, bubbles, detail) {
dispatchEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element
if (!utils.is.element(element) || !utils.is.string(type)) {
if (!utils.is.element(element) || utils.is.empty(type)) {
return;
}
// Create and dispatch the event
const event = new CustomEvent(type, {
bubbles: utils.is.boolean(bubbles) ? bubbles : false,
bubbles,
detail: Object.assign({}, detail, {
plyr: utils.is.plyr(this) ? this : null,
}),
@@ -626,6 +577,15 @@ const utils = {
element.setAttribute('aria-pressed', state);
},
// Format string
format(input, ...args) {
if (utils.is.empty(input)) {
return input;
}
return input.toString().replace(/{(\d+)}/g, (match, i) => utils.is.string(args[i]) ? args[i] : '');
},
// Get percentage
getPercentage(current, max) {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
@@ -737,6 +697,24 @@ const utils = {
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
getProviderByUrl(url) {
// YouTube
+6
View File
@@ -26,6 +26,12 @@
width: 100%;
}
button {
font: inherit;
line-height: inherit;
width: auto;
}
// Ignore focus
&:focus {
outline: 0;
+15 -10
View File
@@ -3,14 +3,12 @@
// YouTube, Vimeo, etc
// --------------------------------------------------------------
.plyr__video-embed {
// Default to 16:9 ratio but this is set by JavaScript based on config
$padding: ((100 / 16) * 9);
$height: 240;
$offset: to-percentage(($height - $padding) / ($height / 50));
// Default to 16:9 ratio but this is set by JavaScript based on config
$embed-padding: ((100 / 16) * 9);
.plyr__video-embed {
height: 0;
padding-bottom: to-percentage($padding);
padding-bottom: to-percentage($embed-padding);
position: relative;
iframe {
@@ -22,6 +20,17 @@
user-select: none;
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
> div {
@@ -30,7 +39,3 @@
transform: translateY(-$offset);
}
}
// To allow mouse events to be captured if full support
.plyr--full-ui .plyr__video-embed iframe {
pointer-events: none;
}
+2 -1
View File
@@ -35,7 +35,7 @@
right: -3px;
text-align: left;
white-space: nowrap;
z-index: 1;
z-index: 3;
> div {
overflow: hidden;
@@ -74,6 +74,7 @@
align-items: center;
color: $plyr-menu-color;
display: flex;
font-size: $plyr-font-size-menu;
padding: ceil($plyr-control-padding / 2) ($plyr-control-padding * 2);
user-select: none;
width: 100%;
+22
View File
@@ -0,0 +1,22 @@
// --------------------------------------------------------------
// Faux poster overlay
// --------------------------------------------------------------
.plyr__poster {
background-color: #000;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: 100% 100%;
height: 100%;
left: 0;
opacity: 0;
position: absolute;
top: 0;
transition: opacity 0.3s ease;
width: 100%;
z-index: 1;
}
.plyr--stopped .plyr__poster {
opacity: 1;
}
+1 -1
View File
@@ -19,7 +19,7 @@
&::-webkit-slider-runnable-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 {
+7 -2
View File
@@ -23,7 +23,12 @@
// Hide sound controls on iOS
// It's not supported to change volume using JavaScript:
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
.plyr--is-ios .plyr__volume,
.plyr--is-ios [data-plyr='mute'] {
.plyr--is-ios .plyr__volume {
display: none !important;
}
// Vimeo has no toggle mute method so hide mute button
// https://github.com/vimeo/player.js/issues/236#issuecomment-384663183
.plyr--is-ios.plyr--vimeo [data-plyr='mute'] {
display: none !important;
}
+1
View File
@@ -32,6 +32,7 @@
@import 'components/embed';
@import 'components/menus';
@import 'components/progress';
@import 'components/poster';
@import 'components/sliders';
@import 'components/times';
@import 'components/tooltips';
+2 -1
View File
@@ -8,8 +8,9 @@ $plyr-font-size-small: 14px !default;
$plyr-font-size-large: 18px !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-menu: $plyr-font-size-small !default;
$plyr-font-weight-regular: 500 !default;
$plyr-font-weight-bold: 600 !default;
-9
View File
@@ -2,15 +2,6 @@
// Hiding content nicely
// --------------------------------------------------------------
// Attributes
.plyr--full-ui [hidden] {
display: none;
}
.plyr--full-ui [aria-hidden='true'] {
display: none;
}
// Screen reader only elements
.plyr__sr-only {
clip: rect(1px, 1px, 1px, 1px);
+1787 -113
View File
File diff suppressed because it is too large Load Diff