(() => {
if (window.__qlearnPresenterBootstrapInstalled) return;
window.__qlearnPresenterBootstrapInstalled = true;
function startPresenter(config = {}) {
if (window.__qlearnPresenter) {
window.__qlearnPresenter.exit(false);
}
const presenterMode = config.mode || "single";
const sessionId = config.sessionId || "standalone";
const detectedPage = window.__hudraPageDetector?.resolve();
if (!detectedPage?.supported) {
window.__hudraPageDetector?.showNotice(
"Hudra couldn’t start because this page does not contain a supported lesson."
);
return;
}
const { contentRoot, pageTitleElement } = detectedPage;
let pageTitle = pageTitleElement?.textContent?.trim() || "";
function isHidden(element) {
if (!element || element.hidden) return true;
const computedStyle = window.getComputedStyle(element);
return (
computedStyle.display === "none" ||
computedStyle.visibility === "hidden"
);
}
const visibleSlideHeadings = [
...contentRoot.querySelectorAll("h2, h3")
].filter((heading) => {
const hasText = heading.textContent.trim().length > 0;
const hasMedia = Boolean(
heading.querySelector("img, video, iframe, svg, canvas, picture")
);
return (hasText || hasMedia) && !isHidden(heading);
});
const slideDefinitions = [];
if (visibleSlideHeadings.length) {
visibleSlideHeadings.forEach((heading, index) => {
const nextHeading = visibleSlideHeadings[index + 1] || null;
const range = document.createRange();
range.setStartBefore(heading);
if (nextHeading) {
range.setEndBefore(nextHeading);
} else {
range.setEnd(contentRoot, contentRoot.childNodes.length);
}
slideDefinitions.push({
index,
headingLevel: heading.tagName,
fragment: range.cloneContents()
});
if (typeof range.detach === "function") {
range.detach();
}
});
} else {
const range = document.createRange();
range.selectNodeContents(contentRoot);
slideDefinitions.push({
index: 0,
headingLevel: null,
fragment: range.cloneContents()
});
if (typeof range.detach === "function") {
range.detach();
}
}
const presentationLayer = document.createElement("div");
presentationLayer.id = "qlearn-presenter-layer";
document.body.appendChild(presentationLayer);
const pageHeader = document.createElement("div");
pageHeader.id = "qlearn-presenter-page-title";
pageHeader.textContent = pageTitle;
presentationLayer.appendChild(pageHeader);
const presenterCredit = document.createElement("div");
presenterCredit.id = "qlearn-presenter-credit";
presenterCredit.textContent = "Hudra";
presentationLayer.appendChild(presenterCredit);
const gameCredit = document.createElement("div");
gameCredit.id = "qlearn-presenter-game";
gameCredit.textContent = "The Game";
presentationLayer.appendChild(gameCredit);
const slidesContainer = document.createElement("div");
slidesContainer.id = "qlearn-presenter-slides";
presentationLayer.appendChild(slidesContainer);
const toast = document.createElement("div");
toast.id = "qlearn-presenter-toast";
presentationLayer.appendChild(toast);
const audienceHiddenOverlay = document.createElement("div");
audienceHiddenOverlay.id = "qlearn-presenter-audience-hidden";
audienceHiddenOverlay.innerHTML = `
Audience Hidden
`;
presentationLayer.appendChild(audienceHiddenOverlay);
let slideWrappers = slideDefinitions.map((slide, index) => {
const wrapper = document.createElement("div");
wrapper.className = "qlearn-presenter-slide";
wrapper.dataset.slideIndex = String(index);
if (slide.headingLevel) {
wrapper.dataset.headingLevel = slide.headingLevel.toLowerCase();
}
wrapper.appendChild(slide.fragment.cloneNode(true));
slidesContainer.appendChild(wrapper);
return wrapper;
});
slideWrappers.forEach((wrapper) => {
wrapper
.querySelectorAll(
"script, style, noscript, [hidden], [aria-hidden='true']"
)
.forEach((element) => element.remove());
wrapper.querySelectorAll("h1").forEach((element) => element.remove());
});
function isImageOnlySlide(wrapper) {
const images = [...wrapper.querySelectorAll("img")].filter((image) => {
const style = window.getComputedStyle(image);
return style.display !== "none" && style.visibility !== "hidden";
});
if (images.length !== 1) return false;
if (wrapper.querySelector("video, iframe, canvas, svg, table, pre, code, ul, ol")) {
return false;
}
const walker = document.createTreeWalker(
wrapper,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
return node.textContent.trim()
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
}
}
);
return !walker.nextNode();
}
slideWrappers.forEach((wrapper) => {
wrapper.classList.toggle("qlp-image-only", isImageOnlySlide(wrapper));
});
const style = document.createElement("style");
style.id = "qlearn-presenter-style";
style.textContent = `
body.qlearn-presenting {
overflow: hidden !important;
background: #111 !important;
}
body.qlearn-presenting > *:not(#qlearn-presenter-layer):not(#qlearn-presenter-controls) {
visibility: hidden !important;
}
#qlearn-presenter-layer {
visibility: visible !important;
position: fixed !important;
inset: 0 !important;
z-index: 2147483646 !important;
overflow: hidden !important;
background: white !important;
--qlp-slide-pad-top: 11vh;
--qlp-slide-pad-side: 7vw;
--qlp-slide-pad-bottom: 13vh;
--qlp-body-size: clamp(21px, 2.1vw, 33px);
--qlp-h2-size: clamp(44px, 5.8vw, 78px);
--qlp-h3-size: clamp(38px, 4.8vw, 66px);
--qlp-media-height: 58vh;
}
#qlearn-presenter-layer.qlp-aspect-16-10 {
--qlp-slide-pad-top: 10vh;
--qlp-slide-pad-side: 6.5vw;
--qlp-slide-pad-bottom: 11vh;
--qlp-body-size: clamp(20px, 1.95vw, 31px);
--qlp-h2-size: clamp(42px, 5.2vw, 72px);
--qlp-h3-size: clamp(36px, 4.3vw, 60px);
--qlp-media-height: 62vh;
}
#qlearn-presenter-layer,
#qlearn-presenter-layer * {
visibility: visible !important;
}
#qlearn-presenter-page-title {
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
z-index: 10 !important;
box-sizing: border-box !important;
min-height: 48px !important;
padding: 12px 7vw 10px !important;
overflow: hidden !important;
border-bottom: 1px solid rgba(0, 0, 0, 0.12) !important;
background: rgba(255, 255, 255, 0.97) !important;
color: #3a3a3a !important;
font-family: Arial, sans-serif !important;
font-size: clamp(16px, 1.4vw, 22px) !important;
font-weight: 600 !important;
line-height: 1.2 !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
#qlearn-presenter-credit {
position: absolute !important;
right: 18px !important;
bottom: 12px !important;
z-index: 20 !important;
color: rgba(0, 0, 0, 0.52) !important;
font-family: Arial, sans-serif !important;
font-size: 11px !important;
font-weight: 400 !important;
line-height: 1 !important;
pointer-events: none !important;
white-space: nowrap !important;
}
#qlearn-presenter-game {
position: absolute !important;
left: 18px !important;
bottom: 12px !important;
z-index: 95 !important;
color: rgba(255, 255, 255, 0.72) !important;
mix-blend-mode: difference !important;
font-family: Arial, sans-serif !important;
font-size: 11px !important;
font-weight: 400 !important;
line-height: 1 !important;
pointer-events: none !important;
white-space: nowrap !important;
opacity: 1 !important;
transition: opacity 700ms ease !important;
}
#qlearn-presenter-game.qlearn-presenter-game-hidden {
opacity: 0 !important;
}
#qlearn-presenter-slides {
position: absolute !important;
inset: 0 !important;
overflow: hidden !important;
}
.qlearn-presenter-slide {
display: block !important;
position: absolute !important;
inset: 0 !important;
z-index: 0 !important;
box-sizing: border-box !important;
overflow-x: hidden !important;
overflow-y: auto !important;
padding: var(--qlp-slide-pad-top) var(--qlp-slide-pad-side) var(--qlp-slide-pad-bottom) !important;
background: white !important;
color: #222 !important;
font-family: Arial, sans-serif !important;
font-size: var(--qlp-body-size) !important;
line-height: 1.4 !important;
opacity: 0 !important;
visibility: hidden !important;
transform: translateY(8px) !important;
pointer-events: none !important;
transition:
opacity 220ms ease,
transform 220ms ease,
visibility 0s linear 220ms !important;
}
.qlearn-presenter-slide.qlearn-slide-active {
z-index: 1 !important;
opacity: 1 !important;
visibility: visible !important;
transform: translateY(0) !important;
pointer-events: auto !important;
transition:
opacity 220ms ease,
transform 220ms ease,
visibility 0s linear 0s !important;
}
@media (prefers-reduced-motion: reduce) {
.qlearn-presenter-slide {
transform: none !important;
transition: none !important;
}
}
.qlearn-presenter-slide > div,
.qlearn-presenter-slide section,
.qlearn-presenter-slide article {
max-width: none !important;
}
.qlearn-presenter-slide h2 {
margin: 0 0 0.7em !important;
padding: 0 !important;
color: #222 !important;
font-family: Arial, sans-serif !important;
font-size: var(--qlp-h2-size) !important;
font-weight: 700 !important;
line-height: 1.06 !important;
}
.qlearn-presenter-slide h3 {
margin: 0 0 0.7em !important;
padding: 0 !important;
color: #222 !important;
font-family: Arial, sans-serif !important;
font-size: var(--qlp-h3-size) !important;
font-weight: 700 !important;
line-height: 1.1 !important;
}
.qlearn-presenter-slide h4 {
margin: 1em 0 0.45em !important;
color: #222 !important;
font-family: Arial, sans-serif !important;
font-size: clamp(27px, 3vw, 42px) !important;
font-weight: 700 !important;
line-height: 1.15 !important;
}
.qlearn-presenter-slide h5,
.qlearn-presenter-slide h6 {
margin: 0.9em 0 0.4em !important;
color: #222 !important;
font-family: Arial, sans-serif !important;
font-size: 1.05em !important;
font-weight: 700 !important;
line-height: 1.2 !important;
}
.qlearn-presenter-slide h2:has(img),
.qlearn-presenter-slide h3:has(img) {
margin: 0 0 0.7em !important;
padding: 0 !important;
line-height: 1 !important;
}
.qlearn-presenter-slide h2 img,
.qlearn-presenter-slide h3 img {
display: block !important;
width: 100% !important;
max-width: 100% !important;
height: auto !important;
max-height: 28vh !important;
object-fit: contain !important;
}
.qlearn-presenter-slide p {
margin-top: 0 !important;
margin-bottom: 0.8em !important;
}
.qlearn-presenter-slide ul,
.qlearn-presenter-slide ol {
margin-top: 0.4em !important;
margin-bottom: 0.8em !important;
padding-left: 1.5em !important;
}
.qlearn-presenter-slide li {
margin-bottom: 0.3em !important;
}
.qlearn-presenter-slide a {
color: #0068a8 !important;
text-decoration: underline !important;
}
.qlearn-presenter-slide img {
max-width: 100% !important;
max-height: calc(var(--qlp-media-height) * var(--qlp-media-scale, 1)) !important;
width: auto !important;
height: auto !important;
object-fit: contain !important;
}
.qlearn-presenter-slide video {
display: block !important;
max-width: 100% !important;
max-height: calc(var(--qlp-media-height) * var(--qlp-media-scale, 1)) !important;
width: auto !important;
height: auto !important;
object-fit: contain !important;
}
.qlearn-presenter-slide iframe {
display: block !important;
width: min(90vw, 1200px) !important;
height: min(65vh, 720px) !important;
max-width: 100% !important;
border: 0 !important;
}
.qlearn-presenter-slide table {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
border-collapse: collapse !important;
table-layout: auto !important;
}
.qlearn-presenter-slide tbody,
.qlearn-presenter-slide tr,
.qlearn-presenter-slide td,
.qlearn-presenter-slide th {
height: auto !important;
max-width: 100% !important;
}
.qlearn-presenter-slide td,
.qlearn-presenter-slide th {
box-sizing: border-box !important;
padding: 0.35em !important;
vertical-align: top !important;
}
.qlearn-presenter-slide [style*="width"] {
max-width: 100% !important;
}
.qlearn-presenter-slide-content {
width: 100% !important;
box-sizing: border-box !important;
zoom: var(--qlp-content-scale, 1);
}
.qlearn-presenter-slide.qlp-tight {
padding-top: 8vh !important;
padding-bottom: 9vh !important;
}
.qlearn-presenter-slide.qlp-very-tight {
padding-top: 7vh !important;
padding-right: 5vw !important;
padding-bottom: 8vh !important;
padding-left: 5vw !important;
}
.qlearn-presenter-slide .qlp-continuation-title::after {
content: " · continued";
font-size: 0.42em;
font-weight: 500;
opacity: 0.62;
white-space: nowrap;
}
.qlearn-presenter-slide .qlp-compact-block {
font-size: 0.82em !important;
}
/* A slide containing exactly one image uses the full audience viewport. */
.qlearn-presenter-slide.qlp-image-only {
padding: 0 !important;
overflow: hidden !important;
background: #fff !important;
}
.qlearn-presenter-slide.qlp-image-only .qlearn-presenter-slide-content {
width: 100vw !important;
height: 100vh !important;
zoom: 1 !important;
}
.qlearn-presenter-slide.qlp-image-only img {
position: absolute !important;
inset: 0 !important;
display: block !important;
width: 100vw !important;
height: 100vh !important;
max-width: none !important;
max-height: none !important;
margin: auto !important;
object-fit: contain !important;
}
#qlearn-presenter-layer.qlp-showing-image-only #qlearn-presenter-page-title {
display: none !important;
}
#qlearn-presenter-audience-hidden {
position: absolute !important;
inset: 0 !important;
z-index: 80 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
background: #000 !important;
color: rgba(255, 255, 255, 0.92) !important;
font: 600 clamp(24px, 3vw, 44px)/1.2 Arial, sans-serif !important;
letter-spacing: 0.02em !important;
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
transition: opacity 180ms ease, visibility 0s linear 180ms !important;
}
#qlearn-presenter-audience-hidden.qlearn-presenter-audience-hidden-visible {
opacity: 1 !important;
visibility: visible !important;
transition: opacity 180ms ease, visibility 0s linear 0s !important;
}
#qlearn-presenter-toast {
position: absolute !important;
left: 50% !important;
bottom: 78px !important;
transform: translateX(-50%) translateY(10px) !important;
z-index: 30 !important;
padding: 9px 14px !important;
border-radius: 999px !important;
background: rgba(0, 0, 0, 0.78) !important;
color: white !important;
font: 14px Arial, sans-serif !important;
opacity: 0 !important;
transition: opacity 140ms ease, transform 140ms ease !important;
pointer-events: none !important;
white-space: nowrap !important;
}
#qlearn-presenter-toast.qlearn-presenter-toast-visible {
opacity: 1 !important;
transform: translateX(-50%) translateY(0) !important;
}
body.qlearn-presenter-audience #qlearn-presenter-controls {
display: none !important;
}
#qlearn-presenter-controls {
visibility: visible !important;
position: fixed !important;
left: 50% !important;
bottom: 20px !important;
transform: translateX(-50%) !important;
z-index: 2147483647 !important;
display: flex !important;
align-items: center !important;
gap: 12px !important;
padding: 10px 16px !important;
border-radius: 999px !important;
background: rgba(0, 0, 0, 0.8) !important;
color: white !important;
font: 16px Arial, sans-serif !important;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.3) !important;
}
#qlearn-presenter-controls,
#qlearn-presenter-controls * {
visibility: visible !important;
}
#qlearn-presenter-controls button {
border: 0 !important;
border-radius: 7px !important;
padding: 8px 13px !important;
background: white !important;
color: #222 !important;
cursor: pointer !important;
font: inherit !important;
}
#qlearn-presenter-controls button:hover:not(:disabled) {
background: #e8e8e8 !important;
}
#qlearn-presenter-controls button:disabled {
cursor: default !important;
opacity: 0.4 !important;
}
#qlearn-presenter-controls [data-role="counter"] {
min-width: 110px !important;
text-align: center !important;
white-space: nowrap !important;
}
`;
document.head.appendChild(style);
function applyAspectMode() {
const ratio = window.innerWidth / Math.max(window.innerHeight, 1);
// Only two supported layouts: 16:9 (1.778) and 16:10 (1.6).
const isSixteenTen = Math.abs(ratio - 1.6) < Math.abs(ratio - 16 / 9);
presentationLayer.classList.toggle("qlp-aspect-16-10", isSixteenTen);
presentationLayer.classList.toggle("qlp-aspect-16-9", !isSixteenTen);
presentationLayer.dataset.aspect = isSixteenTen ? "16:10" : "16:9";
}
function wrapSlideContent(wrapper) {
let content = wrapper.querySelector(":scope > .qlearn-presenter-slide-content");
if (content) return content;
content = document.createElement("div");
content.className = "qlearn-presenter-slide-content";
while (wrapper.firstChild) content.appendChild(wrapper.firstChild);
wrapper.appendChild(content);
return content;
}
function slideOverflows(wrapper) {
const content = wrapSlideContent(wrapper);
return (
content.getBoundingClientRect().bottom >
wrapper.getBoundingClientRect().bottom - 4 ||
wrapper.scrollHeight > wrapper.clientHeight + 4
);
}
function resetFit(wrapper) {
wrapper.classList.remove("qlp-tight", "qlp-very-tight");
wrapper.style.setProperty("--qlp-content-scale", "1");
wrapper.style.setProperty("--qlp-media-scale", "1");
}
function fitSlide(wrapper, allowSmall = true) {
resetFit(wrapper);
if (wrapper.classList.contains("qlp-image-only")) return true;
if (!slideOverflows(wrapper)) return true;
wrapper.style.setProperty("--qlp-media-scale", "0.86");
if (!slideOverflows(wrapper)) return true;
wrapper.classList.add("qlp-tight");
if (!slideOverflows(wrapper)) return true;
wrapper.classList.add("qlp-very-tight");
if (!slideOverflows(wrapper)) return true;
const scales = allowSmall
? [0.96, 0.92, 0.88, 0.84, 0.80]
: [0.96, 0.92, 0.88];
for (const scale of scales) {
wrapper.style.setProperty("--qlp-content-scale", String(scale));
if (!slideOverflows(wrapper)) return true;
}
return false;
}
function makeContinuationHeading(sourceHeading) {
const heading = sourceHeading.cloneNode(true);
heading.classList.add("qlp-continuation-title");
return heading;
}
function splitList(list) {
const items = [...list.children].filter((item) => item.tagName === "LI");
if (items.length < 2) return [list.cloneNode(true)];
const midpoint = Math.ceil(items.length / 2);
return [items.slice(0, midpoint), items.slice(midpoint)].map((group) => {
const copy = list.cloneNode(false);
group.forEach((item) => copy.appendChild(item.cloneNode(true)));
return copy;
});
}
function divideOversizedBlock(block) {
if (block.matches("ul, ol")) return splitList(block);
return [block.cloneNode(true)];
}
function createGeneratedSlide(template, heading, continued) {
const wrapper = document.createElement("div");
wrapper.className = "qlearn-presenter-slide";
wrapper.dataset.headingLevel = template.dataset.headingLevel || "h2";
const content = document.createElement("div");
content.className = "qlearn-presenter-slide-content";
if (heading) {
content.appendChild(continued ? makeContinuationHeading(heading) : heading.cloneNode(true));
}
wrapper.appendChild(content);
return wrapper;
}
function paginateWrapper(original) {
const originalContent = wrapSlideContent(original);
const blocks = [...originalContent.children];
if (!blocks.length || fitSlide(original, false)) return [original];
const first = blocks[0]?.matches("h2, h3") ? blocks.shift() : null;
const generated = [];
let currentWrapper = createGeneratedSlide(original, first, false);
slidesContainer.appendChild(currentWrapper);
generated.push(currentWrapper);
function currentContent() {
return currentWrapper.querySelector(".qlearn-presenter-slide-content");
}
function newContinuation() {
currentWrapper = createGeneratedSlide(original, first, true);
slidesContainer.appendChild(currentWrapper);
generated.push(currentWrapper);
}
for (const sourceBlock of blocks) {
const pieces = divideOversizedBlock(sourceBlock);
for (const piece of pieces) {
currentContent().appendChild(piece);
resetFit(currentWrapper);
if (!slideOverflows(currentWrapper)) continue;
piece.remove();
const hasBody = [...currentContent().children].some((el) => !el.matches("h2, h3"));
if (hasBody) newContinuation();
currentContent().appendChild(piece);
if (!fitSlide(currentWrapper, false)) {
if (piece.matches("table, pre, iframe, video, img, div")) {
piece.classList.add("qlp-compact-block");
}
fitSlide(currentWrapper, true);
}
}
}
original.remove();
return generated;
}
function paginateAndFitSlides() {
applyAspectMode();
const originals = [...slideWrappers];
const rebuilt = [];
originals.forEach((wrapper) => rebuilt.push(...paginateWrapper(wrapper)));
slideWrappers = rebuilt;
slideWrappers.forEach((wrapper, index) => {
wrapper.dataset.slideIndex = String(index);
fitSlide(wrapper, true);
});
}
let resizeTimer = null;
function refitAfterResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
applyAspectMode();
slideWrappers.forEach((wrapper) => fitSlide(wrapper, true));
if (slideWrappers[current]) slideWrappers[current].scrollTop = 0;
}, 160);
}
applyAspectMode();
paginateAndFitSlides();
window.addEventListener("resize", refitAfterResize);
const controls = document.createElement("div");
controls.id = "qlearn-presenter-controls";
controls.innerHTML = `
`;
document.body.appendChild(controls);
const previousButton = controls.querySelector('[data-action="prev"]');
const nextButton = controls.querySelector('[data-action="next"]');
const counter = controls.querySelector('[data-role="counter"]');
let current = 0;
let toastTimer = null;
let audienceHidden = false;
let audienceZoom = 100;
let audienceScrollY = 0;
function applyAudienceView() {
const slide = slideWrappers[current];
if (!slide) return;
slide.style.zoom = `${audienceZoom}%`;
slide.scrollTop = audienceScrollY;
requestAnimationFrame(() => { audienceScrollY = slide.scrollTop; });
}
function setAudienceZoom(value, announce = true) {
audienceZoom = Math.max(70, Math.min(150, Math.round(Number(value) / 5) * 5));
applyAudienceView();
if (announce) showToast(`Audience zoom ${audienceZoom}%`);
sendPresenterState();
}
function adjustAudienceZoom(delta) { setAudienceZoom(audienceZoom + Number(delta || 0)); }
function scrollAudience(delta) {
const slide = slideWrappers[current];
if (!slide) return;
slide.scrollBy({ top: Number(delta || 0), behavior: "smooth" });
setTimeout(() => { audienceScrollY = slide.scrollTop; sendPresenterState(); }, 180);
}
function resetAudienceView(announce = true) {
audienceZoom = 100;
audienceScrollY = 0;
applyAudienceView();
if (announce) showToast("Audience view reset");
sendPresenterState();
}
function setAudienceHidden(hidden, announce = true) {
audienceHidden = Boolean(hidden);
audienceHiddenOverlay.classList.toggle(
"qlearn-presenter-audience-hidden-visible",
audienceHidden
);
if (announce) {
showToast(audienceHidden ? "Audience hidden" : "Audience restored");
}
sendPresenterState();
}
function toggleAudienceHidden() {
if (videoModeActive) {
showToast("Pause the video before hiding the audience screen");
return;
}
setAudienceHidden(!audienceHidden);
}
function showToast(message) {
toast.textContent = message;
toast.classList.add("qlearn-presenter-toast-visible");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toast.classList.remove("qlearn-presenter-toast-visible");
}, 1200);
}
function isVisibleMedia(element) {
if (!element) return false;
const mediaStyle = window.getComputedStyle(element);
return mediaStyle.display !== "none" && mediaStyle.visibility !== "hidden";
}
function findPlayableVideo(slide) {
if (!slide) return null;
const directVideos = [...slide.querySelectorAll("video")];
const directVideo = directVideos.find(isVisibleMedia);
if (directVideo) {
return { video: directVideo, frame: null };
}
const videoFrames = [
...slide.querySelectorAll(
'iframe[data-media-type="video"], iframe[title*="video" i], iframe[src*="media_attachments_iframe"]'
)
].filter(isVisibleMedia);
for (const frame of videoFrames) {
try {
const frameDocument = frame.contentDocument || frame.contentWindow?.document;
const frameVideo = frameDocument?.querySelector("video");
if (frameVideo) return { video: frameVideo, frame };
} catch (error) {
// Cross-origin players cannot be controlled directly.
}
}
return null;
}
let activeVideo = null;
let activeVideoFrame = null;
let activeFullscreenDocument = null;
let videoModeActive = false;
let videoFullscreenActive = false;
let deliberatelyLeavingVideoFullscreen = false;
function getFullscreenElement(doc) {
return doc?.fullscreenElement || doc?.webkitFullscreenElement || null;
}
async function requestElementFullscreen(element) {
if (!element) return false;
try {
if (element.requestFullscreen) {
await element.requestFullscreen();
return true;
}
if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
return true;
}
} catch (error) {
console.warn("Hudra fullscreen request failed:", error);
}
return false;
}
async function exitDocumentFullscreen(doc) {
if (!doc || !getFullscreenElement(doc)) return;
try {
if (doc.exitFullscreen) {
await doc.exitFullscreen();
} else if (doc.webkitExitFullscreen) {
doc.webkitExitFullscreen();
}
} catch (error) {
console.warn("Hudra could not exit video fullscreen:", error);
}
}
function removeVideoListeners() {
if (activeVideo) {
activeVideo.removeEventListener("ended", onActiveVideoEnded);
}
if (activeFullscreenDocument && activeFullscreenDocument !== document) {
activeFullscreenDocument.removeEventListener("fullscreenchange", onVideoFullscreenChange);
activeFullscreenDocument.removeEventListener("webkitfullscreenchange", onVideoFullscreenChange);
}
document.removeEventListener("fullscreenchange", onVideoFullscreenChange);
document.removeEventListener("webkitfullscreenchange", onVideoFullscreenChange);
}
function clearVideoState() {
removeVideoListeners();
activeVideo = null;
activeVideoFrame = null;
activeFullscreenDocument = null;
videoModeActive = false;
videoFullscreenActive = false;
deliberatelyLeavingVideoFullscreen = false;
}
async function stopActiveVideo({ pause = true, toastMessage = "⏸ Video paused" } = {}) {
if (!videoModeActive) return false;
const video = activeVideo;
const fullscreenDoc = activeFullscreenDocument;
deliberatelyLeavingVideoFullscreen = true;
if (pause && video && !video.paused) video.pause();
await exitDocumentFullscreen(fullscreenDoc);
if (fullscreenDoc !== document) {
await exitDocumentFullscreen(document);
}
clearVideoState();
if (toastMessage) showToast(toastMessage);
return true;
}
async function onActiveVideoEnded() {
await stopActiveVideo({
pause: false,
toastMessage: "↩ Video finished · returning to slide"
});
}
function onVideoFullscreenChange() {
if (!videoModeActive || deliberatelyLeavingVideoFullscreen) return;
const ownerFullscreen = getFullscreenElement(activeFullscreenDocument);
const parentFullscreen = getFullscreenElement(document);
if (videoFullscreenActive && !ownerFullscreen && !parentFullscreen) {
if (activeVideo && !activeVideo.paused) activeVideo.pause();
clearVideoState();
showToast("⏸ Video paused · returned to slide");
}
}
async function startCurrentSlideVideo() {
const slide = slideWrappers[current];
const playable = findPlayableVideo(slide);
if (!playable) {
showToast("No controllable video on this slide");
return false;
}
const { video, frame } = playable;
activeVideo = video;
activeVideoFrame = frame;
activeFullscreenDocument = video.ownerDocument || document;
videoModeActive = true;
deliberatelyLeavingVideoFullscreen = false;
video.addEventListener("ended", onActiveVideoEnded);
document.addEventListener("fullscreenchange", onVideoFullscreenChange);
document.addEventListener("webkitfullscreenchange", onVideoFullscreenChange);
if (activeFullscreenDocument !== document) {
activeFullscreenDocument.addEventListener("fullscreenchange", onVideoFullscreenChange);
activeFullscreenDocument.addEventListener("webkitfullscreenchange", onVideoFullscreenChange);
}
try {
if (video.ended) video.currentTime = 0;
// Prefer the actual video. If Canvas blocks that, fullscreen its iframe.
videoFullscreenActive = await requestElementFullscreen(video);
if (!videoFullscreenActive && frame) {
activeFullscreenDocument = document;
videoFullscreenActive = await requestElementFullscreen(frame);
}
await video.play();
showToast(
videoFullscreenActive
? "▶ Video playing fullscreen"
: "▶ Video playing · fullscreen unavailable"
);
return true;
} catch (error) {
console.warn("Hudra could not control this video:", error);
await stopActiveVideo({ pause: true, toastMessage: null });
showToast("Video player could not be controlled");
return false;
}
}
async function toggleCurrentSlideVideo() {
if (videoModeActive) {
return stopActiveVideo({
pause: true,
toastMessage: "⏸ Video paused · returning to slide"
});
}
return startCurrentSlideVideo();
}
function makePreviewDocument(index) {
const slide = slideWrappers[index];
if (!slide) return "";
const clone = slide.cloneNode(true);
clone.classList.add("qlearn-slide-active");
clone.style.position = "relative";
clone.style.inset = "auto";
clone.style.width = "100%";
clone.style.height = "100%";
clone.style.opacity = "1";
clone.style.visibility = "visible";
clone.style.transform = "none";
clone.style.overflow = "hidden";
if (index === current) {
clone.style.zoom = `${audienceZoom}%`;
clone.dataset.hudraScrollY = String(audienceScrollY);
}
clone.querySelectorAll("video").forEach((video) => {
const replacement = document.createElement("div");
replacement.className = "hudra-preview-media";
replacement.textContent = "Video";
if (video.getAttribute("poster")) {
replacement.style.backgroundImage = `url('${video.getAttribute("poster").replace(/'/g, "%27")}')`;
}
video.replaceWith(replacement);
});
clone.querySelectorAll("iframe").forEach((frame) => {
const replacement = document.createElement("div");
replacement.className = "hudra-preview-media";
replacement.textContent = frame.title || "Embedded content";
frame.replaceWith(replacement);
});
return `${clone.outerHTML}