Code Snippet: GDPR compliant YouTube Videos

Version: 1.5.0 (Feb 26, 2023)



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 until the visitor has consented to this.


The simplest and shortest syntax for the shortcode is:

[ma-gdpr-youtube video="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

For the shortcode to work, it is mandatory to specify the desired YouTube video with either video or id.

In addition, the shortcode allows some more parameters, which are explained further.


The video parameter is used to specify a YouTube ID or a YouTube URL for the desired video.
The Video ID is currently usually 11 digits long and can contain letters, numbers, hyphens and underscores.
If a URL is specified, the ID is automatically determined from it. Numerous variants of YouTube URLs are supported.

Thus, the video parameter can completely replace the former id parameter.


When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.
[ma-gdpr-youtube video="vPhg6sc1Mk4"]
When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.
[ma-gdpr-youtube video=""]
When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.
[ma-gdpr-youtube video=""]


The id parameter was previously used to specify the video ID. It should no longer be used, and may not be supported in future versions. From now on, the video parameter should be used instead.

The Video ID is currently usually 11 digits long and can contain letters, numbers, hyphens and underscores.

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

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


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 is kept as two numbers separated by a colon or a slash: "width:height" or "width/height"
The thumbnail and the YouTube Player will automatically fit the aspect ratio specified here. The video itself will be shown in the original aspect ratio, possibly then with black bars.


[ma-gdpr-youtube video="vPhg6sc1Mk4" width="350px" 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.
FIKlikattuasi, tämä video ladataan Youtuben palvelimilta. Katso lisätietoja meidän tietosuojaselosteesta.
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.
JAクリックすると、この動画が YouTube サーバーから読み込まれます。詳細については、プライバシー ポリシー をご覧ください。

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 video="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 video="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 video="vPhg6sc1Mk4" gdpr-text-size="20px"]

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


The design of the privacy notice can be customized as needed.
White font on red bar? No problem!


[ma-gdpr-youtube video="vPhg6sc1Mk4" notice-style="background-color:red; color:white"]
When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.


With an own CSS class this can be done even more targeted. For example, you can also control the design of the link:


body .my-notice-class {
  background-color: red;
body .my-notice-class a {
  color: white;
  font-weight: bold;
[ma-gdpr-youtube video="vPhg6sc1Mk4" notice-class="my-notice-class"]
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 video="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.

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 video="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.

Since version 1.5.0 of this snippet, the original title of the video can also be used as alt and title attribute.


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 video="vPhg6sc1Mk4" thumbnail=""]

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

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


The snippet shows the preview image from YouTube, but no title to the video.
The parameter title-text can be used to display a title above the preview image.

[ma-gdpr-youtube video="vPhg6sc1Mk4" title-text="Sea Noise"]
Sea Noise
When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.

The title is displayed by default in the top left corner, white font in standard size, with a subtle shadow to make the text readable even on bright thumbnails.

Since version 1.5.0 of this snippet, the original title of the video can also be used as title-text attribute.


The default style for the title is defined as:

.ma-gdpr-youtube-title {
  width: 100%; 
  top: 1em; 
  padding: 0 1em; 
  color: white; 
  text-shadow: black 1px 1px 2px;

The parameter title-style can be used to adjust the positioning and design of the title.


Sea Noise
When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.
[ma-gdpr-youtube video="vPhg6sc1Mk4"title-text="Sea Noise" title-style="top:unset; bottom:30%;text-align:center"]


Sea Noise
When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.
[ma-gdpr-youtube video="vPhg6sc1Mk4" title-text="Sea Noise" title-style="top:1rem; font-size:2em; color:red; text-align:center;"]


To globally control the positioning and design of the titles, the parameter title-class allows to specify a global CSS class.


body .my-video-title-class {
  top: 1rem; 
  font-size: 1.5em; 
  line-height: 1;
  color: blue; 
  text-align: center;
body .my-video-title-class span {
  display: inline-block;
  transform: rotate(-15deg);
[ma-gdpr-youtube video="vPhg6sc1Mk4" title-text="Sea Noise <span>with Class</span>" title-class="my-video-title-class"]
Sea Noise with Class
When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.


As of version 1.2.0 the snippet provides different play button types.
The default button type is youtube, which does not need to be specified explicitly.
The parameter play-button can be used to select the desired button:

Button Type youtube

[ma-gdpr-youtube video="vPhg6sc1Mk4" play-button="youtube"]

Button Typ circle

[ma-gdpr-youtube video="vPhg6sc1Mk4" play-button="circle"]

Button Typ circle-o

[ma-gdpr-youtube video="vPhg6sc1Mk4" play-button="circle-o"]

Button Typ play

[ma-gdpr-youtube video="vPhg6sc1Mk4" play-button="play"]


The color of the YouTube Play button is #f61c0d. The other button types have white as default color.
The parameter play-button-color can be used to set a different color for the button.

When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.
[ma-gdpr-youtube video="vPhg6sc1Mk4" play-button-color="blue"]
When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.
[ma-gdpr-youtube video="vPhg6sc1Mk4" play-button="circle" play-button-color="green"]
When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.
[ma-gdpr-youtube video="vPhg6sc1Mk4" play-button="circle-o" play-button-color="black"]


The parameter play-button-class allows you to use a custom CSS class for the play button.
A green play button on the "Green Energy" page (Post ID 1245)? No problem!


body.postid-1245 .my-play-button {
[ma-gdpr-youtube video="vPhg6sc1Mk4" play-button-class="my-play-button"]
When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.


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 video="vPhg6sc1Mk4" new-window=1]

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

Global Parameters

All shortcode parameters can be defined globally.
This simplifies the uniform design of the videos and spares the definition of the parameters for each individual video.

For this purpose, a global variable $GLOBALS['ma_gdpr_youtube'] is created as an array and supplied with the desired settings.
This can be done quite easily with another code snippet with e.g. the following content:

$GLOBALS['ma_gdpr_youtube'] = [
 'play-button' => 'play',
 'play-button-class' => 'my-play-button',

A special solution applies here to the gdpr-text parameter, which is language-sensitive.
The specification of a global parameter gdpr-text sets the text globally to a uniform value.
The addition of a language flag controls the text output per language.

$GLOBALS['ma_gdpr_youtube'] = [
 'gdpr-text' => 'Read the {privacy-policy-link}.',
 'gdpr-text-de' => 'Info zum <a href="{privacy-policy-url}" target="_blank">Datenschutz</a>',

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.


rel=0When paused or at the end of the video, "Similar videos" are displayed. This cannot be turned off.
But you can control which videos are shown:
rel=1 Similar videos from the whole YouTube archive
rel=0 Similar videos only from the current channel
modestbranding=1By default, a YouTube logo is displayed at the bottom of the player.
With modestbranding=1 this logo is hidden.
fs=0By default, an icon button is displayed for full-screen display of the video.
With fs=0 this button will not be displayed.

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.

YouTube Title Loader

Since version 1.5.0 of the snippet, the title of the video can be automatically loaded from YouTube.
This title can then be displayed with the special parameter title-text="@", and can also be used for the attribution of the preview image with alt="@" and title="@".
If the YouTube video offers a suitable translation for the language currently used on the website, this language version is used.


[ma-gdpr-youtube video="CwRvM2TfYbs" title-text="@" alt="@" title="@"]

On the German version of this page you'll see the German title of this video.

Gentle healing music of health and to calm the nervous system, deep relaxation! Say Life Yes
Gentle healing music of health and to calm the nervous system, deep relaxation! Say Life Yes
When clicked, this video is loaded from YouTube servers. See our Privacy Policy for details.

FAQ – Frequently Asked Questions

Safari provides a setting "Auto-Play" for media.
If this setting is set to "Never Auto-Play" or "Stop media with sound", the automatic start of the video is suppressed.
Unfortunately, I am not aware of any way to bypass this lock.

Yes, by using SVG code.
For this we need a SVG symbol definition. Add a code block at the end of your page:
(In Gutenberg as HTML, in Oxygen as Code Block).

<svg 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-custom" viewBox="0 0 20 20">
      <path fill="currentColor" d="M5 20c-0.128 0-0.256-0.049-0.354-0.146-0.195-0.195-0.195-0.512 0-0.707l8.646-8.646-8.646-8.646c-0.195-0.195-0.195-0.512 0-0.707s0.512-0.195 0.707 0l9 9c0.195 0.195 0.195 0.512 0 0.707l-9 9c-0.098 0.098-0.226 0.146-0.354 0.146z"/>

At the ID, after ma-gdpr-youtube-play-button-, you specify the desired identifier, e.g. custom, for your own play button.
The <path /> is the SVG code for your own button. If you specify fill="currentColor" here, you can control the color of the button with the shortcode parameter play-button-color.
Please also adapt the viewBox to your SVG code.

In your shortcode you can now select your own button with the parameter play-button="custom".


The code snippet is available for download here:

Version 1.5.0, 2023-02-26

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 6.0.2, 6.1.1
- Oxygen 3.9, 4.0.4, 4.1.1, 4.4
- PHP 7.4, 8.0, 8.1
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.5.0
Plugin URI:
Description:	en:
Copyright:		© 2021-2023, Matthias Altmann

Version History:
Date		Version		Description
2023-02-26	1.5.0		New Features:
						- Title of the video is now automatically retrieved from YouTube and stored locally.
						  Request to YouTube is made using the current language to retrieve translated title if 
						  available. Titles are stored per language. So only one request per video and language. 
						  Title from YouTube can be displayed using parameter title-text="@". 
						  Title from YouTube can also be used as alt="@" and title="@" for the thumbnail.
						- Shortcode attributes title-text, gdpr-text now allow HTML (e.g. for bold, links, ...)
						- If video is played in a modal (Oxygen, Bricks), don't restart video after closing modal.
						  Implemented via visibility check in onReady event.
						  (Requested by Manuel Mochkal)
						- Added compatibility check for allow_url_fopen. Must be On for the snippet to work.
						- Thumbnail size is now based on width and aspect-ratio instead of calculated height.
						- Changed JS function variable from ytVidPlay to ma_gdpr_youtube_ytVidPlay
2023-02-09	1.4.0		New Features:
						- Added parameter "video" with parsing of YouTube URL variants
						- Added global configuration via $GLOBALS['ma_gdpr_youtube']
						- Added Finnish translations for GDPR text
						  (Thanks to Thao Le)
2022-11-27	1.3.0		New Features:
						- Added Japanese translations for GDPR text
						  (Thanks to Viorel-Cosmin Miron)
						- Added support for WPML
						  (Thanks to Viorel-Cosmin Miron)
						  Please note there's a WPML bug preventing the creation a policy link with correct language.
						- Migrated JavaScript from jQuery to vanilla JS (ES6) to eliminate jQuery dependency.
						- Changed init skip for JSON calls (introduced in 1.1.0) to allow rendering in Oxygen Builder 
2022-09-25	1.2.0		New Features:
						- Added new shortcode parameters title-text, title-class, title-style for title overlay
						- Added new shortcode parameters notice-class, notice-style for GDPR text banner
						- Added new shortcode parameters play-button, play-button-color, play-button-style for button variations
						- Added original image size to source set for custom thumbnail by ID
						- Removed double '.' for German GDPR text.
						  (Thanks to Tobias Maximilian Hietsch for reporting)
						- Removed excess trailing comma at sprintf arguments
						  (Thanks to Nils Bäßler for reporting)
2022-02-07	1.1.0		New Features:
						- Support for webp thumbnail image format
						  (Requested by Artur Gilbert, Yan Kiara)
						- Added lazy loading for thumbnail images
						  (Requested by Yan Kiara)
						- Support for additional YouTube player parameters (e.g. modestbranding=1)
						  (Requested by Lau Fa)
						- Added Dansk translations for GDPR text
						  (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		New 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
						- 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.5.0';

	// ===== CONFIGURATION ==============================================================================================
	public static $timing				= false; 	// Write timing 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 or 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'],
		'fi' => ['Klikattuasi, tämä video ladataan Youtuben palvelimilta. Katso lisätietoja meidän %s.', 'tietosuojaselosteesta'],
		'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'],
		'ja' => ['クリックすると、この動画が YouTube サーバーから読み込まれます。詳細については、%s をご覧ください。', 'プライバシー ポリシー'],
	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 $trace_level_base	= 0;		// initial trace 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_urlformat_image 	= '';
	private static $yt_urlformat_watch 	= '';
	private static $total_runtime		= 0;

	private static $yt_image_sizes		= [ // various resolutions, not all might be available!
	// see
	//	tag		 						  aspect ratio	availability	resolution	black bars
	//	'default' 			=> 120,		// 		4:3		guaranteed		120x90		
		'mqdefault' 		=> 320,		// 		16:9	guaranteed		320x180		no
		'hqdefault'			=> 480,		// 		4:3		guaranteed		480x360		yes
		'sddefault'			=> 640,		// 		4:3		optional		640x480		yes
		'hq720'				=> 1280,	// 		16:9	optional		1280x720	no
		'maxresdefault'		=> 1920,	// 		16:9	optional		(highest)	no
		// 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::$trace_level_base = count(debug_backtrace()) -1;}

		// check thumbnails directory
		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']);
		// for timing
		add_action('shutdown', [__CLASS__,'total_runtime']);

		if ( ($_GET['ct_builder']??null) == 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));}
		self::$total_runtime += $et-$st;
	 * Return padding for 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(!DEBUG_BACKTRACE_PROVIDE_OBJECT|DEBUG_BACKTRACE_IGNORE_ARGS)) - self::$trace_level_base);
	 * Logs total timing for shortcodes on a page 
	public static function total_runtime(){
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', self::get_callstack_padding(), __CLASS__, __FUNCTION__,self::$total_runtime));}
	 * 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() use($type, $msg){
			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|null	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(string $text='privacy policy') {
		$pplink = get_the_privacy_policy_link();
		return  $pplink ? $pplink : $text; 
	 * Parses a YouTube URL for a video ID.
	 * Handles numerous URL formats, or plain video ID.
	 * @param string $s	The URL/string to parse
	 * @return array	Array containing keys v (video ID), optional t (start time), rel (related switch), or empty
	private static function parse_yt_url(string $s=''): array {
		$st = microtime(true);
		$retval = [];
		// regex for parsing youtube url variants
		$re = '/^
			(?:https?\:)?\/{2}														# protocol http, https, or schemeless
			(?:www\.)?																# optional www
			(?:youtu\.be|youtube\.com|youtube-nocookie\.com)\/						# domain variants
			(?:(?:embed|shorts|user\/\w+\#(?:\w+\/)+|watch|ytscreeningroom)[\/]?) ?	# optional specs
			(?:[\?\&]? (?:v|vi)[\/=])?												# optional v, vi parameter
			([A-Za-z0-9\-\_]+)														# THE ID
		// parse url
		if (preg_match($re,$s,$matches)) 							{$retval['v'] = $matches[1];}
		// id only?
		else if (preg_match('/^([A-Za-z0-9\-\_]+)$/',$s,$matches))	{$retval['v'] = $matches[1];}
		if (isset($retval['v'])) { // found video id? try to find more parameters
			// timecode
			if (preg_match('/(?:[&\?])(?:t|start)=(\d+)/',$s,$matches)) {$retval['t'] = $matches[1];}
			// rel
			if (preg_match('/(?:[&\?])rel=(\d+)/',$s,$matches)) {$retval['rel'] = $matches[1];}
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s("%s") => %s Timing: %.5f sec.', 
			self::get_callstack_padding(), __CLASS__, __FUNCTION__, $s, json_encode($retval), $et-$st));}
		return $retval;
	 * Retrieve video info from YouTube, for current language
	 * @param string $url	The watch URL, containing the video id
	 * @param string $lang	The current language
	 * @param string $info_file_path The file path of the cache file
	 * @param object $info	The info object to fill
	 * @return object|null	The info object
	private static function retrieve_video_info(string $url, string $lang, string $info_file_path, object $info) {
		// retrieve from YouTube using current language
		$options = ['http'=>['method'=>'GET','header'=>"Accept-language: {$lang}\r\n"]];
		$context = stream_context_create($options);
		if ($html = @file_get_contents($url,false,$context)) {
			// optional: save as HTML - for debugging
			if (($matches[1]??null) == 'YouTube') {
				// YouTube doesn't provide proper attributes.
				Goto DONE;
			// try to find translated title from the script
			preg_match('/{"playerOverlayVideoDetailsRenderer":{"title":{"simpleText":"(.+?)"}/',$html,$matches) && $info->title->$lang = $matches[1];
			// or get title from title or meta title
			if (!($info->title->$lang??null))	
				preg_match('/<title>(.+?)<\/title>/s',$html,$matches) && $info->title->$lang = preg_replace('/- YouTube$/','',$matches[1]);
			if (!($info->title->$lang??null))	
				preg_match('/<meta name="title" content="(.+?)">/',$html,$matches) && $info->title->$lang = $matches[1];
			// get some other info for maybe later use
			preg_match('/<meta name="description" content="(.+?)">/',$html,$matches)			&& $info->description->$lang = $matches[1];
			preg_match('/<meta name="keywords" content="(.+?)">/',$html,$matches)				&& $info->keywords->$lang = $matches[1];
			preg_match('/<meta itemprop="genre" content="(.+?)">/',$html,$matches)				&& $info->genre->$lang = $matches[1];
			preg_match('/<link rel="shortlinkUrl" href="(.+?)">/',$html,$matches)				&& $info->shortlinkurl = $matches[1];
			preg_match('/<meta itemprop="isFamilyFriendly" content="(.+?)">/',$html,$matches)	&& $info->familyfriendly = $matches[1];
			preg_match('/<meta itemprop="datePublished" content="(.+?)">/',$html,$matches)		&& $info->datepublished = $matches[1];
			preg_match('/<meta itemprop="uploadDate" content="(.+?)">/',$html,$matches)			&& $info->dateuploaded = $matches[1];

			// save 
			@file_put_contents($info_file_path, json_encode($info,JSON_PRETTY_PRINT));
		return $info;
	 * Get the info for a video, for the current language, either from cache or from YouTube
	 * @param string $id 	The video id
	 * @param string $lang	The currnet language
	 * @return object|null	The info object
	private static function get_video_info(string $id, string $lang) {
		$st = microtime(true);
		$info_file_path = self::$thumbnails_base->dir.'/'.$id.'/'.$id.'.json';
		$info = null;
		$url = sprintf(self::$yt_urlformat_watch,$id);

		// create an empty info object. Some fields may be available in different languages
		$info = (object)[
			'title'			=> (object)[],
			'description'	=> (object)[],
			'keywords'		=> (object)[],
			'genre'			=> (object)[],
			'shortlinkurl'	=> '',
			'familyfriendly'=> '',
			'datepublished'	=> '',
			'dateuploaded'	=> '',
		if (file_exists($info_file_path)) {
			// read from cache
			if ($info = json_decode(@file_get_contents($info_file_path))) {
				// current language already available?
				if (property_exists($info->title,$lang)) {
					goto DONE;
		// either info or language missing. Retrieve from YouTube
		$info = self::retrieve_video_info($url,$lang,$info_file_path,$info);

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

	 * 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(string $id): bool {
		$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;}
			// retrieve thumbnails
			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_urlformat_image, $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(): string {
		$retval = get_locale();
		// Is a translation plugin active? Supporting Polylang, WPML 
		foreach (['pll_current_language','wpml_current_language'] as $func) {
			if (function_exists($func)) {$retval = $func(); break;}
		$retval = str_replace('_','-',$retval);
		$retval = explode('-',@$retval)[0];
		return $retval;

	 * 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(array $shortcode_atts = [], string $content = '') {
		$st = microtime(true);
		if (WP_DEBUG && self::$timing) {self::$trace_level_base = count(debug_backtrace()) -1;}
		$lang = self::get_current_language();

		// get defaults for unspecified attributes
		$atts_default = [
			'id'				=> null,
			'video'				=> null,
			'uniqid'			=> null,
			'width'				=> self::$default_width,
			'aspect-ratio'		=> self::$default_aspect_ratio,
			'notice-class'		=> null,
			'notice-style'		=> null,
			'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,
			'alt'				=> null,
			'title'				=> null,
			'thumbnail'			=> null,
			'title-text'		=> null,
			'title-class'		=> null,
			'title-style'		=> null,
			'play-button'		=> 'youtube', // currently included: youtube, circle, circle-o, play
			'play-button-style'	=> null,
			'play-button-color'	=> null,
			'new-window'		=> self::$default_new_window,
		// merge global settings
		$atts = array_merge($atts_default, $GLOBALS['ma_gdpr_youtube']??[]);
		// choose correct language for gdpr-text
		if ($GLOBALS['ma_gdpr_youtube']['gdpr-text-'.strtolower($lang)]??'') {
			$atts['gdpr-text'] = $GLOBALS['ma_gdpr_youtube']['gdpr-text-'.strtolower($lang)];
		// allow html in shortcode attributes title-text, gdpr-text
		foreach (['title-text','gdpr-text'] as $att) {
			if ($shortcode_atts[$att]??'') {$shortcode_atts[$att] = html_entity_decode($shortcode_atts[$att]);}
		// merge shortcode attributes
		$atts = (object)array_merge($atts, $shortcode_atts);

		if ($atts->video) {
			$video = self::parse_yt_url($atts->video);
			if (isset($video['v'])) {
				$atts->id = $video['v'];
				if (isset($video['t'])) 	{$atts->t = $video['t'];}
				if (isset($video['rel'])) 	{$atts->rel = $video['rel'];}

		// 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);}

		// load youtube attributes
		if ($info = self::get_video_info($atts->id,$lang)) {
			if ($atts->alt == '@') 				$atts->alt = esc_html($info->title->$lang??'');
			if ($atts->title == '@') 			$atts->title = esc_html($info->title->$lang??'');
			if ($atts->{'title-text'} == '@') 	$atts->{'title-text'} = esc_html($info->title->$lang??'');

		$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 = [];
				// add original size
				$img_src = wp_get_attachment_image_src($atts->thumbnail,'full');	// '0': url, '1': width, '2': height, '3': resized
				$mime_type = get_post_mime_type($atts->thumbnail);
				$image_sizes['original'] = [
					'file'		=> $metadata['file'],
					'width'		=> $metadata['width'],
					'height'	=> $metadata['height'],
					'mime-type'	=> $mime_type,
					'filesize'	=> $metadata['filesize']??null,
					'url'		=> $img_src[0],
					'key'		=> 'full',
				// add resized 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': resized
					$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 ...> <img ...> </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('/',str_replace(':','/',$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'});

		// title overlay
		$title_overlay = !empty($atts->{'title-text'})
			? sprintf('<div class="ma-gdpr-youtube-title %1$s" %2$s>%3$s</div>',
				$atts->{'title-class'} ?? '',
				$atts->{'title-style'} ? 'style="'.$atts->{'title-style'}.'"' : '',
			: '';

		// play button style, color
		$play_button_style = '';
		if ($atts->{'play-button-style'}) {$play_button_style .= $atts->{'play-button-style'}.';';}
		if ($atts->{'play-button-color'}) {$play_button_style .= 'color:'.$atts->{'play-button-color'}.';';}
		if ($play_button_style) {$play_button_style = 'style="'.$play_button_style.'"';}

		// old: based on caclulated height
		// $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">'.
		// new: based on aspect-ratio
		$retval = sprintf(	'<div id="%7$s" data-video-id="%2$s" class="ma-gdpr-youtube-wrapper" style="width:%3$s;aspect-ratio:%16$s;" data-new-window="%8$s" data-yt-parameters="%9$s">'.
								'<svg class="ma-gdpr-youtube-button button-%13$s %14$s" %15$s><use xlink:href="#ma-gdpr-youtube-play-button-%13$s"></use></svg>'.
								'<div class="ma-gdpr-youtube-notice %11$s" style="font-size:%6$s; %12$s">%5$s</div>'.
					/*1*/	self::$thumbnails_base->url, 
					/*2*/	$atts->id,
					/*3*/	$block_width,
					/*4*/	$block_height,
					/*5*/	$atts->{'gdpr-text'},
					/*6*/	$atts->{'gdpr-text-size'},
					/*7*/	$atts->uniqid,
					/*8*/	$atts->{'new-window'},
					/*9*/	count($yt_parameters) ? base64_encode($yt_parameters_json) : '',
					/*10*/	$title_overlay,
					/*11*/	$atts->{'notice-class'} ?? '',
					/*12*/	$atts->{'notice-style'} ?? '',
					/*13*/	$atts->{'play-button'} ?? '',
					/*14*/	$atts->{'play-button-class'} ?? '',
					/*15*/	$play_button_style,
					/*16*/	str_replace(':','/',$atts->{'aspect-ratio'})
		self::$footercode_needed = 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__, /*$atts->id??''*/  json_encode($shortcode_atts), $et-$st));}
		self::$total_runtime += $et-$st;
		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; isolation:isolate;}
			.ma-gdpr-youtube-thumbnail {position:absolute; z-index:1; 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; z-index:4; top:50%; left:50%; transform:translate(-50%,-50%); width:70px; height:70px; cursor:pointer; color:white;}
			.ma-gdpr-youtube-button.button-youtube {color:#f61c0d;}
			.ma-gdpr-youtube-button.button-circle {filter:drop-shadow(0px 0px 4px darkgray);}
			.ma-gdpr-youtube-button.button-circle-o {filter:drop-shadow(0px 0px 4px darkgray);}
			.ma-gdpr-youtube-notice {position:absolute; z-index:2; 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;}
			.ma-gdpr-youtube-title {position:absolute; z-index:3; width:100%; top:1em; padding:0 1em; color:white; text-shadow: black 1px 1px 2px;}
		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) {
			let $retval = null;
			let $yt_parameters = $ytWrapper.getAttribute('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 (let $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 */

		/* click handler */
		document.querySelectorAll('.ma-gdpr-youtube-wrapper :is(img,.ma-gdpr-youtube-button)').forEach(($trigger) => {
				/* get closest wrapper */
				const $ytWrapper = this.closest('.ma-gdpr-youtube-wrapper'); 
				/* check if video should be played in new window */
				const $new_window = $ytWrapper.getAttribute('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 */
					const $ytVidID = $ytWrapper.getAttribute('data-video-id');
					/* check if additional yt parameters have been specified */
					const $yt_parameters = get_yt_parameters_from_wrapper($ytWrapper);
					const $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 */
				/* check if youtube player api has already been loaded */
				if ( document.querySelectorAll('#ma-gdpr-youtube-player-api').length==0 ) {
					/* handler for YT API loaded */
					window.onYouTubeIframeAPIReady = function() {
						$magdpryt_debug && console.log('YouTube API ready.');
					/* load the YouTube API */
					$magdpryt_debug && console.log('Loading YouTube API...');
					const $script = document.createElement('script');
					$ = 'ma-gdpr-youtube-player-api';
					$script.src = '';
				} else {
					/* YouTube API is already loaded */

		/* This gets called when the YT iframe onReady event fires */
		window.ma_gdpr_youtube_ytVidPlay = function($ytWrapper) {
			/* get the video id from the parent div's id attribute */
			const $ytVidID = $ytWrapper.getAttribute('data-video-id');
			const $apiID = $ytWrapper.getAttribute('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 inner dimensions of the wrapper */
			const $ytWrapperWidth = window.getComputedStyle($ytWrapper).width;
			const $ytWrapperHeight = window.getComputedStyle($ytWrapper).height;
			$magdpryt_debug && console.log('Video WxH',[$ytWrapperWidth,$ytWrapperHeight]);

			/* remove styles from wrapper */
			$ = $ytWrapperHeight;
			$ = 'unset';

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

			const $apiConfig = {
				width: parseInt($ytWrapperWidth),
				height: parseInt($ytWrapperHeight),
				videoId: $ytVidID,
				host: '',
				enablejsapi: 1,
				playerapiid: $apiID,
				rel: 0,
				events: {
					'onReady': function($event) { 
						$magdpryt_debug && console.log('Video ready.',$event);
						/* check if video is visible. The ready event is also fired if closing a modal on e.g. Oxygen, Bricks. This prevents playing the video again in background */
						if ($ && $ && ($ === null)) {return;}
						$magdpryt_debug && console.log('Starting video.');
						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 */
							/* check for wrappers that are already a YT iframe */
							document.querySelectorAll('').forEach(($elm) => { 
								if ( ($elm.getAttribute('id') != $apiID) ) { /* skip the current video */
									$magdpryt_debug && console.log('Pausing other video '+$elm.getAttribute('data-video-id')+'.');
									/* send pause command */
									$elm.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*')
						if ($ == 2) { /* pause */
							$magdpryt_debug && console.log('Pausing video '+$'.');

			/* check if additional yt parameters have been specified */
			let $yt_parameters = get_yt_parameters_from_wrapper($ytWrapper);
			if ($yt_parameters) {
				$magdpryt_debug && console.log('YouTube API parameters for video '+$ytWrapper.getAttribute('data-video-id')+':',$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-symbols" 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-youtube" viewBox="0 0 500 350" >
						<path fill="currentColor" 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"/>
					<symbol id="ma-gdpr-youtube-play-button-circle" viewBox="0 0 24 28" >
						<path fill="currentColor" d="M12 2c6.625 0 12 5.375 12 12s-5.375 12-12 12-12-5.375-12-12 5.375-12 12-12zM18 14.859c0.313-0.172 0.5-0.5 0.5-0.859s-0.187-0.688-0.5-0.859l-8.5-5c-0.297-0.187-0.688-0.187-1-0.016-0.313 0.187-0.5 0.516-0.5 0.875v10c0 0.359 0.187 0.688 0.5 0.875 0.156 0.078 0.328 0.125 0.5 0.125s0.344-0.047 0.5-0.141z"/>
					<symbol id="ma-gdpr-youtube-play-button-circle-o" viewBox="0 0 24 28" >
						<path fill="currentColor" d="M18.5 14c0 0.359-0.187 0.688-0.5 0.859l-8.5 5c-0.156 0.094-0.328 0.141-0.5 0.141s-0.344-0.047-0.5-0.125c-0.313-0.187-0.5-0.516-0.5-0.875v-10c0-0.359 0.187-0.688 0.5-0.875 0.313-0.172 0.703-0.172 1 0.016l8.5 5c0.313 0.172 0.5 0.5 0.5 0.859zM20.5 14c0-4.688-3.813-8.5-8.5-8.5s-8.5 3.813-8.5 8.5 3.813 8.5 8.5 8.5 8.5-3.813 8.5-8.5zM24 14c0 6.625-5.375 12-12 12s-12-5.375-12-12 5.375-12 12-12 12 5.375 12 12z"/>
					<symbol id="ma-gdpr-youtube-play-button-play" viewBox="0 0 24 28" >
						<path fill="currentColor" d="M21.625 14.484l-20.75 11.531c-0.484 0.266-0.875 0.031-0.875-0.516v-23c0-0.547 0.391-0.781 0.875-0.516l20.75 11.531c0.484 0.266 0.484 0.703 0 0.969z"/>

		if (self::$footercode_minimize) { $symbol = preg_replace('/\r?\n[\t ]*/','',$symbol); }
		echo $symbol;
		// @since 1.4.0 debugging info
		$codebase = basename(__FILE__) == 'ma-gdpr-youtube.php' ? 'Plugin' : 'Code Snippet';
		echo sprintf('<span data-nosnippet style="display:none">%s %s %s</span>', $codebase, self::SLUG, self::VERSION); 

		$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));}
		self::$total_runtime += $et-$st;


// Initialize

// Warn about incompatibilities (currently none)
	if (is_admin()) {
		if (!isset($GLOBALS['MA_GDPR_YouTube_Incompatibilities'])) {$GLOBALS['MA_GDPR_YouTube_Incompatibilities'] = [];}
		$codebase = basename(__FILE__) == 'ma-gdpr-youtube.php' ? 'Plugin' : 'Code Snippet';
		// PHP allow_url_fopen = Off
		if (!ini_get('allow_url_fopen')) 
			{$GLOBALS['MA_GDPR_YouTube_Incompatibilities'][] = 'PHP setting <code>allow_url_fopen</code> needs to be <b><On</b> for the '.$codebase.' to work correctly.';}
		if (count($GLOBALS['MA_GDPR_YouTube_Incompatibilities'])) {
			if (WP_DEBUG && MA_GDPR_YouTube::$timing) 
				{error_log('MA_GDPR_YouTube / Incompatibilities: '.print_r($GLOBALS['MA_GDPR_YouTube_Incompatibilities'],true));}
			add_action('admin_notices', function() use ($codebase){
				if (WP_DEBUG ) {error_log('MA_GDPR_YouTube/ Incompatibilities: '.print_r($GLOBALS['MA_GDPR_YouTube_Incompatibilities'],true));}
				echo '<div class="notice notice-warning is-dismissible">
						<p>The '.$codebase.'  "'.MA_GDPR_YouTube::TITLE.'" is skipped: '.implode(' or ',$GLOBALS['MA_GDPR_YouTube_Incompatibilities']).'</p>
}, 1000); 

	if (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 (is_favicon()) 			return; 	// don't run for favicon request
	if ((($_SERVER['QUERY_STRING']??null)	== 'service-worker'))			return;	// don't run for service-worker
	if ((($_SERVER['REQUEST_URI']??null)	== '/favicon.ico'))				return;	// don't run for favicon
	if ((strpos(($_SERVER['REQUEST_URI']??''),'/apple-touch-icon') === 0))	return;	// don't run for apple touch icons
	if ((strpos(($_SERVER['REQUEST_URI']??''),'/wp-content/') === 0))		return;	// don't run for dynamic wp-content file
	if (wp_is_json_request()) { // we need to handle some JSON requests to render shortcode in Oxygen Builder
		// check for Oxygen actions that might involve shortcodes
		if (!in_array(($_REQUEST['action']??null), ['ct_render_shortcode','ct_exec_code','oxy_render_easy_posts'])) {
			if (WP_DEBUG && MA_GDPR_YouTube::$timing) 
				{error_log(sprintf('%s skipping for JSON request: %s', MA_GDPR_YouTube::TITLE, json_encode($_REQUEST)));}
	if (is_admin()) {
		global $pagenow;
		if (	($pagenow != 'post-new.php') 
		&& 	(	($pagenow != 'post.php') || ($pagenow == 'post.php' && (($_REQUEST['action']??null) != 'edit'))	) 
		) return; // only load on specific requests where Gutenberg is involved
	$codebase = basename(__FILE__) == 'ma-gdpr-youtube.php' ? 'Plugin' : 'Code Snippet';
	if (WP_DEBUG && MA_GDPR_YouTube::$timing) 
		{error_log(sprintf('%s Initializing %s for %srequest URI="%s" action="%s"', MA_GDPR_YouTube::TITLE, $codebase, wp_is_json_request()?'JSON ':'',$_SERVER['REQUEST_URI']??'', $_REQUEST['action']??''));}


}, 1200); 

First published: Jun 15, 2021 on Code Snippet: GDPR compliant YouTube Videos
magnifier   Code Snippet ma-gdpr-youtube 1.5.0