ChiefRed преди 3 години
ревизия
5251a63830
променени са 5 файла, в които са добавени 418 реда и са изтрити 0 реда
  1. +1
    -0
      .gitignore
  2. +196
    -0
      README.md
  3. +97
    -0
      index.html
  4. +15
    -0
      scripts.js
  5. +109
    -0
      styles.css

+ 1
- 0
.gitignore Целия файл

@@ -0,0 +1 @@
styled-youtube-embedding-labwork.code-workspace

+ 196
- 0
README.md Целия файл

@@ -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!

+ 97
- 0
index.html Целия файл

@@ -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&nbsp;YouTube Embedding&nbsp;Labwork</h1>
<noscript>
<p>This page requires JavaScript to function correctly.</p>
</noscript>
<p>The page layout is responsive.</p>
</header>
<section>
<h2>High&nbsp;resolution video&nbsp;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&nbsp;resolution video&nbsp;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&nbsp;aspect&nbsp;ratio video&nbsp;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&nbsp;<a href="https://www.flaticon.com/" title="Flaticon"
rel="noopener noreferrer nofollow" target="_blank">www.flaticon.com</a>
&bull;
Loading indicator from&nbsp;<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>

+ 15
- 0
scripts.js Целия файл

@@ -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))

+ 109
- 0
styles.css Целия файл

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

Loading…
Отказ
Запис