From 4ee7034f7780664c687253c958a92a08422bb5e2 Mon Sep 17 00:00:00 2001 From: Splines Date: Thu, 23 May 2024 01:02:02 +0200 Subject: [PATCH] Open interactive media if chapters OR references are present --- .../javascripts/thyme/chapter_manager.js | 115 ++++--- .../javascripts/thyme/metadata_manager.js | 303 +++++++++--------- app/assets/javascripts/thyme/thyme_player.js | 25 +- 3 files changed, 248 insertions(+), 195 deletions(-) diff --git a/app/assets/javascripts/thyme/chapter_manager.js b/app/assets/javascripts/thyme/chapter_manager.js index 72e3f266a..ece8cc46d 100644 --- a/app/assets/javascripts/thyme/chapter_manager.js +++ b/app/assets/javascripts/thyme/chapter_manager.js @@ -8,25 +8,31 @@ class ChapterManager { this.iaBackButton = iaBackButton; } - load() { + /** + * Loads chapters from the video element and displays them in the interactive area. + * @param {function} onLoad - Callback function that is called when chapters have been loaded. + * It receives a boolean value that indicates whether chapters are present. + */ + load(onLoad) { let initialChapters = true; - const videoId = thymeAttributes.video.id; - const chaptersElement = $("#" + videoId + ' track[kind="chapters"]').get(0); + const chapters = this.#getChapters(); const chapterManager = this; - /* after video metadata have been loaded, display chapters in the interactive area Originally (and more appropriately, according to the standards), only the 'loadedmetadata' event was used. However, Firefox triggers this event too soon, i.e. when the readyStates for chapters and elements are 1 (loading) instead of 2 (loaded) for the events, see https://www.w3schools.com/jsref/event_oncanplay.asp */ video.addEventListener("loadedmetadata", function () { - if (initialChapters && chaptersElement.readyState === 2) { + if (initialChapters && chapters.readyState === 2) { chapterManager.#displayChapters(); initialChapters = false; + if (onLoad) { + onLoad(chapters.track ? (chapters.track.cues.length > 0) : false); + } } }); video.addEventListener("canplay", function () { - if (initialChapters && chaptersElement.readyState === 2) { + if (initialChapters && chapters.readyState === 2) { chapterManager.#displayChapters(); initialChapters = false; } @@ -66,56 +72,65 @@ class ChapterManager { } } - #displayChapters() { + #getChapters() { const videoId = thymeAttributes.video.id; + return $("#" + videoId + ' track[kind="chapters"]').get(0); + } + + #displayChapters() { const chapterListId = this.chapterListId; const iaBackButton = this.iaBackButton; const chapterList = $("#" + chapterListId); - const chaptersElement = $("#" + videoId + ' track[kind="chapters"]').get(0); - let chaptersTrack; - if (chaptersElement.readyState === 2 && (chaptersTrack = chaptersElement.track)) { - chaptersTrack.mode = "hidden"; - let times = []; - // read out the chapter track cues and generate html elements for chapters, - // run katex on them - for (let i = 0; i < chaptersTrack.cues.length; i++) { - const cue = chaptersTrack.cues[i]; - const chapterName = cue.text; - const start = cue.startTime; - times.push(start); - const $listItem = $("
  • "); - const $link = $("", { - id: "c-" + start, - text: chapterName, - }); - chapterList.append($listItem.append($link)); - const chapterElement = $link.get(0); - thymeUtility.renderLatex(chapterElement); - $link.data("text", chapterName); - // if a chapter element is clicked, transport to chapter start time - $link.on("click", function () { - iaBackButton.update(); - video.currentTime = this.id.replace("c-", ""); - }); - } - // store start times as data attribute - chapterList.get(0).dataset.times = JSON.stringify(times); - chapterList.show(); - // if the chapters cue changes (i.e. a switch between chapters), highlight - // current chapter elment and scroll it into view, remove highlighting from - // old chapter - $(chaptersTrack).on("cuechange", function () { - $("#" + chapterListId + " li a").removeClass("current"); - if (this.activeCues.length > 0) { - const activeStart = this.activeCues[0].startTime; - const chapter = document.getElementById("c-" + activeStart); - if (chapter) { - $(chapter).addClass("current"); - chapter.scrollIntoView(); - } - } + const chapters = this.#getChapters(); + if (chapters.readyState != 2) { + return; + } + const track = chapters.track; + if (!track) { + return; + } + + track.mode = "hidden"; + let times = []; + // read out the chapter track cues and generate html elements for chapters, + // run katex on them + for (let i = 0; i < track.cues.length; i++) { + const cue = track.cues[i]; + const chapterName = cue.text; + const start = cue.startTime; + times.push(start); + const $listItem = $("
  • "); + const $link = $("", { + id: "c-" + start, + text: chapterName, + }); + chapterList.append($listItem.append($link)); + const chapterElement = $link.get(0); + thymeUtility.renderLatex(chapterElement); + $link.data("text", chapterName); + // if a chapter element is clicked, transport to chapter start time + $link.on("click", function () { + iaBackButton.update(); + video.currentTime = this.id.replace("c-", ""); }); } + // store start times as data attribute + chapterList.get(0).dataset.times = JSON.stringify(times); + chapterList.show(); + // if the chapters cue changes (i.e. a switch between chapters), highlight + // current chapter elment and scroll it into view, remove highlighting from + // old chapter + $(track).on("cuechange", function () { + $("#" + chapterListId + " li a").removeClass("current"); + if (this.activeCues.length > 0) { + const activeStart = this.activeCues[0].startTime; + const chapter = document.getElementById("c-" + activeStart); + if (chapter) { + $(chapter).addClass("current"); + chapter.scrollIntoView(); + } + } + }); } } diff --git a/app/assets/javascripts/thyme/metadata_manager.js b/app/assets/javascripts/thyme/metadata_manager.js index 760e58bb4..2e9e6967d 100644 --- a/app/assets/javascripts/thyme/metadata_manager.js +++ b/app/assets/javascripts/thyme/metadata_manager.js @@ -7,10 +7,14 @@ class MetadataManager { this.metadataListId = metadataListId; } - load() { + /** + * Loads metadata from the video element and displays them in the interactive area. + * @param {function} onLoad - Callback function that is called when metadata have been loaded. + * It receives a boolean value that indicates whether metadata is present. + */ + load(onLoad) { let initialMetadata = true; - const videoId = thymeAttributes.video.id; - const metadataElement = $("#" + videoId + ' track[kind="metadata"]').get(0); + const metadata = this.#getMetadata(); const metadataManager = this; /* after video metadata have been loaded, display chapters in the interactive area @@ -19,13 +23,16 @@ class MetadataManager { i.e. when the readyStates for chapters and elements are 1 (loading) instead of 2 (loaded) for the events, see https://www.w3schools.com/jsref/event_oncanplay.asp */ video.addEventListener("loadedmetadata", function () { - if (initialMetadata && metadataElement.readyState === 2) { + if (initialMetadata && metadata.readyState === 2) { metadataManager.#displayMetadata(); initialMetadata = false; + if (onLoad) { + onLoad(metadata.track ? (metadata.track.cues.length > 0) : false); + } } }); video.addEventListener("canplay", function () { - if (initialMetadata && metadataElement.readyState === 2) { + if (initialMetadata && metadata.readyState === 2) { metadataManager.#displayMetadata(); initialMetadata = false; } @@ -67,153 +74,163 @@ class MetadataManager { } } + #getMetadata() { + const videoId = thymeAttributes.video.id; + return $("#" + videoId + ' track[kind="metadata"]').get(0); + } + // set up the metadata elements #displayMetadata() { const video = thymeAttributes.video; const metadataManager = this; const metadataListId = this.metadataListId; const $metaList = $("#" + metadataListId); - const metadataElement = $("#" + video.id + ' track[kind="metadata"]').get(0); + const metadata = this.#getMetadata(); - let metaTrack; - if (metadataElement.readyState === 2 && (metaTrack = metadataElement.track)) { - metaTrack.mode = "hidden"; - let times = []; - // read out the metadata track cues and generate html elements for - // metadata, run katex on them - for (let i = 0; i < metaTrack.cues.length; i++) { - const cue = metaTrack.cues[i]; - const meta = JSON.parse(cue.text); - const start = cue.startTime; - times.push(start); - const $listItem = $("
  • ", { - id: "m-" + start, - }); - $listItem.hide(); - const $link = $("", { - text: meta.reference, - class: "item", - id: "l-" + start, - }); - const $videoIcon = $("", { - text: "video_library", - class: "material-icons", - }); - const $videoRef = $("", { - href: meta.video, - target: "_blank", - }); - $videoRef.append($videoIcon); - if (!meta.video) { - $videoRef.hide(); - } - const $manIcon = $("", { - text: "library_books", - class: "material-icons", - }); - const $manRef = $("", { - href: meta.manuscript, - target: "_blank", - }); - $manRef.append($manIcon); - if (!meta.manuscript) { - $manRef.hide(); - } - const $scriptIcon = $("", { - text: "menu_book", - class: "material-icons", - }); - const $scriptRef = $("", { - href: meta.script, - target: "_blank", - }); - $scriptRef.append($scriptIcon); - if (!meta.script) { - $scriptRef.hide(); - } - const $quizIcon = $("", { - text: "videogame_asset", - class: "material-icons", - }); - const $quizRef = $("", { - href: meta.quiz, - target: "_blank", - }); - $quizRef.append($quizIcon); - if (!meta.quiz) { - $quizRef.hide(); - } - const $extIcon = $("", { - text: "link", - class: "material-icons", - }); - const $extRef = $("", { - href: meta.link, - target: "_blank", - }); - $extRef.append($extIcon); - if (!meta.link) { - $extRef.hide(); - } - const $description = $("
    ", { - text: meta.text, - class: "mx-3", - }); - const $explanation = $("
    ", { - text: meta.explanation, - class: "m-3", - }); - const $details = $("
    "); - $details.append($link).append($description).append($explanation); - let $icons = $("
    ", { - style: "flex-shrink: 3; display: flex; flex-direction: column;", - }); - $icons.append($videoRef).append($manRef).append($scriptRef).append($quizRef).append($extRef); - $listItem.append($details).append($icons); - $metaList.append($listItem); - $videoRef.on("click", function () { - video.pause(); - }); - $manRef.on("click", function () { - video.pause(); - }); - $extRef.on("click", function () { - video.pause(); - }); - $link.on("click", function () { - // displayBackButton(); - video.currentTime = this.id.replace("l-", ""); - }); - let metaElement = $listItem.get(0); - thymeUtility.renderLatex(metaElement); + if (metadata.readyState != 2) { + return; + } + const track = metadata.track; + if (!track) { + return; + } + + track.mode = "hidden"; + let times = []; + // read out the metadata track cues and generate html elements for + // metadata, run katex on them + for (let i = 0; i < track.cues.length; i++) { + const cue = track.cues[i]; + const meta = JSON.parse(cue.text); + const start = cue.startTime; + times.push(start); + const $listItem = $("
  • ", { + id: "m-" + start, + }); + $listItem.hide(); + const $link = $("", { + text: meta.reference, + class: "item", + id: "l-" + start, + }); + const $videoIcon = $("", { + text: "video_library", + class: "material-icons", + }); + const $videoRef = $("", { + href: meta.video, + target: "_blank", + }); + $videoRef.append($videoIcon); + if (!meta.video) { + $videoRef.hide(); } - // store metadata start times as data attribute - $metaList.get(0).dataset.times = JSON.stringify(times); - // if user jumps to a new position in the video, display all metadata - // that start before this time and hide all that start later - $(video).on("seeked", function () { - const time = video.currentTime; - metadataManager.metaIntoView(time); - }); - // if the metadata cue changes, highlight all current media and scroll - // them into view - $(metaTrack).on("cuechange", function () { - let j = 0; - $("#" + metadataListId + " li").removeClass("current"); - while (j < this.activeCues.length) { - const activeStart = this.activeCues[j].startTime; - let metalink = document.getElementById("m-" + activeStart); - if (metalink) { - $(metalink).show(); - $(metalink).addClass("current"); - } - ++j; - } - const currentLength = $("#" + metadataListId + " .current").length; - if (currentLength > 0) { - $("#" + metadataListId + " .current").get(length - 1).scrollIntoView(); - } + const $manIcon = $("", { + text: "library_books", + class: "material-icons", + }); + const $manRef = $("", { + href: meta.manuscript, + target: "_blank", + }); + $manRef.append($manIcon); + if (!meta.manuscript) { + $manRef.hide(); + } + const $scriptIcon = $("", { + text: "menu_book", + class: "material-icons", + }); + const $scriptRef = $("", { + href: meta.script, + target: "_blank", + }); + $scriptRef.append($scriptIcon); + if (!meta.script) { + $scriptRef.hide(); + } + const $quizIcon = $("", { + text: "videogame_asset", + class: "material-icons", + }); + const $quizRef = $("", { + href: meta.quiz, + target: "_blank", + }); + $quizRef.append($quizIcon); + if (!meta.quiz) { + $quizRef.hide(); + } + const $extIcon = $("", { + text: "link", + class: "material-icons", + }); + const $extRef = $("", { + href: meta.link, + target: "_blank", + }); + $extRef.append($extIcon); + if (!meta.link) { + $extRef.hide(); + } + const $description = $("
    ", { + text: meta.text, + class: "mx-3", + }); + const $explanation = $("
    ", { + text: meta.explanation, + class: "m-3", }); + const $details = $("
    "); + $details.append($link).append($description).append($explanation); + let $icons = $("
    ", { + style: "flex-shrink: 3; display: flex; flex-direction: column;", + }); + $icons.append($videoRef).append($manRef).append($scriptRef).append($quizRef).append($extRef); + $listItem.append($details).append($icons); + $metaList.append($listItem); + $videoRef.on("click", function () { + video.pause(); + }); + $manRef.on("click", function () { + video.pause(); + }); + $extRef.on("click", function () { + video.pause(); + }); + $link.on("click", function () { + // displayBackButton(); + video.currentTime = this.id.replace("l-", ""); + }); + let metaElement = $listItem.get(0); + thymeUtility.renderLatex(metaElement); } + // store metadata start times as data attribute + $metaList.get(0).dataset.times = JSON.stringify(times); + // if user jumps to a new position in the video, display all metadata + // that start before this time and hide all that start later + $(video).on("seeked", function () { + const time = video.currentTime; + metadataManager.metaIntoView(time); + }); + // if the metadata cue changes, highlight all current media and scroll + // them into view + $(track).on("cuechange", function () { + let j = 0; + $("#" + metadataListId + " li").removeClass("current"); + while (j < this.activeCues.length) { + const activeStart = this.activeCues[j].startTime; + let metalink = document.getElementById("m-" + activeStart); + if (metalink) { + $(metalink).show(); + $(metalink).addClass("current"); + } + ++j; + } + const currentLength = $("#" + metadataListId + " .current").length; + if (currentLength > 0) { + $("#" + metadataListId + " .current").get(length - 1).scrollIntoView(); + } + }); } } diff --git a/app/assets/javascripts/thyme/thyme_player.js b/app/assets/javascripts/thyme/thyme_player.js index c8afaf225..a428023c9 100644 --- a/app/assets/javascripts/thyme/thyme_player.js +++ b/app/assets/javascripts/thyme/thyme_player.js @@ -129,8 +129,29 @@ $(document).on("turbolinks:load", function () { const metadataManager = new MetadataManager("metadata"); thymeAttributes.chapterManager = chapterManager; thymeAttributes.metadataManager = metadataManager; - chapterManager.load(); - metadataManager.load(); + + let hasChapters = undefined; + chapterManager.load((_hasChapters) => { + hasChapters = _hasChapters; + onVideoDataReady(); + }); + + let hasMetadata = undefined; + metadataManager.load((_hasMetadata) => { + hasMetadata = _hasMetadata; + onVideoDataReady(); + }); + + function onVideoDataReady() { + if (hasChapters === undefined || hasMetadata === undefined) { + return; + } + + if (hasChapters || hasMetadata) { + // Open the interactive area + $("#ia-active").trigger("click"); + } + } /* INTERACTIVE AREA