Skip to content

Commit 31344c2

Browse files
authored
Merge pull request #152 from posit-dev/fix-use-standard-yt-player
fix: use standard YT player
2 parents 953ec2a + efdef03 commit 31344c2

4 files changed

Lines changed: 4 additions & 265 deletions

File tree

great_docs/assets/great-docs.scss

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6270,55 +6270,6 @@ html[data-bs-theme="dark"] .quarto-video {
62706270
border-color: rgba(255, 255, 255, 0.15);
62716271
}
62726272

6273-
/* Placeholder that replaces a YouTube iframe until the user clicks */
6274-
.gd-video-placeholder {
6275-
position: absolute;
6276-
top: 0;
6277-
left: 0;
6278-
width: 100%;
6279-
height: 100%;
6280-
cursor: pointer;
6281-
background: #000;
6282-
6283-
&:focus-visible {
6284-
outline: 3px solid var(--bs-primary);
6285-
outline-offset: -3px;
6286-
}
6287-
}
6288-
6289-
/* Thumbnail stretches to fill the ratio container */
6290-
.gd-video-thumb {
6291-
display: block;
6292-
width: 100%;
6293-
height: 100%;
6294-
object-fit: cover;
6295-
}
6296-
6297-
/* Centered play button overlay */
6298-
.gd-video-play {
6299-
position: absolute;
6300-
top: 50%;
6301-
left: 50%;
6302-
transform: translate(-50%, -50%);
6303-
transition: opacity 0.15s ease;
6304-
opacity: 0.9;
6305-
line-height: 0;
6306-
6307-
.gd-video-placeholder:hover &,
6308-
.gd-video-placeholder:focus-visible & {
6309-
opacity: 1;
6310-
}
6311-
6312-
.gd-play-bg {
6313-
transition: fill 0.15s ease;
6314-
}
6315-
6316-
.gd-video-placeholder:hover & .gd-play-bg {
6317-
fill: #f00;
6318-
fill-opacity: 1;
6319-
}
6320-
}
6321-
63226273

63236274
/* ---- Keyboard Shortcuts Help Overlay ---- */
63246275

great_docs/assets/video-embed.js

Lines changed: 1 addition & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -2,110 +2,13 @@
22
* Video embedding enhancements for Great Docs
33
*
44
* Improves performance and UX for embedded videos:
5-
* - YouTube: lightweight thumbnail placeholder with click-to-play
6-
* (avoids loading heavy YouTube iframe until the user requests it)
5+
* - YouTube: uses the standard YouTube embedded player with native controls
76
* - Vimeo / other service iframes: IntersectionObserver lazy loading
87
* - Local <video> elements: sets preload="metadata" for faster page loads
98
*/
109
(function () {
1110
"use strict";
1211

13-
// YouTube play-button SVG (same style as YouTube's own overlay)
14-
var PLAY_SVG =
15-
'<svg viewBox="0 0 68 48" width="68" height="48" xmlns="http://www.w3.org/2000/svg">' +
16-
'<path class="gd-play-bg" d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55' +
17-
"C3.97 2.33 2.27 4.81 1.48 7.74.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19" +
18-
'C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24' +
19-
's-.06-10.95-1.48-16.26z" fill="#212121" fill-opacity="0.8"/>' +
20-
'<path d="M45 24 27 14v20z" fill="#fff"/>' +
21-
"</svg>";
22-
23-
/**
24-
* Extract a YouTube video ID from an embed URL.
25-
*/
26-
function getYouTubeId(src) {
27-
var m = src.match(
28-
/(?:youtube\.com|youtube-nocookie\.com)\/embed\/([^?&#/]+)/
29-
);
30-
return m ? m[1] : null;
31-
}
32-
33-
/**
34-
* Replace YouTube iframes with a static thumbnail + play button.
35-
* The real iframe is loaded only when the user clicks or presses Enter/Space.
36-
*/
37-
function enhanceYouTube() {
38-
var iframes = document.querySelectorAll(
39-
'.quarto-video iframe[src*="youtube.com/embed"],' +
40-
'.quarto-video iframe[src*="youtube-nocookie.com/embed"]'
41-
);
42-
43-
for (var i = 0; i < iframes.length; i++) {
44-
(function (iframe) {
45-
var videoId = getYouTubeId(iframe.src);
46-
if (!videoId) return;
47-
48-
var wrapper = iframe.closest(".quarto-video");
49-
if (!wrapper) return;
50-
51-
var originalSrc = iframe.src;
52-
var title =
53-
iframe.getAttribute("title") ||
54-
iframe.getAttribute("aria-label") ||
55-
"Video";
56-
57-
// Build placeholder
58-
var ph = document.createElement("div");
59-
ph.className = "gd-video-placeholder";
60-
ph.setAttribute("role", "button");
61-
ph.setAttribute("aria-label", "Play video: " + title);
62-
ph.setAttribute("tabindex", "0");
63-
64-
// Thumbnail image (try maxresdefault, fall back to hqdefault)
65-
var thumb = document.createElement("img");
66-
thumb.className = "gd-video-thumb";
67-
thumb.alt = title;
68-
thumb.loading = "lazy";
69-
thumb.src =
70-
"https://img.youtube.com/vi/" + videoId + "/maxresdefault.jpg";
71-
thumb.onerror = function () {
72-
this.src =
73-
"https://img.youtube.com/vi/" + videoId + "/hqdefault.jpg";
74-
this.onerror = null;
75-
};
76-
77-
// Play button overlay
78-
var play = document.createElement("div");
79-
play.className = "gd-video-play";
80-
play.innerHTML = PLAY_SVG;
81-
82-
ph.appendChild(thumb);
83-
ph.appendChild(play);
84-
85-
// Swap: hide iframe (and prevent it from loading), show placeholder
86-
iframe.removeAttribute("src");
87-
iframe.style.display = "none";
88-
wrapper.appendChild(ph);
89-
90-
function load() {
91-
ph.remove();
92-
iframe.style.display = "";
93-
// Append autoplay so the video starts immediately after click
94-
var sep = originalSrc.indexOf("?") === -1 ? "?" : "&";
95-
iframe.src = originalSrc + sep + "autoplay=1";
96-
}
97-
98-
ph.addEventListener("click", load);
99-
ph.addEventListener("keydown", function (e) {
100-
if (e.key === "Enter" || e.key === " ") {
101-
e.preventDefault();
102-
load();
103-
}
104-
});
105-
})(iframes[i]);
106-
}
107-
}
108-
10912
/**
11013
* Lazy-load non-YouTube iframes (Vimeo, Loom, etc.) via IntersectionObserver.
11114
* The src is deferred until the iframe scrolls near the viewport.
@@ -157,7 +60,6 @@
15760

15861
// --- Entry point ---
15962
function init() {
160-
enhanceYouTube();
16163
lazyLoadIframes();
16264
enhanceVideoElements();
16365
}

tests/test_great_docs.py

Lines changed: 1 addition & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -39421,23 +39421,6 @@ def test_video_embed_js_is_iife():
3942139421
assert content.rstrip().endswith("})();")
3942239422

3942339423

39424-
def test_video_embed_js_has_youtube_enhancement():
39425-
"""video-embed.js contains YouTube thumbnail placeholder logic."""
39426-
39427-
from great_docs.core import GreatDocs
39428-
39429-
with tempfile.TemporaryDirectory() as tmp_dir:
39430-
docs = GreatDocs(project_path=tmp_dir)
39431-
content = (docs.assets_path / "video-embed.js").read_text(encoding="utf-8")
39432-
39433-
assert "enhanceYouTube" in content
39434-
assert "getYouTubeId" in content
39435-
assert "gd-video-placeholder" in content
39436-
assert "gd-video-thumb" in content
39437-
assert "gd-video-play" in content
39438-
assert "img.youtube.com" in content
39439-
39440-
3944139424
def test_video_embed_js_has_lazy_loading():
3944239425
"""video-embed.js contains IntersectionObserver lazy-loading for non-YouTube iframes."""
3944339426

@@ -39466,85 +39449,8 @@ def test_video_embed_js_has_video_element_enhancement():
3946639449
assert '"preload"' in content
3946739450

3946839451

39469-
def test_video_embed_js_youtube_id_extraction():
39470-
"""video-embed.js can extract YouTube IDs from embed URLs."""
39471-
39472-
from great_docs.core import GreatDocs
39473-
39474-
with tempfile.TemporaryDirectory() as tmp_dir:
39475-
docs = GreatDocs(project_path=tmp_dir)
39476-
content = (docs.assets_path / "video-embed.js").read_text(encoding="utf-8")
39477-
39478-
# The regex should match both youtube.com and youtube-nocookie.com
39479-
assert "youtube.com" in content
39480-
assert "youtube-nocookie.com" in content
39481-
39482-
39483-
def test_video_embed_js_accessibility():
39484-
"""video-embed.js includes proper accessibility attributes."""
39485-
39486-
from great_docs.core import GreatDocs
39487-
39488-
with tempfile.TemporaryDirectory() as tmp_dir:
39489-
docs = GreatDocs(project_path=tmp_dir)
39490-
content = (docs.assets_path / "video-embed.js").read_text(encoding="utf-8")
39491-
39492-
# Placeholder should have role="button" for screen readers
39493-
assert '"button"' in content
39494-
39495-
# Should support keyboard activation
39496-
assert '"Enter"' in content
39497-
assert '" "' in content # Space key
39498-
39499-
# Should have aria-label
39500-
assert "aria-label" in content
39501-
39502-
# Should have tabindex for keyboard focus
39503-
assert "tabindex" in content
39504-
39505-
39506-
def test_video_embed_js_autoplay_on_click():
39507-
"""video-embed.js appends autoplay=1 when loading the player after click."""
39508-
39509-
from great_docs.core import GreatDocs
39510-
39511-
with tempfile.TemporaryDirectory() as tmp_dir:
39512-
docs = GreatDocs(project_path=tmp_dir)
39513-
content = (docs.assets_path / "video-embed.js").read_text(encoding="utf-8")
39514-
39515-
assert "autoplay=1" in content
39516-
39517-
39518-
def test_video_embed_js_thumbnail_fallback():
39519-
"""video-embed.js falls back from maxresdefault to hqdefault thumbnail."""
39520-
39521-
from great_docs.core import GreatDocs
39522-
39523-
with tempfile.TemporaryDirectory() as tmp_dir:
39524-
docs = GreatDocs(project_path=tmp_dir)
39525-
content = (docs.assets_path / "video-embed.js").read_text(encoding="utf-8")
39526-
39527-
assert "maxresdefault" in content
39528-
assert "hqdefault" in content
39529-
assert "onerror" in content
39530-
39531-
39532-
def test_video_embed_js_play_button_svg():
39533-
"""video-embed.js includes an inline SVG play button."""
39534-
39535-
from great_docs.core import GreatDocs
39536-
39537-
with tempfile.TemporaryDirectory() as tmp_dir:
39538-
docs = GreatDocs(project_path=tmp_dir)
39539-
content = (docs.assets_path / "video-embed.js").read_text(encoding="utf-8")
39540-
39541-
assert "PLAY_SVG" in content
39542-
assert "<svg" in content
39543-
assert "gd-play-bg" in content
39544-
39545-
3954639452
def test_scss_has_video_styles():
39547-
"""great-docs.scss contains styling for video containers and placeholders."""
39453+
"""great-docs.scss contains styling for video containers."""
3954839454

3954939455
from great_docs.core import GreatDocs
3955039456

@@ -39555,12 +39461,6 @@ def test_scss_has_video_styles():
3955539461
# Video container border
3955639462
assert ".quarto-video" in content
3955739463

39558-
# Placeholder styles
39559-
assert ".gd-video-placeholder" in content
39560-
assert ".gd-video-thumb" in content
39561-
assert ".gd-video-play" in content
39562-
assert ".gd-play-bg" in content
39563-
3956439464

3956539465
def test_scss_video_container_has_border():
3956639466
"""The .quarto-video container has a border and border-radius."""
@@ -39593,20 +39493,6 @@ def test_scss_video_dark_mode():
3959339493
)
3959439494

3959539495

39596-
def test_scss_video_placeholder_positioning():
39597-
"""The placeholder overlay is positioned absolutely to fill the container."""
39598-
39599-
from great_docs.core import GreatDocs
39600-
39601-
with tempfile.TemporaryDirectory() as tmp_dir:
39602-
docs = GreatDocs(project_path=tmp_dir)
39603-
content = (docs.assets_path / "great-docs.scss").read_text(encoding="utf-8")
39604-
39605-
# The placeholder must cover the entire ratio container
39606-
assert "position: absolute" in content
39607-
assert "cursor: pointer" in content
39608-
39609-
3961039496
def test_scss_video_focus_visible():
3961139497
"""The placeholder has :focus-visible styling for keyboard accessibility."""
3961239498

user_guide/22-videos.qmd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ Here's a live example:
2929
{{< video https://www.youtube.com/watch?v=d8-Jyh-NaMw >}}
3030

3131
::: {.callout-tip}
32-
## Performance
32+
## YouTube Controls
3333

34-
YouTube embeds are automatically optimized. Instead of loading the full YouTube player on page load, Great Docs displays a lightweight thumbnail image with a play button. The player loads only when a visitor clicks, which significantly improves page load times (especially on pages with multiple videos).
34+
YouTube embeds use the standard YouTube player, giving visitors full access to native controls including ad skipping, captions, playback speed, and quality settings.
3535
:::
3636

3737
### Start Time

0 commit comments

Comments
 (0)