Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f687b81b70 | |||
| bbb11e611e | |||
| 90919411e9 | |||
| 1491b017a0 | |||
| 1655150092 | |||
| ceb6c9a100 | |||
| 00bbce08fb | |||
| 91a4b86860 | |||
| 5aece6fa06 | |||
| a70b94afe2 | |||
| 9ebc2719d3 | |||
| b46aae1833 | |||
| 30e6a40865 | |||
| 5ca769807e | |||
| 72a71a605b | |||
| 261cd086c7 | |||
| 9e19b526b9 | |||
| 6c617a0ef1 | |||
| a812650fea | |||
| fec7a77d6f | |||
| 971e261067 | |||
| 27407ba021 | |||
| ef8e58ede4 | |||
| f1b275aedc | |||
| b647af256c | |||
| d2e9ed3467 | |||
| 5b39986835 | |||
| a97b08e8ea | |||
| 56d1be9447 | |||
| a241cb5215 | |||
| 042b1a8294 | |||
| 6d79b8cd4c | |||
| 88d766aeae | |||
| 119b471b84 | |||
| 7f079e0ec3 | |||
| 46fe3eecff | |||
| 3061a701d5 | |||
| e45109e1d7 | |||
| e138e6d51e | |||
| aef1363b04 | |||
| 766dd03d81 | |||
| ab393651ec | |||
| ffd265d0ae | |||
| 72155472dd | |||
| 9b7170834e | |||
| 3e57a87bf7 | |||
| a15d1c9f1c | |||
| a095a64f90 | |||
| 2374d6b1c4 | |||
| 5ed3ff9084 | |||
| 385be55510 | |||
| 3082d0d128 | |||
| f7e242f054 | |||
| 2874505004 | |||
| ed9e0c13d7 | |||
| 10be94fa99 | |||
| ee79c46145 | |||
| 384010a2c0 | |||
| 1e47019122 | |||
| 536c65e82c | |||
| cdf14932ec | |||
| 3b20dbd9fd | |||
| e4d975af00 | |||
| 2782a00e7c | |||
| 91d192dd7c | |||
| b1e3abc795 | |||
| 3395e8df90 | |||
| cce143a7da | |||
| d593005b32 |
@@ -9,6 +9,7 @@
|
||||
"ignore": ["attribute", "class"]
|
||||
}
|
||||
],
|
||||
"string-no-newline": null,
|
||||
"indentation": 4,
|
||||
"string-quotes": "single",
|
||||
"max-nesting-depth": 2,
|
||||
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost/dev/plyr/demo",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"demo": {
|
||||
"sass": {
|
||||
"demo.css": "demo/src/sass/bundles/demo.scss",
|
||||
"error.css": "demo/src/sass/bundles/error.csss"
|
||||
"error.css": "demo/src/sass/bundles/error.scss"
|
||||
},
|
||||
"js": {
|
||||
"demo.js": "demo/src/js/demo.js"
|
||||
|
||||
@@ -1,3 +1,95 @@
|
||||
# v3.3.6
|
||||
|
||||
* Vimeo fixes for mute state
|
||||
* Vimeo ID fix (fixes #945)
|
||||
* Use `<div>` for poster container
|
||||
* Tooltip fixes for unicode languages (fixes #943)
|
||||
|
||||
# v3.3.5
|
||||
|
||||
* Removed `.load()` call as it breaks HLS (see #870)
|
||||
|
||||
# v3.3.4
|
||||
|
||||
* Fix for controls sometimes not showing while video is playing
|
||||
* Fixed logic for show home tab on option select
|
||||
|
||||
# v3.3.3
|
||||
|
||||
* Reverted change to show home tab on option select due to usability regression
|
||||
|
||||
# v3.3.2
|
||||
|
||||
* Fix for ads running in audio
|
||||
* Fix for setting poster on source change
|
||||
|
||||
## v3.3.0
|
||||
|
||||
* Now using a custom poster image element to hide the YouTube play button and give more control over when the poster image shows
|
||||
* Renamed `showPosterOnEnd` to `resetOnEnd` as it makes more sense and now works for all players and does not reload media
|
||||
* Fix for same domain SVG URLs (raised by Jochem in Slack)
|
||||
* [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/Window/URL) is polyfill now required
|
||||
* Added pause className (fixes #941)
|
||||
* Button height set in CSS (auto) (fixes #928)
|
||||
* Don't autoplay cloned original media (fixes #936)
|
||||
* Return to the home menu pane after selecting an option
|
||||
|
||||
## v3.2.4
|
||||
|
||||
* Fix issue wher player never reports as ready if controls is empty array
|
||||
* Fix issue where screen reader labels were removed from time displays
|
||||
* Fix issue where custom controls placeholders were not populated
|
||||
* Custom controls HTML example updated
|
||||
* Fix for aria-label being set to the initial state on toggle buttons, overriding the inner labels
|
||||
* Fix for hidden mute button on iOS (not functional for Vimeo due to API limitations) (fixes #656)
|
||||
|
||||
## v3.2.3
|
||||
|
||||
* Fix for iOS 9 throwing error for `name` property in fullscreen API (fixes #908)
|
||||
|
||||
## v3.2.2
|
||||
|
||||
* Fix for regression in 3.2.1 resulting in hidden buffer display (fixes #920)
|
||||
* Cleaned up incorrect use of `aria-hidden` attribute
|
||||
|
||||
## v3.2.1
|
||||
|
||||
* Accessibility improvements for the controls (part of #905 fixes)
|
||||
* Fix for context menu showing on YouTube (thanks Anthony Recenello in Slack)
|
||||
* Vimeo fix for their API not returning the right duration until playback begins (fixes #891)
|
||||
|
||||
## v3.2.0
|
||||
|
||||
* Fullscreen fixes (thanks @friday)
|
||||
* Menu fix for if speed not in config
|
||||
* Menu z-index fix (thanks @danielsarin)
|
||||
* i18n fix for missing "Normal" string (thanks @danielsarin)
|
||||
* Safer check for active caption (thanks @Antonio-Laguna)
|
||||
* Add custom property fallback (thanks @friday)
|
||||
* Fixed bug for captions with no srclang and labels and improved logic (fixes #875)
|
||||
* Fix for `playing` false positive (fixes #898)
|
||||
* Fix for IE issue with navigator.language (thanks @nicolasthy) (fixes #893)
|
||||
* Fix for Vimeo controls missing on iOS (thanks @verde-io) (fixes #807)
|
||||
* Fix for double vimeo caption rendering (fixes #877)
|
||||
|
||||
## v3.1.0
|
||||
|
||||
* Styling fixes
|
||||
|
||||
## v3.1.0-beta.2
|
||||
|
||||
* YouTube playback speed fixes
|
||||
|
||||
## v3.1.0-beta.1
|
||||
|
||||
* HTML5 quality selection
|
||||
* Improvements to the YouTube quality selection
|
||||
|
||||
## v3.0.11
|
||||
|
||||
* Muted and autoplay fixes
|
||||
* Small bug fixes from Sentry logs
|
||||
|
||||
## v3.0.10
|
||||
|
||||
* Docs fix
|
||||
|
||||
+3
-1
@@ -59,6 +59,7 @@ i18n: {
|
||||
captions: 'Captions',
|
||||
settings: 'Settings',
|
||||
speed: 'Speed',
|
||||
normal: 'Normal',
|
||||
quality: 'Quality',
|
||||
loop: 'Loop',
|
||||
start: 'Start',
|
||||
@@ -120,7 +121,8 @@ const controls = `
|
||||
<progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress>
|
||||
<span role="tooltip" class="plyr__tooltip">00:00</span>
|
||||
</div>
|
||||
<div class="plyr__time">00:00</div>
|
||||
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
|
||||
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
|
||||
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
|
||||
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
|
||||
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+101
-22
@@ -245,7 +245,13 @@ function objectFrozen(obj) {
|
||||
}
|
||||
|
||||
function truncate(str, max) {
|
||||
return !max || str.length <= max ? str : str.substr(0, max) + '\u2026';
|
||||
if (typeof max !== 'number') {
|
||||
throw new Error('2nd argument to `truncate` function should be a number');
|
||||
}
|
||||
if (typeof str !== 'string' || max === 0) {
|
||||
return str;
|
||||
}
|
||||
return str.length <= max ? str : str.substr(0, max) + '\u2026';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -544,10 +550,9 @@ function jsonSize(value) {
|
||||
}
|
||||
|
||||
function serializeValue(value) {
|
||||
var maxLength = 40;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value.length <= maxLength ? value : value.substr(0, maxLength - 1) + '\u2026';
|
||||
var maxLength = 40;
|
||||
return truncate(value, maxLength);
|
||||
} else if (
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean' ||
|
||||
@@ -1777,7 +1782,7 @@ Raven.prototype = {
|
||||
// webpack (using a build step causes webpack #1617). Grunt verifies that
|
||||
// this value matches package.json during build.
|
||||
// See: https://github.com/getsentry/raven-js/issues/465
|
||||
VERSION: '3.24.0',
|
||||
VERSION: '3.24.2',
|
||||
|
||||
debug: false,
|
||||
|
||||
@@ -2066,7 +2071,11 @@ Raven.prototype = {
|
||||
*/
|
||||
_promiseRejectionHandler: function(event) {
|
||||
this._logDebug('debug', 'Raven caught unhandled promise rejection:', event);
|
||||
this.captureException(event.reason);
|
||||
this.captureException(event.reason, {
|
||||
extra: {
|
||||
unhandledPromiseRejection: true
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -2207,6 +2216,14 @@ Raven.prototype = {
|
||||
|
||||
// stack[0] is `throw new Error(msg)` call itself, we are interested in the frame that was just before that, stack[1]
|
||||
var initialCall = isArray$1(stack.stack) && stack.stack[1];
|
||||
|
||||
// if stack[1] is `Raven.captureException`, it means that someone passed a string to it and we redirected that call
|
||||
// to be handled by `captureMessage`, thus `initialCall` is the 3rd one, not 2nd
|
||||
// initialCall => captureException(string) => captureMessage(string)
|
||||
if (initialCall && initialCall.func === 'Raven.captureException') {
|
||||
initialCall = stack.stack[2];
|
||||
}
|
||||
|
||||
var fileurl = (initialCall && initialCall.url) || '';
|
||||
|
||||
if (
|
||||
@@ -3004,17 +3021,30 @@ Raven.prototype = {
|
||||
status_code: null
|
||||
};
|
||||
|
||||
return origFetch.apply(this, args).then(function(response) {
|
||||
fetchData.status_code = response.status;
|
||||
return origFetch
|
||||
.apply(this, args)
|
||||
.then(function(response) {
|
||||
fetchData.status_code = response.status;
|
||||
|
||||
self.captureBreadcrumb({
|
||||
type: 'http',
|
||||
category: 'fetch',
|
||||
data: fetchData
|
||||
self.captureBreadcrumb({
|
||||
type: 'http',
|
||||
category: 'fetch',
|
||||
data: fetchData
|
||||
});
|
||||
|
||||
return response;
|
||||
})
|
||||
['catch'](function(err) {
|
||||
// if there is an error performing the request
|
||||
self.captureBreadcrumb({
|
||||
type: 'http',
|
||||
category: 'fetch',
|
||||
data: fetchData,
|
||||
level: 'error'
|
||||
});
|
||||
|
||||
throw err;
|
||||
});
|
||||
|
||||
return response;
|
||||
});
|
||||
};
|
||||
},
|
||||
wrappedBuiltIns
|
||||
@@ -3027,7 +3057,7 @@ Raven.prototype = {
|
||||
if (_document.addEventListener) {
|
||||
_document.addEventListener('click', self._breadcrumbEventHandler('click'), false);
|
||||
_document.addEventListener('keypress', self._keypressEventHandler(), false);
|
||||
} else {
|
||||
} else if (_document.attachEvent) {
|
||||
// IE8 Compatibility
|
||||
_document.attachEvent('onclick', self._breadcrumbEventHandler('click'));
|
||||
_document.attachEvent('onkeypress', self._keypressEventHandler());
|
||||
@@ -3750,7 +3780,11 @@ Raven.prototype = {
|
||||
},
|
||||
|
||||
_logDebug: function(level) {
|
||||
if (this._originalConsoleMethods[level] && this.debug) {
|
||||
// We allow `Raven.debug` and `Raven.config(DSN, { debug: true })` to not make backward incompatible API change
|
||||
if (
|
||||
this._originalConsoleMethods[level] &&
|
||||
(this.debug || this._globalOptions.debug)
|
||||
) {
|
||||
// In IE<10 console methods do not have their own 'apply' method
|
||||
Function.prototype.apply.call(
|
||||
this._originalConsoleMethods[level],
|
||||
@@ -3823,11 +3857,11 @@ var singleton = Raven$1;
|
||||
* const someAppReporter = new Raven.Client();
|
||||
* const someOtherAppReporter = new Raven.Client();
|
||||
*
|
||||
* someAppReporter('__DSN__', {
|
||||
* someAppReporter.config('__DSN__', {
|
||||
* ...config goes here
|
||||
* });
|
||||
*
|
||||
* someOtherAppReporter('__OTHER_DSN__', {
|
||||
* someOtherAppReporter.config('__OTHER_DSN__', {
|
||||
* ...config goes here
|
||||
* });
|
||||
*
|
||||
@@ -3914,6 +3948,39 @@ singleton.Client = Client;
|
||||
'airplay',
|
||||
'fullscreen',
|
||||
], */
|
||||
/* i18n: {
|
||||
restart: '重新開始',
|
||||
rewind: '快退{seektime}秒',
|
||||
play: '播放',
|
||||
pause: '暫停',
|
||||
fastForward: '快進{seektime}秒',
|
||||
seek: '尋求',
|
||||
played: '發揮',
|
||||
buffered: '緩衝的',
|
||||
currentTime: '當前時間戳',
|
||||
duration: '長短',
|
||||
volume: '音量',
|
||||
mute: '靜音',
|
||||
unmute: '取消靜音',
|
||||
enableCaptions: '開啟字幕',
|
||||
disableCaptions: '關閉字幕',
|
||||
enterFullscreen: '進入全螢幕',
|
||||
exitFullscreen: '退出全螢幕',
|
||||
frameTitle: '球員為{title}',
|
||||
captions: '字幕',
|
||||
settings: '設定',
|
||||
speed: '速度',
|
||||
normal: '正常',
|
||||
quality: '質量',
|
||||
loop: '循環',
|
||||
start: 'Start',
|
||||
end: 'End',
|
||||
all: 'All',
|
||||
reset: '重啟',
|
||||
disabled: '殘',
|
||||
enabled: '啟用',
|
||||
advertisement: '廣告',
|
||||
}, */
|
||||
captions: {
|
||||
active: true
|
||||
},
|
||||
@@ -3960,8 +4027,21 @@ singleton.Client = Client;
|
||||
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'
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 576
|
||||
}, {
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 720
|
||||
}, {
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 1080
|
||||
}, {
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 1440
|
||||
}],
|
||||
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
|
||||
tracks: [{
|
||||
@@ -3998,7 +4078,6 @@ singleton.Client = Client;
|
||||
case types.youtube:
|
||||
player.source = {
|
||||
type: 'video',
|
||||
title: 'View From A Blue Moon',
|
||||
sources: [{
|
||||
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
||||
provider: 'youtube'
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+7
-1
@@ -6,8 +6,14 @@
|
||||
<title>Doh. Looks like something went wrong.</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico">
|
||||
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png">
|
||||
|
||||
<!-- Docs styles -->
|
||||
<link rel="stylesheet" href="dist/error.css">
|
||||
<link rel="stylesheet" href="dist/error.css?v=2">
|
||||
|
||||
<!-- Preload -->
|
||||
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
|
||||
|
||||
+11
-8
@@ -27,7 +27,7 @@
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
|
||||
<!-- Docs styles -->
|
||||
<link rel="stylesheet" href="dist/demo.css">
|
||||
<link rel="stylesheet" href="dist/demo.css?v=2">
|
||||
|
||||
<!-- Preload -->
|
||||
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
|
||||
@@ -93,16 +93,18 @@
|
||||
<main>
|
||||
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
|
||||
<!-- Video files -->
|
||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" type="video/mp4">
|
||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.webm" type="video/webm">
|
||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
|
||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
|
||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
|
||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4" type="video/mp4" size="1440">
|
||||
|
||||
<!-- Text track file -->
|
||||
<!-- Caption files -->
|
||||
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
|
||||
default>
|
||||
<track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
|
||||
|
||||
<!-- Fallback for browsers that don't support the <video> element -->
|
||||
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" download>Download</a>
|
||||
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
|
||||
</video>
|
||||
|
||||
<ul>
|
||||
@@ -112,7 +114,7 @@
|
||||
<title>HTML5</title>
|
||||
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
|
||||
</svg>
|
||||
<a href="http://viewfromabluemoon.com/" target="_blank">View From A Blue Moon</a> © Brainfarm
|
||||
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank">View From A Blue Moon</a> © Brainfarm
|
||||
</small>
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--audio" hidden>
|
||||
@@ -139,7 +141,7 @@
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--vimeo" hidden>
|
||||
<small>
|
||||
<a href="https://vimeo.com/ondemand/viewfromabluemoon4k" target="_blank">View From A Blue Moon</a> on
|
||||
<a href="https://vimeo.com/76979871" target="_blank">The New Vimeo Player</a> on
|
||||
<span class="color--vimeo">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>Vimeo</title>
|
||||
@@ -169,7 +171,8 @@
|
||||
</aside>
|
||||
|
||||
<!-- Polyfills -->
|
||||
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Plyr core script -->
|
||||
<script src="../dist/plyr.js" crossorigin="anonymous"></script>
|
||||
|
||||
+55
-5
@@ -74,6 +74,39 @@ import Raven from 'raven-js';
|
||||
'airplay',
|
||||
'fullscreen',
|
||||
], */
|
||||
/* i18n: {
|
||||
restart: '重新開始',
|
||||
rewind: '快退{seektime}秒',
|
||||
play: '播放',
|
||||
pause: '暫停',
|
||||
fastForward: '快進{seektime}秒',
|
||||
seek: '尋求',
|
||||
played: '發揮',
|
||||
buffered: '緩衝的',
|
||||
currentTime: '當前時間戳',
|
||||
duration: '長短',
|
||||
volume: '音量',
|
||||
mute: '靜音',
|
||||
unmute: '取消靜音',
|
||||
enableCaptions: '開啟字幕',
|
||||
disableCaptions: '關閉字幕',
|
||||
enterFullscreen: '進入全螢幕',
|
||||
exitFullscreen: '退出全螢幕',
|
||||
frameTitle: '球員為{title}',
|
||||
captions: '字幕',
|
||||
settings: '設定',
|
||||
speed: '速度',
|
||||
normal: '正常',
|
||||
quality: '質量',
|
||||
loop: '循環',
|
||||
start: 'Start',
|
||||
end: 'End',
|
||||
all: 'All',
|
||||
reset: '重啟',
|
||||
disabled: '殘',
|
||||
enabled: '啟用',
|
||||
advertisement: '廣告',
|
||||
}, */
|
||||
captions: {
|
||||
active: true,
|
||||
},
|
||||
@@ -119,10 +152,28 @@ import Raven from 'raven-js';
|
||||
player.source = {
|
||||
type: 'video',
|
||||
title: 'View From A Blue Moon',
|
||||
sources: [{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4',
|
||||
type: 'video/mp4',
|
||||
}],
|
||||
sources: [
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 576,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 720,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 1080,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 1440,
|
||||
},
|
||||
],
|
||||
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
|
||||
tracks: [
|
||||
{
|
||||
@@ -164,7 +215,6 @@ import Raven from 'raven-js';
|
||||
case types.youtube:
|
||||
player.source = {
|
||||
type: 'video',
|
||||
title: 'View From A Blue Moon',
|
||||
sources: [{
|
||||
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
||||
provider: 'youtube',
|
||||
|
||||
@@ -3,12 +3,6 @@
|
||||
// ==========================================================================
|
||||
@charset 'UTF-8';
|
||||
|
||||
// Libs
|
||||
@import '../lib/fontface';
|
||||
@import '../lib/mixins';
|
||||
@import '../lib/normalize';
|
||||
@import '../lib/reset';
|
||||
|
||||
// Settings
|
||||
@import '../settings/colors';
|
||||
@import '../settings/cosmetic';
|
||||
@@ -17,6 +11,12 @@
|
||||
@import '../settings/spacing';
|
||||
@import '../settings/type';
|
||||
|
||||
// Libs
|
||||
@import '../lib/fontface';
|
||||
@import '../lib/mixins';
|
||||
@import '../lib/normalize';
|
||||
@import '../lib/reset';
|
||||
|
||||
// Layout
|
||||
@import '../layout/error';
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ video {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
// Style full supported player
|
||||
|
||||
@@ -16,3 +16,4 @@ $plyr-font-size-captions-base: $plyr-font-size-base;
|
||||
$plyr-font-size-captions-small: $plyr-font-size-small;
|
||||
$plyr-font-size-captions-medium: 18px;
|
||||
$plyr-font-size-captions-large: 21px;
|
||||
$plyr-font-size-menu: $plyr-font-size-base;
|
||||
|
||||
@@ -6,5 +6,6 @@ h1 {
|
||||
@include font-size($font-size-h1);
|
||||
font-weight: $font-weight-bold;
|
||||
letter-spacing: $letter-spacing-headings;
|
||||
margin: 0 0 ($spacing-base / 2);
|
||||
line-height: 1.2;
|
||||
margin: 0 0 $spacing-base;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2603
-2079
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2603
-2079
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+7
-6
@@ -129,7 +129,7 @@ const build = {
|
||||
tasks.js.push(name);
|
||||
const { output } = paths[bundle];
|
||||
|
||||
gulp.task(name, () =>
|
||||
return gulp.task(name, () =>
|
||||
gulp
|
||||
.src(bundles[bundle].js[key])
|
||||
.pipe(sourcemaps.init())
|
||||
@@ -162,7 +162,7 @@ const build = {
|
||||
const name = `sass:${key}`;
|
||||
tasks.sass.push(name);
|
||||
|
||||
gulp.task(name, () =>
|
||||
return gulp.task(name, () =>
|
||||
gulp
|
||||
.src(bundles[bundle].sass[key])
|
||||
.pipe(sass())
|
||||
@@ -180,7 +180,7 @@ const build = {
|
||||
tasks.sprite.push(name);
|
||||
|
||||
// Process Icons
|
||||
gulp.task(name, () =>
|
||||
return gulp.task(name, () =>
|
||||
gulp
|
||||
.src(paths[bundle].src.sprite)
|
||||
.pipe(
|
||||
@@ -287,7 +287,8 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
||||
'plyr.polyfilled.js',
|
||||
'defaults.js',
|
||||
];
|
||||
gulp
|
||||
|
||||
return gulp
|
||||
.src(files.map(file => path.join(root, `src/js/${file}`)))
|
||||
.pipe(replace(semver, `v${version}`))
|
||||
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
|
||||
@@ -406,7 +407,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
||||
});
|
||||
|
||||
// Do everything
|
||||
gulp.task('publish', () => {
|
||||
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo');
|
||||
gulp.task('publish', callback => {
|
||||
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo', callback);
|
||||
});
|
||||
}
|
||||
|
||||
+13
-12
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plyr",
|
||||
"version": "3.0.10",
|
||||
"version": "3.3.6",
|
||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||
"homepage": "https://plyr.io",
|
||||
"main": "./dist/plyr.js",
|
||||
@@ -8,27 +8,27 @@
|
||||
"sass": "./src/sass/plyr.scss",
|
||||
"style": "./dist/plyr.css",
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.2.2",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"del": "^3.0.0",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-plugin-import": "^2.9.0",
|
||||
"eslint-plugin-import": "^2.11.0",
|
||||
"git-branch": "^2.0.1",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-autoprefixer": "^5.0.0",
|
||||
"gulp-better-rollup": "^3.1.0",
|
||||
"gulp-clean-css": "^3.9.3",
|
||||
"gulp-clean-css": "^3.9.4",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-filter": "^5.1.0",
|
||||
"gulp-open": "^3.0.1",
|
||||
"gulp-rename": "^1.2.2",
|
||||
"gulp-replace": "^0.6.1",
|
||||
"gulp-s3": "^0.11.0",
|
||||
"gulp-sass": "^3.2.1",
|
||||
"gulp-sass": "^4.0.1",
|
||||
"gulp-size": "^3.0.0",
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"gulp-svgmin": "^1.2.4",
|
||||
@@ -37,16 +37,16 @@
|
||||
"gulp-util": "^3.0.8",
|
||||
"prettier-eslint": "^8.8.1",
|
||||
"prettier-stylelint": "^0.4.2",
|
||||
"rollup-plugin-babel": "^3.0.3",
|
||||
"rollup-plugin-commonjs": "^9.1.0",
|
||||
"rollup-plugin-babel": "^3.0.4",
|
||||
"rollup-plugin-commonjs": "^9.1.3",
|
||||
"rollup-plugin-node-resolve": "^3.3.0",
|
||||
"run-sequence": "^2.2.1",
|
||||
"stylelint": "^9.1.3",
|
||||
"stylelint-config-prettier": "^3.0.4",
|
||||
"stylelint": "^9.2.0",
|
||||
"stylelint-config-prettier": "^3.2.0",
|
||||
"stylelint-config-recommended": "^2.1.0",
|
||||
"stylelint-config-sass-guidelines": "^5.0.0",
|
||||
"stylelint-order": "^0.8.1",
|
||||
"stylelint-scss": "^2.5.0",
|
||||
"stylelint-scss": "^3.1.0",
|
||||
"stylelint-selector-bem-pattern": "^2.0.0"
|
||||
},
|
||||
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
|
||||
@@ -69,6 +69,7 @@
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"custom-event-polyfill": "^0.3.0",
|
||||
"loadjs": "^3.5.4",
|
||||
"raven-js": "^3.24.0"
|
||||
"npm": "^6.0.0",
|
||||
"raven-js": "^3.24.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +39,13 @@ Check out the [changelog](changelog.md) to see what's new with Plyr.
|
||||
|
||||
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
|
||||
|
||||
| Type | Maintainer | Link |
|
||||
| --------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| WordPress | Ryan Anthony Drake ([@iamryandrake](https://github.com/iamryandrake)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
|
||||
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
|
||||
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
|
||||
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
|
||||
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
|
||||
| Type | Maintainer | Link |
|
||||
| --------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
|
||||
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
|
||||
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
|
||||
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
|
||||
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
|
||||
|
||||
## Quick setup
|
||||
|
||||
@@ -128,7 +128,7 @@ See [initialising](#initialising) for more information on advanced setups.
|
||||
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.0.10/plyr.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.3.6/plyr.js"></script>
|
||||
```
|
||||
|
||||
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
|
||||
@@ -144,13 +144,13 @@ Include the `plyr.css` stylsheet into your `<head>`
|
||||
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.0.10/plyr.css">
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.6/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.10/plyr.svg`.
|
||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.6/plyr.svg`.
|
||||
|
||||
## Ads
|
||||
|
||||
@@ -210,7 +210,7 @@ You can specify a range of arguments for the constructor to use:
|
||||
* 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.
|
||||
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
|
||||
|
||||
Here's some examples
|
||||
|
||||
@@ -226,20 +226,32 @@ Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElemen
|
||||
const player = new Plyr(document.getElementById('player'));
|
||||
```
|
||||
|
||||
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList):
|
||||
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) (see note below):
|
||||
|
||||
```javascript
|
||||
const player = new Plyr(document.querySelectorAll('.js-player'));
|
||||
```
|
||||
|
||||
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds
|
||||
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds.
|
||||
|
||||
##### Setting up multiple players
|
||||
|
||||
You have two choices here. You can either use a simple array loop to map the constructor:
|
||||
|
||||
```javascript
|
||||
const players = Array.from(document.querySelectorAll('.js-player')).map(player => new Plyr(player));
|
||||
const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p));
|
||||
```
|
||||
|
||||
...or use a static method where you can pass a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of elements:
|
||||
|
||||
```javascript
|
||||
const players = Plyr.setup('.js-player');
|
||||
```
|
||||
|
||||
Both options will also return an array of instances in the order of they were in the DOM for the string selector or the source NodeList or Array.
|
||||
|
||||
##### Passing options
|
||||
|
||||
The second argument for the constructor is the [options](#options) object:
|
||||
|
||||
```javascript
|
||||
@@ -248,7 +260,7 @@ const player = new Plyr('#player', {
|
||||
});
|
||||
```
|
||||
|
||||
The constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
|
||||
In all cases, the constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
|
||||
|
||||
#### Options
|
||||
|
||||
@@ -279,7 +291,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
||||
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
|
||||
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
|
||||
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
|
||||
| `showPosterOnEnd` | Boolean | false | This will restore and _reload_ HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution. |
|
||||
| `resetOnEnd` | Boolean | false | Reset the playback to the start once playback is complete. |
|
||||
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
|
||||
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
|
||||
| `duration` | Number | `null` | Specify a custom duration for media. |
|
||||
@@ -374,8 +386,9 @@ player.fullscreen.active; // false;
|
||||
| -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. |
|
||||
| `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. |
|
||||
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
|
||||
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
|
||||
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
|
||||
| `stopped` | ✓ | - | Returns a boolean indicating if the current player is stopped. |
|
||||
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
|
||||
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
|
||||
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
|
||||
@@ -388,7 +401,7 @@ player.fullscreen.active; // false;
|
||||
| `quality`¹ | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
|
||||
| `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`² | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
|
||||
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
|
||||
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
|
||||
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. |
|
||||
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
|
||||
@@ -599,6 +612,8 @@ Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's
|
||||
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
|
||||
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
|
||||
|
||||
_Note_: These need updating to use the new v3 syntax but would still work.
|
||||
|
||||
## Fullscreen
|
||||
|
||||
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
|
||||
@@ -607,19 +622,20 @@ Fullscreen in Plyr is supported by all browsers that [currently support it](http
|
||||
|
||||
Plyr supports the last 2 versions of most _modern_ browsers.
|
||||
|
||||
| Browser | Supported |
|
||||
| ------------- | --------- |
|
||||
| Safari | ✓ |
|
||||
| Mobile Safari | ✓¹ |
|
||||
| Firefox | ✓ |
|
||||
| Chrome | ✓ |
|
||||
| Opera | ✓ |
|
||||
| Edge | ✓ |
|
||||
| IE11 | ✓ |
|
||||
| IE10 | ✓² |
|
||||
| Browser | Supported |
|
||||
| ------------- | ------------- |
|
||||
| Safari | ✓ |
|
||||
| Mobile Safari | ✓¹ |
|
||||
| Firefox | ✓ |
|
||||
| Chrome | ✓ |
|
||||
| Opera | ✓ |
|
||||
| Edge | ✓ |
|
||||
| IE11 | ✓³ |
|
||||
| IE10 | ✓²³ |
|
||||
|
||||
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
|
||||
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options))
|
||||
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)).
|
||||
3. Polyfills required. See below.
|
||||
|
||||
### Polyfills
|
||||
|
||||
@@ -668,8 +684,10 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
|
||||
|
||||
## Donate
|
||||
|
||||
Plyr costs money to run, not only my time - I donate that for free but domains, hosting and more. Any help is appreciated...
|
||||
[Donate to support Plyr](https://www.paypal.me/pottsy/20usd)
|
||||
Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated...
|
||||
|
||||
* [Donate via Patron](https://www.patreon.com/plyr)
|
||||
* [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
|
||||
|
||||
## Mentions
|
||||
|
||||
@@ -707,10 +725,14 @@ Credit to the PayPal HTML5 Video player from which Plyr's caption functionality
|
||||
|
||||
## Thanks
|
||||
|
||||
[](https://www.fastly.com/)
|
||||
[](https://www.fastly.com/)
|
||||
|
||||
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.
|
||||
|
||||
[](https://sentry.io/)
|
||||
|
||||
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
|
||||
|
||||
## Copyright and License
|
||||
|
||||
[The MIT license](license.md)
|
||||
|
||||
+46
-7
@@ -3,9 +3,10 @@
|
||||
// TODO: Create as class
|
||||
// ==========================================================================
|
||||
|
||||
import controls from './controls';
|
||||
import i18n from './i18n';
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
import controls from './controls';
|
||||
|
||||
const captions = {
|
||||
// Setup captions
|
||||
@@ -46,6 +47,7 @@ const captions = {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject the container
|
||||
if (!utils.is.element(this.elements.captions)) {
|
||||
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
|
||||
@@ -148,7 +150,49 @@ const captions = {
|
||||
|
||||
// Get the current track for the current language
|
||||
getCurrentTrack() {
|
||||
return captions.getTracks.call(this).find(track => track.language.toLowerCase() === this.language);
|
||||
const tracks = captions.getTracks.call(this);
|
||||
|
||||
if (!tracks.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get track based on current language
|
||||
let track = tracks.find(track => track.language.toLowerCase() === this.language);
|
||||
|
||||
// Get the <track> with default attribute
|
||||
if (!track) {
|
||||
track = utils.getElement.call(this, 'track[default]');
|
||||
}
|
||||
|
||||
// Get the first track
|
||||
if (!track) {
|
||||
[track] = tracks;
|
||||
}
|
||||
|
||||
return track;
|
||||
},
|
||||
|
||||
// Get UI label for track
|
||||
getLabel(track) {
|
||||
let currentTrack = track;
|
||||
|
||||
if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) {
|
||||
currentTrack = captions.getCurrentTrack.call(this);
|
||||
}
|
||||
|
||||
if (utils.is.track(currentTrack)) {
|
||||
if (!utils.is.empty(currentTrack.label)) {
|
||||
return currentTrack.label;
|
||||
}
|
||||
|
||||
if (!utils.is.empty(currentTrack.language)) {
|
||||
return track.language.toUpperCase();
|
||||
}
|
||||
|
||||
return i18n.get('enabled', this.config);
|
||||
}
|
||||
|
||||
return i18n.get('disabled', this.config);
|
||||
},
|
||||
|
||||
// Display active caption if it contains text
|
||||
@@ -206,11 +250,6 @@ const captions = {
|
||||
|
||||
// Display captions container and button (for initialization)
|
||||
show() {
|
||||
// If there's no caption toggle, bail
|
||||
if (!utils.is.element(this.elements.buttons.captions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to load the value from storage
|
||||
let active = this.storage.get('captions');
|
||||
|
||||
|
||||
Vendored
+222
-140
@@ -2,11 +2,12 @@
|
||||
// Plyr controls
|
||||
// ==========================================================================
|
||||
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
import ui from './ui';
|
||||
import i18n from './i18n';
|
||||
import captions from './captions';
|
||||
import html5 from './html5';
|
||||
import i18n from './i18n';
|
||||
import support from './support';
|
||||
import ui from './ui';
|
||||
import utils from './utils';
|
||||
|
||||
// Sniff out the browser
|
||||
const browser = utils.getBrowser();
|
||||
@@ -14,11 +15,6 @@ const browser = utils.getBrowser();
|
||||
const controls = {
|
||||
// Webkit polyfill for lower fill range
|
||||
updateRangeFill(target) {
|
||||
// WebKit only
|
||||
if (!browser.isWebkit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get range from event if event passed
|
||||
const range = utils.is.event(target) ? target.target : target;
|
||||
|
||||
@@ -27,23 +23,88 @@ const controls = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set aria value for https://github.com/sampotts/plyr/issues/905
|
||||
range.setAttribute('aria-valuenow', range.value);
|
||||
|
||||
// WebKit only
|
||||
if (!browser.isWebkit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set CSS custom property
|
||||
range.style.setProperty('--value', `${range.value / range.max * 100}%`);
|
||||
},
|
||||
|
||||
// Get icon URL
|
||||
getIconUrl() {
|
||||
const url = new URL(this.config.iconUrl, window.location);
|
||||
const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody);
|
||||
|
||||
return {
|
||||
url: this.config.iconUrl,
|
||||
absolute: this.config.iconUrl.indexOf('http') === 0 || (browser.isIE && !window.svg4everybody),
|
||||
cors,
|
||||
};
|
||||
},
|
||||
|
||||
// Find the UI controls and store references in custom controls
|
||||
// TODO: Allow settings menus with custom controls
|
||||
findElements() {
|
||||
try {
|
||||
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
|
||||
|
||||
// Buttons
|
||||
this.elements.buttons = {
|
||||
play: utils.getElements.call(this, this.config.selectors.buttons.play),
|
||||
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
|
||||
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
|
||||
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
|
||||
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
|
||||
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
|
||||
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
|
||||
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
|
||||
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
|
||||
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
|
||||
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
|
||||
};
|
||||
|
||||
// Progress
|
||||
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
|
||||
|
||||
// Inputs
|
||||
this.elements.inputs = {
|
||||
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
|
||||
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
|
||||
};
|
||||
|
||||
// Display
|
||||
this.elements.display = {
|
||||
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
|
||||
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
|
||||
duration: utils.getElement.call(this, this.config.selectors.display.duration),
|
||||
};
|
||||
|
||||
// Seek tooltip
|
||||
if (utils.is.element(this.elements.progress)) {
|
||||
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Log it
|
||||
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
|
||||
|
||||
// Restore native video controls
|
||||
this.toggleNativeControls(true);
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Create <svg> icon
|
||||
createIcon(type, attributes) {
|
||||
const namespace = 'http://www.w3.org/2000/svg';
|
||||
const iconUrl = controls.getIconUrl.call(this);
|
||||
const iconPath = `${!iconUrl.absolute ? iconUrl.url : ''}#${this.config.iconPrefix}`;
|
||||
const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
|
||||
|
||||
// Create <svg>
|
||||
const icon = document.createElementNS(namespace, 'svg');
|
||||
@@ -51,6 +112,7 @@ const controls = {
|
||||
icon,
|
||||
utils.extend(attributes, {
|
||||
role: 'presentation',
|
||||
focusable: 'false',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -205,7 +267,6 @@ const controls = {
|
||||
|
||||
// Add aria attributes
|
||||
attributes['aria-pressed'] = false;
|
||||
attributes['aria-label'] = i18n.get(label, this.config);
|
||||
} else {
|
||||
button.appendChild(controls.createIcon.call(this, icon));
|
||||
button.appendChild(controls.createLabel.call(this, label));
|
||||
@@ -237,6 +298,7 @@ const controls = {
|
||||
'label',
|
||||
{
|
||||
for: attributes.id,
|
||||
id: `${attributes.id}-label`,
|
||||
class: this.config.classNames.hidden,
|
||||
},
|
||||
i18n.get(type, this.config),
|
||||
@@ -254,6 +316,12 @@ const controls = {
|
||||
step: 0.01,
|
||||
value: 0,
|
||||
autocomplete: 'off',
|
||||
// A11y fixes for https://github.com/sampotts/plyr/issues/905
|
||||
role: 'slider',
|
||||
'aria-labelledby': `${attributes.id}-label`,
|
||||
'aria-valuemin': 0,
|
||||
'aria-valuemax': 100,
|
||||
'aria-valuenow': 0,
|
||||
},
|
||||
attributes,
|
||||
),
|
||||
@@ -280,6 +348,8 @@ const controls = {
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: 0,
|
||||
role: 'presentation',
|
||||
'aria-hidden': true,
|
||||
},
|
||||
attributes,
|
||||
),
|
||||
@@ -313,22 +383,14 @@ const controls = {
|
||||
|
||||
// Create time display
|
||||
createTime(type) {
|
||||
const container = utils.createElement('div', {
|
||||
class: 'plyr__time',
|
||||
});
|
||||
const attributes = utils.getAttributesFromSelector(this.config.selectors.display[type]);
|
||||
|
||||
container.appendChild(
|
||||
utils.createElement(
|
||||
'span',
|
||||
{
|
||||
class: this.config.classNames.hidden,
|
||||
},
|
||||
i18n.get(type, this.config),
|
||||
),
|
||||
);
|
||||
|
||||
container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.display[type]), '00:00'));
|
||||
const container = utils.createElement('div', utils.extend(attributes, {
|
||||
class: `plyr__time ${attributes.class}`,
|
||||
'aria-label': i18n.get(type, this.config),
|
||||
}), '00:00');
|
||||
|
||||
// Reference for updates
|
||||
this.elements.display[type] = container;
|
||||
|
||||
return container;
|
||||
@@ -353,7 +415,7 @@ const controls = {
|
||||
}),
|
||||
);
|
||||
|
||||
const faux = utils.createElement('span', { 'aria-hidden': true });
|
||||
const faux = utils.createElement('span', { hidden: '' });
|
||||
|
||||
label.appendChild(radio);
|
||||
label.appendChild(faux);
|
||||
@@ -428,15 +490,11 @@ const controls = {
|
||||
|
||||
// Hide/show a tab
|
||||
toggleTab(setting, toggle) {
|
||||
const tab = this.elements.settings.tabs[setting];
|
||||
const pane = this.elements.settings.panes[setting];
|
||||
|
||||
utils.toggleHidden(tab, !toggle);
|
||||
utils.toggleHidden(pane, !toggle);
|
||||
utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
|
||||
},
|
||||
|
||||
// Set the YouTube quality menu
|
||||
// TODO: Support for HTML5
|
||||
// Set the quality menu
|
||||
// TODO: Vimeo support
|
||||
setQualityMenu(options) {
|
||||
// Menu required
|
||||
if (!utils.is.element(this.elements.settings.panes.quality)) {
|
||||
@@ -449,14 +507,15 @@ const controls = {
|
||||
// Set options if passed and filter based on config
|
||||
if (utils.is.array(options)) {
|
||||
this.options.quality = options.filter(quality => this.config.quality.options.includes(quality));
|
||||
} else {
|
||||
this.options.quality = this.config.quality.options;
|
||||
}
|
||||
|
||||
// Toggle the pane and tab
|
||||
const toggle = !utils.is.empty(this.options.quality) && this.isYouTube;
|
||||
const toggle = !utils.is.empty(this.options.quality) && this.options.quality.length > 1;
|
||||
controls.toggleTab.call(this, type, toggle);
|
||||
|
||||
// Check if we need to toggle the parent
|
||||
controls.checkMenu.call(this);
|
||||
|
||||
// If we're hiding, nothing more to do
|
||||
if (!toggle) {
|
||||
return;
|
||||
@@ -470,20 +529,19 @@ const controls = {
|
||||
let label = '';
|
||||
|
||||
switch (quality) {
|
||||
case 'hd2160':
|
||||
case 2160:
|
||||
label = '4K';
|
||||
break;
|
||||
|
||||
case 'hd1440':
|
||||
label = 'WQHD';
|
||||
break;
|
||||
|
||||
case 'hd1080':
|
||||
case 1440:
|
||||
case 1080:
|
||||
case 720:
|
||||
label = 'HD';
|
||||
break;
|
||||
|
||||
case 'hd720':
|
||||
label = 'HD';
|
||||
case 576:
|
||||
case 480:
|
||||
label = 'SD';
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -497,9 +555,16 @@ const controls = {
|
||||
return controls.createBadge.call(this, label);
|
||||
};
|
||||
|
||||
this.options.quality.forEach(quality =>
|
||||
controls.createMenuItem.call(this, quality, list, type, controls.getLabel.call(this, 'quality', quality), getBadge(quality)),
|
||||
);
|
||||
// Sort options by the config and then render options
|
||||
this.options.quality
|
||||
.sort((a, b) => {
|
||||
const sorting = this.config.quality.options;
|
||||
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
|
||||
})
|
||||
.forEach(quality => {
|
||||
const label = controls.getLabel.call(this, 'quality', quality);
|
||||
controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality));
|
||||
});
|
||||
|
||||
controls.updateSetting.call(this, type, list);
|
||||
},
|
||||
@@ -509,34 +574,17 @@ const controls = {
|
||||
getLabel(setting, value) {
|
||||
switch (setting) {
|
||||
case 'speed':
|
||||
return value === 1 ? 'Normal' : `${value}×`;
|
||||
return value === 1 ? i18n.get('normal', this.config) : `${value}×`;
|
||||
|
||||
case 'quality':
|
||||
switch (value) {
|
||||
case 'hd2160':
|
||||
return '2160P';
|
||||
case 'hd1440':
|
||||
return '1440P';
|
||||
case 'hd1080':
|
||||
return '1080P';
|
||||
case 'hd720':
|
||||
return '720P';
|
||||
case 'large':
|
||||
return '480P';
|
||||
case 'medium':
|
||||
return '360P';
|
||||
case 'small':
|
||||
return '240P';
|
||||
case 'tiny':
|
||||
return 'Tiny';
|
||||
case 'default':
|
||||
return 'Auto';
|
||||
default:
|
||||
return value;
|
||||
if (utils.is.number(value)) {
|
||||
return `${value}p`;
|
||||
}
|
||||
|
||||
return utils.toTitleCase(value);
|
||||
|
||||
case 'captions':
|
||||
return controls.getLanguage.call(this);
|
||||
return captions.getLabel.call(this);
|
||||
|
||||
default:
|
||||
return null;
|
||||
@@ -544,18 +592,27 @@ const controls = {
|
||||
},
|
||||
|
||||
// Update the selected setting
|
||||
updateSetting(setting, container) {
|
||||
updateSetting(setting, container, input) {
|
||||
const pane = this.elements.settings.panes[setting];
|
||||
let value = null;
|
||||
let list = container;
|
||||
|
||||
switch (setting) {
|
||||
case 'captions':
|
||||
value = this.captions.active ? this.captions.language : i18n.get('disabled', this.config);
|
||||
if (this.captions.active) {
|
||||
if (this.options.captions.length > 2 || !this.options.captions.some(lang => lang === 'enabled')) {
|
||||
value = this.captions.language;
|
||||
} else {
|
||||
value = 'enabled';
|
||||
}
|
||||
} else {
|
||||
value = '';
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
value = this[setting];
|
||||
value = !utils.is.empty(input) ? input : this[setting];
|
||||
|
||||
// Get default
|
||||
if (utils.is.empty(value)) {
|
||||
@@ -563,7 +620,7 @@ const controls = {
|
||||
}
|
||||
|
||||
// Unsupported value
|
||||
if (!this.options[setting].includes(value)) {
|
||||
if (!utils.is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
|
||||
this.debug.warn(`Unsupported value of '${value}' for ${setting}`);
|
||||
return;
|
||||
}
|
||||
@@ -582,17 +639,19 @@ const controls = {
|
||||
list = pane && pane.querySelector('ul');
|
||||
}
|
||||
|
||||
// Update the label
|
||||
if (!utils.is.empty(value)) {
|
||||
const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`);
|
||||
label.innerHTML = controls.getLabel.call(this, setting, value);
|
||||
// If there's no list it means it's not been rendered...
|
||||
if (!utils.is.element(list)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the radio option
|
||||
// Update the label
|
||||
const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`);
|
||||
label.innerHTML = controls.getLabel.call(this, setting, value);
|
||||
|
||||
// Find the radio option and check it
|
||||
const target = list && list.querySelector(`input[value="${value}"]`);
|
||||
|
||||
if (utils.is.element(target)) {
|
||||
// Check it
|
||||
target.checked = true;
|
||||
}
|
||||
},
|
||||
@@ -643,21 +702,6 @@ const controls = {
|
||||
|
||||
// Get current selected caption language
|
||||
// TODO: rework this to user the getter in the API?
|
||||
getLanguage() {
|
||||
if (!this.supported.ui) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (support.textTracks && captions.getTracks.call(this).length && this.captions.active) {
|
||||
const currentTrack = captions.getCurrentTrack.call(this);
|
||||
|
||||
if (utils.is.track(currentTrack)) {
|
||||
return currentTrack.label;
|
||||
}
|
||||
}
|
||||
|
||||
return i18n.get('disabled', this.config);
|
||||
},
|
||||
|
||||
// Set a list of available captions languages
|
||||
setCaptionsMenu() {
|
||||
@@ -666,21 +710,24 @@ const controls = {
|
||||
const list = this.elements.settings.panes.captions.querySelector('ul');
|
||||
|
||||
// Toggle the pane and tab
|
||||
const hasTracks = captions.getTracks.call(this).length;
|
||||
controls.toggleTab.call(this, type, hasTracks);
|
||||
const toggle = captions.getTracks.call(this).length;
|
||||
controls.toggleTab.call(this, type, toggle);
|
||||
|
||||
// Empty the menu
|
||||
utils.emptyElement(list);
|
||||
|
||||
// Check if we need to toggle the parent
|
||||
controls.checkMenu.call(this);
|
||||
|
||||
// If there's no captions, bail
|
||||
if (!hasTracks) {
|
||||
if (!toggle) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-map the tracks into just the data we need
|
||||
const tracks = captions.getTracks.call(this).map(track => ({
|
||||
language: track.language,
|
||||
label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(),
|
||||
language: !utils.is.empty(track.language) ? track.language : 'enabled',
|
||||
label: captions.getLabel.call(this, track),
|
||||
}));
|
||||
|
||||
// Add the "Disabled" option to turn off captions
|
||||
@@ -696,12 +743,15 @@ const controls = {
|
||||
track.language,
|
||||
list,
|
||||
'language',
|
||||
track.label || track.language,
|
||||
controls.createBadge.call(this, track.language.toUpperCase()),
|
||||
track.label,
|
||||
track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null,
|
||||
track.language.toLowerCase() === this.captions.language.toLowerCase(),
|
||||
);
|
||||
});
|
||||
|
||||
// Store reference
|
||||
this.options.captions = tracks.map(track => track.language);
|
||||
|
||||
controls.updateSetting.call(this, type, list);
|
||||
},
|
||||
|
||||
@@ -720,7 +770,9 @@ const controls = {
|
||||
const type = 'speed';
|
||||
|
||||
// Set the speed options
|
||||
if (!utils.is.array(options)) {
|
||||
if (utils.is.array(options)) {
|
||||
this.options.speed = options;
|
||||
} else if (this.isHTML5 || this.isVimeo) {
|
||||
this.options.speed = [
|
||||
0.5,
|
||||
0.75,
|
||||
@@ -730,15 +782,13 @@ const controls = {
|
||||
1.75,
|
||||
2,
|
||||
];
|
||||
} else {
|
||||
this.options.speed = options;
|
||||
}
|
||||
|
||||
// Set options if passed and filter based on config
|
||||
this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
|
||||
|
||||
// Toggle the pane and tab
|
||||
const toggle = !utils.is.empty(this.options.speed);
|
||||
const toggle = !utils.is.empty(this.options.speed) && this.options.speed.length > 1;
|
||||
controls.toggleTab.call(this, type, toggle);
|
||||
|
||||
// Check if we need to toggle the parent
|
||||
@@ -752,25 +802,24 @@ const controls = {
|
||||
// Get the list to populate
|
||||
const list = this.elements.settings.panes.speed.querySelector('ul');
|
||||
|
||||
// Show the pane and tab
|
||||
utils.toggleHidden(this.elements.settings.tabs.speed, false);
|
||||
utils.toggleHidden(this.elements.settings.panes.speed, false);
|
||||
|
||||
// Empty the menu
|
||||
utils.emptyElement(list);
|
||||
|
||||
// Create items
|
||||
this.options.speed.forEach(speed => controls.createMenuItem.call(this, speed, list, type, controls.getLabel.call(this, 'speed', speed)));
|
||||
this.options.speed.forEach(speed => {
|
||||
const label = controls.getLabel.call(this, 'speed', speed);
|
||||
controls.createMenuItem.call(this, speed, list, type, label);
|
||||
});
|
||||
|
||||
controls.updateSetting.call(this, type, list);
|
||||
},
|
||||
|
||||
// Check if we need to hide/show the settings menu
|
||||
checkMenu() {
|
||||
const speedHidden = this.elements.settings.tabs.speed.getAttribute('hidden') !== null;
|
||||
const languageHidden = this.elements.settings.tabs.captions.getAttribute('hidden') !== null;
|
||||
const { tabs } = this.elements.settings;
|
||||
const visible = !utils.is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden);
|
||||
|
||||
utils.toggleHidden(this.elements.settings.menu, speedHidden && languageHidden);
|
||||
utils.toggleHidden(this.elements.settings.menu, !visible);
|
||||
},
|
||||
|
||||
// Show/hide menu
|
||||
@@ -783,7 +832,7 @@ const controls = {
|
||||
return;
|
||||
}
|
||||
|
||||
const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.getAttribute('aria-hidden') === 'true';
|
||||
const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.hasAttribute('hidden');
|
||||
|
||||
if (utils.is.event(event)) {
|
||||
const isMenuItem = utils.is.element(form) && form.contains(event.target);
|
||||
@@ -808,7 +857,7 @@ const controls = {
|
||||
}
|
||||
|
||||
if (utils.is.element(form)) {
|
||||
form.setAttribute('aria-hidden', !show);
|
||||
utils.toggleHidden(form, !show);
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.menu.open, show);
|
||||
|
||||
if (show) {
|
||||
@@ -824,7 +873,7 @@ const controls = {
|
||||
const clone = tab.cloneNode(true);
|
||||
clone.style.position = 'absolute';
|
||||
clone.style.opacity = 0;
|
||||
clone.setAttribute('aria-hidden', false);
|
||||
clone.removeAttribute('hidden');
|
||||
|
||||
// Prevent input's being unchecked due to the name being identical
|
||||
Array.from(clone.querySelectorAll('input[name]')).forEach(input => {
|
||||
@@ -849,11 +898,9 @@ const controls = {
|
||||
},
|
||||
|
||||
// Toggle Menu
|
||||
showTab(event) {
|
||||
showTab(target = '') {
|
||||
const { menu } = this.elements.settings;
|
||||
const tab = event.target;
|
||||
const show = tab.getAttribute('aria-expanded') === 'false';
|
||||
const pane = document.getElementById(tab.getAttribute('aria-controls'));
|
||||
const pane = document.getElementById(target);
|
||||
|
||||
// Nothing to show, bail
|
||||
if (!utils.is.element(pane)) {
|
||||
@@ -868,7 +915,7 @@ const controls = {
|
||||
|
||||
// Hide all other tabs
|
||||
// Get other tabs
|
||||
const current = menu.querySelector('[role="tabpanel"][aria-hidden="false"]');
|
||||
const current = menu.querySelector('[role="tabpanel"]:not([hidden])');
|
||||
const container = current.parentNode;
|
||||
|
||||
// Set other toggles to be expanded false
|
||||
@@ -912,12 +959,16 @@ const controls = {
|
||||
}
|
||||
|
||||
// Set attributes on current tab
|
||||
current.setAttribute('aria-hidden', true);
|
||||
utils.toggleHidden(current, true);
|
||||
current.setAttribute('tabindex', -1);
|
||||
|
||||
// Set attributes on target
|
||||
pane.setAttribute('aria-hidden', !show);
|
||||
tab.setAttribute('aria-expanded', show);
|
||||
utils.toggleHidden(pane, false);
|
||||
|
||||
const tabs = utils.getElements.call(this, `[aria-controls="${target}"]`);
|
||||
Array.from(tabs).forEach(tab => {
|
||||
tab.setAttribute('aria-expanded', true);
|
||||
});
|
||||
pane.removeAttribute('tabindex');
|
||||
|
||||
// Focus the first item
|
||||
@@ -1043,6 +1094,7 @@ const controls = {
|
||||
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
||||
const menu = utils.createElement('div', {
|
||||
class: 'plyr__menu',
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
menu.appendChild(
|
||||
@@ -1057,7 +1109,7 @@ const controls = {
|
||||
const form = utils.createElement('form', {
|
||||
class: 'plyr__menu__container',
|
||||
id: `plyr-settings-${data.id}`,
|
||||
'aria-hidden': true,
|
||||
hidden: '',
|
||||
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
|
||||
role: 'tablist',
|
||||
tabindex: -1,
|
||||
@@ -1067,7 +1119,6 @@ const controls = {
|
||||
|
||||
const home = utils.createElement('div', {
|
||||
id: `plyr-settings-${data.id}-home`,
|
||||
'aria-hidden': false,
|
||||
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
|
||||
role: 'tabpanel',
|
||||
});
|
||||
@@ -1118,11 +1169,10 @@ const controls = {
|
||||
this.config.settings.forEach(type => {
|
||||
const pane = utils.createElement('div', {
|
||||
id: `plyr-settings-${data.id}-${type}`,
|
||||
'aria-hidden': true,
|
||||
hidden: '',
|
||||
'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`,
|
||||
role: 'tabpanel',
|
||||
tabindex: -1,
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
const back = utils.createElement(
|
||||
@@ -1177,6 +1227,10 @@ const controls = {
|
||||
|
||||
this.elements.controls = container;
|
||||
|
||||
if (this.isHTML5) {
|
||||
controls.setQualityMenu.call(this, html5.getQualityOptions.call(this));
|
||||
}
|
||||
|
||||
controls.setSpeedMenu.call(this);
|
||||
|
||||
return container;
|
||||
@@ -1189,7 +1243,7 @@ const controls = {
|
||||
const icon = controls.getIconUrl.call(this);
|
||||
|
||||
// Only load external sprite using AJAX
|
||||
if (icon.absolute) {
|
||||
if (icon.cors) {
|
||||
utils.loadSprite(icon.url, 'sprite-plyr');
|
||||
}
|
||||
}
|
||||
@@ -1201,17 +1255,21 @@ const controls = {
|
||||
let container = null;
|
||||
this.elements.controls = null;
|
||||
|
||||
// HTML or Element passed as the option
|
||||
// Set template properties
|
||||
const props = {
|
||||
id: this.id,
|
||||
seektime: this.config.seekTime,
|
||||
title: this.config.title,
|
||||
};
|
||||
let update = true;
|
||||
|
||||
if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) {
|
||||
// String or HTMLElement passed as the option
|
||||
container = this.config.controls;
|
||||
} else if (utils.is.function(this.config.controls)) {
|
||||
// A custom function to build controls
|
||||
// The function can return a HTMLElement or String
|
||||
container = this.config.controls({
|
||||
id: this.id,
|
||||
seektime: this.config.seekTime,
|
||||
title: this.config.title,
|
||||
});
|
||||
container = this.config.controls.call(this, props);
|
||||
} else {
|
||||
// Create controls
|
||||
container = controls.create.call(this, {
|
||||
@@ -1219,10 +1277,34 @@ const controls = {
|
||||
seektime: this.config.seekTime,
|
||||
speed: this.speed,
|
||||
quality: this.quality,
|
||||
captions: controls.getLanguage.call(this),
|
||||
captions: captions.getLabel.call(this),
|
||||
// TODO: Looping
|
||||
// loop: 'None',
|
||||
});
|
||||
update = false;
|
||||
}
|
||||
|
||||
// Replace props with their value
|
||||
const replace = input => {
|
||||
let result = input;
|
||||
|
||||
Object.entries(props).forEach(([
|
||||
key,
|
||||
value,
|
||||
]) => {
|
||||
result = utils.replaceAll(result, `{${key}}`, value);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Update markup
|
||||
if (update) {
|
||||
if (utils.is.string(this.config.controls)) {
|
||||
container = replace(container);
|
||||
} else if (utils.is.element(container)) {
|
||||
container.innerHTML = replace(container.innerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
// Controls container
|
||||
@@ -1241,13 +1323,13 @@ const controls = {
|
||||
// Inject controls HTML
|
||||
if (utils.is.element(container)) {
|
||||
target.appendChild(container);
|
||||
} else {
|
||||
} else if (container) {
|
||||
target.insertAdjacentHTML('beforeend', container);
|
||||
}
|
||||
|
||||
// Find the elements if need be
|
||||
if (!utils.is.element(this.elements.controls)) {
|
||||
utils.findElements.call(this);
|
||||
controls.findElements.call(this);
|
||||
}
|
||||
|
||||
// Edge sometimes doesn't finish the paint so force a redraw
|
||||
|
||||
+35
-24
@@ -47,8 +47,8 @@ const defaults = {
|
||||
// Auto hide the controls
|
||||
hideControls: true,
|
||||
|
||||
// Revert to poster on finish (HTML5 - will cause reload)
|
||||
showPosterOnEnd: false,
|
||||
// Reset to start when playback ended
|
||||
resetOnEnd: false,
|
||||
|
||||
// Disable the standard context menu
|
||||
disableContextMenu: true,
|
||||
@@ -56,24 +56,26 @@ const defaults = {
|
||||
// Sprite (for icons)
|
||||
loadSprite: true,
|
||||
iconPrefix: 'plyr',
|
||||
iconUrl: 'https://cdn.plyr.io/3.0.10/plyr.svg',
|
||||
iconUrl: 'https://cdn.plyr.io/3.3.6/plyr.svg',
|
||||
|
||||
// Blank video (used to prevent errors on source change)
|
||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||
|
||||
// Quality default
|
||||
quality: {
|
||||
default: 'default',
|
||||
default: 576,
|
||||
options: [
|
||||
'hd2160',
|
||||
'hd1440',
|
||||
'hd1080',
|
||||
'hd720',
|
||||
'large',
|
||||
'medium',
|
||||
'small',
|
||||
'tiny',
|
||||
'default',
|
||||
4320,
|
||||
2880,
|
||||
2160,
|
||||
1440,
|
||||
1080,
|
||||
720,
|
||||
576,
|
||||
480,
|
||||
360,
|
||||
240,
|
||||
'default', // YouTube's "auto"
|
||||
],
|
||||
},
|
||||
|
||||
@@ -113,7 +115,7 @@ const defaults = {
|
||||
// Captions settings
|
||||
captions: {
|
||||
active: false,
|
||||
language: window.navigator.language.split('-')[0],
|
||||
language: (navigator.language || navigator.userLanguage).split('-')[0],
|
||||
},
|
||||
|
||||
// Fullscreen settings
|
||||
@@ -155,10 +157,10 @@ const defaults = {
|
||||
// Localisation
|
||||
i18n: {
|
||||
restart: 'Restart',
|
||||
rewind: 'Rewind {seektime} secs',
|
||||
rewind: 'Rewind {seektime}s',
|
||||
play: 'Play',
|
||||
pause: 'Pause',
|
||||
fastForward: 'Forward {seektime} secs',
|
||||
fastForward: 'Forward {seektime}s',
|
||||
seek: 'Seek',
|
||||
played: 'Played',
|
||||
buffered: 'Buffered',
|
||||
@@ -175,6 +177,7 @@ const defaults = {
|
||||
captions: 'Captions',
|
||||
settings: 'Settings',
|
||||
speed: 'Speed',
|
||||
normal: 'Normal',
|
||||
quality: 'Quality',
|
||||
loop: 'Loop',
|
||||
start: 'Start',
|
||||
@@ -182,19 +185,24 @@ const defaults = {
|
||||
all: 'All',
|
||||
reset: 'Reset',
|
||||
disabled: 'Disabled',
|
||||
enabled: 'Enabled',
|
||||
advertisement: 'Ad',
|
||||
},
|
||||
|
||||
// URLs
|
||||
urls: {
|
||||
vimeo: {
|
||||
api: 'https://player.vimeo.com/api/player.js',
|
||||
sdk: 'https://player.vimeo.com/api/player.js',
|
||||
iframe: 'https://player.vimeo.com/video/{0}?{1}',
|
||||
api: 'https://vimeo.com/api/v2/video/{0}.json',
|
||||
},
|
||||
youtube: {
|
||||
api: 'https://www.youtube.com/iframe_api',
|
||||
sdk: 'https://www.youtube.com/iframe_api',
|
||||
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
|
||||
poster: 'https://img.youtube.com/vi/{0}/maxresdefault.jpg,https://img.youtube.com/vi/{0}/hqdefault.jpg',
|
||||
},
|
||||
googleIMA: {
|
||||
api: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
||||
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -318,14 +326,17 @@ const defaults = {
|
||||
|
||||
// Class hooks added to the player in different states
|
||||
classNames: {
|
||||
video: 'plyr__video-wrapper',
|
||||
embed: 'plyr__video-embed',
|
||||
ads: 'plyr__ads',
|
||||
control: 'plyr__control',
|
||||
type: 'plyr--{0}',
|
||||
provider: 'plyr--{0}',
|
||||
stopped: 'plyr--stopped',
|
||||
video: 'plyr__video-wrapper',
|
||||
embed: 'plyr__video-embed',
|
||||
embedContainer: 'plyr__video-embed__container',
|
||||
poster: 'plyr__poster',
|
||||
ads: 'plyr__ads',
|
||||
control: 'plyr__control',
|
||||
playing: 'plyr--playing',
|
||||
paused: 'plyr--paused',
|
||||
stopped: 'plyr--stopped',
|
||||
loading: 'plyr--loading',
|
||||
error: 'plyr--has-error',
|
||||
hover: 'plyr--hover',
|
||||
|
||||
+13
-11
@@ -55,7 +55,7 @@ class Fullscreen {
|
||||
|
||||
// Get prefix
|
||||
this.prefix = Fullscreen.prefix;
|
||||
this.name = Fullscreen.name;
|
||||
this.property = Fullscreen.property;
|
||||
|
||||
// Scroll position
|
||||
this.scrollPosition = { x: 0, y: 0 };
|
||||
@@ -68,13 +68,15 @@ class Fullscreen {
|
||||
});
|
||||
|
||||
// Fullscreen toggle on double click
|
||||
utils.on(this.player.elements.container, 'dblclick', () => {
|
||||
utils.on(this.player.elements.container, 'dblclick', event => {
|
||||
// Ignore double click in controls
|
||||
if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggle();
|
||||
});
|
||||
|
||||
// Prevent double click on controls bubbling up
|
||||
utils.on(this.player.elements.controls, 'dblclick', event => event.stopPropagation());
|
||||
|
||||
// Update the UI
|
||||
this.update();
|
||||
}
|
||||
@@ -88,7 +90,7 @@ class Fullscreen {
|
||||
static get prefix() {
|
||||
// No prefix
|
||||
if (utils.is.function(document.exitFullscreen)) {
|
||||
return false;
|
||||
return '';
|
||||
}
|
||||
|
||||
// Check for fullscreen support by vendor prefix
|
||||
@@ -111,7 +113,7 @@ class Fullscreen {
|
||||
return value;
|
||||
}
|
||||
|
||||
static get name() {
|
||||
static get property() {
|
||||
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
|
||||
}
|
||||
|
||||
@@ -136,7 +138,7 @@ class Fullscreen {
|
||||
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
||||
}
|
||||
|
||||
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.name}Element`];
|
||||
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
|
||||
|
||||
return element === this.target;
|
||||
}
|
||||
@@ -174,7 +176,7 @@ class Fullscreen {
|
||||
} else if (!this.prefix) {
|
||||
this.target.requestFullscreen();
|
||||
} else if (!utils.is.empty(this.prefix)) {
|
||||
this.target[`${this.prefix}Request${this.name}`]();
|
||||
this.target[`${this.prefix}Request${this.property}`]();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,10 +193,10 @@ class Fullscreen {
|
||||
} else if (!Fullscreen.native) {
|
||||
toggleFallback.call(this, false);
|
||||
} else if (!this.prefix) {
|
||||
document.cancelFullScreen();
|
||||
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
||||
} else if (!utils.is.empty(this.prefix)) {
|
||||
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
|
||||
document[`${this.prefix}${action}${this.name}`]();
|
||||
document[`${this.prefix}${action}${this.property}`]();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
// ==========================================================================
|
||||
// Plyr HTML5 helpers
|
||||
// ==========================================================================
|
||||
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
|
||||
const html5 = {
|
||||
getSources() {
|
||||
if (!this.isHTML5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.media.querySelectorAll('source');
|
||||
},
|
||||
|
||||
// Get quality levels
|
||||
getQualityOptions() {
|
||||
if (!this.isHTML5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get sources
|
||||
const sources = html5.getSources.call(this);
|
||||
|
||||
if (utils.is.empty(sources)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get <source> with size attribute
|
||||
const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size')));
|
||||
|
||||
// If none, bail
|
||||
if (utils.is.empty(sizes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reduce to unique list
|
||||
return utils.dedupe(sizes.map(source => Number(source.getAttribute('size'))));
|
||||
},
|
||||
|
||||
extend() {
|
||||
if (!this.isHTML5) {
|
||||
return;
|
||||
}
|
||||
|
||||
const player = this;
|
||||
|
||||
// Quality
|
||||
Object.defineProperty(player.media, 'quality', {
|
||||
get() {
|
||||
// Get sources
|
||||
const sources = html5.getSources.call(player);
|
||||
|
||||
if (utils.is.empty(sources)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
|
||||
|
||||
if (utils.is.empty(matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Number(matches[0].getAttribute('size'));
|
||||
},
|
||||
set(input) {
|
||||
// Get sources
|
||||
const sources = html5.getSources.call(player);
|
||||
|
||||
if (utils.is.empty(sources)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get matches for requested size
|
||||
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
|
||||
|
||||
// No matches for requested size
|
||||
if (utils.is.empty(matches)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get supported sources
|
||||
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
|
||||
|
||||
// No supported sources
|
||||
if (utils.is.empty(supported)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger change event
|
||||
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
|
||||
quality: input,
|
||||
});
|
||||
|
||||
// Get current state
|
||||
const { currentTime, playing } = player;
|
||||
|
||||
// Set new source
|
||||
player.media.src = supported[0].getAttribute('src');
|
||||
|
||||
// Load new source
|
||||
player.media.load();
|
||||
|
||||
// Resume playing
|
||||
if (playing) {
|
||||
player.play();
|
||||
}
|
||||
|
||||
// Restore time
|
||||
player.currentTime = currentTime;
|
||||
|
||||
// Trigger change event
|
||||
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
|
||||
quality: input,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// Cancel current network requests
|
||||
// See https://github.com/sampotts/plyr/issues/174
|
||||
cancelRequests() {
|
||||
if (!this.isHTML5) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove child sources
|
||||
utils.removeElement(html5.getSources());
|
||||
|
||||
// Set blank video src attribute
|
||||
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
|
||||
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
|
||||
this.media.setAttribute('src', this.config.blankVideo);
|
||||
|
||||
// Load the new empty source
|
||||
// This will cancel existing requests
|
||||
// See https://github.com/sampotts/plyr/issues/174
|
||||
this.media.load();
|
||||
|
||||
// Debugging
|
||||
this.debug.log('Cancelled network requests');
|
||||
},
|
||||
};
|
||||
|
||||
export default html5;
|
||||
+42
-19
@@ -2,10 +2,9 @@
|
||||
// Plyr Event Listeners
|
||||
// ==========================================================================
|
||||
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
import controls from './controls';
|
||||
import ui from './ui';
|
||||
import utils from './utils';
|
||||
|
||||
// Sniff out the browser
|
||||
const browser = utils.getBrowser();
|
||||
@@ -254,7 +253,7 @@ class Listeners {
|
||||
utils.on(this.player.media, 'timeupdate seeking', event => ui.timeUpdate.call(this.player, event));
|
||||
|
||||
// Display duration
|
||||
utils.on(this.player.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this.player, event));
|
||||
utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => ui.durationUpdate.call(this.player, event));
|
||||
|
||||
// Check for audio tracks on load
|
||||
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
|
||||
@@ -266,30 +265,41 @@ class Listeners {
|
||||
// Handle the media finishing
|
||||
utils.on(this.player.media, 'ended', () => {
|
||||
// Show poster on end
|
||||
if (this.player.isHTML5 && this.player.isVideo && this.player.config.showPosterOnEnd) {
|
||||
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
|
||||
// Restart
|
||||
this.player.restart();
|
||||
|
||||
// Re-load media
|
||||
this.player.media.load();
|
||||
}
|
||||
});
|
||||
|
||||
// Check for buffer progress
|
||||
utils.on(this.player.media, 'progress playing', event => ui.updateProgress.call(this.player, event));
|
||||
|
||||
// Handle native mute
|
||||
// Handle volume changes
|
||||
utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event));
|
||||
|
||||
// Handle native play/pause
|
||||
utils.on(this.player.media, 'playing play pause ended emptied', event => ui.checkPlaying.call(this.player, event));
|
||||
// Handle play/pause
|
||||
utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event));
|
||||
|
||||
// Loading
|
||||
// Loading state
|
||||
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
|
||||
|
||||
// Check if media failed to load
|
||||
// utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.player, event));
|
||||
|
||||
// If autoplay, then load advertisement if required
|
||||
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
|
||||
utils.on(this.player.media, 'playing', () => {
|
||||
if (!this.player.ads) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If ads are enabled, wait for them first
|
||||
if (this.player.ads.enabled && !this.player.ads.initialized) {
|
||||
// Wait for manager response
|
||||
this.player.ads.managerPromise.then(() => this.player.ads.play()).catch(() => this.player.play());
|
||||
}
|
||||
});
|
||||
|
||||
// Click video
|
||||
if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) {
|
||||
// Re-fetch the wrapper
|
||||
@@ -321,7 +331,7 @@ class Listeners {
|
||||
// Disable right click
|
||||
if (this.player.supported.ui && this.player.config.disableContextMenu) {
|
||||
utils.on(
|
||||
this.player.media,
|
||||
this.player.elements.wrapper,
|
||||
'contextmenu',
|
||||
event => {
|
||||
event.preventDefault();
|
||||
@@ -345,13 +355,16 @@ class Listeners {
|
||||
this.player.storage.set({ speed: this.player.speed });
|
||||
});
|
||||
|
||||
// Quality change
|
||||
utils.on(this.player.media, 'qualitychange', () => {
|
||||
// Update UI
|
||||
controls.updateSetting.call(this.player, 'quality');
|
||||
|
||||
// Quality request
|
||||
utils.on(this.player.media, 'qualityrequested', event => {
|
||||
// Save to storage
|
||||
this.player.storage.set({ quality: this.player.quality });
|
||||
this.player.storage.set({ quality: event.detail.quality });
|
||||
});
|
||||
|
||||
// Quality change
|
||||
utils.on(this.player.media, 'qualitychange', event => {
|
||||
// Update UI
|
||||
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
|
||||
});
|
||||
|
||||
// Caption language change
|
||||
@@ -476,12 +489,19 @@ class Listeners {
|
||||
on(this.player.elements.settings.form, 'click', event => {
|
||||
event.stopPropagation();
|
||||
|
||||
// Go back to home tab on click
|
||||
const showHomeTab = () => {
|
||||
const id = `plyr-settings-${this.player.id}-home`;
|
||||
controls.showTab.call(this.player, id);
|
||||
};
|
||||
|
||||
// Settings menu items - use event delegation as items are added/removed
|
||||
if (utils.matches(event.target, this.player.config.selectors.inputs.language)) {
|
||||
proxy(
|
||||
event,
|
||||
() => {
|
||||
this.player.language = event.target.value;
|
||||
showHomeTab();
|
||||
},
|
||||
'language',
|
||||
);
|
||||
@@ -490,6 +510,7 @@ class Listeners {
|
||||
event,
|
||||
() => {
|
||||
this.player.quality = event.target.value;
|
||||
showHomeTab();
|
||||
},
|
||||
'quality',
|
||||
);
|
||||
@@ -498,11 +519,13 @@ class Listeners {
|
||||
event,
|
||||
() => {
|
||||
this.player.speed = parseFloat(event.target.value);
|
||||
showHomeTab();
|
||||
},
|
||||
'speed',
|
||||
);
|
||||
} else {
|
||||
controls.showTab.call(this.player, event);
|
||||
const tab = event.target;
|
||||
controls.showTab.call(this.player, tab.getAttribute('aria-controls'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+11
-49
@@ -2,14 +2,10 @@
|
||||
// Plyr Media
|
||||
// ==========================================================================
|
||||
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
import youtube from './plugins/youtube';
|
||||
import html5 from './html5';
|
||||
import vimeo from './plugins/vimeo';
|
||||
import ui from './ui';
|
||||
|
||||
// Sniff out the browser
|
||||
const browser = utils.getBrowser();
|
||||
import youtube from './plugins/youtube';
|
||||
import utils from './utils';
|
||||
|
||||
const media = {
|
||||
// Setup media
|
||||
@@ -32,23 +28,6 @@ const media = {
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
|
||||
}
|
||||
|
||||
if (this.supported.ui) {
|
||||
// Check for picture-in-picture support
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
|
||||
|
||||
// Check for airplay support
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
||||
|
||||
// If there's no autoplay attribute, assume the video is stopped and add state class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.config.autoplay);
|
||||
|
||||
// Add iOS class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
||||
|
||||
// Add touch class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
||||
}
|
||||
|
||||
// Inject the player wrapper
|
||||
if (this.isVideo) {
|
||||
// Create the wrapper div
|
||||
@@ -58,6 +37,13 @@ const media = {
|
||||
|
||||
// Wrap the video in a container
|
||||
utils.wrap(this.media, this.elements.wrapper);
|
||||
|
||||
// Faux poster container
|
||||
this.elements.poster = utils.createElement('div', {
|
||||
class: this.config.classNames.poster,
|
||||
});
|
||||
|
||||
this.elements.wrapper.appendChild(this.elements.poster);
|
||||
}
|
||||
|
||||
if (this.isEmbed) {
|
||||
@@ -74,33 +60,9 @@ const media = {
|
||||
break;
|
||||
}
|
||||
} else if (this.isHTML5) {
|
||||
ui.setTitle.call(this);
|
||||
html5.extend.call(this);
|
||||
}
|
||||
},
|
||||
|
||||
// Cancel current network requests
|
||||
// See https://github.com/sampotts/plyr/issues/174
|
||||
cancelRequests() {
|
||||
if (!this.isHTML5) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove child sources
|
||||
utils.removeElement(this.media.querySelectorAll('source'));
|
||||
|
||||
// Set blank video src attribute
|
||||
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
|
||||
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
|
||||
this.media.setAttribute('src', this.config.blankVideo);
|
||||
|
||||
// Load the new empty source
|
||||
// This will cancel existing requests
|
||||
// See https://github.com/sampotts/plyr/issues/174
|
||||
this.media.load();
|
||||
|
||||
// Debugging
|
||||
this.debug.log('Cancelled network requests');
|
||||
},
|
||||
};
|
||||
|
||||
export default media;
|
||||
|
||||
+31
-17
@@ -6,8 +6,8 @@
|
||||
|
||||
/* global google */
|
||||
|
||||
import utils from '../utils';
|
||||
import i18n from '../i18n';
|
||||
import utils from '../utils';
|
||||
|
||||
class Ads {
|
||||
/**
|
||||
@@ -18,7 +18,6 @@ class Ads {
|
||||
constructor(player) {
|
||||
this.player = player;
|
||||
this.publisherId = player.config.ads.publisherId;
|
||||
this.enabled = player.isHTML5 && player.isVideo && player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length;
|
||||
this.playing = false;
|
||||
this.initialized = false;
|
||||
this.elements = {
|
||||
@@ -44,6 +43,10 @@ class Ads {
|
||||
this.load();
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the IMA SDK
|
||||
*/
|
||||
@@ -52,7 +55,7 @@ class Ads {
|
||||
// Check if the Google IMA3 SDK is loaded or load it ourselves
|
||||
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
|
||||
utils
|
||||
.loadScript(this.player.config.urls.googleIMA.api)
|
||||
.loadScript(this.player.config.urls.googleIMA.sdk)
|
||||
.then(() => {
|
||||
this.ready();
|
||||
})
|
||||
@@ -160,6 +163,9 @@ class Ads {
|
||||
// We only overlay ads as we only support video.
|
||||
request.forceNonLinearFullSlot = false;
|
||||
|
||||
// Mute based on current state
|
||||
request.setAdWillPlayMuted(!this.player.muted);
|
||||
|
||||
this.loader.requestAds(request);
|
||||
} catch (e) {
|
||||
this.onAdError(e);
|
||||
@@ -206,25 +212,27 @@ class Ads {
|
||||
this.cuePoints = this.manager.getCuePoints();
|
||||
|
||||
// Add advertisement cue's within the time line if available
|
||||
this.cuePoints.forEach(cuePoint => {
|
||||
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
|
||||
const seekElement = this.player.elements.progress;
|
||||
if (!utils.is.empty(this.cuePoints)) {
|
||||
this.cuePoints.forEach(cuePoint => {
|
||||
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
|
||||
const seekElement = this.player.elements.progress;
|
||||
|
||||
if (seekElement) {
|
||||
const cuePercentage = 100 / this.player.duration * cuePoint;
|
||||
const cue = utils.createElement('span', {
|
||||
class: this.player.config.classNames.cues,
|
||||
});
|
||||
if (utils.is.element(seekElement)) {
|
||||
const cuePercentage = 100 / this.player.duration * cuePoint;
|
||||
const cue = utils.createElement('span', {
|
||||
class: this.player.config.classNames.cues,
|
||||
});
|
||||
|
||||
cue.style.left = `${cuePercentage.toString()}%`;
|
||||
seekElement.appendChild(cue);
|
||||
cue.style.left = `${cuePercentage.toString()}%`;
|
||||
seekElement.appendChild(cue);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get skippable state
|
||||
// TODO: Skip button
|
||||
// this.manager.getAdSkippableState();
|
||||
// this.player.debug.warn(this.manager.getAdSkippableState());
|
||||
|
||||
// Set volume to match player
|
||||
this.manager.setVolume(this.player.volume);
|
||||
@@ -385,6 +393,10 @@ class Ads {
|
||||
this.player.on('seeked', () => {
|
||||
const seekedTime = this.player.currentTime;
|
||||
|
||||
if (utils.is.empty(this.cuePoints)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cuePoints.forEach((cuePoint, index) => {
|
||||
if (time < cuePoint && cuePoint < seekedTime) {
|
||||
this.manager.discardAdBreak();
|
||||
@@ -396,7 +408,9 @@ class Ads {
|
||||
// Listen to the resizing of the window. And resize ad accordingly
|
||||
// TODO: eventually implement ResizeObserver
|
||||
window.addEventListener('resize', () => {
|
||||
this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
|
||||
if (this.manager) {
|
||||
this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+54
-10
@@ -2,10 +2,10 @@
|
||||
// Vimeo plugin
|
||||
// ==========================================================================
|
||||
|
||||
import utils from './../utils';
|
||||
import captions from './../captions';
|
||||
import controls from './../controls';
|
||||
import ui from './../ui';
|
||||
import utils from './../utils';
|
||||
|
||||
const vimeo = {
|
||||
setup() {
|
||||
@@ -18,7 +18,7 @@ const vimeo = {
|
||||
// Load the API if not already
|
||||
if (!utils.is.object(window.Vimeo)) {
|
||||
utils
|
||||
.loadScript(this.config.urls.vimeo.api)
|
||||
.loadScript(this.config.urls.vimeo.sdk)
|
||||
.then(() => {
|
||||
vimeo.ready.call(this);
|
||||
})
|
||||
@@ -35,10 +35,14 @@ const vimeo = {
|
||||
setAspectRatio(input) {
|
||||
const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':');
|
||||
const padding = 100 / ratio[0] * ratio[1];
|
||||
const height = 240;
|
||||
const offset = (height - padding) / (height / 50);
|
||||
this.elements.wrapper.style.paddingBottom = `${padding}%`;
|
||||
this.media.style.transform = `translateY(-${offset}%)`;
|
||||
|
||||
if (this.supported.ui) {
|
||||
const height = 240;
|
||||
const offset = (height - padding) / (height / 50);
|
||||
|
||||
this.media.style.transform = `translateY(-${offset}%)`;
|
||||
}
|
||||
},
|
||||
|
||||
// API Ready
|
||||
@@ -49,12 +53,14 @@ const vimeo = {
|
||||
const options = {
|
||||
loop: player.config.loop.active,
|
||||
autoplay: player.autoplay,
|
||||
// muted: player.muted,
|
||||
byline: false,
|
||||
portrait: false,
|
||||
title: false,
|
||||
speed: true,
|
||||
transparent: 0,
|
||||
gesture: 'media',
|
||||
playsinline: !this.config.fullscreen.iosNative,
|
||||
};
|
||||
const params = utils.buildUrlParams(options);
|
||||
|
||||
@@ -63,31 +69,58 @@ const vimeo = {
|
||||
|
||||
// Get from <div> if needed
|
||||
if (utils.is.empty(source)) {
|
||||
source = player.media.getAttribute(this.config.attributes.embed.id);
|
||||
source = player.media.getAttribute(player.config.attributes.embed.id);
|
||||
}
|
||||
|
||||
const id = utils.parseVimeoId(source);
|
||||
|
||||
// Build an iframe
|
||||
const iframe = utils.createElement('iframe');
|
||||
const src = `https://player.vimeo.com/video/${id}?${params}`;
|
||||
const src = utils.format(player.config.urls.vimeo.iframe, id, params);
|
||||
iframe.setAttribute('src', src);
|
||||
iframe.setAttribute('allowfullscreen', '');
|
||||
iframe.setAttribute('allowtransparency', '');
|
||||
iframe.setAttribute('allow', 'autoplay');
|
||||
|
||||
// Inject the package
|
||||
const wrapper = utils.createElement('div');
|
||||
const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
|
||||
wrapper.appendChild(iframe);
|
||||
player.media = utils.replaceElement(wrapper, player.media);
|
||||
|
||||
// Get poster image
|
||||
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
|
||||
if (utils.is.empty(response)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the URL for thumbnail
|
||||
const url = new URL(response[0].thumbnail_large);
|
||||
|
||||
// Get original image
|
||||
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
|
||||
|
||||
// Set attribute
|
||||
player.media.setAttribute('poster', url.href);
|
||||
|
||||
// Update
|
||||
ui.setPoster.call(player);
|
||||
});
|
||||
|
||||
// Setup instance
|
||||
// https://github.com/vimeo/player.js
|
||||
player.embed = new window.Vimeo.Player(iframe);
|
||||
player.embed = new window.Vimeo.Player(iframe, {
|
||||
autopause: player.config.autopause,
|
||||
muted: player.muted,
|
||||
});
|
||||
|
||||
player.media.paused = true;
|
||||
player.media.currentTime = 0;
|
||||
|
||||
// Disable native text track rendering
|
||||
if (player.supported.ui) {
|
||||
player.embed.disableTextTrack();
|
||||
}
|
||||
|
||||
// Create a faux HTML5 API using the Vimeo API
|
||||
player.media.play = () => {
|
||||
player.embed.play().then(() => {
|
||||
@@ -124,7 +157,9 @@ const vimeo = {
|
||||
utils.dispatchEvent.call(player, player.media, 'seeking');
|
||||
|
||||
// Seek after events
|
||||
player.embed.setCurrentTime(time);
|
||||
player.embed.setCurrentTime(time).catch(() => {
|
||||
// Do nothing
|
||||
});
|
||||
|
||||
// Restore pause state
|
||||
if (paused) {
|
||||
@@ -310,6 +345,15 @@ const vimeo = {
|
||||
if (parseInt(data.percent, 10) === 1) {
|
||||
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
|
||||
}
|
||||
|
||||
// Get duration as if we do it before load, it gives an incorrect value
|
||||
// https://github.com/sampotts/plyr/issues/891
|
||||
player.embed.getDuration().then(value => {
|
||||
if (value !== player.media.duration) {
|
||||
player.media.duration = value;
|
||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
player.embed.on('seeked', () => {
|
||||
|
||||
+85
-18
@@ -2,9 +2,67 @@
|
||||
// YouTube plugin
|
||||
// ==========================================================================
|
||||
|
||||
import utils from './../utils';
|
||||
import controls from './../controls';
|
||||
import ui from './../ui';
|
||||
import utils from './../utils';
|
||||
|
||||
// Standardise YouTube quality unit
|
||||
function mapQualityUnit(input) {
|
||||
switch (input) {
|
||||
case 'hd2160':
|
||||
return 2160;
|
||||
|
||||
case 2160:
|
||||
return 'hd2160';
|
||||
|
||||
case 'hd1440':
|
||||
return 1440;
|
||||
|
||||
case 1440:
|
||||
return 'hd1440';
|
||||
|
||||
case 'hd1080':
|
||||
return 1080;
|
||||
|
||||
case 1080:
|
||||
return 'hd1080';
|
||||
|
||||
case 'hd720':
|
||||
return 720;
|
||||
|
||||
case 720:
|
||||
return 'hd720';
|
||||
|
||||
case 'large':
|
||||
return 480;
|
||||
|
||||
case 480:
|
||||
return 'large';
|
||||
|
||||
case 'medium':
|
||||
return 360;
|
||||
|
||||
case 360:
|
||||
return 'medium';
|
||||
|
||||
case 'small':
|
||||
return 240;
|
||||
|
||||
case 240:
|
||||
return 'small';
|
||||
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
function mapQualityUnits(levels) {
|
||||
if (utils.is.empty(levels)) {
|
||||
return levels;
|
||||
}
|
||||
|
||||
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
|
||||
}
|
||||
|
||||
const youtube = {
|
||||
setup() {
|
||||
@@ -19,7 +77,7 @@ const youtube = {
|
||||
youtube.ready.call(this);
|
||||
} else {
|
||||
// Load the API
|
||||
utils.loadScript(this.config.urls.youtube.api).catch(error => {
|
||||
utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
|
||||
this.debug.warn('YouTube API failed to load', error);
|
||||
});
|
||||
|
||||
@@ -59,7 +117,7 @@ const youtube = {
|
||||
// Or via Google API
|
||||
const key = this.config.keys.google;
|
||||
if (utils.is.string(key) && !utils.is.empty(key)) {
|
||||
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`;
|
||||
const url = utils.format(this.config.urls.youtube.api, videoId, key);
|
||||
|
||||
utils
|
||||
.fetch(url)
|
||||
@@ -103,6 +161,9 @@ const youtube = {
|
||||
const container = utils.createElement('div', { id });
|
||||
player.media = utils.replaceElement(container, player.media);
|
||||
|
||||
// Set poster image
|
||||
player.media.setAttribute('poster', utils.format(player.config.urls.youtube.poster, videoId));
|
||||
|
||||
// Setup instance
|
||||
// https://developers.google.com/youtube/iframe_api_reference
|
||||
player.embed = new window.YT.Player(id, {
|
||||
@@ -168,14 +229,10 @@ const youtube = {
|
||||
|
||||
utils.dispatchEvent.call(player, player.media, 'error');
|
||||
},
|
||||
onPlaybackQualityChange(event) {
|
||||
// Get the instance
|
||||
const instance = event.target;
|
||||
|
||||
// Get current quality
|
||||
player.media.quality = instance.getPlaybackQuality();
|
||||
|
||||
utils.dispatchEvent.call(player, player.media, 'qualitychange');
|
||||
onPlaybackQualityChange() {
|
||||
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
|
||||
quality: player.media.quality,
|
||||
});
|
||||
},
|
||||
onPlaybackRateChange(event) {
|
||||
// Get the instance
|
||||
@@ -216,6 +273,9 @@ const youtube = {
|
||||
return Number(instance.getCurrentTime());
|
||||
},
|
||||
set(time) {
|
||||
// Vimeo will automatically play on seek
|
||||
const { paused } = player.media;
|
||||
|
||||
// Set seeking flag
|
||||
player.media.seeking = true;
|
||||
|
||||
@@ -224,6 +284,11 @@ const youtube = {
|
||||
|
||||
// Seek after events sent
|
||||
instance.seekTo(time);
|
||||
|
||||
// Restore pause state
|
||||
if (paused) {
|
||||
player.pause();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -240,15 +305,18 @@ const youtube = {
|
||||
// Quality
|
||||
Object.defineProperty(player.media, 'quality', {
|
||||
get() {
|
||||
return instance.getPlaybackQuality();
|
||||
return mapQualityUnit(instance.getPlaybackQuality());
|
||||
},
|
||||
set(input) {
|
||||
const quality = input;
|
||||
|
||||
// Set via API
|
||||
instance.setPlaybackQuality(mapQualityUnit(quality));
|
||||
|
||||
// Trigger request event
|
||||
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
|
||||
quality: input,
|
||||
quality,
|
||||
});
|
||||
|
||||
instance.setPlaybackQuality(input);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -294,8 +362,7 @@ const youtube = {
|
||||
});
|
||||
|
||||
// Get available speeds
|
||||
const options = instance.getAvailablePlaybackRates();
|
||||
controls.setSpeedMenu.call(player, options);
|
||||
player.options.speed = instance.getAvailablePlaybackRates();
|
||||
|
||||
// Set the tabindex to avoid focus entering iframe
|
||||
if (player.supported.ui) {
|
||||
@@ -401,7 +468,7 @@ const youtube = {
|
||||
}
|
||||
|
||||
// Get quality
|
||||
controls.setQualityMenu.call(player, instance.getAvailableQualityLevels());
|
||||
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
|
||||
|
||||
break;
|
||||
|
||||
|
||||
+116
-52
@@ -1,26 +1,24 @@
|
||||
// ==========================================================================
|
||||
// Plyr
|
||||
// plyr.js v3.0.10
|
||||
// plyr.js v3.3.6
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
|
||||
import { providers, types } from './types';
|
||||
import defaults from './defaults';
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
|
||||
import captions from './captions';
|
||||
import Console from './console';
|
||||
import controls from './controls';
|
||||
import defaults from './defaults';
|
||||
import Fullscreen from './fullscreen';
|
||||
import Listeners from './listeners';
|
||||
import Storage from './storage';
|
||||
import Ads from './plugins/ads';
|
||||
|
||||
import captions from './captions';
|
||||
import controls from './controls';
|
||||
import media from './media';
|
||||
import Ads from './plugins/ads';
|
||||
import source from './source';
|
||||
import Storage from './storage';
|
||||
import support from './support';
|
||||
import { providers, types } from './types';
|
||||
import ui from './ui';
|
||||
import utils from './utils';
|
||||
|
||||
// Private properties
|
||||
// TODO: Use a WeakMap for private globals
|
||||
@@ -57,7 +55,7 @@ class Plyr {
|
||||
this.config = utils.extend(
|
||||
{},
|
||||
defaults,
|
||||
options,
|
||||
options || {},
|
||||
(() => {
|
||||
try {
|
||||
return JSON.parse(this.media.getAttribute('data-plyr-config'));
|
||||
@@ -97,6 +95,7 @@ class Plyr {
|
||||
this.options = {
|
||||
speed: [],
|
||||
quality: [],
|
||||
captions: [],
|
||||
};
|
||||
|
||||
// Debugging
|
||||
@@ -133,7 +132,9 @@ class Plyr {
|
||||
}
|
||||
|
||||
// Cache original element state for .destroy()
|
||||
this.elements.original = this.media.cloneNode(true);
|
||||
const clone = this.media.cloneNode(true);
|
||||
clone.autoplay = false;
|
||||
this.elements.original = clone;
|
||||
|
||||
// Set media type based on tag or data attribute
|
||||
// Supported: video, audio, vimeo, youtube
|
||||
@@ -174,12 +175,17 @@ class Plyr {
|
||||
if (truthy.includes(params.autoplay)) {
|
||||
this.config.autoplay = true;
|
||||
}
|
||||
if (truthy.includes(params.playsinline)) {
|
||||
this.config.inline = true;
|
||||
}
|
||||
if (truthy.includes(params.loop)) {
|
||||
this.config.loop.active = true;
|
||||
}
|
||||
|
||||
// TODO: replace fullscreen.iosNative with this playsinline config option
|
||||
// YouTube requires the playsinline in the URL
|
||||
if (this.isYouTube) {
|
||||
this.config.playsinline = truthy.includes(params.playsinline);
|
||||
} else {
|
||||
this.config.playsinline = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// <div> with attributes
|
||||
@@ -213,7 +219,7 @@ class Plyr {
|
||||
this.config.autoplay = true;
|
||||
}
|
||||
if (this.media.hasAttribute('playsinline')) {
|
||||
this.config.inline = true;
|
||||
this.config.playsinline = true;
|
||||
}
|
||||
if (this.media.hasAttribute('muted')) {
|
||||
this.config.muted = true;
|
||||
@@ -230,7 +236,7 @@ class Plyr {
|
||||
}
|
||||
|
||||
// Check for support again but with type
|
||||
this.supported = support.check(this.type, this.provider, this.config.inline);
|
||||
this.supported = support.check(this.type, this.provider, this.config.playsinline);
|
||||
|
||||
// If no support for even API, bail
|
||||
if (!this.supported.api) {
|
||||
@@ -286,6 +292,11 @@ class Plyr {
|
||||
|
||||
// Setup ads if provided
|
||||
this.ads = new Ads(this);
|
||||
|
||||
// Autoplay if required
|
||||
if (this.config.autoplay) {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
@@ -322,11 +333,6 @@ class Plyr {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If ads are enabled, wait for them first
|
||||
if (this.ads.enabled && !this.ads.initialized) {
|
||||
return this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play());
|
||||
}
|
||||
|
||||
// Return the promise (for HTML5)
|
||||
return this.media.play();
|
||||
}
|
||||
@@ -342,6 +348,13 @@ class Plyr {
|
||||
this.media.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playing state
|
||||
*/
|
||||
get playing() {
|
||||
return Boolean(this.ready && !this.paused && !this.ended);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paused state
|
||||
*/
|
||||
@@ -350,10 +363,10 @@ class Plyr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playing state
|
||||
* Get stopped state
|
||||
*/
|
||||
get playing() {
|
||||
return Boolean(!this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true));
|
||||
get stopped() {
|
||||
return Boolean(this.paused && this.currentTime === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -383,8 +396,9 @@ class Plyr {
|
||||
*/
|
||||
stop() {
|
||||
if (this.isHTML5) {
|
||||
this.media.load();
|
||||
} else {
|
||||
this.pause();
|
||||
this.restart();
|
||||
} else if (utils.is.function(this.media.stop)) {
|
||||
this.media.stop();
|
||||
}
|
||||
}
|
||||
@@ -431,7 +445,7 @@ class Plyr {
|
||||
}
|
||||
|
||||
// Set
|
||||
this.media.currentTime = parseFloat(targetTime.toFixed(4));
|
||||
this.media.currentTime = targetTime;
|
||||
|
||||
// Logging
|
||||
this.debug.log(`Seeking to ${this.currentTime} seconds`);
|
||||
@@ -477,7 +491,7 @@ class Plyr {
|
||||
*/
|
||||
get duration() {
|
||||
// Faux duration set via config
|
||||
const fauxDuration = parseInt(this.config.duration, 10);
|
||||
const fauxDuration = parseFloat(this.config.duration);
|
||||
|
||||
// True duration
|
||||
const realDuration = this.media ? Number(this.media.duration) : 0;
|
||||
@@ -524,8 +538,8 @@ class Plyr {
|
||||
// Set the player volume
|
||||
this.media.volume = volume;
|
||||
|
||||
// If muted, and we're increasing volume, reset muted state
|
||||
if (this.muted && volume > 0) {
|
||||
// If muted, and we're increasing volume manually, reset muted state
|
||||
if (!utils.is.empty(value) && this.muted && volume > 0) {
|
||||
this.muted = false;
|
||||
}
|
||||
}
|
||||
@@ -655,29 +669,38 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Set playback quality
|
||||
* Currently YouTube only
|
||||
* @param {string} input - Quality level
|
||||
* Currently HTML5 & YouTube only
|
||||
* @param {number} input - Quality level
|
||||
*/
|
||||
set quality(input) {
|
||||
let quality = null;
|
||||
|
||||
if (utils.is.string(input)) {
|
||||
quality = input;
|
||||
if (!utils.is.empty(input)) {
|
||||
quality = Number(input);
|
||||
}
|
||||
|
||||
if (!utils.is.string(quality)) {
|
||||
if (!utils.is.number(quality) || quality === 0) {
|
||||
quality = this.storage.get('quality');
|
||||
}
|
||||
|
||||
if (!utils.is.string(quality)) {
|
||||
if (!utils.is.number(quality)) {
|
||||
quality = this.config.quality.selected;
|
||||
}
|
||||
|
||||
if (!this.options.quality.includes(quality)) {
|
||||
this.debug.warn(`Unsupported quality option (${quality})`);
|
||||
if (!utils.is.number(quality)) {
|
||||
quality = this.config.quality.default;
|
||||
}
|
||||
|
||||
if (!this.options.quality.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.options.quality.includes(quality)) {
|
||||
const closest = utils.closest(this.options.quality, quality);
|
||||
this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`);
|
||||
quality = closest;
|
||||
}
|
||||
|
||||
// Update config
|
||||
this.config.quality.selected = quality;
|
||||
|
||||
@@ -769,17 +792,18 @@ class Plyr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the poster image for a HTML5 video
|
||||
* Set the poster image for a video
|
||||
* @param {input} - the URL for the new poster image
|
||||
*/
|
||||
set poster(input) {
|
||||
if (!this.isHTML5 || !this.isVideo) {
|
||||
this.debug.warn('Poster can only be set on HTML5 video');
|
||||
if (!this.isVideo) {
|
||||
this.debug.warn('Poster can only be set for video');
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils.is.string(input)) {
|
||||
this.media.setAttribute('poster', input);
|
||||
ui.setPoster.call(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,7 +811,7 @@ class Plyr {
|
||||
* Get the current poster image
|
||||
*/
|
||||
get poster() {
|
||||
if (!this.isHTML5 || !this.isVideo) {
|
||||
if (!this.isVideo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -815,13 +839,13 @@ class Plyr {
|
||||
* @param {boolean} input - Whether to enable captions
|
||||
*/
|
||||
toggleCaptions(input) {
|
||||
// If there's no full support, or there's no caption toggle
|
||||
if (!this.supported.ui || !utils.is.element(this.elements.buttons.captions)) {
|
||||
// If there's no full support
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the method is called without parameter, toggle based on current value
|
||||
const show = utils.is.boolean(input) ? input : this.elements.container.className.indexOf(this.config.classNames.captions.active) === -1;
|
||||
const show = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
|
||||
|
||||
// Nothing to change...
|
||||
if (this.captions.active === show) {
|
||||
@@ -851,17 +875,29 @@ class Plyr {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle captions based on input
|
||||
this.toggleCaptions(!utils.is.empty(input));
|
||||
|
||||
// If empty string is passed, assume disable captions
|
||||
if (utils.is.empty(input)) {
|
||||
this.toggleCaptions(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize
|
||||
const language = input.toLowerCase();
|
||||
|
||||
// Check for support
|
||||
if (!this.options.captions.includes(language)) {
|
||||
this.debug.warn(`Unsupported language option: ${language}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure captions are enabled
|
||||
this.toggleCaptions(true);
|
||||
|
||||
// Enabled only
|
||||
if (language === 'enabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
// If nothing to change, bail
|
||||
if (this.language === language) {
|
||||
return;
|
||||
@@ -1019,6 +1055,11 @@ class Plyr {
|
||||
// then set the timer to hide the controls
|
||||
if (!show || this.playing) {
|
||||
this.timers.controls = setTimeout(() => {
|
||||
// We need controls of course...
|
||||
if (!utils.is.element(this.elements.controls)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the mouse is over the controls (and not entering fullscreen), bail
|
||||
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
|
||||
return;
|
||||
@@ -1029,8 +1070,8 @@ class Plyr {
|
||||
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
|
||||
}
|
||||
|
||||
// Check if controls toggled
|
||||
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, true);
|
||||
// Set hideControls class
|
||||
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, this.config.hideControls);
|
||||
|
||||
// Trigger event and close menu
|
||||
if (toggled) {
|
||||
@@ -1203,6 +1244,29 @@ class Plyr {
|
||||
static loadSprite(url, id) {
|
||||
return utils.loadSprite(url, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup multiple instances
|
||||
* @param {*} selector
|
||||
* @param {object} options
|
||||
*/
|
||||
static setup(selector, options = {}) {
|
||||
let targets = null;
|
||||
|
||||
if (utils.is.string(selector)) {
|
||||
targets = Array.from(document.querySelectorAll(selector));
|
||||
} else if (utils.is.nodeList(selector)) {
|
||||
targets = Array.from(selector);
|
||||
} else if (utils.is.array(selector)) {
|
||||
targets = selector.filter(i => utils.is.element(i));
|
||||
}
|
||||
|
||||
if (utils.is.empty(targets)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return targets.map(t => new Plyr(t, options));
|
||||
}
|
||||
}
|
||||
|
||||
export default Plyr;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// ==========================================================================
|
||||
// Plyr Polyfilled Build
|
||||
// plyr.js v3.0.10
|
||||
// plyr.js v3.3.6
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
|
||||
import 'babel-polyfill';
|
||||
|
||||
import 'custom-event-polyfill';
|
||||
|
||||
import Plyr from './plyr';
|
||||
|
||||
export default Plyr;
|
||||
|
||||
+11
-9
@@ -2,11 +2,12 @@
|
||||
// Plyr source update
|
||||
// ==========================================================================
|
||||
|
||||
import { providers } from './types';
|
||||
import utils from './utils';
|
||||
import html5 from './html5';
|
||||
import media from './media';
|
||||
import ui from './ui';
|
||||
import support from './support';
|
||||
import { providers } from './types';
|
||||
import ui from './ui';
|
||||
import utils from './utils';
|
||||
|
||||
const source = {
|
||||
// Add elements to HTML5 media (source, tracks, etc)
|
||||
@@ -31,13 +32,14 @@ const source = {
|
||||
}
|
||||
|
||||
// Cancel current network requests
|
||||
media.cancelRequests.call(this);
|
||||
html5.cancelRequests.call(this);
|
||||
|
||||
// Destroy instance and re-setup
|
||||
this.destroy.call(
|
||||
this,
|
||||
() => {
|
||||
// TODO: Reset menus here
|
||||
// Reset quality options
|
||||
this.options.quality = [];
|
||||
|
||||
// Remove elements
|
||||
utils.removeElement(this.media);
|
||||
@@ -53,7 +55,7 @@ const source = {
|
||||
this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5;
|
||||
|
||||
// Check for support
|
||||
this.supported = support.check(this.type, this.provider, this.config.inline);
|
||||
this.supported = support.check(this.type, this.provider, this.config.playsinline);
|
||||
|
||||
// Create new markup
|
||||
switch (`${this.provider}:${this.type}`) {
|
||||
@@ -92,8 +94,8 @@ const source = {
|
||||
if (this.config.autoplay) {
|
||||
this.media.setAttribute('autoplay', '');
|
||||
}
|
||||
if ('poster' in input) {
|
||||
this.media.setAttribute('poster', input.poster);
|
||||
if (!utils.is.empty(input.poster)) {
|
||||
this.poster = input.poster;
|
||||
}
|
||||
if (this.config.loop.active) {
|
||||
this.media.setAttribute('loop', '');
|
||||
@@ -101,7 +103,7 @@ const source = {
|
||||
if (this.config.muted) {
|
||||
this.media.setAttribute('muted', '');
|
||||
}
|
||||
if (this.config.inline) {
|
||||
if (this.config.playsinline) {
|
||||
this.media.setAttribute('playsinline', '');
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -12,16 +12,16 @@ const support = {
|
||||
|
||||
// Check for support
|
||||
// Basic functionality vs full UI
|
||||
check(type, provider, inline) {
|
||||
check(type, provider, playsinline) {
|
||||
let api = false;
|
||||
let ui = false;
|
||||
const browser = utils.getBrowser();
|
||||
const playsInline = browser.isIPhone && inline && support.inline;
|
||||
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
|
||||
|
||||
switch (`${provider}:${type}`) {
|
||||
case 'html5:video':
|
||||
api = support.video;
|
||||
ui = api && support.rangeInput && (!browser.isIPhone || playsInline);
|
||||
ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline);
|
||||
break;
|
||||
|
||||
case 'html5:audio':
|
||||
@@ -32,7 +32,7 @@ const support = {
|
||||
case 'youtube:video':
|
||||
case 'vimeo:video':
|
||||
api = true;
|
||||
ui = support.rangeInput && (!browser.isIPhone || playsInline);
|
||||
ui = support.rangeInput && (!browser.isIPhone || canPlayInline);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -59,7 +59,7 @@ const support = {
|
||||
|
||||
// Inline playback support
|
||||
// https://webkit.org/blog/6784/new-video-policies-for-ios/
|
||||
inline: 'playsInline' in document.createElement('video'),
|
||||
playsinline: 'playsInline' in document.createElement('video'),
|
||||
|
||||
// Check for mime type support against a player instance
|
||||
// Credits: http://diveintohtml5.info/everything.html
|
||||
@@ -73,6 +73,11 @@ const support = {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check directly if codecs specified
|
||||
if (type.includes('codecs=')) {
|
||||
return media.canPlayType(type).replace(/no/, '');
|
||||
}
|
||||
|
||||
// Type specific checks
|
||||
if (this.isVideo) {
|
||||
switch (type) {
|
||||
|
||||
+48
-13
@@ -2,10 +2,14 @@
|
||||
// Plyr UI
|
||||
// ==========================================================================
|
||||
|
||||
import utils from './utils';
|
||||
import captions from './captions';
|
||||
import controls from './controls';
|
||||
import i18n from './i18n';
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
|
||||
// Sniff out the browser
|
||||
const browser = utils.getBrowser();
|
||||
|
||||
const ui = {
|
||||
addStyleHook() {
|
||||
@@ -48,11 +52,6 @@ const ui = {
|
||||
this.listeners.controls();
|
||||
}
|
||||
|
||||
// If there's no controls, bail
|
||||
if (!utils.is.element(this.elements.controls)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove native controls
|
||||
ui.toggleNativeControls.call(this);
|
||||
|
||||
@@ -71,8 +70,11 @@ const ui = {
|
||||
// Reset loop state
|
||||
this.loop = null;
|
||||
|
||||
// Reset quality options
|
||||
this.options.quality = [];
|
||||
// Reset quality setting
|
||||
this.quality = null;
|
||||
|
||||
// Reset volume display
|
||||
ui.updateVolume.call(this);
|
||||
|
||||
// Reset time display
|
||||
ui.timeUpdate.call(this);
|
||||
@@ -80,6 +82,18 @@ const ui = {
|
||||
// Update the UI
|
||||
ui.checkPlaying.call(this);
|
||||
|
||||
// Check for picture-in-picture support
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
|
||||
|
||||
// Check for airplay support
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
||||
|
||||
// Add iOS class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
||||
|
||||
// Add touch class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
||||
|
||||
// Ready for API calls
|
||||
this.ready = true;
|
||||
|
||||
@@ -90,6 +104,9 @@ const ui = {
|
||||
|
||||
// Set the title
|
||||
ui.setTitle.call(this);
|
||||
|
||||
// Set the poster image
|
||||
ui.setPoster.call(this);
|
||||
},
|
||||
|
||||
// Setup aria attribute for play and iframe title
|
||||
@@ -123,20 +140,38 @@ const ui = {
|
||||
|
||||
// Default to media type
|
||||
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
|
||||
const format = i18n.get('frameTitle', this.config);
|
||||
|
||||
iframe.setAttribute('title', i18n.get('frameTitle', this.config));
|
||||
iframe.setAttribute('title', format.replace('{title}', title));
|
||||
}
|
||||
},
|
||||
|
||||
// Set the poster image
|
||||
setPoster() {
|
||||
if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the inline style
|
||||
const posters = this.poster.split(',');
|
||||
this.elements.poster.style.backgroundImage = posters.map(p => `url('${p}')`).join(',');
|
||||
},
|
||||
|
||||
// Check playing state
|
||||
checkPlaying() {
|
||||
checkPlaying(event) {
|
||||
// Class hooks
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused);
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
|
||||
|
||||
// Set ARIA state
|
||||
utils.toggleState(this.elements.buttons.play, this.playing);
|
||||
|
||||
// Only update controls on non timeupdate events
|
||||
if (utils.is.event(event) && event.type === 'timeupdate') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle controls
|
||||
this.toggleControls(!this.playing);
|
||||
},
|
||||
@@ -274,10 +309,10 @@ const ui = {
|
||||
}
|
||||
|
||||
// Always display hours if duration is over an hour
|
||||
const displayHours = utils.getHours(this.duration) > 0;
|
||||
const forceHours = utils.getHours(this.duration) > 0;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
target.textContent = utils.formatTime(time, displayHours, inverted);
|
||||
target.textContent = utils.formatTime(time, forceHours, inverted);
|
||||
},
|
||||
|
||||
// Handle time change event
|
||||
|
||||
+54
-76
@@ -3,7 +3,6 @@
|
||||
// ==========================================================================
|
||||
|
||||
import loadjs from 'loadjs';
|
||||
|
||||
import support from './support';
|
||||
import { providers } from './types';
|
||||
|
||||
@@ -269,14 +268,14 @@ const utils = {
|
||||
parent.appendChild(utils.createElement(type, attributes, text));
|
||||
},
|
||||
|
||||
// Remove an element
|
||||
// Remove element(s)
|
||||
removeElement(element) {
|
||||
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
|
||||
if (utils.is.nodeList(element) || utils.is.array(element)) {
|
||||
Array.from(element).forEach(utils.removeElement);
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils.is.nodeList(element) || utils.is.array(element)) {
|
||||
Array.from(element).forEach(utils.removeElement);
|
||||
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -375,6 +374,25 @@ const utils = {
|
||||
return attributes;
|
||||
},
|
||||
|
||||
// Toggle hidden
|
||||
toggleHidden(element, hidden) {
|
||||
if (!utils.is.element(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hide = hidden;
|
||||
|
||||
if (!utils.is.boolean(hide)) {
|
||||
hide = !element.hasAttribute('hidden');
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
element.setAttribute('hidden', '');
|
||||
} else {
|
||||
element.removeAttribute('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
// Toggle class on an element
|
||||
toggleClass(element, className, toggle) {
|
||||
if (utils.is.element(element)) {
|
||||
@@ -393,19 +411,6 @@ const utils = {
|
||||
return utils.is.element(element) && element.classList.contains(className);
|
||||
},
|
||||
|
||||
// Toggle hidden attribute on an element
|
||||
toggleHidden(element, toggle) {
|
||||
if (!utils.is.element(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (toggle) {
|
||||
element.setAttribute('hidden', '');
|
||||
} else {
|
||||
element.removeAttribute('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
// Element matches selector
|
||||
matches(element, selector) {
|
||||
const prototype = { Element };
|
||||
@@ -429,60 +434,6 @@ const utils = {
|
||||
return this.elements.container.querySelector(selector);
|
||||
},
|
||||
|
||||
// Find the UI controls and store references in custom controls
|
||||
// TODO: Allow settings menus with custom controls
|
||||
findElements() {
|
||||
try {
|
||||
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
|
||||
|
||||
// Buttons
|
||||
this.elements.buttons = {
|
||||
play: utils.getElements.call(this, this.config.selectors.buttons.play),
|
||||
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
|
||||
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
|
||||
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
|
||||
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
|
||||
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
|
||||
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
|
||||
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
|
||||
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
|
||||
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
|
||||
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
|
||||
};
|
||||
|
||||
// Progress
|
||||
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
|
||||
|
||||
// Inputs
|
||||
this.elements.inputs = {
|
||||
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
|
||||
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
|
||||
};
|
||||
|
||||
// Display
|
||||
this.elements.display = {
|
||||
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
|
||||
duration: utils.getElement.call(this, this.config.selectors.display.duration),
|
||||
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
|
||||
};
|
||||
|
||||
// Seek tooltip
|
||||
if (utils.is.element(this.elements.progress)) {
|
||||
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Log it
|
||||
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
|
||||
|
||||
// Restore native video controls
|
||||
this.toggleNativeControls(true);
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Get the focused element
|
||||
getFocusElement() {
|
||||
let focused = document.activeElement;
|
||||
@@ -586,15 +537,15 @@ const utils = {
|
||||
},
|
||||
|
||||
// Trigger event
|
||||
dispatchEvent(element, type, bubbles, detail) {
|
||||
dispatchEvent(element, type = '', bubbles = false, detail = {}) {
|
||||
// Bail if no element
|
||||
if (!utils.is.element(element) || !utils.is.string(type)) {
|
||||
if (!utils.is.element(element) || utils.is.empty(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and dispatch the event
|
||||
const event = new CustomEvent(type, {
|
||||
bubbles: utils.is.boolean(bubbles) ? bubbles : false,
|
||||
bubbles,
|
||||
detail: Object.assign({}, detail, {
|
||||
plyr: utils.is.plyr(this) ? this : null,
|
||||
}),
|
||||
@@ -626,6 +577,15 @@ const utils = {
|
||||
element.setAttribute('aria-pressed', state);
|
||||
},
|
||||
|
||||
// Format string
|
||||
format(input, ...args) {
|
||||
if (utils.is.empty(input)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return input.toString().replace(/{(\d+)}/g, (match, i) => utils.is.string(args[i]) ? args[i] : '');
|
||||
},
|
||||
|
||||
// Get percentage
|
||||
getPercentage(current, max) {
|
||||
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
|
||||
@@ -737,6 +697,24 @@ const utils = {
|
||||
return utils.extend(target, ...sources);
|
||||
},
|
||||
|
||||
// Remove duplicates in an array
|
||||
dedupe(array) {
|
||||
if (!utils.is.array(array)) {
|
||||
return array;
|
||||
}
|
||||
|
||||
return array.filter((item, index) => array.indexOf(item) === index);
|
||||
},
|
||||
|
||||
// Get the closest value in an array
|
||||
closest(array, value) {
|
||||
if (!utils.is.array(array) || !array.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
|
||||
},
|
||||
|
||||
// Get the provider for a given URL
|
||||
getProviderByUrl(url) {
|
||||
// YouTube
|
||||
@@ -745,7 +723,7 @@ const utils = {
|
||||
}
|
||||
|
||||
// Vimeo
|
||||
if (/^https?:\/\/player.vimeo.com\/video\/\d{8,}(?=\b|\/)/.test(url)) {
|
||||
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
|
||||
return providers.vimeo;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// Ignore focus
|
||||
&:focus {
|
||||
outline: 0;
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
// YouTube, Vimeo, etc
|
||||
// --------------------------------------------------------------
|
||||
|
||||
.plyr__video-embed {
|
||||
// Default to 16:9 ratio but this is set by JavaScript based on config
|
||||
$padding: ((100 / 16) * 9);
|
||||
$height: 240;
|
||||
$offset: to-percentage(($height - $padding) / ($height / 50));
|
||||
// Default to 16:9 ratio but this is set by JavaScript based on config
|
||||
$embed-padding: ((100 / 16) * 9);
|
||||
|
||||
.plyr__video-embed {
|
||||
height: 0;
|
||||
padding-bottom: to-percentage($padding);
|
||||
padding-bottom: to-percentage($embed-padding);
|
||||
position: relative;
|
||||
|
||||
iframe {
|
||||
@@ -22,15 +20,22 @@
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Vimeo hack
|
||||
> div {
|
||||
// If the full custom UI is supported
|
||||
.plyr--full-ui .plyr__video-embed {
|
||||
$height: 240;
|
||||
$offset: to-percentage(($height - $embed-padding) / ($height / 50));
|
||||
|
||||
// To allow mouse events to be captured if full support
|
||||
iframe {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Only used for Vimeo
|
||||
> .plyr__video-embed__container {
|
||||
padding-bottom: to-percentage($height);
|
||||
position: relative;
|
||||
transform: translateY(-$offset);
|
||||
}
|
||||
}
|
||||
// To allow mouse events to be captured if full support
|
||||
.plyr--full-ui .plyr__video-embed iframe {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
right: -3px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
z-index: 3;
|
||||
|
||||
> div {
|
||||
overflow: hidden;
|
||||
@@ -74,6 +74,7 @@
|
||||
align-items: center;
|
||||
color: $plyr-menu-color;
|
||||
display: flex;
|
||||
font-size: $plyr-font-size-menu;
|
||||
padding: ceil($plyr-control-padding / 2) ($plyr-control-padding * 2);
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// --------------------------------------------------------------
|
||||
// Faux poster overlay
|
||||
// --------------------------------------------------------------
|
||||
|
||||
.plyr__poster {
|
||||
background-color: #000;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.plyr--stopped .plyr__poster {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
@include plyr-range-track();
|
||||
background-image: linear-gradient(to right, currentColor var(--value), transparent var(--value));
|
||||
background-image: linear-gradient(to right, currentColor var(--value, 0%), transparent var(--value, 0%));
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
transform: translate(-50%, 10px) scale(0.8);
|
||||
transform-origin: 50% 100%;
|
||||
transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;
|
||||
white-space: nowrap;
|
||||
z-index: 2;
|
||||
|
||||
// The background triangle
|
||||
|
||||
@@ -23,7 +23,12 @@
|
||||
// Hide sound controls on iOS
|
||||
// It's not supported to change volume using JavaScript:
|
||||
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
|
||||
.plyr--is-ios .plyr__volume,
|
||||
.plyr--is-ios [data-plyr='mute'] {
|
||||
.plyr--is-ios .plyr__volume {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// Vimeo has no toggle mute method so hide mute button
|
||||
// https://github.com/vimeo/player.js/issues/236#issuecomment-384663183
|
||||
.plyr--is-ios.plyr--vimeo [data-plyr='mute'] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
@import 'components/embed';
|
||||
@import 'components/menus';
|
||||
@import 'components/progress';
|
||||
@import 'components/poster';
|
||||
@import 'components/sliders';
|
||||
@import 'components/times';
|
||||
@import 'components/tooltips';
|
||||
|
||||
@@ -8,8 +8,9 @@ $plyr-font-size-small: 14px !default;
|
||||
$plyr-font-size-large: 18px !default;
|
||||
$plyr-font-size-xlarge: 21px !default;
|
||||
|
||||
$plyr-font-size-time: 14px !default;
|
||||
$plyr-font-size-time: $plyr-font-size-small !default;
|
||||
$plyr-font-size-badge: 9px !default;
|
||||
$plyr-font-size-menu: $plyr-font-size-small !default;
|
||||
|
||||
$plyr-font-weight-regular: 500 !default;
|
||||
$plyr-font-weight-bold: 600 !default;
|
||||
|
||||
@@ -2,15 +2,6 @@
|
||||
// Hiding content nicely
|
||||
// --------------------------------------------------------------
|
||||
|
||||
// Attributes
|
||||
.plyr--full-ui [hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.plyr--full-ui [aria-hidden='true'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Screen reader only elements
|
||||
.plyr__sr-only {
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
|
||||
Reference in New Issue
Block a user