From 5b6babf66e51cb819062eb77033856a470458a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janis=20Daniel=20Da=CC=88hne?= Date: Sat, 21 May 2022 23:24:07 +0200 Subject: [PATCH 1/5] - started sync video plugin --- js/config.js | 4 +++- plugin/videoSync/plugin.js | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 plugin/videoSync/plugin.js diff --git a/js/config.js b/js/config.js index 77da3057370..f5714d910aa 100644 --- a/js/config.js +++ b/js/config.js @@ -278,6 +278,8 @@ export default { // The display mode that will be used to show slides display: 'block', + syncStartVideoFromSpeakView: true, + // Hide cursor if inactive hideInactiveCursor: true, @@ -290,4 +292,4 @@ export default { // Plugin objects to register and use for this presentation plugins: [] -} \ No newline at end of file +} diff --git a/plugin/videoSync/plugin.js b/plugin/videoSync/plugin.js new file mode 100644 index 00000000000..b2b19efc932 --- /dev/null +++ b/plugin/videoSync/plugin.js @@ -0,0 +1,45 @@ +/*! + * reveal.js Zoom plugin + */ +const Plugin = () => { + + let isSpeakerView = false; //true: presenter/speaker view, false: main view + let isSpeakerPreviewFrame = false; //we only want to zoom in the main presenter frame (not presenter preview frame) + let broadcastChannel = new BroadcastChannel('zoom_channel'); + + return { + id: 'videoSync', + + init: function( reveal ) { + + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + + isSpeakerView = params.hasOwnProperty('receiver') + + //present html has a main view (iframe) and a preview next frame (iframe) + //preview don't have controls (so they are explicitly hidden) + isSpeakerPreviewFrame = isSpeakerView && params.hasOwnProperty('controls') + + if (isSpeakerView) { + if (isSpeakerPreviewFrame) { + console.log(`preview`) + console.log(document) + console.log(document.querySelectorAll(`video`)) + } else { + console.log(`speaker`) + console.log(document) + console.log(document.querySelectorAll(`video`)) + } + } + + }, + + destroy: () => { + + } + + } +}; + +export default Plugin; From f076bcdc744b3e51b37804fb1fed2836ce5a35a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janis=20Daniel=20Da=CC=88hne?= Date: Sat, 21 May 2022 23:24:18 +0200 Subject: [PATCH 2/5] - add build --- gulpfile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index cd75adf06fa..2cac4ada6df 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -127,6 +127,7 @@ gulp.task('plugins', () => { { name: 'RevealNotes', input: './plugin/notes/plugin.js', output: './plugin/notes/notes' }, { name: 'RevealZoom', input: './plugin/zoom/plugin.js', output: './plugin/zoom/zoom' }, { name: 'RevealMath', input: './plugin/math/plugin.js', output: './plugin/math/math' }, + { name: 'RevealVideoSync', input: './plugin/videoSync/plugin.js', output: './plugin/videoSync/videoSync' }, ].map( plugin => { return rollup({ cache: cache[plugin.input], @@ -316,4 +317,4 @@ gulp.task('serve', () => { gulp.watch(['test/*.html'], gulp.series('test')) -}) \ No newline at end of file +}) From 4d4b2e86c8f7908e3b4efa4a92834bc12e33b10a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janis=20Daniel=20Da=CC=88hne?= Date: Sat, 4 Jun 2022 11:29:36 +0200 Subject: [PATCH 3/5] - added sync plugin (currently only for video sync) --- gulpfile.js | 2 +- js/config.js | 2 + plugin/sync/plugin.js | 185 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 plugin/sync/plugin.js diff --git a/gulpfile.js b/gulpfile.js index 2cac4ada6df..b708a27528c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -127,7 +127,7 @@ gulp.task('plugins', () => { { name: 'RevealNotes', input: './plugin/notes/plugin.js', output: './plugin/notes/notes' }, { name: 'RevealZoom', input: './plugin/zoom/plugin.js', output: './plugin/zoom/zoom' }, { name: 'RevealMath', input: './plugin/math/plugin.js', output: './plugin/math/math' }, - { name: 'RevealVideoSync', input: './plugin/videoSync/plugin.js', output: './plugin/videoSync/videoSync' }, + { name: 'RevealSync', input: './plugin/sync/plugin.js', output: './plugin/sync/sync' }, ].map( plugin => { return rollup({ cache: cache[plugin.input], diff --git a/js/config.js b/js/config.js index f5714d910aa..fcb20df2f6c 100644 --- a/js/config.js +++ b/js/config.js @@ -286,6 +286,8 @@ export default { // Time before the cursor is hidden (in ms) hideCursorTime: 5000, + syncVideoFromSpeakView: true, + // Script dependencies to load dependencies: [], diff --git a/plugin/sync/plugin.js b/plugin/sync/plugin.js new file mode 100644 index 00000000000..d8496d7668f --- /dev/null +++ b/plugin/sync/plugin.js @@ -0,0 +1,185 @@ +/*! + * reveal.js sync plugin + */ + +const Plugin = () => { + + let isSpeakerView = false; //true: presenter/speaker view, false: main view + let isSpeakerPreviewFrame = false; //we only want to zoom in the main presenter frame (not presenter preview frame) + let broadcastChannel = new BroadcastChannel('sync_channel'); + + /** + * @typedef {Object} SyncMessage + * @property {("play"|"pause"|"seeked"|"ratechange")} action + * @property {string} xpathToVideo + * @property {number} volume + * @property {number} currentTime + * @property {number} playbackRate + */ + + + //from https://stackoverflow.com/questions/2661818/javascript-get-xpath-of-a-node + /** + * returns a dom node from a xpath expression + * @param xpath the xpath + * @returns {Node} the found node + */ + function getElementByXpath(xpath) { + return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; + } + + //from https://stackoverflow.com/questions/2661818/javascript-get-xpath-of-a-node + //modified to not use ids (because they might be not unique) + /** + * returns the xpath for a dom element + * @param domElement the dom element + * @returns {string} the xpath to the element + */ + function getXPathForElement(domElement) { + const idx = (sib, name) => sib + ? idx(sib.previousElementSibling, name || sib.localName) + (sib.localName === name) + : 1; + const segs = elm => !elm || elm.nodeType !== 1 + ? [''] + : [...segs(elm.parentNode), elm instanceof HTMLElement + ? `${elm.localName}[${idx(elm)}]` + : `*[local-name() = "${elm.localName}"][${idx(elm)}]`]; + return segs(domElement).join('/'); + } + + return { + id: 'videoSync', + + init: function (reveal) { + + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + + isSpeakerView = params.hasOwnProperty('receiver'); + + //present html has a main view (iframe) and a preview next frame (iframe) + //preview don't have controls (so they are explicitly hidden) + isSpeakerPreviewFrame = isSpeakerView && params.hasOwnProperty('controls') + + if (isSpeakerView) { + if (!isSpeakerPreviewFrame) { + + let sendMessage = (videoEl, action) => { + + if (!reveal.getConfig().syncVideoFromSpeakView) { + return + } + + let xpathToVideo = null; + + try { + xpathToVideo = getXPathForElement(videoEl); + } catch (err) { + console.error(err); + return + } + + /** @type {SyncMessage} */ + const msg = { + action: action, + xpathToVideo: xpathToVideo, + volume: videoEl.volume, + currentTime: videoEl.currentTime, + playbackRate: videoEl.playbackRate + } + broadcastChannel.postMessage(msg); + } + + const allVideoEls = document.querySelectorAll(`video`) + for (let i = 0; i < allVideoEls.length; i++) { + /** @type {HTMLVideoElement} */ + const videoEl = allVideoEls[i] + //speaker video should be muted + videoEl.muted = true + //ensure at least the speaker has controls + videoEl.controls = true + videoEl.addEventListener(`play`, (e) => { + sendMessage(videoEl, `play`) + }) + videoEl.addEventListener(`pause`, (e) => { + sendMessage(videoEl, `pause`) + }) + //actually speaker video should be muted... + //does not handle mute/unmute + // videoEl.addEventListener(`volumechange`, (e) => { + // sendMessage(videoEl,`volumechange`) + // }) + // videoEl.addEventListener(`seeking`, (e) => { + // sendMessage(videoEl,`seeking`) + // }) + videoEl.addEventListener(`seeked`, (e) => { + sendMessage(videoEl, `seeked`) + }) + videoEl.addEventListener(`ratechange`, (e) => { + sendMessage(videoEl, `ratechange`) + }) + } + } + } else { + + broadcastChannel.addEventListener('message', (event) => { + + if (!reveal.getConfig().syncVideoFromSpeakView) { + return + } + + /** @type {SyncMessage} */ + let message = event.data; + + let xpathToVideo = message.xpathToVideo; + /** @type {HTMLVideoElement} */ + let video = null + + try { + video = getElementByXpath(xpathToVideo); + } catch (err) { + console.error(err) + return + } + if (!(video instanceof HTMLVideoElement)) { + console.error(`videoSync: video not found for xpath ${xpathToVideo}`) + return + } + + switch (message.action) { + case "play": { + video.play() + break; + } + case "pause": { + video.pause() + break; + } + case "seeked": { + video.currentTime = message.currentTime + break + } + case "ratechange": { + video.playbackRate = message.playbackRate + break + } + // case "volumechange": { + // video.volume = message.volume + // break; + // } + } + + }) + + } + + }, + + destroy: () => { + + } + + } +}; + +export default Plugin; From af82e9b313b078a205a265174ab4fccb03d0caf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janis=20Daniel=20Da=CC=88hne?= Date: Sat, 4 Jun 2022 11:30:49 +0200 Subject: [PATCH 4/5] - removed video sync old file --- plugin/videoSync/plugin.js | 45 -------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 plugin/videoSync/plugin.js diff --git a/plugin/videoSync/plugin.js b/plugin/videoSync/plugin.js deleted file mode 100644 index b2b19efc932..00000000000 --- a/plugin/videoSync/plugin.js +++ /dev/null @@ -1,45 +0,0 @@ -/*! - * reveal.js Zoom plugin - */ -const Plugin = () => { - - let isSpeakerView = false; //true: presenter/speaker view, false: main view - let isSpeakerPreviewFrame = false; //we only want to zoom in the main presenter frame (not presenter preview frame) - let broadcastChannel = new BroadcastChannel('zoom_channel'); - - return { - id: 'videoSync', - - init: function( reveal ) { - - const urlSearchParams = new URLSearchParams(window.location.search); - const params = Object.fromEntries(urlSearchParams.entries()); - - isSpeakerView = params.hasOwnProperty('receiver') - - //present html has a main view (iframe) and a preview next frame (iframe) - //preview don't have controls (so they are explicitly hidden) - isSpeakerPreviewFrame = isSpeakerView && params.hasOwnProperty('controls') - - if (isSpeakerView) { - if (isSpeakerPreviewFrame) { - console.log(`preview`) - console.log(document) - console.log(document.querySelectorAll(`video`)) - } else { - console.log(`speaker`) - console.log(document) - console.log(document.querySelectorAll(`video`)) - } - } - - }, - - destroy: () => { - - } - - } -}; - -export default Plugin; From d5e16c5b435359e5bd721641fab4aede9610d1df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janis=20Daniel=20Da=CC=88hne?= Date: Sat, 4 Jun 2022 11:39:49 +0200 Subject: [PATCH 5/5] - removed old config option --- js/config.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/config.js b/js/config.js index fcb20df2f6c..e33131b4042 100644 --- a/js/config.js +++ b/js/config.js @@ -278,8 +278,6 @@ export default { // The display mode that will be used to show slides display: 'block', - syncStartVideoFromSpeakView: true, - // Hide cursor if inactive hideInactiveCursor: true,