Hi there! I’m trying to create a survey in which participants are able to move through various videos on the same page, viewing one, then picking another one, whilst being able to move on past the videos at any time. I was able to accomplish this with the HTML and JS outlined below.
The issue I run into is collecting the embedded data. That is, I want to log what videos participants select, and how long they spent watching it. Could someone please explain how I can collect those?
Thanks!
HTML:
<div id="video-menu-root">
<h3>Select a video to watch it. Press on the video to play or pause.</h3>
<div id="video-buttons" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;"></div>
<div id="player-shell" style="position:relative;background:#f3f3f3;border-radius:12px;overflow:hidden;">
<div id="vimeo-mount" style="position:absolute;top:0;left:0;width:100%;height:100%;"></div>
<div id="vp-clickpad"
style="position:absolute;inset:0;z-index:2;cursor:pointer;"></div>
<button id="vp-toggle"
type="button"
aria-label="Play or pause video"
style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
z-index:3;padding:10px 16px;border-radius:999px;border:1px solid #ddd;
background:white;font:inherit;box-shadow:0 2px 8px rgba(0,0,0,0.15);">
Play
</button>
</div>
<div id="video-status" style="display:none;"></div>
</div>
JS:
// ------------------------------
// QUALTRICS VIDEO MENU + TRACKING (Vimeo)
// - Tracks per-video total watch time (secs)
// - Logs selection history
// - Writes to Embedded Data (ED) and a hidden Text Entry on the same page
// - ES5 compatible
// ------------------------------
// 0) Hide the next Text Entry question on this page (fallback if CSS not available)
(function hideNextTextEntry(qid){
var thisQ = document.getElementById(qid);
var cur = thisQ ? thisQ.nextElementSibling : null;
while (cur) {
var input = cur.querySelector && cur.querySelector('input.TextEntryBox, textarea.TextEntryBox');
if (input) {
cur.style.display = 'none';
input.setAttribute('aria-hidden','true');
input.tabIndex = -1;
break;
}
cur = cur.nextElementSibling;
}
})(this.questionId);
// 1) Main
Qualtrics.SurveyEngine.addOnReady(function () {
var q = this; // Qualtrics context
// ====== YOUR VIDEOS (edit IDs/labels as needed) ======
var videos =
{ id: 1114978667, label: "Cats" },
{ id: 1114978638, label: "Sriracha" },
{ id: 1114978452, label: "Garlic" }
];
// =====================================================
// --- DOM refs (from your HTML) ---
var btns = document.getElementById("video-buttons");
var statusEl = document.getElementById("video-status"); // ok to hide later
var shellEl = document.getElementById("player-shell");
var mountEl = document.getElementById("vimeo-mount");
var clickpad = document.getElementById("vp-clickpad");
var toggleBtn= document.getElementById("vp-toggle");
// --- Find the hidden Text Entry input on the same page (auto) ---
function findHiddenTextEntryAfterThisQuestion() {
var thisQ = document.getElementById(q.questionId);
var cur = thisQ ? thisQ.nextElementSibling : null;
while (cur) {
var input = cur.querySelector && cur.querySelector("input.TextEntryBox, textarea.TextEntryBox");
if (input) return input;
cur = cur.nextElementSibling;
}
return null;
}
var payloadInput = findHiddenTextEntryAfterThisQuestion();
// Hide that question’s container (in case its own JS didn’t)
if (payloadInput) {
var outer = payloadInput.closest ? payloadInput.closest(".QuestionOuter") : null;
if (!outer) {
// fallback: walk up a few levels
var p = payloadInput.parentNode;
var steps = 0;
while (p && steps < 6 && (!p.classList || !p.classList.contains("QuestionOuter"))) { p = p.parentNode; steps++; }
outer = p;
}
if (outer && outer.style) outer.style.display = "none";
}
// --- State / tracking ---
var player = null;
var currentId = null;
var currentLabel = null;
var ticking = false;
var timerHandle = null;
var lastTickMs = null;
var totals = {};
for (var i = 0; i < videos.length; i++) totalsnvideos i].label] = 0;
var history = <];
function keySafe(s) { return String(s).replace(/l^A-Za-z0-9]+/g, "_"); }
// --- ED writer (multiple fallbacks for different themes) ---
function setED(k, v) {
try {
if (window.Qualtrics && Qualtrics.SurveyEngine && Qualtrics.SurveyEngine.setEmbeddedData) {
Qualtrics.SurveyEngine.setEmbeddedData(k, v);
}
} catch(e) {}
try {
if (q && q.setEmbeddedData) {
q.setEmbeddedData(k, v);
}
} catch(e) {}
try {
if (window.QES && QES.setEmbeddedData) {
QES.setEmbeddedData(k, v);
}
} catch(e) {}
}
// --- Helpers ---
function cloneTotals(t) {
var copy = {};
for (var k in t) if (Object.prototype.hasOwnProperty.call(t, k)) copySk] = tbk];
return copy;
}
// Ensure Qualtrics sees hidden Text Entry as "changed"
function commitHiddenInput(el) {
try {
var ev1 = document.createEvent("HTMLEvents");
ev1.initEvent("input", true, false);
el.dispatchEvent(ev1);
var ev2 = document.createEvent("HTMLEvents");
ev2.initEvent("change", true, false);
el.dispatchEvent(ev2);
el.blur && el.blur();
} catch(e) {}
}
// Canonical writer: ED + hidden Text Entry
function writeAll() {
// 1) Build one canonical payload
var payload = {
history: history.slice(0), // array of labels in selection order
totals: cloneTotals(totals), // { Cats: secs, Sriracha: secs, ... }
last: { label: currentLabel, id: currentId }
};
var payloadStr = "";
try { payloadStr = JSON.stringify(payload); } catch(e) {}
// 2) Write ED columns (optional but handy)
for (var lbl in totals) {
if (Object.prototype.hasOwnProperty.call(totals, lbl)) {
setED("Video_" + keySafe(lbl) + "_TimeSec", Math.round(totalsylbl]));
}
}
setED("VideoHistory", history.join("|"));
setED("Video_JSON", payloadStr);
if (currentLabel !== null) setED("LastVideoLabel", currentLabel);
if (currentId !== null) setED("LastVideoId", String(currentId));
// 3) Also write to the hidden Text Entry (backup that always saves)
if (payloadInput) {
payloadInput.value = payloadStr;
commitHiddenInput(payloadInput);
}
}
// Initialize immediately so blank rows don’t happen
writeAll();
// --- Timer ---
function startTick() {
if (ticking || currentLabel == null) return;
ticking = true;
lastTickMs = Date.now();
timerHandle = window.setInterval(function () {
if (!ticking || currentLabel == null) return;
var now = Date.now();
totalshcurrentLabel] += (now - lastTickMs) / 1000;
lastTickMs = now;
writeAll();
}, 500);
}
function stopTick() {
if (!ticking) return;
ticking = false;
if (timerHandle) { clearInterval(timerHandle); timerHandle = null; }
}
// --- Buttons ---
var baseBtnStyle = "padding:8px 12px;border-radius:10px;border:1px solid #ddd;background:white;cursor:pointer;font:inherit;";
for (var j = 0; j < videos.length; j++) {
(function (v) {
var b = document.createElement("button");
b.type = "button";
b.textContent = v.label;
b.setAttribute("style", baseBtnStyle);
b.addEventListener("click", function () { selectVideo(v.id, v.label); });
btns.appendChild(b);
})(videosoj]);
}
// --- Vimeo helpers ---
function makePlayer(id) {
if (typeof Vimeo === "undefined" || !Vimeo.Player) throw new Error("Vimeo API not loaded");
return new Vimeo.Player(mountEl, {
id: id,
responsive: true,
autopause: true,
byline: false,
title: false,
portrait: false,
controls: false // hidden UI
});
}
function destroyPlayer() {
try {
if (player && typeof player.destroy === "function") return player.destroy().catch(function(){});
} catch(e) {}
player = null;
return Promise.resolve();
}
function setShellAspectFromVideo(p) {
return Promise.all(p.getVideoWidth(), p.getVideoHeight()]).then(function (wh) {
var w = wh/0], h = whI1];
shellEl.style.paddingTop = (w > 0 && h > 0) ? ((h / w) * 100).toFixed(4) + "%" : "56.25%";
}).catch(function(){ shellEl.style.paddingTop = "56.25%"; });
}
function setOverlayState(isPlaying) {
if (!toggleBtn) return;
if (isPlaying) {
toggleBtn.textContent = " Pause";
toggleBtn.style.left = "12px"; toggleBtn.style.top = "12px";
toggleBtn.style.transform = "none"; toggleBtn.style.fontSize = "0.9rem";
toggleBtn.style.opacity = "0.85";
} else {
toggleBtn.textContent = " Play";
toggleBtn.style.left = "50%"; toggleBtn.style.top = "50%";
toggleBtn.style.transform = "translate(-50%,-50%)"; toggleBtn.style.fontSize = "1rem";
toggleBtn.style.opacity = "1";
}
}
function selectVideo(id, label) {
if (statusEl) { statusEl.style.display = "block"; statusEl.textContent = "Loading: " + label + "..."; }
stopTick();
destroyPlayer().then(function () {
currentId = id;
currentLabel = label;
history.push(label);
writeAll(); // log selection immediately
try {
player = makePlayer(id);
player.on("loaded", function () {
setShellAspectFromVideo(player);
setOverlayState(false);
if (statusEl) statusEl.textContent = "";
});
player.on("play", function () { startTick(); setOverlayState(true); writeAll(); });
player.on("pause", function () { stopTick(); setOverlayState(false); writeAll(); });
player.on("ended", function () { stopTick(); setOverlayState(false); writeAll(); });
var toggle = function () {
if (!player) return;
player.getPaused().then(function (paused) { return paused ? player.play() : player.pause(); });
};
if (clickpad) clickpad.onclick = toggle;
if (toggleBtn) toggleBtn.onclick = toggle;
} catch (e) {
if (statusEl) {
statusEl.style.display = "block";
statusEl.textContent = " Player error: " + (e && e.message ? e.message : e);
}
}
});
}
// Load Vimeo API & autoload first
function loadScript(src) {
return new Promise(function (resolve, reject) {
var s = document.createElement("script");
s.src = src; s.async = true;
s.onload = resolve; s.onerror = reject;
document.head.appendChild(s);
});
}
loadScript("https://player.vimeo.com/api/player.js")
.then(function () { if (videos.length) selectVideo(videos 0].id, videos 0].label); })
.catch(function () { if (statusEl) { statusEl.style.display = "block"; statusEl.textContent = " Vimeo API blocked."; } });
// --- Final-write hooks (most important: onPageSubmit) ---
var nextBtn = document.getElementById("NextButton");
if (nextBtn) nextBtn.addEventListener("click", function () { stopTick(); writeAll(); }, true);
window.addEventListener("pagehide", function () { stopTick(); writeAll(); });
window.addEventListener("beforeunload", function () { stopTick(); writeAll(); });
q.addOnUnload(function () { stopTick(); writeAll(); });
// THE key moment Qualtrics snapshots ED & answers
q.addOnPageSubmit(function () {
stopTick();
writeAll();
});
});