-
Notifications
You must be signed in to change notification settings - Fork 165
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Generate FILE.narrated.sozi.html syncing presentation with an audio t…
…rack This commit allows a Sozi presentation to be played in sync with a recorded narrative.flac audio file without requiring the entire presentation to be converted to a video format. This format has two benefits: 1. Presentation quality is preserved in the html+svg+js format, 2. Exported file size is kept minimal.
- Loading branch information
Showing
4 changed files
with
178 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
|
||
window.addEventListener("load", () => { | ||
const narrative = document.getElementById("narrative") | ||
|
||
const timeToSlide = (() => { | ||
const timeToSlide = new Map() | ||
let prevSlide = 0 | ||
for (const x of narrative.dataset.timeToSlide.split(",")) { | ||
const [time, slide] = x.split(":") | ||
prevSlide = parseInt((slide === undefined) ? prevSlide+1 : slide) | ||
timeToSlide.set(parseInt(time), prevSlide - 1) | ||
} | ||
return timeToSlide | ||
})() | ||
const slideToTimeIntervals = (() => { | ||
const slideToTimeIntervals = new Map() | ||
const process = (start, end, slide) => { | ||
let timeIntervals = slideToTimeIntervals.get(slide) | ||
if (timeIntervals === undefined) { | ||
timeIntervals = [] | ||
slideToTimeIntervals.set(slide, timeIntervals) | ||
} | ||
timeIntervals.push([start, end]) | ||
} | ||
let start, slide | ||
for (const [end, nextSlide] of timeToSlide) { | ||
if (start === undefined) { | ||
start = end | ||
slide = nextSlide | ||
continue | ||
} | ||
process(start, end, slide) | ||
start = end | ||
slide = nextSlide | ||
} | ||
if (start !== undefined) { | ||
process(start, Number.POSITIVE_INFINITY, slide) | ||
} | ||
return slideToTimeIntervals | ||
})() | ||
|
||
let currentSlide = 0 | ||
|
||
const updateAudio = (newSlide) => { | ||
currentSlide = newSlide | ||
const timeIntervals = slideToTimeIntervals.get(newSlide) | ||
if (timeIntervals === undefined) { | ||
narrative.pause() | ||
return | ||
} | ||
const t = narrative.currentTime | ||
let nearestTime | ||
for (const [start, end] of timeIntervals) { | ||
if (start <= t && t < end) { | ||
return | ||
} | ||
if (nearestTime === undefined || start <= t) { | ||
nearestTime = start | ||
} | ||
} | ||
narrative.currentTime = nearestTime | ||
} | ||
const updateSlide = (target) => { | ||
const t = narrative.currentTime | ||
let targetSlide = 0 | ||
for (const [time, slide] of timeToSlide) { | ||
if (time <= t) { | ||
targetSlide = slide | ||
} | ||
} | ||
if (targetSlide !== currentSlide) { | ||
currentSlide = targetSlide | ||
target.postMessage({name:"moveToFrame", args: [currentSlide]},"*") | ||
} | ||
} | ||
window.addEventListener("message", (e) => { | ||
switch(e.data.name) { | ||
case "loaded": | ||
const target = e.source | ||
target.postMessage({name:"notifyOnFrameChange"},"*") | ||
const frame = document.querySelector("iframe") | ||
setInterval(() => { | ||
frame.focus() | ||
}, 1000) | ||
narrative.ontimeupdate = () => { | ||
updateSlide(target) | ||
} | ||
break | ||
case "frameChange": | ||
updateAudio(e.data.index) | ||
break | ||
} | ||
}) | ||
}, false) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
{# This Source Code Form is subject to the terms of the Mozilla Public | ||
# License, v. 2.0. If a copy of the MPL was not distributed with this | ||
# file, You can obtain one at http://mozilla.org/MPL/2.0/. #} | ||
|
||
{% raw %} | ||
<!doctype html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
<title>{{ pres.title }}</title> | ||
<style> | ||
html, body { | ||
width: 100%; | ||
height: 100%; | ||
margin: 0; | ||
} | ||
.presentation-container { | ||
position: fixed; | ||
top: 0; | ||
left: 0; | ||
width: 100%; | ||
height: 100%; | ||
} | ||
iframe { | ||
border: none; | ||
} | ||
audio { | ||
position: fixed; | ||
right: 0; | ||
bottom: 0; | ||
width: 25%; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<iframe class="presentation-container" src="{{ soziHtml }}"> | ||
The Sozi presentation should play here. | ||
</iframe> | ||
<audio id="narrative" controls data-time-to-slide="0:1,1:3,2:6"> | ||
<source src="narrative.flac" type="audio/flac" preload="auto"> | ||
Audio HTML5 tag is not supported! | ||
</audio> | ||
{% endraw %} | ||
<script>{{'{% raw %}'}}{{ js|safe }}{{'{% endraw %}'}}</script> | ||
</body> | ||
</html> |