mirror of
https://github.com/mehotkhan/BandersnatchInteractive.git
synced 2025-07-28 01:33:23 +00:00
Merge pull request #21 from CyberShadow/pull-20190727-205645
Many fixes and improvements
This commit is contained in:
commit
a0984c1f46
2 changed files with 339 additions and 206 deletions
|
@ -1,15 +1,12 @@
|
||||||
|
// 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;
|
||||||
var captions = {};
|
|
||||||
var currentSegment;
|
// Persistent state
|
||||||
var currentMoment;
|
var ls = window.localStorage || {};
|
||||||
var nextSegment = null;
|
|
||||||
var momentSelected = null;
|
|
||||||
var persistentState = bv.stateHistory;
|
|
||||||
var globalChoices = {};
|
|
||||||
|
|
||||||
function msToString(ms) {
|
function msToString(ms) {
|
||||||
return new Date(ms).toUTCString().split(' ')[4];
|
return new Date(ms).toUTCString().split(' ')[4];
|
||||||
|
@ -19,76 +16,73 @@ function getCurrentMs() {
|
||||||
return Math.round(document.getElementById("video").currentTime * 1000.0);
|
return Math.round(document.getElementById("video").currentTime * 1000.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateJs(cond) {
|
function preconditionToJS(cond) {
|
||||||
if (cond[0] == 'persistentState') {
|
if (cond[0] == 'persistentState') {
|
||||||
return '!!persistentState["' + cond[1] + '"]';
|
return 'JSON.parse(ls["persistentState_' + cond[1] + '"])';
|
||||||
} else if (cond[0] == 'not') {
|
} else if (cond[0] == 'not') {
|
||||||
return '!(' + generateJs(cond[1]) + ')';
|
return '!(' + preconditionToJS(cond[1]) + ')';
|
||||||
} else if (cond[0] == 'and') {
|
} else if (cond[0] == 'and') {
|
||||||
var conds = [];
|
return '(' + cond.slice(1).map(preconditionToJS).join(' && ') + ')';
|
||||||
for (var i = 1; i < cond.length; i++) {
|
|
||||||
conds.push('(' + generateJs(cond[i]) + ')');
|
|
||||||
}
|
|
||||||
return '(' + conds.join(' && ') + ')';
|
|
||||||
} else if (cond[0] == 'or') {
|
} else if (cond[0] == 'or') {
|
||||||
var conds = [];
|
return '(' + cond.slice(1).map(preconditionToJS).join(' || ') + ')';
|
||||||
for (var i = 1; i < cond.length; i++) {
|
} else if (cond[0] == 'eql' && cond.length == 3) {
|
||||||
conds.push('(' + generateJs(cond[i]) + ')');
|
return '(' + cond.slice(1).map(preconditionToJS).join(' == ') + ')';
|
||||||
}
|
} else if (cond === false) {
|
||||||
return '(' + conds.join(' || ') + ')';
|
return false;
|
||||||
|
} else if (cond === true) {
|
||||||
|
return true;
|
||||||
|
} else if (typeof cond === 'string') {
|
||||||
|
return cond;
|
||||||
} else {
|
} else {
|
||||||
console.log('unsupported condition!', cond);
|
console.log('unsupported condition!', cond);
|
||||||
return 'true';
|
return 'true';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkPrecondition(segmentId) {
|
function evalPrecondition(precondition, text) {
|
||||||
let precondition = bv.preconditions[segmentId];
|
|
||||||
|
|
||||||
if (precondition) {
|
if (precondition) {
|
||||||
let cond = generateJs(precondition);
|
let cond = preconditionToJS(precondition);
|
||||||
let match = eval(cond);
|
let match = eval(cond);
|
||||||
|
console.log('precondition', text, ':', cond, '==', match);
|
||||||
console.log(cond, '==', match);
|
|
||||||
|
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findSegment(id) {
|
function checkPrecondition(preconditionId) {
|
||||||
if (id.startsWith('nsg-')) {
|
return evalPrecondition(bv.preconditions[preconditionId], preconditionId);
|
||||||
id = id.substr(4);
|
}
|
||||||
}
|
|
||||||
if (SegmentMap.segments[id]) {
|
|
||||||
// check precondition
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segmentGroups[id]) {
|
function resolveSegmentGroup(sg) {
|
||||||
for (v of segmentGroups[id]) {
|
let results = [];
|
||||||
if (v.segmentGroup) {
|
for (let v of segmentGroups[sg]) {
|
||||||
return findSegment(v.segmentGroup);
|
if (v.precondition) {
|
||||||
} else if (v.segment) {
|
if (!checkPrecondition(v.precondition))
|
||||||
// check precondition
|
continue;
|
||||||
return v.segment;
|
}
|
||||||
} else {
|
if (v.segmentGroup) {
|
||||||
if (checkPrecondition(v))
|
results.push(resolveSegmentGroup(v.segmentGroup));
|
||||||
return v;
|
} else if (v.segment) {
|
||||||
}
|
// TODO: does the included precondition override or
|
||||||
|
// complement the segment precondition?
|
||||||
|
if (!checkPrecondition(v.segment))
|
||||||
|
continue;
|
||||||
|
results.push(v.segment);
|
||||||
|
} else {
|
||||||
|
if (!checkPrecondition(v))
|
||||||
|
continue;
|
||||||
|
results.push(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return id;
|
console.log('segment group', sg, '=>', results);
|
||||||
}
|
return results[0];
|
||||||
|
|
||||||
function getChoiceMs(choiceId) {
|
|
||||||
var segmentId = findSegment(choiceId);
|
|
||||||
return getSegmentMs(segmentId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
||||||
return k;
|
return k;
|
||||||
}
|
}
|
||||||
|
@ -100,16 +94,17 @@ 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 (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;
|
let momentId = segmentId + '/' + i;
|
||||||
}
|
if (ms >= m.startMs && ms < m.endMs && evalPrecondition(m.precondition, 'moment ' + momentId)) {
|
||||||
}
|
result[momentId] = m;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function newList(id) {
|
function newList(id) {
|
||||||
|
@ -129,9 +124,13 @@ function addItem(ul, text, url) {
|
||||||
ul.appendChild(li);
|
ul.appendChild(li);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var nextChoice = -1;
|
||||||
|
var nextSegment = null;
|
||||||
|
|
||||||
function setNextSegment(segmentId, comment) {
|
function setNextSegment(segmentId, comment) {
|
||||||
console.log('setNextSegment', segmentId, comment);
|
console.log('setNextSegment', segmentId, comment);
|
||||||
nextSegment = segmentId;
|
nextSegment = segmentId;
|
||||||
|
nextChoice = -1;
|
||||||
var ul = newList("nextSegment");
|
var ul = newList("nextSegment");
|
||||||
var caption = 'nextSegment: ' + segmentId;
|
var caption = 'nextSegment: ' + segmentId;
|
||||||
addItem(ul, comment ? caption + ' (' + comment + ')' : caption,
|
addItem(ul, comment ? caption + ' (' + comment + ')' : caption,
|
||||||
|
@ -140,142 +139,267 @@ function setNextSegment(segmentId, comment) {
|
||||||
|
|
||||||
function addZones(segmentId) {
|
function addZones(segmentId) {
|
||||||
var ul = newList("interactionZones");
|
var ul = newList("interactionZones");
|
||||||
var caption = 'currentSegment(' + segmentId + ')';
|
let caption = 'currentSegment(' + segmentId + ')';
|
||||||
addItem(ul, caption, 'javascript:playSegment("' + segmentId + '")');
|
addItem(ul, caption, 'javascript:playSegment("' + segmentId + '")');
|
||||||
|
|
||||||
var v = segmentMap.segments[segmentId];
|
var v = segmentMap.segments[segmentId];
|
||||||
if (v && v.ui && v.ui.interactionZones) {
|
if (v && v.ui && v.ui.interactionZones) {
|
||||||
var index = 0;
|
var index = 0;
|
||||||
for (z of v.ui.interactionZones) {
|
for (var z of v.ui.interactionZones) {
|
||||||
var startMs = z[0];
|
var startMs = z[0];
|
||||||
var stopMs = z[1];
|
var stopMs = z[1];
|
||||||
var caption = segmentId + ' interactionZone ' + index;
|
let caption = segmentId + ' interactionZone ' + index;
|
||||||
addItem(ul, caption, 'javascript:seek(' + startMs + ')');
|
addItem(ul, caption, 'javascript:seek(' + startMs + ')');
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ul = newList("nextSegments");
|
ul = newList("nextSegments");
|
||||||
|
let defaultSegmentId = null;
|
||||||
for (const [k, v] of Object.entries(segmentMap.segments[segmentId].next)) {
|
for (const [k, v] of Object.entries(segmentMap.segments[segmentId].next)) {
|
||||||
var caption = captions[k] ? captions[k] : k;
|
let caption = k;
|
||||||
if (segmentMap.segments[segmentId].defaultNext == k) {
|
if (segmentMap.segments[segmentId].defaultNext == k) {
|
||||||
caption = '[' + caption + ']';
|
caption = '[' + caption + ']';
|
||||||
setNextSegment(k);
|
defaultSegmentId = k;
|
||||||
}
|
}
|
||||||
addItem(ul, caption, 'javascript:playSegment("' + k + '")');
|
addItem(ul, caption, 'javascript:playSegment("' + k + '")');
|
||||||
}
|
}
|
||||||
|
setNextSegment(defaultSegmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentChoiceMoment = null;
|
||||||
|
|
||||||
function addChoices(r) {
|
function addChoices(r) {
|
||||||
|
currentChoiceMoment = r;
|
||||||
|
nextChoice = -1;
|
||||||
var ul = newList("choices");
|
var ul = newList("choices");
|
||||||
document.getElementById("choiceCaption").innerHTML = '';
|
document.getElementById("choiceCaption").innerHTML = '';
|
||||||
if (!r) return;
|
if (!r) return;
|
||||||
index = 0;
|
|
||||||
|
|
||||||
for (x of r.choices) {
|
nextChoice = r.defaultChoiceIndex;
|
||||||
console.log(x.id, 'choice saved');
|
|
||||||
globalChoices[x.id] = x;
|
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
for (let x of r.choices) {
|
||||||
var caption = r.defaultChoiceIndex == index ? '[' + x.text + ']' : x.text;
|
var caption = r.defaultChoiceIndex == index ? '[' + x.text + ']' : x.text;
|
||||||
addItem(ul, caption, 'javascript:choice("' +
|
addItem(ul, caption, 'javascript:choice(' + index + ')');
|
||||||
(x.segmentId ? x.segmentId : (x.sg ? x.sg : x.id)) + '", "' + x.text + '", "' + x.id + '")');
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("choiceCaption").innerHTML = choicePoints[r.id].description;
|
if (r.id in choicePoints)
|
||||||
|
document.getElementById("choiceCaption").innerHTML = choicePoints[r.id].description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function momentStart(m, seeked) {
|
||||||
function updateProgressBar(ms, r) {
|
console.log('momentStart', m, seeked);
|
||||||
var p = 0;
|
if (m.choices) {
|
||||||
|
addChoices(m);
|
||||||
if (r && ms > r.startMs && ms < r.endMs) {
|
|
||||||
p = 100 - Math.floor((ms - r.startMs) * 100 / (r.endMs - r.startMs));
|
|
||||||
}
|
}
|
||||||
|
if (!seeked)
|
||||||
|
applyImpression(m.impressionData);
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("progress").style.width = p + '%';
|
function momentUpdate(m, ms) {
|
||||||
|
//console.log('momentUpdate', m);
|
||||||
|
if (m.choices) {
|
||||||
|
var p = 100 - ((ms - m.startMs) * 100.0 / (m.endMs - m.startMs));
|
||||||
|
document.getElementById("progress").style.width = p + '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function momentEnd(m, seeked) {
|
||||||
|
console.log('momentEnd', m, seeked);
|
||||||
|
if (m.choices) {
|
||||||
|
addChoices(null);
|
||||||
|
document.getElementById("progress").style.width = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var timerId = 0;
|
var timerId = 0;
|
||||||
|
var lastMs = 0;
|
||||||
var switchFrom = null;
|
var currentSegment;
|
||||||
var switchTo = null;
|
var lastSegment = null;
|
||||||
|
var segmentTransition = false;
|
||||||
function ontimeout(nextSegment) {
|
var lastMoments = [];
|
||||||
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();
|
||||||
|
currentSegment = getSegmentId(ms);
|
||||||
|
|
||||||
var segmentId = getSegmentId(ms);
|
if (timerId) {
|
||||||
|
clearTimeout(timerId);
|
||||||
// ontimeupdate resolution is about a second, better use timer
|
timerId = 0;
|
||||||
clearTimeout(timerId);
|
|
||||||
if (segmentId && nextSegment && nextSegment != segmentId) {
|
|
||||||
var timeLeft = SegmentMap.segments[segmentId].endTimeMs - ms;
|
|
||||||
timerId = setTimeout(ontimeout, timeLeft, nextSegment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSegment != segmentId) {
|
// Distinguish between the user seeking manually with <video> controls,
|
||||||
console.log('ontimeupdate', currentSegment, segmentId, ms, msToString(ms));
|
// and the video playing normally (past some timestamp / boundary).
|
||||||
currentSegment = segmentId;
|
let timeElapsed = ms - lastMs;
|
||||||
addZones(segmentId);
|
let seeked = timeElapsed < 0 || timeElapsed >= 2000;
|
||||||
currentMoment = null;
|
lastMs = ms;
|
||||||
addChoices(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var r = getMoment(ms);
|
// Recalculate title and hash only when we pass some meaningful timestamp.
|
||||||
if (r && momentSelected != r.id) {
|
let placeChanged = false;
|
||||||
updateProgressBar(ms, r);
|
|
||||||
if (currentMoment != r.id) {
|
// Handle segment change
|
||||||
currentMoment = r.id;
|
if (lastSegment != currentSegment) {
|
||||||
console.log('interaction', currentMoment);
|
console.log('ontimeupdate', lastSegment, '->', currentSegment, ms, msToString(ms), seeked);
|
||||||
addChoices(r);
|
lastSegment = currentSegment;
|
||||||
|
if (!seeked) {
|
||||||
|
if (playNextSegment()) {
|
||||||
|
// playSegment decided to seek, which means that this
|
||||||
|
// currentSegment is invalid, and a recursive
|
||||||
|
// ontimeupdate invocation should have taken care of
|
||||||
|
// things already. Return.
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
addZones(currentSegment);
|
||||||
currentMoment = null;
|
placeChanged = true;
|
||||||
addChoices(0);
|
|
||||||
updateProgressBar(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var naturalTransition = !seeked || segmentTransition;
|
||||||
|
segmentTransition = false;
|
||||||
|
|
||||||
|
var currentMoments = getMoments(currentSegment, ms);
|
||||||
|
for (let k in lastMoments)
|
||||||
|
if (!(k in currentMoments)) {
|
||||||
|
momentEnd(lastMoments[k], !naturalTransition);
|
||||||
|
placeChanged = true;
|
||||||
|
}
|
||||||
|
for (let k in lastMoments)
|
||||||
|
if (k in currentMoments)
|
||||||
|
momentUpdate(lastMoments[k], ms);
|
||||||
|
for (let k in currentMoments)
|
||||||
|
if (!(k in lastMoments)) {
|
||||||
|
momentStart(currentMoments[k], !naturalTransition);
|
||||||
|
placeChanged = true;
|
||||||
|
}
|
||||||
|
lastMoments = currentMoments;
|
||||||
|
|
||||||
|
if (placeChanged) {
|
||||||
|
let title = 'Bandersnatch';
|
||||||
|
title += ' - Chapter ' + currentSegment;
|
||||||
|
for (let k in currentMoments) {
|
||||||
|
let m = currentMoments[k];
|
||||||
|
if (m.type.substr(0, 6) == 'scene:') {
|
||||||
|
if (m.id && m.id in choicePoints && choicePoints[m.id].description)
|
||||||
|
title += ' - Choice "' + choicePoints[m.id].description + '"';
|
||||||
|
else
|
||||||
|
title += ' - Choice ' + (m.id || k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.title = title;
|
||||||
|
|
||||||
|
let hash = currentSegment;
|
||||||
|
// Pick the moment which starts closer to the current timestamp.
|
||||||
|
let bestMomentStart = segmentMap.segments[currentSegment].startTimeMs;
|
||||||
|
for (let k in currentMoments) {
|
||||||
|
let m = currentMoments[k];
|
||||||
|
if (m.startMs > bestMomentStart) {
|
||||||
|
hash = k;
|
||||||
|
bestMomentStart = m.startMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hash = '#' + hash;
|
||||||
|
lastHash = hash; // suppress onhashchange event
|
||||||
|
location.hash = hash;
|
||||||
|
ls.place = hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ontimeupdate resolution is about a second. Augment it using timer.
|
||||||
|
let nextEvent = segmentMap.segments[currentSegment].endTimeMs;
|
||||||
|
for (let k in currentMoments) {
|
||||||
|
let m = currentMoments[k];
|
||||||
|
if (m.endMs < nextEvent)
|
||||||
|
nextEvent = m.endMs;
|
||||||
|
}
|
||||||
|
for (let m of momentsBySegment[currentSegment] || [])
|
||||||
|
if (ms < m.startMs && m.startMs < nextEvent)
|
||||||
|
nextEvent = m.startMs;
|
||||||
|
var timeLeft = nextEvent - ms;
|
||||||
|
if (timeLeft > 0)
|
||||||
|
timerId = setTimeout(ontimeupdate, timeLeft);
|
||||||
}
|
}
|
||||||
|
|
||||||
function jumpForward(ms) {
|
function playNextSegment() {
|
||||||
|
if (nextChoice >= 0) {
|
||||||
|
let x = currentChoiceMoment.choices[nextChoice];
|
||||||
|
if (x.segmentId)
|
||||||
|
nextSegment = x.segmentId;
|
||||||
|
else if (x.sg)
|
||||||
|
nextSegment = resolveSegmentGroup(x.sg);
|
||||||
|
else
|
||||||
|
nextSegment = null;
|
||||||
|
console.log('choice', nextChoice, 'nextSegment', nextSegment);
|
||||||
|
applyImpression(x.impressionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
let breadcrumb = 'breadcrumb_' + nextSegment;
|
||||||
|
if (!(breadcrumb in ls))
|
||||||
|
ls[breadcrumb] = lastSegment;
|
||||||
|
|
||||||
|
segmentTransition = true;
|
||||||
|
let segment = nextSegment;
|
||||||
|
nextSegment = null;
|
||||||
|
if (segment)
|
||||||
|
return playSegment(segment, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jumpForward() {
|
||||||
var ms = getCurrentMs();
|
var ms = getCurrentMs();
|
||||||
var segmentId = getSegmentId(ms);
|
var segmentId = getSegmentId(ms);
|
||||||
var v = segmentMap.segments[segmentId];
|
|
||||||
|
|
||||||
var interactionMs = 0;
|
var interactionMs = 0;
|
||||||
if (v && v.ui && v.ui.interactionZones) {
|
let moments = momentsBySegment[segmentId] || [];
|
||||||
for (z of v.ui.interactionZones) {
|
// Find the earliest moment within this segment after cursor
|
||||||
var startMs = z[0];
|
for (let m of moments)
|
||||||
var stopMs = z[1];
|
if (m.startMs >= ms && (interactionMs == 0 || m.startMs < interactionMs))
|
||||||
if (ms < startMs)
|
interactionMs = m.startMs;
|
||||||
interactionMs = startMs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interactionMs) {
|
if (interactionMs) {
|
||||||
seek(interactionMs);
|
seek(interactionMs);
|
||||||
} else {
|
} else {
|
||||||
playSegment(nextSegment);
|
playNextSegment();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function jumpBack() {
|
function jumpBack() {
|
||||||
var ms = getCurrentMs();
|
var ms = getCurrentMs();
|
||||||
var segmentId = getSegmentId(ms);
|
var segmentId = getSegmentId(ms);
|
||||||
var startMs = getSegmentMs(segmentId);
|
let segment = segmentMap.segments[segmentId];
|
||||||
var previousSegment = getSegmentId(startMs - 1000);
|
|
||||||
console.log('jumpBack from-to', segmentId, previousSegment);
|
var interactionMs = 0;
|
||||||
playSegment(previousSegment);
|
let moments = momentsBySegment[segmentId] || [];
|
||||||
|
let inMoment = false;
|
||||||
|
// Find the latest moment within this segment before cursor
|
||||||
|
for (let m of moments) {
|
||||||
|
if (m.endMs < ms && m.startMs > interactionMs)
|
||||||
|
interactionMs = m.startMs;
|
||||||
|
if (m.startMs != segment.startTimeMs && m.startMs <= ms && ms < m.endMs)
|
||||||
|
inMoment = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interactionMs) {
|
||||||
|
seek(interactionMs);
|
||||||
|
} else if (inMoment) {
|
||||||
|
seek(segment.startTimeMs);
|
||||||
|
} else {
|
||||||
|
let breadcrumb = 'breadcrumb_' + segmentId;
|
||||||
|
if (breadcrumb in ls) {
|
||||||
|
// Jump to last moment in previous segment
|
||||||
|
segmentId = ls[breadcrumb];
|
||||||
|
segment = segmentMap.segments[segmentId];
|
||||||
|
|
||||||
|
interactionMs = segment.startTimeMs;
|
||||||
|
let moments = momentsBySegment[segmentId] || [];
|
||||||
|
for (let m of moments)
|
||||||
|
if (m.startMs > interactionMs)
|
||||||
|
interactionMs = m.startMs;
|
||||||
|
seek(interactionMs);
|
||||||
|
} else {
|
||||||
|
seek(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFullScreen() {
|
function toggleFullScreen() {
|
||||||
|
@ -310,120 +434,129 @@ function togglePlayPause() {
|
||||||
else v.pause();
|
else v.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onload() {
|
window.onload = function() {
|
||||||
var video_selector = document.getElementById("video");
|
var video_selector = document.getElementById("video");
|
||||||
var video_source_selector = document.getElementById("video-source");
|
var video_source_selector = document.getElementById("video-source");
|
||||||
var file_selector = document.getElementById("file-selector");
|
var file_selector = document.getElementById("file-selector");
|
||||||
|
function startPlayback() {
|
||||||
|
file_selector.style.display = 'none';
|
||||||
|
if (window.location.hash)
|
||||||
|
playHash(window.location.hash);
|
||||||
|
else if (ls.place)
|
||||||
|
playHash(ls.place);
|
||||||
|
else
|
||||||
|
playSegment(null);
|
||||||
|
video_selector.play();
|
||||||
|
}
|
||||||
if (video_source_selector.getAttribute("src") == '') {
|
if (video_source_selector.getAttribute("src") == '') {
|
||||||
console.log('no video')
|
console.log('no video');
|
||||||
file_selector.style.display = 'table';
|
file_selector.style.display = 'table';
|
||||||
document.getElementById("wrapper-video").style.display = 'none';
|
document.getElementById("wrapper-video").style.display = 'none';
|
||||||
|
} else {
|
||||||
|
startPlayback();
|
||||||
}
|
}
|
||||||
document.getElementById('fileinput').addEventListener('change', function () {
|
document.getElementById('fileinput').addEventListener('change', function () {
|
||||||
var file = this.files[0];
|
var file = this.files[0];
|
||||||
var fileUrl = URL.createObjectURL(file)
|
var fileUrl = URL.createObjectURL(file);
|
||||||
video_selector.src = fileUrl;
|
video_selector.src = fileUrl;
|
||||||
video_selector.play();
|
|
||||||
file_selector.style.display = 'none';
|
|
||||||
document.getElementById("wrapper-video").style.display = 'block';
|
document.getElementById("wrapper-video").style.display = 'block';
|
||||||
|
startPlayback();
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
video_selector.ontimeupdate = ontimeupdate;
|
video_selector.ontimeupdate = ontimeupdate;
|
||||||
|
|
||||||
var c = document.getElementById("c");
|
var c = document.getElementById("c");
|
||||||
c.ondblclick = toggleFullScreen;
|
c.ondblclick = toggleFullScreen;
|
||||||
c.onclick = function () {
|
video_selector.onclick = function (e) {
|
||||||
// can't togglePlayPause here, choice buttons stop the video
|
togglePlayPause();
|
||||||
// should use preventdefault or something
|
e.preventDefault();
|
||||||
// use spacebar for now
|
|
||||||
// mind that autoplay is disabled in latest chrome, so play after click
|
|
||||||
document.getElementById("video").play();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.onkeypress = function (e) {
|
document.onkeypress = function (e) {
|
||||||
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)
|
||||||
|
return;
|
||||||
if (e.code == 'KeyF')
|
if (e.code == 'KeyF')
|
||||||
toggleFullScreen();
|
toggleFullScreen();
|
||||||
if (e.code == 'KeyR')
|
if (e.code == 'KeyR')
|
||||||
playSegment(0);
|
playSegment(0);
|
||||||
if (e.code == 'Space')
|
if (e.code == 'Space')
|
||||||
togglePlayPause();
|
togglePlayPause();
|
||||||
}
|
};
|
||||||
|
video_selector.onkeydown = function(e) {
|
||||||
|
if (e.code == 'Space')
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
document.onkeydown = function (evt) {
|
document.onkeydown = function (e) {
|
||||||
var v = document.getElementById("video");
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)
|
||||||
|
return;
|
||||||
if (evt.key == 'ArrowLeft') {
|
if (e.key == 'ArrowLeft')
|
||||||
jumpBack();
|
jumpBack();
|
||||||
}
|
if (e.key == 'ArrowRight')
|
||||||
|
|
||||||
if (evt.key == 'ArrowRight') {
|
|
||||||
jumpForward();
|
jumpForward();
|
||||||
}
|
if (e.key == 'ArrowUp')
|
||||||
}
|
video_selector.playbackRate = video_selector.playbackRate * 2.0;
|
||||||
|
if (e.key == 'ArrowDown')
|
||||||
|
video_selector.playbackRate = video_selector.playbackRate / 2.0;
|
||||||
|
};
|
||||||
|
|
||||||
if (location.hash) {
|
window.onhashchange = function() {
|
||||||
var segmentId = location.hash.slice(1);
|
playHash(window.location.hash);
|
||||||
playSegment(segmentId);
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function seek(ms) {
|
function seek(ms) {
|
||||||
clearTimeout(timerId);
|
|
||||||
console.log('seek', ms);
|
console.log('seek', ms);
|
||||||
momentSelected = null;
|
|
||||||
document.getElementById("video").currentTime = ms / 1000.0;
|
document.getElementById("video").currentTime = ms / 1000.0;
|
||||||
|
ontimeupdate(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function choice(choiceId, text, id) {
|
function choice(choiceIndex) {
|
||||||
var segmentId = findSegment(choiceId);
|
nextChoice = choiceIndex;
|
||||||
console.log('choice', choiceId, 'nextSegment', segmentId);
|
newList("choices");
|
||||||
applyImpression(globalChoices[id]);
|
if (!currentChoiceMoment.config.disableImmediateSceneTransition)
|
||||||
setNextSegment(segmentId, text);
|
playNextSegment();
|
||||||
momentSelected = choiceId;
|
|
||||||
addChoices(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyImpression(obj) {
|
function applyImpression(impressionData) {
|
||||||
if (!obj) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
persistentState[variable] = value;
|
ls["persistentState_" + variable] = JSON.stringify(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPlaybackImpression(segmentId) {
|
function playSegment(segmentId, noSeek) {
|
||||||
let moments = momentsBySegment[segmentId];
|
if (!segmentId || typeof segmentId === "undefined")
|
||||||
|
segmentId = segmentMap.initialSegment;
|
||||||
|
var oldSegment = getSegmentId(getCurrentMs());
|
||||||
|
console.log('playSegment', oldSegment, '->', segmentId);
|
||||||
|
if (!noSeek || oldSegment != segmentId) {
|
||||||
|
var ms = getSegmentMs(segmentId);
|
||||||
|
seek(ms);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!moments) {
|
var lastHash = '';
|
||||||
console.log('warning - no moments');
|
function playHash(hash) {
|
||||||
|
// console.log('playHash', lastHash, '->', hash);
|
||||||
|
if (hash == lastHash)
|
||||||
return;
|
return;
|
||||||
}
|
lastHash = hash;
|
||||||
|
if (hash) {
|
||||||
for (moment of moments) {
|
hash = hash.slice(1);
|
||||||
if (moment.type != 'notification:playbackImpression') {
|
if (hash[0] == 't')
|
||||||
continue;
|
seek(Number(Math.round(hash.slice(1) * 1000.0)));
|
||||||
|
else {
|
||||||
|
let loc = hash.split('/');
|
||||||
|
let segmentId = loc[0];
|
||||||
|
if (loc.length > 1)
|
||||||
|
seek(momentsBySegment[segmentId][loc[1]].startMs);
|
||||||
|
else
|
||||||
|
seek(getSegmentMs(segmentId));
|
||||||
}
|
}
|
||||||
|
|
||||||
applyImpression(moment);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function playSegment(segmentId) {
|
|
||||||
clearTimeout(timerId);
|
|
||||||
if (!segmentId || segmentId == "undefined")
|
|
||||||
segmentId = '1A';
|
|
||||||
console.log('playSegment', segmentId);
|
|
||||||
applyPlaybackImpression(segmentId);
|
|
||||||
location.hash = segmentId;
|
|
||||||
document.title = 'Bandersnatch - Chapter ' + segmentId;
|
|
||||||
var ms = getSegmentMs(segmentId);
|
|
||||||
seek(ms);
|
|
||||||
}
|
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body onload=onload()>
|
<body>
|
||||||
<div>
|
<div>
|
||||||
<div id="c">
|
<div id="c">
|
||||||
<section role="banner">
|
<section role="banner">
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
<li><kbd>F</kbd> - Toggle fullscreen</li>
|
<li><kbd>F</kbd> - Toggle fullscreen</li>
|
||||||
<li><kbd>R</kbd> - Restart video</li>
|
<li><kbd>R</kbd> - Restart video</li>
|
||||||
<li><kbd>→</kbd> - Jump to the next segment (or to the next interaction zone)</li>
|
<li><kbd>→</kbd> - Jump to the next segment (or to the next interaction zone)</li>
|
||||||
<li><kbd>←</kbd> - Jump to the previous segment</li>
|
<li><kbd>←</kbd> - Jump to the previous segment (or interaction zone)</li>
|
||||||
<li><kbd>Space</kbd> - Toggle play and pause</li>
|
<li><kbd>Space</kbd> - Toggle play and pause</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div id="wrapper-video">
|
<div id="wrapper-video">
|
||||||
<video id="video" loop preload="metadata">
|
<video id="video" preload="metadata">
|
||||||
<source id="video-source" src="">
|
<source id="video-source" src="">
|
||||||
<track label="English" kind="subtitles" srclang="en"
|
<track label="English" kind="subtitles" srclang="en"
|
||||||
src="subtitle/Black.Mirror.Bandersnatch.2018.WEBRip.x264-NoGRP.vtt" default>
|
src="subtitle/Black.Mirror.Bandersnatch.2018.WEBRip.x264-NoGRP.vtt" default>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue