From 83b56d1270cdb179b64a1be51f8c1e7fe586c35e Mon Sep 17 00:00:00 2001 From: Marvin Borner Date: Mon, 1 Apr 2019 20:16:24 +0200 Subject: Added content --- plugin/audio/audio-slideshow.js | 520 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 520 insertions(+) create mode 100644 plugin/audio/audio-slideshow.js (limited to 'plugin/audio/audio-slideshow.js') diff --git a/plugin/audio/audio-slideshow.js b/plugin/audio/audio-slideshow.js new file mode 100644 index 0000000..ef70e21 --- /dev/null +++ b/plugin/audio/audio-slideshow.js @@ -0,0 +1,520 @@ +/***************************************************************** + ** Author: Asvin Goel, goel@telematique.eu + ** + ** Audio slideshow is a plugin for reveal.js allowing to + ** automatically play audio files for a slide deck. After an audio + ** file has completed playing the next slide or fragment is + ** automatically shown and the respective audio file is played. + ** If no audio file is available, a blank audio file with default + ** duration is played instead. + ** + ** Version: 0.6.1 + ** + ** License: MIT license (see LICENSE.md) + ** + ******************************************************************/ + +var RevealAudioSlideshow = window.RevealAudioSlideshow || (function () { + + // default parameters + var prefix = "audio/"; + var suffix = ".ogg"; + var textToSpeechURL = null; // no text to speech converter +// var textToSpeechURL = "http://api.voicerss.org/?key=[YOUR_KEY]&hl=en-gb&c=ogg&src="; // the text to speech converter + var defaultNotes = false; // use slide notes as default for the text to speech converter + var defaultText = false; // use slide text as default for the text to speech converter + var defaultDuration = 5; // value in seconds + var advance = 0; // advance to next slide after given time in milliseconds after audio has played, use negative value to not advance + var autoplay = false; // automatically start slideshow + var playerOpacity = .05; // opacity when the mouse is far from to the audioplayer + var startAtFragment = false; // when moving to a slide, start at the current fragment or at the start of the slide + var playerStyle = "position: fixed; bottom: 4px; left: 25%; width: 50%; height:75px; z-index: 33;"; // style used for container of audio controls + // ------------------ + + var silence; + var currentAudio = null; + var previousAudio = null; + var timer = null; + + Reveal.addEventListener('fragmentshown', function (event) { + if (timer) { + clearTimeout(timer); + timer = null; + } +//console.debug( "fragmentshown "); + selectAudio(); + }); + + Reveal.addEventListener('fragmenthidden', function (event) { + if (timer) { + clearTimeout(timer); + timer = null; + } +//console.debug( "fragmenthidden "); + selectAudio(); + }); + + Reveal.addEventListener('ready', function (event) { + setup(); +//console.debug( "ready "); + selectAudio(); + document.dispatchEvent(new CustomEvent('stopplayback')); + + }); + + Reveal.addEventListener('slidechanged', function (event) { + if (timer) { + clearTimeout(timer); + timer = null; + } +//console.debug( "slidechanged "); + var indices = Reveal.getIndices(); + if (!startAtFragment && typeof indices.f !== 'undefined' && indices.f >= 0) { + // hide fragments when slide is shown + Reveal.slide(indices.h, indices.v, -1); + } + + selectAudio(); + }); + + Reveal.addEventListener('paused', function (event) { + if (timer) { + clearTimeout(timer); + timer = null; + } + currentAudio.pause(); + }); + + Reveal.addEventListener('resumed', function (event) { + if (timer) { + clearTimeout(timer); + timer = null; + } + }); + + Reveal.addEventListener('overviewshown', function (event) { + if (timer) { + clearTimeout(timer); + timer = null; + } + currentAudio.pause(); + document.querySelector(".audio-controls").style.visibility = "hidden"; + }); + + Reveal.addEventListener('overviewhidden', function (event) { + if (timer) { + clearTimeout(timer); + timer = null; + } + document.querySelector(".audio-controls").style.visibility = "visible"; + }); + + function selectAudio(previousAudio) { + if (currentAudio) { + currentAudio.pause(); + currentAudio.style.display = "none"; + } + var indices = Reveal.getIndices(); + var id = "audioplayer-" + indices.h + '.' + indices.v; + if (indices.f != undefined && indices.f >= 0) id = id + '.' + indices.f; + currentAudio = document.getElementById(id); + if (currentAudio) { + currentAudio.style.display = "block"; + if (previousAudio) { + if (currentAudio.id != previousAudio.id) { + currentAudio.volume = previousAudio.volume; + currentAudio.muted = previousAudio.muted; +//console.debug( "Play " + currentAudio.id); + currentAudio.play(); + } + } else if (autoplay) { + currentAudio.play(); + } + + } + } + + + function setup() { + // deprecated parameters + if (Reveal.getConfig().audioPrefix) { + prefix = Reveal.getConfig().audioPrefix; + console.warn('Setting parameter "audioPrefix" is deprecated!'); + } + if (Reveal.getConfig().audioSuffix) { + suffix = Reveal.getConfig().audioSuffix; + console.warn('Setting parameter "audioSuffix" is deprecated!'); + } + if (Reveal.getConfig().audioTextToSpeechURL) { + textToSpeechURL = Reveal.getConfig().audioTextToSpeechURL; + console.warn('Setting parameter "audioTextToSpeechURL" is deprecated!'); + } + if (Reveal.getConfig().audioDefaultDuration) { + defaultDuration = Reveal.getConfig().audioDefaultDuration; + console.warn('Setting parameter "audioDefaultDuration" is deprecated!'); + } + if (Reveal.getConfig().audioAutoplay) { + autoplay = Reveal.getConfig().audioAutoplay; + console.warn('Setting parameter "audioAutoplay" is deprecated!'); + } + if (Reveal.getConfig().audioPlayerOpacity) { + playerOpacity = Reveal.getConfig().audioPlayerOpacity; + console.warn('Setting parameter "audioPlayerOpacity" is deprecated!'); + } + + // set parameters + var config = Reveal.getConfig().audio; + if (config) { + if (config.prefix != null) prefix = config.prefix; + if (config.suffix != null) suffix = config.suffix; + if (config.textToSpeechURL != null) textToSpeechURL = config.textToSpeechURL; + if (config.defaultNotes != null) defaultNotes = config.defaultNotes; + if (config.defaultText != null) defaultText = config.defaultText; + if (config.defaultDuration != null) defaultDuration = config.defaultDuration; + if (config.advance != null) advance = config.advance; + if (config.autoplay != null) autoplay = config.autoplay; + if (config.playerOpacity != null) playerOpacity = config.playerOpacity; + if (config.playerStyle != null) playerStyle = config.playerStyle; + } + + if ('ontouchstart' in window || navigator.msMaxTouchPoints) { + opacity = 1; + } + if (Reveal.getConfig().audioStartAtFragment) startAtFragment = Reveal.getConfig().audioStartAtFragment; + + // set style so that audio controls are shown on hover + var css = '.audio-controls>audio { opacity:' + playerOpacity + ';} .audio-controls:hover>audio { opacity:1;}'; + style = document.createElement('style'); + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + document.getElementsByTagName('head')[0].appendChild(style); + + silence = new SilentAudio(defaultDuration); // create the wave file + + var divElement = document.createElement('div'); + divElement.className = "audio-controls"; + divElement.setAttribute('style', playerStyle); + document.querySelector(".reveal").appendChild(divElement); + + // create audio players for all slides + var horizontalSlides = document.querySelectorAll('.reveal .slides>section'); + for (var h = 0, len1 = horizontalSlides.length; h < len1; h++) { + var verticalSlides = horizontalSlides[h].querySelectorAll('section'); + if (!verticalSlides.length) { + setupAllAudioElements(divElement, h, 0, horizontalSlides[h]); + } else { + for (var v = 0, len2 = verticalSlides.length; v < len2; v++) { + setupAllAudioElements(divElement, h, v, verticalSlides[v]); + } + } + } + } + + function getText(textContainer) { + var elements = textContainer.querySelectorAll('[data-audio-text]'); + for (var i = 0, len = elements.length; i < len; i++) { + // replace all elements with data-audio-text by specified text + textContainer.innerHTML = textContainer.innerHTML.replace(elements[i].outerHTML, elements[i].getAttribute('data-audio-text')); + } + return textContainer.textContent.trim().replace(/\s+/g, ' '); + } + + function setupAllAudioElements(container, h, v, slide) { + var textContainer = document.createElement('div'); + var text = null; + if (!slide.hasAttribute('data-audio-src')) { + // determine text for TTS + if (slide.hasAttribute('data-audio-text')) { + text = slide.getAttribute('data-audio-text'); + } else if (defaultNotes && Reveal.getSlideNotes(slide)) { + // defaultNotes + var div = document.createElement("div"); + div.innerHTML = Reveal.getSlideNotes(slide); + text = div.textContent || ''; + } else if (defaultText) { + textContainer.innerHTML = slide.innerHTML; + // remove fragments + var fragments = textContainer.querySelectorAll('.fragment'); + for (var f = 0, len = fragments.length; f < len; f++) { + textContainer.innerHTML = textContainer.innerHTML.replace(fragments[f].outerHTML, ''); + } + text = getText(textContainer); + } +// alert( h + '.' + v + ": " + text ); +// console.log( h + '.' + v + ": " + text ); + } + setupAudioElement(container, h + '.' + v, slide.getAttribute('data-audio-src'), text, slide.querySelector(':not(.fragment) > video[data-audio-controls]')); + var i = 0; + var fragments; + while ((fragments = slide.querySelectorAll('.fragment[data-fragment-index="' + i + '"]')).length > 0) { + var audio = null; + var video = null; + var text = ''; + for (var f = 0, len = fragments.length; f < len; f++) { + if (!audio) audio = fragments[f].getAttribute('data-audio-src'); + if (!video) video = fragments[f].querySelector('video[data-audio-controls]'); + // determine text for TTS + if (fragments[f].hasAttribute('data-audio-text')) { + text += fragments[f].getAttribute('data-audio-text') + ' '; + } else if (defaultText) { + textContainer.innerHTML = fragments[f].textContent; + text += getText(textContainer); + } + } +//console.log( h + '.' + v + '.' + i + ": >" + text +"<") + setupAudioElement(container, h + '.' + v + '.' + i, audio, text, video); + i++; + } + } + + function linkVideoToAudioControls(audioElement, videoElement) { + audioElement.addEventListener('playing', function (event) { + videoElement.currentTime = audioElement.currentTime; + }); + audioElement.addEventListener('play', function (event) { + videoElement.currentTime = audioElement.currentTime; + if (videoElement.paused) videoElement.play(); + }); + audioElement.addEventListener('pause', function (event) { + videoElement.currentTime = audioElement.currentTime; + if (!videoElement.paused) videoElement.pause(); + }); + audioElement.addEventListener('volumechange', function (event) { + videoElement.volume = audioElement.volume; + videoElement.muted = audioElement.muted; + }); + audioElement.addEventListener('seeked', function (event) { + videoElement.currentTime = audioElement.currentTime; + }); + + // add silent audio to video to be used as fallback + var audioSource = audioElement.querySelector('source[data-audio-silent]'); + if (audioSource) audioElement.removeChild(audioSource); + audioSource = document.createElement('source'); + var videoSilence = new SilentAudio(Math.round(videoElement.duration + .5)); // create the wave file + audioSource.src = videoSilence.dataURI; + audioSource.setAttribute("data-audio-silent", videoElement.duration); + audioElement.appendChild(audioSource, audioElement.firstChild); + } + + function setupFallbackAudio(audioElement, text, videoElement) { + // default file cannot be read + if (textToSpeechURL != null && text != null && text != "") { + var audioSource = document.createElement('source'); + audioSource.src = textToSpeechURL + encodeURIComponent(text); + audioSource.setAttribute('data-tts', audioElement.id.split('-').pop()); + audioElement.appendChild(audioSource, audioElement.firstChild); + } else { + if (!audioElement.querySelector('source[data-audio-silent]')) { + // create silent source if not yet existent + var audioSource = document.createElement('source'); + audioSource.src = silence.dataURI; + audioSource.setAttribute("data-audio-silent", defaultDuration); + audioElement.appendChild(audioSource, audioElement.firstChild); + } + } + } + + function setupAudioElement(container, indices, audioFile, text, videoElement) { + var audioElement = document.createElement('audio'); + audioElement.setAttribute('style', "position: relative; top: 20px; left: 10%; width: 80%;"); + audioElement.id = "audioplayer-" + indices; + audioElement.style.display = "none"; + audioElement.setAttribute('controls', ''); + audioElement.setAttribute('preload', 'none'); + + if (videoElement) { + // connect play, pause, volumechange, mute, timeupdate events to video + if (videoElement.duration) { + linkVideoToAudioControls(audioElement, videoElement); + } else { + videoElement.onloadedmetadata = function () { + linkVideoToAudioControls(audioElement, videoElement); + }; + } + } + audioElement.addEventListener('ended', function (event) { + if (typeof Recorder == 'undefined' || !Recorder.isRecording) { + // determine whether and when slideshow advances with next slide + var advanceNow = advance; + var slide = Reveal.getCurrentSlide(); + // check current fragment + var indices = Reveal.getIndices(); + if (typeof indices.f !== 'undefined' && indices.f >= 0) { + var fragment = slide.querySelector('.fragment[data-fragment-index="' + indices.f + '"][data-audio-advance]'); + if (fragment) { + advanceNow = fragment.getAttribute('data-audio-advance'); + } + } else if (slide.hasAttribute('data-audio-advance')) { + advanceNow = slide.getAttribute('data-audio-advance'); + } + // advance immediately or set a timer - or do nothing + if (advance == "true" || advanceNow == 0) { + var previousAudio = currentAudio; + Reveal.next(); + selectAudio(previousAudio); + } else if (advanceNow > 0) { + timer = setTimeout(function () { + var previousAudio = currentAudio; + Reveal.next(); + selectAudio(previousAudio); + timer = null; + }, advanceNow); + } + } + }); + audioElement.addEventListener('play', function (event) { + var evt = new CustomEvent('startplayback'); + evt.timestamp = 1000 * audioElement.currentTime; + document.dispatchEvent(evt); + + if (timer) { + clearTimeout(timer); + timer = null; + } + // preload next audio element so that it is available after slide change + var indices = Reveal.getIndices(); + var nextId = "audioplayer-" + indices.h + '.' + indices.v; + if (indices.f != undefined && indices.f >= 0) { + nextId = nextId + '.' + (indices.f + 1); + } else { + nextId = nextId + '.0'; + } + var nextAudio = document.getElementById(nextId); + if (!nextAudio) { + nextId = "audioplayer-" + indices.h + '.' + (indices.v + 1); + nextAudio = document.getElementById(nextId); + if (!nextAudio) { + nextId = "audioplayer-" + (indices.h + 1) + '.0'; + nextAudio = document.getElementById(nextId); + } + } + if (nextAudio) { +//console.debug( "Preload: " + nextAudio.id ); + nextAudio.load(); + } + }); + audioElement.addEventListener('pause', function (event) { + if (timer) { + clearTimeout(timer); + timer = null; + } + document.dispatchEvent(new CustomEvent('stopplayback')); + }); + audioElement.addEventListener('seeked', function (event) { + var evt = new CustomEvent('seekplayback'); + evt.timestamp = 1000 * audioElement.currentTime; + document.dispatchEvent(evt); + if (timer) { + clearTimeout(timer); + timer = null; + } + }); + + if (audioFile != null) { + // Support comma separated lists of audio sources + audioFile.split(',').forEach(function (source) { + var audioSource = document.createElement('source'); + audioSource.src = source; + audioElement.insertBefore(audioSource, audioElement.firstChild); + }); + } else { + var audioExists = false; + try { + // check if audio file exists + var xhr = new XMLHttpRequest(); + xhr.open('HEAD', prefix + indices + suffix, true); + xhr.onload = function () { + if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) { + var audioSource = document.createElement('source'); + audioSource.src = prefix + indices + suffix; + audioElement.insertBefore(audioSource, audioElement.firstChild); + audioExists = true; + } else { + setupFallbackAudio(audioElement, text, videoElement); + } + } + xhr.send(null); + } catch (error) { +//console.log("Error checking audio" + audioExists); + // fallback if checking of audio file fails (e.g. when running the slideshow locally) + var audioSource = document.createElement('source'); + audioSource.src = prefix + indices + suffix; + audioElement.insertBefore(audioSource, audioElement.firstChild); + setupFallbackAudio(audioElement, text, videoElement); + } + } + if (audioFile != null || defaultDuration > 0) { + container.appendChild(audioElement); + } + } + + +})(); + +/***************************************************************** + ** Create SilentAudio + ** based on: RIFFWAVE.js v0.03 + ** http://www.codebase.es/riffwave/riffwave.js + ** + ** Usage: + ** silence = new SilentAudio( 10 ); // create 10 seconds wave file + ** + ******************************************************************/ + +var FastBase64 = { + chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", + encLookup: [], + Init: function () { + for (var e = 0; 4096 > e; e++) this.encLookup[e] = this.chars[e >> 6] + this.chars[63 & e] + }, + Encode: function (e) { + for (var h = e.length, a = "", t = 0; h > 2;) n = e[t] << 16 | e[t + 1] << 8 | e[t + 2], a += this.encLookup[n >> 12] + this.encLookup[4095 & n], h -= 3, t += 3; + if (h > 0) { + var s = (252 & e[t]) >> 2, i = (3 & e[t]) << 4; + if (h > 1 && (i |= (240 & e[++t]) >> 4), a += this.chars[s], a += this.chars[i], 2 == h) { + var r = (15 & e[t++]) << 2; + r |= (192 & e[t]) >> 6, a += this.chars[r] + } + 1 == h && (a += "="), a += "=" + } + return a + } +}; +FastBase64.Init(); +var SilentAudio = function (e) { + function h(e) { + return [255 & e, e >> 8 & 255, e >> 16 & 255, e >> 24 & 255] + } + + function a(e) { + return [255 & e, e >> 8 & 255] + } + + function t(e) { + for (var h = [], a = 0, t = e.length, s = 0; t > s; s++) h[a++] = 255 & e[s], h[a++] = e[s] >> 8 & 255; + return h + } + + this.data = [], this.wav = [], this.dataURI = "", this.header = { + chunkId: [82, 73, 70, 70], + chunkSize: 0, + format: [87, 65, 86, 69], + subChunk1Id: [102, 109, 116, 32], + subChunk1Size: 16, + audioFormat: 1, + numChannels: 1, + sampleRate: 8e3, + byteRate: 0, + blockAlign: 0, + bitsPerSample: 8, + subChunk2Id: [100, 97, 116, 97], + subChunk2Size: 0 + }, this.Make = function (e) { + for (var s = 0; s < e * this.header.sampleRate; s++) this.data[s] = 127; + this.header.blockAlign = this.header.numChannels * this.header.bitsPerSample >> 3, this.header.byteRate = this.header.blockAlign * this.sampleRate, this.header.subChunk2Size = this.data.length * (this.header.bitsPerSample >> 3), this.header.chunkSize = 36 + this.header.subChunk2Size, this.wav = this.header.chunkId.concat(h(this.header.chunkSize), this.header.format, this.header.subChunk1Id, h(this.header.subChunk1Size), a(this.header.audioFormat), a(this.header.numChannels), h(this.header.sampleRate), h(this.header.byteRate), a(this.header.blockAlign), a(this.header.bitsPerSample), this.header.subChunk2Id, h(this.header.subChunk2Size), 16 == this.header.bitsPerSample ? t(this.data) : this.data), this.dataURI = "data:audio/wav;base64," + FastBase64.Encode(this.wav) + }, this.Make(e) +}; -- cgit v1.2.3