
Introduction
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?
Solution
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 from 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 youtube-nocookie.com 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.
Shortcode
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:

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.
Thumbnail
The shortcode automatically loads the thumbnail for this video from YouTube.
Formats
Different image formats are retrieved:
| Format Name | Aspect Ratio | Size |
|---|---|---|
| mqdefault | 16:9 | 320 x 180 px |
| hqdefault | 4:3 | 480 x 360 px |
| sddefault | 4:3 | 640 x 480 px |
| hq720 | 16:9 | 1280 x 720 px |
| maxresdefault | 16:9 | Original 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.
Notes:
- 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 itself which image size to load.
Cache
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 the video parameter.
In addition, the shortcode allows some more parameters, which are explained further.
video
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.
Samples:

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

[ma-gdpr-youtube video="https://www.youtube.com/watch?v=vPhg6sc1Mk4"]

[ma-gdpr-youtube video="https://youtu.be/vPhg6sc1Mk4"]
aspect-ratio
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.
Example:
[ma-gdpr-youtube video="vPhg6sc1Mk4" width="350px" aspect-ratio="1:1"]

gdpr-text
The code snippet defines a notice text about the privacy policy in different languages:
| Language | Text |
|---|---|
| DA | Når du har trykket, vil videoen blive indlæst fra YouTube’s servere. Se vores privatlivspolitik for flere informationer. |
| DE | Bei Klick wird dieses Video von den YouTube Servern geladen. Details siehe Datenschutzerklärung. |
| EN | When clicked, this video is loaded from YouTube servers. See our privacy policy for details. |
| ES | Al hacer clic, este vídeo se carga desde los servidores de YouTube. Consulte la política de privacidad para más detalles. |
| FI | Klikattuasi, tämä video ladataan Youtuben palvelimilta. Katso lisätietoja meidän tietosuojaselosteesta. |
| FR | En cliquant, cette vidéo est chargée depuis les serveurs de YouTube. Voir la 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 Adatkezelési Tájékoztatót oldalt. |
| IT | Quando 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.
Example:
[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."]

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 can be useful, for example, if several videos are displayed on a page and the note text should be displayed once centrally rather than for each individual video.
[ma-gdpr-youtube video="vPhg6sc1Mk4" gdpr-text=""]

gdpr-text-size
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.
Example:
[ma-gdpr-youtube video="vPhg6sc1Mk4" gdpr-text-size="20px"]

notice-style
The design of the privacy notice can be customized as needed.
White font on red bar? No problem!
Example:
[ma-gdpr-youtube video="vPhg6sc1Mk4" notice-style="background-color:red; color:white"]

notice-class
With an own CSS class this can be done even more targeted. For example, you can also control the design of the link:
Example:
<style>
body .my-notice-class {
background-color: red;
color:white;
}
body .my-notice-class a {
color: white;
font-weight: bold;
}
</style>
[ma-gdpr-youtube video="vPhg6sc1Mk4" notice-class="my-notice-class"]

width
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.
Example:
[ma-gdpr-youtube video="vPhg6sc1Mk4" width="300px"]

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"]

The original title of the video and other metadata can also be used in these parameters.
thumbnail
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.

[ma-gdpr-youtube video="vPhg6sc1Mk4" thumbnail="https://www.altmann.de/wp-content/uploads/2021/06/gdpr-youtube-beach.jpg"]

[ma-gdpr-youtube video="vPhg6sc1Mk4" thumbnail="1682"]
title-text
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"]

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.
The original title of the video and other metadata can also be used in this parameter.
title-style
The default style for the title is defined as:
.ma-gdpr-youtube-title {
position:absolute;
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.
Positioning

[ma-gdpr-youtube video="vPhg6sc1Mk4"title-text="Sea Noise" title-style="top:unset; bottom:30%;text-align:center"]
Design

[ma-gdpr-youtube video="vPhg6sc1Mk4" title-text="Sea Noise" title-style="top:1rem; font-size:2em; color:red; text-align:center;"]
title-class
To globally control the positioning and design of the titles, the parameter title-class allows to specify a global CSS class.
Beispiel:
<style>
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);
color:red;
}
</style>
[ma-gdpr-youtube video="vPhg6sc1Mk4" title-text="Sea Noise <span>with Class</span>" title-class="my-video-title-class"]

play-button
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"]
play-button-color
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.

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

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

[ma-gdpr-youtube video="vPhg6sc1Mk4" play-button="circle-o" play-button-color="black"]
play-button-class
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!
Beispiel:
<style>
body.postid-1245 .my-play-button {
color:green;
}
</style>
[ma-gdpr-youtube video="vPhg6sc1Mk4" play-button-class="my-play-button"]

start
With the parameter start you can start the video playback at a specific position in seconds..
This works fine with the embedded player as well as videos opened in a new window (see parameter new-window).

[ma-gdpr-youtube video="vPhg6sc1Mk4" start=20]

[ma-gdpr-youtube video="vPhg6sc1Mk4" start=20 new-window=1]
new-window
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]

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: https://developers.google.com/youtube/player_parameters
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.
Examples:
| Parameter | Meaning |
|---|---|
rel=0 | When 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 archiverel=0 Similar videos only from the current channel |
modestbranding=1 | By default, a YouTube logo is displayed at the bottom of the player. With modestbranding=1 this logo is hidden. |
fs=0 | By 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 Metadata
YouTube provides a whole range of additional metadata for each video.
Using special tags, this metadata can be transferred to the parameters alt, title, and title-text.
| Tag | Bedeutung |
|---|---|
@title@ | Original title of the video |
@description@ | Description for the video |
@genre@ | Genre of the video |
@author_name@ | Name of the author |
@author_url@ | URL of the author’s profile |
@duration_hms@ | Duration of the video in format hh:mm:ss |
Some metadata, such as title and description, may be provided by YouTube in different languages. In this case, they will automatically be displayed in the language currently used on the website.
Sample:
[ma-gdpr-youtube video="CwRvM2TfYbs" alt="@description@" title="@title@" title-text="@title@<br>@description@<br>@genre@<br>@author_name@"]
On the German version of this page you’ll see the German title of this video.

🌿 Музыка для расслабления, медитации, учебы, чтения, массажа, спа или сна. Эта музыка идеально подходит для борьбы с тревогой, стрессом или бессонницей, так...
Music
Lucid Dream
Notes on page builders
Oxygen Builder
When closing Oxygen Modals, the src attribute of IFrames is reset to stop the video.
Oxygen implements this with this.src = this.src.
This simultaneously removes all event handlers of this snippet.
When a modal is closed, the video is stopped, but the snippet loses all control over the state of the video.
From this point on, the snippet can no longer automatically stop videos when a new video is started.
FAQ – Frequently Asked Questions
Download
This JSON download can be imported directly into the plugins Code Snippets or Advanced Scripts. Don’t forget to activate the snippet after import.
If you are using a different snippet plugin, you can copy the source code instead and create a new snippet yourself.
ma-gdpr-youtube.code-snippets.json
Version 1.8.1, 2025-12-15
Donation ❤️
I enjoy developing code snippets and solving typical requirements with them.
The snippets are provided for free use.
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.
Disclaimer
I have developed and tested the code snippet to the best of my knowledge under the software versions listed in source code.
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.
Change Log
See “Version History” in Source Code
Source Code
<?php
/*
Plugin Name: MA GDPR YouTube
Description: GDPR compliant YouTube video embedding
Author: <a href="https://www.altmann.de/">Matthias Altmann</a>
Project: Code Snippet: GDPR Compliant YouTube Embed
Version: 1.8.1
Plugin URI: https://www.altmann.de/blog/code-snippet-gdpr-compliant-youtube-videos/
Description: en: https://www.altmann.de/blog/code-snippet-gdpr-compliant-youtube-videos/
de: https://www.altmann.de/blog/code-snippet-dsgvo-konforme-youtube-videos/
Copyright: © 2021-2025, Matthias Altmann
NOTES
Oxygen Builder
Modals, on close, reset src attribute of iframes to stop running videos.
That is done by this.src = this.src, which also kills event handlers we assigned.
If a modal is closed with a playing video, it's stopped, but we'll lose control
over it since we don't get any further events.
TESTED WITH:
Product Versions
--------------------------------------------------------------------------------------------------------------
PHP 7.4 ... 8.4
WordPress 6.4.2 ... 6.9
Bricks 1.9.5 ... 2.1.4
Oxygen 4.8.1 ... 4.9.3
--------------------------------------------------------------------------------------------------------------
VERSION HISTORY
Date Version Description
--------------------------------------------------------------------------------------------------------------
2025-12-15 1.8.1 Fixes:
- Corrected thumbnailUrl parse pattern
Changes:
- HTML Repsonse from YouTume is now always stored to allow analysis.
Therefore, cache folder is created directly after retrieval of response,
also if an error occurred during parsing.
- Description and keywords from meta itemprop, fallback meta name,
to prioritize video info over page info.
For keywords read from itemprop, adding spaces after commas.
2025-12-14 1.8.0 New Features:
- More details retrieved from YouTube:
- Canonical video URL
- Thumbnail URL
- Author details (name, profile URL)
- Duration (in seconds and h:m:s format)
- New tags for title, alt, title-text attributes:
- @author_name@ - Author name
- @author_url@ - Author profile URL
- @duration_hms@ - Duration in h:m:s format
- Microdata (<meta itemprop...>) added to video wrapper div for better SEO.
(Thanks to Hubertus Pohlmann for the suggestion)
Please note that the original props from YouTube are used, not the custom ones,
like customized title.
Changes:
- Skipping retrieval of video details if the video ID is shorter than 11 characters
to avoid creating invalid video folders and JSON files.
This is especially important for Bricks Builder, which executes shortcodes
while still typing in the editor.
- When downloading details from YouTube, the snippet version is stored in the JSON file.
This allows to check if the JSON (and thus video details) are retrieved and parsed with
an older version of the snippet, and should be retrieved again to assure all details
are available.
- Thumbnails are now retrieved using cURL instead of file_get_contents to avoid the
requirement for allow_url_fopen. (Thanks to Roland Laich for the suggestion)
- Replaced error suppression (@ operator) with try-catch
unpublished 1.7.5 New features:
- New shortcode parameter "priority" (0/1) to control the loading behavior of the video
thumbnail, e.g. if video is placed above the fold.
priority=0: loading="lazy" (default)
priority=1: loading="eager" fetchpriority="high"
(Thanks to Tobias Haas for the feature request)
2025-07-17 1.7.4 Fixes:
- Replace "\u0026" with "&" in YouTube video title.
(Thanks to Tobias Haas for reporting)
>> Video folder in /wp-content/uploads/ma-gdpr-youtube/ must be deleted manually
to re-trigger video metadata retrieval and storage.
2024-10-14 1.7.3 Changes:
- Retrieval of video details now uses cURL for better handling of YouTube responses.
(Huge thanks to Oscar Obianenue for reporting and supporting analysis and tests)
2024-10-11 1.7.2 Fixes:
- Fixed missing styling of video preview (play button, GDPR text) in Bricks builder.
2024-07-03 1.7.1 Changes:
- Added width and height attributes for thumbnail images
- Player observer now handles fullscreen correctly
(Thanks to Michael Herceg for reporting)
2024-04-28 1.7.0 Reorganization of code base.
New Features:
- For better accessibility, the Enter key can now be used to start and stop videos.
(Thanks to Stephan Koenigk for his feature request and pre-release tests)
- alt, title and title-text now support placeholders @title@ and @description@,
language specific, if available. The use of @ is deprecated.
Changes:
- For invalid IDs, don't create directory, store json, attempt to retrieve thumbnails
- In Builders Bricks and Oxygen, click handler is deactivated to allow selecting element
- Preparation for Bricks Element. Coming soon.
2023-12-30 1.6.0 New Features:
- Complete rebuild of JS
- Video player management via players registry and observer
- Pause current video if another one is started
- Pause video if modal/popup closed (evaluated by visibility of parent DOM element)
- Support for dynamically embedded videos using AJAX calls:
- Removed init prevention for AJAX calls
- Added PHP method MA_GDPR_YouTube::enable_footercode() to trigger output of footer
code (styles, scripts, svg) for video embeds dynamically loaded by AJAX calls
- Play click handler is now assigned as onclick event instead of collecting all
videos after page load. This eliminates the need for an extra click handler
initialization for players dynamically loaded after page load.
- Optimizations for accessibility
Fixes:
- Prefixed wrapper/player IDs with snippet slug to prevent IDs starting with number
- Fallback for aspect ratio via padding-top (CSS variable, @supports rule)
for older browsers not supporting aspect-ratio like Safari < V15
Changes:
- Changed debug URL switch from "debug" to "ma-gdpr-youtube-debug" to avoid conflicts
- Changed cache folder name from ma-gdpr-youtube-thumbnails to ma-gdpr-youtube incl. renaming
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, ...)
Fixes:
- 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)
Changes:
- 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.
(https://wpml.org/forums/topic/get_the_privacy_policy_link-should-be-translated/#post-8153387)
Changes:
- Migrated JavaScript from jQuery to vanilla JS (ES6) to eliminate jQuery dependency.
Fixes:
- 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
Fixes:
- 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"
Fixes:
- 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
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 {
public const TITLE = 'MA GDPR YouTube';
public const SLUG = 'ma-gdpr-youtube';
public const VERSION = '1.8.1';
// ===== CONFIGURATION ==============================================================================================
/** Default width of the video block. Can be specified in %, px */
public $default_width = '100%';
/** Default aspect ratio of the video block. Syntax X:X or X/X */
public $default_aspect_ratio = '16/9';
/** GDPR notice text in different languages. */
private $default_gdpr_text = [
'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 をご覧ください。', 'プライバシー ポリシー'],
];
/** Default font size for GDPR text */
public $default_gdpr_text_size = '.7em';
/** Default open video in new window */
private $default_new_window = false;
/** Enable timing info to WordPress debug.log if WP_DEBUG also enabled.
* - false/0: Disabled
* - true/1: Enabled
* - 2: Extended
*/
public $timing = false;
// ===== INTERNAL. DO NOT EDIT. =====================================================================================
private $incompatibilities = []; // incompatibilities detected before initialization
private $content_base = null; // will be set to the content base folder dir and url
private $footercode_needed = false; // will be set to true if shortcode used on current page
private $footercode_minimize = true; // should we minimize all footer code (style, script, svg)?
private $timing_total_runtime = 0;
private $yt_urlformat_image = 'https://img.youtube.com/vi%s/%s/%s.%s';
private $yt_urlformat_watch = 'https://www.youtube.com/watch?v=%s';
private $yt_image_sizes = [ // various resolutions, not all might be available!
// see https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
// 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, ...
];
// user agent string for cURL requests
private $curl_user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36';
//-------------------------------------------------------------------------------------------------------------------
/**
* 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
*/
function __construct() {
$st = microtime(true);
$GLOBALS[__CLASS__] = $this;
if (wp_doing_cron()) return; // don't run for CRON requests
if (WP_DEBUG && $this->timing>1) {error_log(sprintf('%s Initializing...',__CLASS__));}
if (!defined('MA_GDPR_YouTube_Version')) define('MA_GDPR_YouTube_Version',self::VERSION);
if ($this->is_incompatible()) return;
// check content directory
$this->content_base = $this->get_content_base();
if (!$this->content_base) return;
add_shortcode('ma-gdpr-youtube', [$this, 'shortcode']);
add_action('wp_footer',[$this,'footercode']);
add_action('init', [$this, 'init_builder'], 50);
// add a handler for logging total runtime
add_action('shutdown', [$this, 'total_runtime']);
$et = microtime(true);
if (WP_DEBUG && $this->timing>1) {error_log(sprintf(' %s Initialization Timing %.5f s.',__CLASS__, $et-$st));}
$this->timing_total_runtime += $et-$st;
}
//-------------------------------------------------------------------------------------------------------------------
/**
* Logs total timing for shortcodes on a page
*/
public function total_runtime(){
if (WP_DEBUG && $this->timing) {error_log(sprintf('%s Runtime: %.5f sec.', __CLASS__, $this->timing_total_runtime));}
}
//-------------------------------------------------------------------------------------------------------------------
/**
* Checks for incompatibilities. Registers admin notice.
* @return bool `true` if any incompatibilities found
*
* @since 1.8.0 removed check for allow_url_fopen, now using cURL
*/
private function is_incompatible(): bool{
$incomp = [];
if (!extension_loaded('curl')) {$incomp[] = 'PHP extension <code>curl</code> needs to be <b>enabled</b>';}
$this->incompatibilities = $incomp;
if (count($incomp) && is_admin()) {
add_action('admin_notices', function(){
if (WP_DEBUG ) {error_log(self::TITLE.' Incompatibilities: '.implode(', ',$this->incompatibilities));}
echo '<div class="notice notice-error is-dismissible">
<p>The '.$this->get_script_details()->combined.' is skipped: '.implode(', ',$this->incompatibilities).'.</p>
</div>';
});
}
return count($incomp) ? true : false;
}
//-------------------------------------------------------------------------------------------------------------------
/**
* Init actions for Builder support.
*/
public function init_builder() {
// OXYGEN
if ( ($_GET['ct_builder']??null) == true) {
// emit styles, script, svg when Oxygen Builder is active to support video preview
$this->footercode_needed = true;
}
// BRICKS
if (defined('BRICKS_VERSION')) {
if ( ($_GET['bricks']??null) == 'run') {
// emit styles, script, svg when Bricks Builder is active to support video preview
$this->footercode_needed = true;
}
// load Bricks element add-on (not yet available)
foreach([__DIR__,$this->content_base->dir] as $module_dir) {
$module_filepath = $module_dir.'/'.self::SLUG.'-bricks-element.php';
if (file_exists($module_filepath)) {
call_user_func('\Bricks\Elements::register_element', $module_filepath);
break;
}
}
}
}
//-------------------------------------------------------------------------------------------------------------------
/**
* Return base dir/url for video content.
* Create directory /wp-content/uploads/ma-gdpr-youtube/ if necessary
* Rename from old scheme ma-gdpr-youtube-thumbnails if exists
* @return object|null A dirinfo object (->dir, ->url)
*/
public function get_content_base(): ?object {
$retval = (object)['dir'=>null,'url'=>''];
$content_dir_info = wp_get_upload_dir();
$retval->dir = $content_dir_info['basedir'].'/'.self::SLUG;
$retval->url = $content_dir_info['baseurl'].'/'.self::SLUG;
// rename folder from old to new scheme
if (file_exists($retval->dir.'-thumbnails')) {
try {rename($retval->dir.'-thumbnails',$retval->dir);} catch (Exception $e) {/*ignore*/}
}
// create content folder if not exists
if (!file_exists($retval->dir)) {
try {
mkdir($retval->dir);
} catch (Exception $e) {
add_action('admin_notices', function(){
echo '<div class="notice notice-error"><p>['.self::TITLE.'] Error creating content base folder <code>wp-content/uploads/'.self::SLUG.'</code>.</p></div>';
});
error_log(sprintf('%s Error creating content base folder.', __CLASS__));
return null;
}
}
if (!is_writable($retval->dir)) {
add_action('admin_notices', function(){
echo '<div class="notice notice-error"><p>['.self::TITLE.'] Folder <code>wp-content/uploads/'.self::SLUG.'</code> is not writable. Please correct folder permissions.</p></div>';
});
}
// create scheme-less URL
$retval->url = preg_replace('/^https?\:/','',$retval->url);
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 function get_privacy_policy_link(string $text='privacy policy'): string {
$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 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
/x';
// 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 && $this->timing>1) {error_log(sprintf(' %s("%s") => %s Timing: %.5f sec.', __METHOD__, $s, json_encode($retval), $et-$st));}
return $retval;
}
//-------------------------------------------------------------------------------------------------------------------
/**
* Parse embedded YouTube page HTML for video info
* @param string $id The video ID
* @param string $lang The current language
* @param string &$html The HTML content of the embedded YouTube page
* @param object $info The info object to fill
*
* @since 1.8.0 Added retrieval of more video details:
* - Canonical video URL
* - Thumbnail URL
* - Author details (name, profile URL)
* - Duration (in seconds and h:m:s format)
*/
private function parse_embeded_info(string $id, string $lang, string &$html, object $info): void {
// try to find translated title in script the script
preg_match('/{"playerOverlayVideoDetailsRenderer":{"title":{"simpleText":"(.+?)"}/',$html,$matches) && $info->title->$lang = $matches[1];
// ... and replace "\u0026" by & in title
if ($info->title->$lang??null) $info->title->$lang = str_replace('\u0026','&',$info->title->$lang??'');
// if title not found in script 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 from meta tags
// description from meta itemprop
preg_match('/<meta itemprop="description" content="(.+?)">/',$html,$matches) && $info->description->$lang = $matches[1];
// fallback: description from meta name
if (!($info->description->$lang??null)) {
preg_match('/<meta name="description" content="(.+?)">/',$html,$matches) && $info->description->$lang = $matches[1];
}
// keywords ffrom meta itemprop
preg_match('/<meta itemprop="keywords" content="(.+?)">/',$html,$matches) && $info->keywords->$lang = $matches[1];
// keywords from meta itemprop don't have spaces after commas, so add them.
if ($info->keywords->$lang??null) {
$info->keywords->$lang = preg_replace('/,(?! )/u',', ',$info->keywords->$lang);
}
// fallback: keywords from meta name
if (!($info->keywords->$lang??null)) {
preg_match('/<meta name="keywords" content="(.+?)">/',$html,$matches) && $info->keywords->$lang = $matches[1];
}
// video URL default
$info->url = 'https://www.youtube.com/watch?v='.$id;
// find more specific video URL
preg_match('/
<div .*?
itemscope\s+itemid="([^"]+)" .*?
itemtype="http\:\/\/schema.org\/VideoObject"
/six',$html,$matches) && $info->url = $matches[1];
preg_match('/<link rel="shortlinkUrl" href="(.+?)">/',$html,$matches) && $info->shortlinkurl = $matches[1];
preg_match('/<link itemprop="thumbnailUrl" href="(.+?)">/',$html,$matches) && $info->thumbnailurl = $matches[1];
// author
if (preg_match('/
<span[^>]*itemprop="author"[^>]*> .*? # author span start
<link\s+itemprop="url"\s+href="([^"]+)"> .*? # capture profile URL
<link\s+itemprop="name"\s+content="([^"]+)"> # capture author name
/six',$html,$matches)) {
$info->author_name = $matches[2];
$info->author_url = $matches[1];
};
preg_match('/<meta itemprop="genre" content="(.+?)">/',$html,$matches) && $info->genre->$lang = $matches[1];
preg_match('/<meta itemprop="duration" content="(.+?)">/',$html,$matches) && $info->duration = $matches[1];
// parse duration into hh:mm:ss
if (isset($info->duration)) {
if (preg_match('/P?T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/',$info->duration,$matches)) {
$h = intval($matches[1]??'0');
$m = intval($matches[2]??'0');
$s = intval($matches[3]??'0');
// if minutes >= 60, convert to hours
if ($m>=60) {$h += floor($m/60); $m = $m % 60;}
$info->duration_hms = sprintf('%02d:%02d:%02d', $h, $m, $s);
}
}
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];
// look for background image (= response validity check)
$bgimage_found = preg_match('/background\-image: url\(\'https:\/\/i\.ytimg\.com\/vi\/'.$id.'\//',$html);
}
//-------------------------------------------------------------------------------------------------------------------
/**
* Retrieve video info from YouTube, for current language
* @param string $id The video id
* @param string $url The watch URL, containing the video id
* @param string $lang The current language
* @param object $info The info object to fill
* @return object|null The info object, containing the following fields:
* - `title` (object) translatable titles
* (object) {language} => title
* - `description` (object) translatable descriptions
* - `keywords` (object) translatable keywords
* - `url` (string) canonical video URL
* - `shortlinkurl` (string) shortlink URL
* - `thumbnailurl` (string) thumbnail URL
* - `author_name` (string) author name
* - `author_url` (string) author profile URL
* - `genre` (object) translatable genre
* (object) {language} => genre
* - `duration` (string) duration in ISO 8601 format
* - `duration_hms` (string) duration in hh:mm:ss format
* - `familyfriendly` (string) family friendly flag
* - `datepublished` (string) publication date
* - `dateuploaded` (string) upload date
* - `error` (string) possibly error message
*
* Note:
* Bricks Builder already executes shortcodes while still typing in the editor.
* Besides this causing multiple requests in a short time frame, it also causes
* requests with incomplete, and therefore invalid URLs or IDs.
* To avoid creating invalid video folders and JSON files,
* - the retrieval is skipped if the ID is shorter than 11 characters (minimum length of YouTube video IDs),
* - the cache folder is only created if valid info is retrieved.
*
* @since 1.7.3 retrieval using cURL instead of file_get_contents
*/
private function retrieve_video_info(string $id, string $url, string $lang, object $info): ?object {
// remove any previous error contained in info object
if ($info && is_object($info) && property_exists($info, 'error')) unset($info->error);
if (strlen($id)<11) {
// skip retrieval for invalid IDs
$info->error = sprintf('Invalid video ID %s: Expected length is 11 characters.', $id);
return $info;
}
$vid_dir = $this->content_base->dir.'/'.$id;
$info_filepath = $vid_dir.'/'.$id.'.json';
// retrieve from YouTube using current language
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Accept-Language: '.$lang,
'User-Agent: '.$this->curl_user_agent,
],
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLINFO_HEADER_OUT => true,
]);
$curl_result = curl_exec($ch);
$curl_info = curl_getinfo($ch);
$curl_error = curl_error($ch);
if (!$curl_result) {
// return error details
$info->error = sprintf('%s %s',$curl_info['http_code'], $curl_error);
return $info;
}
$curl_result = explode("\r\n\r\n",$curl_result,2);
$html = count($curl_result)==2 ? $curl_result[1] : '';
if ($html) {
// check/create cache folder
if (!file_exists($vid_dir)) {
try {
mkdir($vid_dir);
} catch (Exception $e) {
$info->error = sprintf('Error creating cache folder for video %s.', $id);
error_log(sprintf('[%s] %s %s', self::TITLE, $info->error, $e->getMessage()));
return $info;
}
}
// for debugging and analysis: save original HTML response in video folder
try {file_put_contents(sprintf('%1$s/%2$s/%2$s.html',$this->content_base->dir, $id),$html);} catch (Exception $e) {/*ignore*/}
// store snippet version used to retrieve and parse data
$info->version = self::VERSION;
// parse details from YouTube HTML page
// @since 1.8.0 moved to separate method
$this->parse_embeded_info($id, $lang, $html, $info);
// as a response validity check, look for background image, which is part of the video player div inline CSS
$bgimage_found = preg_match('/background\-image: url\(\'https:\/\/i\.ytimg\.com\/vi\/'.$id.'\//',$html);
if ( (!$info->dateuploaded??'') && (!$bgimage_found)) {
// invalid video details, because neither meta uploadDate nor CSS background-image $id found
$info->error = sprintf('Invalid details received from YouTube for video %s.', $id);
error_log(sprintf('[%s] %s', self::TITLE, $info->error, $id));
return $info;
}
// save retrieved info to JSON file
try {
file_put_contents($info_filepath, json_encode($info,JSON_PRETTY_PRINT));
} catch (Exception $e) {
$info->error = sprintf('Error saving video info for video %s.', $id);
error_log(sprintf('[%s] %s %s', self::TITLE, $info->error, $e->getMessage()));
return $info;
}
}
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 function get_video_info(string $id, string $lang): ?object {
$st = microtime(true);
$info = null;
$url = sprintf($this->yt_urlformat_watch,$id);
// create an empty info object. Some fields (marked with "lng") may be available in different languages
$info = (object)[
'version' => null, // snippet version used to retrieve details
'title' => (object)[], // lng
'description' => (object)[], // lng
'keywords' => (object)[], // lng
'genre' => (object)[], // lng
'url' => '',
'shortlinkurl' => '',
'thumbnailurl' => '',
'author_name' => '',
'author_url' => '',
'duration' => '',
'duration_hms' => '',
'familyfriendly'=> '',
'datepublished' => '',
'dateuploaded' => '',
'thumbsloaded' => false, // will be set by check_thumbnails
];
$cache_needs_refresh = true;
$info_filepath = $this->content_base->dir.'/'.$id.'/'.$id.'.json';
if (file_exists($info_filepath)) {
$cache_needs_refresh = false;
// read from cache
try {
$info_raw = file_get_contents($info_filepath);
$info_obj = json_decode($info_raw);
if (json_last_error()!==JSON_ERROR_NONE){
throw new Exception(sprintf('JSON decode error for video %s: %s', $id, json_last_error_msg()));
}
$info = $info_obj;
// check if info for current language available and version up-to-date
// version check
if (!version_compare($info->version??'',self::VERSION,'>=')) $cache_needs_refresh = true;
// current language already available?
if (!property_exists($info->title,$lang)) $cache_needs_refresh = true;
} catch (Exception $e) {
$info->error = $e->getMessage();
error_log(sprintf('[%s] %s. Will refresh.', self::TITLE, $info->error));
$cache_needs_refresh = true;
}
}
if ($cache_needs_refresh) {
// either file doesn't exist yet, or info or language missing, or retrieved with an earlier snippet version.
// Retrieve fresh details from YouTube.
$info = $this->retrieve_video_info($id, $url, $lang, $info);
}
$et = microtime(true);
if (WP_DEBUG && $this->timing>1) {error_log(sprintf(' %s("%s","%s") Timing: %.5f sec.', __METHOD__, $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
* @param object $info The video info object
* @return bool True if downloaded, false if not downloaded
*
* @since 1.8.0 retrieval using cURL instead of file_get_contents
*/
private function check_thumbnails(string $id, object $info): bool {
$retval = false;
if (!$this->content_base) return $retval;
$st = microtime(true);
$vid_dir = $this->content_base->dir.'/'.$id;
if ($info->thumbsloaded) {
// JSON says yes, but double check if all thumbnails exist
foreach ($this->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)) {
$info->thumbsloaded = false;
break 2;
}
}
}
}
if (!($info->thumbsloaded??false)) {
$vid_dir = $this->content_base->dir.'/'.$id;
// retrieve thumbnails
foreach ($this->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($this->yt_urlformat_image, $url_appendix, $id, $size_tag, $ext);
// retrieve thumbnail from YouTube and store to cache
try {
$ch = curl_init($img_url);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['User-Agent: '.$this->curl_user_agent],
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLINFO_HEADER_OUT => true,
]);
$curl_result = curl_exec($ch);
$curl_info = curl_getinfo($ch);
$curl_error = curl_error($ch);
//curl_close($ch); // has no effect since PHP 8, deprecated as of PHP 8.5
if ($curl_error) {throw new Exception(sprintf('%s %s',$curl_info['http_code'], $curl_error));}
// check for HTTP response code 200 (e.g. 404 is not regarded as error by cURL)
if ($curl_info['http_code']!=200) {throw new Exception(sprintf('HTTP %s received',$curl_info['http_code']));}
// extract body
$curl_result = explode("\r\n\r\n",$curl_result,2);
$img = count($curl_result)==2 ? $curl_result[1] : '';
if (!$img) {throw new Exception('No image data received from YouTube');}
else {file_put_contents($img_path, $img); }
} catch (Exception $e) {
$error = sprintf('Error retrieving thumbnail %s for video %s: %s', $size_tag, $id, $e->getMessage());
error_log(sprintf('[%s] %s', self::TITLE, $error));
}
}
}
}
$info->thumbsloaded = wp_date('Y-m-d H:i:s');
$info_filepath = $vid_dir.'/'.$id.'.json';
try {
file_put_contents($info_filepath, json_encode($info,JSON_PRETTY_PRINT));
$retval = true;
} catch (Exception $e) {
$info->error = sprintf('Error saving video info for video %s.', $id);
error_log(sprintf('[%s] %s %s', self::TITLE, $info->error, $e->getMessage()));
}
} else {
$retval = true;
}
$et = microtime(true);
if (WP_DEBUG && $this->timing>1) {error_log(sprintf(' %s("%s") Timing: %.5f sec.', __METHOD__, $id, $et-$st));}
return $retval;
}
//-------------------------------------------------------------------------------------------------------------------
/**
* Get the current page/post get_current_language
* @return string The language code as e.g. "de", "en"
*/
private 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 (or error message as text)
*/
public function shortcode(array $shortcode_atts = [], string $content = '') {
$st = microtime(true);
$id = null;
try { // try-catch block replaces goto statements from earlier versions.
$lang = $this->get_current_language();
$retval = '';
// get defaults for unspecified attributes
$atts_default = [
'id' => null, // deprecated
'video' => null,
'uniqid' => null,
'width' => $this->default_width,
'aspect-ratio' => $this->default_aspect_ratio,
'notice-class' => null,
'notice-style' => null,
'gdpr-text' => isset($this->default_gdpr_text[$lang])
? sprintf($this->default_gdpr_text[$lang][0],self::get_privacy_policy_link($this->default_gdpr_text[$lang][1]))
: sprintf($this->default_gdpr_text['en'][0],self::get_privacy_policy_link($this->default_gdpr_text['en'][1])),
'gdpr-text-size' => null,
'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' => $this->default_new_window,
'priority' => false, // @since 1.7.5: if true, the video will be loaded before the page content
];
// 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 = $this->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 https://developers.google.com/youtube/player_parameters?hl=de#Parameters for a list of parameters
// Note! rel=0 not supported anymore since September 2019 - without hacks which we won't support
// See https://www.amblemedia.com/disable-suggested-videos-on-youtube-embeds/
$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 == '' )) {throw new Exception('Missing video id.'); }
if (preg_match('/[^A-Za-z0-9\-\_]/',$atts->id)) {throw new Exception('Invalid video id.');}
// set $id for catch block
$id = $atts->id;
// generate an unique id (for the case a video is embedded multiple times)
$atts->uniqid = self::SLUG.'-'.$atts->id.'-'.uniqid();
if (!$this->content_base) {throw new Exception('Content directory is not available.'); }
// load youtube attributes
$video_info = $this->get_video_info($atts->id,$lang);
if (!$video_info) {throw new Exception('Error retrieving video info.');}
// @since 1.7.3 Check for errors during retrieval of video details from YouTube
if ($video_info->error??null) {throw new Exception(sprintf('Error retrieving video info: %s', $video_info->error));}
// @tags@
// legacy, deprecated
if ($atts->alt == '@') $atts->alt = esc_html($video_info->title->$lang??$video_info->title->en??'');
if ($atts->title == '@') $atts->title = esc_html($video_info->title->$lang??$video_info->title->en??'');
if ($atts->{'title-text'} == '@') $atts->{'title-text'} = esc_html($video_info->title->$lang??$video_info->title->en??'');
// @since 1.7.0 insert YouTube meta data
// supported tags: @title@, @description@, @genre@, @author_name@, @author_url@, @duration_hms@
// replace in alt, title, title-text
foreach (['alt','title','title-text'] as $attr) {
foreach (['title','description','genre','author_name','author_url','duration_hms'] as $meta) {
$atts->{$attr} = str_replace('@'.$meta.'@',
is_object($video_info->$meta??null) // localized attribute?
? ($video_info->$meta->$lang ?? $video_info->$meta->en ?? '') // current language, or english fallback
: ($video_info->$meta ?? ''), // non-localized attribute
$atts->{$attr}??'');
}
}
// check if we already have a thumbnail
if (!$this->check_thumbnails($atts->id, $video_info)) {throw new Exception('Error retrieving thumbnails.');}
$thumbnail = '';
$sources = [];
$click_handler = 'onclick="ma_gdpr_youtube.click(this)"';
// @since 1.7.5: if priority is set, the thumbnail will be loaded with higher priority
$load_atts = $atts->priority ? 'loading="eager" fetchpriority="high"' : 'loading="lazy"';
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);
foreach ($image_sizes as $key => $data) {
// to improve thumbnail quality, use higher res image if we reach half its size
$sources[] = sprintf('<source media="(min-width:%dpx)" type="%s" srcset="%s" width="%d" height="%d">',
$data['width']/2, $data['mime-type'], $data['url'], $data['width'], $data['height']);
}
// create thumbnail
$thumbnail .= sprintf('<picture class="ma-gdpr-youtube-thumbnail" '.$click_handler.'>%s '.
'<img src="%s" width="%d" height="%d" alt="%s" title="%s" %s>'.
'</picture>',
implode('',array_reverse($sources)), $largest->url, $largest->width, $largest->height, $atts->alt??'', $atts->title??'', $load_atts);
} else {
// thumbnail URL
// no width and height attributes because URL could be external, and retrieving img sizes would require extra request
$sources = [];
$thumbnail .= '<picture class="ma-gdpr-youtube-thumbnail" '.$click_handler.'>';
$thumbnail .= sprintf('<img src="%s" alt="%s" title="%s" %s>',$atts->thumbnail, $atts->alt, $atts->title, $load_atts);
$thumbnail .= '</picture>';
}
} else {
$source_list = [];
$sizes = [];
// get sources
foreach ($this->yt_image_sizes as $size_tag => $size) {
$source_list[] = [$size_tag,$size];
}
// get smallest thumbnail first
[$size_tag,$size] = array_shift($source_list);
$img_src = $this->content_base->url.'/'.$atts->id.'/'.$atts->id.'_'.$size_tag.'.jpg';
$img_path = $this->content_base->dir.'/'.$atts->id.'/'.$atts->id.'_'.$size_tag.'.jpg';
$img_info = getimagesize($img_path);
$img_width = $img_info[0];
$img_height = $img_info[1];
$tmp_thumbnail = sprintf('<picture class="ma-gdpr-youtube-thumbnail" %s> '.
'%s '.
'<img src="%s" alt="%s" title="%s" width="%d" height="%d" %s></picture>',
$click_handler, '%s' /* later */, $img_src, $atts->alt, $atts->title, $img_width, $img_height, $load_atts
);
// get larger thumbnails
while (count($source_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 = $this->content_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];
$img_height = $img_info[1];
// 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[] = sprintf('<source media="(min-width:%dpx)" type="image/%s" srcset="%s" width="%d" height="%d">',
$img_width/2, $mime,
$this->content_base->url.'/'.$atts->id.'/'.$atts->id.'_'.$size_tag.'.'.$ext,
$img_width, $img_height,
);
}
}
}
}
$thumbnail .= sprintf($tmp_thumbnail, implode('',array_reverse($sources)));
}
// calculate dimensions of video block depending on width and aspect ratio
[$arw,$arh] = explode('/',str_replace(':','/',$atts->{'aspect-ratio'}),2); // aspect ratio elements
[$width_value, $width_unit] = ['100','%']; // default width value and unit
// split width value and unit
preg_match('/^(\d+)(.+)$/',$atts->width,$matches);
if (count($matches) == 3) {array_shift($matches); [$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'}.'"' : '',
$atts->{'title-text'}
)
: '';
// 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.'"';}
// gdpr text size
$gdpr_text_size = '';
if ($atts->{'gdpr-text-size'}) {$gdpr_text_size = 'font-size:'.$atts->{'gdpr-text-size'}.';';}
// @since 1.8.0 generate microdata (<meta itemprop ...>) based on details from YouTube
$itemprops = [];
// reguired properties
if ($video_info->title->$lang??null) $itemprops[] = sprintf('<meta itemprop="name" content="%s"/>', esc_attr($video_info->title->$lang));
if ($video_info->thumbnailurl??null) $itemprops[] = sprintf('<meta itemprop="thumbnailUrl" content="%s"/>', esc_attr($video_info->thumbnailurl));
if ($video_info->dateuploaded??null) $itemprops[] = sprintf('<meta itemprop="uploadDate" content="%s"/>', esc_attr($video_info->dateuploaded));
// optional properties
if ($video_info->url??null) $itemprops[] = sprintf('<meta itemprop="contentUrl" content="%s"/>', esc_attr($video_info->url));
if ($video_info->description->$lang??null) $itemprops[] = sprintf('<meta itemprop="description" content="%s"/>', esc_attr($video_info->description->$lang));
if ($video_info->duration??null) $itemprops[] = sprintf('<meta itemprop="duration" content="%s"/>', esc_attr($video_info->duration));
// not part of VideoObject, but of CreativeWork
if ($video_info->genre->$lang??null) $itemprops[] = sprintf('<meta itemprop="genre" content="%s"/>', esc_attr($video_info->genre->$lang));
if ($video_info->author_name??null) $itemprops[] = sprintf('<div itemprop="author" itemscope itemtype="http://schema.org/Person"><meta itemprop="name" content="%1$s"/>%2$s</div>',
esc_attr($video_info->author_name),
($video_info->author_url??null) ? sprintf('<meta itemprop="url" content="%s"/>', esc_attr($video_info->author_url)) : '');
$itemprop_html = implode("\n", $itemprops);
// @since 1.5.0 player size based on aspect-ratio
// @since 1.6.0 width/height/aspect-ratio now set via CSS var for older browsers not supporting aspect-ratio, like e.g. Safari <V15
// @since 1.8.0 add microdata (<meta itemprop...>)
$retval = sprintf( '<div id="%7$s" data-video-id="%2$s" class="ma-gdpr-youtube-wrapper"
style="--_width:%3$s;--_height:%4$s;--_aspect-ratio:%16$s;"
data-new-window="%8$s" data-yt-parameters="%9$s"
itemscope itemtype="http://schema.org/VideoObject"
>'.
$itemprop_html.
$thumbnail.
'<svg class="ma-gdpr-youtube-button button-%13$s %14$s" %15$s tabindex="0" role="button" aria-label="play video" '.$click_handler.'><use xlink:href="#ma-gdpr-youtube-play-button-%13$s"></use></svg>'.
'%10$s'.
'<div class="ma-gdpr-youtube-notice %11$s" style="%6$s %12$s">%5$s</div>'.
'</div>',
/*1*/ $this->content_base->url,
/*2*/ $atts->id,
/*3*/ $block_width,
/*4*/ $block_height,
/*5*/ $atts->{'gdpr-text'},
/*6*/ $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'})
);
$this->footercode_needed = true;
} catch (Exception $e) {
$retval = sprintf('[%s] Error processing shortcode %s: %s',self::TITLE, $id ? 'for video '.$id : '', $e->getMessage());
}
$et = microtime(true);
if (WP_DEBUG && $this->timing>1) {error_log(sprintf(' %s(%s) Timing: %.5f sec.', __METHOD__, json_encode($shortcode_atts), $et-$st));}
$this->timing_total_runtime += $et-$st;
return $retval;
}
//-------------------------------------------------------------------------------------------------------------------
/**
* Enables emission of footercode (styles, scripts, svg) for the video embed.
* Can be used for pages where video is embedded dynamically by an AJAX call.
* On the parent page, call MA_GDPR_YouTube::enable_footercode()
* @since 1.6.0
*/
public static function enable_footercode() {
$GLOBALS[__CLASS__]->footercode_needed = true;
}
//-------------------------------------------------------------------------------------------------------------------
/**
* Emits the footer code (styles, script, svg) to handle the YouTube embedding
*/
public function footercode() {
if (!$this->footercode_needed) return;
$st = microtime(true);
/** @since 1.4.0 debugging info */
echo sprintf('<span id="%2$s-info" data-nosnippet style="display:none">%1$s %2$s %3$s</span>', $this->get_script_details()->type, self::SLUG, self::VERSION);
// emit style
// @since 1.6.0: @supports rule provides wrapper height for older browsers not supporting aspect-ratio, like e.g. Safari <V15
$style = <<<'END_OF_STYLE'
<style id="ma-gdpr-youtube-style">
.ma-gdpr-youtube-wrapper {position:relative; display:flex; isolation:isolate; width:var(--_width);aspect-ratio:var(--_aspect-ratio);}
@supports not (aspect-ratio:1/1) {.ma-gdpr-youtube-wrapper{height:var(--_height); padding-top:var(--_height);}}
.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;}
.ma-gdpr-youtube-title a {color:white; font: inherit;}
</style>
END_OF_STYLE;
if ($this->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" type="text/javascript">
"use strict";
/* Check for Builder preview panes */
if (
(typeof window.parent?.bricksData?.wpEditor != 'undefined') /* Bricks*/
|| (window.parent?.angular) /* Oxygen */
) {
/* Dummy Object w/o functionality for Bricks Builder */
window.ma_gdpr_youtube = {
init: function(){},
click: function($target){},
}
}
if ((typeof window.ma_gdpr_youtube == 'undefined')){
window.ma_gdpr_youtube = {
debug: (new URLSearchParams(window.location.search)).get('ma-gdpr-youtube-debug')!=null,
player_observer_timer: 1000,
player_observer_interval: null,
players: {}, /* list of active players */
fullscreen: null,
last_played: null,
init: function(){
this.debug && console.log('MA GDPR YouTube initialized.');
window.YT = null; /* prevent JS errors in Oxygen Builder */
},
get_player_state: function($input,$simple=false) {
/* helper to get player status. $input can be a state (integer), player apiID (string) or a real player object */
let $state = null;
if (typeof $input == 'number') {$state = $input;}
else if (typeof $input == 'string') {$input = this.players[$input].player;}
if (typeof $input == 'object') {
if ($input && typeof $input.getPlayerState !== 'undefined') {$state = $input.getPlayerState();}
}
/* if $simple is true, return 'PLAY' or 'STOP' */
if ($simple) {
if ((window.YT == null) || (window.YT.PlayerState == null)) return $state;
switch ($state) {
case YT.PlayerState.UNSTARTED: $state = 'STOP'; break;
case YT.PlayerState.ENDED: $state = 'STOP'; break;
case YT.PlayerState.PLAYING: $state = 'PLAY'; break;
case YT.PlayerState.PAUSED: $state = 'STOP'; break;
case YT.PlayerState.BUFFERING: $state = 'PLAY'; break;
case YT.PlayerState.CUED: $state = 'STOP'; break;
default: $state = 'STOP';
}
}
return $state;
},
player_observer_init: function(){
if (this.player_observer_interval == null) {
this.player_observer_interval = setInterval(this.player_observer, this.player_observer_timer);
this.debug && console.log('MA GDPR YouTube Player Observer initialized.');
/* observe fullscreen change */
document.addEventListener('fullscreenchange', function($event){
ma_gdpr_youtube.debug && console.log('MA GDPR YouTube Player Observer FullscreenChange',$event);
let $element_id = document.fullscreenElement?.id??null;
if ($element_id) { /* fullscreen started */
ma_gdpr_youtube.fullscreen = $element_id;
} else { /* fullscreen ended */
ma_gdpr_youtube.fullscreen = null;
}
ma_gdpr_youtube.debug && console.log('MA GDPR YouTube Player Observer has fullscreen '+ma_gdpr_youtube.fullscreen);
});
}
},
player_observer: function() {
/* pauses video if iframe is not visible anymore - e.g. closed modal - but also not fullscreen */
ma_gdpr_youtube.debug && console.log('MA GDPR YouTube Player Observer (entries: '+Object.values(ma_gdpr_youtube.players).length+')');
for (let $playerID in ma_gdpr_youtube.players) {
let $player = ma_gdpr_youtube.players[$playerID];
let $playerState = ma_gdpr_youtube.get_player_state($player.player);
let $playerStateSimple = ma_gdpr_youtube.get_player_state($playerState,true);
ma_gdpr_youtube.debug && console.log('MA GDPR YouTube Player Observer checking player '+$playerID+', state '+$playerState+' '+$playerStateSimple);
if ($playerStateSimple=='PLAY') {
let $iframe = document.getElementById($playerID);
if ($iframe) {
if ($iframe.id == ma_gdpr_youtube.fullscreen) {
ma_gdpr_youtube.debug && console.log('MA GDPR YouTube Player Observer iframe is fullscreen, skipping.');
} else {
ma_gdpr_youtube.debug && console.log('MA GDPR YouTube Player Observer checking iframe offsetParent '+$iframe.offsetParent);
ma_gdpr_youtube.debug && console.log('MA GDPR YouTube Player Observer player details ',$player);
if ($iframe.offsetParent === null) {
ma_gdpr_youtube.debug && console.log('MA GDPR YouTube Player Observer pauses hidden video '+$playerID);
$player.player.pauseVideo();
}
}
}
}
}
},
player_stop_all_except: function($apiID) {
/* loop through players and pause all except this one */
this.last_played = null;
for (let $playerID in ma_gdpr_youtube.players) {
ma_gdpr_youtube.debug && console.log('Checking existing player '+$playerID);
if ($playerID != $apiID) {
ma_gdpr_youtube.debug && console.log('Pausing existing player '+$playerID);
let $player = ma_gdpr_youtube.players[$playerID];
if (ma_gdpr_youtube.get_player_state($playerID,true)==='PLAY') {this.last_played = $playerID;}
$player.player.pauseVideo();
}
}
},
player_currently_playing: function() {
ma_gdpr_youtube.debug && console.log('player_currently_playing');
for (let $playerID in this.players) {
ma_gdpr_youtube.debug && console.log(' checking '+$playerID);
if (this.get_player_state($playerID,true)==='PLAY') {
ma_gdpr_youtube.debug && console.log(' -> currently playing '+$playerID);
return $playerID;
}
}
ma_gdpr_youtube.debug && console.log(' -> none playing');
return null;
},
click: function($target){
/* get closest wrapper */
let $wrapper = $target.closest('.ma-gdpr-youtube-wrapper');
if (!$wrapper) return;
if ($wrapper.getAttribute('data-new-window') == '1') {
/* get the video id from the parent div's id attribute */
let $videoID = $wrapper.getAttribute('data-video-id');
/* check if additional yt parameters have been specified */
let $yt_parameters = ma_gdpr_youtube.get_parameters_from_wrapper($wrapper);
let $ytp = $yt_parameters ? '&' + (new URLSearchParams($yt_parameters).toString()) : '';
window.open('https://www.youtube.com/watch?v='+$videoID+$ytp,'video-'+$videoID);
return;
}
/* initialize the player observer */
ma_gdpr_youtube.player_observer_init();
/* Instantiate a new YT player (replacing wrapper), and play it when ready */
/* check if YouTube API has already been loaded */
if ( document.querySelectorAll('#ma-gdpr-youtube-player-api').length==0 ) {
/* handler for YT API loaded */
window.onYouTubeIframeAPIReady = function() {
ma_gdpr_youtube.debug && console.log('YouTube API ready.');
ma_gdpr_youtube.video_start($wrapper);
};
/* load the YouTube API */
ma_gdpr_youtube.debug && console.log('Loading YouTube API...');
const $script = document.createElement('script');
$script.id = 'ma-gdpr-youtube-player-api';
$script.src = 'https://www.youtube.com/iframe_api';
document.body.appendChild($script);
} else {
/* YouTube API is already loaded */
ma_gdpr_youtube.video_start($wrapper);
}
},
get_parameters_from_wrapper: function($wrapper) {
let $retval = null;
let $yt_parameters = $wrapper.getAttribute('data-yt-parameters');
if ($yt_parameters) {
this.debug && console.log('yt-parameters RAW',$yt_parameters);
$yt_parameters = atob($yt_parameters);
this.debug && console.log('yt-parameters JSON',$yt_parameters);
$yt_parameters = JSON.parse($yt_parameters);
this.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;
},
video_start: function($wrapper) {
/* get the video id from the parent div's id attribute */
const $videoID = $wrapper.getAttribute('data-video-id');
const $apiID = $wrapper.getAttribute('id');
this.debug && console.log('Starting video '+$videoID+' from wrapper '+$apiID);
/* get the inner dimensions of the wrapper */
const $wrapperWidth = window.getComputedStyle($wrapper).width;
const $wrapperHeight = window.getComputedStyle($wrapper).height;
this.debug && console.log('Video WxH',[$wrapperWidth,$wrapperHeight]);
/* remove styles from wrapper */
$wrapper.style.height = $wrapperHeight;
$wrapper.style.padding = 'unset';
if (!YT) return; /* prevent JS errors in Oxygen Builder */
const $apiConfig = {
width: parseInt($wrapperWidth),
height: parseInt($wrapperHeight),
videoId: $videoID,
host: 'https://www.youtube-nocookie.com',
enablejsapi: 1,
playerapiid: $apiID,
rel: 0,
events: {
'onReady': function($event) {
ma_gdpr_youtube.debug && console.log('YouTube onReady.',{apiID:$apiID, event:$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 ($event.target && $event.target.getIframe() && ($event.target.getIframe().offsetParent === null)) {return;}
ma_gdpr_youtube.debug && console.log('Starting video.',{apiID:$apiID, event:$event});
let $player = ma_gdpr_youtube.players[$apiID];
$player && $player.player && (typeof $player.player.playVideo!=='undefined') && $player.player.playVideo();
},
'onStateChange': function($event) {
ma_gdpr_youtube.debug && console.log('YouTube onStateChange '+$apiID+', '+$event.data);
if ((YT == null) || (YT.PlayerState == null)) return;
if ($event.data == YT.PlayerState.UNSTARTED) {
ma_gdpr_youtube.debug && console.log('YouTube video unstarted '+$apiID);
}
if ($event.data == YT.PlayerState.ENDED) {
ma_gdpr_youtube.debug && console.log('YouTube video ended '+$apiID);
}
if ($event.data == YT.PlayerState.PLAYING) {
ma_gdpr_youtube.debug && console.log('YouTube video playing '+$apiID);
ma_gdpr_youtube.player_stop_all_except($apiID);
}
if ($event.data == YT.PlayerState.PAUSED) {
ma_gdpr_youtube.debug && console.log('YouTube video paused '+$apiID);
}
if ($event.data == YT.PlayerState.BUFFERING) {
ma_gdpr_youtube.debug && console.log('YouTube video buffering '+$apiID);
}
if ($event.data == YT.PlayerState.CUED) {
ma_gdpr_youtube.debug && console.log('YouTube video cued '+$apiID);
}
}
}
};
/* check if additional yt parameters have been specified */
let $yt_parameters = ma_gdpr_youtube.get_parameters_from_wrapper($wrapper);
if ($yt_parameters) {
ma_gdpr_youtube.debug && console.log('YouTube API custom parameters',{apiID:$apiID,parameters:$yt_parameters});
$apiConfig.playerVars = $yt_parameters;
}
ma_gdpr_youtube.debug && console.log('apiConfig',$apiConfig);
ma_gdpr_youtube.players[$apiID] = {player:new YT.Player($apiID, $apiConfig)};
},
};
ma_gdpr_youtube.init();
}
/* Accessibility: Handle Space or Enter as play click */
document.querySelectorAll('.ma-gdpr-youtube-button').forEach( ($elm) => {
$elm.addEventListener('keyup', function($event) {
if ($event.key==='Enter') {
$event.preventDefault();
$event.stopPropagation();
$event.target.parentNode.querySelector('.ma-gdpr-youtube-thumbnail').click();
}
});
});
/* Accessibility: Handle Space or Enter on BODY for playing or stopped video */
document.addEventListener('keyup', function($event) {
if (($event.key==='Enter') && ($event.target?.tagName==='BODY')) {
ma_gdpr_youtube.debug && console.log('Currently playing: '+ma_gdpr_youtube.player_currently_playing());
ma_gdpr_youtube.debug && console.log('Last played: '+ma_gdpr_youtube.last_played);
$event.preventDefault();
$event.stopPropagation();
if (ma_gdpr_youtube.player_currently_playing()) {
ma_gdpr_youtube.debug && console.log('Stopping all');
ma_gdpr_youtube.player_stop_all_except('');
} else {
ma_gdpr_youtube.debug && console.log('Start playing '+ma_gdpr_youtube.last_played);
let $player = ma_gdpr_youtube.players[ma_gdpr_youtube.last_played];
$player && $player.player && (typeof $player.player.playVideo!=='undefined') && $player.player.playVideo();
}
}
});
</script>
END_OF_SCRIPT;
if ($this->footercode_minimize) {
$script = preg_replace('/\/\*.*?\*\//s','',$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="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;">
<defs>
<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>
<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>
<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>
<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"/>
</symbol>
</defs>
</svg>
END_OF_SYMBOL;
if ($this->footercode_minimize) { $symbol = preg_replace('/\r?\n[\t ]*/','',$symbol); }
echo $symbol;
$et = microtime(true);
if (WP_DEBUG && $this->timing>1) {error_log(sprintf('%s() Timing: %.5f sec.', __METHOD__, $et-$st));}
$this->timing_total_runtime += $et-$st;
}
//===================================================================================================================
// UTILS
//===================================================================================================================
//-------------------------------------------------------------------------------------------------------------------
/**
* Returns the script details as type, version, combined.
* @return object The script type and version:
* - `type`: The type as 'Plugin' or 'Code Snippet'.
* - `version`: The version.
* - `combined`: The combination of type and version.
* - `full`: The combination of type, title and version
*/
public function get_script_details() {
$type = basename(__FILE__) == 'ma-gdpr-youtube.php' ? 'Plugin' : 'Code Snippet';
$retval =(object)[
'type' => $type,
'name' => self::TITLE,
'version' => self::VERSION,
'combined' => sprintf('%s "%s" %s', $type, self::TITLE, self::VERSION),
];
return $retval;;
}
}
//===================================================================================================================
// Initialize
new MA_GDPR_YouTube();
endif;