-
Notifications
You must be signed in to change notification settings - Fork 26
/
Copy pathlite-vimeo-embed.js
223 lines (198 loc) Β· 7.27 KB
/
lite-vimeo-embed.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
const style = document.head.appendChild(document.createElement('style'));
style.textContent = /*css*/`
lite-vimeo {
aspect-ratio: 16 / 9;
background-color: #000;
position: relative;
display: block;
contain: content;
background-position: center center;
background-size: cover;
cursor: pointer;
}
lite-vimeo > iframe {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border: 0;
}
lite-vimeo > .ltv-playbtn {
font-size: 10px;
padding: 0;
width: 6.5em;
height: 4em;
background: rgba(23, 35, 34, .75);
z-index: 1;
opacity: .8;
border-radius: .5em;
transition: opacity .2s ease-out, background .2s ease-out;
outline: 0;
border: 0;
cursor: pointer;
}
lite-vimeo:hover > .ltv-playbtn {
background-color: rgb(0, 173, 239);
opacity: 1;
}
/* play button triangle */
lite-vimeo > .ltv-playbtn::before {
content: '';
border-style: solid;
border-width: 10px 0 10px 20px;
border-color: transparent transparent transparent #fff;
}
lite-vimeo > .ltv-playbtn,
lite-vimeo > .ltv-playbtn::before {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
}
/* Post-click styles */
lite-vimeo.ltv-activated {
cursor: unset;
}
lite-vimeo.ltv-activated::before,
lite-vimeo.ltv-activated > .ltv-playbtn {
opacity: 0;
pointer-events: none;
}
`;
/**
* Ported from https://github.com/paulirish/lite-youtube-embed
*
* A lightweight vimeo embed. Still should feel the same to the user, just MUCH faster to initialize and paint.
*
* Thx to these as the inspiration
* https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html
* https://autoplay-youtube-player.glitch.me/
*
* Once built it, I also found these:
* https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube (ππ)
* https://github.com/Daugilas/lazyYT
* https://github.com/vb/lazyframe
*/
class LiteVimeo extends (globalThis.HTMLElement ?? class {}) {
/**
* Begin pre-connecting to warm up the iframe load
* Since the embed's network requests load within its iframe,
* preload/prefetch'ing them outside the iframe will only cause double-downloads.
* So, the best we can do is warm up a few connections to origins that are in the critical path.
*
* Maybe `<link rel=preload as=document>` would work, but it's unsupported: http://crbug.com/593267
* But TBH, I don't think it'll happen soon with Site Isolation and split caches adding serious complexity.
*/
static _warmConnections() {
if (LiteVimeo.preconnected) return;
LiteVimeo.preconnected = true;
// The iframe document and most of its subresources come right off player.vimeo.com
addPrefetch('preconnect', 'https://player.vimeo.com');
// Images
addPrefetch('preconnect', 'https://i.vimeocdn.com');
// Files .js, .css
addPrefetch('preconnect', 'https://f.vimeocdn.com');
// Metrics
addPrefetch('preconnect', 'https://fresnel.vimeocdn.com');
}
connectedCallback() {
this.videoId = this.getAttribute('videoid');
/**
* Lo, the vimeo placeholder image! (aka the thumbnail, poster image, etc)
* We have to use the Vimeo API.
*/
let { width, height } = getThumbnailDimensions(this.getBoundingClientRect());
let devicePixelRatio = window.devicePixelRatio || 1;
if (devicePixelRatio >= 2) devicePixelRatio *= .75;
width = Math.round(width * devicePixelRatio);
height = Math.round(height * devicePixelRatio);
fetch(`https://vimeo.com/api/v2/video/${this.videoId}.json`)
.then(response => response.json())
.then(data => {
let thumbnailUrl = data[0].thumbnail_large;
thumbnailUrl = thumbnailUrl.replace(/-d_[\dx]+$/i, `-d_${width}x${height}`);
this.style.backgroundImage = `url("${thumbnailUrl}")`;
});
let playBtnEl = this.querySelector('.ltv-playbtn');
// A label for the button takes priority over a [playlabel] attribute on the custom-element
this.playLabel = (playBtnEl && playBtnEl.textContent.trim()) || this.getAttribute('playlabel') || 'Play video';
if (!playBtnEl) {
playBtnEl = document.createElement('button');
playBtnEl.type = 'button';
playBtnEl.setAttribute('aria-label', this.playLabel);
playBtnEl.classList.add('ltv-playbtn');
this.append(playBtnEl);
}
playBtnEl.removeAttribute('href');
// On hover (or tap), warm up the TCP connections we're (likely) about to use.
this.addEventListener('pointerover', LiteVimeo._warmConnections, {
once: true
});
// Once the user clicks, add the real iframe and drop our play button
// TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time
// We'd want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003
this.addEventListener('click', this.addIframe);
}
addIframe() {
if (this.classList.contains('ltv-activated')) return;
this.classList.add('ltv-activated');
const iframeEl = document.createElement('iframe');
iframeEl.width = 640;
iframeEl.height = 360;
// No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include
iframeEl.title = this.playLabel;
iframeEl.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture';
// AFAIK, the encoding here isn't necessary for XSS, but we'll do it only because this is a URL
// https://stackoverflow.com/q/64959723/89484
iframeEl.src = `https://player.vimeo.com/video/${encodeURIComponent(this.videoId)}?autoplay=1`;
this.append(iframeEl);
// Set focus for a11y
iframeEl.addEventListener('load', iframeEl.focus, { once: true });
}
}
if (globalThis.customElements && !globalThis.customElements.get('lite-vimeo')) {
globalThis.customElements.define('lite-vimeo', LiteVimeo);
}
/**
* Add a <link rel={preload | preconnect} ...> to the head
*/
function addPrefetch(kind, url, as) {
const linkElem = document.createElement('link');
linkElem.rel = kind;
linkElem.href = url;
if (as) {
linkElem.as = as;
}
linkElem.crossorigin = true;
document.head.append(linkElem);
}
/**
* Get the thumbnail dimensions to use for a given player size.
*
* @param {Object} options
* @param {number} options.width The width of the player
* @param {number} options.height The height of the player
* @return {Object} The width and height
*/
function getThumbnailDimensions({ width, height }) {
let roundedWidth = width;
let roundedHeight = height;
// If the original width is a multiple of 320 then we should
// not round up. This is to keep the native image dimensions
// so that they match up with the actual frames from the video.
//
// For example 640x360, 960x540, 1280x720, 1920x1080
//
// Round up to nearest 100 px to improve cacheability at the
// CDN. For example, any width between 601 pixels and 699
// pixels will render the thumbnail at 700 pixels width.
if (roundedWidth % 320 !== 0) {
roundedWidth = Math.ceil(width / 100) * 100;
roundedHeight = Math.round((roundedWidth / width) * height);
}
return {
width: roundedWidth,
height: roundedHeight
};
}