Code Snippet: GDPR compliant YouTube Videos

Version: 1.1.0 (Feb 07, 2022)



YouTube videos are embedded on many websites.

If you use the standard embedding methods provided by WordPress (Classic Editor, Gutenberg) or page builders (Oxygen, and many others) for this purpose and don't ask for the visitor's consent beforehand, you'll probably violate the General Data Protection Regulation (GDPR) in the EU: When displaying a page that contains a YouTube video, a connection to the YouTube servers is automatically established in order to load the preview image. In the process, the visitor's IP address, and thus personal information, is transmitted to YouTube. That's not allowed without explicit consent.

So how can you embed YouTube videos on your website in a GDPR compliant way?


The code snippet described here provides a WordPress shortcode that enables a GDPR compliant and at the same time as simple and efficient as possible embedding of YouTube videos.

Using this shortcode, the preview image for the video is already retrieved by the YouTube server and cached locally. The visitor's browser is given this local preview image, along with a note about the privacy policy. The browser does not need to establish a connection to YouTube at this point. The actual video is only loaded from YouTube once the visitor has clicked on it. This procedure is GDPR compliant because the visitor must first actively confirm that he wants to load the video from YouTube.

The code snippet also uses the special domain to retrieve the video, thus avoiding the use of cookies by YouTube.

And by the way, this solution should also have a significant positive impact on the well-known speed test tools (Page Speed Insights, GTMetrix, …), since no data is loaded from external servers.


The simplest syntax for the shortcode is:

[ma-gdpr-youtube id="4jlOF09WRw8"]

where 4jlOF09WRw8 is the ID of the YouTube video. You can easily copy the ID from the URL of the YouTube video:

This shortcode can easily be written directly in the Classic or Gutenberg editor.
In page builders, like e.g. Oxygen, there is usually a separate "Shortcode" element available for this purpose.

This shortcode generates the following output:

When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.

The thumbnail is not loaded from YouTube, but from the local cache.
In the center, the thumbnail is overlaid by the play button similar to the one on YouTube.
And at the bottom, the visitor sees a note on privacy policy in a colored bar.


The shortcode automatically loads the thumbnail for this video from YouTube.


Different image formats are retrieved:

Format NameAspect RatioSize
mqdefault16:9320 x 180 px
hqdefault4:3480 x 360 px
sddefault4:3640 x 480 px
hq72016:91280 x 720 px
maxresdefault16:9Original size, e.g. 1920 x 1080 px

The image formats are used for optimal display of the thumbnail in different screen resolutions.
The snippet uses the higher resolution thumbnail already at half of the next higher resolution to improve the display quality.


  • Although YouTube states in the documentation that the above formats are always available, some formats are missing for some videos. The snippet detects this and uses a suitable of the available formats instead.
  • The snippet loads the mentioned image formats in the two file formats JPG and WEBP and provides both to the browser for selection. The browser decides itself which format is loaded. This is for compatibility with older browsers.
  • The snippet provides the browser with a choice of all image sizes. The browser decides which image size to load based on the current size of the browser window (not the actual display size of the image!).


The thumbnails loaded from YouTube are automatically stored on the server in the /wp-content/uploads/ma-gdpr-youtube-thumbnails/ directory. The directory is created automatically if it doesn't already exist. A separate subdirectory is created for each video ID.

Shortcode Parameters

Besides the mandatory id parameter, the shortcode allows several other parameters, which are explained below.


The default aspect ratio for YouTube videos is 16:9.
The aspect-ratio parameter allows you to use a different aspect ratio, such as 4:3 or 1:1 (square).
Any aspect ratio can be specified here as long as the syntax "width:height" is kept as two numbers separated by a colon.
The 16:9 thumbnail will automatically fit the aspect ratio specified here.


[ma-gdpr-youtube id="vPhg6sc1Mk4" width="400px" aspect-ratio="1:1"]

When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.


The code snippet defines a notice text about the privacy policy in different languages:

DANår du har trykket, vil videoen blive indlæst fra YouTube's servere. Se vores privatlivspolitik for flere informationer.
DEBei Klick wird dieses Video von den YouTube Servern geladen. Details siehe Datenschutzerklärung.
ENWhen clicked, this video is loaded from YouTube servers. See our privacy policy for details.
ESAl hacer clic, este vídeo se carga desde los servidores de YouTube. Consulte la política de privacidad para más detalles.
FREn cliquant, cette vidéo est chargée depuis les serveurs de YouTube. Voir la politique de confidentialité.
HUKattintás után ez a videó a Youtube szervereiről kerül lejátszásra. A részletekért olvassa el az Adatkezelési Tájékoztatót oldalt.
ITQuando si clicca, questo video viene caricato dai server di YouTube. Vedere l'informativa sulla privacy per i dettagli.

The language is selected based on your website's or page language. Polylang language support is included in the snippet.
If no default text is available for your page's language, English is selected instead.

The notice text can be customized to your own requirements, for example other languages, via parameter gdpr-text, and can contain the following placeholders:
{privacy-policy-url} will be replaced by the URL of the privacy policy page configured in WordPress.
{privacy-policy-link} will be replaced with a full link to the privacy policy page configured in WordPress.

Note: The predefined default texts include a link to the privacy policy, if it's properly configured in WordPress.


[ma-gdpr-youtube id="vPhg6sc1Mk4" gdpr-text="Wanneer erop wordt geklikt, wordt deze video van de YouTube-servers geladen. Zie het {privacy-policy-link} voor details."]

Wanneer erop wordt geklikt, wordt deze video van de YouTube-servers geladen. Zie het Privacy Policy voor details.

Note: In this case the page title of the privacy policy is displayed in English, since this is the default language for this page.

The parameter gdpr-text can also be set to be empty. In this case, no hint text is displayed at all. This is especially conceivable in conjunction with the parameter new-window.

[ma-gdpr-youtube id="vPhg6sc1Mk4" gdpr-text=""]


By default, the notice text is displayed in the font size 0.7em, i.e. 70% of the text size defined for this block.
The gdpr-text-size parameter can be used to adjust the text size to your own requirements. Any valid CSS specification for the text size is allowed.


[ma-gdpr-youtube id="vPhg6sc1Mk4" gdpr-text-size="20px"]

When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.


By default, a width of 100% is set for the video block. The video block thus occupies the entire width of the enclosing block, e.g. a DIV or a column. The default of 100% allows easy adjustment of the responsive view by the parent element.
The height is automatically calculated from the width and aspect ratio.

If needed, the width can be changed with the width parameter. All valid CSS specifications for the width of an element are allowed.


[ma-gdpr-youtube id="vPhg6sc1Mk4" width="300px"]

When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.

Note: I recommend to leave the width at the default of 100% and instead style the enclosing block for responsive viewports using CSS media queries.


The parameter new-window opens the video in a new browser tab directly on the YouTube page.
This may be desirable if you want to display only very small thumbnails of the videos in WordPress, which are unsuitable for direct integration of the player.

[ma-gdpr-youtube id="vPhg6sc1Mk4" new-window=1]

When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.

alt / title

The alt and title parameters set the corresponding HTML attributes for the preview image. The alt attribute is used to support search engines and accessibility, the title attribute provides the tooltip text.

[ma-gdpr-youtube id="vPhg6sc1Mk4" alt="Video with sea noise" title="Sea Noise"]

Video with sea noise
When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.


With the parameter thumbnail you can use your own thumbnail instead of the one loaded from YouTube.
Either a full URL to an image can be specified here, or the media ID of an image that has already been uploaded to WordPress.

When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.

[ma-gdpr-youtube id="vPhg6sc1Mk4" thumbnail=""]

When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.

[ma-gdpr-youtube id="vPhg6sc1Mk4" thumbnail="1682"]

More YouTube Parameters

YouTube itself offers even more parameters for controlling video playback and the appearance of the player.
An overview of the available parameters can be found here:
Caution! Only the English version of this page is up to date. Language switching is done on the top right of the page using the language selection list.

YouTube Player API

When clicking on a video, the YouTube Player is loaded from the YouTube servers via YouTube Player API.
All the usual YouTube functions are available here.
I implemented the YouTube player in a way that when clicking on a video, a possibly other, already running video is paused.
Thus, two videos never run at the same time.


The code snippet is available for download here:

Version 1.1.0, 2022-02-07

Please note: The title of the code snippet has changed from "GDPR YouTube" to "MA GDPR YouTube".

For installation and use of the downloaded JSON file you will need the plugin Code Snippets or Advanced Scripts.
You can install the JSON file using the "Import" function of the plugin. 
Don't forget to activate the snippet after import.

Alternative: At the end of this page you can view and copy the complete source code of the snippet.

New functionalities and bug fixes are documented in the change log.


I have developed and tested the code snippet to the best of my knowledge under WordPress 5.9 and Oxygen 3.9.
I provide the code snippet for free use.
I cannot give any guarantee for the functionality because of the countless possible variations in WordPress environments.
Download and use of this code snippet is at your own risk and responsibility.


I enjoy developing code snippets and solving requirements with them. I provide the snippets free of charge.

If you like, you can honor my many hours of work with a small coffee donation via PayPal.

  When clicking the button, a connection to PayPal is established.

Your donation will of course be taxed properly by me.

Change Log

See "Version History" in Source Code

Source Code

Plugin Name:	MA GDPR YouTube
Description:	GDPR compliant YouTube video embedding
Author:			<a href="">Matthias Altmann</a>
Project:		Code Snippet: GDPR Compliant YouTube Embed
Version:		1.1.0
Plugin URI:
Description:	en:
Copyright:		© 2021-2022, Matthias Altmann

Version History:
Date		Version		Description
2022-02-07	1.1.0		New Features:
						- Support for webp thumbnail image format
						  (Requested by Artur Gilbert, Yan Kiara)
						- Support for additional YouTube player parameters (e.g. modestbranding=1)
						  (Requested by Lau Fa)
						- Added Dansk translations for notice text and button label
						  (Thanks to Theis L. Soelberg)
						- Added shortcode parameters alt and title for thumbnail image
						  (Requested by Yan Kiara)
						- Added shortcode parameters thumbnail (URL or media ID)
						  (Requested by Viorel-Cosmin Miron)
						- Added width/height attributes for thumbnail images
						  (Requested by Viorel-Cosmin Miron)
						- Added JS console debugging by URL parameter "debug"
						- Optimization of SVG symbol minimizing
2021-08-05	1.0.6		Features:
						- Using scheme-less URL to avoid issues with wrong WordPress URL configuration
						- Added parameter new-window to play video in a new window
						- Added "_" to valid character check on video id
						- Hide GDPR notice block if text is empty
						- Load and cache YouTube thumbnails only on very first appearance of a new video ID 
						  to improve performance, if specific YouTube thumbnail sizes are not available
						Bug Fixes:
						- Check for availability of specific thumbnail sizes (might not be available from YouTube)
2021-06-17	1.0.5		Fix: Correction in Hungarian translation
2021-06-17	1.0.4		Features: 
						- Added "-" to valid character check on video id (thanks to Zoltán Kőrösi)
						- Added Hungarian GDPR text (thanks to Zoltán Kőrösi)
2021-06-17	1.0.3		Fix: Check GET parameter "ct_builder" before accessing it
2021-06-15	1.0.2		Feature: Add link to privacy policy to default gdpr text if configured in WordPress
2021-06-15	1.0.1		Fix: Allow same video embedded multiple times
2021-06-15	1.0.0		Initial Release

if (!class_exists('MA_GDPR_YouTube')) :

class MA_GDPR_YouTube {

	const TITLE							= 'MA GDPR YouTube';
	const SLUG							= 'ma-gdpr-youtube';
	const VERSION						= '1.1.0';

	// ===== CONFIGURATION ==============================================================================================
	public static $timing				= false; 	// Write timing info to wordpress debug.log if WP_DEBUG enabled		
	public static $debug				= false; 	// Write debug info to wordpress debug.log if WP_DEBUG enabled	

	private static $default_aspect_ratio		= '16:9';		// aspect ratio of the video block. Syntax X:X
	private static $default_gdpr_text 			= [ 			// GDPR notice text in different languages.  
		'da' => ['Når du har trykket, vil videoen blive indlæst fra YouTube\'s servere. Se vores %s for flere informationer.','privatlivspolitik'],
		'de' => ['Bei Klick wird dieses Video von den YouTube Servern geladen. Details siehe %s.', 'Datenschutzerklärung.'],
		'en' => ['When clicked, this video is loaded from YouTube servers. See our %s for details.', 'privacy policy'],
		'es' => ['Al hacer clic, este vídeo se carga desde los servidores de YouTube. Consulte la %s para más detalles.', 'política de privacidad'],
		'fr' => ['En cliquant, cette vidéo est chargée depuis les serveurs de YouTube. Voir la %s.', 'politique de confidentialité'], 
		'hu' => ['Kattintás után ez a videó a Youtube szervereiről kerül lejátszásra. A részletekért olvassa el az %s oldalt.', 'Adatkezelési Tájékoztatót'],
		'it' => ['Quando si clicca, questo video viene caricato dai server di YouTube. Vedere %s per i dettagli.', 'l\'informativa sulla privacy'],
	private static $default_gdpr_text_size		= '.7em';		// font size for GDPR text
	private static $default_width				= '100%';		// width of the video block. Can be specified in %, px
	private static $default_new_window			= false;		// open video in new window

	// ===== INTERNAL ===================================================================================================
	private static $debug_level_base	= 0;		// initial debug level - for indentation of debug messages
	private static $thumbnails_base		= null;		// will be set to the thumbnail base folder dir and url
	private static $footercode_needed	= false;	// will be set to true if shortcode used on current page
	private static $footercode_minimize = true;		// should we minimize all footer code (style, script, svg)?
	private static $yt_image_url 		= '';
	private static $yt_image_sizes		= [ // various resolutions, not all might be available!
	// see
	//	tag		 						  aspect ratio	availability	resolution
	//	'default' 			=> 120,		// 		4:3		guaranteed		120x90
		'mqdefault' 		=> 320,		// 		16:9	guaranteed		320x180
		'hqdefault'			=> 480,		// 		4:3		guaranteed		480x360
		'sddefault'			=> 640,		// 		4:3		optional		640x480
		'hq720'				=> 1280,	// 		16:9	optional		1280x720
		'maxresdefault'		=> 1920,	// 		16:9	optional		(highest, depends on video, e.g. 1280x720, 1920x1080, ...)

	 * Initialize Snippet: 
	 * - Add shortcode "ma-gdpr-youtube"
	 * - Register hook action for wp_footer to emit footer code (style, script)
	 * - Set flag to emit footer code (style, script) when in Oxygen Builder
	public static function init() {
		$st = microtime(true);
		if (WP_DEBUG && (self::$timing || self::$debug)) {self::$debug_level_base = count(debug_backtrace()) -1;}

		self::$thumbnails_base = self::get_thumbnails_base();
		if (!self::$thumbnails_base) 	{self::set_admin_notice('warning', 'Error creating thumbnail cache base folder.'); return;}

		add_shortcode('ma-gdpr-youtube', [__CLASS__, 'shortcode']);

		if ( isset($_GET['ct_builder']) && ($_GET['ct_builder'] == true) ) {
			// emit styles, script, svg when Oxygen Builder is active
			self::$footercode_needed = true;
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', self::get_callstack_padding(), __CLASS__, __FUNCTION__, $et-$st));}
	 * Return padding for debug/timing strings. Evaluated based on initial debug_backtrace count. 
	 * @return int			The callstack level
	private static function get_callstack_padding(){
		return str_pad('',count(debug_backtrace()) - self::$debug_level_base);
	 * Set a WP Admin notice
	 * @param string $type		The notice type (error, warning, success, info)
	 * @param string $msg		The message
	private static function set_admin_notice($type, $msg) {
		add_action('admin_notices', function(){
			echo sprintf('<div class="notice notice-%s"><p>[%s] %s</p></div>', $type, MA_GDPR_YouTube::TITLE, $msg);
		error_log(sprintf('%s%s::%s() %s.', self::get_callstack_padding(), __CLASS__, __FUNCTION__, ucfirst($type).': '.$msg)); 

	 * Return base dir/url for video thumbnails. 
	 * Create directory /wp-content/ma-gdpr-youtube-thumbnails/ if necessary
	 * @return object		A dirinfo object (->dir, ->url)
	private static function get_thumbnails_base() {
		$st = microtime(true);

		$retval = (object)['dir'=>null,'url'=>''];
		$thumbnail_dir_info = wp_get_upload_dir();
		$retval->dir = $thumbnail_dir_info['basedir'].'/ma-gdpr-youtube-thumbnails';
		$retval->url = $thumbnail_dir_info['baseurl'].'/ma-gdpr-youtube-thumbnails';
		// create thumbnails folder if not exists
		if (!file_exists($retval->dir)) {
			if (!@mkdir($retval->dir)) {
				error_log(sprintf('[%s] Error creating thumbnail cache base folder.', self::TITLE)); 
				return null;
		// create scheme-less URL
		$retval->url = preg_replace('/^https?\:/','',$retval->url);

		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', self::get_callstack_padding(), __CLASS__, __FUNCTION__, $et-$st));}

		return $retval;
	 * Return a link to the privacy policy page (if configured in WordPress) or just the passed text
	 * @param string $text	The text to return if privacy policy page is not defined in WordPress
	 * @return string		The HTML link element for the privacy policy page or the initial text
	private static function get_privacy_policy_link($text) {
		$pplink = get_the_privacy_policy_link();
		return  $pplink ? $pplink : $text; 

	 * Handle the shortcode "ma-gdpr-youtube". 
	 * @param array $atts		The shortcode attributes
	 * @param string $content	The content of the shortcode
	 * @return string			The output
	public static function shortcode($atts, $content = '') {
		$st = microtime(true);
		$lang = self::get_current_language();

		// get defaults for unspecified atts
		$atts_default = [
			'id'				=> null,
			'uniqid'			=> null,
			'width'				=> self::$default_width,
			'aspect-ratio'		=> self::$default_aspect_ratio,
			'gdpr-text'			=> isset(self::$default_gdpr_text[$lang]) 
									? sprintf(self::$default_gdpr_text[$lang][0],self::get_privacy_policy_link(self::$default_gdpr_text[$lang][1])) 
									: sprintf(self::$default_gdpr_text['en'][0],self::get_privacy_policy_link(self::$default_gdpr_text['en'][1])),
			'gdpr-text-size'	=> self::$default_gdpr_text_size,
			'new-window'		=> self::$default_new_window,
			'alt'				=> '',
			'title'				=> '',
			'thumbnail'			=> null,
		$atts = (object)array_merge($atts_default, $atts);

		// any other parameter will be passed to youtube directly
		// see for a list of parameters
		// Note! rel=0 not supported anymore since September 2019 - without hacks which we won't support
		// See
		$yt_parameters = [];
		foreach ($atts as $att_key => $att_val) {
			if (!in_array($att_key,array_keys($atts_default))) {
				$yt_parameters[$att_key] = $att_val;
		$yt_parameters_json = json_encode($yt_parameters);

		if (!isset($atts->id) || ($atts->id == '' )) 			{return sprintf('[%s] Missing video id.',self::TITLE);}
		if (preg_match('/[^A-Za-z0-9\-\_]/',$atts->id))			{return sprintf('[%s] Invalid video id.',self::TITLE);}
		// generate an unique id (for the case a video is embedded multiple times)
		$atts->uniqid = $atts->id.'-'.uniqid();

		if (!self::$thumbnails_base) 							{return sprintf('[%s] Error creating thumbnail directory.',self::TITLE);}
		// check if we already have a thumbnail

		if (!self::check_thumbnails($atts->id)) 				{return sprintf('[%s] Error retrieving thumbnails.',self::TITLE);}

		$thumbnail = '';
		$sources = [];

		if ($atts->thumbnail) {
			if (is_numeric($atts->thumbnail)) {
				// numeric value => media id
				// get available image sizes
				$metadata = wp_get_attachment_metadata($atts->thumbnail);
				$image_sizes = [];
				foreach ($metadata['sizes'] as $key => $data) {
					// retrieve url for specific size
					$img_src = wp_get_attachment_image_src($atts->thumbnail,$key);	// '0': url, '1': width, '2': height, '3': resised
					$data['url'] = $img_src['0']; 
					$data['key'] = $key;
					$image_sizes[$key] = $data;
				// sort image sizes by width ascending
				uasort($image_sizes, function($a,$b){
					if ($a['width'] == $b['width']) {return 0;}
					return ($a['width'] < $b['width']) ? -1 : 1;

				// get largest image 
				$largest = (object)end($image_sizes);

				if (true) { // variant 1 by <picture> <source ...> <source ...> <ing ...> </picture>
					foreach ($image_sizes as $key => $data) {
						// to improve thumbnail quality, use higher res image if we reach half its size 
						$sources[] = '<source media="(min-width:'.($data['width']/2).'px)" type="'.$data['mime-type'].'" srcset="'.$data['url'].'">';
					// create thumbnail
					$thumbnail .= sprintf('<picture class="ma-gdpr-youtube-thumbnail">%s <img loading="lazy" src="%s" width="%s" height="%s" alt="%s" title="%s"></picture>',
										implode('',array_reverse($sources)), $largest->url, $largest->width, $largest->height, $atts->alt, $atts->title);


				if (false) { // variant 2 by <picture> <img ... srcset ... sizes ... > </picture>
					$srcset = wp_get_attachment_image_srcset($atts->thumbnail, $largest->key);
					// create sizes attribute 
					$sizes = [];
					foreach ($image_sizes as $key => $data) {
						$sizes[] = '(min-width: '.($data['width']/2).'px) '.$data['width'].'px';
					// largest as default in last position
					$sizes[] = $largest->width.'px';
					$sizes = implode(', ',$sizes);
					// create thumbnail
					$thumbnail .= '<picture class="ma-gdpr-youtube-thumbnail">';
					$thumbnail .= sprintf('<img loading="lazy" width="%s" height="%s" src="%s" alt="%s" title="%s" srcset="%s" sizes="%s">',
									$largest->width, $largest->height, $largest->url, $atts->alt, $atts->title, /*implode(', ',$srcsets) */ $srcset, $sizes);
					$thumbnail .= '</picture>';

			} else {
				$sources = [];
				$thumbnail .= '<picture class="ma-gdpr-youtube-thumbnail">';
				$thumbnail .= sprintf('<img loading="lazy" src="%s" alt="%s" title="%s">',$atts->thumbnail, $atts->alt, $atts->title);
				$thumbnail .= '</picture>';
		} else {
			$source_list = [];
			$sizes = [];
			// get sources
			foreach (self::$yt_image_sizes as $size_tag => $size) {
				$source_list[] = [$size_tag,$size];
			// get smallest thumbnail first
			list ($size_tag,$size) = array_shift($source_list);
			$img_src = self::$thumbnails_base->url.'/'.$atts->id.'/'.$atts->id.'_'.$size_tag.'.jpg';
			// get larger thumbnails
			while (count($source_list)) {
				list ($size_tag,$size) = array_shift($source_list);
				foreach(['jpg'=>'jpeg','webp'=>'webp',] as $ext => $mime) { // will be reversed! so webp before jpg to have jpg before webp in final output
					$img_path = self::$thumbnails_base->dir.'/'.$atts->id.'/'.$atts->id.'_'.$size_tag.'.'.$ext;
					if (file_exists($img_path)) {
						// get real image size
						$img_info = getimagesize($img_path);
						if ($img_info) {
							$img_width = $img_info[0];
							// skip if we already have this size/format combo (maxres might be same as hq720)
							if (in_array($img_width.'_'.$ext,$sizes)) {continue;}
							$sizes[] = $img_width.'_'.$ext;
							// to improve thumbnail quality, use higher res image if we reach half its size 
							$sources[] = '<source media="(min-width:'.($img_width/2).'px)" type="image/'.$mime.'" srcset="'.self::$thumbnails_base->url.'/'.$atts->id.'/'.$atts->id.'_'.$size_tag.'.'.$ext.'">';
			$thumbnail .= '<picture class="ma-gdpr-youtube-thumbnail">' . implode('',array_reverse($sources)) . '<img src="'.$img_src.'" alt="'.$atts->alt.'" title="'.$atts->title.'"></picture>';

		// calculate dimensions of video block depending on width and aspect ratio
		list ($arw,$arh) = explode(':',$atts->{'aspect-ratio'},2); // aspect ratio elements
		list ($width_value, $width_unit) = ['100','%']; // default width value and unit
		// split width value and unit
		if (count($matches) == 3) {array_shift($matches); list ($width_value, $width_unit) =  $matches;}
		// calculate block dimensions
		$block_width = $width_value.$width_unit;
		$block_height = ($width_value * ($arh/$arw)) . $width_unit;

		// privacy policy url and link
		$atts->{'gdpr-text'} = str_replace('{privacy-policy-url}', get_privacy_policy_url(), $atts->{'gdpr-text'});
		$atts->{'gdpr-text'} = str_replace('{privacy-policy-link}', get_the_privacy_policy_link(), $atts->{'gdpr-text'});

		$retval = sprintf(	'<div id="%7$s" data-video-id="%2$s" class="ma-gdpr-youtube-wrapper" style="width:%3$s;height:%4$s;padding-top:%4$s;" data-new-window="%8$s" data-yt-parameters="%9$s">'.
								'<svg class="ma-gdpr-youtube-button"><use xlink:href="#ma-gdpr-youtube-play-button"></use></svg>'.
								'<div class="ma-gdpr-youtube-notice" style="font-size:%6$s;">%5$s</div>'.
							count($yt_parameters) ? base64_encode($yt_parameters_json) : '',
		self::$footercode_needed = true;

		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', self::get_callstack_padding(), __CLASS__, __FUNCTION__, $et-$st));}

		return $retval;


	 * Check if thumbnails for video $id have been downloaded. Load if not yet available.
	 * @param string $id	The YouTube video ID
	 * @return	bool		True if downloaded, false if not downloaded
	private static function check_thumbnails($id) {
		$st = microtime(true);

		$retval = false;

		if (!self::$thumbnails_base) 	{goto DONE;}
		// check if directory for this video exists
		$vid_dir = self::$thumbnails_base->dir.'/'.$id;
		if (!file_exists($vid_dir)) {
			if (!@mkdir($vid_dir)) {error_log(sprintf('[%s] Error creating thumbnail cache folder for video %s.', self::TITLE, $id)); goto DONE;}
			foreach (self::$yt_image_sizes as $size_tag => $size) {
				foreach(['jpg'=>'','webp'=>'_webp'] as $ext => $url_appendix) {

					$img_path = $vid_dir.'/'.$id.'_'.$size_tag.'.'.$ext;
					if (!file_exists($img_path)) {
						$img_url = sprintf(self::$yt_image_url, $url_appendix, $id, $size_tag, $ext);
						// load and cache thumbnail
						if ($img_data = @file_get_contents($img_url)) {
							file_put_contents($img_path, $img_data);
		$retval = true;

		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s(%s) Timing: %.5f sec.', self::get_callstack_padding(), __CLASS__, __FUNCTION__, $id, $et-$st));}

		return $retval;
	 * Get the current page/post get_current_language
	 * @return string		The language code as e.g. "de", "en"
	private static function get_current_language(){
		$retval = get_locale();
		// Is Polylang available? 
		if (function_exists('pll_current_language')) {$retval = pll_current_language();}
		$retval = str_replace('_','-',$retval);
		$retval = explode('-',@$retval)[0];
		return $retval;
	 * Emits the footer code (styles, script) to handle the YouTube embedding
	public static function footercode() {
		$st = microtime(true);

		if (!self::$footercode_needed) {goto DONE;}
		// emit style
		$style = <<<'END_OF_STYLE'
		<style id="ma-gdpr-youtube-style">
			.ma-gdpr-youtube-wrapper {position:relative; display:flex;}
			.ma-gdpr-youtube-thumbnail {position:absolute; top:0; left:0; width:100%; height:100%; display:flex; cursor:pointer;}
			.ma-gdpr-youtube-thumbnail img {width:100%; height:100%; object-fit:cover; object-position:50% 50%;}
			.ma-gdpr-youtube-button {position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:70px; height:auto; cursor:pointer;}
			.ma-gdpr-youtube-notice {position:absolute; width:100%; left:0; right:0; bottom:0; max-width:100%; text-align:center; font-size:.7em; background-color:rgba(255,255,255,.8); padding:.2em .5em;}
			.ma-gdpr-youtube-notice:empty {display:none;}
		if (self::$footercode_minimize) { 
			$style = preg_replace('/\/\*.*?\*\//','',$style); 
			$style = preg_replace('/\r?\n */','',$style); 
			$style = preg_replace('/\t/','',$style); 

		echo $style;
		// emit code
		$script = <<<'END_OF_SCRIPT'
		<script id="ma-gdpr-youtube-script">
		var $magdpryt_debug = /[?&]debug/.test(;

		function get_yt_parameters_from_wrapper($ytWrapper) {
			var $retval = null;
			var $yt_parameters = $ytWrapper.attr('data-yt-parameters');
			if ($yt_parameters) {
				$magdpryt_debug && console.log('yt-parameters RAW',$yt_parameters);
				$yt_parameters = atob($yt_parameters);
				$magdpryt_debug && console.log('yt-parameters JSON',$yt_parameters);
				$yt_parameters = JSON.parse($yt_parameters);
				$magdpryt_debug && console.log('yt-parameters Object',$yt_parameters);
				if ($yt_parameters.hasOwnProperty) {
					for (var $key in $yt_parameters) {
						if (!isNaN($yt_parameters[$key])) {$yt_parameters[$key] = parseInt($yt_parameters[$key]);}
					$yt_parameters.enablejsapi = 1;
					$retval = $yt_parameters;
			return $retval;

				window.ma_gdpr_youtube_player = null;
				window.YT = null; /* prevent JS errors in Oxygen Builder */

				$('.ma-gdpr-youtube-wrapper :is(img,.ma-gdpr-youtube-button)').click(function() {
					/* get closest wrapper */
					var $ytWrapper = $(this).closest('.ma-gdpr-youtube-wrapper'); 
					/* check if video should be played in new window */
					var $new_window = $ytWrapper.attr('data-new-window')=='1';
					$magdpryt_debug && console.log('YouTube open in new-window:',$new_window);

					if ($new_window) {
						/* Get the video id from the parent div's id attribute */
						var $ytVidID = $ytWrapper.attr('data-video-id');
						/* check if additional yt parameters have been specified */
						var $yt_parameters = get_yt_parameters_from_wrapper($ytWrapper);
						var $ytp = $yt_parameters ? '&' + (new URLSearchParams($yt_parameters).toString()) : '';''+$ytVidID+$ytp,'video-'+$ytVidID);

					/* Instantiate a new YT player (replacing out wrapper), and play it when ready */
					if ( $('#ma-gdpr-youtube-player-api').length==0 ) { /* check if youtube player api has already been loaded */
						/* Load the YouTube API */
						window.onYouTubeIframeAPIReady = function() {
							$magdpryt_debug && console.log('YouTube API ready.');
						$magdpryt_debug && console.log('Loading YouTube API...');
						$('<script id="ma-gdpr-youtube-player-api" src=""><\/script>').appendTo('body');
					} else {
						/* YouTube API is already loaded */
				/* This gets called when the onReady event fires */
				window.ytVidPlay = function($ytWrapper) {
					/* Get the video id from the parent div's id attribute */
					var $ytVidID = $ytWrapper.attr('data-video-id');
					var $apiID = $ytWrapper.attr('id');
					$magdpryt_debug && console.log('Starting video '+$ytVidID+' from wrapper '+$apiID);
					window.ma_gdpr_youtube_player && (typeof window.ma_gdpr_youtube_player.pauseVideo!=='undefined') && window.ma_gdpr_youtube_player.pauseVideo();
					/* Get the dimensions of the wrapper */
					var $ytWrapperWidth = $ytWrapper.innerWidth();
					var $ytWrapperHeight = $ytWrapper.innerHeight();
					$magdpryt_debug && console.log('Video WxH',[$ytWrapperWidth,$ytWrapperHeight]);

					/* remove styles from wrapper */

					if (!YT) return; /* prevent JS errors in Oxygen Builder */


					var $apiConfig = {
						width: $ytWrapperWidth,
						height: $ytWrapperHeight ,
						videoId: $ytVidID,
						host: '',
						enablejsapi: 1,
						playerapiid: $apiID,
						rel: 0,
						events: {
							'onReady': function(event) { 
								$magdpryt_debug && console.log('Video ready.');
								window.ma_gdpr_youtube_player && (typeof window.ma_gdpr_youtube_player.playVideo!=='undefined') && window.ma_gdpr_youtube_player.playVideo();
							'onStateChange': function(event) {
								/* if multiple YT players are open, stop others when one is (re-)started. */
								/* see */
								$magdpryt_debug && console.log('Event',event);
								if ( == 1) { /* play */
									$('').each(function() { /* check for wrappers that are already a YT iframe */
										if ( ($(this).attr('id') != $apiID) ) { /* skip the current video */
											$magdpryt_debug && console.log('Pausing other video '+$(this).attr('data-video-id')+'.');
											/* send pause command */
											$(this)[0].contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*')
								if ( == 2) { /* pause */
									$magdpryt_debug && console.log('Pausing video ''.');

					/* check if additional yt parameters have been specified */
					var $yt_parameters = get_yt_parameters_from_wrapper($ytWrapper);
					if ($yt_parameters) {
						$apiConfig.playerVars = $yt_parameters;

					$magdpryt_debug && console.log('apiConfig',$apiConfig);
					window.ma_gdpr_youtube_player  = new YT.Player($apiID, $apiConfig);
		if (self::$footercode_minimize) { 
			$script = preg_replace('/\/\*.*?\*\//','',$script); 
			$script = preg_replace('/\r?\n */','',$script); 
			$script = preg_replace('/\t/','',$script); 
		echo $script;

		// emit play button svg symbol
		$symbol = <<<'END_OF_SYMBOL'
			<svg id="ma-gdpr-youtube-symbol" version="1.1" xmlns="" xmlns:xlink="" 
						aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;">
					<symbol id="ma-gdpr-youtube-play-button" viewBox="0 0 500 350" >
						<path fill="#f61c0d" d="M500,74.767C500,33.472,466.55,0,425.277,0 H74.722C33.45,0,0,33.472,0,74.767v200.467C0,316.527,33.45,350,74.722,350h350.555C466.55,350,500,316.527,500,275.233V74.767z  M200,259.578v-188.3l142.789,94.15L200,259.578z"/>
						<path fill="white" d="M199.928,71.057l0.074,188.537l142.98-94.182 L199.928,71.057z"/>
		if (self::$footercode_minimize) { $symbol = preg_replace('/\r?\n[\t ]*/','',$symbol); }
		echo $symbol;


		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', self::get_callstack_padding(), __CLASS__, __FUNCTION__, $et-$st));}



// Initialize

// Warn about incompatibilities (currently none)
	if (is_admin()) {
		if (!isset($GLOBALS['MA_GDPR_YouTube_Incompatibilities'])) {$GLOBALS['MA_GDPR_YouTube_Incompatibilities'] = [];}
		if (count($GLOBALS['MA_GDPR_YouTube_Incompatibilities'])) {
			if (WP_DEBUG && (MA_GDPR_YouTube::$debug || MA_GDPR_YouTube::$timing)) 
				{error_log('MA_GDPR_YouTube / Incompatibilities: '.print_r($GLOBALS['MA_GDPR_YouTube_Incompatibilities'],true));}
			add_action('admin_notices', function(){
				if (WP_DEBUG ) {error_log('MA_GDPR_YouTube/ Incompatibilities: '.print_r($GLOBALS['MA_GDPR_YouTube_Incompatibilities'],true));}
				$implementation = basename(__FILE__) == 'ma-gdpr-youtube.php' ? 'Plugin' : 'Code Snippet';
				echo '<div class="notice notice-warning is-dismissible">
						<p>The '.$implementation.'  "'.MA_GDPR_YouTube::TITLE.'" is skipped: '.implode(' or ',$GLOBALS['MA_GDPR_YouTube_Incompatibilities']).'</p>
}, 1000); 

	if (isset($GLOBALS['MA_GDPR_YouTubeIncompatibilities']) && count($GLOBALS['MA_GDPR_YouTube_Incompatibilities'])) return;
	if (wp_doing_ajax()) 		return; 	// don't run for AJAX requests
	if (wp_doing_cron()) 		return; 	// don't run for CRON requests
	if (wp_is_json_request()) 	return; 	// don't run for JSON requests
	if (is_favicon()) 			return; 	// don't run for favicon request
	if (isset($_SERVER['QUERY_STRING']) && ($_SERVER['QUERY_STRING'] == 'service-worker'))			return;	// don't run for service-worker
	if (isset($_SERVER['REQUEST_URI']) 	&& ($_SERVER['REQUEST_URI'] == '/favicon.ico'))				return;	// don't run for favicon
	if (isset($_SERVER['REQUEST_URI']) 	&& (strpos($_SERVER['REQUEST_URI'],'/wp-content/') === 0))	return;	// don't run for dynamic wp-content file
	if (is_admin()) {
		global $pagenow;
		if (	($pagenow != 'post-new.php') 
		&& 	(	($pagenow != 'post.php') || ($pagenow == 'post.php' && isset($_REQUEST['action']) && ($_REQUEST['action'] != 'edit'))	) 
		) return; // only load on specific requests where Gutenberg is involved
	$implementation = basename(__FILE__) == 'ma-gdpr-youtube.php' ? 'Plugin' : 'Code Snippet';
	if (WP_DEBUG && (MA_GDPR_YouTube::$debug || MA_GDPR_YouTube::$timing)) 
		{error_log(sprintf('%s Initializing %s for request URI="%s" action="%s"', MA_GDPR_YouTube::TITLE, $implementation, @$_SERVER['REQUEST_URI'], @$_REQUEST['action']));}


}, 1200); 

First published: Jun 15, 2021 on Code Snippet: GDPR compliant YouTube Videos