Progressively enhance <iframe> embeds

This commit is contained in:
Sam Potts 2018-01-12 19:35:46 +11:00
parent 2e5d06ad85
commit d9ec1d1b8e
15 changed files with 204 additions and 110 deletions

View File

@ -13,6 +13,7 @@ This is a massive release. A _mostly_ complete rewrite in ES6. What started out
* Added AirPlay support (again, Safari only)
* Added `playsinline` support for iOS 10+
* Soundcloud removed until I can work on a plugin framework
* Embedded players are now progressively enhanced - no more empty `<div>`s!
### Other stuff
@ -68,27 +69,33 @@ You gotta break eggs to make an omelette. Sadly, there's quite a few breaking ch
Because we're using the fancy new ES6 syntax, you will need to polyfill for vintage browsers if you want to use Plyr and still support them. Luckily there's a decent service for this that makes it painless, [https://polyfill.io](polyfill.io).
## v2.0.18
- Fix for YouTube .getVideoData() issue (fixes #709)
* Fix for YouTube .getVideoData() issue (fixes #709)
## v2.0.17
- Vimeo controls fix (fixes #697)
- SVG4everybody compatibility fix
- Allow Plyr.setup event listeners to be set up as separate event listeners (https://github.com/sampotts/plyr/pull/703)
- Added title to the layer html template (for custom controls) (https://github.com/sampotts/plyr/pull/649)
- Target is null bug fix (https://github.com/sampotts/plyr/pull/617)
- fix #684 memory leaks issues after destroy (https://github.com/sampotts/plyr/pull/700)
* Vimeo controls fix (fixes #697)
* SVG4everybody compatibility fix
* Allow Plyr.setup event listeners to be set up as separate event listeners (https://github.com/sampotts/plyr/pull/703)
* Added title to the layer html template (for custom controls) (https://github.com/sampotts/plyr/pull/649)
* Target is null bug fix (https://github.com/sampotts/plyr/pull/617)
* fix #684 memory leaks issues after destroy (https://github.com/sampotts/plyr/pull/700)
## v2.0.16
- Fullscreen bug fix (fixes #664)
* Fullscreen bug fix (fixes #664)
## v2.0.15
- Demo fix
* Demo fix
## v2.0.14
- CDN URL updates. Sorry, still working on V3 as hard as I can...
* CDN URL updates. Sorry, still working on V3 as hard as I can...
## v2.0.13
- Repo moved and Vimeo demo fix
* Repo moved and Vimeo demo fix
## v2.0.12

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.js vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"name": "plyr",
"version": "3.0.0-beta.1",
"version": "3.0.0-beta.2",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"main": "./dist",
@ -34,7 +34,7 @@
"gulp-util": "^3.0.8",
"rollup-plugin-babel": "^3.0.3",
"rollup-plugin-commonjs": "^8.2.6",
"rollup-plugin-node-resolve": "^3.0.0",
"rollup-plugin-node-resolve": "^3.0.2",
"rollup-plugin-uglify": "^2.0.1",
"run-sequence": "^2.2.1",
"stylelint": "^8.4.0",
@ -46,15 +46,7 @@
"stylelint-selector-bem-pattern": "^2.0.0",
"uglify-es": "^3.3.5"
},
"keywords": [
"HTML5 Video",
"HTML5 Audio",
"Media Player",
"DASH",
"Shaka",
"WordPress",
"HLS"
],
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
"repository": {
"type": "git",
"url": "git://github.com/sampotts/plyr.git"

View File

@ -91,22 +91,24 @@ Plyr extends upon the standard HTML5 markup so that's all you need for those typ
</audio>
```
For YouTube and Vimeo, Plyr uses the standard YouTube API markup (an empty `<div>`):
For YouTube and Vimeo players, Plyr uses progressive enhancement to enhance the default `<iframe>` embeds. Below are some examples. The `plyr__video-embed` classname will make the embed responsive. You can add the `autoplay`, `loop` and `playsinline` (YouTube only) query parameters to the URL and they will be set as config options automatically. For YouTube, the `origin` should be updated to reflect the domain you're hosting the embed on, or you can opt to omit it.
#### YouTube embed
```html
<div id="player" data-plyr-provider="youtube" data-plyr-embed-id="bTqVqk7FSmY"></div>
<div class="plyr__video-embed" id="player">
<iframe src="https://www.youtube.com/embed/bTqVqk7FSmY?origin=https://plyr.io&amp;iv_load_policy=3&amp;modestbranding=1&amp;playsinline=1&amp;showinfo=0&amp;rel=0&amp;enablejsapi=1" allowfullscreen allowtransparency allow="autoplay"></iframe>
</div>
```
#### Vimeo embed
```html
<div id="player" data-plyr-provider="vimeo" data-plyr-embed-id="143418951"></div>
<div class="plyr__video-embed" id="player">
<iframe src="https://player.vimeo.com/video/76979871?loop=false&amp;byline=false&amp;portrait=false&amp;title=false&amp;speed=true&amp;transparent=0&amp;gesture=media" allowfullscreen allowtransparency allow="autoplay"></iframe>
</div>
```
Note: In both cases, `data-plyr-embed-id` value can be the ID or URL for the media.
### JavaScript
Include the `plyr.js` script before the closing `</body>` tag and then call `plyr.setup()`. More info on `setup()` can be found under
@ -120,7 +122,7 @@ Include the `plyr.js` script before the closing `</body>` tag and then call `ply
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.1/plyr.js"></script>
<script src="https://cdn.plyr.io/3.0.0-beta.2/plyr.js"></script>
```
### CSS
@ -134,13 +136,13 @@ Include the `plyr.css` stylsheet into your `<head>`
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.0.0-beta.1/plyr.css">
<link rel="stylesheet" href="https://cdn.plyr.io/3.0.0-beta.2/plyr.css">
```
### SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.0.0-beta.1/plyr.svg`.
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.0.0-beta.2/plyr.svg`.
## Advanced
@ -211,8 +213,7 @@ Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList):
const player = new Plyr(document.querySelectorAll('.js-player'));
```
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>` or `[data-plyr-provider]` (for embeds) element itself or a container
element.
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds
The second argument for the constructor is the [#options](options) object:

View File

@ -5,8 +5,8 @@
const noop = () => {};
export default class Console {
constructor(player) {
this.enabled = window.console && player.config.debug;
constructor(enabled = false) {
this.enabled = window.console && enabled;
if (this.enabled) {
this.log('Debugging enabled');

View File

@ -50,7 +50,7 @@ const media = {
}
// Inject the player wrapper
if (this.isVideo || this.isYouTube || this.isVimeo) {
if (this.isVideo) {
// Create the wrapper div
this.elements.wrapper = utils.createElement('div', {
class: this.config.classNames.video,

View File

@ -8,19 +8,12 @@ import ui from './../ui';
const vimeo = {
setup() {
// Remove old containers
const containers = utils.getElements.call(this, `[id^="${this.provider}-"]`);
Array.from(containers).forEach(utils.removeElement);
// Add embed class for responsive
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set intial ratio
vimeo.setAspectRatio.call(this);
// Set ID
this.media.setAttribute('id', utils.generateId(this.provider));
// Load the API if not already
if (!utils.is.object(window.Vimeo)) {
utils.loadScript(this.config.urls.vimeo.api, () => {
@ -57,15 +50,21 @@ const vimeo = {
transparent: 0,
gesture: 'media',
};
const params = utils.buildUrlParameters(options);
const id = utils.parseVimeoId(player.embedId);
const params = utils.buildUrlParams(options);
const id = utils.parseVimeoId(player.media.getAttribute('src'));
// Build an iframe
const iframe = utils.createElement('iframe');
const src = `https://player.vimeo.com/video/${id}?${params}`;
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
player.media.appendChild(iframe);
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
// Inject the package
const wrapper = utils.createElement('div');
wrapper.appendChild(iframe);
player.media = utils.replaceElement(wrapper, player.media);
// Setup instance
// https://github.com/vimeo/player.js

View File

@ -8,24 +8,15 @@ import ui from './../ui';
const youtube = {
setup() {
const videoId = utils.parseYouTubeId(this.embedId);
// Remove old containers
const containers = utils.getElements.call(this, `[id^="${this.provider}-"]`);
Array.from(containers).forEach(utils.removeElement);
// Add embed class for responsive
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set aspect ratio
youtube.setAspectRatio.call(this);
// Set ID
this.media.setAttribute('id', utils.generateId(this.provider));
// Setup API
if (utils.is.object(window.YT)) {
youtube.ready.call(this, videoId);
if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) {
youtube.ready.call(this);
} else {
// Load the API
utils.loadScript(this.config.urls.youtube.api);
@ -36,7 +27,7 @@ const youtube = {
// Add to queue
window.onYouTubeReadyCallbacks.push(() => {
youtube.ready.call(this, videoId);
youtube.ready.call(this);
});
// Set callback to process queue
@ -49,7 +40,7 @@ const youtube = {
},
// Get the media title
getTitle() {
getTitle(videoId) {
// Try via undocumented API method first
// This method disappears now and then though...
// https://github.com/sampotts/plyr/issues/709
@ -65,7 +56,6 @@ const youtube = {
// Or via Google API
const key = this.config.keys.google;
const videoId = utils.parseYouTubeId(this.embedId);
if (utils.is.string(key) && !utils.is.empty(key)) {
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`;
@ -88,12 +78,24 @@ const youtube = {
},
// API ready
ready(videoId) {
ready() {
const player = this;
// Ignore already setup (race condition)
const currentId = player.media.getAttribute('id');
if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = utils.parseYouTubeId(player.media.getAttribute('src'));
const id = utils.generateId(player.provider);
const container = utils.createElement('div', { id });
player.media = utils.replaceElement(container, player.media);
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(player.media.id, {
player.embed = new window.YT.Player(id, {
videoId,
playerVars: {
autoplay: player.config.autoplay ? 1 : 0, // Autoplay
@ -110,8 +112,8 @@ const youtube = {
widget_referrer: window && window.location.href,
// Captions are flaky on YouTube
cc_load_policy: this.captions.active ? 1 : 0,
cc_lang_pref: this.config.captions.language,
cc_load_policy: player.captions.active ? 1 : 0,
cc_lang_pref: player.config.captions.language,
},
events: {
onError(event) {
@ -179,7 +181,7 @@ const youtube = {
const instance = event.target;
// Get the title
youtube.getTitle.call(player);
youtube.getTitle.call(player, videoId);
// Create a faux HTML5 API using the YouTube API
player.media.play = () => {

View File

@ -1,6 +1,6 @@
// ==========================================================================
// Plyr
// plyr.js v3.0.0-beta.1
// plyr.js v3.0.0-beta.2
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
@ -66,7 +66,7 @@ class Plyr {
} catch (e) {
return {};
}
})()
})(),
);
// Elements cache
@ -103,7 +103,7 @@ class Plyr {
// Debugging
// TODO: move to globals
this.debug = new Console(this);
this.debug = new Console(this.config.debug);
// Log config options and support
this.debug.log('Config', this.config);
@ -141,35 +141,61 @@ class Plyr {
// Supported: video, audio, vimeo, youtube
const type = this.media.tagName.toLowerCase();
// Embed attributes
const attributes = {
provider: 'data-plyr-provider',
id: 'data-plyr-embed-id',
};
// Embed properties
let iframe = null;
let url = null;
let params = null;
// Different setup based on type
switch (type) {
// TODO: Handle passing an iframe for true progressive enhancement
// case 'iframe':
case 'div':
this.type = types.video; // Audio will come later for external providers
this.provider = this.media.getAttribute(attributes.provider);
this.embedId = this.media.getAttribute(attributes.id);
// Find the frame
iframe = this.media.querySelector('iframe');
// <iframe> required
if (!utils.is.element(iframe)) {
this.debug.error('Setup failed: <iframe> is missing');
return;
}
// Audio will come later for external providers
this.type = types.video;
// Detect provider
url = iframe.getAttribute('src');
this.provider = utils.getProviderByUrl(url);
// Get attributes from URL and set config
params = utils.getUrlParams(url);
if (!utils.is.empty(params)) {
const truthy = [
'1',
'true',
];
if (truthy.includes(params.autoplay)) {
this.config.autoplay = true;
}
if (truthy.includes(params.playsinline)) {
this.config.inline = true;
}
if (truthy.includes(params.loop)) {
this.config.loop.active = true;
}
}
// Unsupported provider
if (utils.is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
this.debug.error('Setup failed: Invalid provider');
return;
}
// Try and get the embed id
if (utils.is.empty(this.embedId)) {
this.debug.error('Setup failed: Embed ID or URL missing');
return;
}
// Rework elements
this.elements.container = this.media;
this.media = iframe;
// Clean up
this.media.removeAttribute(attributes.provider);
this.media.removeAttribute(attributes.id);
// Reset classname
this.elements.container.className = '';
break;
@ -178,22 +204,19 @@ class Plyr {
this.type = type;
this.provider = providers.html5;
// Get config from attributes
if (this.media.hasAttribute('crossorigin')) {
this.config.crossorigin = true;
}
if (this.media.hasAttribute('autoplay')) {
this.config.autoplay = true;
}
if (this.media.hasAttribute('playsinline')) {
this.config.inline = true;
}
if (this.media.hasAttribute('muted')) {
this.config.muted = true;
}
if (this.media.hasAttribute('loop')) {
this.config.loop.active = true;
}
@ -221,8 +244,10 @@ class Plyr {
this.media.plyr = this;
// Wrap media
this.elements.container = utils.createElement('div');
utils.wrap(this.media, this.elements.container);
if (!utils.is.element(this.elements.container)) {
this.elements.container = utils.createElement('div');
utils.wrap(this.media, this.elements.container);
}
// Allow focus to be captured
this.elements.container.setAttribute('tabindex', 0);
@ -1054,7 +1079,6 @@ class Plyr {
// GC for embed
this.embed = null;
this.embedId = null;
// If it's a soft destroy, make minimal changes
if (soft) {
@ -1082,11 +1106,7 @@ class Plyr {
}
} else {
// Replace the container with the original element provided
const parent = this.elements.container.parentNode;
if (utils.is.element(parent)) {
parent.replaceChild(this.elements.original, this.elements.container);
}
utils.replaceElement(this.elements.original, this.elements.container);
// Event
utils.dispatchEvent.call(this, this.elements.original, 'destroyed', true);
@ -1119,7 +1139,9 @@ class Plyr {
window.clearInterval(this.timers.playing);
// Destroy YouTube API
this.embed.destroy();
if (this.embed !== null) {
this.embed.destroy();
}
// Clean up
done();
@ -1129,7 +1151,9 @@ class Plyr {
case 'vimeo:video':
// Destroy Vimeo API
// then clean up (wait, to prevent postmessage errors)
this.embed.unload().then(done);
if (this.embed !== null) {
this.embed.unload().then(done);
}
// Vimeo does not always return
window.setTimeout(done, 200);

View File

@ -67,8 +67,9 @@ const source = {
case 'youtube:video':
case 'vimeo:video':
this.media = utils.createElement('div');
this.embedId = input.sources[0].src;
this.media = utils.createElement('div', {
src: input.sources[0].src,
});
break;
default:
@ -136,7 +137,7 @@ const source = {
ui.build.call(this);
}
},
true
true,
);
},
};

View File

@ -3,6 +3,7 @@
// ==========================================================================
import support from './support';
import { providers } from './types';
const utils = {
// Check variable types
@ -103,7 +104,7 @@ const utils = {
element.callbacks.forEach(cb => cb.call(null, event));
element.callbacks = null;
},
false
false,
);
}
@ -168,7 +169,7 @@ const utils = {
prefix + id,
JSON.stringify({
content: text,
})
}),
);
}
@ -274,6 +275,17 @@ const utils = {
}
},
// Replace element
replaceElement(newChild, oldChild) {
if (!utils.is.element(oldChild) || !utils.is.element(oldChild.parentNode) || !utils.is.element(newChild)) {
return null;
}
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
},
// Set attributes
setAttributes(element, attributes) {
if (!utils.is.element(element) || utils.is.empty(attributes)) {
@ -491,7 +503,7 @@ const utils = {
event.preventDefault();
}
},
false
false,
);
},
@ -617,14 +629,37 @@ const utils = {
return utils.extend(target, ...sources);
},
// Get the provider for a given URL
getProviderByUrl(url) {
// YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube;
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{8,}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
return null;
},
// Parse YouTube ID from URL
parseYouTubeId(url) {
if (utils.is.empty(url)) {
return null;
}
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url;
},
// Parse Vimeo ID from URL
parseVimeoId(url) {
if (utils.is.empty(url)) {
return null;
}
if (utils.is.number(Number(url))) {
return url;
}
@ -633,8 +668,40 @@ const utils = {
return url.match(regex) ? RegExp.$2 : url;
},
// Convert a URL to a location object
parseUrl(url) {
const parser = document.createElement('a');
parser.href = url;
return parser;
},
// Get URL query parameters
getUrlParams(input) {
let search = input;
// Parse URL if needed
if (input.startsWith('http://') || input.startsWith('https://')) {
({ search } = this.parseUrl(input));
}
if (this.is.empty(search)) {
return null;
}
const hashes = search.slice(search.indexOf('?') + 1).split('&');
return hashes.reduce((params, hash) => {
const [
key,
val,
] = hash.split('=');
return Object.assign(params, { [key]: decodeURIComponent(val) });
}, {});
},
// Convert object to URL parameters
buildUrlParameters(input) {
buildUrlParams(input) {
if (!utils.is.object(input)) {
return '';
}

View File

@ -11,6 +11,7 @@
height: 0;
padding-bottom: to-percentage($padding);
position: relative;
iframe {
border: 0;