Merge branch 'develop' into css-variables

# Conflicts:
#	demo/dist/demo.css
#	demo/dist/demo.min.js.map
#	demo/index.html
#	dist/plyr.css
#	dist/plyr.min.js.map
#	dist/plyr.min.mjs.map
#	dist/plyr.polyfilled.min.js.map
#	dist/plyr.polyfilled.min.mjs.map
#	gulpfile.js
#	src/sass/base.scss
#	src/sass/components/control.scss
#	src/sass/settings/colors.scss
#	src/sass/settings/controls.scss
This commit is contained in:
Sam Potts 2020-03-30 10:45:57 +11:00
commit da943b384c
110 changed files with 27759 additions and 10721 deletions

5
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,5 @@
# These are supported funding model platforms
github: sampotts
patreon: plyr
open_collective: plyr

2
.gitignore vendored
View File

@ -3,9 +3,7 @@ node_modules
credentials.json
*.mp4
!dist/blank.mp4
index-*.html
npm-debug.log
yarn-error.log
package-lock.json
*.webm
.idea/

6
.gitpod.yml Normal file
View File

@ -0,0 +1,6 @@
tasks:
- init: npm install && npm i gulp -g
command: gulp
ports:
- port: 3000
onOpen: open-preview

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v13.8.0

View File

@ -2,11 +2,11 @@
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
"dbaeumer.vscode-eslint",
"wix.vscode-import-cost",
"esbenp.prettier-vscode",
"shinnn.stylelint",
"wayou.vscode-todo-highlight"
"wayou.vscode-todo-highlight",
"wix.vscode-import-cost",
"stylelint.vscode-stylelint",
"pflannery.vscode-versionlens"
]
}

View File

@ -10,13 +10,15 @@
"src": "./src/js/plyr.polyfilled.js",
"dist": "./dist/",
"formats": ["es", "umd"],
"namespace": "Plyr"
"namespace": "Plyr",
"polyfill": true
},
"demo.js": {
"src": "./demo/src/js/demo.js",
"dist": "./demo/dist/",
"formats": ["iife"],
"namespace": "Demo"
"namespace": "Demo",
"polyfill": true
}
},
"css": {

View File

@ -1,4 +1,62 @@
## v3.5.4
### v3.5.10
- iOS volume display fix
### v3.5.9
- Fix for regression with volume control width
- Ensure poster image is not downloaded again for HTML5 videos
### v3.5.8
- Added `flex-direction` property to fix some issues introduced in v3.5.7 when using custom CSS
- Cleaned up the organisation of some of the SCSS files (should not effect CSS output)
- Added `referrerPolicy` option for Vimeo to prevent an issue present in the demo site
- Remove all Vimeo controls for Pro & Premium accounts
- Improve thumbnail size calculations when size is set per css (thanks @ydylla)
- Add previewThumbnails source setter (thanks @ydylla)
- More speed setting logic improvements
### v3.5.7
- Typescript typings (thanks @ondratra)
- `togglePlay` now also returns a `Promise` (thanks @azizhk)
- Documentation improvements and typo fixes (thanks @ffpetrovic, @skerbis, @ayunami2000, @pjbaert, @MaxGiting, @0xflotus and @thatrobotdev)
- Accessibility tweak for the play button (thanks @lunika)
- Fix for ads configuration (thanks @SoftCreatR)
- Fix handling listener return value (thanks @taion)
- Added localisation key for PIP (picture-in-picture) (thanks @lmislm)
- Preserve viewBox attribute in SVG sprite symbols (thanks @bseib)
- Fix being unable to unmute autoplayed video on iOS (thanks @sumanbh)
- Fixed Plyr container not resizing responsively (thanks @shravan2x)
- Change vimeo demo video (thanks @thatrobotdev)
- Fix for `Uncaught RangeError: Maximum call stack size exceeded` (thanks @laukstein)
- Improve fullscreen experience on some devices (thanks @savroff)
- Improvements to buffering state for embedded players (thanks @doostinharrell)
- Prevents IE11 with resetOnEnd option set to true to play video again (thanks @Felipe K. De Boni)
- Fix for multiple poster image downloads (use the native poster only for HTML5 videos)
- Various presentational fixes
- Removed logic to hide/show volume controls based on audio track detection due to it's problematic nature. If you want to hide volume control, use the `controls` option to do so.
- Fix preview thumbnail scrubbing not working on mobile touch devices (thanks @ydylla)
- Add download attribute to download button (thanks @Code1110)
- Trap keyboard focus only when fullscreen (thanks @k-jensen)
- Improvements to speed options - you can now specify all options in the UI (YouTube and Vimeo only accept 0.5-2) (thanks @ydylla)
- Improve/fix quality change state restoring (thanks @ydylla)
_Note:_ This update contains CSS changes.
### v3.5.6
- Another Edge fix (thanks Nick Hawk via Slack)
### v3.5.5
- YouTube fix for when there are other embeds on the page (thanks @aFarkas)
- Separated demo dependencies into their own package.json
- Fix for Edge controls flexbox issue when resizing the player (thanks Nick Hawk via Slack)
- More aspect ratio fixes
### v3.5.4
- Added: Set download URL via new setter
- Improvement: The order of the `controls` option now effects the order in the DOM - i.e. you can re-order the controls - Note: this may break any custom CSS you have setup. Please see the changes in the PR to the default SASS
@ -11,7 +69,7 @@
- Improvement: Automatic aspect ratio for YouTube is now supported, meaning all aspect ratios are set based on media content - Note: we're now using a different API to get YouTube video metadata so you may need to adjust any CSPs you have setup
- Fix for menu in the Shadow DOM (thanks @emielbeinema)
## v3.5.3
### v3.5.3
- Improved the usage of the `ratio` config option; it now works as expected and for all video types. The default has not changed, it is to dynamically, where possible (except YouTube where 16:9 is used) determine the ratio from the media source so this is not a breaking change.
- Added new `ratio` getter and setter
@ -19,11 +77,11 @@
- Fix: Allow absolute paths in preview thumbnails
- Improvement: Allow optional hours and ms in VTT parser in preview thumbnails
## v3.5.2
### v3.5.2
- Fixed issue where the preview thumbnail was present while scrubbing
## v3.5.1
### v3.5.1
- Fixed build issues with babel and browserslist
@ -153,7 +211,9 @@ Thanks @friday for the following:
- Suggestion: Remove array newline rule
- Contributions improvements
- fix: html5.cancelRequest not remove source tag correctly (thanks @a60814billy)
and other folks for:
- Fix: html5.cancelRequest not remove source tag correctly (thanks @a60814billy)
- remove event listeners in destroy() (thanks @cky917)
- Fix markdown in README (thanks @azu)
- Some parts of the accessibility improvements outlined in #905 (more on the way...)

View File

@ -27,7 +27,13 @@ Please follow the instructions in our issue templates. Don't use github issues t
* Fork Plyr, and create a new branch in your fork, based on the **develop** branch
* To test locally, you can use the demo. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build while you are working, and run a local server from the repository root directory. If you have Python installed, this command should work: `python -m SimpleHTTPServer 8080`. Then go to `http://localhost:8080/demo/`
* To test locally, you can use the demo site. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build and it will run a local web server for development and watch for any changes.
### Online one-click setup
Alternatively can also use Gitpod, a free online Visual Studio Code-like IDE. With a single click it will automatically launch a ready-to-code workspace with all the dependencies pre-installed, gulp watching for changes and the web server running, so that you can start coding straightaway.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/from-referrer/)
* Develop and test your modifications.

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

16373
demo/dist/demo.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
demo/dist/demo.svg vendored

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

2
demo/dist/error.css vendored

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@
<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" />
<!-- Opengraph -->
<!-- Open Graph -->
<meta
property="og:title"
content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player"
@ -66,7 +66,7 @@
<body>
<div class="grid">
<header>
<h1>Plyr</h1>
<h1>Pl<span>a</span>y<span>e</span>r</h1>
<p>
A simple, accessible and customisable media player for
<button type="button" class="faux-link" data-source="video">
@ -109,7 +109,7 @@
</p>
<p>
Premium video monitization from
Premium video monetization from
<a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi" />
<span class="sr-only">ai.vi</span>
@ -117,27 +117,25 @@
</p>
<div class="call-to-action">
<span class="button--with-count">
<a <a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr-button">
<svg class="icon" role="presentation">
<title>GitHub</title>
<path
d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr">
<svg class="icon" role="presentation">
<title>GitHub</title>
<path
d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6
C5.9,15.9,6,15.6,6,15.4c0-0.2,0-0.7,0-1.4C3.8,14.5,3.3,13,3.3,13c-0.4-0.9-0.9-1.2-0.9-1.2c-0.7-0.5,0.1-0.5,0.1-0.5
c0.8,0.1,1.2,0.8,1.2,0.8C4.4,13.4,5.6,13,6,12.8c0.1-0.5,0.3-0.9,0.5-1.1c-1.8-0.2-3.6-0.9-3.6-4c0-0.9,0.3-1.6,0.8-2.1
c-0.1-0.2-0.4-1,0.1-2.1c0,0,0.7-0.2,2.2,0.8c0.6-0.2,1.3-0.3,2-0.3c0.7,0,1.4,0.1,2,0.3c1.5-1,2.2-0.8,2.2-0.8
c0.4,1.1,0.2,1.9,0.1,2.1c0.5,0.6,0.8,1.3,0.8,2.1c0,3.1-1.9,3.7-3.7,3.9C9.7,12,10,12.5,10,13.2c0,1.1,0,1.9,0,2.2
c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z"
></path>
</svg>
Download on GitHub
</a>
</span>
></path>
</svg>
Download on GitHub
</a>
</div>
</header>
<main>
<!-- style="--plyr-color-main: #47bb4d; --plyr-video-control-bg-hover: var(--plyr-color-main); " -->
<div id="container">
<!-- style="--plyr-color-main: #47bb4d; --plyr-video-control-background-hover: var(--plyr-color-main); " -->
<div id="container" style="--plyr-color-main: #1ac266;">
<video
controls
crossorigin
@ -235,7 +233,8 @@
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/76979871" target="_blank">The New Vimeo Player</a> on&nbsp;
<a href="https://vimeo.com/40648169" target="_blank">Toob “Wavaphon” Music Video</a>
on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
@ -267,23 +266,13 @@
<a
href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank"
class="js-shr-button"
class="js-shr"
>tweet it</a
>
👍
</p>
</aside>
<!-- Polyfills -->
<script
src="https://cdn.polyfill.io/v3/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL,Math.trunc&flags=gated"
crossorigin="anonymous"
></script>
<!-- Sharing libary (https://shr.one) -->
<script src="https://cdn.shr.one/2.0.0-beta.2/shr.js" crossorigin="anonymous"></script>
<!-- Docs script -->
<script src="dist/demo.js" crossorigin="anonymous"></script>
</body>
</html>

14
demo/package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "plyr-demo",
"version": "1.0.0",
"description": "Demo for Plyr",
"homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>",
"dependencies": {
"core-js": "^3.6.4",
"custom-event-polyfill": "^1.0.7",
"raven-js": "^3.27.2",
"shr-buttons": "2.0.3",
"url-polyfill": "^1.1.8"
}
}

View File

@ -4,9 +4,16 @@
// Please see readme.md in the root or github.com/sampotts/plyr
// ==========================================================================
import './tab-focus';
import 'custom-event-polyfill';
import 'url-polyfill';
import Raven from 'raven-js';
import Shr from 'shr-buttons';
import Plyr from '../../../src/js/plyr';
import sources from './sources';
import toggleClass from './toggle-class';
(() => {
const { host } = window.location;
@ -18,45 +25,15 @@ import Plyr from '../../../src/js/plyr';
document.addEventListener('DOMContentLoaded', () => {
Raven.context(() => {
const selector = '#player';
const container = document.getElementById('container');
if (window.Shr) {
window.Shr.setup('.js-shr-button', {
count: {
classname: 'button__count',
},
});
}
// Setup tab focus
const tabClassName = 'tab-focus';
// Remove class on blur
document.addEventListener('focusout', event => {
if (!event.target.classList || container.contains(event.target)) {
return;
}
event.target.classList.remove(tabClassName);
});
// Add classname to tabbed elements
document.addEventListener('keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
const focused = document.activeElement;
if (!focused || !focused.classList || container.contains(focused)) {
return;
}
focused.classList.add(tabClassName);
}, 10);
// Setup share buttons
Shr.setup('.js-shr', {
count: {
className: 'button__count',
},
wrapper: {
className: 'button--with-count',
},
});
// Setup the player
@ -73,9 +50,6 @@ import Plyr from '../../../src/js/plyr';
captions: {
active: true,
},
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
enabled: env.prod || env.dev,
publisherId: '918848828995742',
@ -87,6 +61,10 @@ import Plyr from '../../../src/js/plyr';
'https://cdn.plyr.io/static/demo/thumbs/240p.vtt',
],
},
vimeo: {
// Prevent Vimeo blocking plyr.io demo site
referrerPolicy: 'no-referrer',
},
});
// Expose for tinkering in the console
@ -94,131 +72,12 @@ import Plyr from '../../../src/js/plyr';
// Setup type toggle
const buttons = document.querySelectorAll('[data-source]');
const types = {
video: 'video',
audio: 'audio',
youtube: 'youtube',
vimeo: 'vimeo',
};
let currentType = window.location.hash.replace('#', '');
const historySupport = window.history && window.history.pushState;
// Toggle class on an element
function toggleClass(element, className, state) {
if (element) {
element.classList[state ? 'add' : 'remove'](className);
}
}
// Set a new source
function newSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (
!(type in types) ||
(!init && type === currentType) ||
(!currentType.length && type === types.video)
) {
return;
}
switch (type) {
case types.video:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
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: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
};
break;
case types.audio:
player.source = {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
};
break;
case types.youtube:
player.source = {
type: 'video',
sources: [
{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
},
],
};
break;
case types.vimeo:
player.source = {
type: 'video',
sources: [
{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
},
],
};
break;
default:
break;
}
// Set the current type for next time
currentType = type;
const types = Object.keys(sources);
const historySupport = Boolean(window.history && window.history.pushState);
let currentType = window.location.hash.substring(1);
const hasCurrentType = !currentType.length;
function render(type) {
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
@ -227,9 +86,31 @@ import Plyr from '../../../src/js/plyr';
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
cite.setAttribute('hidden', '');
// eslint-disable-next-line no-param-reassign
cite.hidden = true;
});
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
document.querySelector(`.plyr__cite--${type}`).hidden = false;
}
// Set a new source
function setSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (
!types.includes(type) ||
(!init && type === currentType) ||
(!currentType.length && type === 'video')
) {
return;
}
// Set the new source
player.source = sources[type];
// Set the current type for next time
currentType = type;
render(type);
}
// Bind to each button
@ -237,7 +118,7 @@ import Plyr from '../../../src/js/plyr';
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
newSource(type);
setSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
@ -247,36 +128,27 @@ import Plyr from '../../../src/js/plyr';
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && 'type' in event.state) {
newSource(event.state.type);
if (event.state && Object.keys(event.state).includes('type')) {
setSource(event.state.type);
}
});
// On load
if (historySupport) {
const video = !currentType.length;
// If there's no current type set, assume video
if (video) {
currentType = types.video;
}
// Replace current history state
if (currentType in types) {
window.history.replaceState(
{
type: currentType,
},
'',
video ? '' : `#${currentType}`,
);
}
// If it's not video, load the source
if (currentType !== types.video) {
newSource(currentType, true);
}
// If there's no current type set, assume video
if (hasCurrentType) {
currentType = 'video';
}
// Replace current history state
if (historySupport && types.includes(currentType)) {
window.history.replaceState({ type: currentType }, '', hasCurrentType ? '' : `#${currentType}`);
}
// If it's not video, load the source
if (currentType !== 'video') {
setSource(currentType, true);
}
render(currentType);
});
});

78
demo/src/js/sources.js Normal file
View File

@ -0,0 +1,78 @@
const sources = {
video: {
type: 'video',
title: 'View From A Blue Moon',
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: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
},
audio: {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
},
youtube: {
type: 'video',
sources: [
{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
},
],
},
vimeo: {
type: 'video',
sources: [
{
src: 'https://vimeo.com/40648169',
provider: 'vimeo',
},
],
},
};
export default sources;

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

@ -0,0 +1,31 @@
// Setup tab focus
const container = document.getElementById('container');
const tabClassName = 'tab-focus';
// Remove class on blur
document.addEventListener('focusout', event => {
if (!event.target.classList || container.contains(event.target)) {
return;
}
event.target.classList.remove(tabClassName);
});
// Add classname to tabbed elements
document.addEventListener('keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
const focused = document.activeElement;
if (!focused || !focused.classList || container.contains(focused)) {
return;
}
focused.classList.add(tabClassName);
}, 10);
});

View File

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

View File

@ -6,11 +6,9 @@
.button,
.button__count {
align-items: center;
background: $color-button-background;
border: 0;
border-radius: $border-radius-base;
box-shadow: 0 1px 1px rgba(#000, 0.1);
color: $color-button-text;
display: inline-flex;
padding: ($spacing-base * 0.75);
position: relative;
@ -21,14 +19,16 @@
// Buttons
.button {
background: $color-button-background;
color: $color-button-text;
font-weight: $font-weight-bold;
padding-left: $spacing-base;
padding-right: $spacing-base;
padding-left: ($spacing-base * 1.25);
padding-right: ($spacing-base * 1.25);
transition: all 0.2s ease;
&:hover,
&:focus {
color: $gray-dark;
background: $color-button-background-hover;
// Remove the underline/border
&::after {
@ -65,12 +65,14 @@
// Count bubble
.button__count {
animation: fadein 0.2s ease;
margin-left: ($spacing-base / 2);
background: $color-button-count-background;
color: $color-button-count-text;
margin-left: ($spacing-base * 0.75);
&::before {
border: $arrow-size solid transparent;
border-left-width: 0;
border-right-color: $color-button-background;
border-right-color: $color-button-count-background;
content: '';
height: 0;
position: absolute;

View File

@ -6,6 +6,13 @@ header {
padding-bottom: $spacing-base;
text-align: center;
h1 span {
animation: shrinkHide 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s forwards;
display: inline-block;
font-weight: $font-weight-light;
opacity: 0.5;
}
.call-to-action {
margin-top: ($spacing-base * 1.5);
}
@ -15,5 +22,9 @@ header {
max-width: 360px;
padding-bottom: ($spacing-base * 2);
text-align: left;
p:first-of-type {
@include font-size($font-size-base + 1);
}
}
}

View File

@ -19,5 +19,5 @@ label svg {
a .icon,
.btn .icon {
margin-right: floor($spacing-base / 3);
margin-right: ($spacing-base / 2);
}

View File

@ -12,7 +12,6 @@ button.faux-link {
a {
border-bottom: 1px dotted currentColor;
color: $color-link;
font-weight: $font-weight-bold;
position: relative;
text-decoration: none;
transition: all 0.2s ease;

View File

@ -2,16 +2,10 @@
// Examples
// ==========================================================================
// For non supported browsers
video {
max-width: 100%;
vertical-align: middle;
}
// Example players
.plyr {
border-radius: $border-radius-base;
box-shadow: 0 2px 5px rgba(#000, 0.2);
box-shadow: 0 2px 15px rgba(#000, 0.1);
margin: $spacing-base auto;
&.plyr--audio {
@ -34,17 +28,9 @@ video {
// Style full supported player
.plyr__cite {
display: none;
margin-top: $spacing-base;
color: $color-gray-500;
.icon {
margin-right: ceil($spacing-base / 6);
}
}
.plyr--video:not(.plyr--youtube):not(.plyr--vimeo) ~ ul .plyr__cite--video,
.plyr--audio ~ ul .plyr__cite--audio,
.plyr--youtube ~ ul .plyr__cite--youtube,
.plyr--vimeo ~ ul .plyr__cite--vimeo {
display: block;
}

View File

@ -35,11 +35,10 @@ main {
aside {
align-items: center;
background: #fff;
color: $gray;
display: flex;
flex-shrink: 0;
justify-content: center;
padding: ($spacing-base * 0.75);
padding: $spacing-base;
position: relative;
text-align: center;
text-shadow: none;

View File

@ -11,3 +11,17 @@
opacity: 1;
}
}
@keyframes shrinkHide {
0% {
opacity: 0.5;
width: 38px;
}
20% {
width: 45px;
}
100% {
opacity: 0;
width: 0;
}
}

View File

@ -36,7 +36,7 @@
@return #{$rem}rem;
}
@mixin font-size($size: 16) {
@mixin font-size($size: $font-size-base) {
font-size: $size * 1px; // Fallback in px
font-size: calculate-rem($size);
}

View File

@ -2,31 +2,41 @@
// Colors
// ==========================================================================
// Greyscale
$gray-dark: #343f4a;
$gray: #55646b;
$gray-light: #cbd0d3;
$gray-lighter: #dbe3e8;
$off-white: #f2f5f7;
// Grayscale
$color-gray-900: hsl(210, 15%, 16%);
$color-gray-800: lighten($color-gray-900, 9%);
$color-gray-700: lighten($color-gray-800, 9%);
$color-gray-600: lighten($color-gray-700, 9%);
$color-gray-500: lighten($color-gray-600, 9%);
$color-gray-400: lighten($color-gray-500, 9%);
$color-gray-300: lighten($color-gray-400, 9%);
$color-gray-200: lighten($color-gray-300, 9%);
$color-gray-100: lighten($color-gray-200, 9%);
$color-gray-50: lighten($color-gray-100, 9%);
// Branding
$color-brand-primary: hsl(198, 100%, 50%);
// Text
$color-text: #fff;
// Plyr
$color-brand-primary: #1aafff;
$color-text: $color-gray-700;
$color-headings: $color-brand-primary;
// Brands
$color-twitter: #4baaf4;
$color-youtube: #cc181e;
$color-vimeo: #19b7ed;
// Elements
$color-link: #fff;
$color-background: $color-brand-primary;
$color-link: $color-brand-primary;
// Background
$color-background-from: hsl(198, 100%, 94%);
$color-background-to: hsl(198, 100%, 98%);
// Buttons
$color-button-background: #fff;
$color-button-text: $gray;
$color-button-background: $color-brand-primary;
$color-button-text: #fff;
$color-button-background-hover: hsl(198, 100%, 55%);
$color-button-count-background: #fff;
$color-button-count-text: $color-gray-600;
// Focus
$tab-focus-default-color: #fff;

View File

@ -9,4 +9,4 @@ $arrow-size: 5px;
$border-radius-base: 4px;
// Background
$page-background: linear-gradient(to left top, lighten($color-background, 10%), darken($color-background, 20%));
$page-background: linear-gradient(to left top, $color-background-from, $color-background-to);

View File

@ -14,6 +14,9 @@ $plyr-font-size-badges: 9px;
// Other
$plyr-font-smoothing: true;
// Colors
$plyr-color-main: $color-brand-primary;
// Captions
$plyr-font-size-captions-base: $plyr-font-size-base;
$plyr-font-size-captions-small: $plyr-font-size-small;

View File

@ -2,4 +2,4 @@
// Colors
// ==========================================================================
$spacing-base: 20px;
$spacing-base: 16px;

View File

@ -14,7 +14,6 @@ body {
font-family: $font-sans-serif;
font-weight: $font-weight-medium;
line-height: $line-height-base;
text-shadow: 0 1px 1px rgba(#000, 0.15);
}
button,
@ -26,7 +25,7 @@ textarea {
p,
small {
margin: 0 0 $spacing-base;
margin: 0 0 ($spacing-base * 1.5);
}
small {

View File

@ -4,8 +4,9 @@
h1 {
@include font-size($font-size-h1);
color: $color-headings;
font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-headings;
line-height: 1.2;
margin: 0 0 $spacing-base;
margin: 0 0 ($spacing-base * 1.5);
}

28
demo/yarn.lock Normal file
View File

@ -0,0 +1,28 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
core-js@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.4.tgz#3a2837fc48e582e1ae25907afcd6cf03b0cc7a07"
integrity sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==
custom-event-polyfill@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
raven-js@^3.27.2:
version "3.27.2"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.27.2.tgz#6c33df952026cd73820aa999122b7b7737a66775"
integrity sha512-mFWQcXnhRFEQe5HeFroPaEghlnqy7F5E2J3Fsab189ondqUzcjwSVi7el7F36cr6PvQYXoZ1P2F5CSF2/azeMQ==
shr-buttons@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/shr-buttons/-/shr-buttons-2.0.3.tgz#2ffd021fc3d789e1510ce2736b938bd09ea1da5a"
integrity sha512-sPAgHiw4uaIt9TnxTfyZEedDChcldSVtnBHE44cpe/mSC7rqm4IEKZRLYqnVlTcGM+FSDNBPUNpSf50Q2ntd+w==
url-polyfill@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.5.tgz#bec79b72b5407dba6d8cced2e32e4ab273aa9fb1"
integrity sha512-9XjIJ6nwrU+nGd8t90Ze0Zs7t8A+SU0gqsqPttj6j3zAVe5q0HFcuv37nDBdVSPpi4aTHTfbUF/i+ZVD+o2EbA==

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

1643
dist/plyr.js vendored

File diff suppressed because it is too large Load Diff

4
dist/plyr.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/plyr.min.mjs vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1639
dist/plyr.mjs vendored

File diff suppressed because it is too large Load Diff

4087
dist/plyr.polyfilled.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4083
dist/plyr.polyfilled.mjs vendored

File diff suppressed because it is too large Load Diff

2
dist/plyr.svg vendored

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -1,493 +1,8 @@
// ==========================================================================
// Gulp build script
// ==========================================================================
/* global require, __dirname */
/* eslint no-console: "off" */
const path = require('path');
const gulp = require('gulp');
// ------------------------------------
// JavaScript
// ------------------------------------
const terser = require('gulp-terser');
const rollup = require('gulp-better-rollup');
const babel = require('rollup-plugin-babel');
const commonjs = require('rollup-plugin-commonjs');
const resolve = require('rollup-plugin-node-resolve');
// ------------------------------------
// CSS
// ------------------------------------
const sass = require('gulp-sass');
const clean = require('gulp-clean-css');
const prefix = require('gulp-autoprefixer');
const postcss = require('gulp-postcss');
const customprops = require('postcss-custom-properties');
// ------------------------------------
// Images
// ------------------------------------
const svgstore = require('gulp-svgstore');
const imagemin = require('gulp-imagemin');
// ------------------------------------
// Utils
// ------------------------------------
const del = require('del');
const filter = require('gulp-filter');
const header = require('gulp-header');
const gitbranch = require('git-branch');
const rename = require('gulp-rename');
const replace = require('gulp-replace');
const ansi = require('ansi-colors');
const log = require('fancy-log');
const open = require('gulp-open');
const plumber = require('gulp-plumber');
const size = require('gulp-size');
const sourcemaps = require('gulp-sourcemaps');
const through = require('through2');
// ------------------------------------
// Deployment
// ------------------------------------
const aws = require('aws-sdk');
const publish = require('gulp-awspublish');
const FastlyPurge = require('fastly-purge');
// ------------------------------------
// Configs
// ------------------------------------
const pkg = require('./package.json');
const build = require('./build.json');
const deploy = require('./deploy.json');
// ------------------------------------
// Info from package
// ------------------------------------
const { browserslist: browsers, version } = pkg;
const minSuffix = '.min';
const HubRegistry = require('gulp-hub');
// Get AWS config
Object.values(deploy).forEach(target => {
Object.assign(target, {
publisher: publish.create({
region: target.region,
params: {
Bucket: target.bucket,
},
credentials: new aws.SharedIniFileCredentials({ profile: 'plyr' }),
}),
});
});
// Paths
const paths = {
plyr: {
// Source paths
src: {
sass: path.join(__dirname, 'src/sass/**/*.scss'),
js: path.join(__dirname, 'src/js/**/*.js'),
sprite: path.join(__dirname, 'src/sprite/*.svg'),
},
// Output paths
output: path.join(__dirname, 'dist/'),
},
demo: {
// Source paths
src: {
sass: path.join(__dirname, 'demo/src/sass/**/*.scss'),
js: path.join(__dirname, 'demo/src/js/**/*.js'),
},
// Output paths
output: path.join(__dirname, 'demo/dist/'),
// Demo
root: path.join(__dirname, 'demo/'),
},
upload: [
path.join(__dirname, `dist/*${minSuffix}.*`),
path.join(__dirname, 'dist/*.css'),
path.join(__dirname, 'dist/*.svg'),
path.join(__dirname, `demo/dist/*${minSuffix}.*`),
path.join(__dirname, 'demo/dist/*.css'),
path.join(__dirname, 'demo/dist/*.svg'),
],
};
// Task arrays
const tasks = {
css: [],
js: [],
sprite: [],
clean: 'clean',
};
// Size plugin
const sizeOptions = { showFiles: true, gzip: true };
// Clean out /dist
gulp.task(tasks.clean, done => {
const dirs = [paths.plyr.output, paths.demo.output].map(dir => path.join(dir, '**/*'));
// Don't delete the mp4
dirs.push(`!${path.join(paths.plyr.output, '**/*.mp4')}`);
del(dirs);
done();
});
// JavaScript
Object.entries(build.js).forEach(([filename, entry]) => {
entry.formats.forEach(format => {
const name = `js:${filename}:${format}`;
tasks.js.push(name);
const polyfill = filename.includes('polyfilled');
const extension = format === 'es' ? 'mjs' : 'js';
gulp.task(name, () =>
gulp
.src(entry.src)
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(
rollup(
{
plugins: [
resolve(),
commonjs(),
babel({
presets: [
[
'@babel/env',
{
// debug: true,
useBuiltIns: polyfill ? 'usage' : false,
corejs: polyfill ? 3 : undefined,
},
],
],
babelrc: false,
exclude: [/\/core-js\//],
}),
],
},
{
name: entry.namespace,
format,
},
),
)
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
.pipe(
rename({
extname: `.${extension}`,
}),
)
.pipe(gulp.dest(entry.dist))
.pipe(filter(`**/*.${extension}`))
.pipe(terser())
.pipe(rename({ suffix: minSuffix }))
.pipe(size(sizeOptions))
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(entry.dist)),
);
});
});
// CSS
Object.entries(build.css).forEach(([filename, entry]) => {
const name = `css:${filename}`;
tasks.css.push(name);
gulp.task(name, () =>
gulp
.src(entry.src)
.pipe(plumber())
.pipe(sass())
.pipe(
prefix(browsers, {
cascade: false,
}),
)
.pipe(postcss([customprops()]))
.pipe(clean())
.pipe(size(sizeOptions))
.pipe(gulp.dest(entry.dist)),
);
});
// SVG Sprites
Object.entries(build.sprite).forEach(([filename, entry]) => {
const name = `sprite:${filename}`;
tasks.sprite.push(name);
gulp.task(name, () =>
gulp
.src(entry.src)
.pipe(plumber())
.pipe(imagemin())
.pipe(svgstore())
.pipe(rename({ basename: path.parse(filename).name }))
.pipe(size(sizeOptions))
.pipe(gulp.dest(entry.dist)),
);
});
// Build all JS
gulp.task('js', () => gulp.parallel(...tasks.js));
// Watch for file changes
gulp.task('watch', () => {
// Plyr core
gulp.watch(paths.plyr.src.js, gulp.parallel(...tasks.js));
gulp.watch(paths.plyr.src.sass, gulp.parallel(...tasks.css));
gulp.watch(paths.plyr.src.sprite, gulp.parallel(...tasks.sprite));
// Demo
gulp.watch(paths.demo.src.js, gulp.parallel(...tasks.js));
gulp.watch(paths.demo.src.sass, gulp.parallel(...tasks.css));
});
// Build distribution
gulp.task('build', gulp.series(tasks.clean, gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite)));
// Default gulp task
gulp.task('default', gulp.series('build', 'watch'));
// Publish a version to CDN and demo
// --------------------------------------------
// Get deployment config
let credentials = {};
try {
credentials = require('./credentials.json'); //eslint-disable-line
} catch (e) {
// Do nothing
}
// Get branch info
const branch = {
current: gitbranch.sync(),
master: 'master',
beta: 'beta',
};
const maxAge = 31536000; // 1 year
const options = {
cdn: {
headers: {
'Cache-Control': `max-age=${maxAge}`,
},
},
demo: {
uploadPath: branch.current === branch.beta ? 'beta' : null,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
},
},
symlinks(ver, filename) {
return {
headers: {
// http://stackoverflow.com/questions/2272835/amazon-s3-object-redirect
'x-amz-website-redirect-location': `/${ver}/${filename}`,
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
},
};
},
};
const regex =
'(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?';
const semver = new RegExp(`v${regex}`, 'gi');
const localPath = new RegExp('(../)?dist', 'gi');
const versionPath = `https://${deploy.cdn.domain}/${version}`;
const cdnpath = new RegExp(`${deploy.cdn.domain}/${regex}/`, 'gi');
const renameFile = rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
});
// Check we're on the correct branch to deploy
const canDeploy = () => {
const allowed = [branch.master, branch.beta];
if (!allowed.includes(branch.current)) {
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
return false;
}
return true;
};
gulp.task('version', done => {
if (!canDeploy()) {
done();
return null;
}
const { domain } = deploy.cdn;
log(`Uploading ${ansi.green.bold(version)} to ${ansi.cyan(domain)}...`);
// Replace versioned URLs in source
const files = ['plyr.js', 'plyr.polyfilled.js', 'config/defaults.js'];
return gulp
.src(files.map(file => path.join(__dirname, `src/js/${file}`)), { base: '.' })
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${domain}/${version}/`))
.pipe(gulp.dest('./'));
});
// Publish version to CDN bucket
gulp.task('cdn', done => {
if (!canDeploy()) {
done();
return null;
}
const { domain, publisher } = deploy.cdn;
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`);
// Upload to CDN
return (
gulp
.src(paths.upload)
.pipe(renameFile)
// Remove min suffix from source map URL
.pipe(
replace(
/sourceMappingURL=([\w-?.]+)/,
(match, filename) => `sourceMappingURL=${filename.replace(minSuffix, '')}`,
),
)
.pipe(size(sizeOptions))
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.cdn.headers))
.pipe(publish.reporter())
);
});
// Purge the fastly cache incase any 403/404 are cached
gulp.task('purge', () => {
if (!Object.keys(credentials).includes('fastly')) {
throw new Error('Fastly credentials required to purge cache.');
}
const { fastly } = credentials;
const list = [];
return gulp
.src(paths.upload)
.pipe(
through.obj((file, enc, cb) => {
const filename = file.path.split('/').pop();
list.push(`${versionPath}/${filename.replace(minSuffix, '')}`);
cb(null);
}),
)
.on('end', () => {
const purge = new FastlyPurge(fastly.token);
list.forEach(url => {
log(`Purging ${ansi.cyan(url)}...`);
purge.url(url, (error, result) => {
if (error) {
log.error(error);
} else if (result) {
log(result);
}
});
});
});
});
// Publish to demo bucket
gulp.task('demo', done => {
if (!canDeploy()) {
done();
return null;
}
const { publisher } = deploy.demo;
const { domain } = deploy.cdn;
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`);
// Replace versioned files in readme.md
gulp.src([`${__dirname}/readme.md`])
.pipe(replace(cdnpath, `${domain}/${version}/`))
.pipe(gulp.dest(__dirname));
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
const index = `${paths.demo.root}index.html`;
const error = `${paths.demo.root}error.html`;
const pages = [index];
if (branch.current === branch.master) {
pages.push(error);
}
return gulp
.src(pages)
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.demo.headers))
.pipe(publish.reporter());
});
gulp.task('error', done => {
// Only update CDN for master (prod)
if (!canDeploy() || branch.current !== branch.master) {
done();
return null;
}
const { publisher } = deploy.cdn;
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
// Upload error.html to cdn
return gulp
.src(`${paths.demo.root}error.html`)
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.demo.headers))
.pipe(publish.reporter());
});
// Open the demo site to check it's ok
gulp.task('open', () => {
const { domain } = deploy.demo;
return gulp.src(__filename).pipe(
open({
uri: `https://${domain}/${branch.current === branch.beta ? 'beta' : ''}`,
}),
);
});
// Do everything
gulp.task(
'deploy',
gulp.series(
'version',
tasks.clean,
gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite),
'cdn',
'demo',
'purge',
'open',
),
);
gulp.registry(new HubRegistry(['tasks/*.js']));

View File

@ -1,10 +1,11 @@
{
"name": "plyr",
"version": "3.5.4",
"version": "3.5.10",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>",
"main": "dist/plyr.js",
"types": "src/js/plyr.d.ts",
"module": "dist/plyr.min.mjs",
"jsnext:main": "dist/plyr.min.mjs",
"browser": "dist/plyr.min.js",
@ -33,65 +34,67 @@
"lint": "eslint src/js && npm run-script remark",
"lint:fix": "eslint --fix src/js",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"deploy": "yarn lint && gulp deploy"
"deploy": "yarn lint && gulp version && gulp build && gulp deploy",
"prettier": "prettier --write ./src/js/*.js"
},
"devDependencies": {
"ansi-colors": "^3.2.4",
"aws-sdk": "^2.466.0",
"@babel/core": "^7.4.5",
"@babel/preset-env": "^7.4.5",
"babel-eslint": "^10.0.1",
"del": "^4.1.1",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^4.3.0",
"eslint-plugin-import": "^2.17.3",
"eslint-plugin-simple-import-sort": "^3.1.1",
"ansi-colors": "^4.1.1",
"autoprefixer": "^9.7.5",
"aws-sdk": "^2.648.0",
"@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.0",
"babel-eslint": "^10.1.0",
"browser-sync": "^2.26.7",
"del": "^5.1.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.1.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-simple-import-sort": "^5.0.2",
"fancy-log": "^1.3.3",
"fastly-purge": "^1.0.1",
"git-branch": "^2.0.1",
"gulp": "^4.0.2",
"gulp-autoprefixer": "^6.1.0",
"gulp-awspublish": "^4.0.0",
"gulp-awspublish": "^4.1.1",
"gulp-better-rollup": "^4.0.1",
"gulp-clean-css": "^4.2.0",
"gulp-filter": "^6.0.0",
"gulp-header": "^2.0.7",
"gulp-imagemin": "^6.0.0",
"gulp-header": "^2.0.9",
"gulp-hub": "^4.2.0",
"gulp-imagemin": "^7.1.0",
"gulp-open": "^3.0.1",
"gulp-plumber": "^1.2.1",
"gulp-postcss": "^8.0.0",
"gulp-rename": "^1.4.0",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.0.0",
"gulp-sass": "^4.0.2",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-svgstore": "^7.0.1",
"gulp-terser": "^1.2.0",
"postcss-custom-properties": "^8.0.10",
"prettier-eslint": "^8.8.2",
"postcss-clean": "^1.1.0",
"postcss-custom-properties": "^9.1.1",
"prettier-eslint": "^9.0.1",
"prettier-stylelint": "^0.4.2",
"remark-cli": "^6.0.1",
"remark-validate-links": "^8.0.2",
"rollup": "^1.13.0",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-commonjs": "^10.0.0",
"rollup-plugin-node-resolve": "^5.0.1",
"stylelint": "^10.0.1",
"stylelint-config-prettier": "^5.2.0",
"stylelint-config-recommended": "^2.2.0",
"stylelint-config-sass-guidelines": "^6.0.0",
"stylelint-order": "^3.0.0",
"stylelint-scss": "^3.8.0",
"remark-cli": "^7.0.1",
"remark-validate-links": "^10.0.0",
"rollup": "^2.2.0",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"stylelint": "^13.2.1",
"stylelint-config-prettier": "^8.0.1",
"stylelint-config-recommended": "^3.0.0",
"stylelint-config-sass-guidelines": "^7.0.0",
"stylelint-order": "^4.0.0",
"stylelint-scss": "^3.16.0",
"stylelint-selector-bem-pattern": "^2.1.0",
"through2": "^3.0.1"
},
"dependencies": {
"core-js": "^3.1.3",
"core-js": "^3.6.4",
"custom-event-polyfill": "^1.0.7",
"loadjs": "^3.6.1",
"rangetouch": "^2.0.0",
"raven-js": "^3.27.1",
"url-polyfill": "^1.1.5"
"loadjs": "^4.2.0",
"rangetouch": "^2.0.1",
"url-polyfill": "^1.1.8"
}
}

View File

@ -9,22 +9,29 @@
"**/node_modules": true,
"**/dist": true
},
// Linting
"stylelint.enable": true,
"css.validate": false,
"scss.validate": false,
"javascript.validate.enable": false,
// Prettier
"prettier.eslintIntegration": true,
"prettier.stylelintIntegration": true,
// Formatting
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
// Trim on save
"files.trimTrailingWhitespace": true
"files.trimTrailingWhitespace": true,
// Special file associations
"files.associations": {
".eslintrc": "jsonc"
},
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}
}

212
readme.md
View File

@ -1,18 +1,19 @@
Plyr is 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](#donate) - [Slack](https://bit.ly/plyr-chat) - [![npm version](https://badge.fury.io/js/plyr.svg)](https://badge.fury.io/js/plyr)
[Checkout the demo](https://plyr.io) - [Donate](#donate) - [Slack](https://bit.ly/plyr--chat)
[![npm version](https://badge.fury.io/js/plyr.svg)](https://badge.fury.io/js/plyr) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/sampotts/plyr) [![Financial Contributors on Open Collective](https://opencollective.com/plyr/all/badge.svg?label=financial+contributors)](https://opencollective.com/plyr)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io)
# Features
- 📼 **HTML Video & Audio, YouTube & Vimeo** - support for the major formats
- 💪 **Accessible** - full support for VTT captions and screen readers
- 🔧 **[Customisable](#html)** - make the player look how you want with the markup you want
- 😎 **Good HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
- 🔧 **[Customizable](#html)** - make the player look how you want with the markup you want
- 😎 **Clean HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
`<span>` or `<a href="#">` button hacks
- 📱 **Responsive** - works with any screen size
- 📼 **HTML Video & Audio** - support for both formats
- 📺 **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback
- 💵 **[Monetization](#ads)** - make money from your videos
- 📹 **[Streaming](#demos)** - support for hls.js, Shaka and dash.js streaming playback
- 🎛 **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
@ -25,7 +26,7 @@ Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vi
- 📖 **Multiple captions** - support for multiple caption tracks
- 🌎 **i18n support** - support for internationalization of controls
- 👌 **[Preview thumbnails](#preview-thumbnails)** - support for displaying preview thumbnails
- 🤟 **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
- 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required
- 💁‍♀️ **SASS** - to include in your build processes
### Demos
@ -109,7 +110,15 @@ Or the `<div>` non progressively enhanced method:
## JavaScript
Include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below.
You can use Plyr as an ES6 module as follows:
```javascript
import Plyr from 'plyr';
const player = new Plyr('#player');
```
Alternatively you can include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below.
```html
<script src="path/to/plyr.js"></script>
@ -123,18 +132,18 @@ See [initialising](#initialising) for more information on advanced setups.
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
```html
<script src="https://cdn.plyr.io/3.5.4/plyr.js"></script>
<script src="https://cdn.plyr.io/3.5.10/plyr.js"></script>
```
...or...
```html
<script src="https://cdn.plyr.io/3.5.4/plyr.polyfilled.js"></script>
<script src="https://cdn.plyr.io/3.5.10/plyr.polyfilled.js"></script>
```
## CSS
Include the `plyr.css` stylsheet into your `<head>`
Include the `plyr.css` stylsheet into your `<head>`.
```html
<link rel="stylesheet" href="path/to/plyr.css" />
@ -143,13 +152,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.5.4/plyr.css" />
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.10/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.5.4/plyr.svg`.
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.5.10/plyr.svg`.
# Ads
@ -161,12 +170,40 @@ Plyr has partnered up with [vi.ai](https://vi.ai/publisher-video-monetization/?a
Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues.
If you do not wish to use Vi, you can set your own `ads.tagUrl` [option](#options).
# Advanced
## SASS
## Custom CSS
You can use `bundle.scss` file included in `/src` as part of your build and change variables to suit your design. The SASS require you to
use the [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) plugin (you should be already!) as all declarations use the W3C definitions.
If you want to change any design tokens used for the rendering of the player, you can do so using [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties).
Here's a list of the properties and what they are used for:
| Name | Description | Fallback |
| ------------------- | ----------------------------------------------- | --------------------- |
| `--plyr-color-main` | The main branding color - used for highlighting | `hsl(198, 100%, 50%)` |
You can set them in your CSS:
```css
.player {
--plyr-color-main: #1ac266;
}
```
...or in your HTML:
```html
<video class="player" style="--plyr-color-main: #1ac266;">
...
</vieo>
```
### SASS
You can use `plyr.scss` file included in `/src/sass` as part of your build and change variables to suit your design. The SASS requires you to
use [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) (you should be already!) as all declarations use the W3C definitions.
The HTML markup uses the BEM methodology with `plyr` as the block, e.g. `.plyr__controls`. You can change the class hooks in the options to match any custom CSS
you write. Check out the JavaScript source for more on this.
@ -204,7 +241,7 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
You can specify a range of arguments for the constructor to use:
- A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
- A [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
- A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
- A [jQuery](https://jquery.com) object
@ -212,7 +249,7 @@ _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element
#### Single player
Passing a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList):
Passing a CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector):
```javascript
const player = new Plyr('#player');
@ -238,7 +275,7 @@ You have two choices here. You can either use a simple array loop to map the con
const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p));
```
...or use a static method where you can pass a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of elements, or a [JQuery](https://jquery.com) object:
...or use a static method where you can pass a [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement), or a [JQuery](https://jquery.com) object:
```javascript
const players = Plyr.setup('.js-player');
@ -269,7 +306,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. |
| `debug` | Boolean | `false` | Display debugging information in the console |
| `controls` | Array, Function or Element | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; `id` (the unique id for the player), `seektime` (the seektime step in seconds), and `title` (the media title). See [controls.md](controls.md) for more info on how the html needs to be structured. |
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If you're using the default controls are used then you can specify which settings to show in the menu |
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If the default controls are used, you can specify which settings to show in the menu |
| `i18n` | Object | See [defaults.js](/src/js/config/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
| `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. |
| `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. |
@ -295,10 +332,10 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution (`true`/`false`/`'force'`). `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
| `ratio` | String | `null` | Force an aspect ratio for all videos. The format is `'w:h'` - e.g. `'16:9'` or `'4:3'`. If this is not specified then the default for HTML5 and Vimeo is to use the native resolution of the video. As dimensions are not available from YouTube via SDK, 16:9 is forced as a sensible default. |
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. |
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically. |
| `quality` | Object | `{ default: 576, options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240] }` | `default` is the default quality level (if it exists in your sources). `options` are the options to display. This is used to filter the available sources. |
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
| `ads` | Object | `{ enabled: false, publisherId: '' }` | `enabled`: Whether to enable advertisements. `publisherId`: Your unique [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) publisher ID. |
| `ads` | Object | `{ enabled: false, publisherId: '', tagUrl: '' }` | `enabled`: Whether to enable advertisements. `publisherId`: Your unique [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) publisher ID. `tagUrl` is a URL for a custom VAST tag if you're not using Vi. |
| `urls` | Object | See source. | If you wish to override any API URLs then you can do so here. You can also set a custom download URL for the download button. |
| `vimeo` | Object | `{ byline: false, portrait: false, title: false, speed: true, transparent: false }` | See [Vimeo embed options](https://github.com/vimeo/player.js/#embed-options). Some are set automatically based on other config options, namely: `loop`, `autoplay`, `muted`, `gesture`, `playsinline` |
| `youtube` | Object | `{ noCookie: false, rel: 0, showinfo: 0, iv_load_policy: 3, modestbranding: 1 }` | See [YouTube embed options](https://developers.google.com/youtube/player_parameters#Parameters). The only custom option is `noCookie` to use an alternative to YouTube that doesn't use cookies (useful for GDPR, etc). Some are set automatically based on other config options, namely: `autoplay`, `hl`, `controls`, `disablekb`, `playsinline`, `cc_load_policy`, `cc_lang_pref`, `widget_referrer` |
@ -342,30 +379,30 @@ player.play(); // Start playback
player.fullscreen.enter(); // Enter fullscreen
```
| Method | Parameters | Description |
| ------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `play()`&sup1; | - | Start playback. |
| `pause()` | - | Pause playback. |
| `togglePlay(toggle)` | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. |
| `stop()` | - | Stop playback and reset to start. |
| `restart()` | - | Restart playback. |
| `rewind(seekTime)` | Number | Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used. |
| `forward(seekTime)` | Number | Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used. |
| `increaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
| `decreaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
| `toggleCaptions(toggle)` | Boolean | Toggle captions display. If no parameter is passed, it will toggle based on current status. |
| `fullscreen.enter()` | - | Enter fullscreen. If fullscreen is not supported, a fallback "full window/viewport" is used instead. |
| `fullscreen.exit()` | - | Exit fullscreen. |
| `fullscreen.toggle()` | - | Toggle fullscreen. |
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
| `once(event, function)` | String, Function | Add an event listener for the specified event once. |
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
| `supports(type)` | String | Check support for a mime type. |
| `destroy()` | - | Destroy the instance and garbage collect any elements. |
| Method | Parameters | Description |
| -------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `play()`&sup1; | - | Start playback. |
| `pause()` | - | Pause playback. |
| `togglePlay(toggle)`&sup1; | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. |
| `stop()` | - | Stop playback and reset to start. |
| `restart()` | - | Restart playback. |
| `rewind(seekTime)` | Number | Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used. |
| `forward(seekTime)` | Number | Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used. |
| `increaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
| `decreaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
| `toggleCaptions(toggle)` | Boolean | Toggle captions display. If no parameter is passed, it will toggle based on current status. |
| `fullscreen.enter()` | - | Enter fullscreen. If fullscreen is not supported, a fallback "full window/viewport" is used instead. |
| `fullscreen.exit()` | - | Exit fullscreen. |
| `fullscreen.toggle()` | - | Toggle fullscreen. |
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
| `once(event, function)` | String, Function | Add an event listener for the specified event once. |
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
| `supports(type)` | String | Check support for a mime type. |
| `destroy()` | - | Destroy the instance and garbage collect any elements. |
1. For HTML5 players, `play()` will return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) in _some_ browsers - WebKit and Mozilla [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing.
1. For HTML5 players, `play()` will return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) for most browsers - e.g. Chrome, Firefox, Opera, Safari and Edge [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing.
## Getters and Setters
@ -438,6 +475,9 @@ player.source = {
},
],
poster: '/path/to/poster.jpg',
previewThumbnails: {
src: '/path/to/thumbnails.vtt',
},
tracks: [
{
kind: 'captions',
@ -507,13 +547,14 @@ player.source = {
_Note:_ `src` property for YouTube and Vimeo can either be the video ID or the whole URL.
| Property | Type | Description |
| -------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | String | Either `video` or `audio`. _Note:_ YouTube and Vimeo are currently not supported as audio sources. |
| `title` | String | _Optional._ Title of the new media. Used for the `aria-label` attribute on the play button, and outer container. YouTube and Vimeo are populated automatically. |
| `sources` | Array | This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required. |
| `poster`&sup1; | String | The URL for the poster image (HTML5 video only). |
| `tracks`&sup1; | String | An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above, it will render as `<track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default>` and similar for the French version. Booleans are converted to HTML5 value-less attributes. |
| Property | Type | Description |
| ------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | String | Either `video` or `audio`. _Note:_ YouTube and Vimeo are currently not supported as audio sources. |
| `title` | String | _Optional._ Title of the new media. Used for the `aria-label` attribute on the play button, and outer container. YouTube and Vimeo are populated automatically. |
| `sources` | Array | This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required. |
| `poster`&sup1; | String | The URL for the poster image (HTML5 video only). |
| `tracks`&sup1; | String | An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above, it will render as `<track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default>` and similar for the French version. Booleans are converted to HTML5 value-less attributes. |
| `previewThumbnails`&sup1; | Object | The same object like in the `previewThumbnails` constructor option. This means you can either change the thumbnails vtt via the `src` key or disable the thumbnails plugin for the next video by passing `{ enabled: false }`. |
1. HTML5 only
@ -622,16 +663,16 @@ 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 | ✓&sup3; |
| IE10 | ✓&sup2;&sup3; |
| Browser | Supported |
| ------------- | --------------- |
| Safari | ✓ |
| Mobile Safari | ✓&sup1; |
| Firefox | ✓ |
| Chrome | ✓ |
| Opera | ✓ |
| Edge | ✓ |
| IE11 | ✓&sup3; |
| IE10 | ✓<sup>2,3</sup> |
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)).
@ -655,7 +696,7 @@ The arguments are:
- Provider (`html5`, `youtube` or `vimeo`)
- Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
## Disable support programatically
## Disable support programmatically
The `enabled` option can be used to disable certain User Agents. For example, if you don't want to use Plyr for smartphones, you could use:
@ -671,14 +712,15 @@ If a User Agent is disabled but supports `<video>` and `<audio>` natively, it wi
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
| Type | Maintainer | Link |
| --------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
| Angular | Simon Bobrov ([@smnbbrv](https://github.com/smnbbrv)) | [https://github.com/smnbbrv/ngx-plyr](https://github.com/smnbbrv/ngx-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/) |
| Angular | Simon Bobrov ([@smnbbrv](https://github.com/smnbbrv)) | [https://github.com/smnbbrv/ngx-plyr](https://github.com/smnbbrv/ngx-plyr) |
| React | Chintan Prajapati ([@chintan9](https://github.com/chintan9)) | [https://github.com/chintan9/plyr-react](https://github.com/chintan9/plyr-react) |
| 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) |
| REDAXO | FriendsOfRedaxo / skerbis ([@skerbis](https://friendsofredaxo.github.io)) | [https://github.com/FriendsOfREDAXO/plyr](https://github.com/FriendsOfREDAXO/plyr) |
# Issues
@ -703,6 +745,7 @@ Plyr costs money to run, not only my time. I donate my time for free as I enjoy
- [HTML5 Weekly #177](http://html5weekly.com/issues/177)
- [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f)
- [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/)
- [Front End Focus #177](https://frontendfoc.us/issues/177)
- [Hacker News](https://news.ycombinator.com/item?id=9136774)
- [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04)
- [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player)
@ -722,13 +765,11 @@ Plyr costs money to run, not only my time. I donate my time for free as I enjoy
- [Sparkk TV](https://www.sparkktv.com/)
- [@halfhalftravel](https://www.halfhalftravel.com/)
Let me know on [Twitter](https://twitter.com/sam_potts) I can add you to the above list. It'd be awesome to see how you're using Plyr :-)
If you want to be added to the list, open a pull request. It'd be awesome to see how you're using Plyr 😎
# Useful links and credits
Credit to the PayPal HTML5 Video player from which Plyr's caption functionality was originally ported from:
- [PayPal's Accessible HTML5 Video Player](https://github.com/paypal/accessible-html5-video-player)
- [PayPal's Accessible HTML5 Video Player (which Plyr was originally ported from)](https://github.com/paypal/accessible-html5-video-player)
- [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw)
# Thanks
@ -741,6 +782,29 @@ Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN servic
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
<a href="https://github.com/sampotts/plyr/graphs/contributors"><img src="https://opencollective.com/plyr/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/plyr/contribute)]
#### Individuals
<a href="https://opencollective.com/plyr"><img src="https://opencollective.com/plyr/individuals.svg?width=890"></a>
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/plyr/contribute)]
<a href="https://opencollective.com/plyr/organization/0/website"><img src="https://opencollective.com/plyr/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/plyr/organization/1/website"><img src="https://opencollective.com/plyr/organization/1/avatar.svg"></a><a href="https://opencollective.com/plyr/organization/2/website"><img src="https://opencollective.com/plyr/organization/2/avatar.svg"></a>
# Copyright and License
[The MIT license](license.md)

View File

@ -133,7 +133,8 @@ const captions = {
});
// Turn off native caption rendering to avoid double captions
Object.assign(track, { mode: 'hidden' });
// eslint-disable-next-line no-param-reassign
track.mode = 'hidden';
// Add event listener for cue changes
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
@ -150,7 +151,11 @@ const captions = {
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
// Update available languages in list
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this);
}
},

View File

@ -61,7 +61,7 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.5.2/plyr.svg',
iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@ -69,7 +69,10 @@ const defaults = {
// Quality default
quality: {
default: 576,
// The options to display in the UI, if available for the source media
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
forced: false,
onChange: null,
},
// Set loops
@ -82,7 +85,8 @@ const defaults = {
// Speed default and options to display
speed: {
selected: 1,
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
// The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],
},
// Keyboard shortcut settings
@ -128,6 +132,7 @@ const defaults = {
// 'fast-forward',
'progress',
'current-time',
// 'duration',
'mute',
'volume',
'captions',
@ -163,6 +168,7 @@ const defaults = {
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
pip: 'PIP',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
@ -195,7 +201,7 @@ const defaults = {
},
youtube: {
sdk: 'https://www.youtube.com/iframe_api',
api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}', // 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title),fileDetails)&part=snippet',
api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
@ -413,6 +419,11 @@ const defaults = {
title: false,
speed: true,
transparent: false,
// These settings require a pro or premium account to work
sidedock: false,
controls: false,
// Custom settings from Plyr
referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
},
// YouTube plugin

43
src/js/controls.js vendored
View File

@ -28,7 +28,7 @@ import {
import { off, on } from './utils/events';
import i18n from './utils/i18n';
import is from './utils/is';
import loadSprite from './utils/loadSprite';
import loadSprite from './utils/load-sprite';
import { extend } from './utils/objects';
import { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';
import { formatTime, getHours } from './utils/time';
@ -111,7 +111,7 @@ const controls = {
setAttributes(
icon,
extend(attributes, {
role: 'presentation',
'aria-hidden': 'true',
focusable: 'false',
}),
);
@ -139,10 +139,7 @@ const controls = {
// Create hidden text label
createLabel(key, attr = {}) {
const text = i18n.get(key, this.config);
const attributes = Object.assign({}, attr, {
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' '),
});
const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') };
return createElement('span', attributes, text);
},
@ -402,7 +399,8 @@ const controls = {
// https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
bindMenuItemShortcuts(menuItem, type) {
// Navigate through menus via arrow keys and space
on(
on.call(
this,
menuItem,
'keydown keyup',
event => {
@ -452,7 +450,7 @@ const controls = {
// Enter will fire a `click` event but we still need to manage focus
// So we bind to keyup which fires after and set focus here
on(menuItem, 'keyup', event => {
on.call(this, menuItem, 'keyup', event => {
if (event.which !== 13) {
return;
}
@ -1046,7 +1044,7 @@ const controls = {
},
// Set a list of available captions languages
setSpeedMenu(options) {
setSpeedMenu() {
// Menu required
if (!is.element(this.elements.settings.panels.speed)) {
return;
@ -1055,15 +1053,8 @@ const controls = {
const type = 'speed';
const list = this.elements.settings.panels.speed.querySelector('[role="menu"]');
// Set the speed options
if (is.array(options)) {
this.options.speed = options;
} else if (this.isHTML5 || this.isVimeo) {
this.options.speed = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
}
// Set options if passed and filter based on config
this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
// Filter out invalid speeds
this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);
// Toggle the pane and tab
const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
@ -1380,7 +1371,9 @@ const controls = {
}
// Volume range control
if (control === 'volume') {
// Ignored on iOS as it's handled globally
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
if (control === 'volume' && !browser.isIos) {
// Set the attributes
const attributes = {
max: 1,
@ -1463,7 +1456,7 @@ const controls = {
bindMenuItemShortcuts.call(this, menuItem, type);
// Show menu on click
on(menuItem, 'click', () => {
on.call(this, menuItem, 'click', () => {
showMenuPanel.call(this, type, false);
});
@ -1515,7 +1508,8 @@ const controls = {
);
// Go back via keyboard
on(
on.call(
this,
pane,
'keydown',
event => {
@ -1535,7 +1529,7 @@ const controls = {
);
// Go back via button click
on(backButton, 'click', () => {
on.call(this, backButton, 'click', () => {
showMenuPanel.call(this, 'home', false);
});
@ -1581,6 +1575,11 @@ const controls = {
target: '_blank',
});
// Set download attribute for HTML5 only
if (this.isHTML5) {
attributes.download = '';
}
const { download } = this.config.urls;
if (!is.url(download) && this.isEmbed) {

View File

@ -4,83 +4,11 @@
// https://webkit.org/blog/7929/designing-websites-for-iphone-x/
// ==========================================================================
import { repaint } from './utils/animation';
import browser from './utils/browser';
import { hasClass, toggleClass, trapFocus } from './utils/elements';
import { getElements, hasClass, toggleClass } from './utils/elements';
import { on, triggerEvent } from './utils/events';
import is from './utils/is';
function onChange() {
if (!this.enabled) {
return;
}
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
button.pressed = this.active;
}
// Trigger an event
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Trap focus in container
if (!browser.isIos) {
trapFocus.call(this.player, this.target, this.active);
}
}
function toggleFallback(toggle = false) {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
}
// Toggle scroll
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Force full viewport on iPhone X+
if (browser.isIos) {
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter(part => part.trim() !== property)
.join(',');
}
// Force a repaint as sometimes Safari doesn't want to fill the screen
setTimeout(() => repaint(this.target), 100);
}
// Toggle button and fire events
onChange.call(this);
}
import { silencePromise } from './utils/promise';
class Fullscreen {
constructor(player) {
@ -105,7 +33,7 @@ class Fullscreen {
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
// TODO: Filter for target??
onChange.call(this);
this.onChange();
},
);
@ -119,6 +47,9 @@ class Fullscreen {
this.toggle();
});
// Tap focus when in fullscreen
on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));
// Update the UI
this.update();
}
@ -188,7 +119,7 @@ class Fullscreen {
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
return element === this.target;
return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
}
// Get target element
@ -198,6 +129,97 @@ class Fullscreen {
: this.player.elements.container;
}
onChange() {
if (!this.enabled) {
return;
}
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
button.pressed = this.active;
}
// Trigger an event
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
}
toggleFallback(toggle = false) {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
}
// Toggle scroll
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Force full viewport on iPhone X+
if (browser.isIos) {
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter(part => part.trim() !== property)
.join(',');
}
}
// Toggle button and fire events
this.onChange();
}
// Trap focus inside container
trapFocus(event) {
// Bail if iOS, not active, not the tab key
if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = document.activeElement;
const focusable = getElements.call(
this.player,
'a[href], button:not(:disabled), input:not(:disabled), [tabindex]',
);
const [first] = focusable;
const last = focusable[focusable.length - 1];
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
}
// Update UI
update() {
if (this.enabled) {
@ -230,9 +252,9 @@ class Fullscreen {
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitEnterFullscreen();
} else if (!Fullscreen.native || this.forceFallback) {
toggleFallback.call(this, true);
this.toggleFallback(true);
} else if (!this.prefix) {
this.target.requestFullscreen();
this.target.requestFullscreen({ navigationUI: 'hide' });
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
@ -247,9 +269,9 @@ class Fullscreen {
// iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen();
this.player.play();
silencePromise(this.player.play());
} else if (!Fullscreen.native || this.forceFallback) {
toggleFallback.call(this, false);
this.toggleFallback(false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) {

View File

@ -6,6 +6,7 @@ import support from './support';
import { removeElement } from './utils/elements';
import { triggerEvent } from './utils/events';
import is from './utils/is';
import { silencePromise } from './utils/promise';
import { setAspectRatio } from './utils/style';
const html5 = {
@ -30,6 +31,11 @@ const html5 = {
// Get quality levels
getQualityOptions() {
// Whether we're forcing all options (e.g. for streaming)
if (this.config.quality.forced) {
return this.config.quality.options;
}
// Get sizes from <source> elements
return html5.getSources
.call(this)
@ -37,15 +43,20 @@ const html5 = {
.filter(Boolean);
},
extend() {
setup() {
if (!this.isHTML5) {
return;
}
const player = this;
// Set aspect ratio if set
setAspectRatio.call(player);
// Set speed options from config
player.options.speed = player.config.speed.options;
// Set aspect ratio if fixed
if (!is.empty(this.config.ratio)) {
setAspectRatio.call(player);
}
// Quality
Object.defineProperty(player.media, 'quality', {
@ -58,36 +69,46 @@ const html5 = {
return source && Number(source.getAttribute('size'));
},
set(input) {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find(s => Number(s.getAttribute('size')) === input);
// No matching source found
if (!source) {
if (player.quality === input) {
return;
}
// Get current state
const { currentTime, paused, preload, readyState } = player.media;
// If we're using an an external handler...
if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
player.config.quality.onChange(input);
} else {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find(s => Number(s.getAttribute('size')) === input);
// Set new source
player.media.src = source.getAttribute('src');
// No matching source found
if (!source) {
return;
}
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
player.currentTime = currentTime;
// Get current state
const { currentTime, paused, preload, readyState, playbackRate } = player.media;
// Resume playing
if (!paused) {
player.play();
}
});
// Set new source
player.media.src = source.getAttribute('src');
// Load new source
player.media.load();
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
player.speed = playbackRate;
player.currentTime = currentTime;
// Resume playing
if (!paused) {
silencePromise(player.play());
}
});
// Load new source
player.media.load();
}
}
// Trigger change event

View File

@ -6,9 +6,10 @@ import controls from './controls';
import ui from './ui';
import { repaint } from './utils/animation';
import browser from './utils/browser';
import { getElement, getElements, matches, toggleClass, toggleHidden } from './utils/elements';
import { getElement, getElements, matches, toggleClass } from './utils/elements';
import { off, on, once, toggleListener, triggerEvent } from './utils/events';
import is from './utils/is';
import { silencePromise } from './utils/promise';
import { getAspectRatio, setAspectRatio } from './utils/style';
class Listeners {
@ -99,7 +100,7 @@ class Listeners {
case 75:
// Space and K key
if (!repeat) {
player.togglePlay();
silencePromise(player.togglePlay());
}
break;
@ -301,14 +302,6 @@ class Listeners {
},
);
// Force edge to repaint on exit fullscreen
// TODO: Fix weird bug where Edge doesn't re-draw when exiting fullscreen
/* if (browser.isEdge) {
on.call(player, elements.container, 'exitfullscreen', () => {
setTimeout(() => repaint(elements.container), 100);
});
} */
// Set a gutter for Vimeo
const setGutter = (ratio, padding, toggle) => {
if (!player.isVimeo) {
@ -344,8 +337,13 @@ class Listeners {
on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {
const { target, usingNative } = player.fullscreen;
// Ignore for iOS native
if (!player.isEmbed || target !== elements.container) {
// Ignore events not from target
if (target !== elements.container) {
return;
}
// If it's not an embed and no ratio specified
if (!player.isEmbed && is.empty(player.config.ratio)) {
return;
}
@ -380,19 +378,15 @@ class Listeners {
controls.durationUpdate.call(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
on.call(player, player.media, 'canplay loadeddata', () => {
toggleHidden(elements.volume, !player.hasAudio);
toggleHidden(elements.buttons.mute, !player.hasAudio);
});
// Handle the media finishing
on.call(player, player.media, 'ended', () => {
// Show poster on end
if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {
// Restart
player.restart();
// Call pause otherwise IE11 will start playing the video again
player.pause();
}
});
@ -438,9 +432,21 @@ class Listeners {
if (player.ended) {
this.proxy(event, player.restart, 'restart');
this.proxy(event, player.play, 'play');
this.proxy(
event,
() => {
silencePromise(player.play());
},
'play',
);
} else {
this.proxy(event, player.togglePlay, 'play');
this.proxy(
event,
() => {
silencePromise(player.togglePlay());
},
'play',
);
}
});
}
@ -516,7 +522,7 @@ class Listeners {
}
// Only call default handler if not prevented in custom handler
if (returned && is.function(defaultHandler)) {
if (returned !== false && is.function(defaultHandler)) {
defaultHandler.call(player, event);
}
}
@ -546,7 +552,14 @@ class Listeners {
// Play/pause toggle
if (elements.buttons.play) {
Array.from(elements.buttons.play).forEach(button => {
this.bind(button, 'click', player.togglePlay, 'play');
this.bind(
button,
'click',
() => {
silencePromise(player.togglePlay());
},
'play',
);
});
}
@ -606,12 +619,19 @@ class Listeners {
this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');
// Settings menu - click toggle
this.bind(elements.buttons.settings, 'click', event => {
// Prevent the document click listener closing the menu
event.stopPropagation();
this.bind(
elements.buttons.settings,
'click',
event => {
// Prevent the document click listener closing the menu
event.stopPropagation();
event.preventDefault();
controls.toggleMenu.call(player, event);
});
controls.toggleMenu.call(player, event);
},
null,
false,
); // Can't be passive as we're preventing default
// Settings menu - keyboard toggle
// We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
@ -666,7 +686,7 @@ class Listeners {
const code = event.keyCode ? event.keyCode : event.which;
const attribute = 'play-on-seeked';
if (is.keyboardEvent(event) && (code !== 39 && code !== 37)) {
if (is.keyboardEvent(event) && code !== 39 && code !== 37) {
return;
}
@ -681,7 +701,7 @@ class Listeners {
// If we're done seeking and it was playing, resume playback
if (play && done) {
seek.removeAttribute(attribute);
player.play();
silencePromise(player.play());
} else if (!done && player.playing) {
seek.setAttribute(attribute, '');
player.pause();
@ -732,7 +752,7 @@ class Listeners {
});
// Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering
this.bind(elements.progress, 'mouseleave click', () => {
this.bind(elements.progress, 'mouseleave touchend click', () => {
const { previewThumbnails } = player;
if (previewThumbnails && previewThumbnails.loaded) {

View File

@ -39,15 +39,17 @@ const media = {
wrap(this.media, this.elements.wrapper);
// Faux poster container
this.elements.poster = createElement('div', {
class: this.config.classNames.poster,
});
if (this.isEmbed) {
this.elements.poster = createElement('div', {
class: this.config.classNames.poster,
});
this.elements.wrapper.appendChild(this.elements.poster);
this.elements.wrapper.appendChild(this.elements.poster);
}
}
if (this.isHTML5) {
html5.extend.call(this);
html5.setup.call(this);
} else if (this.isYouTube) {
youtube.setup.call(this);
} else if (this.isVimeo) {

View File

@ -10,7 +10,8 @@ import { createElement } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import i18n from '../utils/i18n';
import is from '../utils/is';
import loadScript from '../utils/loadScript';
import loadScript from '../utils/load-script';
import { silencePromise } from '../utils/promise';
import { formatTime } from '../utils/time';
import { buildUrlParams } from '../utils/urls';
@ -136,7 +137,7 @@ class Ads {
cb: Date.now(),
AV_WIDTH: 640,
AV_HEIGHT: 480,
AV_CDIM2: this.publisherId,
AV_CDIM2: config.publisherId,
};
const base = 'https://go.aniview.com/api/adserver6/vast/';
@ -172,6 +173,17 @@ class Ads {
// We assume the adContainer is the video container of the plyr element that will house the ads
this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);
// Create ads loader
this.loader = new google.ima.AdsLoader(this.elements.displayContainer);
// Listen and respond to ads loaded and error events
this.loader.addEventListener(
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
event => this.onAdsManagerLoaded(event),
false,
);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);
// Request video ads to be pre-loaded
this.requestAds();
}
@ -183,17 +195,6 @@ class Ads {
const { container } = this.player.elements;
try {
// Create ads loader
this.loader = new google.ima.AdsLoader(this.elements.displayContainer);
// Listen and respond to ads loaded and error events
this.loader.addEventListener(
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
event => this.onAdsManagerLoaded(event),
false,
);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);
// Request video ads
const request = new google.ima.AdsRequest();
request.adTagUrl = this.tagUrl;
@ -369,7 +370,12 @@ class Ads {
// TODO: So there is still this thing where a video should only be allowed to start
// playing when the IMA SDK is ready or has failed
this.loadAds();
if (this.player.ended) {
this.loadAds();
} else {
// The SDK won't allow new ads to be called without receiving a contentComplete()
this.loader.contentComplete();
}
break;
@ -510,7 +516,7 @@ class Ads {
this.playing = false;
// Play video
this.player.media.play();
silencePromise(this.player.media.play());
}
/**
@ -563,6 +569,8 @@ class Ads {
this.on('loaded', resolve);
this.player.debug.log(this.manager);
});
// Now that the manager has been destroyed set it to also be un-initialized
this.initialized = false;
// Now request some new advertisements
this.requestAds();

View File

@ -63,6 +63,20 @@ const parseVtt = vttDataString => {
* - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered
*/
const fitRatio = (ratio, outer) => {
const targetRatio = outer.width / outer.height;
const result = {};
if (ratio > targetRatio) {
result.width = outer.width;
result.height = (1 / ratio) * outer.width;
} else {
result.height = outer.height;
result.width = ratio * outer.height;
}
return result;
};
class PreviewThumbnails {
/**
* PreviewThumbnails constructor.
@ -90,7 +104,7 @@ class PreviewThumbnails {
}
load() {
// Togglethe regular seek tooltip
// Toggle the regular seek tooltip
if (this.player.elements.display.seekTooltip) {
this.player.elements.display.seekTooltip.hidden = this.enabled;
}
@ -100,6 +114,10 @@ class PreviewThumbnails {
}
this.getThumbnails().then(() => {
if (!this.enabled) {
return;
}
// Render DOM elements
this.render();
@ -119,19 +137,32 @@ class PreviewThumbnails {
throw new Error('Missing previewThumbnails.src config attribute');
}
// If string, convert into single-element list
const urls = is.string(src) ? [src] : src;
// Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
const promises = urls.map(u => this.getThumbnail(u));
Promise.all(promises).then(() => {
// Resolve promise
const sortAndResolve = () => {
// Sort smallest to biggest (e.g., [120p, 480p, 1080p])
this.thumbnails.sort((x, y) => x.height - y.height);
this.player.debug.log('Preview thumbnails', this.thumbnails);
resolve();
});
};
// Via callback()
if (is.function(src)) {
src(thumbnails => {
this.thumbnails = thumbnails;
sortAndResolve();
});
}
// VTT urls
else {
// If string, convert into single-element list
const urls = is.string(src) ? [src] : src;
// Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
const promises = urls.map(u => this.getThumbnail(u));
// Resolve
Promise.all(promises).then(sortAndResolve);
}
});
}
@ -221,8 +252,8 @@ class PreviewThumbnails {
}
startScrubbing(event) {
// Only act on left mouse button (0), or touch device (event.button is false)
if (event.button === false || event.button === 0) {
// Only act on left mouse button (0), or touch device (event.button does not exist or is false)
if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {
this.mouseDown = true;
// Wait until media has a duration
@ -310,6 +341,15 @@ class PreviewThumbnails {
this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);
}
destroy() {
if (this.elements.thumb.container) {
this.elements.thumb.container.remove();
}
if (this.elements.scrubbing.container) {
this.elements.scrubbing.container.remove();
}
}
showImageAtCurrentTime() {
if (this.mouseDown) {
this.setScrubbingContainerSize();
@ -536,8 +576,16 @@ class PreviewThumbnails {
get thumbContainerHeight() {
if (this.mouseDown) {
// Can't use media.clientHeight - HTML5 video goes big and does black bars above and below
return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio);
const { height } = fitRatio(this.thumbAspectRatio, {
width: this.player.media.clientWidth,
height: this.player.media.clientHeight,
});
return height;
}
// If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)
if (this.sizeSpecifiedInCSS) {
return this.elements.thumb.imageContainer.clientHeight;
}
return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);
@ -580,7 +628,7 @@ class PreviewThumbnails {
}
determineContainerAutoSizing() {
if (this.elements.thumb.imageContainer.clientHeight > 20) {
if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {
// This will prevent auto sizing in this.setThumbContainerSizeAndPos()
this.sizeSpecifiedInCSS = true;
}
@ -592,6 +640,12 @@ class PreviewThumbnails {
const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);
this.elements.thumb.imageContainer.style.height = `${this.thumbContainerHeight}px`;
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
} else if (this.elements.thumb.imageContainer.clientHeight > 20 && this.elements.thumb.imageContainer.clientWidth < 20) {
const thumbWidth = Math.floor(this.elements.thumb.imageContainer.clientHeight * this.thumbAspectRatio);
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
} else if (this.elements.thumb.imageContainer.clientHeight < 20 && this.elements.thumb.imageContainer.clientWidth > 20) {
const thumbHeight = Math.floor(this.elements.thumb.imageContainer.clientWidth / this.thumbAspectRatio);
this.elements.thumb.imageContainer.style.height = `${thumbHeight}px`;
}
this.setThumbContainerPos();
@ -620,9 +674,12 @@ class PreviewThumbnails {
// Can't use 100% width, in case the video is a different aspect ratio to the video container
setScrubbingContainerSize() {
this.elements.scrubbing.container.style.width = `${this.player.media.clientWidth}px`;
// Can't use media.clientHeight - html5 video goes big and does black bars above and below
this.elements.scrubbing.container.style.height = `${this.player.media.clientWidth / this.thumbAspectRatio}px`;
const { width, height } = fitRatio(this.thumbAspectRatio, {
width: this.player.media.clientWidth,
height: this.player.media.clientHeight,
});
this.elements.scrubbing.container.style.width = `${width}px`;
this.elements.scrubbing.container.style.height = `${height}px`;
}
// Sprites need to be offset to the correct location
@ -635,9 +692,9 @@ class PreviewThumbnails {
const multiplier = this.thumbContainerHeight / frame.h;
// eslint-disable-next-line no-param-reassign
previewImage.style.height = `${Math.floor(previewImage.naturalHeight * multiplier)}px`;
previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;
// eslint-disable-next-line no-param-reassign
previewImage.style.width = `${Math.floor(previewImage.naturalWidth * multiplier)}px`;
previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;
// eslint-disable-next-line no-param-reassign
previewImage.style.left = `-${frame.x * multiplier}px`;
// eslint-disable-next-line no-param-reassign

View File

@ -9,7 +9,7 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import loadScript from '../utils/loadScript';
import loadScript from '../utils/load-script';
import { extend } from '../utils/objects';
import { format, stripHTML } from '../utils/strings';
import { setAspectRatio } from '../utils/style';
@ -42,23 +42,28 @@ function assurePlaybackState(play) {
const vimeo = {
setup() {
const player = this;
// Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
toggleClass(player.elements.wrapper, player.config.classNames.embed, true);
// Set speed options from config
player.options.speed = player.config.speed.options;
// Set intial ratio
setAspectRatio.call(this);
setAspectRatio.call(player);
// Load the SDK if not already
if (!is.object(window.Vimeo)) {
loadScript(this.config.urls.vimeo.sdk)
loadScript(player.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(this);
vimeo.ready.call(player);
})
.catch(error => {
this.debug.warn('Vimeo SDK (player.js) failed to load', error);
player.debug.warn('Vimeo SDK (player.js) failed to load', error);
});
} else {
vimeo.ready.call(this);
vimeo.ready.call(player);
}
},
@ -99,6 +104,11 @@ const vimeo = {
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
// Set the referrer policy if required
if (!is.empty(config.referrerPolicy)) {
iframe.setAttribute('referrerPolicy', config.referrerPolicy);
}
// Get poster, if already set
const { poster } = player;
// Inject the package
@ -191,18 +201,13 @@ const vimeo = {
return speed;
},
set(input) {
player.embed
.setPlaybackRate(input)
.then(() => {
speed = input;
triggerEvent.call(player, player.media, 'ratechange');
})
.catch(error => {
// Hide menu item (and menu if empty)
if (error.name === 'Error') {
controls.setSpeedMenu.call(player, []);
}
});
player.embed.setPlaybackRate(input).then(() => {
speed = input;
triggerEvent.call(player, player.media, 'ratechange');
}).catch(() => {
// Cannot set Playback Rate, Video is probably not on Pro account
player.options.speed = [1];
});
},
});
@ -335,6 +340,14 @@ const vimeo = {
}
});
player.embed.on('bufferstart', () => {
triggerEvent.call(player, player.media, 'waiting');
});
player.embed.on('bufferend', () => {
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('play', () => {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');

View File

@ -7,8 +7,8 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import loadImage from '../utils/loadImage';
import loadScript from '../utils/loadScript';
import loadImage from '../utils/load-image';
import loadScript from '../utils/load-script';
import { extend } from '../utils/objects';
import { format, generateId } from '../utils/strings';
import { setAspectRatio } from '../utils/style';
@ -297,7 +297,9 @@ const youtube = {
});
// Get available speeds
player.options.speed = instance.getAvailablePlaybackRates();
const speeds = instance.getAvailablePlaybackRates();
// Filter based on config
player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));
// Set the tabindex to avoid focus entering iframe
if (player.supported.ui) {
@ -416,6 +418,12 @@ const youtube = {
break;
case 3:
// Trigger waiting event to add loading classes to container as the video buffers.
triggerEvent.call(player, player.media, 'waiting');
break;
default:
break;
}

620
src/js/plyr.d.ts vendored Normal file
View File

@ -0,0 +1,620 @@
// Type definitions for plyr 3.5
// Project: https://plyr.io
// Definitions by: ondratra <https://github.com/ondratra>
// TypeScript Version: 3.0
export = Plyr;
export as namespace Plyr;
declare class Plyr {
/**
* Setup a new instance
*/
static setup(targets: NodeList | HTMLElement | HTMLElement[] | string, options?: Plyr.Options): Plyr[];
/**
* Check for support
* @param mediaType
* @param provider
* @param playsInline Whether the player has the playsinline attribute (only applicable to iOS 10+)
*/
static supported(mediaType?: Plyr.MediaType, provider?: Plyr.Provider, playsInline?: boolean): Plyr.Support;
constructor(targets: NodeList | HTMLElement | HTMLElement[] | string, options?: Plyr.Options);
/**
* Indicates if the current player is HTML5.
*/
readonly isHTML5: boolean;
/**
* Indicates if the current player is an embedded player.
*/
readonly isEmbed: boolean;
/**
* Indicates if the current player is playing.
*/
readonly playing: boolean;
/**
* Indicates if the current player is paused.
*/
readonly paused: boolean;
/**
* Indicates if the current player is stopped.
*/
readonly stopped: boolean;
/**
* Indicates if the current player has finished playback.
*/
readonly ended: boolean;
/**
* Returns a float between 0 and 1 indicating how much of the media is buffered
*/
readonly buffered: number;
/**
* Gets or sets the currentTime for the player. The setter accepts a float in seconds.
*/
currentTime: number;
/**
* Indicates if the current player is seeking.
*/
readonly seeking: boolean;
/**
* Returns the duration for the current media.
*/
readonly duration: number;
/**
* Gets or sets the volume for the player. The setter accepts a float between 0 and 1.
*/
volume: number;
/**
* Gets or sets the muted state of the player. The setter accepts a boolean.
*/
muted: boolean;
/**
* Indicates if the current media has an audio track.
*/
readonly hasAudio: boolean;
/**
* Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5.
*/
speed: number;
/**
* Gets or sets the quality for the player. The setter accepts a value from the options specified in your config.
*/
quality: number;
/**
* Gets or sets the current loop state of the player.
*/
loop: boolean;
/**
* Gets or sets the current source for the player.
*/
source: Plyr.SourceInfo;
/**
* Gets or sets the current poster image URL for the player.
*/
poster: string;
/**
* Gets or sets the autoplay state of the player.
*/
autoplay: boolean;
/**
* Gets or sets the caption track by index. 1 means the track is missing or captions is not active
*/
currentTrack: number;
/**
* Gets or sets the preferred captions language for the player. The setter accepts an ISO twoletter language code. Support for the languages is dependent on the captions you include.
* If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use currentTrack instead.
*/
language: string;
/**
* Gets or sets the picture-in-picture state of the player. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+.
*/
pip: boolean;
/**
* Gets or sets the aspect ratio for embedded players.
*/
ratio?: string;
/**
* Returns the current video Provider
*/
readonly provider: 'html5' | 'vimeo' | 'youtube';
/**
* Returns the native API for Vimeo or Youtube players
*/
readonly embed?: any;
readonly fullscreen: Plyr.FullscreenControl;
/**
* Start playback.
* For HTML5 players, play() will return a Promise in some browsers - WebKit and Mozilla according to MDN at time of writing.
*/
play(): Promise<void> | void;
/**
* Pause playback.
*/
pause(): void;
/**
* Toggle playback, if no parameters are passed, it will toggle based on current status.
*/
togglePlay(toggle?: boolean): boolean;
/**
* Stop playback and reset to start.
*/
stop(): void;
/**
* Restart playback.
*/
restart(): void;
/**
* Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used.
*/
rewind(seekTime?: number): void;
/**
* Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used.
*/
forward(seekTime?: number): void;
/**
* Increase volume by the specified step. If no parameter is passed, the default step will be used.
*/
increaseVolume(step?: number): void;
/**
* Increase volume by the specified step. If no parameter is passed, the default step will be used.
*/
decreaseVolume(step?: number): void;
/**
* Toggle captions display. If no parameter is passed, it will toggle based on current status.
*/
toggleCaptions(toggle?: boolean): void;
/**
* Trigger the airplay dialog on supported devices.
*/
airplay(): void;
/**
* Toggle the controls (video only). Takes optional truthy value to force it on/off.
*/
toggleControls(toggle: boolean): void;
/**
* Add an event listener for the specified event.
*/
on(
event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
callback: (this: this, event: Plyr.PlyrEvent) => void,
): void;
/**
* Add an event listener for the specified event once.
*/
once(
event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
callback: (this: this, event: Plyr.PlyrEvent) => void,
): void;
/**
* Remove an event listener for the specified event.
*/
off(
event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
callback: (this: this, event: Plyr.PlyrEvent) => void,
): void;
/**
* Check support for a mime type.
*/
supports(type: string): boolean;
/**
* Destroy lib instance
*/
destroy(): void;
}
declare namespace Plyr {
type MediaType = 'audio' | 'video';
type Provider = 'html5' | 'youtube' | 'vimeo';
type StandardEvent =
| 'progress'
| 'playing'
| 'play'
| 'pause'
| 'timeupdate'
| 'volumechange'
| 'seeking'
| 'seeked'
| 'ratechange'
| 'ended'
| 'enterfullscreen'
| 'exitfullscreen'
| 'captionsenabled'
| 'captionsdisabled'
| 'languagechange'
| 'controlshidden'
| 'controlsshown'
| 'ready';
type Html5Event =
| 'loadstart'
| 'loadeddata'
| 'loadedmetadata'
| 'canplay'
| 'canplaythrough'
| 'stalled'
| 'waiting'
| 'emptied'
| 'cuechange'
| 'error';
type YoutubeEvent = 'statechange' | 'qualitychange' | 'qualityrequested';
interface FullscreenControl {
/**
* Indicates if the current player is in fullscreen mode.
*/
readonly active: boolean;
/**
* Indicates if the current player has fullscreen enabled.
*/
readonly enabled: boolean;
/**
* Enter fullscreen. If fullscreen is not supported, a fallback ""full window/viewport"" is used instead.
*/
enter(): void;
/**
* Exit fullscreen.
*/
exit(): void;
/**
* Toggle fullscreen.
*/
toggle(): void;
}
interface Options {
/**
* Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below.
*/
enabled?: boolean;
/**
* Display debugging information in the console
*/
debug?: boolean;
/**
* If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function;
* id (the unique id for the player), seektime (the seektime step in seconds), and title (the media title). See controls.md for more info on how the html needs to be structured.
* Defaults to ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']
*/
controls?: string[] | ((id: string, seektime: number, title: string) => unknown) | Element;
/**
* If you're using the default controls are used then you can specify which settings to show in the menu
* Defaults to ['captions', 'quality', 'speed', 'loop']
*/
settings?: string[];
/**
* Used for internationalization (i18n) of the text within the UI.
*/
i18n?: any;
/**
* Load the SVG sprite specified as the iconUrl option (if a URL). If false, it is assumed you are handling sprite loading yourself.
*/
loadSprite?: boolean;
/**
* Specify a URL or path to the SVG sprite. See the SVG section for more info.
*/
iconUrl?: string;
/**
* Specify the id prefix for the icons used in the default controls (e.g. plyr-play would be plyr).
* This is to prevent clashes if you're using your own SVG sprite but with the default controls.
* Most people can ignore this option.
*/
iconPrefix?: string;
/**
* Specify a URL or path to a blank video file used to properly cancel network requests.
*/
blankUrl?: string;
/**
* Autoplay the media on load. This is generally advised against on UX grounds. It is also disabled by default in some browsers.
* If the autoplay attribute is present on a <video> or <audio> element, this will be automatically set to true.
*/
autoplay?: boolean;
/**
* Only allow one player playing at once.
*/
autopause?: boolean;
/**
* The time, in seconds, to seek when a user hits fast forward or rewind.
*/
seekTime?: number;
/**
* A number, between 0 and 1, representing the initial volume of the player.
*/
volume?: number;
/**
* Whether to start playback muted. If the muted attribute is present on a <video> or <audio> element, this will be automatically set to true.
*/
muted?: boolean;
/**
* Click (or tap) of the video container will toggle play/pause.
*/
clickToPlay?: boolean;
/**
* Disable right click menu on video to help as very primitive obfuscation to prevent downloads of content.
*/
disableContextMenu?: boolean;
/**
* Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen.
* As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly.
*/
hideControls?: boolean;
/**
* Reset the playback to the start once playback is complete.
*/
resetOnEnd?: boolean;
/**
* Enable keyboard shortcuts for focused players only or globally
*/
keyboard?: KeyboardOptions;
/**
* controls: Display control labels as tooltips on :hover & :focus (by default, the labels are screen reader only).
* seek: Display a seek tooltip to indicate on click where the media would seek to.
*/
tooltips?: TooltipOptions;
/**
* Specify a custom duration for media.
*/
duration?: number;
/**
* Displays the duration of the media on the metadataloaded event (on startup) in the current time display.
* This will only work if the preload attribute is not set to none (or is not set at all) and you choose not to display the duration (see controls option).
*/
displayDuration?: boolean;
/**
* Display the current time as a countdown rather than an incremental counter.
*/
invertTime?: boolean;
/**
* Allow users to click to toggle the above.
*/
toggleInvert?: boolean;
/**
* Allows binding of event listeners to the controls before the default handlers. See the defaults.js for available listeners.
* If your handler prevents default on the event (event.preventDefault()), the default handler will not fire.
*/
listeners?: { [key: string]: (error: PlyrEvent) => void };
/**
* active: Toggles if captions should be active by default. language: Sets the default language to load (if available). 'auto' uses the browser language.
* update: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in unselectable language options).
*/
captions?: CaptionOptions;
/**
* enabled: Toggles whether fullscreen should be enabled. fallback: Allow fallback to a full-window solution.
* iosNative: whether to use native iOS fullscreen when entering fullscreen (no custom controls)
*/
fullscreen?: FullScreenOptions;
/**
* The aspect ratio you want to use for embedded players.
*/
ratio?: string;
/**
* enabled: Allow use of local storage to store user settings. key: The key name to use.
*/
storage?: StorageOptions;
/**
* selected: The default speed for playback. options: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically.
*/
speed?: SpeedOptions;
/**
* Currently only supported by YouTube. default is the default quality level, determined by YouTube. options are the options to display.
*/
quality?: QualityOptions;
/**
* active: Whether to loop the current video. If the loop attribute is present on a <video> or <audio> element,
* this will be automatically set to true This is an object to support future functionality.
*/
loop?: LoopOptions;
/**
* enabled: Whether to enable vi.ai ads. publisherId: Your unique vi.ai publisher ID.
*/
ads?: AdOptions;
/**
* Vimeo Player Options.
*/
vimeo?: object;
/**
* Youtube Player Options.
*/
youtube?: object;
}
interface QualityOptions {
default: number;
options: number[];
}
interface LoopOptions {
active: boolean;
}
interface AdOptions {
enabled: boolean;
publisherId: string;
}
interface SpeedOptions {
selected: number;
options: number[];
}
interface KeyboardOptions {
focused?: boolean;
global?: boolean;
}
interface TooltipOptions {
controls?: boolean;
seek?: boolean;
}
interface FullScreenOptions {
enabled?: boolean;
fallback?: boolean;
allowAudio?: boolean;
iosNative?: boolean;
}
interface CaptionOptions {
active?: boolean;
language?: string;
update?: boolean;
}
interface StorageOptions {
enabled?: boolean;
key?: string;
}
interface SourceInfo {
/**
* Note: YouTube and Vimeo are currently not supported as audio sources.
*/
type: MediaType;
/**
* Title of the new media. Used for the aria-label attribute on the play button, and outer container. YouTube and Vimeo are populated automatically.
*/
title?: string;
/**
* This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required.
*/
sources: Source[];
/**
* The URL for the poster image (HTML5 video only).
*/
poster?: string;
/**
* An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above,
* it will render as <track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default> and similar for the French version.
* Booleans are converted to HTML5 value-less attributes.
*/
tracks?: Track[];
}
interface Source {
/**
* The URL of the media file (or YouTube/Vimeo URL).
*/
src: string;
/**
* The MIME type of the media file (if HTML5).
*/
type?: string;
provider?: Provider;
size?: number;
}
type TrackKind = 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
interface Track {
/**
* Indicates how the text track is meant to be used
*/
kind: TrackKind;
/**
* Indicates a user-readable title for the track
*/
label: string;
/**
* The language of the track text data. It must be a valid BCP 47 language tag. If the kind attribute is set to subtitles, then srclang must be defined.
*/
srcLang?: string;
/**
* The URL of the track (.vtt file).
*/
src: string;
default?: boolean;
}
interface PlyrEvent extends CustomEvent {
readonly detail: { readonly plyr: Plyr };
}
interface Support {
api: boolean;
ui: boolean;
}
}

View File

@ -1,6 +1,6 @@
// ==========================================================================
// Plyr
// plyr.js v3.5.3
// plyr.js v3.5.10
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
@ -15,7 +15,7 @@ import Fullscreen from './fullscreen';
import Listeners from './listeners';
import media from './media';
import Ads from './plugins/ads';
import PreviewThumbnails from './plugins/previewThumbnails';
import PreviewThumbnails from './plugins/preview-thumbnails';
import source from './source';
import Storage from './storage';
import support from './support';
@ -24,9 +24,10 @@ import { closest } from './utils/arrays';
import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';
import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
import is from './utils/is';
import loadSprite from './utils/loadSprite';
import loadSprite from './utils/load-sprite';
import { clamp } from './utils/numbers';
import { cloneDeep, extend } from './utils/objects';
import { silencePromise } from './utils/promise';
import { getAspectRatio, reduceAspectRatio, setAspectRatio, validateRatio } from './utils/style';
import { parseUrl } from './utils/urls';
@ -303,7 +304,7 @@ class Plyr {
// Autoplay if required
if (this.isHTML5 && this.config.autoplay) {
setTimeout(() => this.play(), 10);
setTimeout(() => silencePromise(this.play()), 10);
}
// Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
@ -356,7 +357,7 @@ class Plyr {
// Intecept play with ads
if (this.ads && this.ads.enabled) {
this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play());
this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));
}
// Return the promise (for HTML5)
@ -368,10 +369,10 @@ class Plyr {
*/
pause() {
if (!this.playing || !is.function(this.media.pause)) {
return;
return null;
}
this.media.pause();
return this.media.pause();
}
/**
@ -411,10 +412,10 @@ class Plyr {
const toggle = is.boolean(input) ? input : !this.playing;
if (toggle) {
this.play();
} else {
this.pause();
return this.play();
}
return this.pause();
}
/**
@ -441,7 +442,7 @@ class Plyr {
* @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
*/
rewind(seekTime) {
this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime);
this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;
}
/**
@ -449,7 +450,7 @@ class Plyr {
* @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
*/
forward(seekTime) {
this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime);
this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;
}
/**

View File

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

View File

@ -5,6 +5,7 @@
import { providers } from './config/types';
import html5 from './html5';
import media from './media';
import PreviewThumbnails from './plugins/preview-thumbnails';
import support from './support';
import ui from './ui';
import { createElement, insertElement, removeElement } from './utils/elements';
@ -130,9 +131,20 @@ const source = {
this.media.load();
}
// Reload thumbnails
if (this.previewThumbnails) {
this.previewThumbnails.load();
// Update previewThumbnails config & reload plugin
if (!is.empty(input.previewThumbnails)) {
Object.assign(this.config.previewThumbnails, input.previewThumbnails);
// Cleanup previewThumbnails plugin if it was loaded
if (this.previewThumbnails && this.previewThumbnails.loaded) {
this.previewThumbnails.destroy();
this.previewThumbnails = null;
}
// Create new instance if it is still enabled
if (this.config.previewThumbnails.enabled) {
this.previewThumbnails = new PreviewThumbnails(this);
}
}
// Update the fullscreen support

View File

@ -10,7 +10,7 @@ import { getElement, toggleClass } from './utils/elements';
import { ready, triggerEvent } from './utils/events';
import i18n from './utils/i18n';
import is from './utils/is';
import loadImage from './utils/loadImage';
import loadImage from './utils/load-image';
const ui = {
addStyleHook() {
@ -172,6 +172,11 @@ const ui = {
// Set property synchronously to respect the call order
this.media.setAttribute('poster', poster);
// HTML5 uses native poster attribute
if (this.isHTML5) {
return Promise.resolve(poster);
}
// Wait until ui is ready
return (
ready
@ -198,7 +203,9 @@ const ui = {
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
ui.togglePoster.call(this, true);
return poster;
})
);
@ -214,6 +221,7 @@ const ui = {
// Set state
Array.from(this.elements.buttons.play || []).forEach(target => {
Object.assign(target, { pressed: this.playing });
target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
});
// Only update controls on non timeupdate events

View File

@ -2,7 +2,6 @@
// Animation utils
// ==========================================================================
import { toggleHidden } from './elements';
import is from './is';
export const transitionEndEvent = (() => {
@ -21,14 +20,19 @@ export const transitionEndEvent = (() => {
})();
// Force repaint of element
export function repaint(element) {
export function repaint(element, delay) {
setTimeout(() => {
try {
toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line
toggleHidden(element, false);
// eslint-disable-next-line no-param-reassign
element.hidden = true;
// eslint-disable-next-line no-unused-expressions
element.offsetHeight;
// eslint-disable-next-line no-param-reassign
element.hidden = false;
} catch (e) {
// Do nothing
}
}, 0);
}, delay);
}

View File

@ -2,7 +2,6 @@
// Element utils
// ==========================================================================
import { toggleListener } from './events';
import is from './is';
import { extend } from './objects';
@ -192,11 +191,8 @@ export function toggleHidden(element, hidden) {
hide = !element.hidden;
}
if (hide) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
// eslint-disable-next-line no-param-reassign
element.hidden = hide;
}
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
@ -225,7 +221,7 @@ export function hasClass(element, className) {
// Element matches selector
export function matches(element, selector) {
const prototype = { Element };
const {prototype} = Element;
function match() {
return Array.from(document.querySelectorAll(selector)).includes(this);
@ -251,39 +247,6 @@ export function getElement(selector) {
return this.elements.container.querySelector(selector);
}
// Trap focus inside container
export function trapFocus(element = null, toggle = false) {
if (!is.element(element)) {
return;
}
const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = focusable[0];
const last = focusable[focusable.length - 1];
const trap = event => {
// Bail if not tab key or not fullscreen
if (event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = document.activeElement;
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
};
toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
}
// Set focus and tab focus class
export function setFocus(element = null, tabFocus = false) {
if (!is.element(element)) {

View File

@ -90,9 +90,7 @@ export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
// Create and dispatch the event
const event = new CustomEvent(type, {
bubbles,
detail: Object.assign({}, detail, {
plyr: this,
}),
detail: { ...detail, plyr: this,},
});
// Dispatch the event

View File

@ -19,7 +19,7 @@ const isEvent = input => instanceOf(input, Event);
const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);
const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));
const isPromise = input => instanceOf(input, Promise);
const isPromise = input => instanceOf(input, Promise) && isFunction(input.then);
const isEmpty = input =>
isNullOrUndefined(input) ||

14
src/js/utils/promise.js Normal file
View File

@ -0,0 +1,14 @@
import is from './is';
/**
* Silence a Promise-like object.
* This is useful for avoiding non-harmful, but potentially confusing "uncaught
* play promise" rejection error messages.
* @param {Object} value An object that may or may not be `Promise`-like.
*/
export function silencePromise(value) {
if (is.promise(value)) {
value.then(null, () => {});
}
}
export default { silencePromise };

View File

@ -27,15 +27,8 @@ export function reduceAspectRatio(ratio) {
}
export function getAspectRatio(input) {
const parse = ratio => {
if (!validateRatio(ratio)) {
return null;
}
return ratio.split(':').map(Number);
};
// Provided ratio
const parse = ratio => (validateRatio(ratio) ? ratio.split(':').map(Number) : null);
// Try provided ratio
let ratio = parse(input);
// Get from config
@ -63,11 +56,12 @@ export function setAspectRatio(input) {
return {};
}
const { wrapper } = this.elements;
const ratio = getAspectRatio.call(this, input);
const [w, h] = is.array(ratio) ? ratio : [0, 0];
const padding = (100 / w) * h;
this.elements.wrapper.style.paddingBottom = `${padding}%`;
wrapper.style.paddingBottom = `${padding}%`;
// For Vimeo we have an extra <div> to hide the standard controls and UI
if (this.isVimeo && this.supported.ui) {
@ -75,7 +69,7 @@ export function setAspectRatio(input) {
const offset = (height - padding) / (height / 50);
this.media.style.transform = `translateY(-${offset}%)`;
} else if (this.isHTML5) {
this.elements.wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
}
return { padding, ratio };

View File

@ -13,7 +13,7 @@ export const getSeconds = value => Math.trunc(value % 60, 10);
export function formatTime(time = 0, displayHours = false, inverted = false) {
// Bail if the value isn't a number
if (!is.number(time)) {
return formatTime(null, displayHours, inverted);
return formatTime(undefined, displayHours, inverted);
}
// Format time component to add leading zero

View File

@ -5,24 +5,28 @@
// Base
.plyr {
@include plyr-font-smoothing($plyr-font-smoothing);
align-items: center;
direction: ltr;
display: flex;
flex-direction: column;
font-family: var(--plyr-font-family);
font-variant-numeric: tabular-nums; // Force monosace-esque number widths
font-weight: var(--plyr-font-weight-regular);
height: 100%;
line-height: var(--plyr-line-height);
max-width: 100%;
min-width: 200px;
position: relative;
text-shadow: none;
transition: box-shadow 0.3s ease;
z-index: 0; // Force any border radius
// Media elements
video,
audio {
border-radius: inherit;
height: auto;
vertical-align: middle;
audio,
iframe {
display: block;
height: 100%;
width: 100%;
}

View File

@ -3,10 +3,10 @@
// --------------------------------------------------------------
.plyr__badge {
background: $plyr-badge-bg;
background: var(--plyr-badge-background, $plyr-badge-background);
border-radius: 2px;
color: $plyr-badge-color;
font-size: var(--plyr-font-size-badge);
color: var(--plyr-badge-color, $plyr-badge-color);
font-size: var(--plyr-font-size-badge, $plyr-font-size-badge);
line-height: 1;
padding: 3px 4px;
}

View File

@ -10,7 +10,6 @@
.plyr__captions {
animation: plyr-fade-in 0.3s ease;
bottom: 0;
color: var(--plyr-captions-text-color);
display: none;
font-size: $plyr-font-size-captions-small;
left: 0;
@ -21,9 +20,12 @@
width: 100%;
.plyr__caption {
background: var(--plyr-captions-background);
background: $plyr-captions-background;
background: var(--plyr-captions-background, $plyr-captions-background);
border-radius: 2px;
box-decoration-break: clone;
color: $plyr-captions-text-color;
color: var(--plyr-captions-text-color, $plyr-captions-text-color);
line-height: 185%;
padding: 0.2em 0.5em;
white-space: pre-wrap;

View File

@ -5,21 +5,22 @@
.plyr__control {
background: transparent;
border: 0;
border-radius: var(--plyr-control-radius);
border-radius: $plyr-control-radius;
border-radius: var(--plyr-control-radius, $plyr-control-radius);
color: inherit;
cursor: pointer;
flex-shrink: 0;
overflow: visible; // IE11
padding: var(--plyr-control-padding);
padding: var(--plyr-control-padding, $plyr-control-padding);
position: relative;
transition: all 0.3s ease;
svg {
display: block;
fill: currentColor;
height: var(--plyr-control-icon-size);
height: var(--plyr-control-icon-size, $plyr-control-icon-size);
pointer-events: none;
width: var(--plyr-control-icon-size);
width: var(--plyr-control-icon-size, $plyr-control-icon-size);
}
// Default focus
@ -50,64 +51,3 @@ a.plyr__control {
.plyr__control.plyr__control--pressed .label--not-pressed {
display: none;
}
// Audio control
.plyr--audio .plyr__control {
&.plyr__tab-focus,
&:hover,
&[aria-expanded='true'] {
background: $plyr-audio-control-bg-hover;
color: $plyr-audio-control-color-hover;
}
}
// Video control
.plyr--video .plyr__control {
svg {
filter: drop-shadow(0 1px 1px rgba(#000, 0.15));
}
// Hover and tab focus
&.plyr__tab-focus,
&:hover,
&[aria-expanded='true'] {
background: var(--plyr-video-control-bg-hover);
color: var(--plyr-video-control-color-hover);
}
}
// Large play button (video only)
.plyr__control--overlaid {
background: var(--plyr-video-control-bg-hover);
border: 0;
border-radius: 100%;
box-shadow: 0 1px 1px rgba(#000, 0.15);
color: var(--plyr-video-control-color-hover);
display: none;
left: 50%;
padding: ceil($plyr-control-spacing * 1.5);
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
// Offset icon to make the play button look right
svg {
left: 2px; // Offset to make the play button look right
position: relative;
}
&:hover,
&:focus {
background: var(--plyr-video-control-bg-hover);
}
}
.plyr--playing .plyr__control--overlaid {
opacity: 0;
visibility: hidden;
}
.plyr--full-ui.plyr--video .plyr__control--overlaid {
display: block;
}

View File

@ -12,11 +12,11 @@
align-items: center;
display: flex;
justify-content: flex-end;
min-width: 0; // Fix for Edge issue where content would overflow
text-align: center;
.plyr__progress__container {
flex: 1;
min-width: 0; // Fix for Edge issue where content would overflow
}
// Spacing
@ -41,14 +41,6 @@
&.plyr__time + .plyr__time {
padding-left: 0;
}
&.plyr__volume {
padding-right: ($plyr-control-spacing / 2);
}
&.plyr__volume:first-child {
padding-right: 0;
}
}
// Hide empty controls
@ -57,40 +49,6 @@
}
}
// Audio controls
.plyr--audio .plyr__controls {
background: $plyr-audio-controls-bg;
border-radius: inherit;
color: $plyr-audio-control-color;
padding: $plyr-control-spacing;
}
// Video controls
.plyr--video .plyr__controls {
background: linear-gradient(rgba($plyr-video-controls-bg, 0), rgba($plyr-video-controls-bg, 0.7));
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
bottom: 0;
color: $plyr-video-control-color;
left: 0;
padding: ($plyr-control-spacing * 2) ($plyr-control-spacing / 2) ($plyr-control-spacing / 2);
position: absolute;
right: 0;
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
z-index: 3;
@media (min-width: $plyr-bp-sm) {
padding: ($plyr-control-spacing * 3.5) $plyr-control-spacing $plyr-control-spacing;
}
}
// Hide video controls
.plyr--video.plyr--hide-controls .plyr__controls {
opacity: 0;
pointer-events: none;
transform: translateY(100%);
}
// Some options are hidden by default
.plyr [data-plyr='captions'],
.plyr [data-plyr='pip'],

View File

@ -24,7 +24,7 @@
// The actual menu container
&__container {
animation: plyr-popup 0.2s ease;
background: $plyr-menu-bg;
background: $plyr-menu-background;
border-radius: 4px;
bottom: 100%;
box-shadow: $plyr-menu-shadow;
@ -39,14 +39,13 @@
> div {
overflow: hidden;
transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1),
width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1), width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
// Arrow
&::after {
border: 4px solid transparent;
border-top-color: $plyr-menu-bg;
border-top-color: $plyr-menu-background;
content: '';
height: 0;
position: absolute;
@ -74,8 +73,7 @@
color: $plyr-menu-color;
display: flex;
font-size: $plyr-font-size-menu;
padding: ceil($plyr-control-padding / 2)
ceil($plyr-control-padding * 1.5);
padding: ceil($plyr-control-padding / 2) ceil($plyr-control-padding * 1.5);
user-select: none;
width: 100%;
@ -174,7 +172,7 @@
&[aria-checked='true'] {
&::before {
background: $plyr-color-main;
background: var(--plyr-color-main, $plyr-color-main);
}
&::after {
opacity: 1;

View File

@ -67,25 +67,16 @@ $plyr-progress-offset: $plyr-range-thumb-height;
}
}
.plyr--video .plyr__progress__buffer {
box-shadow: 0 1px 1px rgba(#000, 0.15);
color: $plyr-video-progress-buffered-bg;
}
.plyr--audio .plyr__progress__buffer {
color: $plyr-audio-progress-buffered-bg;
}
// Loading state
.plyr--loading .plyr__progress__buffer {
animation: plyr-progress 1s linear infinite;
background-image: linear-gradient(
-45deg,
$plyr-progress-loading-bg 25%,
var(--plyr-progress-loading-background, $plyr-progress-loading-background) 25%,
transparent 25%,
transparent 50%,
$plyr-progress-loading-bg 50%,
$plyr-progress-loading-bg 75%,
var(--plyr-progress-loading-background, $plyr-progress-loading-background) 50%,
var(--plyr-progress-loading-background, $plyr-progress-loading-background) 75%,
transparent 75%,
transparent
);
@ -95,9 +86,9 @@ $plyr-progress-offset: $plyr-range-thumb-height;
}
.plyr--video.plyr--loading .plyr__progress__buffer {
background-color: $plyr-video-progress-buffered-bg;
background-color: $plyr-video-progress-buffered-background;
}
.plyr--audio.plyr--loading .plyr__progress__buffer {
background-color: $plyr-audio-progress-buffered-bg;
background-color: $plyr-audio-progress-buffered-background;
}

View File

@ -9,7 +9,7 @@
border: 0;
border-radius: ($plyr-range-thumb-height * 2);
// color is used in JS to populate lower fill for WebKit
color: $plyr-range-fill-bg;
color: $plyr-range-fill-background;
display: block;
height: $plyr-range-max-height;
margin: 0;
@ -19,11 +19,7 @@
&::-webkit-slider-runnable-track {
@include plyr-range-track();
background-image: linear-gradient(
to right,
currentColor var(--value, 0%),
transparent var(--value, 0%)
);
background-image: linear-gradient(to right, currentColor var(--value, 0%), transparent var(--value, 0%));
}
&::-webkit-slider-thumb {
@ -96,69 +92,3 @@
}
}
}
// Video range inputs
.plyr--full-ui.plyr--video input[type='range'] {
&::-webkit-slider-runnable-track {
background-color: $plyr-video-range-track-bg;
}
&::-moz-range-track {
background-color: $plyr-video-range-track-bg;
}
&::-ms-track {
background-color: $plyr-video-range-track-bg;
}
// Pressed styles
&:active {
&::-webkit-slider-thumb {
@include plyr-range-thumb-active();
}
&::-moz-range-thumb {
@include plyr-range-thumb-active();
}
&::-ms-thumb {
@include plyr-range-thumb-active();
}
}
}
// Audio range inputs
.plyr--full-ui.plyr--audio input[type='range'] {
&::-webkit-slider-runnable-track {
background-color: $plyr-audio-range-track-bg;
}
&::-moz-range-track {
background-color: $plyr-audio-range-track-bg;
}
&::-ms-track {
background-color: $plyr-audio-range-track-bg;
}
// Pressed styles
&:active {
&::-webkit-slider-thumb {
@include plyr-range-thumb-active(
$plyr-audio-range-thumb-shadow-color
);
}
&::-moz-range-thumb {
@include plyr-range-thumb-active(
$plyr-audio-range-thumb-shadow-color
);
}
&::-ms-thumb {
@include plyr-range-thumb-active(
$plyr-audio-range-thumb-shadow-color
);
}
}
}

View File

@ -18,7 +18,3 @@
display: none;
}
}
.plyr--video .plyr__time {
text-shadow: 0 1px 1px rgba(#000, 0.15);
}

View File

@ -3,7 +3,7 @@
// --------------------------------------------------------------
.plyr__tooltip {
background: $plyr-tooltip-bg;
background: $plyr-tooltip-background;
border-radius: $plyr-tooltip-radius;
bottom: 100%;
box-shadow: $plyr-tooltip-shadow;
@ -27,7 +27,7 @@
&::before {
border-left: $plyr-tooltip-arrow-size solid transparent;
border-right: $plyr-tooltip-arrow-size solid transparent;
border-top: $plyr-tooltip-arrow-size solid $plyr-tooltip-bg;
border-top: $plyr-tooltip-arrow-size solid $plyr-tooltip-background;
bottom: -$plyr-tooltip-arrow-size;
content: '';
height: 0;

View File

@ -1,55 +0,0 @@
// --------------------------------------------------------------
// Video styles
// --------------------------------------------------------------
.plyr--video {
background: #000;
overflow: hidden;
// Menu open
&.plyr--menu-open {
overflow: visible;
}
}
.plyr__video-wrapper {
background: #000;
border-radius: inherit;
overflow: hidden;
position: relative;
// Require z-index to force border-radius
z-index: 0;
}
// Default to 16:9 ratio but this is set by JavaScript based on config
$embed-padding: ((100 / 16) * 9);
.plyr__video-embed,
.plyr__video-wrapper--fixed-ratio {
height: 0;
padding-bottom: to-percentage($embed-padding);
}
.plyr__video-embed iframe,
.plyr__video-wrapper--fixed-ratio video {
border: 0;
height: 100%;
left: 0;
position: absolute;
top: 0;
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));
// Only used for Vimeo
> .plyr__video-embed__container {
padding-bottom: to-percentage($height);
position: relative;
transform: translateY(-$offset);
}
}

View File

@ -5,33 +5,21 @@
.plyr__volume {
align-items: center;
display: flex;
flex: 1;
max-width: 110px;
min-width: 80px;
position: relative;
width: 20%;
input[type='range'] {
margin-left: ($plyr-control-spacing / 2);
margin-right: ($plyr-control-spacing / 2);
position: relative;
z-index: 2;
}
@media (min-width: $plyr-bp-sm) {
max-width: 90px;
}
@media (min-width: $plyr-bp-md) {
max-width: 110px;
}
}
// 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
// Auto size on iOS as there's no slider
.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;
min-width: 0;
width: auto;
}

View File

@ -27,7 +27,7 @@ $css-vars-use-native: false !default;
//
// E.G.:
// color: var(--main-color);
// background: var(--main-bg, green);
// background: var(--main-background, green);
///
@function var($args...) {
// CHECK PARAMS

View File

@ -37,7 +37,7 @@
}
@mixin plyr-range-thumb() {
background: $plyr-range-thumb-bg;
background: $plyr-range-thumb-background;
border: 0;
border-radius: 100%;
box-shadow: $plyr-range-thumb-shadow;
@ -47,7 +47,7 @@
width: $plyr-range-thumb-height;
}
@mixin plyr-range-thumb-active($color: rgba($plyr-range-thumb-bg, 0.5)) {
@mixin plyr-range-thumb-active($color: rgba($plyr-range-thumb-background, 0.5)) {
box-shadow: $plyr-range-thumb-shadow, 0 0 0 $plyr-range-thumb-active-shadow-width $color;
}
@ -66,12 +66,13 @@
.plyr__video-wrapper {
height: 100%;
width: 100%;
position: static;
}
// Vimeo requires some different styling
&.plyr--vimeo .plyr__video-wrapper {
height: 0;
position: relative;
top: 50%;
transform: translateY(-50%);
}

View File

@ -23,7 +23,7 @@
// The countdown label
&::after {
background: rgba($plyr-color-gunmetal, 0.8);
background: rgba($plyr-color-gray-900, 0.8);
border-radius: 2px;
bottom: $plyr-control-spacing;
color: #fff;

View File

@ -3,19 +3,19 @@
// --------------------------------------------------------------
$plyr-preview-padding: $plyr-tooltip-padding !default;
$plyr-preview-bg: $plyr-tooltip-bg !default;
$plyr-preview-background: $plyr-tooltip-background !default;
$plyr-preview-radius: $plyr-tooltip-radius !default;
$plyr-preview-shadow: $plyr-tooltip-shadow !default;
$plyr-preview-arrow-size: $plyr-tooltip-arrow-size !default;
$plyr-preview-image-bg: $plyr-color-heather !default;
$plyr-preview-image-background: $plyr-color-gray-200 !default;
$plyr-preview-time-font-size: $plyr-font-size-time !default;
$plyr-preview-time-padding: 3px 6px !default;
$plyr-preview-time-bg: rgba(0, 0, 0, 0.55);
$plyr-preview-time-background: rgba(0, 0, 0, 0.55);
$plyr-preview-time-color: #fff;
$plyr-preview-time-bottom-offset: 6px;
.plyr__preview-thumb {
background-color: $plyr-preview-bg;
background-color: $plyr-preview-background;
border-radius: 3px;
bottom: 100%;
box-shadow: $plyr-preview-shadow;
@ -38,7 +38,7 @@ $plyr-preview-time-bottom-offset: 6px;
&::before {
border-left: $plyr-preview-arrow-size solid transparent;
border-right: $plyr-preview-arrow-size solid transparent;
border-top: $plyr-preview-arrow-size solid $plyr-preview-bg;
border-top: $plyr-preview-arrow-size solid $plyr-preview-background;
bottom: -$plyr-preview-arrow-size;
content: '';
height: 0;
@ -50,7 +50,7 @@ $plyr-preview-time-bottom-offset: 6px;
}
&__image-container {
background: $plyr-preview-image-bg;
background: $plyr-preview-image-background;
border-radius: ($plyr-preview-radius - 1px);
overflow: hidden;
position: relative;
@ -77,7 +77,7 @@ $plyr-preview-time-bottom-offset: 6px;
z-index: 3;
span {
background-color: $plyr-preview-time-bg;
background-color: $plyr-preview-time-background;
border-radius: ($plyr-preview-radius - 1px);
color: $plyr-preview-time-color;
font-size: $plyr-preview-time-font-size;

View File

@ -37,14 +37,16 @@ $css-vars-use-native: true;
@import 'components/poster';
@import 'components/times';
@import 'components/tooltips';
@import 'components/video';
@import 'components/progress';
@import 'components/volume';
@import 'types/audio';
@import 'types/video';
@import 'states/fullscreen';
@import 'plugins/ads';
@import 'plugins/previewThumbnails';
@import 'plugins/preview-thumbnails';
@import 'utils/animation';
@import 'utils/hidden';

View File

@ -2,5 +2,5 @@
// Badges
// ==========================================================================
$plyr-badge-bg: $plyr-color-fiord !default;
$plyr-badge-background: $plyr-color-gray-700 !default;
$plyr-badge-color: #fff !default;

View File

@ -9,10 +9,3 @@ $plyr-font-size-captions-base: $plyr-font-size-base !default;
$plyr-font-size-captions-small: $plyr-font-size-small !default;
$plyr-font-size-captions-medium: $plyr-font-size-large !default;
$plyr-font-size-captions-large: $plyr-font-size-xlarge !default;
@include css-vars(
(
--plyr-captions-background: $plyr-captions-background,
--plyr-captions-text-color: $plyr-captions-text-color
)
);

View File

@ -2,18 +2,32 @@
// Colors
// ==========================================================================
$plyr-color-main: #fff !default;
$plyr-color-gunmetal: #2f343d !default;
$plyr-color-fiord: #4f5b5f !default;
$plyr-color-lynch: #6b7d85 !default;
$plyr-color-heather: #b7c5cd !default;
$plyr-color-main: hsl(198, 100%, 50%) !default;
// Grayscale
$plyr-color-gray-900: hsl(210, 15%, 16%);
$plyr-color-gray-800: lighten($plyr-color-gray-900, 9%);
$plyr-color-gray-700: lighten($plyr-color-gray-800, 9%);
$plyr-color-gray-600: lighten($plyr-color-gray-700, 9%);
$plyr-color-gray-500: lighten($plyr-color-gray-600, 9%);
$plyr-color-gray-400: lighten($plyr-color-gray-500, 9%);
$plyr-color-gray-300: lighten($plyr-color-gray-400, 9%);
$plyr-color-gray-200: lighten($plyr-color-gray-300, 9%);
$plyr-color-gray-100: lighten($plyr-color-gray-200, 9%);
$plyr-color-gray-50: lighten($plyr-color-gray-100, 9%);
@include css-vars(
(
--plyr-color-main: $plyr-color-main,
--plyr-color-gunmetal: $plyr-color-gunmetal,
--plyr-color-fiord: $plyr-color-fiord,
--plyr-color-lynch: $plyr-color-lynch,
--plyr-color-heather: $plyr-color-heather
--plyr-color-gray-900: $plyr-color-gray-900,
--plyr-color-gray-800: $plyr-color-gray-800,
--plyr-color-gray-700: $plyr-color-gray-700,
--plyr-color-gray-600: $plyr-color-gray-600,
--plyr-color-gray-500: $plyr-color-gray-500,
--plyr-color-gray-400: $plyr-color-gray-400,
--plyr-color-gray-300: $plyr-color-gray-300,
--plyr-color-gray-200: $plyr-color-gray-200,
--plyr-color-gray-100: $plyr-color-gray-100,
--plyr-color-gray-50: $plyr-color-gray-50
)
);

View File

@ -7,29 +7,12 @@ $plyr-control-spacing: 10px !default;
$plyr-control-padding: ($plyr-control-spacing * 0.7) !default;
$plyr-control-radius: 3px !default;
$plyr-video-controls-bg: #000 !default;
$plyr-video-controls-background: linear-gradient(rgba(#000, 0), rgba(#000, 0.75)) !default;
$plyr-video-control-color: #fff !default;
$plyr-video-control-color-hover: #000 !default;
$plyr-video-control-bg-hover: var(--plyr-color-main) !default;
$plyr-video-control-color-hover: #fff !default;
$plyr-video-control-background-hover: $plyr-color-main !default;
$plyr-audio-controls-bg: #fff !default;
$plyr-audio-control-color: var(--plyr-color-fiord) !default;
$plyr-audio-controls-background: #fff !default;
$plyr-audio-control-color: $plyr-color-gray-700 !default;
$plyr-audio-control-color-hover: #fff !default;
$plyr-audio-control-bg-hover: var(--plyr-color-main) !default;
@include css-vars(
(
--plyr-control-icon-size: $plyr-control-icon-size,
--plyr-control-spacing: $plyr-control-spacing,
--plyr-control-padding: $plyr-control-padding,
--plyr-control-radius: $plyr-control-radius,
--plyr-video-controls-bg: $plyr-video-controls-bg,
--plyr-video-control-color: $plyr-video-control-color,
--plyr-video-control-color-hover: $plyr-video-control-color-hover,
--plyr-video-control-bg-hover: $plyr-video-control-bg-hover,
--plyr-audio-controls-bg: $plyr-audio-controls-bg,
--plyr-audio-control-color: $plyr-audio-control-color,
--plyr-audio-control-color-hover: $plyr-audio-control-color-hover,
--plyr-audio-control-bg-hover: $plyr-audio-control-bg-hover
)
);
$plyr-audio-control-background-hover: $plyr-color-main !default;

Some files were not shown because too many files have changed in this diff Show More