Compare commits

...

58 Commits

Author SHA1 Message Date
3b20dbd9fd v3.1.0 2018-04-03 22:57:34 +10:00
e4d975af00 Styling fixes 2018-04-03 22:56:19 +10:00
2782a00e7c v3.1.0-beta.2 2018-04-03 22:31:55 +10:00
91d192dd7c YouTube speed menu fix 2018-04-03 22:30:29 +10:00
b1e3abc795 v3.1.0-beta.1 2018-04-02 22:52:02 +10:00
3395e8df90 HTML5 quality selection 2018-04-02 22:40:03 +10:00
cce143a7da v3.0.11 2018-03-30 23:14:07 +11:00
d593005b32 Muted and autoplay fixes, small bug fixes 2018-03-30 23:09:17 +11:00
7be9d5d4d3 v3.0.10 2018-03-30 00:16:42 +11:00
d7141d5ed7 Controls docs, package upgrades 2018-03-30 00:11:48 +11:00
0d0ece94d3 Fix regression 2018-03-29 20:20:37 +11:00
1c06f6d06d Vimeo hotfix 2018-03-29 19:35:02 +11:00
dda8e30b92 Merge branch 'master' of github.com:sampotts/plyr 2018-03-28 22:45:18 +11:00
c4e2e24643 Bug fixes 2018-03-28 22:45:11 +11:00
e020a105a3 Update readme.md 2018-03-28 00:55:55 +11:00
2b7fe9a4f9 v3.0.6 2018-03-28 00:17:15 +11:00
951df64b7f v3.0.5 2018-03-27 23:52:26 +11:00
0976afe282 v3.0.4 2018-03-27 23:47:58 +11:00
7b1e4abda7 Controls fixes 2018-03-27 23:43:38 +11:00
0cf75eed3f Revert API method change 2018-03-27 21:15:11 +11:00
d96957d086 Allow fullscreen in iframe 2018-03-27 21:13:22 +11:00
1a032ea498 Fix for seeking issue 2018-03-27 21:10:06 +11:00
5d079da1b8 Use object.entries 2018-03-27 10:41:06 +11:00
9c1bc6ab08 Fixes for fast forward and issues with event.preventDefault() 2018-03-27 10:36:08 +11:00
3d2ba8c009 Update readme.md 2018-03-22 09:11:42 +11:00
e872ce3f77 Update readme.md 2018-03-22 09:10:50 +11:00
b77756da04 Typo 2018-03-22 01:15:10 +11:00
9b23e13ce8 v3.0.3 2018-03-22 01:13:37 +11:00
5eafe9baff Vimeo offset tweak (fixes #826) 2018-03-22 01:08:08 +11:00
c251c94131 Fix for .stop() method (fixes #819) 2018-03-22 01:02:38 +11:00
17041efc71 Check for array for speed options (fixes #252) 2018-03-22 00:33:14 +11:00
05b8e8a6e0 Restore as float (fixes #828) 2018-03-22 00:28:42 +11:00
f998b996fa Fix for Firefox fullscreen oddness (Fixes #821) 2018-03-22 00:26:01 +11:00
958b47c435 Merge branch 'master' of github.com:sampotts/plyr 2018-03-22 00:06:26 +11:00
a27248d3b6 Merge pull request #820 from saadshahd/patch-1
Fix fast-forward control
2018-03-22 00:05:24 +11:00
1b1f7be7ff Merge branch 'master' of github.com:sampotts/plyr 2018-03-22 00:04:34 +11:00
59d4a27240 Improve Sprite checking (fixes #827) 2018-03-22 00:04:28 +11:00
75e9f3c2e3 Fix fast-forward control
fast-forward control doesn't work.
2018-03-21 12:15:57 +02:00
7132eccf50 Merge pull request #822 from DanielRuf/patch/fix-options-link
fix the options link in the readme
2018-03-21 09:12:06 +11:00
e953c6398c fix the options link in the readme 2018-03-20 15:05:51 +01:00
bb7eea27e5 v3.0.2 2018-03-18 22:46:36 +11:00
595c5e95bc Fix for Safari with adblockers 2018-03-18 22:37:06 +11:00
43e6dcd41d Fix for local storage issue 2018-03-18 01:37:24 +11:00
b06c8ae43f Changelog updated 2018-03-18 01:14:18 +11:00
c7ea13c0c7 Sentry in live only 2018-03-18 01:08:05 +11:00
0f8c6e147b Added Sentry 2018-03-18 00:21:23 +11:00
e566365288 Typo 2018-03-17 23:44:40 +11:00
a06e0f5890 Updated screenshot 2018-03-17 23:40:28 +11:00
3bccc0da01 v3.0.0 2018-03-17 23:33:25 +11:00
a0173d991e Removed beta message 2018-03-17 23:31:34 +11:00
600f0eb8a3 Merge branch 'beta'
# Conflicts:
#	readme.md
2018-03-17 23:30:16 +11:00
5db73b1327 Added buffered getter 2018-03-17 23:27:40 +11:00
5cb1628cd8 Vimeo fix 2018-03-15 10:29:05 +11:00
c74b75e8e1 3.0.0-beta.20 2018-03-13 23:35:17 +11:00
e0562752ea Merge pull request #795 from frogg/patch-1
Added link that explains Webkit's autoplay blocker
2018-03-10 23:29:22 +11:00
e6db374a72 Added link that explains Webkit's autoplay blocker 2018-02-24 16:19:55 +01:00
ab7f277a1b Merge pull request #769 from redxtech/add-vue-plyr-to-readme
Add vue-plyr to readme
2018-02-06 10:51:42 +11:00
d5a1a7ca1c Add vue-plyr to readme 2018-01-28 23:03:05 -07:00
47 changed files with 9424 additions and 3313 deletions

View File

@ -9,6 +9,7 @@
"ignore": ["attribute", "class"]
}
],
"string-no-newline": null,
"indentation": 4,
"string-quotes": "single",
"max-nesting-depth": 2,

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost/dev/plyr/demo",
"webRoot": "${workspaceFolder}"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,72 @@
# Controls
This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs.
This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. You can pass the following to the `controls` option:
## Internationalization using default controls
* `Array` of options (this builds the default controls based on your choices)
* `String` containing the desired HTML
* `Function` that will be executed and should return one of the above
## Using default controls
If you want to use the standard controls as they are, you don't need to pass any options. If you want to turn on off controls, here's the full list:
```javascript
controls: [
'play-large', // The large play button in the center
'restart', // Restart playback
'rewind', // Rewind by the seek time (default 10 seconds)
'play', // Play/pause playback
'fast-forward', // Fast forward by the seek time (default 10 seconds)
'progress', // The progress bar and scrubber for playback and buffering
'current-time', // The current time of playback
'duration', // The full duration of the media
'mute', // Toggle mute
'volume', // Volume control
'captions', // Toggle captions
'settings', // Settings menu
'pip', // Picture-in-picture (currently Safari only)
'airplay', // Airplay (currently Safari only)
'fullscreen', // Toggle fullscreen
];
```
### Internationalization using default controls
You can provide an `i18n` object as one of your options when initialising the plugin which we be used when rendering the controls.
### Example
#### Example
```javascript
i18n: {
restart: "Restart",
rewind: "Rewind {seektime} secs",
play: "Play",
pause: "Pause",
forward: "Forward {seektime} secs",
buffered: "buffered",
currentTime: "Current time",
duration: "Duration",
volume: "Volume",
toggleMute: "Toggle Mute",
toggleCaptions: "Toggle Captions",
toggleFullscreen: "Toggle Fullscreen"
restart: 'Restart',
rewind: 'Rewind {seektime} secs',
play: 'Play',
pause: 'Pause',
fastForward: 'Forward {seektime} secs',
seek: 'Seek',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
duration: 'Duration',
volume: 'Volume',
mute: 'Mute',
unmute: 'Unmute',
enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
speed: 'Speed',
quality: 'Quality',
loop: 'Loop',
start: 'Start',
end: 'End',
all: 'All',
reset: 'Reset',
disabled: 'Disabled',
advertisement: 'Ad',
}
```
@ -29,85 +74,78 @@ Note: `{seektime}` will be replaced with your configured seek time or the defaul
## Using custom HTML
You can specify the HTML for the controls using the `html` option.
You can specify the HTML as a `String` or your `Function` return for the controls using the `controls` option.
The classes and data attributes used in your template should match the `selectors` option.
The classes and data attributes used in your template should match the `selectors` option if you change any.
You need to add several placeholders to your html template that are replaced when rendering:
You need to add several placeholders to your HTML template that are replaced when rendering:
- `{id}` - the dynamically generated ID for the player (for form controls)
- `{seektime}` - the seek time specified in options for fast forward and rewind
- `{title}` - the title of your media, if specified
* `{id}` - the dynamically generated ID for the player (for form controls)
* `{seektime}` - the seek time specified in options for fast forward and rewind
* `{title}` - the title of your media, if specified
You can include only the controls you need when specifying custom html.
### Limitations
* Currently the settings menus are not supported with custom controls HTML
* AirPlay and PiP buttons can be added but you will have to manage feature detection
### Example
This is an example `html` option with all controls.
Here's an example of custom controls markup (this is just all default controls shown).
```javascript
var controls = ["<div class='plyr__controls'>",
"<button type='button' data-plyr='restart'>",
"<svg><use xlink:href='#plyr-restart'></use></svg>",
"<span class='plyr__sr-only'>Restart</span>",
"</button>",
"<button type='button' data-plyr='rewind'>",
"<svg><use xlink:href='#plyr-rewind'></use></svg>",
"<span class='plyr__sr-only'>Rewind {seektime} secs</span>",
"</button>",
"<button type='button' data-plyr='play'>",
"<svg><use xlink:href='#plyr-play'></use></svg>",
"<span class='plyr__sr-only'>Play</span>",
"</button>",
"<button type='button' data-plyr='pause'>",
"<svg><use xlink:href='#plyr-pause'></use></svg>",
"<span class='plyr__sr-only'>Pause</span>",
"</button>",
"<button type='button' data-plyr='fast-forward'>",
"<svg><use xlink:href='#plyr-fast-forward'></use></svg>",
"<span class='plyr__sr-only'>Forward {seektime} secs</span>",
"</button>",
"<span class='plyr__progress'>",
"<label for='seek{id}' class='plyr__sr-only'>Seek</label>",
"<input id='seek{id}' class='plyr__progress--seek' type='range' min='0' max='100' step='0.1' value='0' data-plyr='seek'>",
"<progress class='plyr__progress--played' max='100' value='0' role='presentation'></progress>",
"<progress class='plyr__progress--buffer' max='100' value='0'>",
"<span>0</span>% buffered",
"</progress>",
"<span class='plyr__tooltip'>00:00</span>",
"</span>",
"<span class='plyr__time'>",
"<span class='plyr__sr-only'>Current time</span>",
"<span class='plyr__time--current'>00:00</span>",
"</span>",
"<span class='plyr__time'>",
"<span class='plyr__sr-only'>Duration</span>",
"<span class='plyr__time--duration'>00:00</span>",
"</span>",
"<button type='button' data-plyr='mute'>",
"<svg class='icon--muted'><use xlink:href='#plyr-muted'></use></svg>",
"<svg><use xlink:href='#plyr-volume'></use></svg>",
"<span class='plyr__sr-only'>Toggle Mute</span>",
"</button>",
"<span class='plyr__volume'>",
"<label for='volume{id}' class='plyr__sr-only'>Volume</label>",
"<input id='volume{id}' class='plyr__volume--input' type='range' min='0' max='10' value='5' data-plyr='volume'>",
"<progress class='plyr__volume--display' max='10' value='0' role='presentation'></progress>",
"</span>",
"<button type='button' data-plyr='captions'>",
"<svg class='icon--captions-on'><use xlink:href='#plyr-captions-on'></use></svg>",
"<svg><use xlink:href='#plyr-captions-off'></use></svg>",
"<span class='plyr__sr-only'>Toggle Captions</span>",
"</button>",
"<button type='button' data-plyr='fullscreen'>",
"<svg class='icon--exit-fullscreen'><use xlink:href='#plyr-exit-fullscreen'></use></svg>",
"<svg><use xlink:href='#plyr-enter-fullscreen'></use></svg>",
"<span class='plyr__sr-only'>Toggle Fullscreen</span>",
"</button>",
"</div>"].join("");
const controls = `
<div class="plyr__controls">
<button type="button" class="plyr__control" data-plyr="restart">
<svg role="presentation"><use xlink:href="#plyr-restart"></use></svg>
<span class="plyr__tooltip" role="tooltip">Restart</span>
</button>
<button type="button" class="plyr__control" data-plyr="rewind">
<svg role="presentation"><use xlink:href="#plyr-rewind"></use></svg>
<span class="plyr__tooltip" role="tooltip">Rewind {seektime} secs</span>
</button>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Play, {title}" data-plyr="play">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-pause"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-play"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Pause</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Play</span>
</button>
<button type="button" class="plyr__control" data-plyr="fast-forward">
<svg role="presentation"><use xlink:href="#plyr-fast-forward"></use></svg>
<span class="plyr__tooltip" role="tooltip">Forward {seektime} secs</span>
</button>
<div class="plyr__progress">
<label for="plyr-seek-{id}" class="plyr__sr-only">Seek</label>
<input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" id="plyr-seek-{id}">
<progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress>
<span role="tooltip" class="plyr__tooltip">00:00</span>
</div>
<div class="plyr__time">00:00</div>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span>
</button>
<div class="plyr__volume">
<label for="plyr-volume-{id}" class="plyr__sr-only">Volume</label>
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" id="plyr-volume-{id}">
</div>
<button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-captions-off"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Disable captions</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enable captions</span>
</button>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Enter fullscreen" data-plyr="fullscreen">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enter fullscreen</span>
</button>
</div>
`;
// Setup the player
plyr.setup('.js-player', {
html: controls
});
const player = new Plyr('#player', { controls });
```

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

4348
demo/dist/demo.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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>
@ -163,25 +165,26 @@
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"></path>
</svg>
<p>If you think Plyr's good,
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&url=http%3A%2F%2Fplyr.io&via=Sam_Potts"
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank" data-shr-network="twitter">tweet it</a>
</p>
</aside>
<!-- Polyfills -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent"></script>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values"
crossorigin="anonymous"></script>
<!-- Plyr core script -->
<script src="../dist/plyr.js"></script>
<script src="../dist/plyr.js" crossorigin="anonymous"></script>
<!-- Sharing libary (https://shr.one) -->
<script src="https://cdn.shr.one/1.0.1/shr.js"></script>
<script src="https://cdn.shr.one/1.0.1/shr.js" crossorigin="anonymous"></script>
<!-- Rangetouch to fix <input type="range"> on touch devices (see https://rangetouch.com) -->
<script src="https://cdn.rangetouch.com/1.0.1/rangetouch.js" async></script>
<script src="https://cdn.rangetouch.com/1.0.1/rangetouch.js" async crossorigin="anonymous"></script>
<!-- Docs script -->
<script src="dist/demo.js"></script>
<script src="dist/demo.js" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -4,242 +4,292 @@
// Please see readme.md in the root or github.com/sampotts/plyr
// ==========================================================================
document.addEventListener('DOMContentLoaded', () => {
if (window.shr) {
window.shr.setup({
count: {
classname: 'button__count',
},
});
import Raven from 'raven-js';
(() => {
const isLive = window.location.host === 'plyr.io';
// Raven / Sentry
// For demo site (https://plyr.io) only
if (isLive) {
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
}
// Setup tab focus
const tabClassName = 'tab-focus';
document.addEventListener('DOMContentLoaded', () => {
Raven.context(() => {
if (window.shr) {
window.shr.setup({
count: {
classname: 'button__count',
},
});
}
// Remove class on blur
document.addEventListener('focusout', event => {
event.target.classList.remove(tabClassName);
});
// Setup tab focus
const tabClassName = 'tab-focus';
// Add classname to tabbed elements
document.addEventListener('keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Remove class on blur
document.addEventListener('focusout', event => {
event.target.classList.remove(tabClassName);
});
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
document.activeElement.classList.add(tabClassName);
}, 0);
});
// Add classname to tabbed elements
document.addEventListener('keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Setup the player
const player = new Plyr('#player', {
debug: true,
title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
enabled: true,
},
});
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
document.activeElement.classList.add(tabClassName);
}, 0);
});
// Expose for tinkering in the console
window.player = player;
// Setup the player
const player = new Plyr('video', {
debug: true,
title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
/* controls: [
'play-large',
'restart',
'rewind',
'play',
'fast-forward',
'progress',
'current-time',
'duration',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
'fullscreen',
], */
captions: {
active: true,
},
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
enabled: true,
publisherId: '918848828995742',
},
});
// Setup type toggle
const buttons = document.querySelectorAll('[data-source]');
const types = {
video: 'video',
audio: 'audio',
youtube: 'youtube',
vimeo: 'vimeo',
};
let currentType = window.location.hash.replace('#', '');
const historySupport = window.history && window.history.pushState;
// Expose for tinkering in the console
window.player = player;
// Toggle class on an element
function toggleClass(element, className, state) {
if (element) {
element.classList[state ? 'add' : 'remove'](className);
}
}
// Setup type toggle
const buttons = document.querySelectorAll('[data-source]');
const types = {
video: 'video',
audio: 'audio',
youtube: 'youtube',
vimeo: 'vimeo',
};
let currentType = window.location.hash.replace('#', '');
const historySupport = window.history && window.history.pushState;
// Set a new source
function newSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (!(type in types) || (!init && type === currentType) || (!currentType.length && type === types.video)) {
return;
}
// Toggle class on an element
function toggleClass(element, className, state) {
if (element) {
element.classList[state ? 'add' : 'remove'](className);
}
}
switch (type) {
case types.video:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4',
type: 'video/mp4',
}],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
};
// Set a new source
function newSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (!(type in types) || (!init && type === currentType) || (!currentType.length && type === types.video)) {
return;
}
break;
switch (type) {
case types.video:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
type: 'video/mp4',
size: 576,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
type: 'video/mp4',
size: 720,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
type: 'video/mp4',
size: 1080,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
type: 'video/mp4',
size: 1440,
},
],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
};
case types.audio:
player.source = {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
};
break;
break;
case types.audio:
player.source = {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
};
case types.youtube:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
}],
};
break;
break;
case types.youtube:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
}],
};
case types.vimeo:
player.source = {
type: 'video',
sources: [{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
}],
};
break;
break;
case types.vimeo:
player.source = {
type: 'video',
sources: [{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
}],
};
default:
break;
}
break;
// Set the current type for next time
currentType = type;
default:
break;
}
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Set the current type for next time
currentType = type;
// Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
cite.setAttribute('hidden', '');
});
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
}
// Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Bind to each button
Array.from(buttons).forEach(button => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
cite.setAttribute('hidden', '');
});
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
}
newSource(type);
// Bind to each button
Array.from(buttons).forEach(button => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
newSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && 'type' in event.state) {
newSource(event.state.type);
}
});
// On load
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
const video = !currentType.length;
// If there's no current type set, assume video
if (video) {
currentType = types.video;
}
// Replace current history state
if (currentType in types) {
window.history.replaceState(
{
type: currentType,
},
'',
video ? '' : `#${currentType}`,
);
}
// If it's not video, load the source
if (currentType !== types.video) {
newSource(currentType, true);
}
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && 'type' in event.state) {
newSource(event.state.type);
}
});
// On load
if (historySupport) {
const video = !currentType.length;
// If there's no current type set, assume video
if (video) {
currentType = types.video;
}
// Replace current history state
if (currentType in types) {
window.history.replaceState(
{
type: currentType,
},
'',
video ? '' : `#${currentType}`,
);
}
// If it's not video, load the source
if (currentType !== types.video) {
newSource(currentType, true);
}
// Google analytics
// For demo site (https://plyr.io) only
/* eslint-disable */
if (isLive) {
(function(i, s, o, g, r, a, m) {
i.GoogleAnalyticsObject = r;
i[r] =
i[r] ||
function() {
(i[r].q = i[r].q || []).push(arguments);
};
i[r].l = 1 * new Date();
a = s.createElement(o);
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
window.ga('create', 'UA-40881672-11', 'auto');
window.ga('send', 'pageview');
}
});
// Google analytics
// For demo site (https://plyr.io) only
/* eslint-disable */
if (window.location.host === 'plyr.io') {
(function(i, s, o, g, r, a, m) {
i.GoogleAnalyticsObject = r;
i[r] =
i[r] ||
function() {
(i[r].q = i[r].q || []).push(arguments);
};
i[r].l = 1 * new Date();
a = s.createElement(o);
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
window.ga('create', 'UA-40881672-11', 'auto');
window.ga('send', 'pageview');
}
/* eslint-enable */
/* eslint-enable */
})();

View File

@ -16,3 +16,4 @@ $plyr-font-size-captions-base: $plyr-font-size-base;
$plyr-font-size-captions-small: $plyr-font-size-small;
$plyr-font-size-captions-medium: 18px;
$plyr-font-size-captions-large: 21px;
$plyr-font-size-menu: $plyr-font-size-base;

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

1847
dist/plyr.js vendored

File diff suppressed because it is too large Load Diff

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2131
dist/plyr.polyfilled.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -70,10 +70,11 @@ const paths = {
root: path.join(root, 'demo/'),
},
upload: [
path.join(root, `dist/*${minSuffix}.js`),
path.join(root, `dist/*${minSuffix}.*`),
path.join(root, 'dist/*.css'),
path.join(root, 'dist/*.svg'),
path.join(root, 'demo/dist/**'),
path.join(root, `demo/dist/*${minSuffix}.*`),
path.join(root, 'demo/dist/*.css'),
],
};
@ -303,22 +304,26 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
console.log(`Uploading '${version}' to ${aws.cdn.domain}...`);
// Upload to CDN
return gulp
.src(paths.upload)
.pipe(
rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
}),
)
.pipe(
size({
showFiles: true,
gzip: true,
}),
)
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.cdn, options.cdn));
return (
gulp
.src(paths.upload)
.pipe(
rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
}),
)
// Remove min suffix from source map URL
.pipe(replace(/sourceMappingURL=([\w-?.]+)/, (match, p1) => `sourceMappingURL=${p1.replace(minSuffix, '')}`))
.pipe(
size({
showFiles: true,
gzip: true,
}),
)
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.cdn, options.cdn))
);
});
// Publish to demo bucket

View File

@ -1,6 +1,6 @@
{
"name": "plyr",
"version": "3.0.0-beta.19",
"version": "3.1.0",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"main": "./dist/plyr.js",
@ -13,38 +13,40 @@
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.6.1",
"del": "^3.0.0",
"eslint": "^4.18.2",
"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.10.0",
"git-branch": "^2.0.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^5.0.0",
"gulp-better-rollup": "^3.0.0",
"gulp-better-rollup": "^3.1.0",
"gulp-clean-css": "^3.9.3",
"gulp-concat": "^2.6.1",
"gulp-filter": "^5.1.0",
"gulp-open": "^3.0.0",
"gulp-open": "^3.0.1",
"gulp-rename": "^1.2.2",
"gulp-replace": "^0.6.1",
"gulp-s3": "^0.11.0",
"gulp-sass": "^3.1.0",
"gulp-sass": "^3.2.1",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.4",
"gulp-svgmin": "^1.2.4",
"gulp-svgstore": "^6.1.1",
"gulp-uglify-es": "^1.0.1",
"gulp-util": "^3.0.8",
"prettier-eslint": "^8.8.1",
"prettier-stylelint": "^0.4.2",
"rollup-plugin-babel": "^3.0.3",
"rollup-plugin-commonjs": "^8.4.1",
"rollup-plugin-node-resolve": "^3.2.0",
"rollup-plugin-commonjs": "^9.1.0",
"rollup-plugin-node-resolve": "^3.3.0",
"run-sequence": "^2.2.1",
"stylelint": "^9.1.1",
"stylelint": "^9.2.0",
"stylelint-config-prettier": "^3.0.4",
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.0.0",
"stylelint-order": "^0.8.1",
"stylelint-scss": "^2.5.0",
"stylelint-scss": "^3.0.0",
"stylelint-selector-bem-pattern": "^2.0.0"
},
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
@ -65,6 +67,8 @@
"author": "Sam Potts <sam@potts.es>",
"dependencies": {
"babel-polyfill": "^6.26.0",
"custom-event-polyfill": "^0.3.0"
"custom-event-polyfill": "^0.3.0",
"loadjs": "^3.5.4",
"raven-js": "^3.24.0"
}
}

View File

@ -1,30 +1,30 @@
---
Beware: This version is currently in beta and not production-ready
---
# Plyr
A simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo media player that supports [_modern_](#browser-support) browsers.
[Checkout the demo](https://plyr.io) - [Donate to support Plyr](#donate) - [Chat on Slack](https://bit.ly/plyr-slack)
[Checkout the demo](https://plyr.io) - [Donate to support Plyr](#donate) - [Chat on Slack](https://bit.ly/plyr-chat)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png)](https://plyr.io)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io)
## Features
* **Accessible** - full support for VTT captions and screen readers
* **Lightweight** - just 18KB minified and gzipped
* **[Customisable](#html)** - make the player look how you want with the markup you want
* **Semantic** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
* **Good HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
`<span>` or `<a href="#">` button hacks
* **Responsive** - works with any screen size
* **HTML Video & Audio** - support for both formats
* **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback
* **[Monetization](#ads)** - make money from your videos
* **[Streaming](#streaming)** - support for hls.js, Shaka and dash.js streaming playback
* **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
* **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
* **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
* **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
* **Picture-in-Picture** - supports Safari's picture-in-picture mode
* **Playsinline** - supports the `playsinline` attribute
* **Speed controls** - adjust speed on the fly
* **Multiple captions** - support for multiple caption tracks
* **i18n support** - support for internationalization of controls
* **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
* **SASS** - to include in your build processes
@ -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.0-beta.19/plyr.js"></script>
<script src="https://cdn.plyr.io/3.1.0/plyr.js"></script>
```
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
@ -144,13 +144,23 @@ Include the `plyr.css` stylsheet into your `<head>`
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.0.0-beta.19/plyr.css">
<link rel="stylesheet" href="https://cdn.plyr.io/3.1.0/plyr.css">
```
### SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.0.0-beta.19/plyr.svg`.
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.1.0/plyr.svg`.
## Ads
Plyr has partnered up with [vi.ai](http://vi.ai/publisher-video-monetization/?aid=plyrio) to offer monetization options for your videos. Getting setup is easy:
* [Sign up for a vi.ai account](http://vi.ai/publisher-video-monetization/?aid=plyrio)
* Grab your publisher ID from the code snippet
* Enable ads in the [config options](#options) and enter your publisher ID
Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues.
## Advanced
@ -230,7 +240,7 @@ The NodeList, HTMLElement or string selector can be the target `<video>`, `<audi
const players = Array.from(document.querySelectorAll('.js-player')).map(player => new Plyr(player));
```
The second argument for the constructor is the [#options](options) object:
The second argument for the constructor is the [options](#options) object:
```javascript
const player = new Plyr('#player', {
@ -284,6 +294,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. |
| `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. |
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
| `ads` | Object | `{ enabled: false, publisherId: '' }` | `enabled`: Whether to enable vi.ai ads. `publisherId`: Your unique vi.ai publisher ID. |
1. Vimeo only
@ -366,6 +377,7 @@ player.fullscreen.active; // false;
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
| `seeking` | ✓ | - | Returns a boolean indicating if the current player is seeking. |
| `duration` | ✓ | - | Returns the duration for the current media. |
@ -695,7 +707,7 @@ Credit to the PayPal HTML5 Video player from which Plyr's caption functionality
## Thanks
[![Fastly](https://www.fastly.com/sites/all/themes/custom/fastly2016/logo.png)](https://www.fastly.com/)
[![Fastly](https://cdn.plyr.io/static/demo/fastly-logo.png)](https://www.fastly.com/)
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.

159
src/js/controls.js vendored
View File

@ -5,7 +5,9 @@
import support from './support';
import utils from './utils';
import ui from './ui';
import i18n from './i18n';
import captions from './captions';
import html5 from './html5';
// Sniff out the browser
const browser = utils.getBrowser();
@ -74,7 +76,7 @@ const controls = {
// Create hidden text label
createLabel(type, attr) {
let text = this.config.i18n[type];
let text = i18n.get(type, this.config);
const attributes = Object.assign({}, attr);
switch (type) {
@ -126,7 +128,7 @@ const controls = {
createButton(buttonType, attr) {
const button = utils.createElement('button');
const attributes = Object.assign({}, attr);
let type = buttonType;
let type = utils.toCamelCase(buttonType);
let toggle = false;
let label;
@ -147,7 +149,7 @@ const controls = {
}
// Large play button
switch (type) {
switch (buttonType) {
case 'play':
toggle = true;
label = 'play';
@ -189,7 +191,7 @@ const controls = {
default:
label = type;
icon = type;
icon = buttonType;
}
// Setup toggle icon and labels
@ -204,7 +206,7 @@ const controls = {
// Add aria attributes
attributes['aria-pressed'] = false;
attributes['aria-label'] = this.config.i18n[label];
attributes['aria-label'] = i18n.get(label, this.config);
} else {
button.appendChild(controls.createIcon.call(this, icon));
button.appendChild(controls.createLabel.call(this, label));
@ -238,7 +240,7 @@ const controls = {
for: attributes.id,
class: this.config.classNames.hidden,
},
this.config.i18n[type],
i18n.get(type, this.config),
);
// Seek input
@ -291,11 +293,11 @@ const controls = {
let suffix = '';
switch (type) {
case 'played':
suffix = this.config.i18n.played;
suffix = i18n.get('played', this.config);
break;
case 'buffer':
suffix = this.config.i18n.buffered;
suffix = i18n.get('buffered', this.config);
break;
default:
@ -322,7 +324,7 @@ const controls = {
{
class: this.config.classNames.hidden,
},
this.config.i18n[type],
i18n.get(type, this.config),
),
);
@ -383,6 +385,16 @@ const controls = {
const clientRect = this.elements.inputs.seek.getBoundingClientRect();
const visible = `${this.config.classNames.tooltip}--visible`;
const toggle = toggle => {
utils.toggleClass(this.elements.display.seekTooltip, visible, toggle);
};
// Hide on touch
if (this.touch) {
toggle(false);
return;
}
// Determine percentage, if already visible
if (utils.is.event(event)) {
percent = 100 / clientRect.width * (event.pageX - clientRect.left);
@ -411,7 +423,7 @@ const controls = {
'mouseenter',
'mouseleave',
].includes(event.type)) {
utils.toggleClass(this.elements.display.seekTooltip, visible, event.type === 'mouseenter');
toggle(event.type === 'mouseenter');
}
},
@ -424,8 +436,8 @@ const controls = {
utils.toggleHidden(pane, !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)) {
@ -438,12 +450,10 @@ 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);
// If we're hiding, nothing more to do
@ -459,20 +469,18 @@ 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:
label = 'SD';
break;
default:
@ -486,9 +494,14 @@ 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);
},
@ -501,28 +514,10 @@ const controls = {
return value === 1 ? 'Normal' : `${value}&times;`;
case 'quality':
switch (value) {
case 'hd2160':
return '2160P';
case 'hd1440':
return '1440P';
case 'hd1080':
return '1080P';
case 'hd720':
return '720P';
case 'large':
return '480P';
case 'medium':
return '360P';
case 'small':
return '240P';
case 'tiny':
return 'Tiny';
case 'default':
return 'Auto';
default:
return value;
if (utils.is.number(value)) {
return `${value}p`;
}
return utils.toTitleCase(value);
case 'captions':
return controls.getLanguage.call(this);
@ -533,18 +528,18 @@ 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 : '';
value = this.captions.active ? this.captions.language : i18n.get('disabled', this.config);
break;
default:
value = this[setting];
value = !utils.is.empty(input) ? input : this[setting];
// Get default
if (utils.is.empty(value)) {
@ -552,7 +547,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;
}
@ -617,7 +612,7 @@ const controls = {
class: this.config.classNames.control,
'data-plyr-loop-action': option,
}),
this.config.i18n[option]
i18n.get(option, this.config)
);
if (['start', 'end'].includes(option)) {
@ -637,11 +632,7 @@ const controls = {
return null;
}
if (!support.textTracks || !captions.getTracks.call(this).length) {
return this.config.i18n.none;
}
if (this.captions.active) {
if (support.textTracks && captions.getTracks.call(this).length && this.captions.active) {
const currentTrack = captions.getCurrentTrack.call(this);
if (utils.is.track(currentTrack)) {
@ -649,7 +640,7 @@ const controls = {
}
}
return this.config.i18n.disabled;
return i18n.get('disabled', this.config);
},
// Set a list of available captions languages
@ -659,14 +650,14 @@ 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);
// If there's no captions, bail
if (!hasTracks) {
if (!toggle) {
return;
}
@ -676,10 +667,10 @@ const controls = {
label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(),
}));
// Add the "None" option to turn off captions
// Add the "Disabled" option to turn off captions
tracks.unshift({
language: '',
label: this.config.i18n.none,
label: i18n.get('disabled', this.config),
});
// Generate options
@ -699,7 +690,12 @@ const controls = {
},
// Set a list of available captions languages
setSpeedMenu() {
setSpeedMenu(options) {
// Do nothing if not selected
if (!this.config.controls.includes('settings') || !this.config.settings.includes('speed')) {
return;
}
// Menu required
if (!utils.is.element(this.elements.settings.panes.speed)) {
return;
@ -707,8 +703,10 @@ const controls = {
const type = 'speed';
// Set the default speeds
if (!utils.is.object(this.options.speed) || !Object.keys(this.options.speed).length) {
// Set the speed options
if (utils.is.array(options)) {
this.options.speed = options;
} else if (this.isHTML5 || this.isVimeo) {
this.options.speed = [
0.5,
0.75,
@ -724,9 +722,12 @@ const controls = {
this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
// Toggle the pane and tab
const toggle = !utils.is.empty(this.options.speed);
const toggle = !utils.is.empty(this.options.speed) && this.options.speed.length > 1;
controls.toggleTab.call(this, type, toggle);
// Check if we need to toggle the parent
controls.checkMenu.call(this);
// If we're hiding, nothing more to do
if (!toggle) {
return;
@ -743,11 +744,22 @@ const controls = {
utils.emptyElement(list);
// Create items
this.options.speed.forEach(speed => controls.createMenuItem.call(this, speed, list, type, controls.getLabel.call(this, 'speed', speed)));
this.options.speed.forEach(speed => {
const label = controls.getLabel.call(this, 'speed', speed);
controls.createMenuItem.call(this, speed, list, type, label);
});
controls.updateSetting.call(this, type, list);
},
// Check if we need to hide/show the settings menu
checkMenu() {
const { tabs } = this.elements.settings;
const visible = !utils.is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden);
utils.toggleHidden(this.elements.settings.menu, !visible);
},
// Show/hide menu
toggleMenu(event) {
const { form } = this.elements.settings;
@ -1018,6 +1030,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(
@ -1069,7 +1082,7 @@ const controls = {
'aria-controls': `plyr-settings-${data.id}-${type}`,
'aria-expanded': false,
}),
this.config.i18n[type],
i18n.get(type, this.config),
);
const value = utils.createElement('span', {
@ -1109,7 +1122,7 @@ const controls = {
'aria-controls': `plyr-settings-${data.id}-home`,
'aria-expanded': false,
},
this.config.i18n[type],
i18n.get(type, this.config),
);
pane.appendChild(back);
@ -1152,10 +1165,12 @@ const controls = {
this.elements.controls = container;
if (this.config.controls.includes('settings') && this.config.settings.includes('speed')) {
controls.setSpeedMenu.call(this);
if (this.isHTML5) {
controls.setQualityMenu.call(this, html5.getQualityOptions.call(this));
}
controls.setSpeedMenu.call(this);
return container;
},

View File

@ -56,24 +56,26 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.0.0-beta.19/plyr.svg',
iconUrl: 'https://cdn.plyr.io/3.1.0/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"
],
},
@ -132,7 +134,10 @@ const defaults = {
// Default controls
controls: [
'play-large',
// 'restart',
// 'rewind',
'play',
// 'fast-forward',
'progress',
'current-time',
'mute',
@ -155,7 +160,7 @@ const defaults = {
rewind: 'Rewind {seektime} secs',
play: 'Play',
pause: 'Pause',
forward: 'Forward {seektime} secs',
fastForward: 'Forward {seektime} secs',
seek: 'Seek',
played: 'Played',
buffered: 'Buffered',
@ -178,7 +183,6 @@ const defaults = {
end: 'End',
all: 'All',
reset: 'Reset',
none: 'None',
disabled: 'Disabled',
advertisement: 'Ad',
},
@ -203,7 +207,7 @@ const defaults = {
pause: null,
restart: null,
rewind: null,
forward: null,
fastForward: null,
mute: null,
volume: null,
captions: null,
@ -283,7 +287,7 @@ const defaults = {
pause: '[data-plyr="pause"]',
restart: '[data-plyr="restart"]',
rewind: '[data-plyr="rewind"]',
forward: '[data-plyr="fast-forward"]',
fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]',
fullscreen: '[data-plyr="fullscreen"]',
@ -376,7 +380,7 @@ const defaults = {
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
enabled: false,
publisherId: '918848828995742',
publisherId: '',
},
};

View File

@ -1,5 +1,6 @@
// ==========================================================================
// Fullscreen wrapper
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing
// ==========================================================================
import utils from './utils';
@ -54,6 +55,7 @@ class Fullscreen {
// Get prefix
this.prefix = Fullscreen.prefix;
this.name = Fullscreen.name;
// Scroll position
this.scrollPosition = { x: 0, y: 0 };
@ -66,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 (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();
}
@ -85,7 +89,7 @@ class Fullscreen {
// Get the prefix for handlers
static get prefix() {
// No prefix
if (utils.is.function(document.cancelFullScreen)) {
if (utils.is.function(document.exitFullscreen)) {
return false;
}
@ -98,12 +102,9 @@ class Fullscreen {
];
prefixes.some(pre => {
if (utils.is.function(document[`${pre}CancelFullScreen`])) {
if (utils.is.function(document[`${pre}ExitFullscreen`]) || utils.is.function(document[`${pre}CancelFullScreen`])) {
value = pre;
return true;
} else if (utils.is.function(document.msExitFullscreen)) {
value = 'ms';
return true;
}
return false;
@ -112,11 +113,18 @@ class Fullscreen {
return value;
}
static get name() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
}
// Determine if fullscreen is enabled
get enabled() {
const fallback = this.player.config.fullscreen.fallback && !utils.inFrame();
return (Fullscreen.native || fallback) && this.player.config.fullscreen.enabled && this.player.supported.ui && this.player.isVideo;
return (
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
this.player.config.fullscreen.enabled &&
this.player.supported.ui &&
this.player.isVideo
);
}
// Get active state
@ -130,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}FullscreenElement`];
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.name}Element`];
return element === this.target;
}
@ -166,9 +174,9 @@ class Fullscreen {
} else if (!Fullscreen.native) {
toggleFallback.call(this, true);
} else if (!this.prefix) {
this.target.requestFullScreen();
this.target.requestFullscreen();
} else if (!utils.is.empty(this.prefix)) {
this.target[`${this.prefix}${this.prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen'}`]();
this.target[`${this.prefix}Request${this.name}`]();
}
}
@ -187,7 +195,8 @@ class Fullscreen {
} else if (!this.prefix) {
document.cancelFullScreen();
} else if (!utils.is.empty(this.prefix)) {
document[`${this.prefix}${this.prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen'}`]();
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.name}`]();
}
}

146
src/js/html5.js Normal file
View File

@ -0,0 +1,146 @@
// ==========================================================================
// Plyr HTML5 helpers
// ==========================================================================
import support from './support';
import utils from './utils';
const html5 = {
getSources() {
if (!this.isHTML5) {
return null;
}
return this.media.querySelectorAll('source');
},
// Get quality levels
getQualityOptions() {
if (!this.isHTML5) {
return null;
}
// Get sources
const sources = html5.getSources.call(this);
if (utils.is.empty(sources)) {
return null;
}
// Get <source> with size attribute
const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size')));
// If none, bail
if (utils.is.empty(sizes)) {
return null;
}
// Reduce to unique list
return utils.dedupe(sizes.map(source => Number(source.getAttribute('size'))));
},
extend() {
if (!this.isHTML5) {
return;
}
const player = this;
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
// Get sources
const sources = html5.getSources.call(player);
if (utils.is.empty(sources)) {
return null;
}
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
if (utils.is.empty(matches)) {
return null;
}
return Number(matches[0].getAttribute('size'));
},
set(input) {
// Get sources
const sources = html5.getSources.call(player);
if (utils.is.empty(sources)) {
return;
}
// Get matches for requested size
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
// No matches for requested size
if (utils.is.empty(matches)) {
return;
}
// Get supported sources
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
// No supported sources
if (utils.is.empty(supported)) {
return;
}
// Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality: input,
});
// Get current state
const { currentTime, playing } = player;
// Set new source
player.media.src = supported[0].getAttribute('src');
// Load new source
player.media.load();
// Resume playing
if (playing) {
player.play();
}
// Restore time
player.currentTime = currentTime;
// Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
quality: input,
});
},
});
},
// Cancel current network requests
// See https://github.com/sampotts/plyr/issues/174
cancelRequests() {
if (!this.isHTML5) {
return;
}
// Remove child sources
utils.removeElement(html5.getSources());
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
this.media.setAttribute('src', this.config.blankVideo);
// Load the new empty source
// This will cancel existing requests
// See https://github.com/sampotts/plyr/issues/174
this.media.load();
// Debugging
this.debug.log('Cancelled network requests');
},
};
export default html5;

31
src/js/i18n.js Normal file
View File

@ -0,0 +1,31 @@
// ==========================================================================
// Plyr internationalization
// ==========================================================================
import utils from './utils';
const i18n = {
get(key = '', config = {}) {
if (utils.is.empty(key) || utils.is.empty(config) || !Object.keys(config.i18n).includes(key)) {
return '';
}
let string = config.i18n[key];
const replace = {
'{seektime}': config.seekTime,
'{title}': config.title,
};
Object.entries(replace).forEach(([
key,
value,
]) => {
string = utils.replaceAll(string, key, value);
});
return string;
},
};
export default i18n;

View File

@ -17,6 +17,7 @@ class Listeners {
this.handleKey = this.handleKey.bind(this);
this.toggleMenu = this.toggleMenu.bind(this);
this.firstTouch = this.firstTouch.bind(this);
}
// Handle key presses
@ -187,8 +188,19 @@ class Listeners {
controls.toggleMenu.call(this.player, event);
}
// Device is touch enabled
firstTouch() {
this.player.touch = true;
// Add touch class
utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
// Clean up
utils.off(document.body, 'touchstart', this.firstTouch);
}
// Global window & document listeners
global(toggle) {
global(toggle = true) {
// Keyboard shortcuts
if (this.player.config.keyboard.global) {
utils.toggleListener(window, 'keydown keyup', this.handleKey, toggle, false);
@ -196,6 +208,9 @@ class Listeners {
// Click anywhere closes menu
utils.toggleListener(document.body, 'click', this.toggleMenu, toggle);
// Detect touch by events
utils.on(document.body, 'touchstart', this.firstTouch);
}
// Container listeners
@ -263,18 +278,28 @@ class Listeners {
// 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', event => ui.checkPlaying.call(this.player, event));
// Handle play/pause
utils.on(this.player.media, 'playing play pause ended emptied', 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 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
@ -288,7 +313,7 @@ class Listeners {
// On click play, pause ore restart
utils.on(wrapper, 'click', () => {
// Touch devices will just show controls (if we're hiding controls)
if (this.player.config.hideControls && support.touch && !this.player.paused) {
if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
return;
}
@ -330,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
@ -379,122 +407,132 @@ class Listeners {
// IE doesn't support input event, so we fallback to change
const inputEvent = browser.isIE ? 'change' : 'input';
// Trigger custom and default handlers
const proxy = (event, handlerKey, defaultHandler) => {
const customHandler = this.player.config.listeners[handlerKey];
// Run default and custom handlers
const proxy = (event, defaultHandler, customHandlerKey) => {
const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = utils.is.function(customHandler);
let returned = true;
// Execute custom handler
if (utils.is.function(customHandler)) {
customHandler.call(this.player, event);
if (hasCustomHandler) {
returned = customHandler.call(this.player, event);
}
// Only call default handler if not prevented in custom handler
if (!event.defaultPrevented && utils.is.function(defaultHandler)) {
if (returned && utils.is.function(defaultHandler)) {
defaultHandler.call(this.player, event);
}
};
// Trigger custom and default handlers
const on = (element, type, defaultHandler, customHandlerKey, passive = true) => {
const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = utils.is.function(customHandler);
utils.on(element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);
};
// Play/pause toggle
utils.on(this.player.elements.buttons.play, 'click', event =>
proxy(event, 'play', () => {
this.player.togglePlay();
}),
);
on(this.player.elements.buttons.play, 'click', this.player.togglePlay, 'play');
// Pause
utils.on(this.player.elements.buttons.restart, 'click', event =>
proxy(event, 'restart', () => {
this.player.restart();
}),
);
on(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
// Rewind
utils.on(this.player.elements.buttons.rewind, 'click', event =>
proxy(event, 'rewind', () => {
this.player.rewind();
}),
);
on(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
// Rewind
utils.on(this.player.elements.buttons.forward, 'click', event =>
proxy(event, 'forward', () => {
this.player.forward();
}),
);
on(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward');
// Mute toggle
utils.on(this.player.elements.buttons.mute, 'click', event =>
proxy(event, 'mute', () => {
on(
this.player.elements.buttons.mute,
'click',
() => {
this.player.muted = !this.player.muted;
}),
},
'mute',
);
// Captions toggle
utils.on(this.player.elements.buttons.captions, 'click', event =>
proxy(event, 'captions', () => {
this.player.toggleCaptions();
}),
);
on(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions);
// Fullscreen toggle
utils.on(this.player.elements.buttons.fullscreen, 'click', event =>
proxy(event, 'fullscreen', () => {
on(
this.player.elements.buttons.fullscreen,
'click',
() => {
this.player.fullscreen.toggle();
}),
},
'fullscreen',
);
// Picture-in-Picture
utils.on(this.player.elements.buttons.pip, 'click', event =>
proxy(event, 'pip', () => {
on(
this.player.elements.buttons.pip,
'click',
() => {
this.player.pip = 'toggle';
}),
},
'pip',
);
// Airplay
utils.on(this.player.elements.buttons.airplay, 'click', event =>
proxy(event, 'airplay', () => {
this.player.airplay();
}),
);
on(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay');
// Settings menu
utils.on(this.player.elements.buttons.settings, 'click', event => {
on(this.player.elements.buttons.settings, 'click', event => {
controls.toggleMenu.call(this.player, event);
});
// Settings menu
utils.on(this.player.elements.settings.form, 'click', event => {
on(this.player.elements.settings.form, 'click', event => {
event.stopPropagation();
// Settings menu items - use event delegation as items are added/removed
if (utils.matches(event.target, this.player.config.selectors.inputs.language)) {
proxy(event, 'language', () => {
this.player.language = event.target.value;
});
proxy(
event,
() => {
this.player.language = event.target.value;
},
'language',
);
} else if (utils.matches(event.target, this.player.config.selectors.inputs.quality)) {
proxy(event, 'quality', () => {
this.player.quality = event.target.value;
});
proxy(
event,
() => {
this.player.quality = event.target.value;
},
'quality',
);
} else if (utils.matches(event.target, this.player.config.selectors.inputs.speed)) {
proxy(event, 'speed', () => {
this.player.speed = parseFloat(event.target.value);
});
proxy(
event,
() => {
this.player.speed = parseFloat(event.target.value);
},
'speed',
);
} else {
controls.showTab.call(this.player, event);
}
});
// Seek
utils.on(this.player.elements.inputs.seek, inputEvent, event =>
proxy(event, 'seek', () => {
on(
this.player.elements.inputs.seek,
inputEvent,
event => {
this.player.currentTime = event.target.value / event.target.max * this.player.duration;
}),
},
'seek',
);
// Current time invert
// Only if one time element is used for both currentTime and duration
if (this.player.config.toggleInvert && !utils.is.element(this.player.elements.display.duration)) {
utils.on(this.player.elements.display.currentTime, 'click', () => {
on(this.player.elements.display.currentTime, 'click', () => {
// Do nothing if we're at the start
if (this.player.currentTime === 0) {
return;
@ -506,31 +544,34 @@ class Listeners {
}
// Volume
utils.on(this.player.elements.inputs.volume, inputEvent, event =>
proxy(event, 'volume', () => {
on(
this.player.elements.inputs.volume,
inputEvent,
event => {
this.player.volume = event.target.value;
}),
},
'volume',
);
// Polyfill for lower fill in <input type="range"> for webkit
if (browser.isWebkit) {
utils.on(utils.getElements.call(this.player, 'input[type="range"]'), 'input', event => {
on(utils.getElements.call(this.player, 'input[type="range"]'), 'input', event => {
controls.updateRangeFill.call(this.player, event.target);
});
}
// Seek tooltip
utils.on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
// Toggle controls visibility based on mouse movement
if (this.player.config.hideControls) {
// Watch for cursor over controls so they don't hide when trying to interact
utils.on(this.player.elements.controls, 'mouseenter mouseleave', event => {
this.player.elements.controls.hover = event.type === 'mouseenter';
on(this.player.elements.controls, 'mouseenter mouseleave', event => {
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
});
// Watch for cursor over controls so they don't hide when trying to interact
utils.on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
this.player.elements.controls.pressed = [
'mousedown',
'touchstart',
@ -538,53 +579,58 @@ class Listeners {
});
// Focus in/out on controls
utils.on(this.player.elements.controls, 'focusin focusout', event => {
on(this.player.elements.controls, 'focusin focusout', event => {
this.player.toggleControls(event);
});
}
// Mouse wheel for volume
utils.on(
on(
this.player.elements.inputs.volume,
'wheel',
event =>
proxy(event, 'volume', () => {
// Detect "natural" scroll - suppored on OS X Safari only
// Other browsers on OS X will be inverted until support improves
const inverted = event.webkitDirectionInvertedFromDevice;
const step = 1 / 50;
let direction = 0;
event => {
// Detect "natural" scroll - suppored on OS X Safari only
// Other browsers on OS X will be inverted until support improves
const inverted = event.webkitDirectionInvertedFromDevice;
const step = 1 / 50;
let direction = 0;
// Scroll down (or up on natural) to decrease
if (event.deltaY < 0 || event.deltaX > 0) {
if (inverted) {
this.player.decreaseVolume(step);
direction = -1;
} else {
this.player.increaseVolume(step);
direction = 1;
}
// Scroll down (or up on natural) to decrease
if (event.deltaY < 0 || event.deltaX > 0) {
if (inverted) {
this.player.decreaseVolume(step);
direction = -1;
} else {
this.player.increaseVolume(step);
direction = 1;
}
}
// Scroll up (or down on natural) to increase
if (event.deltaY > 0 || event.deltaX < 0) {
if (inverted) {
this.player.increaseVolume(step);
direction = 1;
} else {
this.player.decreaseVolume(step);
direction = -1;
}
// Scroll up (or down on natural) to increase
if (event.deltaY > 0 || event.deltaX < 0) {
if (inverted) {
this.player.increaseVolume(step);
direction = 1;
} else {
this.player.decreaseVolume(step);
direction = -1;
}
}
// Don't break page scrolling at max and min
if ((direction === 1 && this.player.media.volume < 1) || (direction === -1 && this.player.media.volume > 0)) {
event.preventDefault();
}
}),
// Don't break page scrolling at max and min
if ((direction === 1 && this.player.media.volume < 1) || (direction === -1 && this.player.media.volume > 0)) {
event.preventDefault();
}
},
'volume',
false,
);
}
// Reset on destroy
clear() {
this.global(false);
}
}
export default Listeners;

View File

@ -6,6 +6,7 @@ import support from './support';
import utils from './utils';
import youtube from './plugins/youtube';
import vimeo from './plugins/vimeo';
import html5 from './html5';
import ui from './ui';
// Sniff out the browser
@ -46,7 +47,7 @@ const media = {
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, support.touch);
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
}
// Inject the player wrapper
@ -75,32 +76,10 @@ const media = {
}
} 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;

View File

@ -7,6 +7,7 @@
/* global google */
import utils from '../utils';
import i18n from '../i18n';
class Ads {
/**
@ -171,18 +172,18 @@ class Ads {
*/
pollCountdown(start = false) {
if (!start) {
window.clearInterval(this.countdownTimer);
clearInterval(this.countdownTimer);
this.elements.container.removeAttribute('data-badge-text');
return;
}
const update = () => {
const time = utils.formatTime(Math.max(this.manager.getRemainingTime(), 0));
const label = `${this.player.config.i18n.advertisement} - ${time}`;
const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;
this.elements.container.setAttribute('data-badge-text', label);
};
this.countdownTimer = window.setInterval(update, 100);
this.countdownTimer = setInterval(update, 100);
}
/**
@ -205,21 +206,23 @@ 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
@ -384,6 +387,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();
@ -395,7 +402,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);
}
});
}

View File

@ -4,6 +4,7 @@
import utils from './../utils';
import captions from './../captions';
import controls from './../controls';
import ui from './../ui';
const vimeo = {
@ -34,7 +35,7 @@ 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 = 200;
const height = 240;
const offset = (height - padding) / (height / 50);
this.elements.wrapper.style.paddingBottom = `${padding}%`;
this.media.style.transform = `translateY(-${offset}%)`;
@ -101,10 +102,8 @@ const vimeo = {
};
player.media.stop = () => {
player.embed.stop().then(() => {
player.media.paused = true;
player.currentTime = 0;
});
player.pause();
player.currentTime = 0;
};
// Seeking
@ -141,10 +140,18 @@ const vimeo = {
return speed;
},
set(input) {
player.embed.setPlaybackRate(input).then(() => {
speed = input;
utils.dispatchEvent.call(player, player.media, 'ratechange');
});
player.embed
.setPlaybackRate(input)
.then(() => {
speed = input;
utils.dispatchEvent.call(player, player.media, 'ratechange');
})
.catch(error => {
// Hide menu item (and menu if empty)
if (error.name === 'Error') {
controls.setSpeedMenu.call(player, []);
}
});
},
});
@ -195,9 +202,15 @@ const vimeo = {
// Source
let currentSrc;
player.embed.getVideoUrl().then(value => {
currentSrc = value;
});
player.embed
.getVideoUrl()
.then(value => {
currentSrc = value;
})
.catch(error => {
this.debug.warn(error);
});
Object.defineProperty(player.media, 'currentSrc', {
get() {
return currentSrc;

View File

@ -6,6 +6,64 @@ import utils from './../utils';
import controls from './../controls';
import ui from './../ui';
// Standardise YouTube quality unit
function mapQualityUnit(input) {
switch (input) {
case 'hd2160':
return 2160;
case 2160:
return 'hd2160';
case 'hd1440':
return 1440;
case 1440:
return 'hd1440';
case 'hd1080':
return 1080;
case 1080:
return 'hd1080';
case 'hd720':
return 720;
case 720:
return 'hd720';
case 'large':
return 480;
case 480:
return 'large';
case 'medium':
return 360;
case 360:
return 'medium';
case 'small':
return 240;
case 240:
return 'small';
default:
return 'default';
}
}
function mapQualityUnits(levels) {
if (utils.is.empty(levels)) {
return levels;
}
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
}
const youtube = {
setup() {
// Add embed class for responsive
@ -168,14 +226,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
@ -240,15 +294,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);
},
});
@ -305,10 +362,10 @@ const youtube = {
utils.dispatchEvent.call(player, player.media, 'durationchange');
// Reset timer
window.clearInterval(player.timers.buffering);
clearInterval(player.timers.buffering);
// Setup buffering
player.timers.buffering = window.setInterval(() => {
player.timers.buffering = setInterval(() => {
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
@ -322,7 +379,7 @@ const youtube = {
// Bail if we're at 100%
if (player.media.buffered === 1) {
window.clearInterval(player.timers.buffering);
clearInterval(player.timers.buffering);
// Trigger event
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
@ -337,7 +394,7 @@ const youtube = {
const instance = event.target;
// Reset timer
window.clearInterval(player.timers.playing);
clearInterval(player.timers.playing);
// Handle events
// -1 Unstarted
@ -347,6 +404,16 @@ const youtube = {
// 3 Buffering
// 5 Video cued
switch (event.data) {
case -1:
// Update scrubber
utils.dispatchEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
utils.dispatchEvent.call(player, player.media, 'progress');
break;
case 0:
player.media.paused = true;
@ -377,7 +444,7 @@ const youtube = {
utils.dispatchEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = window.setInterval(() => {
player.timers.playing = setInterval(() => {
utils.dispatchEvent.call(player, player.media, 'timeupdate');
}, 50);
@ -390,7 +457,7 @@ const youtube = {
}
// Get quality
controls.setQualityMenu.call(player, instance.getAvailableQualityLevels());
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
break;

View File

@ -1,6 +1,6 @@
// ==========================================================================
// Plyr
// plyr.js v3.0.0-beta.19
// plyr.js v3.1.0
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
@ -36,6 +36,9 @@ class Plyr {
this.loading = false;
this.failed = false;
// Touch device
this.touch = support.touch;
// Set the media element
this.media = target;
@ -130,7 +133,17 @@ class Plyr {
}
// Cache original element state for .destroy()
this.elements.original = this.media.cloneNode(true);
// TODO: Investigate a better solution as I suspect this causes reported double load issues?
setTimeout(() => {
const clone = this.media.cloneNode(true);
// Prevent the clone autoplaying
if (clone.getAttribute('autoplay')) {
clone.pause();
}
this.elements.original = clone;
}, 0);
// Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube
@ -276,13 +289,18 @@ class Plyr {
this.listeners.container();
// Global listeners
this.listeners.global(true);
this.listeners.global();
// Setup fullscreen
this.fullscreen = new Fullscreen(this);
// Setup ads if provided
this.ads = new Ads(this);
// Autoplay if required
if (this.config.autoplay) {
this.play();
}
}
// ---------------------------------------
@ -293,33 +311,37 @@ class Plyr {
* Types and provider helpers
*/
get isHTML5() {
return this.provider === providers.html5;
return Boolean(this.provider === providers.html5);
}
get isEmbed() {
return this.isYouTube || this.isVimeo;
return Boolean(this.isYouTube || this.isVimeo);
}
get isYouTube() {
return this.provider === providers.youtube;
return Boolean(this.provider === providers.youtube);
}
get isVimeo() {
return this.provider === providers.vimeo;
return Boolean(this.provider === providers.vimeo);
}
get isVideo() {
return this.type === types.video;
return Boolean(this.type === types.video);
}
get isAudio() {
return this.type === types.audio;
return Boolean(this.type === types.audio);
}
/**
* Play the media, or play the advertisement (if they are not blocked)
*/
play() {
// 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());
if (!utils.is.function(this.media.play)) {
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();
}
@ -328,7 +350,7 @@ class Plyr {
* Pause the media
*/
pause() {
if (!this.playing) {
if (!this.playing || !utils.is.function(this.media.pause)) {
return;
}
@ -339,21 +361,21 @@ class Plyr {
* Get paused state
*/
get paused() {
return this.media.paused;
return Boolean(this.media.paused);
}
/**
* Get playing state
*/
get playing() {
return !this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true);
return Boolean(!this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true));
}
/**
* Get ended state
*/
get ended() {
return this.media.ended;
return Boolean(this.media.ended);
}
/**
@ -375,8 +397,11 @@ class Plyr {
* Stop playback
*/
stop() {
this.restart();
this.pause();
if (this.isHTML5) {
this.media.load();
} else if (utils.is.function(this.media.stop)) {
this.media.stop();
}
}
/**
@ -421,7 +446,7 @@ class Plyr {
}
// Set
this.media.currentTime = targetTime.toFixed(4);
this.media.currentTime = parseFloat(targetTime.toFixed(4));
// Logging
this.debug.log(`Seeking to ${this.currentTime} seconds`);
@ -434,11 +459,32 @@ class Plyr {
return Number(this.media.currentTime);
}
/**
* Get buffered
*/
get buffered() {
const { buffered } = this.media;
// YouTube / Vimeo return a float between 0-1
if (utils.is.number(buffered)) {
return buffered;
}
// HTML5
// TODO: Handle buffered chunks of the media
// (i.e. seek to another section buffers only that section)
if (buffered && buffered.length && this.duration > 0) {
return buffered.end(0) / this.duration;
}
return 0;
}
/**
* Get seeking status
*/
get seeking() {
return this.media.seeking;
return Boolean(this.media.seeking);
}
/**
@ -449,7 +495,7 @@ class Plyr {
const fauxDuration = parseInt(this.config.duration, 10);
// True duration
const realDuration = Number(this.media.duration);
const realDuration = this.media ? Number(this.media.duration) : 0;
// If custom duration is funky, use regular duration
return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration;
@ -493,8 +539,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;
}
}
@ -503,7 +549,7 @@ class Plyr {
* Get the current player volume
*/
get volume() {
return this.media.volume;
return Number(this.media.volume);
}
/**
@ -552,7 +598,7 @@ class Plyr {
* Get current muted state
*/
get muted() {
return this.media.muted;
return Boolean(this.media.muted);
}
/**
@ -569,12 +615,16 @@ class Plyr {
}
// Get audio tracks
return this.media.mozHasAudio || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);
return (
Boolean(this.media.mozHasAudio) ||
Boolean(this.media.webkitAudioDecodedByteCount) ||
Boolean(this.media.audioTracks && this.media.audioTracks.length)
);
}
/**
* Set playback speed
* @param {decimal} speed - the speed of playback (0.5-2.0)
* @param {number} speed - the speed of playback (0.5-2.0)
*/
set speed(input) {
let speed = null;
@ -615,34 +665,43 @@ class Plyr {
* Get current playback speed
*/
get speed() {
return this.media.playbackRate;
return Number(this.media.playbackRate);
}
/**
* 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;
@ -715,7 +774,7 @@ class Plyr {
* Get current loop state
*/
get loop() {
return this.media.loop;
return Boolean(this.media.loop);
}
/**
@ -772,7 +831,7 @@ class Plyr {
* Get the current autoplay state
*/
get autoplay() {
return this.config.autoplay;
return Boolean(this.config.autoplay);
}
/**
@ -922,26 +981,32 @@ class Plyr {
// Is the enter fullscreen event
isEnterFullscreen = toggle.type === 'enterfullscreen';
// Whether to show controls
show = [
'mouseenter',
'mousemove',
// Events that show the controls
const showEvents = [
'touchstart',
'touchmove',
'focusin',
].includes(toggle.type);
// Delay hiding on move events
if ([
'mouseenter',
'mousemove',
'focusin',
];
// Events that delay hiding
const delayEvents = [
'touchmove',
'touchend',
].includes(toggle.type)) {
'mousemove',
];
// Whether to show controls
show = showEvents.includes(toggle.type);
// Delay hiding on move events
if (delayEvents.includes(toggle.type)) {
delay = 2000;
}
// Delay a little more for keyboard users
if (toggle.type === 'focusin') {
if (!this.touch && toggle.type === 'focusin') {
delay = 3000;
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
}
@ -951,7 +1016,7 @@ class Plyr {
}
// Clear timer on every call
window.clearTimeout(this.timers.controls);
clearTimeout(this.timers.controls);
// If the mouse is not over the controls, set a timeout to hide them
if (show || this.paused || this.loading) {
@ -969,7 +1034,7 @@ class Plyr {
}
// Delay for hiding on touch
if (support.touch) {
if (this.touch) {
delay = 3000;
}
}
@ -978,6 +1043,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,6 +1099,10 @@ class Plyr {
* @param {boolean} soft - Whether it's a soft destroy (for source changes etc)
*/
destroy(callback, soft = false) {
if (!this.ready) {
return;
}
const done = () => {
// Reset overflow (incase destroyed while in fullscreen)
document.body.style.overflow = '';
@ -1057,12 +1131,12 @@ class Plyr {
callback();
}
} else {
// Unbind listeners
this.listeners.clear();
// Replace the container with the original element provided
utils.replaceElement(this.elements.original, this.elements.container);
// Unbind global listeners
this.listeners.global(false);
// Event
utils.dispatchEvent.call(this, this.elements.original, 'destroyed', true);
@ -1071,15 +1145,27 @@ class Plyr {
callback.call(this.elements.original);
}
// Clear for GC
this.elements = null;
// Reset state
this.ready = false;
// Clear for garbage collection
setTimeout(() => {
this.elements = null;
this.media = null;
}, 200);
}
};
// Stop playback
this.stop();
// Type specific stuff
switch (`${this.provider}:${this.type}`) {
case 'html5:video':
case 'html5:audio':
// Clear timeout
clearTimeout(this.timers.loading);
// Restore native video controls
ui.toggleNativeControls.call(this, true);
@ -1090,11 +1176,11 @@ class Plyr {
case 'youtube:video':
// Clear timers
window.clearInterval(this.timers.buffering);
window.clearInterval(this.timers.playing);
clearInterval(this.timers.buffering);
clearInterval(this.timers.playing);
// Destroy YouTube API
if (this.embed !== null) {
if (this.embed !== null && utils.is.function(this.embed.destroy)) {
this.embed.destroy();
}

View File

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

View File

@ -4,6 +4,7 @@
import { providers } from './types';
import utils from './utils';
import html5 from './html5';
import media from './media';
import ui from './ui';
import support from './support';
@ -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);

View File

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

View File

@ -30,13 +30,9 @@ const support = {
break;
case 'youtube:video':
api = true;
ui = support.rangeInput && (!browser.isIPhone || playsInline);
break;
case 'vimeo:video':
api = true;
ui = support.rangeInput && !browser.isIPhone;
ui = support.rangeInput && (!browser.isIPhone || playsInline);
break;
default:
@ -77,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) {
@ -147,7 +148,7 @@ const support = {
})(),
// Touch
// Remember a device can be moust + touch enabled
// NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
touch: 'ontouchstart' in document.documentElement,
// Detect transitions support

View File

@ -5,7 +5,7 @@
import utils from './utils';
import captions from './captions';
import controls from './controls';
import listeners from './listeners';
import i18n from './i18n';
const ui = {
addStyleHook() {
@ -25,7 +25,7 @@ const ui = {
// Setup the UI
build() {
// Re-attach media element listeners
// TODO: Use event bubbling
// TODO: Use event bubbling?
this.listeners.media();
// Don't setup interface if no support
@ -71,8 +71,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);
@ -95,7 +98,7 @@ const ui = {
// Setup aria attribute for play and iframe title
setTitle() {
// Find the current text
let label = this.config.i18n.play;
let label = i18n.get('play', this.config);
// If there's a media title set, use that for the label
if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) {
@ -124,7 +127,7 @@ const ui = {
// Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
iframe.setAttribute('title', this.config.i18n.frameTitle.replace('{title}', title));
iframe.setAttribute('title', i18n.get('frameTitle', this.config));
}
},
@ -256,21 +259,7 @@ const ui = {
// Check buffer status
case 'playing':
case 'progress':
value = (() => {
const { buffered } = this.media;
if (buffered && buffered.length) {
// HTML5
return utils.getPercentage(buffered.end(0), this.duration);
} else if (utils.is.number(buffered)) {
// YouTube returns between 0 and 1
return buffered * 100;
}
return 0;
})();
ui.setProgress.call(this, this.elements.display.buffer, value);
ui.setProgress.call(this, this.elements.display.buffer, this.buffered * 100);
break;

View File

@ -2,6 +2,8 @@
// Plyr utils
// ==========================================================================
import loadjs from 'loadjs';
import support from './support';
import { providers } from './types';
@ -97,11 +99,10 @@ const utils = {
if (responseType === 'text') {
try {
resolve(JSON.parse(request.responseText));
} catch(e) {
} catch (e) {
resolve(request.responseText);
}
}
else {
} else {
resolve(request.response);
}
});
@ -125,52 +126,10 @@ const utils = {
// Load an external script
loadScript(url) {
return new Promise((resolve, reject) => {
const current = document.querySelector(`script[src="${url}"]`);
// Check script is not already referenced, if so wait for load
if (current !== null) {
current.callbacks = current.callbacks || [];
current.callbacks.push(resolve);
return;
}
// Build the element
const element = document.createElement('script');
// Callback queue
element.callbacks = element.callbacks || [];
element.callbacks.push(resolve);
// Error queue
element.errors = element.errors || [];
element.errors.push(reject);
// Bind callback
element.addEventListener(
'load',
event => {
element.callbacks.forEach(cb => cb.call(null, event));
element.callbacks = null;
},
false,
);
// Bind error handling
element.addEventListener(
'error',
event => {
element.errors.forEach(err => err.call(null, event));
element.errors = null;
},
false,
);
// Set the URL after binding callback
element.src = url;
// Inject
const first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(element, first);
loadjs(url, {
success: resolve,
error: reject,
});
});
},
@ -184,7 +143,14 @@ const utils = {
const hasId = utils.is.string(id);
let isCached = false;
function updateSprite(data) {
const exists = () => document.querySelectorAll(`#${id}`).length;
function injectSprite(data) {
// Check again incase of race condition
if (hasId && exists()) {
return;
}
// Inject content
this.innerHTML = data;
@ -192,8 +158,8 @@ const utils = {
document.body.insertBefore(this, document.body.childNodes[0]);
}
// Only load once
if (!hasId || !document.querySelectorAll(`#${id}`).length) {
// Only load once if ID set
if (!hasId || !exists()) {
// Create container
const container = document.createElement('div');
utils.toggleHidden(container, true);
@ -209,7 +175,7 @@ const utils = {
if (isCached) {
const data = JSON.parse(cached);
updateSprite.call(container, data.content);
injectSprite.call(container, data.content);
return;
}
}
@ -231,7 +197,7 @@ const utils = {
);
}
updateSprite.call(container, result);
injectSprite.call(container, result);
})
.catch(() => {});
}
@ -242,15 +208,6 @@ const utils = {
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
},
// Determine if we're in an iframe
inFrame() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
},
// Wrap an element
wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary.
@ -353,8 +310,11 @@ const utils = {
return;
}
Object.keys(attributes).forEach(key => {
element.setAttribute(key, attributes[key]);
Object.entries(attributes).forEach(([
key,
value,
]) => {
element.setAttribute(key, value);
});
},
@ -481,7 +441,7 @@ const utils = {
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),
forward: utils.getElement.call(this, this.config.selectors.buttons.forward),
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
@ -574,9 +534,9 @@ const utils = {
},
// Toggle event listener
toggleListener(elements, event, callback, toggle, passive, capture) {
toggleListener(elements, event, callback, toggle = false, passive = true, capture = false) {
// Bail if no elemetns, event, or callback
if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
return;
}
@ -596,16 +556,16 @@ const utils = {
const events = event.split(' ');
// Build options
// Default to just capture boolean
let options = utils.is.boolean(capture) ? capture : false;
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;
// If passive events listeners are supported
if (support.passiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive: utils.is.boolean(passive) ? passive : true,
passive,
// Whether the listener is a capturing listener or not
capture: utils.is.boolean(capture) ? capture : false,
capture,
};
}
@ -616,25 +576,25 @@ const utils = {
},
// Bind event handler
on(element, events, callback, passive, capture) {
on(element, events = '', callback, passive = true, capture = false) {
utils.toggleListener(element, events, callback, true, passive, capture);
},
// Unbind event handler
off(element, events, callback, passive, capture) {
off(element, events = '', callback, passive = true, capture = false) {
utils.toggleListener(element, events, callback, false, passive, capture);
},
// 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,
}),
@ -671,6 +631,7 @@ const utils = {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
return 0;
}
return (current / max * 100).toFixed(2);
},
@ -711,6 +672,44 @@ const utils = {
return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
},
// Replace all occurances of a string in a string
replaceAll(input = '', find = '', replace = '') {
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
},
// Convert to title case
toTitleCase(input = '') {
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
},
// Convert string to pascalCase
toPascalCase(input = '') {
let string = input.toString();
// Convert kebab case
string = utils.replaceAll(string, '-', ' ');
// Convert snake case
string = utils.replaceAll(string, '_', ' ');
// Convert to title case
string = utils.toTitleCase(string);
// Convert to pascal case
return utils.replaceAll(string, ' ', '');
},
// Convert string to pascalCase
toCamelCase(input = '') {
let string = input.toString();
// Convert to pascal case
string = utils.toPascalCase(string);
// Convert first character to lowercase
return string.charAt(0).toLowerCase() + string.slice(1);
},
// Deep extend destination object with N more objects
extend(target = {}, ...sources) {
if (!sources.length) {
@ -738,6 +737,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

View File

@ -26,6 +26,11 @@
width: 100%;
}
button {
font: inherit;
line-height: inherit;
}
// Ignore focus
&:focus {
outline: 0;

View File

@ -6,7 +6,7 @@
.plyr__video-embed {
// Default to 16:9 ratio but this is set by JavaScript based on config
$padding: ((100 / 16) * 9);
$height: 200;
$height: 240;
$offset: to-percentage(($height - $padding) / ($height / 50));
height: 0;

View File

@ -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%;
@ -84,7 +85,6 @@
position: absolute;
top: 50%;
transform: translateY(-50%);
transition: border-color 0.2s ease;
}
&--forward {
@ -108,7 +108,6 @@
margin-bottom: floor($plyr-control-padding / 2);
padding-left: ceil($plyr-control-padding * 4);
position: relative;
width: calc(100% - #{$horizontal-padding});
&::after {

View File

@ -3,9 +3,11 @@
// ==========================================================================
.plyr__ads {
border-radius: inherit;
bottom: 0;
cursor: pointer;
left: 0;
overflow: hidden;
position: absolute;
right: 0;
top: 0;

View File

@ -8,8 +8,9 @@ $plyr-font-size-small: 14px !default;
$plyr-font-size-large: 18px !default;
$plyr-font-size-xlarge: 21px !default;
$plyr-font-size-time: 14px !default;
$plyr-font-size-time: $plyr-font-size-small !default;
$plyr-font-size-badge: 9px !default;
$plyr-font-size-menu: $plyr-font-size-small !default;
$plyr-font-weight-regular: 500 !default;
$plyr-font-weight-bold: 600 !default;

1522
yarn.lock

File diff suppressed because it is too large Load Diff