Compare commits

...

71 Commits

Author SHA1 Message Date
43e6dcd41d Fix for local storage issue 2018-03-18 01:37:24 +11:00
b06c8ae43f Changelog updated 2018-03-18 01:14:18 +11:00
c7ea13c0c7 Sentry in live only 2018-03-18 01:08:05 +11:00
0f8c6e147b Added Sentry 2018-03-18 00:21:23 +11:00
e566365288 Typo 2018-03-17 23:44:40 +11:00
a06e0f5890 Updated screenshot 2018-03-17 23:40:28 +11:00
3bccc0da01 v3.0.0 2018-03-17 23:33:25 +11:00
a0173d991e Removed beta message 2018-03-17 23:31:34 +11:00
600f0eb8a3 Merge branch 'beta'
# Conflicts:
#	readme.md
2018-03-17 23:30:16 +11:00
5db73b1327 Added buffered getter 2018-03-17 23:27:40 +11:00
5cb1628cd8 Vimeo fix 2018-03-15 10:29:05 +11:00
c74b75e8e1 3.0.0-beta.20 2018-03-13 23:35:17 +11:00
0538476d6f 3.0.0-beta.19 2018-03-13 22:15:28 +11:00
5ebe18d081 Typography fix 2018-03-13 22:00:40 +11:00
207adde36d 3.0.0-beta.18 2018-03-13 21:44:18 +11:00
1b13ddaa54 Update ads 2018-03-13 21:42:01 +11:00
9981c349be Fix for null manager race condition 2018-03-11 18:23:47 +11:00
b3365a7373 Normalised event names and removed unused 2018-03-11 12:54:51 +11:00
9a0c1c830d Merge pull request #804 from friday/ads-trigger-arguments
[v3] Add optional arguments to Ads.trigger
2018-03-11 10:55:29 +11:00
ef27ba16f4 Add optional argument to Ads.trigger (currently only used for adblocker error) 2018-03-10 16:20:33 +01:00
e206edc1f6 Event listener fixes, loadScript promise, ads tweaks 2018-03-11 02:03:35 +11:00
c734bc4957 Merge branch 'beta' of github.com:sampotts/plyr into beta 2018-03-10 23:32:55 +11:00
572b8a7aca Manually merged PRs 2018-03-10 23:32:48 +11:00
eebae4a227 Merge pull request #802 from gehaktmolen/ad-hotfixes
Advertisement couldnt be loaded when creative dimensions do not align after resizing
2018-03-10 23:32:15 +11:00
e0562752ea Merge pull request #795 from frogg/patch-1
Added link that explains Webkit's autoplay blocker
2018-03-10 23:29:22 +11:00
6a2ca534d2 Removed redundant wrappers within the adsmanager promises. 2018-03-09 14:29:37 +01:00
7adc2bc6c8 Unneeded else has been removed within the play() method. 2018-03-09 13:21:19 +01:00
ba8d7831a7 Made sure play() returns a promise. 2018-03-09 12:50:57 +01:00
69ffcbad27 Ad block detection would not work when calling play() right after creating the player instance, so the adsManager now also rejects on such a case. Also made sure that calling play() will wait for the adsManager promise to resolve or otherwise return the media.play() method. 2018-03-09 11:17:24 +01:00
819f7d1080 Resizing the ad container while having it on display none will return offset width and height of 0, which will cause ads not to play when ad sizes are set within the clients DSP. Also making sure that the inner containers of the ad container are full size. The container is now hidden/ displayed using z-index. 2018-03-07 15:43:48 +01:00
409b588458 Made sure that cue points for midrolls are not displayed when the ad rule for a midroll doesn't exceed the total play time of a video. 2018-03-07 15:17:30 +01:00
e90a603d57 Removed a double this.enabled variable and updated a comment in ads.js. Also made sure the adsmanager promise also can fail, so we can use it to wait for getting the advertisement ready when someone clicks the play button. Otherwise there it can look glitchy when the actual video starts playing and the video ad plays a few seconds later because the vast tag was slow to retrieve. Also fixed a typo. 2018-03-06 17:27:59 +01:00
6f061621ad v3.0.0-beta.17 2018-03-03 23:16:15 +11:00
0300610108 Typo 2018-03-03 23:14:57 +11:00
2fba5f152c 3.0.0-beta.16 2018-03-03 23:06:54 +11:00
317b08c703 Ready event fix, YouTube play event fix, docs update 2018-03-03 23:06:12 +11:00
e6db374a72 Added link that explains Webkit's autoplay blocker 2018-02-24 16:19:55 +01:00
bfb550b8d0 Package updates 2018-02-22 23:46:52 +11:00
174234c166 v3.0.0-beta.15 2018-02-19 09:55:16 +11:00
24b4220de5 Fix IE CORS captions 2018-02-19 09:52:46 +11:00
f1895a4cce Pause button fix, polyfilled build, unminified builds 2018-02-17 19:34:15 +11:00
c2a6306d46 Merge pull request #781 from friday/gulp-unminified-js-output
Build both minified and non-minified js-bundles
2018-02-17 09:22:34 +11:00
7ac732f45b Merge branch 'beta' into gulp-unminified-js-output 2018-02-17 09:22:19 +11:00
c90f1bdf08 v3.0.0-beta.13 2018-02-13 00:02:13 +11:00
6a9be8d16b Fix for custom controls 2018-02-13 00:01:19 +11:00
58c2c52c95 Merge branch 'beta' of github.com:sampotts/plyr into beta 2018-02-11 15:09:40 +11:00
73a39769d4 Fullscreen API changes, color settings tweaks 2018-02-11 15:09:34 +11:00
7221e26eca Merge pull request #780 from friday/captions-ie11-indexsizeerror
Fix harmless but annoying IE error 'IndexSizeError'
2018-02-06 12:51:45 +11:00
98adb8e784 Fix harmless but annoying IE error 'IndexSizeError' 2018-02-06 02:39:01 +01:00
a59dcb2f53 Gulp js build: create both minified and non-minified outputs 2018-02-06 01:57:27 +01:00
d21b58e1c9 Copy 2018-02-06 11:26:13 +11:00
d6e84cbabb Nicer checks 2018-02-06 11:12:03 +11:00
fcccf1d479 Copy 2018-02-06 11:08:26 +11:00
211db12a3d Readme merge 2018-02-06 11:06:46 +11:00
ab7f277a1b Merge pull request #769 from redxtech/add-vue-plyr-to-readme
Add vue-plyr to readme
2018-02-06 10:51:42 +11:00
ce1d5a60d6 Remove eslint-rule 'no-shadow' (common variable names should be able to exist in different scopes) 2018-02-05 23:22:20 +01:00
f67315e20c 3.0.0-beta.12 2018-02-06 00:25:50 +11:00
2150c44036 Added backwards compatibility for <div> embeds 2018-02-06 00:24:48 +11:00
70c9fbdde3 Removed fetch dependency 2018-02-05 21:43:32 +11:00
f3ea31c515 Merge branch 'beta' of github.com:sampotts/plyr into beta
# Conflicts:
#	dist/plyr.js
#	dist/plyr.js.map
2018-02-05 21:28:16 +11:00
1ee88cba16 Testing fetch 2018-02-05 21:26:18 +11:00
af3ae75229 Update readme.md 2018-02-03 23:16:52 +11:00
d76ef3ff91 Small UI tweaks and fix for instanceof issue 2018-01-31 19:33:00 +11:00
2691c7c9d6 Version bump + icon fix 2018-01-30 13:01:05 +11:00
26b1d8ce8f Fix UMD stuff 2018-01-30 12:57:19 +11:00
6fae148fc1 Deploy 2018-01-30 09:26:09 +11:00
bb51647fe2 Merge pull request #772 from sampotts/fix/ads-blocked
Fix: ads blocked and media playing before ad plays
2018-01-30 09:22:54 +11:00
71efbe7a92 Merge branch 'beta' into fix/ads-blocked
# Conflicts:
#	dist/plyr.js
#	dist/plyr.js.map
#	src/js/plugins/ads.js
#	src/js/plyr.js
2018-01-30 09:22:14 +11:00
afd695cb39 Fix typo's 2018-01-29 22:45:46 +01:00
c4eb4c97ac fix(ads): Fixes media from playing when ads are blocked 2018-01-29 22:40:08 +01:00
d5a1a7ca1c Add vue-plyr to readme 2018-01-28 23:03:05 -07:00
54 changed files with 26523 additions and 2268 deletions

View File

@ -8,6 +8,7 @@
"globals": { "Plyr": false, "jQuery": false },
"rules": {
"no-const-assign": 1,
"no-shadow": 0,
"no-this-before-super": 1,
"no-undef": 1,
"no-unreachable": 1,

4
.npmignore Normal file
View File

@ -0,0 +1,4 @@
demo
.github
.vscode
*.code-workspace

View File

@ -1,6 +1,6 @@
{
"plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"],
"extends": ["stylelint-config-sass-guidelines", "stylelint-config-prettier"],
"extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"],
"rules": {
"selector-class-pattern": null,
"selector-no-qualifying-type": [

View File

@ -4,7 +4,8 @@
"plyr.css": "src/sass/plyr.scss"
},
"js": {
"plyr.js": "src/js/plyr.js"
"plyr.js": "src/js/plyr.js",
"plyr.polyfilled.js": "src/js/plyr.polyfilled.js"
}
},
"demo": {

File diff suppressed because it is too large Load Diff

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

4016
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

2
demo/dist/demo.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
demo/dist/demo.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -66,7 +66,7 @@
</p>
<p>Premium video monitization from
<a href="https://vi.ai/publisher-video-monetization/" target="_blank" class="no-border">
<a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi">
<span class="sr-only">ai.vi</span>
</a>
@ -163,13 +163,13 @@
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"></path>
</svg>
<p>If you think Plyr's good,
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&url=http%3A%2F%2Fplyr.io&via=Sam_Potts"
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank" data-shr-network="twitter">tweet it</a>
</p>
</aside>
<!-- Polyfills -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,fetch"></script>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent"></script>
<!-- Plyr core script -->
<script src="../dist/plyr.js"></script>
@ -181,7 +181,7 @@
<script src="https://cdn.rangetouch.com/1.0.1/rangetouch.js" async></script>
<!-- Docs script -->
<script src="dist/demo.js"></script>
<script src="dist/demo.js" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -1,183 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player</title>
<meta name="description" property="og:description" content="A simple HTML5 media player with custom controls and WebVTT captions.">
<meta name="author" content="Sam Potts">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Icons -->
<link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico">
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16">
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png">
<!-- Opengraph -->
<meta property="og:title" content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player">
<meta property="og:site_name" content="Plyr">
<meta property="og:url" content="https://plyr.io">
<meta property="og:image" content="https://cdn.plyr.io/static/icons/1200x630.png">
<!-- Twitter -->
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@sam_potts">
<meta name="twitter:creator" content="@sam_potts">
<meta name="twitter:card" content="summary_large_image">
<!-- Docs styles -->
<link rel="stylesheet" href="dist/demo.css">
<!-- Preload -->
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2">
</head>
<body>
<div class="grid">
<header>
<h1>Plyr</h1>
<p>A simple, accessible and customisable media player for
<button type="button" class="faux-link" data-source="video">
<svg class="icon">
<title>HTML5</title>
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
</svg>Video</button>,
<button type="button" class="faux-link" data-source="audio">
<svg class="icon">
<title>HTML5</title>
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
</svg>Audio</button>,
<button type="button" class="faux-link" data-source="youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z"></path>
</svg>YouTube</button> and
<button type="button" class="faux-link" data-source="vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"></path>
</svg>Vimeo</button>
</p>
<!--<p>Monetization options provided by
<a href="https://vi.ai" target="_blank">vi.ai</a>
</p>-->
<div class="call-to-action">
<span class="button--with-count">
<a href="https://github.com/sampotts/plyr" target="_blank" class="button" data-shr-network="github">
<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>
</div>
</header>
<main>
<video controls crossorigin playsinline poster="media/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
<!-- Video files -->
<source src="media/View_From_A_Blue_Moon_Trailer-HD.mp4" type="video/mp4">
<!--<source src="media/View_From_A_Blue_Moon_Trailer-UHD.mp4" type="video/mp4">-->
<!-- Text track file -->
<track kind="captions" label="English" srclang="en" src="media/View_From_A_Blue_Moon_Trailer-HD.en.vtt" default>
<track kind="captions" label="Français" srclang="fr" src="media/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
<!-- Fallback for browsers that don't support the <video> element -->
<a href="media/View_From_A_Blue_Moon_Trailer-HD.mp4" download>Download</a>
</video>
<ul>
<li class="plyr__cite plyr__cite--video" hidden>
<small>
<svg class="icon">
<title>HTML5</title>
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
</svg>
<a href="http://viewfromabluemoon.com/" target="_blank">View From A Blue Moon</a> &copy; Brainfarm
</small>
</li>
<li class="plyr__cite plyr__cite--audio" hidden>
<small>
<svg class="icon" title="HTML5">
<title>HTML5</title>
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
</svg>
<a href="http://www.kishibashi.com/" target="_blank">Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;</a> &copy; Kishi Bashi
</small>
</li>
<li class="plyr__cite plyr__cite--youtube" hidden>
<small>
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank">View From A Blue Moon</a> on&nbsp;
<span class="color--youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z"></path>
</svg>YouTube
</span>
</small>
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/ondemand/viewfromabluemoon4k" target="_blank">View From A Blue Moon</a> on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"></path>
</svg>Vimeo
</span>
</small>
</li>
</ul>
</main>
</div>
<aside>
<svg class="icon">
<title>Twitter</title>
<path d="M16,3c-0.6,0.3-1.2,0.4-1.9,0.5c0.7-0.4,1.2-1,1.4-1.8c-0.6,0.4-1.3,0.6-2.1,0.8c-0.6-0.6-1.5-1-2.4-1
C9.3,1.5,7.8,3,7.8,4.8c0,0.3,0,0.5,0.1,0.7C5.2,5.4,2.7,4.1,1.1,2.1c-0.3,0.5-0.4,1-0.4,1.7c0,1.1,0.6,2.1,1.5,2.7
c-0.5,0-1-0.2-1.5-0.4c0,0,0,0,0,0c0,1.6,1.1,2.9,2.6,3.2C3,9.4,2.7,9.4,2.4,9.4c-0.2,0-0.4,0-0.6-0.1c0.4,1.3,1.6,2.3,3.1,2.3
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"></path>
</svg>
<p>If you think Plyr's good,
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&url=http%3A%2F%2Fplyr.io&via=Sam_Potts"
target="_blank" data-shr-network="twitter">tweet it</a>
</p>
</aside>
<!-- Polyfills -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,fetch"></script>
<!-- Plyr core script -->
<script src="../dist/plyr.js"></script>
<!-- Sharing libary (https://shr.one) -->
<script src="https://cdn.shr.one/1.0.1/shr.js"></script>
<!-- Rangetouch to fix <input type="range"> on touch devices (see https://rangetouch.com) -->
<script src="https://cdn.rangetouch.com/1.0.1/rangetouch.js" async></script>
<!-- Docs script -->
<script src="dist/demo.js"></script>
</body>
</html>

View File

@ -4,242 +4,257 @@
// Please see readme.md in the root or github.com/sampotts/plyr
// ==========================================================================
document.addEventListener('DOMContentLoaded', () => {
if (window.shr) {
window.shr.setup({
count: {
classname: 'button__count',
},
});
import Raven from 'raven-js';
(() => {
const isLive = window.location.host === 'plyr.io';
// Raven / Sentry
// For demo site (https://plyr.io) only
if (isLive) {
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
}
// Setup tab focus
const tabClassName = 'tab-focus';
document.addEventListener('DOMContentLoaded', () => {
Raven.context(() => {
if (window.shr) {
window.shr.setup({
count: {
classname: 'button__count',
},
});
}
// Remove class on blur
document.addEventListener('focusout', event => {
event.target.classList.remove(tabClassName);
});
// Setup tab focus
const tabClassName = 'tab-focus';
// Add classname to tabbed elements
document.addEventListener('keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Remove class on blur
document.addEventListener('focusout', event => {
event.target.classList.remove(tabClassName);
});
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
window.setTimeout(() => {
document.activeElement.classList.add(tabClassName);
}, 0);
});
// Add classname to tabbed elements
document.addEventListener('keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Setup the player
const player = new window.Plyr('#player', {
debug: true,
title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
enabled: true,
},
});
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
document.activeElement.classList.add(tabClassName);
}, 0);
});
// Expose for testing
window.player = player;
// Setup the player
const player = new Plyr('#player', {
debug: true,
title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
enabled: true,
publisherId: '918848828995742',
},
});
// 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;
// Expose for tinkering in the console
window.player = player;
// Toggle class on an element
function toggleClass(element, className, state) {
if (element) {
element.classList[state ? 'add' : 'remove'](className);
}
}
// 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;
// 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;
}
// Toggle class on an element
function toggleClass(element, className, state) {
if (element) {
element.classList[state ? 'add' : 'remove'](className);
}
}
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-HD.mp4',
type: 'video/mp4',
}],
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',
},
],
};
// 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;
}
break;
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-HD.mp4',
type: 'video/mp4',
}],
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',
},
],
};
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;
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',
},
],
};
case types.youtube:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
}],
};
break;
break;
case types.youtube:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
}],
};
case types.vimeo:
player.source = {
type: 'video',
sources: [{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
}],
};
break;
break;
case types.vimeo:
player.source = {
type: 'video',
sources: [{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
}],
};
default:
break;
}
break;
// Set the current type for next time
currentType = type;
default:
break;
}
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Set the current type for next time
currentType = type;
// Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
cite.setAttribute('hidden', '');
});
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
}
// Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Bind to each button
Array.from(buttons).forEach(button => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
cite.setAttribute('hidden', '');
});
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
}
newSource(type);
// Bind to each button
Array.from(buttons).forEach(button => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
newSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && 'type' in event.state) {
newSource(event.state.type);
}
});
// On load
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
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);
}
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && 'type' in event.state) {
newSource(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);
}
// Google analytics
// For demo site (https://plyr.io) only
/* eslint-disable */
if (isLive) {
(function(i, s, o, g, r, a, m) {
i.GoogleAnalyticsObject = r;
i[r] =
i[r] ||
function() {
(i[r].q = i[r].q || []).push(arguments);
};
i[r].l = 1 * new Date();
a = s.createElement(o);
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
window.ga('create', 'UA-40881672-11', 'auto');
window.ga('send', 'pageview');
}
});
// Google analytics
// For demo site (https://plyr.io) only
/* eslint-disable */
if (window.location.host === 'plyr.io') {
(function(i, s, o, g, r, a, m) {
i.GoogleAnalyticsObject = r;
i[r] =
i[r] ||
function() {
(i[r].q = i[r].q || []).push(arguments);
};
i[r].l = 1 * new Date();
a = s.createElement(o);
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
window.ga('create', 'UA-40881672-11', 'auto');
window.ga('send', 'pageview');
}
/* eslint-enable */
/* eslint-enable */
})();

View File

@ -6,11 +6,11 @@
.button,
.button__count {
align-items: center;
background: #fff;
background: $color-button-background;
border: 0;
border-radius: $border-radius-base;
box-shadow: 0 1px 1px rgba(#000, 0.1);
color: $gray;
color: $color-button-text;
display: inline-flex;
padding: ($spacing-base * 0.75);
position: relative;
@ -71,7 +71,7 @@
&::before {
border: $arrow-size solid transparent;
border-left-width: 0;
border-right-color: #fff;
border-right-color: $color-button-background;
content: '';
height: 0;
position: absolute;

View File

@ -28,6 +28,7 @@ body {
main {
margin: auto;
padding-bottom: 1px; // Collapsing margins
text-align: center;
}

View File

@ -24,5 +24,9 @@ $color-vimeo: #19b7ed;
$color-link: #fff;
$color-background: $color-brand-primary;
// Buttons
$color-button-background: #fff;
$color-button-text: $gray;
// Focus
$tab-focus-default-color: #fff;

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

7334
dist/plyr.js vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/plyr.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

12533
dist/plyr.polyfilled.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/plyr.polyfilled.js.map vendored Normal file

File diff suppressed because one or more lines are too long

2
dist/plyr.polyfilled.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/plyr.polyfilled.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -9,6 +9,7 @@ const path = require('path');
const gulp = require('gulp');
const gutil = require('gulp-util');
const concat = require('gulp-concat');
const filter = require('gulp-filter');
const sass = require('gulp-sass');
const cleancss = require('gulp-clean-css');
const run = require('run-sequence');
@ -24,8 +25,7 @@ const size = require('gulp-size');
const rollup = require('gulp-better-rollup');
const babel = require('rollup-plugin-babel');
const sourcemaps = require('gulp-sourcemaps');
const uglify = require('rollup-plugin-uglify');
const { minify } = require('uglify-es');
const uglify = require('gulp-uglify-es').default;
const commonjs = require('rollup-plugin-commonjs');
const resolve = require('rollup-plugin-node-resolve');
@ -40,6 +40,8 @@ try {
// Do nothing
}
const minSuffix = '.min';
// Paths
const root = __dirname;
const paths = {
@ -68,8 +70,11 @@ const paths = {
root: path.join(root, 'demo/'),
},
upload: [
path.join(root, 'dist/**'),
path.join(root, 'demo/dist/**'),
path.join(root, `dist/*${minSuffix}.*`),
path.join(root, 'dist/*.css'),
path.join(root, 'dist/*.svg'),
path.join(root, `demo/dist/*${minSuffix}.*`),
path.join(root, 'demo/dist/*.css'),
],
};
@ -122,6 +127,7 @@ const build = {
Object.keys(files).forEach(key => {
const name = `js:${key}`;
tasks.js.push(name);
const { output } = paths[bundle];
gulp.task(name, () =>
gulp
@ -135,15 +141,19 @@ const build = {
resolve(),
commonjs(),
babel(babelrc),
uglify({}, minify),
],
},
options,
),
)
.pipe(size(sizeOptions))
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(paths[bundle].output)),
.pipe(gulp.dest(output))
.pipe(filter('**/*.js'))
.pipe(uglify())
.pipe(size(sizeOptions))
.pipe(rename({ suffix: minSuffix }))
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(output)),
);
});
},
@ -266,6 +276,23 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
const semver = new RegExp(`v${regex}`, 'gi');
const localPath = new RegExp('(../)?dist', 'gi');
const versionPath = `https://${aws.cdn.domain}/${version}`;
const cdnpath = new RegExp(`${aws.cdn.domain}/${regex}/`, 'gi');
gulp.task('version', () => {
console.log(`Updating versions to '${version}'...`);
// Replace versioned URLs in source
const files = [
'plyr.js',
'plyr.polyfilled.js',
'defaults.js',
];
gulp
.src(files.map(file => path.join(root, `src/js/${file}`)))
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(path.join(root, 'src/js/')));
});
// Publish version to CDN bucket
gulp.task('cdn', () => {
@ -277,22 +304,26 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
console.log(`Uploading '${version}' to ${aws.cdn.domain}...`);
// Upload to CDN
return gulp
.src(paths.upload)
.pipe(
size({
showFiles: true,
gzip: true,
}),
)
.pipe(
rename(p => {
// eslint-disable-next-line
p.dirname = p.dirname.replace('.', version);
}),
)
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.cdn, options.cdn));
return (
gulp
.src(paths.upload)
.pipe(
rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
}),
)
// Remove min suffix from source map URL
.pipe(replace(/sourceMappingURL=([\w-?.]+)/, (match, p1) => `sourceMappingURL=${p1.replace(minSuffix, '')}`))
.pipe(
size({
showFiles: true,
gzip: true,
}),
)
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.cdn, options.cdn))
);
});
// Publish to demo bucket
@ -304,21 +335,12 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
console.log(`Uploading '${version}' demo to ${aws.demo.domain}...`);
const cdnpath = new RegExp(`${aws.cdn.domain}/${regex}/`, 'gi');
// Replace versioned files in readme.md
gulp
.src([`${root}/readme.md`])
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(root));
// Replace versioned files in plyr.js
gulp
.src(path.join(root, 'src/js/plyr.js'))
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(path.join(root, 'src/js/')));
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
const index = `${paths.demo.root}index.html`;
@ -385,6 +407,6 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
// Do everything
gulp.task('publish', () => {
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo');
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo');
});
}

View File

@ -1,50 +1,51 @@
{
"name": "plyr",
"version": "3.0.0-beta.8",
"version": "3.0.1",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"main": "./dist",
"main": "./dist/plyr.js",
"browser": "./dist/plyr.min.js",
"sass": "./src/sass/plyr.scss",
"style": "./dist/plyr.css",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.1",
"babel-eslint": "^8.2.2",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.6.1",
"del": "^3.0.0",
"eslint": "^4.16.0",
"eslint": "^4.19.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.8.0",
"git-branch": "^1.0.0",
"eslint-plugin-import": "^2.9.0",
"git-branch": "^2.0.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^4.1.0",
"gulp-autoprefixer": "^5.0.0",
"gulp-better-rollup": "^3.0.0",
"gulp-clean-css": "^3.9.2",
"gulp-clean-css": "^3.9.3",
"gulp-concat": "^2.6.1",
"gulp-open": "^2.1.0",
"gulp-filter": "^5.1.0",
"gulp-open": "^3.0.0",
"gulp-rename": "^1.2.2",
"gulp-replace": "^0.6.1",
"gulp-s3": "^0.11.0",
"gulp-sass": "^3.1.0",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^1.12.1",
"gulp-sourcemaps": "^2.6.4",
"gulp-svgmin": "^1.2.4",
"gulp-svgstore": "^6.1.1",
"gulp-uglify-es": "^1.0.1",
"gulp-util": "^3.0.8",
"rollup-plugin-babel": "^3.0.3",
"rollup-plugin-commonjs": "^8.2.6",
"rollup-plugin-node-resolve": "^3.0.2",
"rollup-plugin-uglify": "^3.0.0",
"rollup-plugin-commonjs": "^8.4.1",
"rollup-plugin-node-resolve": "^3.2.0",
"run-sequence": "^2.2.1",
"stylelint": "^8.4.0",
"stylelint-config-prettier": "^2.0.0",
"stylelint-config-sass-guidelines": "^4.1.0",
"stylelint-config-standard": "^18.0.0",
"stylelint-order": "^0.8.0",
"stylelint-scss": "^2.2.0",
"stylelint-selector-bem-pattern": "^2.0.0",
"uglify-es": "^3.3.8"
"stylelint": "^9.1.3",
"stylelint-config-prettier": "^3.0.4",
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.0.0",
"stylelint-order": "^0.8.1",
"stylelint-scss": "^2.5.0",
"stylelint-selector-bem-pattern": "^2.0.0"
},
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
"repository": {
@ -62,5 +63,9 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Sam Potts <sam@potts.es>",
"dependencies": {}
"dependencies": {
"babel-polyfill": "^6.26.0",
"custom-event-polyfill": "^0.3.0",
"raven-js": "^3.23.3"
}
}

408
readme.md
View File

@ -1,33 +1,29 @@
---
Beware: This version is currently in beta and not production-ready
---
# Plyr
A simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo media player that supports [_modern_](#browser-support) browsers.
[Checkout the demo](https://plyr.io) - [Donate to support Plyr](#donate) - [Chat on Slack](http://bit.ly/plyr-chat)
[Checkout the demo](https://plyr.io) - [Donate to support Plyr](#donate) - [Chat on Slack](https://bit.ly/plyr-slack)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png)](https://plyr.io)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io)
## Features
* **Accessible** - full support for VTT captions and screen readers
* **Lightweight** - just 18KB minified and gzipped
* **[Customisable](#html)** - make the player look how you want with the markup you want
* **Semantic** - 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
* **[Streaming](#streaming)** - support for hls.js, Shaka and dash.js streaming playback
* **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
* **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
* **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
* **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
* **i18n support** - support for internationalization of controls
* **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
* **SASS** - to include in your build processes
* **Accessible** - full support for VTT captions and screen readers
* **[Customisable](#html)** - make the player look how you want with the markup you want
* **Semantic** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
`<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](#streaming)** - support for hls.js, Shaka and dash.js streaming playback
* **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
* **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
* **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
* **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
* **i18n support** - support for internationalization of controls
* **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
* **SASS** - to include in your build processes
Oh and yes, it works with Bootstrap.
@ -35,40 +31,25 @@ Oh and yes, it works with Bootstrap.
Check out the [changelog](changelog.md) to see what's new with Plyr.
## CMS plugins
## Plugins & Components
### [WordPress](https://wordpress.org/plugins/plyr/)
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
Created and maintained by Ryan Anthony Drake ([@iamryandrake](https://github.com/iamryandrake))
### [Neos](https://packagist.org/packages/jonnitto/plyr)
Created and maintained by Jon Uhlmann ([@jonnitto](https://github.com/jonnitto))
### [Kirby](https://github.com/dpschen/kirby-plyrtag)
Created and maintained by Dominik Pschenitschni ([@dpschen](https://github.com/dpschen))
## Using package managers
You can grab the source using one of the following package managers.
### npm
```
npm install plyr
```
[https://www.npmjs.com/package/plyr](https://www.npmjs.com/package/plyr)
| Type | Maintainer | Link |
| --------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| WordPress | Ryan Anthony Drake ([@iamryandrake](https://github.com/iamryandrake)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
## Quick setup
Here's a quick run through on getting up and running. There's also a [demo on Codepen](http://codepen.io/sampotts/pen/jARJYp).
Here's a quick run through on getting up and running. There's also a [demo on Codepen](http://codepen.io/sampotts/pen/jARJYp). You can grab all of the source with [NPM](https://www.npmjs.com/package/plyr) using `npm install plyr`.
### HTML
Plyr extends upon the standard HTML5 markup so that's all you need for those types. More info on advanced HTML markup can be found under
[initialising](#initialising).
Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) markup so that's all you need for those types.
#### HTML5 Video
@ -95,36 +76,59 @@ For YouTube and Vimeo players, Plyr uses progressive enhancement to enhance the
#### YouTube embed
We recommend [progressive enhancement](https://www.smashingmagazine.com/2009/04/progressive-enhancement-what-it-is-and-how-to-use-it/) with the embedded players. You can elect to use an `<iframe>` as the source element (which Plyr will progressively enhance) or a bog standard `<div>` with two essential data attributes - `data-plyr-provider` and `data-plyr-embed-id`.
```html
<div class="plyr__video-embed" id="player">
<iframe src="https://www.youtube.com/embed/bTqVqk7FSmY?origin=https://plyr.io&amp;iv_load_policy=3&amp;modestbranding=1&amp;playsinline=1&amp;showinfo=0&amp;rel=0&amp;enablejsapi=1" allowfullscreen allowtransparency allow="autoplay"></iframe>
</div>
```
_Note_: The `plyr__video-embed` classname will make the player a responsive 16:9 (most common) iframe embed. When plyr itself kicks in, your custom `ratio` config option will be used.
Or the `<div>` non progressively enhanced method:
```html
<div id="player" data-plyr-provider="youtube" data-plyr-embed-id="bTqVqk7FSmY"></div>
```
_Note_: The `data-plyr-embed-id` can either be the video ID or URL for the media.
#### Vimeo embed
Much the same as YouTube above.
```html
<div class="plyr__video-embed" id="player">
<iframe src="https://player.vimeo.com/video/76979871?loop=false&amp;byline=false&amp;portrait=false&amp;title=false&amp;speed=true&amp;transparent=0&amp;gesture=media" allowfullscreen allowtransparency allow="autoplay"></iframe>
</div>
```
Or the `<div>` non progressively enhanced method:
```html
<div id="player" data-plyr-provider="vimeo" data-plyr-embed-id="76979871"></div>
```
### JavaScript
Include the `plyr.js` script before the closing `</body>` tag and then call `plyr.setup()`. More info on `setup()` can be found under
[initialising](#initialising).
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>
<script>const player = new Plyr('#player');</script>
```
See [initialising](#initialising) for more information on advanced setups.
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
```html
<script src="https://cdn.plyr.io/3.0.0-beta.8/plyr.js"></script>
<script src="https://cdn.plyr.io/3.0.1/plyr.js"></script>
```
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
### CSS
Include the `plyr.css` stylsheet into your `<head>`
@ -136,13 +140,23 @@ Include the `plyr.css` stylsheet into your `<head>`
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.0.0-beta.8/plyr.css">
<link rel="stylesheet" href="https://cdn.plyr.io/3.0.1/plyr.css">
```
### SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.0.0-beta.8/plyr.svg`.
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.0.1/plyr.svg`.
## Ads
Plyr has partnered up with [vi.ai](http://vi.ai/publisher-video-monetization/?aid=plyrio) to offer monetization options for your videos. Getting setup is easy:
* [Sign up for a vi.ai account](http://vi.ai/publisher-video-monetization/?aid=plyrio)
* Grab your publisher ID from the code snippet
* Enable ads in the [config options](#options) and enter your publisher ID
Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues.
## Advanced
@ -187,11 +201,12 @@ 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 [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
* A [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or Array of [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) -
the first element will be used
* A [jQuery](https://jquery.com) object - if multiple are passed, the first element will be used
* A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
* A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
* A [jQuery](https://jquery.com) object
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup.
Here's some examples
@ -215,6 +230,12 @@ const player = new Plyr(document.querySelectorAll('.js-player'));
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds
##### Setting up multiple players
```javascript
const players = Array.from(document.querySelectorAll('.js-player')).map(player => new Plyr(player));
```
The second argument for the constructor is the [#options](options) object:
```javascript
@ -235,42 +256,43 @@ Options can be passed as an object to the constructor as above or as JSON in `da
Note the single quotes encapsulating the JSON and double quotes on the object keys. Only string values need double quotes.
| Option | Type | Default | Description |
| -------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. |
| `debug` | Boolean | `false` | Display debugging information in the console |
| `controls` | Function or Array | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return a string of HTML for the controls. Three arguments will be passed to your function; id (the unique id for the player), seektime (the seektime step in seconds), and title (the media title). See [controls.md](controls.md) for more info on how the html needs to be structured. |
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If you're using the default controls are used then you can specify which settings to show in the menu |
| `i18n` | Object | See [defaults.js](/src/js/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
| `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. |
| `iconPrefix` | String | `plyr` | 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. |
| `blankUrl` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
| `autoplay` | Boolean | `false` | Autoplay the media on load. 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. |
| `autopause`&sup1; | Boolean | `true` | Only allow one player playing at once. |
| `seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind. |
| `volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player. |
| `muted` | Boolean | `false` | Whether to start playback muted. If the `muted` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
| `showPosterOnEnd` | Boolean | false | This will restore and _reload_ HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution. |
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
| `duration` | Number | `null` | Specify a custom duration for media. |
| `displayDuration` | Boolean | `true` | 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). |
| `invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter. |
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
| `captions` | Object | `{ active: false, language: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). |
| `fullscreen` | Object | `{ enabled: true, fallback: true }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. |
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. |
| `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. |
| `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. |
| Option | Type | Default | Description |
| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. |
| `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 |
| `i18n` | Object | See [defaults.js](/src/js/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
| `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. |
| `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. |
| `iconPrefix` | String | `plyr` | 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. |
| `blankUrl` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
| `autoplay` | Boolean | `false` | Autoplay the media on load. 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. |
| `autopause`&sup1; | Boolean | `true` | Only allow one player playing at once. |
| `seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind. |
| `volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player. |
| `muted` | Boolean | `false` | Whether to start playback muted. If the `muted` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
| `showPosterOnEnd` | Boolean | false | This will restore and _reload_ HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution. |
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
| `duration` | Number | `null` | Specify a custom duration for media. |
| `displayDuration` | Boolean | `true` | 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). |
| `invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter. |
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
| `captions` | Object | `{ active: false, language: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). |
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
| `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. |
| `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. |
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
| `ads` | Object | `{ enabled: false, publisherId: '' }` | `enabled`: Whether to enable vi.ai ads. `publisherId`: Your unique vi.ai publisher ID. |
1. Vimeo only
1. Vimeo only
## API
@ -296,72 +318,81 @@ element.addEventListener('ready', event => {
### Methods
Methods are not chainable. An example use of a method:
Example method use:
```javascript
player.play();
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. |
| `toggleFullscreen(event)` | Event | Toggle fullscreen. Fullscreen can only be initiated by a user event. Exit is possible without user input. |
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls based on the specified boolean. |
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
| `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)` | 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 based on the specified boolean. |
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
| `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) in _some_ browsers - WebKit and Mozilla [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing.
### Getters and Setters
An example setter:
Example setters:
```javascript
player.volume = 0.5;
player.volume = 0.5; // Sets volume at 50%
player.currentTime = 10; // Seeks to 10 seconds
```
An example getter:
Example getters:
```javascript
player.volume; // returns 0.5;
player.volume; // 0.5;
player.currentTime; // 10
player.fullscreen.active; // false;
```
| Property | Getter | Setter | Description |
| --------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isHTML5` | | - | Returns a boolean indicating if the current player is HTML5. |
| `isEmbed` | | - | Returns a boolean indicating if the current player is an embedded player. |
| `paused` | | - | Returns a boolean indicating if the current player is paused. |
| `playing` | | - | Returns a boolean indicating if the current player is playing. |
| `ended` | | - | Returns a boolean indicating if the current player has finished playback. |
| `currentTime` | | | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
| `seeking` | | - | Returns a boolean indicating if the current player is seeking. |
| `duration` | | - | Returns the duration for the current media. |
| `volume` | | | Gets or sets the volume for the player. The setter accepts a float between 0 and 1. |
| `muted` | | | Gets or sets the muted state of the player. The setter accepts a boolean. |
| `hasAudio` | | - | Returns a boolean indicating if the current media has an audio track. |
| `speed` | | | Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5. |
| `quality`&sup1; | | | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
| `loop` | | | Gets or sets the current loop state of the player. The setter accepts a boolean. |
| `source` | | | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
| `poster`&sup2; | | | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | | | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
| `language` | | | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. |
| `pip` | | | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |
| Property | Getter | Setter | Description |
| -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isHTML5` | | - | Returns a boolean indicating if the current player is HTML5. |
| `isEmbed` | | - | Returns a boolean indicating if the current player is an embedded player. |
| `paused` | | - | Returns a boolean indicating if the current player is paused. |
| `playing` | | - | Returns a boolean indicating if the current player is playing. |
| `ended` | | - | Returns a boolean indicating if the current player has finished playback. |
| `buffered` | | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
| `currentTime` | | | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
| `seeking` | | - | Returns a boolean indicating if the current player is seeking. |
| `duration` | | - | Returns the duration for the current media. |
| `volume` | | | Gets or sets the volume for the player. The setter accepts a float between 0 and 1. |
| `muted` | | | Gets or sets the muted state of the player. The setter accepts a boolean. |
| `hasAudio` | | - | Returns a boolean indicating if the current media has an audio track. |
| `speed` | | | Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5. |
| `quality`&sup1; | | | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
| `loop` | | | Gets or sets the current loop state of the player. The setter accepts a boolean. |
| `source` | | | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
| `poster`&sup2; | | | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | | | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
| `language` | | | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. |
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
| `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |
1. YouTube only. HTML5 will follow.
2. HTML5 only
1. YouTube only. HTML5 will follow.
2. HTML5 only
#### The `.source` setter
@ -461,7 +492,7 @@ _Note:_ `src` property for YouTube and Vimeo can either be the video ID or the w
| `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. |
1. HTML5 only
1. HTML5 only
## Events
@ -531,8 +562,8 @@ YouTube and Vimeo are currently supported and function much like a HTML5 video.
to access the API's directly. You can do so via the `embed` property of your player object - e.g. `player.embed`. You can then use the relevant methods from the
third party APIs. More info on the respective API's here:
* [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference)
* [Vimeo player.js Reference](https://github.com/vimeo/player.js)
* [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference)
* [Vimeo player.js Reference](https://github.com/vimeo/player.js)
_Note_: Not all API methods may work 100%. Your mileage may vary. It's better to use the Plyr API where possible.
@ -560,9 +591,9 @@ document then the shortcuts will work when any element has focus, apart from an
Because Plyr is an extension of the standard HTML5 video and audio elements, third party streaming plugins can be used with Plyr. Massive thanks to Matias
Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's a few examples:
* Using [hls.js](https://github.com/dailymotion/hls.js) - [Demo](http://codepen.io/sampotts/pen/JKEMqB)
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
* Using [hls.js](https://github.com/dailymotion/hls.js) - [Demo](http://codepen.io/sampotts/pen/JKEMqB)
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
## Fullscreen
@ -570,22 +601,25 @@ Fullscreen in Plyr is supported by all browsers that [currently support it](http
## Browser support
Plyr supports the last 2 versions of most _modern_ browsers. IE11 is also supported.
Plyr supports the last 2 versions of most _modern_ browsers.
| Browser | Supported |
| ------------- | -------------- |
| Safari | |
| Mobile Safari | &sup1; |
| Firefox | |
| Chrome | |
| Opera | |
| Edge | |
| IE10+ | &sup2; |
| IE9 | API only&sup3; |
| Browser | Supported |
| ------------- | --------- |
| Safari | |
| Mobile Safari | &sup1; |
| Firefox | |
| Chrome | |
| Opera | |
| Edge | |
| IE11 | |
| IE10 | &sup2; |
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled.
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported (v1.0.28+)
3. IE10 has no native fullscreen support, fallback can be used (see [options](#options))
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))
### Polyfills
Plyr uses ES6 which isn't supported in all browsers quite yet. This means some features will need to be polyfilled to be available otherwise you'll run into issues. We've elected to not burden the ~90% of users that do support these features with extra JS and instead leave polyfilling to you to work out based on your needs. The easiest method I've found is to use [polyfill.io](https://polyfill.io) which provides polyfills based on user agent. This is the method the demo uses.
### Checking for support
@ -597,9 +631,9 @@ const supported = Plyr.supported('video', 'html5', true);
The arguments are:
* Media type (`audio` or `video`)
* Provider (`html5`, `youtube` or `vimeo`)
* Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
* Media type (`audio` or `video`)
* Provider (`html5`, `youtube` or `vimeo`)
* Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
### Disable support programatically
@ -635,28 +669,28 @@ Plyr costs money to run, not only my time - I donate that for free but domains,
## Mentions
* [ProductHunt](https://www.producthunt.com/tech/plyr)
* [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/)
* [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/)
* [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)
* [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images)
* [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html)
* [ProductHunt](https://www.producthunt.com/tech/plyr)
* [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/)
* [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/)
* [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)
* [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images)
* [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html)
## Used by
* [Selz.com](https://selz.com)
* [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html)
* [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html)
* [TomTom.com](http://prioritydriving.tomtom.com/)
* [DIGBMX](http://digbmx.com/)
* [Grime Archive](https://grimearchive.com/)
* [koel - A personal music streaming server that works.](http://koel.phanan.net/)
* [Oscar Radio](http://oscar-radio.xyz/)
* [Sparkk TV](https://www.sparkktv.com/)
* [Selz.com](https://selz.com)
* [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html)
* [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html)
* [TomTom.com](http://prioritydriving.tomtom.com/)
* [DIGBMX](http://digbmx.com/)
* [Grime Archive](https://grimearchive.com/)
* [koel - A personal music streaming server that works.](http://koel.phanan.net/)
* [Oscar Radio](http://oscar-radio.xyz/)
* [Sparkk TV](https://www.sparkktv.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 :-)
@ -664,12 +698,12 @@ Let me know on [Twitter](https://twitter.com/sam_potts) I can add you to the abo
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)
* [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw)
* [PayPal's Accessible HTML5 Video Player](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
[![Fastly](https://www.fastly.com/sites/all/themes/custom/fastly2016/logo.png)](https://www.fastly.com/)
[![Fastly](https://cdn.plyr.io/static/demo/fastly-logo.png)](https://www.fastly.com/)
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.

View File

@ -1,5 +1,6 @@
// ==========================================================================
// Plyr Captions
// TODO: Create as class
// ==========================================================================
import support from './support';
@ -39,13 +40,12 @@ const captions = {
// Only Vimeo and HTML5 video supported at this point
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
// Clear menu and hide
if (this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
controls.setCaptionsMenu.call(this);
}
return;
}
// Inject the container
if (!utils.is.element(this.elements.captions)) {
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
@ -56,11 +56,42 @@ const captions = {
// Set the class hook
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this)));
// Get tracks
const tracks = captions.getTracks.call(this);
// If no caption file exists, hide container for caption text
if (utils.is.empty(captions.getTracks.call(this))) {
if (utils.is.empty(tracks)) {
return;
}
// Get browser info
const browser = utils.getBrowser();
// Fix IE captions if CORS is used
// Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) {
const elements = this.media.querySelectorAll('track');
Array.from(elements).forEach(track => {
const src = track.getAttribute('src');
const href = utils.parseUrl(src);
if (href.hostname !== window.location.href.hostname && [
'http:',
'https:',
].includes(href.protocol)) {
utils
.fetch(src, 'blob')
.then(blob => {
track.setAttribute('src', window.URL.createObjectURL(blob));
})
.catch(() => {
utils.removeElement(track);
});
}
});
}
// Set language
captions.setLanguage.call(this);
@ -68,7 +99,7 @@ const captions = {
captions.show.call(this);
// Set available languages in list
if (this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
controls.setCaptionsMenu.call(this);
}
},
@ -78,7 +109,7 @@ const captions = {
// Setup HTML5 track rendering
if (this.isHTML5 && this.isVideo) {
captions.getTracks.call(this).forEach(track => {
// Remove previous bindings
// Show track
utils.on(track, 'cuechange', event => captions.setCue.call(this, event));
// Turn off native caption rendering to avoid double captions
@ -124,7 +155,8 @@ const captions = {
setCue(input) {
// Get the track from the event if needed
const track = utils.is.event(input) ? input.target : input;
const active = track.activeCues[0];
const { activeCues } = track;
const active = activeCues.length && activeCues[0];
const currentTrack = captions.getCurrentTrack.call(this);
// Only display current track

78
src/js/controls.js vendored
View File

@ -50,7 +50,7 @@ const controls = {
icon,
utils.extend(attributes, {
role: 'presentation',
})
}),
);
// Create the <use> to reference sprite
@ -115,8 +115,8 @@ const controls = {
{
class: this.config.classNames.menu.badge,
},
text
)
text,
),
);
return badge;
@ -215,7 +215,16 @@ const controls = {
utils.setAttributes(button, attributes);
this.elements.buttons[type] = button;
// We have multiple play buttons
if (type === 'play') {
if (!utils.is.array(this.elements.buttons[type])) {
this.elements.buttons[type] = [];
}
this.elements.buttons[type].push(button);
} else {
this.elements.buttons[type] = button;
}
return button;
},
@ -229,7 +238,7 @@ const controls = {
for: attributes.id,
class: this.config.classNames.hidden,
},
this.config.i18n[type]
this.config.i18n[type],
);
// Seek input
@ -245,8 +254,8 @@ const controls = {
value: 0,
autocomplete: 'off',
},
attributes
)
attributes,
),
);
this.elements.inputs[type] = input;
@ -271,8 +280,8 @@ const controls = {
max: 100,
value: 0,
},
attributes
)
attributes,
),
);
// Create the label inside
@ -313,8 +322,8 @@ const controls = {
{
class: this.config.classNames.hidden,
},
this.config.i18n[type]
)
this.config.i18n[type],
),
);
container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.display[type]), '00:00'));
@ -340,7 +349,7 @@ const controls = {
value,
checked,
class: 'plyr__sr-only',
})
}),
);
const faux = utils.createElement('span', { 'aria-hidden': true });
@ -418,6 +427,11 @@ const controls = {
// Set the YouTube quality menu
// TODO: Support for HTML5
setQualityMenu(options) {
// Menu required
if (!utils.is.element(this.elements.settings.panes.quality)) {
return;
}
const type = 'quality';
const list = this.elements.settings.panes.quality.querySelector('ul');
@ -473,7 +487,7 @@ const controls = {
};
this.options.quality.forEach(quality =>
controls.createMenuItem.call(this, quality, list, type, controls.getLabel.call(this, 'quality', quality), getBadge(quality))
controls.createMenuItem.call(this, quality, list, type, controls.getLabel.call(this, 'quality', quality), getBadge(quality)),
);
controls.updateSetting.call(this, type, list);
@ -574,6 +588,11 @@ const controls = {
// Set the looping options
/* setLoopMenu() {
// Menu required
if (!utils.is.element(this.elements.settings.panes.loop)) {
return;
}
const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panes.loop.querySelector('ul');
@ -672,7 +691,7 @@ const controls = {
'language',
track.label || track.language,
controls.createBadge.call(this, track.language.toUpperCase()),
track.language.toLowerCase() === this.captions.language.toLowerCase()
track.language.toLowerCase() === this.captions.language.toLowerCase(),
);
});
@ -681,6 +700,11 @@ const controls = {
// Set a list of available captions languages
setSpeedMenu() {
// Menu required
if (!utils.is.element(this.elements.settings.panes.speed)) {
return;
}
const type = 'speed';
// Set the default speeds
@ -728,6 +752,12 @@ const controls = {
toggleMenu(event) {
const { form } = this.elements.settings;
const button = this.elements.buttons.settings;
// Menu and button are required
if (!utils.is.element(form) || !utils.is.element(button)) {
return;
}
const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.getAttribute('aria-hidden') === 'true';
if (utils.is.event(event)) {
@ -893,7 +923,6 @@ const controls = {
// Play/Pause button
if (this.config.controls.includes('play')) {
container.appendChild(controls.createButton.call(this, 'play'));
// container.appendChild(controls.createButton.call(this, 'pause'));
}
// Fast forward button
@ -925,7 +954,7 @@ const controls = {
role: 'tooltip',
class: this.config.classNames.tooltip,
},
'00:00'
'00:00',
);
progress.appendChild(tooltip);
@ -970,7 +999,7 @@ const controls = {
'volume',
utils.extend(attributes, {
id: `plyr-volume-${data.id}`,
})
}),
);
volume.appendChild(range.label);
volume.appendChild(range.input);
@ -997,7 +1026,7 @@ const controls = {
'aria-haspopup': true,
'aria-controls': `plyr-settings-${data.id}`,
'aria-expanded': false,
})
}),
);
const form = utils.createElement('form', {
@ -1040,7 +1069,7 @@ const controls = {
'aria-controls': `plyr-settings-${data.id}-${type}`,
'aria-expanded': false,
}),
this.config.i18n[type]
this.config.i18n[type],
);
const value = utils.createElement('span', {
@ -1080,7 +1109,7 @@ const controls = {
'aria-controls': `plyr-settings-${data.id}-home`,
'aria-expanded': false,
},
this.config.i18n[type]
this.config.i18n[type],
);
pane.appendChild(back);
@ -1147,9 +1176,10 @@ const controls = {
// Null by default
let container = null;
this.elements.controls = null;
// HTML passed as the option
if (utils.is.string(this.config.controls)) {
// HTML or Element passed as the option
if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) {
container = this.config.controls;
} else if (utils.is.function(this.config.controls)) {
// A custom function to build controls
@ -1193,7 +1223,7 @@ const controls = {
}
// Find the elements if need be
if (utils.is.element(this.elements.controls)) {
if (!utils.is.element(this.elements.controls)) {
utils.findElements.call(this);
}
@ -1212,7 +1242,7 @@ const controls = {
this.config.selectors.labels,
' .',
this.config.classNames.hidden,
].join('')
].join(''),
);
Array.from(labels).forEach(label => {

View File

@ -56,7 +56,7 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/2.0.10/plyr.svg',
iconUrl: 'https://cdn.plyr.io/3.0.1/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@ -120,6 +120,7 @@ const defaults = {
fullscreen: {
enabled: true, // Allow fullscreen?
fallback: true, // Fallback for vintage browsers
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
},
// Local storage
@ -179,7 +180,7 @@ const defaults = {
reset: 'Reset',
none: 'None',
disabled: 'Disabled',
advertisment: 'Ad',
advertisement: 'Ad',
},
// URLs
@ -258,7 +259,7 @@ const defaults = {
// Ads
'adsloaded',
'adscontentpause',
'adsconentresume',
'adscontentresume',
'adstarted',
'adsmidpoint',
'adscomplete',
@ -358,15 +359,24 @@ const defaults = {
tabFocus: 'plyr__tab-focus',
},
// Embed attributes
attributes: {
embed: {
provider: 'data-plyr-provider',
id: 'data-plyr-embed-id',
},
},
// API keys
keys: {
google: null,
},
// Advertisements plugin
// Tag is not required as publisher is determined by vi.ai using the domain
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
enabled: false,
publisherId: '',
},
};

View File

@ -1,127 +1,204 @@
// ==========================================================================
// Plyr fullscreen API
// Fullscreen wrapper
// ==========================================================================
import utils from './utils';
// Determine the prefix
const prefix = (() => {
let value = false;
const browser = utils.getBrowser();
if (utils.is.function(document.cancelFullScreen)) {
value = '';
function onChange() {
if (!this.enabled) {
return;
}
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (utils.is.element(button)) {
utils.toggleState(button, this.active);
}
// Trigger an event
utils.dispatchEvent(this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Trap focus in container
if (!browser.isIos) {
utils.trapFocus.call(this.player, this.target, this.active);
}
}
function toggleFallback(toggle = false) {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
}
// Toggle scroll
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
utils.toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Toggle button and fire events
onChange.call(this);
}
class Fullscreen {
constructor(player) {
// Keep reference to parent
this.player = player;
// Get prefix
this.prefix = Fullscreen.prefix;
// Scroll position
this.scrollPosition = { x: 0, y: 0 };
// Register event listeners
// Handle event (incase user presses escape etc)
utils.on(document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {
// TODO: Filter for target??
onChange.call(this);
});
// Fullscreen toggle on double click
utils.on(this.player.elements.container, 'dblclick', () => {
this.toggle();
});
// Prevent double click on controls bubbling up
utils.on(this.player.elements.controls, 'dblclick', event => event.stopPropagation());
// Update the UI
this.update();
}
// Determine if native supported
static get native() {
return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);
}
// Get the prefix for handlers
static get prefix() {
// No prefix
if (utils.is.function(document.cancelFullScreen)) {
return false;
}
// Check for fullscreen support by vendor prefix
[
let value = '';
const prefixes = [
'webkit',
'o',
'moz',
'ms',
'khtml',
].some(pre => {
];
prefixes.some(pre => {
if (utils.is.function(document[`${pre}CancelFullScreen`])) {
value = pre;
return true;
} else if (utils.is.function(document.msExitFullscreen) && document.msFullscreenEnabled) {
// Special case for MS (when isn't it?)
} else if (utils.is.function(document.msExitFullscreen)) {
value = 'ms';
return true;
}
return false;
});
return value;
}
return value;
})();
// Determine if fullscreen is enabled
get enabled() {
const fallback = this.player.config.fullscreen.fallback && !utils.inFrame();
// Fullscreen API
const fullscreen = {
// Get the prefix
prefix,
return (Fullscreen.native || fallback) && this.player.config.fullscreen.enabled && this.player.supported.ui && this.player.isVideo;
}
// Check if we can use it
enabled: document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled,
// Yet again Microsoft awesomeness,
// Sometimes the prefix is 'ms', sometimes 'MS' to keep you on your toes
eventType: prefix === 'ms' ? 'MSFullscreenChange' : `${prefix}fullscreenchange`,
// Is an element fullscreen
isFullScreen(element) {
if (!fullscreen.enabled) {
// Get active state
get active() {
if (!this.enabled) {
return false;
}
const target = utils.is.nullOrUndefined(element) ? document.body : element;
switch (prefix) {
case '':
return document.fullscreenElement === target;
case 'moz':
return document.mozFullScreenElement === target;
default:
return document[`${prefix}FullscreenElement`] === target;
// Fallback using classname
if (!Fullscreen.native) {
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
},
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}FullscreenElement`];
return element === this.target;
}
// Get target element
get target() {
return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container;
}
// Update UI
update() {
if (this.enabled) {
this.player.debug.log(`${Fullscreen.native ? 'Native' : 'Fallback'} fullscreen enabled`);
} else {
this.player.debug.log('Fullscreen not supported and fallback disabled');
}
// Add styling hook to show button
utils.toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
}
// Make an element fullscreen
requestFullScreen(element) {
if (!fullscreen.enabled) {
return false;
}
const target = utils.is.nullOrUndefined(element) ? document.body : element;
return !prefix.length ? target.requestFullScreen() : target[prefix + (prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')]();
},
// Bail from fullscreen
cancelFullScreen() {
if (!fullscreen.enabled) {
return false;
}
return !prefix.length ? document.cancelFullScreen() : document[prefix + (prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')]();
},
// Get the current element
element() {
if (!fullscreen.enabled) {
return null;
}
return !prefix.length ? document.fullscreenElement : document[`${prefix}FullscreenElement`];
},
// Setup fullscreen
setup() {
if (!this.supported.ui || this.isAudio || !this.config.fullscreen.enabled) {
enter() {
if (!this.enabled) {
return;
}
// Check for native support
const nativeSupport = fullscreen.enabled;
// iOS native fullscreen doesn't need the request step
if (browser.isIos && this.player.config.fullscreen.iosNative) {
if (this.player.playing) {
this.target.webkitEnterFullscreen();
}
} else if (!Fullscreen.native) {
toggleFallback.call(this, true);
} else if (!this.prefix) {
this.target.requestFullScreen();
} else if (!utils.is.empty(this.prefix)) {
this.target[`${this.prefix}${this.prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen'}`]();
}
}
if (nativeSupport || (this.config.fullscreen.fallback && !utils.inFrame())) {
this.debug.log(`${nativeSupport ? 'Native' : 'Fallback'} fullscreen enabled`);
// Bail from fullscreen
exit() {
if (!this.enabled) {
return;
}
// Add styling hook to show button
utils.toggleClass(this.elements.container, this.config.classNames.fullscreen.enabled, true);
// iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen();
this.player.play();
} else if (!Fullscreen.native) {
toggleFallback.call(this, false);
} else if (!this.prefix) {
document.cancelFullScreen();
} else if (!utils.is.empty(this.prefix)) {
document[`${this.prefix}${this.prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen'}`]();
}
}
// Toggle state
toggle() {
if (!this.active) {
this.enter();
} else {
this.debug.log('Fullscreen not supported and fallback disabled');
this.exit();
}
}
}
// Toggle state
if (this.elements.buttons && this.elements.buttons.fullscreen) {
utils.toggleState(this.elements.buttons.fullscreen, false);
}
// Trap focus in container
utils.trapFocus.call(this);
},
};
export default fullscreen;
export default Fullscreen;

View File

@ -5,276 +5,280 @@
import support from './support';
import utils from './utils';
import controls from './controls';
import fullscreen from './fullscreen';
import ui from './ui';
// Sniff out the browser
const browser = utils.getBrowser();
const listeners = {
// Global listeners
global() {
let last = null;
class Listeners {
constructor(player) {
this.player = player;
this.lastKey = null;
// Get the key code for an event
const getKeyCode = event => (event.keyCode ? event.keyCode : event.which);
this.handleKey = this.handleKey.bind(this);
this.toggleMenu = this.toggleMenu.bind(this);
}
// Handle key press
const handleKey = event => {
const code = getKeyCode(event);
const pressed = event.type === 'keydown';
const repeat = pressed && code === last;
// Handle key presses
handleKey(event) {
const code = event.keyCode ? event.keyCode : event.which;
const pressed = event.type === 'keydown';
const repeat = pressed && code === this.lastKey;
// Bail if a modifier key is set
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
// Bail if a modifier key is set
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
// If the event is bubbled from the media element
// Firefox doesn't get the keycode for whatever reason
if (!utils.is.number(code)) {
return;
}
// If the event is bubbled from the media element
// Firefox doesn't get the keycode for whatever reason
if (!utils.is.number(code)) {
return;
}
// Seek by the number keys
const seekByKey = () => {
// Divide the max duration into 10th's and times by the number value
this.currentTime = this.duration / 10 * (code - 48);
};
// Handle the key on keydown
// Reset on keyup
if (pressed) {
// Which keycodes should we prevent default
const preventDefault = [
48,
49,
50,
51,
52,
53,
54,
56,
57,
32,
75,
38,
40,
77,
39,
37,
70,
67,
73,
76,
79,
];
// Check focused element
// and if the focused element is not editable (e.g. text input)
// and any that accept key input http://webaim.org/techniques/keyboard/
const focused = utils.getFocusElement();
if (utils.is.element(focused) && utils.matches(focused, this.config.selectors.editable)) {
return;
}
// If the code is found prevent default (e.g. prevent scrolling for arrows)
if (preventDefault.includes(code)) {
event.preventDefault();
event.stopPropagation();
}
switch (code) {
case 48:
case 49:
case 50:
case 51:
case 52:
case 53:
case 54:
case 55:
case 56:
case 57:
// 0-9
if (!repeat) {
seekByKey();
}
break;
case 32:
case 75:
// Space and K key
if (!repeat) {
this.togglePlay();
}
break;
case 38:
// Arrow up
this.increaseVolume(0.1);
break;
case 40:
// Arrow down
this.decreaseVolume(0.1);
break;
case 77:
// M key
if (!repeat) {
this.muted = !this.muted;
}
break;
case 39:
// Arrow forward
this.forward();
break;
case 37:
// Arrow back
this.rewind();
break;
case 70:
// F key
this.toggleFullscreen();
break;
case 67:
// C key
if (!repeat) {
this.toggleCaptions();
}
break;
case 76:
// L key
this.loop = !this.loop;
break;
/* case 73:
this.setLoop('start');
break;
case 76:
this.setLoop();
break;
case 79:
this.setLoop('end');
break; */
default:
break;
}
// Escape is handle natively when in full screen
// So we only need to worry about non native
if (!fullscreen.enabled && this.fullscreen.active && code === 27) {
this.toggleFullscreen();
}
// Store last code for next cycle
last = code;
} else {
last = null;
}
// Seek by the number keys
const seekByKey = () => {
// Divide the max duration into 10th's and times by the number value
this.player.currentTime = this.player.duration / 10 * (code - 48);
};
// Handle the key on keydown
// Reset on keyup
if (pressed) {
// Which keycodes should we prevent default
const preventDefault = [
48,
49,
50,
51,
52,
53,
54,
56,
57,
32,
75,
38,
40,
77,
39,
37,
70,
67,
73,
76,
79,
];
// Check focused element
// and if the focused element is not editable (e.g. text input)
// and any that accept key input http://webaim.org/techniques/keyboard/
const focused = utils.getFocusElement();
if (utils.is.element(focused) && utils.matches(focused, this.player.config.selectors.editable)) {
return;
}
// If the code is found prevent default (e.g. prevent scrolling for arrows)
if (preventDefault.includes(code)) {
event.preventDefault();
event.stopPropagation();
}
switch (code) {
case 48:
case 49:
case 50:
case 51:
case 52:
case 53:
case 54:
case 55:
case 56:
case 57:
// 0-9
if (!repeat) {
seekByKey();
}
break;
case 32:
case 75:
// Space and K key
if (!repeat) {
this.player.togglePlay();
}
break;
case 38:
// Arrow up
this.player.increaseVolume(0.1);
break;
case 40:
// Arrow down
this.player.decreaseVolume(0.1);
break;
case 77:
// M key
if (!repeat) {
this.player.muted = !this.player.muted;
}
break;
case 39:
// Arrow forward
this.player.forward();
break;
case 37:
// Arrow back
this.player.rewind();
break;
case 70:
// F key
this.player.fullscreen.toggle();
break;
case 67:
// C key
if (!repeat) {
this.player.toggleCaptions();
}
break;
case 76:
// L key
this.player.loop = !this.player.loop;
break;
/* case 73:
this.setLoop('start');
break;
case 76:
this.setLoop();
break;
case 79:
this.setLoop('end');
break; */
default:
break;
}
// Escape is handle natively when in full screen
// So we only need to worry about non native
if (!this.player.fullscreen.enabled && this.player.fullscreen.active && code === 27) {
this.player.fullscreen.toggle();
}
// Store last code for next cycle
this.lastKey = code;
} else {
this.lastKey = null;
}
}
// Toggle menu
toggleMenu(event) {
controls.toggleMenu.call(this.player, event);
}
// Global window & document listeners
global(toggle = true) {
// Keyboard shortcuts
if (this.config.keyboard.global) {
utils.on(window, 'keydown keyup', handleKey, false);
} else if (this.config.keyboard.focused) {
utils.on(this.elements.container, 'keydown keyup', handleKey, false);
if (this.player.config.keyboard.global) {
utils.toggleListener(window, 'keydown keyup', this.handleKey, toggle, false);
}
// Click anywhere closes menu
utils.toggleListener(document.body, 'click', this.toggleMenu, toggle);
}
// Container listeners
container() {
// Keyboard shortcuts
if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) {
utils.on(this.player.elements.container, 'keydown keyup', this.handleKey, false);
}
// Detect tab focus
// Remove class on blur/focusout
utils.on(this.elements.container, 'focusout', event => {
utils.toggleClass(event.target, this.config.classNames.tabFocus, false);
utils.on(this.player.elements.container, 'focusout', event => {
utils.toggleClass(event.target, this.player.config.classNames.tabFocus, false);
});
// Add classname to tabbed elements
utils.on(this.elements.container, 'keydown', event => {
utils.on(this.player.elements.container, 'keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
window.setTimeout(() => {
utils.toggleClass(utils.getFocusElement(), this.config.classNames.tabFocus, true);
setTimeout(() => {
utils.toggleClass(utils.getFocusElement(), this.player.config.classNames.tabFocus, true);
}, 0);
});
// Toggle controls visibility based on mouse movement
if (this.config.hideControls) {
if (this.player.config.hideControls) {
// Toggle controls on mouse events and entering fullscreen
utils.on(this.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => {
this.toggleControls(event);
utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => {
this.player.toggleControls(event);
});
}
// Handle user exiting fullscreen by escaping etc
if (fullscreen.enabled) {
utils.on(document, fullscreen.eventType, event => {
this.toggleFullscreen(event);
});
// Fullscreen toggle on double click
utils.on(this.elements.container, 'dblclick', event => {
this.toggleFullscreen(event);
});
}
},
}
// Listen for media events
media() {
// Time change on media
utils.on(this.media, 'timeupdate seeking', event => ui.timeUpdate.call(this, event));
utils.on(this.player.media, 'timeupdate seeking', event => ui.timeUpdate.call(this.player, event));
// Display duration
utils.on(this.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this, event));
utils.on(this.player.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this.player, event));
// Check for audio tracks on load
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
utils.on(this.media, 'loadeddata', () => {
utils.toggleHidden(this.elements.volume, !this.hasAudio);
utils.toggleHidden(this.elements.buttons.mute, !this.hasAudio);
utils.on(this.player.media, 'loadeddata', () => {
utils.toggleHidden(this.player.elements.volume, !this.player.hasAudio);
utils.toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
});
// Handle the media finishing
utils.on(this.media, 'ended', () => {
utils.on(this.player.media, 'ended', () => {
// Show poster on end
if (this.isHTML5 && this.isVideo && this.config.showPosterOnEnd) {
if (this.player.isHTML5 && this.player.isVideo && this.player.config.showPosterOnEnd) {
// Restart
this.restart();
this.player.restart();
// Re-load media
this.media.load();
this.player.media.load();
}
});
// Check for buffer progress
utils.on(this.media, 'progress playing', event => ui.updateProgress.call(this, event));
utils.on(this.player.media, 'progress playing', event => ui.updateProgress.call(this.player, event));
// Handle native mute
utils.on(this.media, 'volumechange', event => ui.updateVolume.call(this, event));
utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event));
// Handle native play/pause
utils.on(this.media, 'playing play pause ended', event => ui.checkPlaying.call(this, event));
utils.on(this.player.media, 'playing play pause ended', event => ui.checkPlaying.call(this.player, event));
// Loading
utils.on(this.media, 'stalled waiting canplay seeked playing', event => ui.checkLoading.call(this, event));
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
// Check if media failed to load
// utils.on(this.media, 'play', event => ui.checkFailed.call(this, event));
// utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.player, event));
// Click video
if (this.supported.ui && this.config.clickToPlay && !this.isAudio) {
if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) {
// Re-fetch the wrapper
const wrapper = utils.getElement.call(this, `.${this.config.classNames.video}`);
const wrapper = utils.getElement.call(this.player, `.${this.player.config.classNames.video}`);
// Bail if there's no wrapper (this should never happen)
if (!utils.is.element(wrapper)) {
@ -284,78 +288,78 @@ const listeners = {
// On click play, pause ore restart
utils.on(wrapper, 'click', () => {
// Touch devices will just show controls (if we're hiding controls)
if (this.config.hideControls && support.touch && !this.paused) {
if (this.player.config.hideControls && support.touch && !this.player.paused) {
return;
}
if (this.paused) {
this.play();
} else if (this.ended) {
this.restart();
this.play();
if (this.player.paused) {
this.player.play();
} else if (this.player.ended) {
this.player.restart();
this.player.play();
} else {
this.pause();
this.player.pause();
}
});
}
// Disable right click
if (this.supported.ui && this.config.disableContextMenu) {
if (this.player.supported.ui && this.player.config.disableContextMenu) {
utils.on(
this.media,
this.player.media,
'contextmenu',
event => {
event.preventDefault();
},
false
false,
);
}
// Volume change
utils.on(this.media, 'volumechange', () => {
utils.on(this.player.media, 'volumechange', () => {
// Save to storage
this.storage.set({ volume: this.volume, muted: this.muted });
this.player.storage.set({ volume: this.player.volume, muted: this.player.muted });
});
// Speed change
utils.on(this.media, 'ratechange', () => {
utils.on(this.player.media, 'ratechange', () => {
// Update UI
controls.updateSetting.call(this, 'speed');
controls.updateSetting.call(this.player, 'speed');
// Save to storage
this.storage.set({ speed: this.speed });
this.player.storage.set({ speed: this.player.speed });
});
// Quality change
utils.on(this.media, 'qualitychange', () => {
utils.on(this.player.media, 'qualitychange', () => {
// Update UI
controls.updateSetting.call(this, 'quality');
controls.updateSetting.call(this.player, 'quality');
// Save to storage
this.storage.set({ quality: this.quality });
this.player.storage.set({ quality: this.player.quality });
});
// Caption language change
utils.on(this.media, 'languagechange', () => {
utils.on(this.player.media, 'languagechange', () => {
// Update UI
controls.updateSetting.call(this, 'captions');
controls.updateSetting.call(this.player, 'captions');
// Save to storage
this.storage.set({ language: this.language });
this.player.storage.set({ language: this.player.language });
});
// Captions toggle
utils.on(this.media, 'captionsenabled captionsdisabled', () => {
utils.on(this.player.media, 'captionsenabled captionsdisabled', () => {
// Update UI
controls.updateSetting.call(this, 'captions');
controls.updateSetting.call(this.player, 'captions');
// Save to storage
this.storage.set({ captions: this.captions.active });
this.player.storage.set({ captions: this.player.captions.active });
});
// Proxy events to container
// Bubble up key events for Edge
utils.on(this.media, this.config.events.concat([
utils.on(this.player.media, this.player.config.events.concat([
'keyup',
'keydown',
]).join(' '), event => {
@ -363,12 +367,12 @@ const listeners = {
// Get error details from media
if (event.type === 'error') {
detail = this.media.error;
detail = this.player.media.error;
}
utils.dispatchEvent.call(this, this.elements.container, event.type, true, detail);
utils.dispatchEvent.call(this.player, this.player.elements.container, event.type, true, detail);
});
},
}
// Listen for control events
controls() {
@ -377,176 +381,171 @@ const listeners = {
// Trigger custom and default handlers
const proxy = (event, handlerKey, defaultHandler) => {
const customHandler = this.config.listeners[handlerKey];
const customHandler = this.player.config.listeners[handlerKey];
// Execute custom handler
if (utils.is.function(customHandler)) {
customHandler.call(this, event);
customHandler.call(this.player, event);
}
// Only call default handler if not prevented in custom handler
if (!event.defaultPrevented && utils.is.function(defaultHandler)) {
defaultHandler.call(this, event);
defaultHandler.call(this.player, event);
}
};
// Play/pause toggle
utils.on(this.elements.buttons.play, 'click', event =>
utils.on(this.player.elements.buttons.play, 'click', event =>
proxy(event, 'play', () => {
this.togglePlay();
})
this.player.togglePlay();
}),
);
// Pause
utils.on(this.elements.buttons.restart, 'click', event =>
utils.on(this.player.elements.buttons.restart, 'click', event =>
proxy(event, 'restart', () => {
this.restart();
})
this.player.restart();
}),
);
// Rewind
utils.on(this.elements.buttons.rewind, 'click', event =>
utils.on(this.player.elements.buttons.rewind, 'click', event =>
proxy(event, 'rewind', () => {
this.rewind();
})
this.player.rewind();
}),
);
// Rewind
utils.on(this.elements.buttons.forward, 'click', event =>
utils.on(this.player.elements.buttons.forward, 'click', event =>
proxy(event, 'forward', () => {
this.forward();
})
this.player.forward();
}),
);
// Mute toggle
utils.on(this.elements.buttons.mute, 'click', event =>
utils.on(this.player.elements.buttons.mute, 'click', event =>
proxy(event, 'mute', () => {
this.muted = !this.muted;
})
this.player.muted = !this.player.muted;
}),
);
// Captions toggle
utils.on(this.elements.buttons.captions, 'click', event =>
utils.on(this.player.elements.buttons.captions, 'click', event =>
proxy(event, 'captions', () => {
this.toggleCaptions();
})
this.player.toggleCaptions();
}),
);
// Fullscreen toggle
utils.on(this.elements.buttons.fullscreen, 'click', event =>
utils.on(this.player.elements.buttons.fullscreen, 'click', event =>
proxy(event, 'fullscreen', () => {
this.toggleFullscreen();
})
this.player.fullscreen.toggle();
}),
);
// Picture-in-Picture
utils.on(this.elements.buttons.pip, 'click', event =>
utils.on(this.player.elements.buttons.pip, 'click', event =>
proxy(event, 'pip', () => {
this.pip = 'toggle';
})
this.player.pip = 'toggle';
}),
);
// Airplay
utils.on(this.elements.buttons.airplay, 'click', event =>
utils.on(this.player.elements.buttons.airplay, 'click', event =>
proxy(event, 'airplay', () => {
this.airplay();
})
this.player.airplay();
}),
);
// Settings menu
utils.on(this.elements.buttons.settings, 'click', event => {
controls.toggleMenu.call(this, event);
});
// Click anywhere closes menu
utils.on(document.documentElement, 'click', event => {
controls.toggleMenu.call(this, event);
utils.on(this.player.elements.buttons.settings, 'click', event => {
controls.toggleMenu.call(this.player, event);
});
// Settings menu
utils.on(this.elements.settings.form, 'click', event => {
utils.on(this.player.elements.settings.form, 'click', event => {
event.stopPropagation();
// Settings menu items - use event delegation as items are added/removed
if (utils.matches(event.target, this.config.selectors.inputs.language)) {
if (utils.matches(event.target, this.player.config.selectors.inputs.language)) {
proxy(event, 'language', () => {
this.language = event.target.value;
this.player.language = event.target.value;
});
} else if (utils.matches(event.target, this.config.selectors.inputs.quality)) {
} else if (utils.matches(event.target, this.player.config.selectors.inputs.quality)) {
proxy(event, 'quality', () => {
this.quality = event.target.value;
this.player.quality = event.target.value;
});
} else if (utils.matches(event.target, this.config.selectors.inputs.speed)) {
} else if (utils.matches(event.target, this.player.config.selectors.inputs.speed)) {
proxy(event, 'speed', () => {
this.speed = parseFloat(event.target.value);
this.player.speed = parseFloat(event.target.value);
});
} else {
controls.showTab.call(this, event);
controls.showTab.call(this.player, event);
}
});
// Seek
utils.on(this.elements.inputs.seek, inputEvent, event =>
utils.on(this.player.elements.inputs.seek, inputEvent, event =>
proxy(event, 'seek', () => {
this.currentTime = event.target.value / event.target.max * this.duration;
})
this.player.currentTime = event.target.value / event.target.max * this.player.duration;
}),
);
// Current time invert
// Only if one time element is used for both currentTime and duration
if (this.config.toggleInvert && !utils.is.element(this.elements.display.duration)) {
utils.on(this.elements.display.currentTime, 'click', () => {
if (this.player.config.toggleInvert && !utils.is.element(this.player.elements.display.duration)) {
utils.on(this.player.elements.display.currentTime, 'click', () => {
// Do nothing if we're at the start
if (this.currentTime === 0) {
if (this.player.currentTime === 0) {
return;
}
this.config.invertTime = !this.config.invertTime;
ui.timeUpdate.call(this);
this.player.config.invertTime = !this.player.config.invertTime;
ui.timeUpdate.call(this.player);
});
}
// Volume
utils.on(this.elements.inputs.volume, inputEvent, event =>
utils.on(this.player.elements.inputs.volume, inputEvent, event =>
proxy(event, 'volume', () => {
this.volume = event.target.value;
})
this.player.volume = event.target.value;
}),
);
// Polyfill for lower fill in <input type="range"> for webkit
if (browser.isWebkit) {
utils.on(utils.getElements.call(this, 'input[type="range"]'), 'input', event => {
controls.updateRangeFill.call(this, event.target);
utils.on(utils.getElements.call(this.player, 'input[type="range"]'), 'input', event => {
controls.updateRangeFill.call(this.player, event.target);
});
}
// Seek tooltip
utils.on(this.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this, event));
utils.on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
// Toggle controls visibility based on mouse movement
if (this.config.hideControls) {
if (this.player.config.hideControls) {
// Watch for cursor over controls so they don't hide when trying to interact
utils.on(this.elements.controls, 'mouseenter mouseleave', event => {
this.elements.controls.hover = event.type === 'mouseenter';
utils.on(this.player.elements.controls, 'mouseenter mouseleave', event => {
this.player.elements.controls.hover = event.type === 'mouseenter';
});
// Watch for cursor over controls so they don't hide when trying to interact
utils.on(this.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
this.elements.controls.pressed = [
utils.on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
this.player.elements.controls.pressed = [
'mousedown',
'touchstart',
].includes(event.type);
});
// Focus in/out on controls
utils.on(this.elements.controls, 'focusin focusout', event => {
this.toggleControls(event);
utils.on(this.player.elements.controls, 'focusin focusout', event => {
this.player.toggleControls(event);
});
}
// Mouse wheel for volume
utils.on(
this.elements.inputs.volume,
this.player.elements.inputs.volume,
'wheel',
event =>
proxy(event, 'volume', () => {
@ -559,10 +558,10 @@ const listeners = {
// Scroll down (or up on natural) to decrease
if (event.deltaY < 0 || event.deltaX > 0) {
if (inverted) {
this.decreaseVolume(step);
this.player.decreaseVolume(step);
direction = -1;
} else {
this.increaseVolume(step);
this.player.increaseVolume(step);
direction = 1;
}
}
@ -570,22 +569,27 @@ const listeners = {
// Scroll up (or down on natural) to increase
if (event.deltaY > 0 || event.deltaX < 0) {
if (inverted) {
this.increaseVolume(step);
this.player.increaseVolume(step);
direction = 1;
} else {
this.decreaseVolume(step);
this.player.decreaseVolume(step);
direction = -1;
}
}
// Don't break page scrolling at max and min
if ((direction === 1 && this.media.volume < 1) || (direction === -1 && this.media.volume > 0)) {
if ((direction === 1 && this.player.media.volume < 1) || (direction === -1 && this.player.media.volume > 0)) {
event.preventDefault();
}
}),
false
false,
);
},
};
}
export default listeners;
// Reset on destroy
clear() {
this.global(false);
}
}
export default Listeners;

View File

@ -86,7 +86,7 @@ const media = {
}
// Remove child sources
Array.from(this.media.querySelectorAll('source')).forEach(utils.removeElement);
utils.removeElement(this.media.querySelectorAll('source'));
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error

View File

@ -8,22 +8,6 @@
import utils from '../utils';
// Build the default tag URL
const getTagUrl = () => {
const params = {
AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
AV_CHANNELID: '5a0458dc28a06145e4519d21',
AV_URL: '127.0.0.1:3000',
cb: 1,
AV_WIDTH: 640,
AV_HEIGHT: 480,
};
const base = 'https://go.aniview.com/api/adserver6/vast/';
return `${base}?${utils.buildUrlParams(params)}`;
};
class Ads {
/**
* Ads constructor.
@ -32,29 +16,10 @@ class Ads {
*/
constructor(player) {
this.player = player;
this.enabled = player.config.ads.enabled;
this.publisherId = player.config.ads.publisherId;
this.enabled = player.isHTML5 && player.isVideo && player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length;
this.playing = false;
this.initialized = false;
// Check if a tag URL is provided.
if (!this.enabled) {
return;
}
// Check if the Google IMA3 SDK is loaded
if (!utils.is.object(window.google)) {
utils.loadScript(player.config.urls.googleIMA.api, () => {
this.ready();
});
} else {
this.ready();
}
}
/**
* Get the ads instance ready.
*/
ready() {
this.elements = {
container: null,
displayContainer: null,
@ -66,32 +31,77 @@ class Ads {
this.safetyTimer = null;
this.countdownTimer = null;
// Set listeners on the Plyr instance
this.listeners();
// Setup a promise to resolve when the IMA manager is ready
this.managerPromise = new Promise((resolve, reject) => {
// The ad is loaded and ready
this.on('loaded', resolve);
// Ads failed
this.on('error', reject);
});
this.load();
}
/**
* Load the IMA SDK
*/
load() {
if (this.enabled) {
// Check if the Google IMA3 SDK is loaded or load it ourselves
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
utils
.loadScript(this.player.config.urls.googleIMA.api)
.then(() => {
this.ready();
})
.catch(() => {
// Script failed to load or is blocked
this.trigger('error', new Error('Google IMA SDK failed to load'));
});
} else {
this.ready();
}
}
}
/**
* Get the ads instance ready
*/
ready() {
// Start ticking our safety timer. If the whole advertisement
// thing doesn't resolve within our set time; we bail
this.startSafetyTimer(12000, 'ready()');
// Setup a simple promise to resolve if the IMA loader is ready
this.loaderPromise = new Promise(resolve => {
this.on('ADS_LOADER_LOADED', () => resolve());
});
// Setup a promise to resolve if the IMA manager is ready
this.managerPromise = new Promise(resolve => {
this.on('ADS_MANAGER_LOADED', () => resolve());
});
// Clear the safety timer
this.managerPromise.then(() => {
this.clearSafetyTimer('onAdsManagerLoaded()');
});
// Set listeners on the Plyr instance
this.listeners();
// Setup the IMA SDK
this.setupIMA();
}
// Build the default tag URL
get tagUrl() {
const params = {
AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
AV_CHANNELID: '5a0458dc28a06145e4519d21',
AV_URL: location.hostname,
cb: Date.now(),
AV_WIDTH: 640,
AV_HEIGHT: 480,
AV_CDIM2: this.publisherId,
};
const base = 'https://go.aniview.com/api/adserver6/vast/';
return `${base}?${utils.buildUrlParams(params)}`;
}
/**
* In order for the SDK to display ads for our video, we need to tell it where to put them,
* so here we define our ad container. This div is set up to render on top of the video player.
@ -104,7 +114,6 @@ class Ads {
// Create the container for our advertisements
this.elements.container = utils.createElement('div', {
class: this.player.config.classNames.ads,
hidden: '',
});
this.player.elements.container.appendChild(this.elements.container);
@ -138,7 +147,7 @@ class Ads {
// Request video ads
const request = new google.ima.AdsRequest();
request.adTagUrl = getTagUrl();
request.adTagUrl = this.tagUrl;
// Specify the linear and nonlinear slot sizes. This helps the SDK
// to select the correct creative if multiple are returned
@ -151,8 +160,6 @@ class Ads {
request.forceNonLinearFullSlot = false;
this.loader.requestAds(request);
this.handleEventListeners('ADS_LOADER_LOADED');
} catch (e) {
this.onAdError(e);
}
@ -164,25 +171,25 @@ class Ads {
*/
pollCountdown(start = false) {
if (!start) {
window.clearInterval(this.countdownTimer);
clearInterval(this.countdownTimer);
this.elements.container.removeAttribute('data-badge-text');
return;
}
const update = () => {
const time = utils.formatTime(this.manager.getRemainingTime());
const label = `${this.player.config.i18n.advertisment} - ${time}`;
const time = utils.formatTime(Math.max(this.manager.getRemainingTime(), 0));
const label = `${this.player.config.i18n.advertisement} - ${time}`;
this.elements.container.setAttribute('data-badge-text', label);
};
this.countdownTimer = window.setInterval(update, 100);
this.countdownTimer = setInterval(update, 100);
}
/**
* This method is called whenever the ads are ready inside the AdDisplayContainer
* @param {Event} adsManagerLoadedEvent
*/
onAdsManagerLoaded(adsManagerLoadedEvent) {
onAdsManagerLoaded(event) {
// Get the ads manager
const settings = new google.ima.AdsRenderingSettings();
@ -192,14 +199,14 @@ class Ads {
// The SDK is polling currentTime on the contentPlayback. And needs a duration
// so it can determine when to start the mid- and post-roll
this.manager = adsManagerLoadedEvent.getAdsManager(this.player, settings);
this.manager = event.getAdsManager(this.player, settings);
// Get the cue points for any mid-rolls by filtering out the pre- and post-roll
this.cuePoints = this.manager.getCuePoints();
// Add advertisement cue's within the time line if available
this.cuePoints.forEach(cuePoint => {
if (cuePoint !== 0 && cuePoint !== -1) {
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
const seekElement = this.player.elements.progress;
if (seekElement) {
@ -231,7 +238,7 @@ class Ads {
});
// Resolve our adsManager
this.handleEventListeners('ADS_MANAGER_LOADED');
this.trigger('loaded');
}
/**
@ -249,17 +256,18 @@ class Ads {
// Proxy event
const dispatchEvent = type => {
utils.dispatchEvent.call(this.player, this.player.media, `ads${type}`);
const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
utils.dispatchEvent.call(this.player, this.player.media, event);
};
switch (event.type) {
case google.ima.AdEvent.Type.LOADED:
// This is the first event sent for an ad - it is possible to determine whether the
// ad is a video ad or an overlay
this.handleEventListeners('LOADED');
this.trigger('loaded');
// Bubble event
dispatchEvent('loaded');
dispatchEvent(event.type);
// Start countdown
this.pollCountdown(true);
@ -277,10 +285,9 @@ class Ads {
case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
// All ads for the current videos are done. We can now request new advertisements
// in case the video is re-played
this.handleEventListeners('ALL_ADS_COMPLETED');
// Fire event
dispatchEvent('allcomplete');
dispatchEvent(event.type);
// TODO: Example for what happens when a next video in a playlist would be loaded.
// So here we load a new video when all ads are done.
@ -312,9 +319,8 @@ class Ads {
// This event indicates the ad has started - the video player can adjust the UI,
// for example display a pause button and remaining time. Fired when content should
// be paused. This usually happens right before an ad is about to cover the content
this.handleEventListeners('CONTENT_PAUSE_REQUESTED');
dispatchEvent('contentpause');
dispatchEvent(event.type);
this.pauseContent();
@ -325,9 +331,8 @@ class Ads {
// appropriate UI actions, such as removing the timer for remaining time detection.
// Fired when content should be resumed. This usually happens when an ad finishes
// or collapses
this.handleEventListeners('CONTENT_RESUME_REQUESTED');
dispatchEvent('contentresume');
dispatchEvent(event.type);
this.pollCountdown();
@ -336,23 +341,11 @@ class Ads {
break;
case google.ima.AdEvent.Type.STARTED:
dispatchEvent('started');
break;
case google.ima.AdEvent.Type.MIDPOINT:
dispatchEvent('midpoint');
break;
case google.ima.AdEvent.Type.COMPLETE:
dispatchEvent('complete');
break;
case google.ima.AdEvent.Type.IMPRESSION:
dispatchEvent('impression');
break;
case google.ima.AdEvent.Type.CLICK:
dispatchEvent('click');
dispatchEvent(event.type);
break;
default:
@ -366,7 +359,7 @@ class Ads {
*/
onAdError(event) {
this.cancel();
this.player.debug.log('Ads error', event);
this.player.debug.warn('Ads error', event);
}
/**
@ -413,39 +406,41 @@ class Ads {
const { container } = this.player.elements;
if (!this.managerPromise) {
return;
this.resumeContent();
}
// Play the requested advertisement whenever the adsManager is ready
this.managerPromise.then(() => {
// Initialize the container. Must be done via a user action on mobile devices
this.elements.displayContainer.initialize();
this.managerPromise
.then(() => {
// Initialize the container. Must be done via a user action on mobile devices
this.elements.displayContainer.initialize();
try {
if (!this.initialized) {
// Initialize the ads manager. Ad rules playlist will start at this time
this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
try {
if (!this.initialized) {
// Initialize the ads manager. Ad rules playlist will start at this time
this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
// Call play to start showing the ad. Single video and overlay ads will
// start at this time; the call will be ignored for ad rules
this.manager.start();
// Call play to start showing the ad. Single video and overlay ads will
// start at this time; the call will be ignored for ad rules
this.manager.start();
}
this.initialized = true;
} catch (adError) {
// An error may be thrown if there was a problem with the
// VAST response
this.onAdError(adError);
}
this.initialized = true;
} catch (adError) {
// An error may be thrown if there was a problem with the
// VAST response
this.onAdError(adError);
}
});
})
.catch(() => {});
}
/**
* Resume our video.
* Resume our video
*/
resumeContent() {
// Hide our ad container
utils.toggleHidden(this.elements.container, true);
// Hide the advertisement container
this.elements.container.style.zIndex = '';
// Ad is stopped
this.playing = false;
@ -460,8 +455,8 @@ class Ads {
* Pause our video
*/
pauseContent() {
// Show our ad container.
utils.toggleHidden(this.elements.container, false);
// Show the advertisement container
this.elements.container.style.zIndex = 3;
// Ad is playing.
this.playing = true;
@ -483,7 +478,7 @@ class Ads {
}
// Tell our instance that we're done for now
this.handleEventListeners('ERROR');
this.trigger('error');
// Re-create our adsManager
this.loadAds();
@ -494,30 +489,38 @@ class Ads {
*/
loadAds() {
// Tell our adsManager to go bye bye
this.managerPromise.then(() => {
// Destroy our adsManager
if (this.manager) {
this.manager.destroy();
}
this.managerPromise
.then(() => {
// Destroy our adsManager
if (this.manager) {
this.manager.destroy();
}
// Re-set our adsManager promises
this.managerPromise = new Promise(resolve => {
this.on('ADS_MANAGER_LOADED', () => resolve());
this.player.debug.log(this.manager);
});
// Re-set our adsManager promises
this.managerPromise = new Promise(resolve => {
this.on('loaded', resolve);
this.player.debug.log(this.manager);
});
// Now request some new advertisements
this.requestAds();
});
// Now request some new advertisements
this.requestAds();
})
.catch(() => {});
}
/**
* Handles callbacks after an ad event was invoked
* @param {string} event - Event type
*/
handleEventListeners(event) {
if (utils.is.function(this.events[event])) {
this.events[event].call(this);
trigger(event, ...args) {
const handlers = this.events[event];
if (utils.is.array(handlers)) {
handlers.forEach(handler => {
if (utils.is.function(handler)) {
handler.apply(this, args);
}
});
}
}
@ -528,7 +531,12 @@ class Ads {
* @return {Ads}
*/
on(event, callback) {
this.events[event] = callback;
if (!utils.is.array(this.events[event])) {
this.events[event] = [];
}
this.events[event].push(callback);
return this;
}
@ -543,7 +551,7 @@ class Ads {
startSafetyTimer(time, from) {
this.player.debug.log(`Safety timer invoked from: ${from}`);
this.safetyTimer = window.setTimeout(() => {
this.safetyTimer = setTimeout(() => {
this.cancel();
this.clearSafetyTimer('startSafetyTimer()');
}, time);

View File

@ -16,9 +16,14 @@ const vimeo = {
// Load the API if not already
if (!utils.is.object(window.Vimeo)) {
utils.loadScript(this.config.urls.vimeo.api, () => {
vimeo.ready.call(this);
});
utils
.loadScript(this.config.urls.vimeo.api)
.then(() => {
vimeo.ready.call(this);
})
.catch(error => {
this.debug.warn('Vimeo API failed to load', error);
});
} else {
vimeo.ready.call(this);
}
@ -51,7 +56,16 @@ const vimeo = {
gesture: 'media',
};
const params = utils.buildUrlParams(options);
const id = utils.parseVimeoId(player.media.getAttribute('src'));
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (utils.is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
const id = utils.parseVimeoId(source);
// Build an iframe
const iframe = utils.createElement('iframe');
@ -302,7 +316,7 @@ const vimeo = {
});
// Rebuild UI
window.setTimeout(() => ui.build.call(player), 0);
setTimeout(() => ui.build.call(player), 0);
},
};

View File

@ -19,7 +19,9 @@ const youtube = {
youtube.ready.call(this);
} else {
// Load the API
utils.loadScript(this.config.urls.youtube.api);
utils.loadScript(this.config.urls.youtube.api).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
// Setup callback for the API
// YouTube has it's own system of course...
@ -59,10 +61,10 @@ const youtube = {
if (utils.is.string(key) && !utils.is.empty(key)) {
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`;
fetch(url)
.then(response => (response.ok ? response.json() : null))
utils
.fetch(url)
.then(result => {
if (result !== null && utils.is.object(result)) {
if (utils.is.object(result)) {
this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this);
}
@ -87,8 +89,16 @@ const youtube = {
return;
}
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (utils.is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = utils.parseYouTubeId(player.media.getAttribute('src'));
const videoId = utils.parseYouTubeId(source);
const id = utils.generateId(player.provider);
const container = utils.createElement('div', { id });
player.media = utils.replaceElement(container, player.media);
@ -186,17 +196,14 @@ const youtube = {
// Create a faux HTML5 API using the YouTube API
player.media.play = () => {
instance.playVideo();
player.media.paused = false;
};
player.media.pause = () => {
instance.pauseVideo();
player.media.paused = true;
};
player.media.stop = () => {
instance.stopVideo();
player.media.paused = true;
};
player.media.duration = instance.getDuration();
@ -298,10 +305,10 @@ const youtube = {
utils.dispatchEvent.call(player, player.media, 'durationchange');
// Reset timer
window.clearInterval(player.timers.buffering);
clearInterval(player.timers.buffering);
// Setup buffering
player.timers.buffering = window.setInterval(() => {
player.timers.buffering = setInterval(() => {
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
@ -315,7 +322,7 @@ const youtube = {
// Bail if we're at 100%
if (player.media.buffered === 1) {
window.clearInterval(player.timers.buffering);
clearInterval(player.timers.buffering);
// Trigger event
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
@ -323,14 +330,14 @@ const youtube = {
}, 200);
// Rebuild UI
window.setTimeout(() => ui.build.call(player), 50);
setTimeout(() => ui.build.call(player), 50);
},
onStateChange(event) {
// Get the instance
const instance = event.target;
// Reset timer
window.clearInterval(player.timers.playing);
clearInterval(player.timers.playing);
// Handle events
// -1 Unstarted
@ -370,7 +377,7 @@ const youtube = {
utils.dispatchEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = window.setInterval(() => {
player.timers.playing = setInterval(() => {
utils.dispatchEvent.call(player, player.media, 'timeupdate');
}, 50);

View File

@ -1,6 +1,6 @@
// ==========================================================================
// Plyr
// plyr.js v3.0.0-beta.8
// plyr.js v3.0.1
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
@ -11,13 +11,13 @@ import support from './support';
import utils from './utils';
import Console from './console';
import Fullscreen from './fullscreen';
import Listeners from './listeners';
import Storage from './storage';
import Ads from './plugins/ads';
import captions from './captions';
import controls from './controls';
import fullscreen from './fullscreen';
import listeners from './listeners';
import media from './media';
import source from './source';
import ui from './ui';
@ -26,12 +26,6 @@ import ui from './ui';
// TODO: Use a WeakMap for private globals
// const globals = new WeakMap();
// Globals
let scrollPosition = {
x: 0,
y: 0,
};
// Plyr instance
class Plyr {
constructor(target, options) {
@ -153,50 +147,53 @@ class Plyr {
// Find the frame
iframe = this.media.querySelector('iframe');
// <iframe> required
if (!utils.is.element(iframe)) {
this.debug.error('Setup failed: <iframe> is missing');
return;
// <iframe> type
if (utils.is.element(iframe)) {
// Detect provider
url = iframe.getAttribute('src');
this.provider = utils.getProviderByUrl(url);
// Rework elements
this.elements.container = this.media;
this.media = iframe;
// Reset classname
this.elements.container.className = '';
// Get attributes from URL and set config
params = utils.getUrlParams(url);
if (!utils.is.empty(params)) {
const truthy = [
'1',
'true',
];
if (truthy.includes(params.autoplay)) {
this.config.autoplay = true;
}
if (truthy.includes(params.playsinline)) {
this.config.inline = true;
}
if (truthy.includes(params.loop)) {
this.config.loop.active = true;
}
}
} else {
// <div> with attributes
this.provider = this.media.getAttribute(this.config.attributes.embed.provider);
// Remove attribute
this.media.removeAttribute(this.config.attributes.embed.provider);
}
// Audio will come later for external providers
this.type = types.video;
// Detect provider
url = iframe.getAttribute('src');
this.provider = utils.getProviderByUrl(url);
// Get attributes from URL and set config
params = utils.getUrlParams(url);
if (!utils.is.empty(params)) {
const truthy = [
'1',
'true',
];
if (truthy.includes(params.autoplay)) {
this.config.autoplay = true;
}
if (truthy.includes(params.playsinline)) {
this.config.inline = true;
}
if (truthy.includes(params.loop)) {
this.config.loop.active = true;
}
}
// Unsupported provider
// Unsupported or missing provider
if (utils.is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
this.debug.error('Setup failed: Invalid provider');
return;
}
// Rework elements
this.elements.container = this.media;
this.media = iframe;
// Reset classname
this.elements.container.className = '';
// Audio will come later for external providers
this.type = types.video;
break;
@ -229,9 +226,6 @@ class Plyr {
return;
}
// Setup local storage for user settings
this.storage = new Storage(this);
// Check for support again but with type
this.supported = support.check(this.type, this.provider, this.config.inline);
@ -241,6 +235,12 @@ class Plyr {
return;
}
// Create listeners
this.listeners = new Listeners(this);
// Setup local storage for user settings
this.storage = new Storage(this);
// Store reference
this.media.plyr = this;
@ -253,9 +253,6 @@ class Plyr {
// Allow focus to be captured
this.elements.container.setAttribute('tabindex', 0);
// Global listeners
listeners.global.call(this);
// Add style hook
ui.addStyleHook.call(this);
@ -275,6 +272,15 @@ class Plyr {
ui.build.call(this);
}
// Container listeners
this.listeners.container();
// Global listeners
this.listeners.global();
// Setup fullscreen
this.fullscreen = new Fullscreen(this);
// Setup ads if provided
this.ads = new Ads(this);
}
@ -287,32 +293,35 @@ class Plyr {
* Types and provider helpers
*/
get isHTML5() {
return this.provider === providers.html5;
return Boolean(this.provider === providers.html5);
}
get isEmbed() {
return this.isYouTube || this.isVimeo;
return Boolean(this.isYouTube || this.isVimeo);
}
get isYouTube() {
return this.provider === providers.youtube;
return Boolean(this.provider === providers.youtube);
}
get isVimeo() {
return this.provider === providers.vimeo;
return Boolean(this.provider === providers.vimeo);
}
get isVideo() {
return this.type === types.video;
return Boolean(this.type === types.video);
}
get isAudio() {
return this.type === types.audio;
return Boolean(this.type === types.audio);
}
/**
* Play the media, or play the advertisement
* Play the media, or play the advertisement (if they are not blocked)
*/
play() {
// Play the ad if setup
// TODO: Fix the occasional play of the video before the Ad fires?
if (!utils.is.function(this.media.play)) {
return null;
}
// If ads are enabled, wait for them first
if (this.ads.enabled && !this.ads.initialized) {
this.ads.play();
return this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play());
}
// Return the promise (for HTML5)
@ -323,7 +332,7 @@ class Plyr {
* Pause the media
*/
pause() {
if (!this.playing) {
if (!this.playing || !utils.is.function(this.media.pause)) {
return;
}
@ -334,21 +343,21 @@ class Plyr {
* Get paused state
*/
get paused() {
return this.media.paused;
return Boolean(this.media.paused);
}
/**
* Get playing state
*/
get playing() {
return !this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true);
return Boolean(!this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true));
}
/**
* Get ended state
*/
get ended() {
return this.media.ended;
return Boolean(this.media.ended);
}
/**
@ -429,11 +438,32 @@ class Plyr {
return Number(this.media.currentTime);
}
/**
* Get buffered
*/
get buffered() {
const { buffered } = this.media;
// YouTube / Vimeo return a float between 0-1
if (utils.is.number(buffered)) {
return buffered;
}
// HTML5
// TODO: Handle buffered chunks of the media
// (i.e. seek to another section buffers only that section)
if (buffered && buffered.length && this.duration > 0) {
return buffered.end(0) / this.duration;
}
return 0;
}
/**
* Get seeking status
*/
get seeking() {
return this.media.seeking;
return Boolean(this.media.seeking);
}
/**
@ -498,7 +528,7 @@ class Plyr {
* Get the current player volume
*/
get volume() {
return this.media.volume;
return Number(this.media.volume);
}
/**
@ -547,7 +577,7 @@ class Plyr {
* Get current muted state
*/
get muted() {
return this.media.muted;
return Boolean(this.media.muted);
}
/**
@ -559,13 +589,21 @@ class Plyr {
return true;
}
if (this.isAudio) {
return true;
}
// Get audio tracks
return this.media.mozHasAudio || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);
return (
Boolean(this.media.mozHasAudio) ||
Boolean(this.media.webkitAudioDecodedByteCount) ||
Boolean(this.media.audioTracks && this.media.audioTracks.length)
);
}
/**
* Set playback speed
* @param {decimal} speed - the speed of playback (0.5-2.0)
* @param {number} speed - the speed of playback (0.5-2.0)
*/
set speed(input) {
let speed = null;
@ -606,7 +644,7 @@ class Plyr {
* Get current playback speed
*/
get speed() {
return this.media.playbackRate;
return Number(this.media.playbackRate);
}
/**
@ -706,7 +744,7 @@ class Plyr {
* Get current loop state
*/
get loop() {
return this.media.loop;
return Boolean(this.media.loop);
}
/**
@ -763,7 +801,7 @@ class Plyr {
* Get the current autoplay state
*/
get autoplay() {
return this.config.autoplay;
return Boolean(this.config.autoplay);
}
/**
@ -843,62 +881,6 @@ class Plyr {
return this.captions.language;
}
/**
* Toggle fullscreen playback
* Requires user input event
* @param {event} event
*/
toggleFullscreen(event) {
// Video only
if (this.isAudio) {
return;
}
// Check for native support
if (fullscreen.enabled) {
if (utils.is.event(event) && event.type === fullscreen.eventType) {
// If it's a fullscreen change event, update the state
this.fullscreen.active = fullscreen.isFullScreen(this.elements.container);
} else {
// Else it's a user request to enter or exit
if (!this.fullscreen.active) {
fullscreen.requestFullScreen(this.elements.container);
} else {
fullscreen.cancelFullScreen();
}
return;
}
} else {
// Otherwise, it's a simple toggle
this.fullscreen.active = !this.fullscreen.active;
// Add class hook
utils.toggleClass(this.elements.container, this.config.classNames.fullscreen.fallback, this.fullscreen.active);
// Make sure we don't lose scroll position
if (this.fullscreen.active) {
scrollPosition = {
x: window.pageXOffset || 0,
y: window.pageYOffset || 0,
};
} else {
window.scrollTo(scrollPosition.x, scrollPosition.y);
}
// Bind/unbind escape key
document.body.style.overflow = this.fullscreen.active ? 'hidden' : '';
}
// Set button state
if (utils.is.element(this.elements.buttons.fullscreen)) {
utils.toggleState(this.elements.buttons.fullscreen, this.fullscreen.active);
}
// Trigger an event
utils.dispatchEvent.call(this, this.media, this.fullscreen.active ? 'enterfullscreen' : 'exitfullscreen');
}
/**
* Toggle picture-in-picture playback on WebKit/MacOS
* TODO: update player with state, support, enabled
@ -998,7 +980,7 @@ class Plyr {
}
// Clear timer on every call
window.clearTimeout(this.timers.controls);
clearTimeout(this.timers.controls);
// If the mouse is not over the controls, set a timeout to hide them
if (show || this.paused || this.loading) {
@ -1024,15 +1006,7 @@ class Plyr {
// If toggle is false or if we're playing (regardless of toggle),
// then set the timer to hide the controls
if (!show || this.playing) {
this.timers.controls = window.setTimeout(() => {
/* this.debug.warn({
pressed: this.elements.controls.pressed,
hover: this.elements.controls.pressed,
playing: this.playing,
paused: this.paused,
loading: this.loading,
}); */
this.timers.controls = setTimeout(() => {
// If the mouse is over the controls (and not entering fullscreen), bail
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
return;
@ -1084,6 +1058,10 @@ class Plyr {
* @param {boolean} soft - Whether it's a soft destroy (for source changes etc)
*/
destroy(callback, soft = false) {
if (!this.ready) {
return;
}
const done = () => {
// Reset overflow (incase destroyed while in fullscreen)
document.body.style.overflow = '';
@ -1094,12 +1072,8 @@ class Plyr {
// If it's a soft destroy, make minimal changes
if (soft) {
if (Object.keys(this.elements).length) {
// Remove buttons
if (this.elements.buttons && this.elements.buttons.play) {
Array.from(this.elements.buttons.play).forEach(button => utils.removeElement(button));
}
// Remove others
// Remove elements
utils.removeElement(this.elements.buttons.play);
utils.removeElement(this.elements.captions);
utils.removeElement(this.elements.controls);
utils.removeElement(this.elements.wrapper);
@ -1116,6 +1090,9 @@ class Plyr {
callback();
}
} else {
// Unbind listeners
this.listeners.clear();
// Replace the container with the original element provided
utils.replaceElement(this.elements.original, this.elements.container);
@ -1127,15 +1104,27 @@ class Plyr {
callback.call(this.elements.original);
}
// Clear for GC
this.elements = null;
// Reset state
this.ready = false;
// Clear for garbage collection
setTimeout(() => {
this.elements = null;
this.media = null;
}, 200);
}
};
// Stop playback
this.stop();
// Type specific stuff
switch (`${this.provider}:${this.type}`) {
case 'html5:video':
case 'html5:audio':
// Clear timeout
clearTimeout(this.timers.loading);
// Restore native video controls
ui.toggleNativeControls.call(this, true);
@ -1146,8 +1135,8 @@ class Plyr {
case 'youtube:video':
// Clear timers
window.clearInterval(this.timers.buffering);
window.clearInterval(this.timers.playing);
clearInterval(this.timers.buffering);
clearInterval(this.timers.playing);
// Destroy YouTube API
if (this.embed !== null) {
@ -1167,7 +1156,7 @@ class Plyr {
}
// Vimeo does not always return
window.setTimeout(done, 200);
setTimeout(done, 200);
break;

14
src/js/plyr.polyfilled.js Normal file
View File

@ -0,0 +1,14 @@
// ==========================================================================
// Plyr Polyfilled Build
// plyr.js v3.0.1
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import 'babel-polyfill';
import 'custom-event-polyfill';
import Plyr from './plyr';
export default Plyr;

View File

@ -136,6 +136,9 @@ const source = {
// Setup interface
ui.build.call(this);
}
// Update the fullscreen support
this.fullscreen.update();
},
true,
);

View File

@ -12,17 +12,18 @@ class Storage {
// Check for actual support (see if we can use it)
static get supported() {
if (!('localStorage' in window)) {
return false;
}
const test = '___test';
// Try to use it (it might be disabled, e.g. user is in private mode)
// see: https://github.com/sampotts/plyr/issues/131
try {
if (!('localStorage' in window)) {
return false;
}
const test = '___test';
// Try to use it (it might be disabled, e.g. user is in private mode)
// see: https://github.com/sampotts/plyr/issues/131
window.localStorage.setItem(test, test);
window.localStorage.removeItem(test);
return true;
} catch (e) {
return false;
@ -30,9 +31,13 @@ class Storage {
}
get(key) {
if (!Storage.supported) {
return null;
}
const store = window.localStorage.getItem(this.key);
if (!Storage.supported || utils.is.empty(store)) {
if (utils.is.empty(store)) {
return null;
}

View File

@ -30,13 +30,9 @@ const support = {
break;
case 'youtube:video':
api = true;
ui = support.rangeInput && (!browser.isIPhone || playsInline);
break;
case 'vimeo:video':
api = true;
ui = support.rangeInput && !browser.isIPhone;
ui = support.rangeInput && (!browser.isIPhone || playsInline);
break;
default:

View File

@ -5,8 +5,6 @@
import utils from './utils';
import captions from './captions';
import controls from './controls';
import fullscreen from './fullscreen';
import listeners from './listeners';
const ui = {
addStyleHook() {
@ -26,19 +24,13 @@ const ui = {
// Setup the UI
build() {
// Re-attach media element listeners
// TODO: Use event bubbling
listeners.media.call(this);
// TODO: Use event bubbling?
this.listeners.media();
// Don't setup interface if no support
if (!this.supported.ui) {
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
// Remove controls
utils.removeElement.call(this, 'controls');
// Remove large play
utils.removeElement.call(this, 'buttons.play');
// Restore native controls
ui.toggleNativeControls.call(this, true);
@ -52,7 +44,7 @@ const ui = {
controls.inject.call(this);
// Re-attach control listeners
listeners.controls.call(this);
this.listeners.controls();
}
// If there's no controls, bail
@ -63,9 +55,6 @@ const ui = {
// Remove native controls
ui.toggleNativeControls.call(this);
// Setup fullscreen
fullscreen.setup.call(this);
// Captions
captions.setup.call(this);
@ -94,7 +83,9 @@ const ui = {
this.ready = true;
// Ready event at end of execution stack
utils.dispatchEvent.call(this, this.media, 'ready');
setTimeout(() => {
utils.dispatchEvent.call(this, this.media, 'ready');
}, 0);
// Set the title
ui.setTitle.call(this);
@ -142,10 +133,8 @@ const ui = {
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused);
// Set aria state
if (utils.is.nodeList(this.elements.buttons.play)) {
Array.from(this.elements.buttons.play).forEach(button => utils.toggleState(button, this.playing));
}
// Set ARIA state
utils.toggleState(this.elements.buttons.play, this.playing);
// Toggle controls
this.toggleControls(!this.playing);
@ -266,21 +255,7 @@ const ui = {
// Check buffer status
case 'playing':
case 'progress':
value = (() => {
const { buffered } = this.media;
if (buffered && buffered.length) {
// HTML5
return utils.getPercentage(buffered.end(0), this.duration);
} else if (utils.is.number(buffered)) {
// YouTube returns between 0 and 1
return buffered * 100;
}
return 0;
})();
ui.setProgress.call(this, this.elements.display.buffer, value);
ui.setProgress.call(this, this.elements.display.buffer, this.buffered * 100);
break;

View File

@ -9,7 +9,7 @@ const utils = {
// Check variable types
is: {
plyr(input) {
return this.instanceof(input, Plyr);
return this.instanceof(input, window.Plyr);
},
object(input) {
return this.getConstructor(input) === Object;
@ -81,26 +81,71 @@ const utils = {
};
},
// Fetch wrapper
// Using XHR to avoid issues with older browsers
fetch(url, responseType = 'text') {
return new Promise((resolve, reject) => {
try {
const request = new XMLHttpRequest();
// Check for CORS support
if (!('withCredentials' in request)) {
return;
}
request.addEventListener('load', () => {
if (responseType === 'text') {
try {
resolve(JSON.parse(request.responseText));
} catch(e) {
resolve(request.responseText);
}
}
else {
resolve(request.response);
}
});
request.addEventListener('error', () => {
throw new Error(request.statusText);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
}
});
},
// Load an external script
loadScript(url, callback) {
const current = document.querySelector(`script[src="${url}"]`);
loadScript(url) {
return new Promise((resolve, reject) => {
const current = document.querySelector(`script[src="${url}"]`);
// Check script is not already referenced, if so wait for load
if (current !== null) {
current.callbacks = current.callbacks || [];
current.callbacks.push(callback);
return;
}
// Check script is not already referenced, if so wait for load
if (current !== null) {
current.callbacks = current.callbacks || [];
current.callbacks.push(resolve);
return;
}
// Build the element
const element = document.createElement('script');
// Build the element
const element = document.createElement('script');
// Callback queue
element.callbacks = element.callbacks || [];
element.callbacks.push(callback);
// Callback queue
element.callbacks = element.callbacks || [];
element.callbacks.push(resolve);
// Bind callback
if (utils.is.function(callback)) {
// Error queue
element.errors = element.errors || [];
element.errors.push(reject);
// Bind callback
element.addEventListener(
'load',
event => {
@ -109,14 +154,24 @@ const utils = {
},
false,
);
}
// Set the URL after binding callback
element.src = url;
// Bind error handling
element.addEventListener(
'error',
event => {
element.errors.forEach(err => err.call(null, event));
element.errors = null;
},
false,
);
// Inject
const first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(element, first);
// Set the URL after binding callback
element.src = url;
// Inject
const first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(element, first);
});
},
// Load an external SVG sprite
@ -160,10 +215,10 @@ const utils = {
}
// Get the sprite
fetch(url)
.then(response => (response.ok ? response.text() : null))
.then(text => {
if (text === null) {
utils
.fetch(url)
.then(result => {
if (utils.is.empty(result)) {
return;
}
@ -171,12 +226,12 @@ const utils = {
window.localStorage.setItem(
prefix + id,
JSON.stringify({
content: text,
content: result,
}),
);
}
updateSprite.call(container, text);
updateSprite.call(container, result);
})
.catch(() => {});
}
@ -260,12 +315,15 @@ const utils = {
// Remove an element
removeElement(element) {
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
return null;
return;
}
if (utils.is.nodeList(element) || utils.is.array(element)) {
Array.from(element).forEach(utils.removeElement);
return;
}
element.parentNode.removeChild(element);
return element;
},
// Remove all child elements
@ -479,46 +537,51 @@ const utils = {
},
// Trap focus inside container
trapFocus() {
trapFocus(element = null, toggle = false) {
if (!utils.is.element(element)) {
return;
}
const focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = focusable[0];
const last = focusable[focusable.length - 1];
utils.on(
this.elements.container,
'keydown',
event => {
// Bail if not tab key or not fullscreen
if (event.key !== 'Tab' || event.keyCode !== 9 || !this.fullscreen.active) {
return;
}
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 = utils.getFocusElement();
// Get the current focused element
const focused = utils.getFocusElement();
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();
}
},
false,
);
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();
}
};
if (toggle) {
utils.on(this.elements.container, 'keydown', trap, false);
} else {
utils.off(this.elements.container, 'keydown', trap, false);
}
},
// Toggle event listener
toggleListener(elements, event, callback, toggle, passive, capture) {
// Bail if no elements
if (utils.is.nullOrUndefined(elements)) {
// Bail if no elemetns, event, or callback
if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
return;
}
// If a nodelist is passed, call itself on each node
if (utils.is.nodeList(elements)) {
if (utils.is.nodeList(elements) || utils.is.array(elements)) {
// Create listener for each node
Array.from(elements).forEach(element => {
if (element instanceof Node) {
@ -565,7 +628,7 @@ const utils = {
// Trigger event
dispatchEvent(element, type, bubbles, detail) {
// Bail if no element
if (!element || !type) {
if (!utils.is.element(element) || !utils.is.string(type)) {
return;
}
@ -573,7 +636,7 @@ const utils = {
const event = new CustomEvent(type, {
bubbles: utils.is.boolean(bubbles) ? bubbles : false,
detail: Object.assign({}, detail, {
plyr: this instanceof Plyr ? this : null,
plyr: utils.is.plyr(this) ? this : null,
}),
});
@ -584,6 +647,12 @@ const utils = {
// Toggle aria-pressed state on a toggle button
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
toggleState(element, input) {
// If multiple elements passed
if (utils.is.array(element) || utils.is.nodeList(element)) {
Array.from(element).forEach(target => utils.toggleState(target, input));
return;
}
// Bail if no target
if (!utils.is.element(element)) {
return;
@ -602,13 +671,20 @@ const utils = {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
return 0;
}
return (current / max * 100).toFixed(2);
},
// Time helpers
getHours(value) { return parseInt((value / 60 / 60) % 60, 10); },
getMinutes(value) { return parseInt((value / 60) % 60, 10); },
getSeconds(value) { return parseInt(value % 60, 10); },
getHours(value) {
return parseInt((value / 60 / 60) % 60, 10);
},
getMinutes(value) {
return parseInt((value / 60) % 60, 10);
},
getSeconds(value) {
return parseInt(value % 60, 10);
},
// Format time to UI friendly string
formatTime(time = 0, displayHours = false, inverted = false) {
@ -779,7 +855,7 @@ const utils = {
// Force repaint of element
repaint(element) {
window.setTimeout(() => {
setTimeout(() => {
utils.toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line
utils.toggleHidden(element, false);

View File

@ -8,6 +8,7 @@
direction: ltr;
font-family: $plyr-font-family;
font-variant-numeric: tabular-nums; // Force monosace-esque number widths
font-weight: $plyr-font-weight-regular;
line-height: $plyr-line-height;
max-width: 100%;

View File

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

View File

@ -59,6 +59,14 @@
margin: 0;
overflow: hidden;
padding: $plyr-control-padding;
li {
margin-top: 2px;
&:first-child {
margin-top: 0;
}
}
}
// Options

View File

@ -91,6 +91,21 @@
@include plyr-tab-focus();
}
}
}
// 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 {
@ -108,21 +123,6 @@
}
}
// 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;
}
}
// Audio range inputs
.plyr--full-ui.plyr--audio input[type='range'] {
&::-webkit-slider-runnable-track {
@ -136,4 +136,19 @@
&::-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

@ -44,8 +44,8 @@
width: $plyr-range-thumb-height;
}
@mixin plyr-range-thumb-active() {
box-shadow: 0 0 0 $plyr-range-thumb-active-shadow-width transparentize($plyr-range-thumb-bg, 0.5);
@mixin plyr-range-thumb-active($color: rgba($plyr-range-thumb-bg, 0.5)) {
box-shadow: $plyr-range-thumb-shadow, 0 0 0 $plyr-range-thumb-active-shadow-width $color;
}
// Fullscreen styles
@ -87,6 +87,11 @@
}
}
// Hide cursor in fullscreen when controls hidden
&.plyr--hide-controls {
cursor: none;
}
// Large captions in full screen on larger screens
@media (min-width: $plyr-bp-lg) {
.plyr__captions {

View File

@ -1,16 +1,27 @@
// ==========================================================================
// Advertisments
// Advertisements
// ==========================================================================
.plyr__ads {
border-radius: inherit;
bottom: 0;
cursor: pointer;
left: 0;
overflow: hidden;
position: absolute;
right: 0;
top: 0;
z-index: 3; // Above the controls
z-index: -1; // Hide it by default
// Make sure the inner container is big enough for the ad creative.
> div,
> div iframe {
height: 100%;
position: absolute;
width: 100%;
}
// The countdown label
&::after {
background: rgba($plyr-color-gunmetal, 0.8);
border-radius: 2px;

View File

@ -10,6 +10,7 @@
@import 'settings/cosmetics';
@import 'settings/type';
@import 'settings/badges';
@import 'settings/captions';
@import 'settings/controls';
@import 'settings/helpers';

View File

@ -0,0 +1,6 @@
// ==========================================================================
// Badges
// ==========================================================================
$plyr-badge-bg: $plyr-color-fiord !default;
$plyr-badge-color: #fff !default;

View File

@ -9,7 +9,7 @@ $plyr-range-thumb-active-shadow-width: 3px !default;
$plyr-range-thumb-height: 14px !default;
$plyr-range-thumb-bg: #fff !default;
$plyr-range-thumb-border: 2px solid transparent !default;
$plyr-range-thumb-shadow: 0 1px 1px rgba($plyr-video-controls-bg, 0.15), 0 0 0 1px rgba($plyr-color-gunmetal, 0.2) !default;
$plyr-range-thumb-shadow: 0 1px 1px rgba(#000, 0.15), 0 0 0 1px rgba($plyr-color-gunmetal, 0.2) !default;
// Track
$plyr-range-track-height: 6px !default;
@ -21,3 +21,4 @@ $plyr-range-fill-bg: $plyr-color-main !default;
// Type specific
$plyr-video-range-track-bg: $plyr-video-progress-buffered-bg !default;
$plyr-audio-range-track-bg: $plyr-audio-progress-buffered-bg !default;
$plyr-audio-range-thumb-shadow-color: rgba(#000, 0.1) !default;

1055
yarn.lock

File diff suppressed because it is too large Load Diff