Skip to content

Commit

Permalink
[MWPW-156244] Reduce TBT Youtube/Vimeo Videos (#2812)
Browse files Browse the repository at this point in the history
* added facades for youtube & vimeo

* added unit tests for youtube and vimeo

* hotfix

* implementing feedback

* implementing feedback

* fixed autoplay on mobile

* made isMobile an instance property

---------

Co-authored-by: milo-pr-merge[bot] <169241390+milo-pr-merge[bot]@users.noreply.github.com>
  • Loading branch information
robert-bogos and milo-pr-merge[bot] authored Sep 4, 2024
1 parent b6cc0ca commit 94f5513
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 49 deletions.
56 changes: 56 additions & 0 deletions libs/blocks/vimeo/vimeo.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,59 @@
position: relative;
padding-bottom: 56.25%;
}

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 > .ltv-playbtn {
font-size: 10px;
padding: 0;
width: 6.5em;
height: 4em;
background: rgb(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;
}

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);
}

lite-vimeo.ltv-activated {
cursor: unset;
}

lite-vimeo.ltv-activated::before,
lite-vimeo.ltv-activated > .ltv-playbtn {
opacity: 0;
pointer-events: none;
}
89 changes: 72 additions & 17 deletions libs/blocks/vimeo/vimeo.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,80 @@
import { createIntersectionObserver, createTag, isInTextNode } from '../../utils/utils.js';
// part of the code is an optimized version of lite-vimeo-embed -> https://github.com/luwes/lite-vimeo-embed
import { replaceKey } from '../../features/placeholders.js';
import { createIntersectionObserver, createTag, getConfig, isInTextNode, loadLink } from '../../utils/utils.js';

export default function init(a) {
if (isInTextNode(a)) return;
const embedVimeo = () => {
const url = new URL(a.href);
let src = url.href;
if (url.hostname !== 'player.vimeo.com') {
const video = url.pathname.split('/')[1];
src = `https://player.vimeo.com/video/${video}?app_id=122963`;
}
const iframe = createTag('iframe', {
src,
style: 'border: 0; top: 0; left: 0; width: 100%; height: 100%; position: absolute;',
class LiteVimeo extends HTMLElement {
static preconnected = false;

connectedCallback() {
this.isMobile = navigator.userAgent.includes('Mobi');
this.videoId = this.getAttribute('videoid');
this.setupThumbnail();
this.setupPlayButton();
this.addEventListener('pointerover', LiteVimeo.warmConnections, { once: true });
this.addEventListener('click', this.addIframe);
}

static warmConnections() {
if (LiteVimeo.preconnected) return;
LiteVimeo.preconnected = true;
['player.vimeo.com',
'i.vimeocdn.com',
'f.vimeocdn.com',
'fresnel.vimeocdn.com',
].forEach((url) => loadLink(`https://${url}`, { rel: 'preconnect' }));
}

setupThumbnail() {
const { width, height } = this.getBoundingClientRect();
const roundedWidth = Math.min(Math.ceil(width / 100) * 100, 1920);
const roundedHeight = Math.round((roundedWidth / width) * height);

fetch(`https://vimeo.com/api/v2/video/${this.videoId}.json`)
.then((response) => response.json())
.then((data) => {
const thumbnailUrl = data[0]?.thumbnail_large?.replace(/-d_[\dx]+$/i, `-d_${roundedWidth}x${roundedHeight}`);
this.style.backgroundImage = `url("${thumbnailUrl}")`;

Check warning on line 36 in libs/blocks/vimeo/vimeo.js

View check run for this annotation

Codecov / codecov/patch

libs/blocks/vimeo/vimeo.js#L35-L36

Added lines #L35 - L36 were not covered by tests
})
.catch((e) => {
window.lana.log(`Error fetching Vimeo thumbnail: ${e}`, { tags: 'errorType=info,module=vimeo' });

Check warning on line 39 in libs/blocks/vimeo/vimeo.js

View check run for this annotation

Codecov / codecov/patch

libs/blocks/vimeo/vimeo.js#L39

Added line #L39 was not covered by tests
});
}

async setupPlayButton() {
const playBtnEl = createTag('button', {
type: 'button',
'aria-label': `${await replaceKey('play-video', getConfig())}`,
class: 'ltv-playbtn',
});
this.append(playBtnEl);
}

addIframe() {
if (this.classList.contains('ltv-activated')) return;
this.classList.add('ltv-activated');
const iframeEl = createTag('iframe', {
style: 'border: 0; top: 0; left: 0; width: 100%; height: 100%; position: absolute; background-color: #000;',
frameborder: '0',
allow: 'autoplay; fullscreen; picture-in-picture',
allowfullscreen: 'true',
title: 'Content from Vimeo',
loading: 'lazy',
allow: 'accelerometer; fullscreen; autoplay; encrypted-media; gyroscope; picture-in-picture',
allowFullscreen: true,
src: `https://player.vimeo.com/video/${encodeURIComponent(this.videoId)}?autoplay=1&muted=${this.isMobile ? 1 : 0}`,
});
const wrapper = createTag('div', { class: 'embed-vimeo' }, iframe);
this.insertAdjacentElement('afterend', iframeEl);
iframeEl.addEventListener('load', () => iframeEl.focus(), { once: true });
this.remove();
}
}

export default async function init(a) {
if (isInTextNode(a)) return;
if (!customElements.get('lite-vimeo')) customElements.define('lite-vimeo', LiteVimeo);

const embedVimeo = () => {
const url = new URL(a.href);
const videoid = url.pathname.split('/')[url.hostname === 'player.vimeo.com' ? 2 : 1];
const liteVimeo = createTag('lite-vimeo', { videoid });
const wrapper = createTag('div', { class: 'embed-vimeo' }, liteVimeo);
a.parentElement.replaceChild(wrapper, a);
};

Expand Down
55 changes: 55 additions & 0 deletions libs/blocks/youtube/youtube.css
Original file line number Diff line number Diff line change
@@ -1 +1,56 @@
@import url('../../styles/iframe.css');

lite-youtube {
top: 0;
left: 0;
width: 100%;
height: 100%;
position: absolute;
cursor: pointer;
}

lite-youtube > .lty-playbtn {
display: block;
width: 68px;
height: 48px;
position: absolute;
cursor: pointer;
transform: translate3d(-50%, -50%, 0);
top: 50%;
left: 50%;
z-index: 1;
background-color: transparent;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 48"><path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z" fill="red"/><path d="M45 24 27 14v20" fill="white"/></svg>');
filter: grayscale(100%);
transition: filter .1s cubic-bezier(0, 0, 0.2, 1);
border: none;
}

lite-youtube:hover > .lty-playbtn,
lite-youtube .lty-playbtn:focus {
filter: none;
}

lite-youtube.lyt-activated {
cursor: unset;
}

lite-youtube.lyt-activated::before,
lite-youtube.lyt-activated > .lty-playbtn {
opacity: 0;
pointer-events: none;
}

.lyt-visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}

.dark-background {
background-color: #000;
}
105 changes: 89 additions & 16 deletions libs/blocks/youtube/youtube.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,109 @@
import { createIntersectionObserver, isInTextNode } from '../../utils/utils.js';
// part of the code is an optimized version of lite-youtube-embed -> https://github.com/paulirish/lite-youtube-embed
import { createIntersectionObserver, createTag, isInTextNode, loadLink } from '../../utils/utils.js';

class LiteYTEmbed extends HTMLElement {
connectedCallback() {
this.isMobile = navigator.userAgent.includes('Mobi');
this.videoId = this.getAttribute('videoid');
const playBtnEl = createTag('button', { type: 'button', class: 'lty-playbtn' });
this.append(playBtnEl);
this.playLabel = this.getAttribute('playlabel') || 'Play';
this.style.backgroundImage = `url("https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg")`;
this.style.backgroundSize = 'cover';
this.style.backgroundPosition = 'center';
const playBtnLabelEl = createTag('span', { class: 'lyt-visually-hidden' });
playBtnLabelEl.textContent = this.playLabel;
playBtnEl.append(playBtnLabelEl);
this.addEventListener('pointerover', LiteYTEmbed.warmConnections, { once: true });
this.addEventListener('click', this.addIframe);
this.needsYTApiForAutoplay = navigator.vendor.includes('Apple') || this.isMobile;
}

static warmConnections() {
if (LiteYTEmbed.preconnected) return;
LiteYTEmbed.preconnected = true;
['www.youtube-nocookie.com',
'www.google.com',
'googleads.g.doubleclick.net',
'static.doubleclick.net',
].forEach((url) => loadLink(`https://${url}`, { rel: 'preconnect' }));
}

static loadYouTubeAPI() {
return new Promise((resolve) => {
if (window.YT?.Player) {
resolve();
return;
}

Check warning on line 37 in libs/blocks/youtube/youtube.js

View check run for this annotation

Codecov / codecov/patch

libs/blocks/youtube/youtube.js#L35-L37

Added lines #L35 - L37 were not covered by tests
const tag = createTag('script', { src: 'https://www.youtube.com/iframe_api' });
window.onYouTubeIframeAPIReady = resolve;
document.head.appendChild(tag);
});
}

async addIframe() {
if (this.classList.contains('lyt-activated')) return;

this.classList.add('lyt-activated');
const params = new URLSearchParams(this.getAttribute('params') || []);
params.append('autoplay', '1');
params.append('playsinline', '1');
if (this.isMobile) params.append('mute', '1');

if (this.needsYTApiForAutoplay) {
await LiteYTEmbed.loadYouTubeAPI();
await new Promise((resolve) => { window.YT.ready(resolve); });
// eslint-disable-next-line
new window.YT.Player(this, {
videoId: this.videoId,
playerVars: Object.fromEntries(params),
allow: 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture',
allowfullscreen: true,
title: this.playLabel,
});

Check warning on line 63 in libs/blocks/youtube/youtube.js

View check run for this annotation

Codecov / codecov/patch

libs/blocks/youtube/youtube.js#L55-L63

Added lines #L55 - L63 were not covered by tests
} else {
const iframeEl = createTag('iframe', {
src: `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${params.toString()}`,
allowFullscreen: true,
allow: 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture',
title: this.playLabel,
});
this.insertAdjacentElement('afterend', iframeEl);
iframeEl.focus();
this.remove();
}
}
}

export default async function init(a) {
if (!customElements.get('lite-youtube')) customElements.define('lite-youtube', LiteYTEmbed);

export default function init(a) {
const embedVideo = () => {
if (isInTextNode(a) || !a.origin?.includes('youtu')) return;
const title = !a.textContent.includes('http') ? a.textContent : 'Youtube Video';
const searchParams = new URLSearchParams(a.search);
const id = searchParams.get('v') || a.pathname.split('/').pop();
searchParams.delete('v');
const src = `https://www.youtube.com/embed/${id}?${searchParams.toString()}`;
const embedHTML = `
<div class="milo-video">
<iframe src="${src}" class="youtube"
webkitallowfullscreen mozallowfullscreen allowfullscreen
allow="encrypted-media; accelerometer; gyroscope; picture-in-picture"
scrolling="no"
id="player-${id}"
title="${title}">
</iframe>
</div>`;
a.insertAdjacentHTML('afterend', embedHTML);
const liteYTElement = createTag('lite-youtube', { videoid: id, playlabel: title });

if (searchParams.toString()) liteYTElement.setAttribute('params', searchParams.toString());

const ytContainer = createTag('div', { class: 'milo-video dark-background' }, liteYTElement);
a.insertAdjacentElement('afterend', ytContainer);
a.remove();

if (document.readyState === 'complete') {
/* eslint-disable-next-line no-underscore-dangle */
// eslint-disable-next-line no-underscore-dangle
window._satellite?.track('trackYoutube');
} else {
document.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') {
/* eslint-disable-next-line no-underscore-dangle */
// eslint-disable-next-line no-underscore-dangle

Check warning on line 101 in libs/blocks/youtube/youtube.js

View check run for this annotation

Codecov / codecov/patch

libs/blocks/youtube/youtube.js#L101

Added line #L101 was not covered by tests
window._satellite?.track('trackYoutube');
}
});
}
};

createIntersectionObserver({ el: a, callback: embedVideo });
}
20 changes: 20 additions & 0 deletions test/blocks/instagram/mocks/embed-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,26 @@ export function createTag(tag, attributes, html) {
return el;
}

export function loadLink(href, { as, callback, crossorigin, rel, fetchpriority } = {}) {
let link = document.head.querySelector(`link[href="${href}"]`);
if (!link) {
link = document.createElement('link');
link.setAttribute('rel', rel);
if (as) link.setAttribute('as', as);
if (crossorigin) link.setAttribute('crossorigin', crossorigin);
if (fetchpriority) link.setAttribute('fetchpriority', fetchpriority);
link.setAttribute('href', href);
if (callback) {
link.onload = (e) => callback(e.type);
link.onerror = (e) => callback(e.type);
}
document.head.appendChild(link);
} else if (callback) {
callback('noop');
}
return link;
}

export const getConfig = () => ({});

export const customFetch = stub();
Expand Down
2 changes: 2 additions & 0 deletions test/blocks/vimeo/mocks/placeholders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// eslint-disable-next-line
export const replaceKey = () => 'placeholder';
Loading

0 comments on commit 94f5513

Please sign in to comment.