assets/scripts.js: Track time and moments properly

WIP.

- Fixes a family of bugs such as pausing the video during a choice
  resulting in the video resuming at the default choice (while still
  paused) after a while.

- Unifies tracking of moments and cleans up redundant code.

- Fixes unnecessary stutter when transitioning between two segments
  that are adjacent in the video file.
This commit is contained in:
Vladimir Panteleev 2019-07-27 23:57:15 +00:00
parent 2c987cd81e
commit 4195108b0d

View file

@ -1,13 +1,15 @@
// Data
var segmentMap = SegmentMap; var segmentMap = SegmentMap;
var bv = bandersnatch.videos['80988062'].interactiveVideoMoments.value; var bv = bandersnatch.videos['80988062'].interactiveVideoMoments.value;
var choicePoints = bv.choicePointNavigatorMetadata.choicePointsMetadata.choicePoints; var choicePoints = bv.choicePointNavigatorMetadata.choicePointsMetadata.choicePoints;
var momentsBySegment = bv.momentsBySegment; var momentsBySegment = bv.momentsBySegment;
var segmentGroups = bv.segmentGroups; var segmentGroups = bv.segmentGroups;
// Global mutable state
var captions = {}; var captions = {};
var currentSegment; var currentSegment;
var currentMoment;
var nextSegment = null; var nextSegment = null;
var momentSelected = null; var currentMoments = [];
var persistentState = bv.stateHistory; var persistentState = bv.stateHistory;
var globalChoices = {}; var globalChoices = {};
@ -82,6 +84,8 @@ function findSegment(id) {
return id; return id;
} }
/// Returns the segment ID at the given timestamp.
/// There will be exactly one segment for any timestamp within the video file.
function getSegmentId(ms) { function getSegmentId(ms) {
for (const [k, v] of Object.entries(segmentMap.segments)) { for (const [k, v] of Object.entries(segmentMap.segments)) {
if (ms >= v.startTimeMs && ms < v.endTimeMs) { if (ms >= v.startTimeMs && ms < v.endTimeMs) {
@ -95,16 +99,16 @@ function getSegmentMs(segmentId) {
return segmentMap.segments[segmentId].startTimeMs; return segmentMap.segments[segmentId].startTimeMs;
} }
function getMoment(ms) { function getMoments(segmentId, ms) {
for (const [k, v] of Object.entries(momentsBySegment)) { let result = {};
for (let r of v) let moments = momentsBySegment[segmentId] || [];
if (r.type == 'scene:cs_bs') { for (let i = 0; i < moments.length; i++) {
if (ms >= r.startMs && ms < r.endMs) { let m = moments[i];
return r; if (ms >= m.startMs && ms < m.endMs) {
result[segmentId + '-' + i] = m;
} }
} }
} return result;
return null;
} }
function newList(id) { function newList(id) {
@ -180,66 +184,75 @@ function addChoices(r) {
document.getElementById("choiceCaption").innerHTML = choicePoints[r.id].description; document.getElementById("choiceCaption").innerHTML = choicePoints[r.id].description;
} }
function momentStart(m) {
function updateProgressBar(ms, r) { console.log('momentStart', m);
var p = 0; if (m.type == 'scene:cs_bs') {
addZones(currentSegment);
if (r && ms > r.startMs && ms < r.endMs) { addChoices(m);
p = 100 - Math.floor((ms - r.startMs) * 100 / (r.endMs - r.startMs));
} }
applyImpression(m.impressionData);
}
function momentUpdate(m, ms) {
//console.log('momentUpdate', m);
if (m.type == 'scene:cs_bs') {
var p = 100 - Math.floor((ms - m.startMs) * 100 / (m.endMs - m.startMs));
document.getElementById("progress").style.width = p + '%'; document.getElementById("progress").style.width = p + '%';
}
}
function momentEnd(m) {
console.log('momentEnd', m);
if (m.type == 'scene:cs_bs') {
setNextSegment(null);
addZones(currentSegment);
addChoices(0);
document.getElementById("progress").style.width = 0;
}
} }
var timerId = 0; var timerId = 0;
var switchFrom = null;
var switchTo = null;
function ontimeout(nextSegment) {
console.log('ontimeout', nextSegment);
if (switchFrom != currentSegment || switchTo != nextSegment) {
playSegment(nextSegment);
}
switchFrom = currentSegment;
switchTo = nextSegment;
}
function ontimeupdate(evt) { function ontimeupdate(evt) {
var ms = getCurrentMs(); var ms = getCurrentMs();
var segmentId = getSegmentId(ms); var segmentId = getSegmentId(ms);
// ontimeupdate resolution is about a second, better use timer // ontimeupdate resolution is about a second. Augment it using timer.
if (timerId) {
clearTimeout(timerId); clearTimeout(timerId);
timerId = 0;
}
if (segmentId && nextSegment && nextSegment != segmentId) { if (segmentId && nextSegment && nextSegment != segmentId) {
var timeLeft = SegmentMap.segments[segmentId].endTimeMs - ms; var timeLeft = segmentMap.segments[segmentId].endTimeMs - ms;
timerId = setTimeout(ontimeout, timeLeft, nextSegment); timerId = setTimeout(ontimeupdate, timeLeft);
} }
if (currentSegment != segmentId) { if (currentSegment != segmentId) {
console.log('ontimeupdate', currentSegment, segmentId, ms, msToString(ms)); console.log('ontimeupdate', currentSegment, segmentId, ms, msToString(ms));
currentSegment = segmentId; // Distinguish between the user seeking manually with <video> controls,
addZones(segmentId); // from the video playing past the current segment end.
currentMoment = null; if (ms > segmentMap.segments[currentSegment].endTimeMs &&
addChoices(0); ms < segmentMap.segments[currentSegment].endTimeMs + 2000) {
// TODO: activate and apply user choice (whether or not it
// was default) instead of just playing the next segment.
playSegment(nextSegment, true);
} else {
playSegment(segmentId, true);
}
} }
var r = getMoment(ms); var moments = getMoments(segmentId, ms);
if (r && momentSelected != r.id) { for (let k in currentMoments)
updateProgressBar(ms, r); if (!(k in moments))
if (currentMoment != r.id) { momentEnd(currentMoments[k]);
currentMoment = r.id; for (let k in currentMoments)
console.log('interaction', currentMoment); if (k in moments)
addChoices(r); momentUpdate(currentMoments[k], ms);
} for (let k in moments)
} else { if (!(k in currentMoments))
currentMoment = null; momentStart(moments[k]);
addChoices(0); currentMoments = moments;
updateProgressBar(0);
}
} }
function jumpForward() { function jumpForward() {
@ -379,19 +392,13 @@ function seek(ms) {
function choice(choiceId, text, id) { function choice(choiceId, text, id) {
var segmentId = findSegment(choiceId); var segmentId = findSegment(choiceId);
console.log('choice', choiceId, 'nextSegment', segmentId); console.log('choice', choiceId, 'nextSegment', segmentId);
applyImpression(globalChoices[id]); applyImpression(globalChoices[id].impressionData);
setNextSegment(segmentId, text); setNextSegment(segmentId, text);
momentSelected = choiceId; momentSelected = choiceId;
addChoices(0); addChoices(0);
} }
function applyImpression(obj) { function applyImpression(impressionData) {
if (!obj) {
return;
}
var impressionData = obj.impressionData;
if (impressionData && impressionData.type == 'userState') { if (impressionData && impressionData.type == 'userState') {
for (const [variable, value] of Object.entries(impressionData.data.persistent)) { for (const [variable, value] of Object.entries(impressionData.data.persistent)) {
console.log('persistentState set', variable, '=', value); console.log('persistentState set', variable, '=', value);
@ -400,31 +407,16 @@ function applyImpression(obj) {
} }
} }
function applyPlaybackImpression(segmentId) { function playSegment(segmentId, noSeek) {
let moments = momentsBySegment[segmentId];
if (!moments) {
console.log('warning - no moments');
return;
}
for (let moment of moments) {
if (moment.type != 'notification:playbackImpression') {
continue;
}
applyImpression(moment);
}
}
function playSegment(segmentId) {
clearTimeout(timerId);
if (!segmentId || typeof segmentId === "undefined") if (!segmentId || typeof segmentId === "undefined")
segmentId = segmentMap.initialSegment; segmentId = segmentMap.initialSegment;
console.log('playSegment', segmentId); console.log('playSegment', currentSegment, '->', segmentId);
applyPlaybackImpression(segmentId);
location.hash = segmentId;
document.title = 'Bandersnatch - Chapter ' + segmentId; document.title = 'Bandersnatch - Chapter ' + segmentId;
var oldSegment = getSegmentId(getCurrentMs());
currentSegment = segmentId;
location.hash = segmentId;
if (!noSeek || oldSegment != segmentId) {
var ms = getSegmentMs(segmentId); var ms = getSegmentMs(segmentId);
seek(ms); seek(ms);
}
} }