class Slide { constructor(slide, idx) { this.isAudioLoaded = this.isVideoLoaded = false this.el = slide this.idx = idx this.duration = slide.dataset.duration this.durationMs = parseInt(this.duration) * 1000 const video = slide.querySelector('.video') if (video) { this.video = load_urls(video.dataset) if (this.video.length > 1 || this.video[0].includes('document')) { this.isDocument = true } this.videoVolume = video.dataset.volume ? parseFloat(video.dataset.volume) : 1 this.videoContainer = video if (this.video.length > 1) { this.zooms = this.video.map(url => url.split('/').pop().split('#')[0].split(',').map(a => Math.round(a))) } } const audio = slide.querySelector('.audio') if (audio) { this.audio = audio.dataset.url // audio does not need to be an array this.audioVolume = audio.dataset.volume ? parseFloat(audio.dataset.volume) : 1 this.audioContainer = audio this.audioContinue = !!audio.dataset.continue } this.initEmbeds() return this } initEmbeds() { if (this.video) { const videoEmbed = this.videoEmbed = new PandoraEmbed({ id: 'slide-' + this.idx, url: this.video[0], container: this.videoContainer }) videoEmbed.on('init', () => { if (this.isDocument) { this.isVideoLoaded = true } }) videoEmbed.on('loaded', () => { console.log('loaded called on embed') this.isVideoLoaded = true }) videoEmbed.on('playing', (positionData) => { if (!this.videoStart) { this.videoStart = positionData.position } }) } if (this.audio) { const audioEmbed = this.audioEmbed = new PandoraEmbed({ id: 'audio-' + this.idx, url: this.audio, container: this.audioContainer }) audioEmbed.on('loaded', () => { console.log('loaded called on audio embed') this.isAudioLoaded = true }) audioEmbed.on('playing', (positionData) => { if (!this.audioStart) { this.audioStart = positionData.position } }) } return this; } isReady() { console.log('isReady called'); if (this.video && this.audio) { return this.isVideoLoaded && this.isAudioLoaded } else if (this.video) { return this.isVideoLoaded } else if (this.audio) { return this.isAudioLoaded } else { return true } } sendToBack() { this.el.style.zIndex = 0 return this } bringToFront() { this.el.style.zIndex = 10 return this } start() { console.log('called start', this.isReady()) this.timeout = setTimeout(next, this.durationMs) this.startTime = new Date() if (this.zooms) { this.startZoom() } else if (this.video && this.video.length) { this.startVideo() } if (this.audioContainer) { // if there is an audio container, we always stop current audio reset_active_audio() } if (this.audio) { this.startAudio() } return this } stop() { if (this.zooms) { this.resetZoom() } else if (this.video && this.video.length) { this.resetVideo() } if (this.audio && !this.audioContinue) { this.resetAudio() } return this } pause() { clearTimeout(this.timeout) this.pauseTime = new Date() if (this.zooms) { this.pauseZoom() } else if (this.video && this.video.length) { this.pauseVideo() } if (this.audio) { this.pauseAudio() } } resume() { const offset = this.pauseTime - this.startTime const timeout = this.durationMs - offset this.timeout = setTimeout(next, timeout) // "Fake" the start time in case the user pauses and resumes again. // NOTE: This seems like a bit of a hack, but the other way seemed convoluted this.startTime = new Date() - offset if (this.zooms) { this.resumeZoom() } else if (this.video && this.video.length) { this.resumeVideo() } if (this.audio) { this.resumeAudio() } } startZoom(offset) { offset = offset || 0 for (var i=1; i { const timeout = Math.round(1000 * (this.duration / this.zooms.length) * j) - offset if (timeout < 0) return this.zoomTimeouts.push(setTimeout(() => { this.videoEmbed.postMessage('options', { 'area': this.zooms[j] }) }, timeout)) })(i); } return this } startVideo() { this.videoEmbed.postMessage('options', { 'paused': false, 'volume': this.videoVolume }) return this } startAudio() { this.audioEmbed.postMessage('options', { 'paused': false, 'volume': this.audioVolume }) activeAudio = this return this } pauseZoom() { this.zoomTimeouts.forEach(clearTimeout) return this } pauseVideo() { this.videoEmbed.postMessage('options', { 'paused': true }) return this } pauseAudio() { this.audioEmbed.postMessage('options', { 'paused': true }) return this } resumeZoom() { const offset = this.pauseTime - this.startTime this.startZoom(offset) return this } resumeVideo() { this.videoEmbed.postMessage('options', { 'paused': false }) return this } resumeAudio() { this.audioEmbed.postMessage('options', { 'paused': false }) return this } resetZoom() { this.zoomTimeouts.forEach(clearTimeout) this.videoEmbed.postMessage('options', { 'area': this.zooms[0] }) return this } resetVideo() { this.videoEmbed.postMessage('options', { 'paused': true, 'position': this.start || 0 }) return this } resetAudio() { this.audioEmbed.postMessage('options', { 'paused': true, 'position': 0 //FIXME: figure correct audio reset }) activeAudio = null; return this } } var slides = [], current = 0, activeAudio, timeout; let globalIsPaused = false let textbUrl = new URLSearchParams(window.location.search).get('textb') || 'https://textb.org/r/housingplaylist2/' // use raw version of page textbUrl = textbUrl.replace('/t/', '/r/') if (!textbUrl.endsWith('/')) textbUrl += '/' fetch(textbUrl) .then(response => response.text()) .then(loadYaml) .then(init) .catch(err => { console.log('error', err) alert('error loading YAML') }) function loadYaml(txt) { const html = txt.split('\n\n') .filter(chunk => chunk.trim()) .map(chunk => { const obj = jsyaml.load(chunk) return obj }) .map(jsonToHTML) .join('') document.body.innerHTML = `
${html} ` return true } function globalPause() { if (globalIsPaused) return; slides[current].pause() globalIsPaused = true } function globalResume() { if (!globalIsPaused) return; slides[current].resume() globalIsPaused = false } function togglePause() { if (globalIsPaused) { globalResume() } else { globalPause() } } function init() { document.querySelectorAll('.slide').forEach(function(slide, idx) { slides.push(new Slide(slide, idx)) }) go(0) } function go(idx) { if (!slides[idx].isReady()) { console.log('slide not ready'); return setTimeout(() => { go(idx) }, 250) } var old = current slides[current].sendToBack() slides[idx].bringToFront() current = idx if (timeout) { clearTimeout(timeout) timeout = null } // timeout = setTimeout(next, Math.round(slides[current].duration) * 1000) slides[old].stop() slides[current].start() } function next() { var idx = (current + 1) % slides.length console.log(current, idx) go(idx) } function previous() { var idx = (current - 1) % slides.length if (idx < 0) { idx += slides.length } go(idx) } function load_urls(dataset) { var urls = [], idx = 0 while (dataset['url_' + idx]) { urls.push(dataset['url_' + idx]) idx += 1 } return urls } window.addEventListener('blur', function(){ setTimeout(function(){ // using the 'setTimout' to let the event pass the run loop if (document.activeElement instanceof HTMLIFrameElement) { window.focus(); } },0); }, false); function reset_active_audio() { if (activeAudio) { activeAudio.resetAudio() } } document.addEventListener('keydown', function(event) { if (event.key == 'ArrowRight') { next() event.preventDefault() } else if (event.key == 'ArrowLeft') { previous() event.preventDefault() } else if (event.keyCode === 32) { // spacebar togglePause() } }, false)