styled-youtube-embedding-labwork.code-workspace |
# styled-youtube-embedding-labwork | |||||
> Embed YouTube video in a webpage with a custom play-button, original poster at the best resolution and responsive container, keeping aspect ratio. | |||||
[Try it](https://chiefred.github.io/styled-youtube-embedding-labwork/) in action! | |||||
Often we need to embed a YouTube video in a custom design (with a custom play button), but for that purpose, we only have the URL of that video. Let`s disassemble the task in several steps. | |||||
## Get \<id-of-video\> from URL | |||||
YouTube video URLs can be provided in several formats, like: | |||||
- https://youtu.be/jIHvgUAW5vE | |||||
- https://youtu.be/jIHvgUAW5vE?t=2 | |||||
- https://www.youtube.com/embed/jIHvgUAW5vE | |||||
- https://www.youtube.com/watch?v=jIHvgUAW5vE&ab_channel=MihaEr%C5%BEen | |||||
In all the examples presented, the desired video identifier will be `jIHvgUAW5vE`. So, we need a way to extract it. | |||||
In most cases, we will do this on the server side by regular expression and even in combination with check: if we have a valid YouTube video URL (of any format), then we can, for example, insert its poster image into the page. | |||||
I will use PHP: | |||||
```php | |||||
<? if ( | |||||
preg_match( | |||||
'/[\/\=]{1}([a-zA-Z0-9_-]{11})([\?\&]{1}|$)/', | |||||
$anyYouTubeVideoURL, | |||||
$matches | |||||
) | |||||
): ?> | |||||
<img | |||||
src="https://img.youtube.com/vi/<?=$matches[1]?>/maxresdefault.jpg" | |||||
alt="video" | |||||
/> | |||||
<? endif ?> | |||||
``` | |||||
## Get the poster image for YouTube video | |||||
As already shown above, poster image can be loaded from `img.youtube.com`. Best resolution images available at URLs like: | |||||
```html | |||||
https://img.youtube.com/vi/<id-of-video>/maxresdefault.jpg</id-of-video> | |||||
``` | |||||
But in some cases they may not exist (when the original video was in low resolution). | |||||
We can find the following variations of the image file names used: | |||||
- maxresdefault | |||||
- mqdefault | |||||
- sddefault | |||||
- hqdefault | |||||
- default | |||||
And we need to determine if the image exists or not. With the 404 error response, YouTube also transmits a default placeholder image, which prevents the `img` tag's `onerror` event handler from being called. Thus, we can only check the "natural dimensions" of the resulting image. | |||||
The idea is to try to load the highest resolution image (`maxresdefault.jpg`) and test the result with onload script: | |||||
```html | |||||
<img | |||||
src="https://img.youtube.com/vi/<id-of-video>/maxresdefault.jpg" | |||||
onload="window.youtube_img_load_check(this)" | |||||
alt="video" | |||||
/> | |||||
``` | |||||
But before the `img` tag, we must register the `youtube_img_load_check` function in the `head` section of the web page: | |||||
```js | |||||
window.youtube_img_load_check = function (e) { | |||||
var thumbnail = [ | |||||
"maxresdefault", | |||||
"mqdefault", | |||||
"sddefault", | |||||
"hqdefault", | |||||
"default", | |||||
]; | |||||
var url = e.getAttribute("src"); | |||||
if (e.naturalWidth === 120 && e.naturalHeight === 90) { | |||||
for (var i = 0, len = thumbnail.length - 1; i < len; i++) { | |||||
if (url.indexOf(thumbnail[i]) > 0) { | |||||
e.setAttribute("src", url.replace(thumbnail[i], thumbnail[i + 1])); | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
``` | |||||
If loading `maxresdefault.jpg` fails, the script will try the next option from the array. Etc. | |||||
The default YouTube stub image size is 120 x 90 pixels. Thus, we can detect errors when checking the `naturalWidth` and `naturalHeight` of the resulting image. | |||||
## Preserve the aspect ratio of the poster image in a responsive design | |||||
The YouTube documentation says, "The standard aspect ratio for YouTube on a computer is 16:9". And most videos are in this format. | |||||
As I found, even videos with a 4:3 aspect ratio in most cases have a poster in 16:9 format, until `default.jpg` which 120x90 (same as 404 error image). | |||||
Thus, we cannot determine the aspect ratio of the video when measuring the loaded poster. That's why I just think all YouTube videos in 16:9 format. The result for my cases is acceptable. Here is the layout. | |||||
HTML: | |||||
```html | |||||
<div class="youtube-video"> | |||||
<div class="youtube-video__aspect"> | |||||
<div class="youtube-video__wrapper"> | |||||
<img | |||||
class="youtube-video__poster" | |||||
src="https://img.youtube.com/vi/<id-of-video>/maxresdefault.jpg" | |||||
onload="window.youtube_img_load_check(this)" | |||||
alt="video" | |||||
loading="lazy" | |||||
/> | |||||
<div | |||||
class="youtube-video__play-icon" | |||||
data-link="https://www.youtube.com/embed/<id-of-video>" | |||||
></div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
``` | |||||
CSS: | |||||
```css | |||||
.youtube-video { | |||||
width: 100%; | |||||
} | |||||
.youtube-video__aspect { | |||||
width: 100%; | |||||
height: 0; | |||||
position: relative; | |||||
padding-top: 56.25%; /* This line gives 16:9 aspect ratio */ | |||||
} | |||||
.youtube-video__poster { | |||||
width: 100%; | |||||
height: 100%; | |||||
position: absolute; | |||||
top: 0; | |||||
left: 0; | |||||
} | |||||
.youtube-video__wrapper { | |||||
/* Needed to properly resize video */ | |||||
width: 100%; | |||||
height: 100%; | |||||
position: absolute; | |||||
top: 0; | |||||
left: 0; | |||||
display: flex; | |||||
justify-content: center; | |||||
align-items: center; | |||||
} | |||||
.youtube-video__play-icon { | |||||
...; | |||||
} | |||||
.youtube-video__iframe { | |||||
/* Needed to properly resize video */ | |||||
width: 100%; | |||||
height: 100%; | |||||
} | |||||
``` | |||||
## Launch YouTube video player | |||||
One final piece of the puzzle - we need to replace the poster image with a real YouTube player when the user hits the play button. This JS can be placed at the bottom of the page. | |||||
```js | |||||
const videos = document.querySelectorAll( | |||||
".youtube-video .youtube-video__play-icon" | |||||
); | |||||
videos.forEach(function (video) { | |||||
video.addEventListener("click", function (e) { | |||||
const link = e.target.dataset.link || null; | |||||
const parent = e.target.closest(".youtube-video__wrapper"); | |||||
if (link && parent) { | |||||
parent.classList.add("loading"); | |||||
parent.innerHTML = | |||||
'<iframe class="youtube-video__iframe" src="' + | |||||
link + | |||||
'?autoplay=1" frameborder="0" allowfullscreen' + | |||||
'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"' + | |||||
"></iframe>"; | |||||
} | |||||
}); | |||||
}); | |||||
``` | |||||
You can optionally use the `.youtube-video__wrapper.loading` CSS selector to show the loading indicator. | |||||
Now [try it](https://chiefred.github.io/styled-youtube-embedding-labwork/) in action! |
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<meta charset="UTF-8"> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||||
<title>Styled YouTube Embedding Labwork</title> | |||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/modern-css-reset/dist/reset.min.css" /> | |||||
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap" rel="stylesheet"> | |||||
<link rel="stylesheet" href="styles.css" /> | |||||
<script> | |||||
window.youtube_img_load_check = function (e) { | |||||
var thumbnail = ["maxresdefault", "mqdefault", "sddefault", "hqdefault", "default"]; | |||||
var url = e.getAttribute("src"); | |||||
if (e.naturalWidth === 120 && e.naturalHeight === 90) { | |||||
for (var i = 0, len = thumbnail.length - 1; i < len; i++) { | |||||
if (url.indexOf(thumbnail[i]) > 0) { | |||||
e.setAttribute("src", url.replace(thumbnail[i], thumbnail[i + 1])); | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
</head> | |||||
<body> | |||||
<div class="content"> | |||||
<header> | |||||
<h1>Styled YouTube Embedding Labwork</h1> | |||||
<noscript> | |||||
<p>This page requires JavaScript to function correctly.</p> | |||||
</noscript> | |||||
<p>The page layout is responsive.</p> | |||||
</header> | |||||
<section> | |||||
<h2>High resolution video example</h2> | |||||
<p>It loads maxresdefault.jpg successfully.</p> | |||||
<div class="youtube-video"> | |||||
<div class="youtube-video__aspect"> | |||||
<div class="youtube-video__wrapper"> | |||||
<img class="youtube-video__poster" | |||||
src="https://img.youtube.com/vi/jIHvgUAW5vE/maxresdefault.jpg" | |||||
onload="window.youtube_img_load_check(this)" alt="1080p resolution youtube-video" | |||||
loading="lazy" /> | |||||
<div class="youtube-video__play-icon" data-link="https://www.youtube.com/embed/jIHvgUAW5vE"> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</section> | |||||
<section> | |||||
<h2>Low resolution video example</h2> | |||||
<p>The maxresdefault.jpg fails to load and switches to mqdefault.jpg which loads successfully.</p> | |||||
<div class="youtube-video"> | |||||
<div class="youtube-video__aspect"> | |||||
<div class="youtube-video__wrapper"> | |||||
<img class="youtube-video__poster" | |||||
src="https://img.youtube.com/vi/chPdcSRI4FU/maxresdefault.jpg" | |||||
onload="window.youtube_img_load_check(this)" alt="144p resolution youtube-video" | |||||
loading="lazy" /> | |||||
<div class="youtube-video__play-icon" data-link="https://www.youtube.com/embed/chPdcSRI4FU"> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</section> | |||||
<section> | |||||
<h2>4:3 aspect ratio video example</h2> | |||||
<div class="youtube-video"> | |||||
<div class="youtube-video__aspect"> | |||||
<div class="youtube-video__wrapper"> | |||||
<img class="youtube-video__poster" | |||||
src="https://img.youtube.com/vi/mM5_T-F1Yn4/maxresdefault.jpg" | |||||
onload="window.youtube_img_load_check(this)" alt="4:3 aspect ratio youtube-video" | |||||
loading="lazy" /> | |||||
<div class="youtube-video__play-icon" data-link="https://www.youtube.com/embed/mM5_T-F1Yn4"> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</section> | |||||
<footer> | |||||
<div class="attributions"> | |||||
Play icon from <a href="https://www.flaticon.com/" title="Flaticon" | |||||
rel="noopener noreferrer nofollow" target="_blank">www.flaticon.com</a> | |||||
• | |||||
Loading indicator from <a href="https://loading.io/" title="LOADING.IO" | |||||
rel="noopener noreferrer nofollow" target="_blank">loading.io</a> | |||||
</div> | |||||
</footer> | |||||
</div> | |||||
<script src="scripts.js"></script> | |||||
</body> | |||||
</html> |
(function (document, window) { | |||||
document.addEventListener("DOMContentLoaded", function (event) { | |||||
const videos = document.querySelectorAll('.youtube-video .youtube-video__play-icon') | |||||
videos.forEach(function (video) { | |||||
video.addEventListener("click", function (e) { | |||||
const link = e.target.dataset.link || null; | |||||
const parent = e.target.closest('.youtube-video__wrapper'); | |||||
if (link && parent) { | |||||
parent.classList.add('loading'); | |||||
parent.innerHTML = '<iframe class="youtube-video__iframe" src="' + link + '?autoplay=1" frameborder="0" allowfullscreen allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"></iframe>'; | |||||
} | |||||
}); | |||||
}) | |||||
}); | |||||
}(document, window)) |
body { | |||||
background-color: #2C5282; | |||||
color: #90CDF4; | |||||
font-family: 'Lato', sans-serif; | |||||
font-size: calc(15px + (26 - 15) * ((100vw - 320px) / (1410 - 320))); | |||||
font-weight: 400; | |||||
padding: 1rem; | |||||
} | |||||
a, | |||||
a:visited { | |||||
color: #90CDF4; | |||||
} | |||||
.content { | |||||
max-width: 1410px; | |||||
min-width: 280px; | |||||
margin: 0 auto; | |||||
} | |||||
h1 { | |||||
color: #EBF8FF; | |||||
padding-bottom: .5rem; | |||||
border-bottom: 2px solid #90CDF4; | |||||
margin: calc(32px + (64 - 32) * ((100vw - 320px) / (1410 - 320))) 0 calc(12px + (20 - 12) * ((100vw - 320px) / (1410 - 320))) 0; | |||||
font-size: 200%; | |||||
font-weight: 700; | |||||
} | |||||
h2 { | |||||
margin: calc(24px + (48 - 24) * ((100vw - 320px) / (1410 - 320))) 0 calc(2px + (6 - 2) * ((100vw - 320px) / (1410 - 320))) 0; | |||||
font-weight: 700; | |||||
font-size: 160%; | |||||
} | |||||
.youtube-video { | |||||
width: 100%; | |||||
background-color: #2C5282; | |||||
border: 3px solid #90CDF4; | |||||
border-radius: 5px; | |||||
padding: 4px; | |||||
margin: calc(6px + (18 - 6) * ((100vw - 320px) / (1410 - 320))) 0; | |||||
} | |||||
.youtube-video__aspect { | |||||
width: 100%; | |||||
height: 0; | |||||
position: relative; | |||||
padding-top: calc(56.25% - 14px); | |||||
} | |||||
.youtube-video__poster { | |||||
width: 100%; | |||||
height: 100%; | |||||
position: absolute; | |||||
top: 0; | |||||
left: 0; | |||||
} | |||||
.youtube-video__wrapper { | |||||
width: 100%; | |||||
height: 100%; | |||||
position: absolute; | |||||
top: 0; | |||||
left: 0; | |||||
display: flex; | |||||
justify-content: center; | |||||
align-items: center; | |||||
} | |||||
.youtube-video__wrapper.loading { | |||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200' viewBox='0 0 100 100' preserveAspectRatio='xMidYMid'%3E%3Ccircle cx='50' cy='50' r='32' style='fill:none;stroke-dasharray:50;stroke-linecap:round;stroke-width:8;stroke:%23e1e6e9'%3E%3CanimateTransform attributeName='transform' type='rotate' dur='1s' repeatCount='indefinite' keyTimes='0;1' values='0 50 50;360 50 50'/%3E%3C/circle%3E%3Ccircle cx='50' cy='50' r='23' style='fill:none;stroke-dasharray:40;stroke-dashoffset:40;stroke-linecap:round;stroke-width:8;stroke:%234ad194'%3E%3CanimateTransform attributeName='transform' type='rotate' dur='1s' repeatCount='indefinite' keyTimes='0;1' values='0 50 50;-360 50 50'/%3E%3C/circle%3E%3C/svg%3E"); | |||||
background-position: center; | |||||
background-repeat: no-repeat; | |||||
background-size: auto; | |||||
} | |||||
.youtube-video__play-icon { | |||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cstyle%3E.a%7Bfill:%234AD194;%7D%3C/style%3E%3Cpath d='M256 45c116.5 0 211 94.5 211 211 0 116.5-94.5 211-211 211 -116.5 0-211-94.5-211-211C45 139.5 139.5 45 256 45L256 45zM502 256C502 120.1 391.9 10 256 10S10 120.1 10 256c0 135.9 110.1 246 246 246S502 391.9 502 256z' class='a'/%3E%3Cpath d='M467 256C467 139.5 372.5 45 256 45 139.5 45 45 139.5 45 256c0 116.5 94.5 211 211 211C372.5 467 467 372.5 467 256L467 256zM223.1 164.8l57.1 33c0.5 0.3 1 0.6 1.4 0.9l57.2 33c13.4 7.7 18.1 24.9 10.4 38.3 -2.6 4.6-6.4 8.2-10.7 10.5l-58.3 33.7 0 0.1 -58.8 33.9c-13.4 7.8-30.6 3.2-38.4-10.2 -2.6-4.4-3.8-9.3-3.8-14.1h-0.1v-67.8 -67.8c0-15.6 12.6-28.2 28.2-28.2C213.1 160 218.6 161.8 223.1 164.8z' fill='%23E1E6E9'/%3E%3Cpath d='M338.8 231.7l-57.2-33c-0.5-0.3-1-0.6-1.4-0.9l-57.1-33c-4.5-3-9.9-4.8-15.7-4.8 -15.6 0-28.2 12.6-28.2 28.2v67.8 67.8h0.1c0 4.8 1.2 9.6 3.8 14.1 7.8 13.4 25 18 38.4 10.2l58.8-33.9 0 0 58.3-33.7c4.3-2.4 8.1-6 10.7-10.5C356.9 256.6 352.2 239.4 338.8 231.7z' class='a'/%3E%3Cpath d='M256 0C114.8 0 0 114.8 0 256s114.8 256 256 256S512 397.2 512 256 397.2 0 256 0zM256 492C125.9 492 20 386.1 20 256S125.9 20 256 20s236 105.9 236 236S386.1 492 256 492z'/%3E%3Cpath d='M256 35C134.1 35 35 134.1 35 256s99.1 221 221 221 221-99.1 221-221C477 134.1 377.9 35 256 35zM256 457c-110.8 0-201-90.2-201-201C55 145.2 145.2 55 256 55s201 90.2 201 201C457 366.8 366.8 457 256 457z'/%3E%3Cpath d='M108.6 159.3c-4.8-2.8-10.9-1.1-13.7 3.7 -10.9 18.8-18.3 39.2-22.1 60.6 -1 5.4 2.7 10.6 8.1 11.6 0.6 0.1 1.2 0.2 1.8 0.2 4.8 0 9-3.4 9.8-8.3 3.4-19.1 10-37.3 19.7-54.1C115 168.2 113.4 162.1 108.6 159.3z'/%3E%3Cpath d='M441.3 239.9c-0.5-5.5-5.3-9.6-10.8-9.1 -5.5 0.5-9.6 5.3-9.1 10.8 3 34.1-4.5 67.8-21.6 97.4 -2.8 4.8-1.1 10.9 3.7 13.7 1.6 0.9 3.3 1.3 5 1.3 3.5 0 6.8-1.8 8.7-5C436.3 315.8 444.6 278.1 441.3 239.9z'/%3E%3Cpath d='M80 245.4c-5.5 0-10 4.5-10 10L70 256c0 5.5 4.5 10 10 10s10-4.5 10-10l0-0.6C90 249.9 85.5 245.4 80 245.4z'/%3E%3Cpath d='M416.5 213.6c1.2 4.5 5.2 7.5 9.7 7.5 0.8 0 1.7-0.1 2.6-0.3 5.3-1.4 8.5-6.9 7.1-12.2l-0.2-0.6c-1.4-5.3-6.9-8.5-12.3-7.1 -5.3 1.4-8.5 6.9-7.1 12.3L416.5 213.6z'/%3E%3Cpath d='M343.8 223l-57-32.9c-0.6-0.4-1.2-0.7-1.7-1l-56.8-32.8c-6.2-4.1-13.5-6.3-21-6.3 -21.1 0-38.2 17.1-38.2 38.2v135.7c0 0.4 0 0.9 0.1 1.3 0.2 6.3 2 12.4 5.1 17.8 6.8 11.7 19.4 19 33 19 6.7 0 13.3-1.8 19.1-5.1l0 0c0.3-0.2 116.7-67.4 117-67.6l0 0c6-3.3 11-8.3 14.4-14.3C368.3 256.8 362 233.5 343.8 223zM340.5 265c-1.6 2.9-4 5.2-6.9 6.8 0 0-0.1 0.1-0.1 0.1l0.1-0.1c-0.3 0.2-116.9 67.5-117.2 67.7l0 0c-2.8 1.6-5.9 2.4-9.1 2.4 -6.5 0-12.5-3.5-15.7-9 -1.6-2.8-2.4-5.9-2.4-9.1 0-0.4 0-0.7-0.1-1.1v-134.6c0-10 8.2-18.2 18.2-18.2 3.6 0 7.2 1.1 10.1 3.1 0.2 0.1 0.4 0.3 0.6 0.4l115.8 66.9c0.3 0.2-0.3-0.2 0 0 0.2 0.1-0.2-0.1 0 0l0 0c4.2 2.4 7.2 6.3 8.4 11C343.5 256 342.9 260.8 340.5 265z'/%3E%3C/svg%3E%0A"); | |||||
background-position: center; | |||||
background-repeat: no-repeat; | |||||
background-size: auto; | |||||
cursor: pointer; | |||||
border-radius: 50%; | |||||
transition: transform .25s; | |||||
width: 150px; | |||||
max-width: 75%; | |||||
height: 150px; | |||||
max-height: 75%; | |||||
opacity: .75; | |||||
} | |||||
.youtube-video__play-icon:hover { | |||||
transform: scale(1.1); | |||||
} | |||||
.youtube-video__iframe { | |||||
width: 100%; | |||||
height: 100%; | |||||
} | |||||
.attributions { | |||||
opacity: .5; | |||||
font-size: 1rem; | |||||
margin-top: 5rem; | |||||
margin-bottom: 1rem; | |||||
text-align: center; | |||||
padding-top: 1.5rem; | |||||
border-top: 2px solid #90CDF4; | |||||
} |