From a9805d3e63b0332afa97093d2d4e90221bc0e0ed Mon Sep 17 00:00:00 2001 From: John Factotum <50942278+johnfactotum@users.noreply.github.com> Date: Fri, 4 Nov 2022 11:47:27 +0000 Subject: [PATCH] EPUB: preliminary work on Media Overlays --- epub.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/epub.js b/epub.js index a93d2de..b3a2804 100644 --- a/epub.js +++ b/epub.js @@ -10,6 +10,7 @@ const NS = { ENC: 'http://www.w3.org/2001/04/xmlenc#', NCX: 'http://www.daisy.org/z3986/2005/ncx/', XLINK: 'http://www.w3.org/1999/xlink', + SMIL: 'http://www.w3.org/ns/SMIL', } const MIME = { @@ -160,12 +161,13 @@ const getMetadata = opf => { : getValue(obj, arr.find(filter))] })) - const prefix = 'rendition:' - const rendition = Object.fromEntries($$($metadata, 'meta') + const getProperties = prefix => Object.fromEntries($$($metadata, 'meta') .filter(filterAttribute('property', x => x?.startsWith(prefix))) .map(el => [el.getAttribute('property').replace(prefix, ''), getElementText(el)])) - return { metadata, rendition } + const rendition = getProperties('rendition:') + const media = getProperties('media:') + return { metadata, rendition, media } } const parseNav = (doc, resolve = f => f) => { @@ -228,6 +230,43 @@ const parseNCX = (doc, resolve = f => f) => { } } +const parseClock = str => { + if (!str) return + const parts = str.split(':').map(x => parseFloat(x)) + if (parts.length === 3) { + const [h, m, s] = parts + return h * 60 * 60 + m * 60 + s + } + if (parts.length === 2) { + const [m, s] = parts + return m * 60 + s + } + const [x, unit] = str.split(/(?=[^\d.])/) + const n = parseFloat(x) + const f = unit === 'h' ? 60 * 60 + : unit === 'min' ? 60 + : unit === 'ms' ? .001 + : 1 + return n * f +} + +const parseSMIL = (doc, resolve = f => f) => { + const { $, $$$ } = childGetter(doc, NS.SMIL) + const resolveHref = href => href ? decodeURI(resolve(href)) : null + return $$$(doc, 'par').map($par => { + const id = $($par, 'text')?.getAttribute('src')?.split('#')?.[1] + const $audio = $($par, 'audio') + return $audio ? { + id, + audio: { + src: resolveHref($audio.getAttribute('src')), + clipBegin: parseClock($audio.getAttribute('clipBegin')), + clipEnd: parseClock($audio.getAttribute('clipEnd')), + }, + } : { id } + }) +} + const isUUID = /([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})/ const getUUID = opf => { @@ -317,7 +356,7 @@ class Resources { const $$itemref = $$($spine, 'itemref') this.manifest = $$($manifest, 'item') - .map(getAttributes('href', 'id', 'media-type', 'properties')) + .map(getAttributes('href', 'id', 'media-type', 'properties', 'media-overlay')) .map(item => { item.href = resolveHref(item.href) item.properties = item.properties?.split(/\s/) @@ -636,6 +675,7 @@ export class EPUB { linear, pageSpread: getPageSpread(properties), resolveHref: href => resolveURL(href, item.href), + loadMediaOverlay: () => this.loadMediaOverlay(item), } }).filter(s => s) @@ -659,8 +699,10 @@ export class EPUB { } this.landmarks ??= this.resources.guide - const { metadata, rendition } = getMetadata(opf) + const { metadata, rendition, media } = getMetadata(opf) this.rendition = rendition + this.media = media + media.duration = parseClock(media.duration) this.dir = this.resources.pageProgressionDirection this.rawMetadata = metadata // useful for debugging, i guess @@ -709,6 +751,14 @@ export class EPUB { const str = await this.loadText(item.href) return this.parser.parseFromString(str, item.mediaType) } + async loadMediaOverlay(item) { + const id = item.mediaOverlay + if (!id) return null + const media = this.resources.getItemByID(id) + const doc = await this.#loadXML(media.href) + const parsed = parseSMIL(doc, url => resolveURL(url, media.href)) + return parsed + } resolveCFI(cfi) { return this.resources.resolveCFI(cfi) }