Privacy-friendly video embeds

If a picture's worth a thousand words and a video comprises many pictures, then a video's worth, erm… quite many words, really. From interviews to product reviews, tutorials to testimonials, behind-the-scenes glimpses to documentaries; video is a rich and highly engaging medium that can diversify and spice up the content of your website.

However, with the advent of privacy regulations on the internet, embedding a video in a web page has become more complicated than dropping an iframe in your HTML code. Many video platforms make use of cookies and similar technologies to track viewing behaviour, which requires consent from your visitors under the GDPR and ePrivacy regulations. This article will focus on Vimeo and YouTube, and how to be mindful of your users' privacy when embedding content from these two major video providers.

Vimeo

This is the simple case. Vimeo has a different business model than YouTube, based on paid memberships rather than advertising revenue. They don't need to track visitors no matter what and make it possible to opt out of any data collecting.

We do this by appending the dnt parameter to the embed URL. Short for Do Not Track, the dnt parameter will have the same effect as the browser setting of the same name. Setting the parameter to true or 1 prevents the Vimeo player from tracking any session data or gathering analytics. If the embed URL was https://player.vimeo.com/video/45010429, it now becomes https://player.vimeo.com/video/45010429?dnt=1.

All that's left is to make the video responsive using the classic intrinsic ratio trick. We wrap the iframe in another element:

html
<div class="video">
<iframe
class="video__iframe"
src="https://player.vimeo.com/video/45010429?dnt=1"
width="640"
height="360"
frameborder="0"
allow="autoplay; fullscreen; picture-in-picture"
allowfullscreen
>
</iframe>
</div>

And apply the following styles:

css
.video {
position: relative;
height: 0;
padding-bottom: 56.25%;
}

.video__iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}

With very little effort and without further measures, we comply with privacy regulations. So let's enjoy a dance, shall we?

Example

YouTube

Embedding YouTube videos requires a bit more work, as YouTube doesn't provide a mechanism to opt out of data collecting completely. There's a "privacy-enhanced" mode that serves the video from a different domain, https://www.youtube-nocookie.com, but contrary to what you might think, this still makes use of browser storage to identify the viewer.

In fact, when using the alternative domain, YouTube merely delays the use of cookies until the viewer clicks the play button on a video. Meanwhile, a unique identifier in the browser's localStorage allows YouTube to track users whether or not they watch the video. As far as privacy regulations go, these different browser storage mechanisms are equivalent. Consent is still needed.

Asking for consent

Privacy laws stipulate that consent must be freely given, informed and specific.

Consent is freely given when it results from a positive action, like the click of a button. We can't assume visitors agree to any data collecting just because they continue to surf on our website. We'll delay loading the video until consent is given.

Our visitors need to be informed. They need to know what they consent to. In this case: tracking and data processing by YouTube. We'll show a notice that explains this in simple terms, and link to YouTube's privacy policy for those who want the full details.

Finally, consent needs to be specific. Users should receive granular control over the types of data collecting they do or do not agree to. They should be able to reject YouTube's use of cookies while allowing Twitter's, for example. While we could resort to the ubiquitous cookie banner, I believe there's a better way. If visitors have to make a choice when landing on a website, they're likely to allow or reject all cookies. Instead, it makes sense to ask for consent at the time it's needed, when the user actually encounters a video. We'll therefore replace the video with a notice. You could call it just-in-time consent, which is now truly specific.

The markup

Let's build on to the video component we had earlier. We add a disclaimer and disable the iframe by turning the src into a data-src attribute. Notice how we're using the youtube-nocookie.com domain anyway, which might not be good, but is still better for privacy.

html
<div class="video">
<iframe
class="video__iframe"
width="560"
height="315"
data-src="https://www.youtube-nocookie.com/embed/I5e6ftNpGsU"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen="allowfullscreen"
>
</iframe>
<form class="video__notice">
<p>
We've embedded content from YouTube here. As YouTube may collect personal data and track your
viewing behaviour, we'll only load the video after you consent to their use of cookies and
similar technologies as described in their
<a href="https://www.youtube.com/t/privacy">privacy policy</a>.
</p>
<button>Allow content from YouTube</button>
</form>
</div>

Styling

We'll hide the iframe and show the notice by default. The notice will disappear when the iframe becomes active.

css
.video__iframe:not[src] {
display: none;
}

.video__iframe[src] + .video__notice {
display: none;
}

To ensure the notice doesn't overflow its container, we'll introduce a variation on the intrinsic ratio trick we used earlier. A pseudo-element ensures the aspect ratio of our video component is at least 16x9, but the container will grow if the notice grows taller, which might happen on smaller screens. A background color on the container offsets the video element from its surroundings, and the notice gets centered vertically with flexbox.

css
.video {
display: flex;
align-items: center;
position: relative;
background-color: rgb(0, 117, 121);
}

.video::before {
display: block;
content: '';
padding-bottom: 56.25%;
width: 0;
height: 0;
}

Then we style our notice:

css
.video__notice {
background-color: rgba(255, 255, 255, 0.9);
text-align: center;
padding: 1.5rem;
width: 100%;
}

.video__notice > * {
max-width: 38rem;
margin-left: auto;
margin-right: auto;
}

.video__notice > button {
font: inherit;
padding: 0.5rem 1rem;
background-color: rgb(0, 117, 121);
border: none;
color: #fff;
cursor: pointer;
}

.video__notice > button:hover,
.video__notice > button:focus
{
background-color: #000;
}

Nailing the thumbnail

It'd be nice to replace the background color on the container with the preview image for our video. The thumbnail hints to the video's content and entices visitors to watch it.

YouTube delivers these thumbnail images from the https://i.ytimg.com domain in different resolutions. The highest resolution image that's guaranteed to exist is https://i.ytimg.com/vi/<VIDEO ID>/hqdefault.jpg, which measures 480px wide and 360px tall. Higher resolution sddefault.jpg and maxresdefault.jpg images might be available for your video, depending on its quality. For the video in our example, that's not the case.

Now, how do we know which image is available? We could check manually if higher resolution images exist by visiting the different URL's, but that method doesn't scale well. An automated method would use YouTube's Data API to retrieve a list of all the thumbnails available for a video. This requires quite a bit of setup, which we won't cover here.

For Cloudinary users, there's a simpler method. We retrieve the highest resolution thumbnail for any YouTube video using a URL in the format https://res.cloudinary.com/<CLOUD NAME>/image/youtube/<VIDEO ID>.jpg. That image can then be scaled and altered using the array of image transformations that Cloudinary offers. Cloudinary probably uses YouTube's Data API behind the scenes, but does the hard work for us.

If you're willing to accept slightly grainy thumbnails, the easiest is of course to settle on the rather low-quality hqdefault image, even for higher resolution videos. That's the path we'll take.

No matter which option you prefer, add the image as a background on the video container:

html
<div
class="video"
style="background-image: url('https://i.ytimg.com/vi/I5e6ftNpGsU/hqdefault.jpg');"
>

<!-- The rest of our markup -->
</div>

And make sure it covers the whole component:

css
.video {
display: flex;
align-items: center;
position: relative;
background-color: rgb(0, 117, 121);
/* New styles */
background-position: center center;
background-size: cover;
}

We keep the background color on the container anyway; it'll be visible while the image loads. The result looks similar to this, where the button doesn't do anything yet:

Example

Activating the videos

Clicking the button on any of our videos should unblock all of them. We'll create an activateVideos function that does just that. We loop over all the iframes with a data-src attribute pointing to the youtube-nocookie.com domain, and set their src attribute.

js
function activateVideos() {
const iframes = document.querySelectorAll('.video__iframe[data-src*="youtube-nocookie.com"]');
iframes.forEach((iframe) => {
iframe.src = iframe.dataset.src;
});
}

Then we add event listeners to the notices:

js
function attachEvents() {
const notices = document.querySelectorAll('.video__notice');
notices.forEach((notice) => {
notice.addEventListener('submit', (event) => {
activateVideos();
event.preventDefault();
});
});
}

attachEvents();

CSS will take care of hiding the notice and showing the now active iframe instead.

Storing the user's preference

Right now, the user has to approve of YouTube videos again each time the page reloads. Similar to a cookie banner, visitors probably expect us to remember their preference. We'll use a cookie, yes, to do that.

To not reinvent the wheel on this one, we'll make use of the lightweight js-cookie library. We'll store a youtube-consent cookie when the user consents to YouTube videos. On every page load, we then check if the cookie's present, and show either the video or the notice depending on that.

The full script:

js
import Cookies from 'js-cookie';

function activateVideos() {
const iframes = document.querySelectorAll('.video__iframe[data-src*="youtube-nocookie.com"]');
iframes.forEach((iframe) => {
iframe.src = iframe.dataset.src;
});
}

function attachEvents() {
const notices = document.querySelectorAll('.video__notice');
notices.forEach((notice) => {
notice.addEventListener('submit', (event) => {
activateVideos();
// Store consent for one year
Cookies.set('youtube-consent', 'true', { expires: 365 });
event.preventDefault();
});
});
}

if (Cookies.get('youtube-consent') === 'true') {
activateVideos();
} else {
attachEvents();
}

Our component is now fully functional.

Example

Revoking consent

The last requirement is an option for users to revoke the consent. This could be a button or a toggle in the site's privacy or cookie policy. The button would delete the cookie, deactivate the videos, and attach the event listeners to the disclaimers again.

js
function deactivateVideos() {
const iframes = document.querySelectorAll('.video__iframe[src*="youtube-nocookie.com"]');
iframes.forEach((iframe) => {
iframe.removeAttribute('src');
});
}

Cookies.remove('youtube-consent');
deactivateVideos();
attachEvents();

Conclusion

It's important to be mindful of your users' privacy when embedding content from a third party. If, like Vimeo, the provider offers privacy controls, default to them; your users can only benefit from it. If not, ask for consent first, and load the content afterwards. Inform your visitors and provide an option to revoke consent.