Skip to content

Instantly share code, notes, and snippets.

@chilang
Created November 16, 2023 15:54
Show Gist options
  • Save chilang/29677e3e48fb829997d038507a52652c to your computer and use it in GitHub Desktop.
Save chilang/29677e3e48fb829997d038507a52652c to your computer and use it in GitHub Desktop.
hyperaudio-static
/*! (C) The Hyperaudio Project. MIT @license: en.wikipedia.org/wiki/MIT_License. */
/*! Version 2.1.4 */
'use strict';
var caption = function () {
var cap = {};
function formatSeconds(seconds) {
if (typeof seconds == 'number') {
//console.log("seconds = "+seconds);
return new Date(seconds.toFixed(3) * 1000).toISOString().substring(11,23);
} else {
console.log('warning - attempting to format the non number: ' + seconds);
return null;
}
}
function convertTimecodeToSrt(timecode) {
//the same as VTT format but milliseconds separated by a comma
return timecode.substring(0,8) + "," + timecode.substring(9,12);
}
cap.init = function (transcriptId, playerId, maxLength, minLength, label, srclang) {
var transcript = document.getElementById(transcriptId);
var words = transcript.querySelectorAll('[data-m]');
var data = {};
data.segments = [];
function segmentMeta(speaker, start, duration, chars) {
this.speaker = speaker;
this.start = start;
this.duration = duration;
this.chars = chars;
this.words = [];
}
function wordMeta(start, duration, text) {
this.start = start;
this.duration = duration;
this.text = text;
}
var thisWordMeta;
var thisSegmentMeta = null;
// defaults
var maxLineLength = 37;
var minLineLength = 21;
var maxWordDuration = 2; //seconds
var captionsVtt = 'WEBVTT\n';
var captionsSrt = '';
var endSentenceDelimiter = /[\.。?؟!]/g;
var midSentenceDelimiter = /[,、–,،و:,…‥]/g;
if (!isNaN(maxLength) && maxLength != null) {
maxLineLength = maxLength;
}
if (!isNaN(minLength) && minLength != null) {
minLineLength = minLength;
}
words.forEach(function (word, i) {
if (thisSegmentMeta === null) {
// create segment meta object
thisSegmentMeta = new segmentMeta('', null, 0, 0, 0);
}
if (word.classList.contains('speaker')) {
// checking that this is not a new segment AND a new empty segment wasn't already created
if (thisSegmentMeta !== null && thisSegmentMeta.start !== null) {
data.segments.push(thisSegmentMeta); // push the previous segment because it's a new speaker
thisSegmentMeta = new segmentMeta('', null, 0, 0, 0);
}
thisSegmentMeta.speaker = word.innerText;
} else {
var thisStart = parseInt(word.getAttribute('data-m')) / 1000;
var thisDuration = parseInt(word.getAttribute('data-d')) / 1000;
if (isNaN(thisStart)) {
thisStart = 0;
}
// data-d (duration) is an optional attribute, if it doesn't exist
// use the start time of the next word (if it exists) or for the last word
// pick a sensible duration.
if (isNaN(thisDuration)) {
if (i < (words.length - 1)) {
thisDuration = (parseInt(words[i+1].getAttribute('data-m') - 1) / 1000) - thisStart;
if (thisDuration > maxWordDuration) {
thisDuration = maxWordDuration;
}
} else {
thisDuration = 5; // sensible default for the last word
}
}
var thisText = word.innerText;
thisWordMeta = new wordMeta(thisStart, thisDuration, thisText);
if (thisSegmentMeta.start === null) {
thisSegmentMeta.start = thisStart;
thisSegmentMeta.duration = 0;
thisSegmentMeta.chars = 0;
}
thisSegmentMeta.duration += thisDuration;
thisSegmentMeta.chars += thisText.length;
thisSegmentMeta.words.push(thisWordMeta);
// remove spaces first just in case
var lastChar = thisText.replace(/\s/g, '').slice(-1);
if (lastChar.match(endSentenceDelimiter)) {
data.segments.push(thisSegmentMeta);
thisSegmentMeta = null;
}
}
});
function captionMeta(start, stop, text) {
this.start = start;
this.stop = stop;
this.text = text;
}
var captions = [];
var thisCaption = null;
data.segments.map(function (segment, i, arr) {
// If the entire segment fits on a line, add it to the captions.
if (segment.chars < maxLineLength) {
if (segment.duration === 0){
if (i < arr.length) {
segment.duration = arr[i+1].start - segment.start;
} else {
segment.duration = 5 * 1000;
}
}
thisCaption = new captionMeta(
formatSeconds(segment.start),
formatSeconds(segment.start + segment.duration),
'',
);
segment.words.forEach(function (wordMeta) {
thisCaption.text += wordMeta.text;
});
thisCaption.text += '\n';
//console.log("0. pushing because the whole segment fits on a line!");
//console.log(thisCaption);
captions.push(thisCaption);
thisCaption = null;
} else {
// The number of chars in this segment is longer than our single line maximum
var charCount = 0;
var lineText = '';
var firstLine = true;
var lastOutTime;
var lastInTime = null;
segment.words.forEach(function (wordMeta, index) {
var lastChar = wordMeta.text.replace(/\s/g, '').slice(-1);
if (lastInTime === null) {
// if it doesn't exist yet set the caption start time to the word's start time.
lastInTime = wordMeta.start;
}
// Are we over the minimum length of a line and hitting a good place to split mid-sentence?
if (charCount + wordMeta.text.length > minLineLength && lastChar.match(midSentenceDelimiter)) {
if (firstLine === true) {
thisCaption = new captionMeta(
formatSeconds(lastInTime),
formatSeconds(wordMeta.start + wordMeta.duration),
'',
);
thisCaption.text += lineText + wordMeta.text + '\n';
//check for last word in segment, if it is we can push a one line caption, if not – move on to second line
if (index + 1 >= segment.words.length) {
//console.log("1. pushing because we're at a good place to split, we're on the first line but it's the last word of the segment.");
//console.log(thisCaption);
captions.push(thisCaption);
thisCaption = null;
} else {
firstLine = false;
}
} else {
// We're on the second line ... we're over the minimum chars and in a good place to split – let's push the caption
thisCaption.stop = formatSeconds(wordMeta.start + wordMeta.duration);
thisCaption.text += lineText + wordMeta.text;
//console.log("2. pushing because we're on the second line and have a good place to split");
//console.log(thisCaption);
captions.push(thisCaption);
thisCaption = null;
firstLine = true;
}
// whether first line or not we should reset ready for a new caption
charCount = 0;
lineText = '';
lastInTime = null;
} else {
// we're not over the minimum length with a suitable splitting point
// If we add this word are we over the maximum?
if (charCount + wordMeta.text.length > maxLineLength) {
if (firstLine === true) {
if (lastOutTime === undefined) {
lastOutTime = wordMeta.start + wordMeta.duration;
}
thisCaption = new captionMeta(formatSeconds(lastInTime), formatSeconds(lastOutTime), '');
thisCaption.text += lineText + '\n';
// It's just the first line so we should only push a new caption if it's the very last word!
if (index >= segment.words.length) {
captions.push(thisCaption);
thisCaption = null;
} else {
firstLine = false;
}
} else {
// We're on the second line and since we're over the maximum with the next word we should push this caption!
thisCaption.stop = formatSeconds(lastOutTime);
thisCaption.text += lineText;
captions.push(thisCaption);
thisCaption = null;
firstLine = true;
}
// do the stuff we need to do to start a new line
charCount = wordMeta.text.length;
lineText = wordMeta.text;
lastInTime = wordMeta.start;
} else {
// We're not over the maximum with this word, update the line length and add the word to the text
charCount += wordMeta.text.length;
lineText += wordMeta.text;
}
}
// for every word update the lastOutTime
lastOutTime = wordMeta.start + wordMeta.duration;
});
// we're out of words for this segment - decision time!
if (thisCaption !== null) {
// The caption had been started, time to add whatever text we have and add a stop point
thisCaption.stop = formatSeconds(lastOutTime);
thisCaption.text += lineText;
//console.log("3. pushing at end of segment when new caption HAS BEEN created");
//console.log(thisCaption);
captions.push(thisCaption);
thisCaption = null;
} else {
// caption hadn't been started yet - create one!
if (lastInTime !== null) {
thisCaption = new captionMeta(formatSeconds(lastInTime), formatSeconds(lastOutTime), lineText);
//console.log("4. pushing at end of segment when new caption has yet to be created");
//console.log(thisCaption);
captions.push(thisCaption);
thisCaption = null;
}
}
}
});
captions.forEach(function (caption, i) {
captionsVtt += '\n' + caption.start + ' --> ' + caption.stop + '\n' + caption.text + '\n';
//console.log(caption.start + ' --> ' + caption.stop + '\n' + caption.text);
captionsSrt += '\n' + (i + 1) + '\n' + convertTimecodeToSrt(caption.start) + ' --> ' + convertTimecodeToSrt(caption.stop) + '\n' + caption.text + '\n';
});
var video = document.getElementById(playerId);
if (video !== null) {
video.addEventListener("loadedmetadata", function listener() {
var track = document.getElementById(playerId+'-vtt');
if (track !== null){
track.kind = "captions";
if (label !== undefined) {
//console.log("setting label as "+label);
track.label = label;
}
if (srclang !== undefined) {
//console.log("setting srclang as "+srclang);
track.srclang = srclang;
}
track.src = "data:text/vtt,"+encodeURIComponent(captionsVtt);
video.textTracks[0].mode = "showing";
video.removeEventListener("loadedmetadata", listener, true);
}
}, true);
if (video.textTracks !== undefined && video.textTracks[0] !== undefined) {
video.textTracks[0].mode = "showing";
}
}
function captionsObj(vtt, srt, data) {
// clean up – remove any double blank lines
// and blank line at the start of srt
if (srt.charAt(0) !== "1") {
srt = srt.slice(1);
}
this.vtt = vtt.replaceAll("\n\n\n","\n\n");
this.srt = srt.replaceAll("\n\n\n","\n\n");
this.data = captions;
}
return new captionsObj(captionsVtt, captionsSrt, captions);
};
return cap;
};
/*! (C) The Hyperaudio Project. MIT @license: en.wikipedia.org/wiki/MIT_License. */
/*! Version 2.1.1b */
'use strict';
// Example wrapper for hyperaudio-lite with search and playbackRate included
let searchForm = document.getElementById('searchForm');
if (searchForm) {
searchForm.addEventListener('submit', function(event){
searchPhrase(document.getElementById('search').value);
event.preventDefault();
}, false);
}
let searchPhrase = function (phrase) {
let htmlWords = document.querySelectorAll('[data-m]');
let htmlWordsLen = htmlWords.length;
let phraseWords = phrase.split(" ");
let phraseWordsLen = phraseWords.length;
let matchedTimes = [];
// clear matched times
let searchMatched = document.querySelectorAll('.search-match');
searchMatched.forEach((match) => {
match.classList.remove('search-match');
});
for (let i = 0; i < htmlWordsLen; i++) {
let numWordsMatched = 0;
let potentiallyMatched = [];
for (let j = 0; j < phraseWordsLen; j++) {
let wordIndex = i + numWordsMatched;
if (wordIndex >= htmlWordsLen) {
break;
}
// regex removes punctuation - NB for htmlWords case we also remove the space
if (phraseWords[j].toLowerCase() == htmlWords[wordIndex].innerHTML.toLowerCase().replace(/[\.,-\/#!$%\^&\*;:{}=\-_`~()\? ]/g,"")) {
potentiallyMatched.push(htmlWords[wordIndex].getAttribute('data-m'));
numWordsMatched++;
} else {
break;
}
// if the num of words matched equal the search phrase we have a winner!
if (numWordsMatched >= phraseWordsLen) {
matchedTimes = matchedTimes.concat(potentiallyMatched);
}
}
}
// display
matchedTimes.forEach(matchedTime => {
document.querySelectorAll("[data-m='"+matchedTime+"']")[0].classList.add("search-match");
});
}
window.onload = function() {
const playbackRateCtrl = document.getElementById('pbr');
const currentPlaybackRate = document.getElementById('currentPbr');
if (playbackRateCtrl !== null) {
playbackRateCtrl.addEventListener('input', function(){
currentPlaybackRate.innerHTML = playbackRateCtrl.value;
hyperplayer.playbackRate = playbackRateCtrl.value;
},false);
}
}
/*! (C) The Hyperaudio Project. MIT @license: en.wikipedia.org/wiki/MIT_License. */
/* Updated for Version 2.1 */
.hyperaudio-transcript .strikethrough {
text-decoration: line-through;
}
.hyperaudio-transcript .annotation, .hyperaudio-transcript .parannotation{
opacity: 0.7;
}
.hyperaudio-transcript .highlight {
background-color: yellow;
}
.hyperaudio-transcript .highlight.active {
background-color: lightGreen;
}
.hyperaudio-transcript header {
font-size: 200%;
}
/*.hyperaudio-transcript a {
cursor: pointer;
color: #000;
}*/
.hyperaudio-transcript a, a.link {
border: 0px;
}
.hyperaudio-transcript .read {
color: #000;
}
.hyperaudio-transcript .active > .active {
color: #000;
}
.hyperaudio-transcript .unread {
color: #777;
}
.hyperaudio-transcript .search-match {
background-color: pink;
}
.hyperaudio-transcript .share-match {
background-color: #66ffad;
}
.hyperaudio-transcript sub:before {
content: '\231C';
}
.hyperaudio-transcript sub.highlight-duration:before {
content: '\231D';
}
.hyperaudio-transcript h5 {
font-size: 130%;
}
[data-m] {
cursor: pointer;
}
body {
font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif;
line-height: 1.5;
}
.speaker {
font-weight: bold;
}
/*! (C) The Hyperaudio Project. MIT @license: en.wikipedia.org/wiki/MIT_License. */
/*! Version 2.1.5 */
'use strict';
function nativePlayer(instance) {
this.player = instance.player;
this.player.addEventListener('pause', instance.pausePlayHead, false);
this.player.addEventListener('play', instance.preparePlayHead, false);
this.paused = true;
this.getTime = () => {
return new Promise((resolve) => {
resolve(this.player.currentTime);
});
}
this.setTime = (seconds) => {
this.player.currentTime = seconds;
}
this.play = () => {
this.player.play();
this.paused = false;
}
this.pause = () => {
this.player.pause();
this.paused = true;
}
}
function soundcloudPlayer(instance) {
this.player = SC.Widget(instance.player.id);
this.player.bind(SC.Widget.Events.PAUSE, instance.pausePlayHead);
this.player.bind(SC.Widget.Events.PLAY, instance.preparePlayHead);
this.paused = true;
this.getTime = () => {
return new Promise((resolve) => {
this.player.getPosition(ms => {
resolve(ms / 1000);
});
});
}
this.setTime = (seconds) => {
this.player.seekTo(seconds * 1000);
}
this.play = () => {
this.player.play();
this.paused = false;
}
this.pause = () => {
this.player.pause();
this.paused = true;
}
}
function videojsPlayer(instance) {
this.player = videojs.getPlayer(instance.player.id);
this.player.addEventListener('pause', instance.pausePlayHead, false);
this.player.addEventListener('play', instance.preparePlayHead, false);
this.paused = true;
this.getTime = () => {
return new Promise((resolve) => {
resolve(this.player.currentTime());
});
}
this.setTime = (seconds) => {
this.player.currentTime(seconds);
}
this.play = () => {
this.player.play();
this.paused = false;
}
this.pause = () => {
this.player.pause();
this.paused = true;
}
}
function vimeoPlayer(instance) {
const iframe = document.querySelector('iframe');
this.player = new Vimeo.Player(iframe);
this.player.setCurrentTime(0);
this.paused = true;
this.player.ready().then(instance.checkPlayHead);
this.player.on('play',instance.preparePlayHead);
this.player.on('pause',instance.pausePlayHead);
this.getTime = () => {
return new Promise((resolve) => {
resolve(this.player.getCurrentTime());
});
}
this.setTime = (seconds) => {
this.player.setCurrentTime(seconds);
}
this.play = () => {
this.player.play();
this.paused = false;
}
this.pause = () => {
this.player.pause();
this.paused = true;
}
}
function youtubePlayer(instance) {
const tag = document.createElement('script');
tag.id = 'iframe-demo';
tag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
this.paused = true;
const previousYTEvent = window.onYouTubeIframeAPIReady;
window.onYouTubeIframeAPIReady = () => {
if (typeof previousYTEvent !== 'undefined') { // used for multiple YouTube players
previousYTEvent();
}
this.player = new YT.Player(instance.player.id, {
events: {
onStateChange: onPlayerStateChange,
},
});
};
let onPlayerStateChange = event => {
if (event.data === 1) {
// playing
instance.preparePlayHead();
this.paused = false;
} else if (event.data === 2) {
// paused
instance.pausePlayHead();
this.paused = true;
}
};
this.getTime = () => {
return new Promise((resolve) => {
resolve(this.player.getCurrentTime());
});
}
this.setTime = (seconds) => {
this.player.seekTo(seconds, true);
}
this.play = () => {
this.player.playVideo();
}
this.pause = () => {
this.player.pauseVideo();
}
}
const hyperaudioPlayerOptions = {
"native": nativePlayer,
"soundcloud": soundcloudPlayer,
"youtube": youtubePlayer,
"videojs": videojsPlayer,
"vimeo": vimeoPlayer
}
function hyperaudioPlayer(playerType, instance) {
if (playerType !== null && playerType !== undefined) {
return new playerType(instance);
} else {
console.warn("HYPERAUDIO LITE WARNING: data-player-type attribute should be set on player if not native, eg SoundCloud, YouTube, Vimeo, VideoJS");
}
}
class HyperaudioLite {
constructor(transcriptId, mediaElementId, minimizedMode, autoscroll, doubleClick, webMonetization, playOnClick) {
this.transcript = document.getElementById(transcriptId);
this.init(mediaElementId, minimizedMode, autoscroll, doubleClick, webMonetization, playOnClick);
}
init = (mediaElementId, minimizedMode, autoscroll, doubleClick, webMonetization, playOnClick) => {
const windowHash = window.location.hash;
const hashVar = windowHash.substring(1, windowHash.indexOf('='));
if (hashVar === this.transcript.id) {
this.hashArray = windowHash.substring(this.transcript.id.length + 2).split(',');
} else {
this.hashArray = [];
}
document.addEventListener(
'selectionchange',
() => {
const mediaFragment = this.getSelectionMediaFragment();
if (mediaFragment !== null) {
document.location.hash = mediaFragment;
}
},
false,
);
this.minimizedMode = minimizedMode;
this.textShot = '';
this.wordIndex = 0;
this.autoscroll = autoscroll;
this.scrollerContainer = this.transcript;
this.scrollerOffset = 0;
this.scrollerDuration = 800;
this.scrollerDelay = 0;
this.doubleClick = doubleClick;
this.webMonetization = webMonetization;
this.playOnClick = playOnClick;
this.highlightedText = false;
this.start = null;
this.myPlayer = null;
this.playerPaused = true;
if (this.autoscroll === true) {
this.scroller = window.Velocity || window.jQuery.Velocity;
}
//Create the array of timed elements (wordArr)
const words = this.transcript.querySelectorAll('[data-m]');
this.wordArr = this.createWordArray(words);
this.parentTag = words[0].parentElement.tagName;
this.parentElements = this.transcript.getElementsByTagName(this.parentTag);
this.player = document.getElementById(mediaElementId);
// Grab the media source and type from the first section if it exists
// and add it to the media element.
const mediaSrc = this.transcript.querySelector('[data-media-src]');
if (mediaSrc !== null && mediaSrc !== undefined) {
this.player.src = mediaSrc.getAttribute('data-media-src');
}
if (this.player.tagName == 'VIDEO' || this.player.tagName == 'AUDIO') {
//native HTML media elements
this.playerType = 'native';
} else {
//assume it is a SoundCloud or YouTube iframe
this.playerType = this.player.getAttribute('data-player-type');
}
this.myPlayer = hyperaudioPlayer(hyperaudioPlayerOptions[this.playerType], this);
this.parentElementIndex = 0;
words[0].classList.add('active');
//this.parentElements[0].classList.add('active');
let playHeadEvent = 'click';
if (this.doubleClick === true) {
playHeadEvent = 'dblclick';
}
this.transcript.addEventListener(playHeadEvent, this.setPlayHead, false);
this.transcript.addEventListener(playHeadEvent, this.checkPlayHead, false);
this.start = this.hashArray[0];
if (!isNaN(parseFloat(this.start))) {
this.highlightedText = true;
let indices = this.updateTranscriptVisualState(this.start);
let index = indices.currentWordIndex;
if (index > 0) {
this.scrollToParagraph(indices.currentParentElementIndex, index);
}
}
this.end = this.hashArray[1];
//TODO convert to binary search for below for quicker startup
if (this.start && this.end) {
for (let i = 1; i < words.length; i++) {
const wordStart = parseInt(words[i].getAttribute('data-m')) / 1000;
if (wordStart > parseFloat(this.start) && parseFloat(this.end) > wordStart) {
words[i].classList.add('share-match');
}
}
}
}; // end init
createWordArray = words => {
let wordArr = [];
words.forEach((word, i) => {
const m = parseInt(word.getAttribute('data-m'));
let p = word.parentNode;
while (p !== document) {
if (
p.tagName.toLowerCase() === 'p' ||
p.tagName.toLowerCase() === 'figure' ||
p.tagName.toLowerCase() === 'ul'
) {
break;
}
p = p.parentNode;
}
wordArr[i] = { n: words[i], m: m, p: p };
wordArr[i].n.classList.add('unread');
});
return wordArr;
};
getSelectionMediaFragment = () => {
let fragment = null;
let selection = null;
if (window.getSelection) {
selection = window.getSelection();
} else if (document.selection) {
selection = document.selection.createRange();
}
// check to see if selection is actually inside the transcript
let insideTranscript = false;
let parentElement = selection.focusNode;
while (parentElement !== null) {
if (parentElement.id === this.transcript.id) {
insideTranscript = true;
break;
}
parentElement = parentElement.parentElement;
}
if (selection.toString() !== '' && insideTranscript === true && selection.focusNode !== null && selection.anchorNode !== null) {
let fNode = selection.focusNode.parentNode;
let aNode = selection.anchorNode.parentNode;
if (aNode.tagName === "P") {
aNode = selection.anchorNode.nextElementSibling;
}
if (fNode.tagName === "P") {
fNode = selection.focusNode.nextElementSibling;
}
if (aNode.getAttribute('data-m') === null || aNode.className === 'speaker') {
aNode = aNode.nextElementSibling;
}
if (fNode.getAttribute('data-m') === null || fNode.className === 'speaker') {
fNode = fNode.previousElementSibling;
}
// if the selection starts with a space we want the next element
if(selection.toString().charAt(0) == " " && aNode !== null) {
aNode = aNode.nextElementSibling;
}
if (aNode !== null) {
let aNodeTime = parseInt(aNode.getAttribute('data-m'), 10);
let aNodeDuration = parseInt(aNode.getAttribute('data-d'), 10);
let fNodeTime;
let fNodeDuration;
if (fNode !== null && fNode.getAttribute('data-m') !== null) {
// if the selection ends in a space we want the previous element if it exists
if(selection.toString().slice(-1) == " " && fNode.previousElementSibling !== null) {
fNode = fNode.previousElementSibling;
}
fNodeTime = parseInt(fNode.getAttribute('data-m'), 10);
fNodeDuration = parseInt(fNode.getAttribute('data-d'), 10);
// if the selection starts with a space we want the next element
}
// 1 decimal place will do
aNodeTime = Math.round(aNodeTime / 100) / 10;
aNodeDuration = Math.round(aNodeDuration / 100) / 10;
fNodeTime = Math.round(fNodeTime / 100) / 10;
fNodeDuration = Math.round(fNodeDuration / 100) / 10;
let nodeStart = aNodeTime;
let nodeDuration = Math.round((fNodeTime + fNodeDuration - aNodeTime) * 10) / 10;
if (aNodeTime >= fNodeTime) {
nodeStart = fNodeTime;
nodeDuration = Math.round((aNodeTime + aNodeDuration - fNodeTime) * 10) / 10;
}
if (nodeDuration === 0 || nodeDuration === null || isNaN(nodeDuration)) {
nodeDuration = 10; // arbitary for now
}
if (isNaN(parseFloat(nodeStart))) {
fragment = null;
} else {
fragment = this.transcript.id + '=' + nodeStart + ',' + Math.round((nodeStart + nodeDuration) * 10) / 10;
}
}
}
return fragment;
};
setPlayHead = e => {
const target = e.target ? e.target : e.srcElement;
// cancel highlight playback
this.highlightedText = false;
// clear elements with class='active'
let activeElements = Array.from(this.transcript.getElementsByClassName('active'));
activeElements.forEach(e => {
e.classList.remove('active');
});
if (this.myPlayer.paused === true && target.getAttribute('data-m') !== null) {
target.classList.add('active');
target.parentNode.classList.add('active');
}
const timeSecs = parseInt(target.getAttribute('data-m')) / 1000;
this.updateTranscriptVisualState(timeSecs);
if (!isNaN(parseFloat(timeSecs))) {
this.end = null;
this.myPlayer.setTime(timeSecs);
if (this.playOnClick === true) {
this.myPlayer.play();
}
}
};
clearTimer = () => {
if (this.timer) clearTimeout(this.timer);
};
preparePlayHead = () => {
this.myPlayer.paused = false;
this.checkPlayHead();
}
pausePlayHead = () => {
this.clearTimer();
this.myPlayer.paused = true;
}
checkPlayHead = () => {
this.clearTimer();
(async (instance) => {
instance.currentTime = await instance.myPlayer.getTime();
if (instance.highlightedText === true) {
instance.currentTime = instance.start;
instance.myPlayer.setTime(instance.currentTime);
instance.highlightedText = false;
}
// no need to check status if the currentTime hasn't changed
instance.checkStatus();
})(this);
}
scrollToParagraph = (currentParentElementIndex, index) => {
let newPara = false;
let scrollNode = this.wordArr[index - 1].n.parentNode;
if (scrollNode !== null && scrollNode.tagName != 'P') {
// it's not inside a para so just use the element
scrollNode = this.wordArr[index - 1].n;
}
if (currentParentElementIndex != this.parentElementIndex) {
if (typeof this.scroller !== 'undefined' && this.autoscroll === true) {
if (scrollNode !== null) {
if (typeof this.scrollerContainer !== 'undefined' && this.scrollerContainer !== null) {
this.scroller(scrollNode, 'scroll', {
container: this.scrollerContainer,
duration: this.scrollerDuration,
delay: this.scrollerDelay,
offset: this.scrollerOffset,
});
} else {
this.scroller(scrollNode, 'scroll', {
duration: this.scrollerDuration,
delay: this.scrollerDelay,
offset: this.scrollerOffset,
});
}
} else {
// the wordlst needs refreshing
let words = this.transcript.querySelectorAll('[data-m]');
this.wordArr = this.createWordArray(words);
this.parentElements = this.transcript.getElementsByTagName(this.parentTag);
}
}
newPara = true;
this.parentElementIndex = currentParentElementIndex;
}
return(newPara);
}
checkStatus = () => {
//check for end time of shared piece
let interval = 0;
if (this.myPlayer.paused === false) {
if (this.end && parseInt(this.end) < parseInt(this.currentTime)) {
this.myPlayer.pause();
this.end = null;
} else {
let newPara = false;
//interval = 0; // used to establish next checkPlayHead
let indices = this.updateTranscriptVisualState(this.currentTime);
let index = indices.currentWordIndex;
if (index > 0) {
newPara = this.scrollToParagraph(indices.currentParentElementIndex, index);
}
//minimizedMode is still experimental - it changes document.title upon every new word
if (this.minimizedMode) {
const elements = transcript.querySelectorAll('[data-m]');
let currentWord = '';
let lastWordIndex = this.wordIndex;
for (let i = 0; i < elements.length; i++) {
if ((' ' + elements[i].className + ' ').indexOf(' active ') > -1) {
currentWord = elements[i].innerHTML;
this.wordIndex = i;
}
}
let textShot = '';
if (this.wordIndex != lastWordIndex) {
textShot = textShot + currentWord;
}
if (textShot.length > 16 || newPara === true) {
document.title = textShot;
textShot = '';
newPara = false;
}
}
if (this.wordArr[index]) {
interval = parseInt(this.wordArr[index].n.getAttribute('data-m') - this.currentTime * 1000);
}
}
if (this.webMonetization === true) {
//check for payment pointer
let activeElements = this.transcript.getElementsByClassName('active');
let paymentPointer = this.checkPaymentPointer(activeElements[activeElements.length - 1]);
if (paymentPointer !== null) {
let metaElements = document.getElementsByTagName('meta');
let wmMeta = document.querySelector("meta[name='monetization']");
if (wmMeta === null) {
wmMeta = document.createElement('meta');
wmMeta.name = 'monetization';
wmMeta.content = paymentPointer;
document.getElementsByTagName('head')[0].appendChild(wmMeta);
} else {
wmMeta.name = 'monetization';
wmMeta.content = paymentPointer;
}
}
}
this.timer = setTimeout(() => {
this.checkPlayHead();
}, interval + 1); // +1 to avoid rounding issues (better to be over than under)
} else {
this.clearTimer();
}
};
checkPaymentPointer = element => {
let paymentPointer = null;
if (typeof(element) != "undefined") {
paymentPointer = element.getAttribute('data-wm');
}
if (paymentPointer !== null) {
return paymentPointer;
} else {
let parent = null;
if (typeof element !== 'undefined') {
parent = element.parentElement;
}
if (parent === null) {
return null;
} else {
return this.checkPaymentPointer(parent);
}
}
};
updateTranscriptVisualState = (currentTime) => {
let index = 0;
let words = this.wordArr.length - 1;
// Binary search https://en.wikipedia.org/wiki/Binary_search_algorithm
while (index <= words) {
const guessIndex = index + ((words - index) >> 1); // >> 1 has the effect of halving and rounding down
const difference = this.wordArr[guessIndex].m / 1000 - currentTime; // wordArr[guessIndex].m represents start time of word
if (difference < 0) {
// comes before the element
index = guessIndex + 1;
} else if (difference > 0) {
// comes after the element
words = guessIndex - 1;
} else {
// equals the element
index = guessIndex;
break;
}
}
this.wordArr.forEach((word, i) => {
let classList = word.n.classList;
let parentClassList = word.n.parentNode.classList;
if (i < index) {
classList.add('read');
classList.remove('unread');
classList.remove('active');
parentClassList.remove('active');
} else {
classList.add('unread');
classList.remove('read');
}
});
this.parentElements = this.transcript.getElementsByTagName(this.parentTag);
//remove active class from all paras
Array.from(this.parentElements).forEach(el => {
if (el.classList.contains('active')) {
el.classList.remove('active');
}
});
// set current word and para to active
if (index > 0) {
if (this.myPlayer.paused === false) {
this.wordArr[index - 1].n.classList.add('active');
}
if (this.wordArr[index - 1].n.parentNode !== null) {
this.wordArr[index - 1].n.parentNode.classList.add('active');
}
}
// Establish current paragraph index
let currentParentElementIndex;
Array.from(this.parentElements).every((el, i) => {
if (el.classList.contains('active')) {
currentParentElementIndex = i;
return false;
}
return true;
});
let indices = {
currentWordIndex: index,
currentParentElementIndex: currentParentElementIndex,
};
return indices;
};
setScrollParameters = (duration, delay, offset, container) => {
this.scrollerContainer = container;
this.scrollerDuration = duration;
this.scrollerDelay = delay;
this.scrollerOffset = offset;
};
toggleAutoScroll = () => {
this.autoscroll = !this.autoscroll;
};
setAutoScroll = state => {
this.autoscroll = state;
};
}
// required for testing
if (typeof module !== 'undefined' && module.exports) {
module.exports = { HyperaudioLite };
}
//export default HyperaudioLite;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment