@@ -0,0 +1 @@ | |||
styled-youtube-embedding-labwork.code-workspace |
@@ -0,0 +1,196 @@ | |||
# 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! |
@@ -0,0 +1,97 @@ | |||
<!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> |
@@ -0,0 +1,15 @@ | |||
(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)) |
@@ -0,0 +1,109 @@ | |||
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; | |||
} |