Skip to content

Commit

Permalink
Merge pull request #12752 from rtibbles/blooming_ell
Browse files Browse the repository at this point in the history
Fix various bugs in the Bloom Player implementation
  • Loading branch information
rtibbles authored Nov 14, 2024
2 parents 458bd60 + 89cf2ea commit 4efcfe7
Show file tree
Hide file tree
Showing 10 changed files with 835 additions and 229 deletions.

Large diffs are not rendered by default.

25 changes: 14 additions & 11 deletions kolibri/core/content/static/bloom/bloomplayer.htm
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
<!doctype html>
<!-- For this file to work it must have a param named "url" which points the folder containing the bloom book. For example,
<iframe src="\bloomplayer.htm?url='https:\\example.com\mybook"></iframe> -->

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body style="background-color: #2e2e2e">
<div id="root"><span style="color: #d65649">Loading Bloom Player...</span></div>
<script
type="text/javascript"
src="bloomPlayer-d1be6dab31acb4a956a6.min.js"
></script>
</body>
<head>
<meta charset="UTF-8" />
</head>
<body style="background-color: #2e2e2e;">
<div id="root"></span></div>
<script type="text/javascript" src="bloomPlayer-6034695c267e8cb6644a.min.js"></script></body>
<!-- At build time, we insert a script tag pointing at bloomPlayer.min.js but with a hash created at build time,
to avoid getting a stale version from a cache (while allowing us to cache it for a long time).
It has a style sheet which sets the same color on the body, but it takes a while to fetch and load,
so we reduce flicker by setting a background color explicitly. -->
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,6 @@
return 300;
},
/* eslint-enable vue/no-unused-properties */
entry() {
return (this.options && this.options.entry) || 'index.htm';
},
isBloom() {
return this.defaultFile.extension === 'bloompub';
},
},
watch: {
userData(newValue) {
Expand All @@ -147,11 +141,11 @@
const hashiProgress = data.progress;
if (hashiProgress !== null && !this.forceDurationBasedProgress) {
this.$emit('updateProgress', hashiProgress);
if (hashiProgress >= 1) {
this.$emit('finished');
}
}
});
this.hashi.on('navigateTo', message => {
this.$emit('navigateTo', message);
});
this.hashi.on(this.hashi.events.RESIZE, scrollHeight => {
this.iframeHeight = scrollHeight;
});
Expand All @@ -162,58 +156,21 @@
this.loading = false;
this.$emit('error', err);
});
let storageUrl = this.defaultFile.storage_url;
if (!this.isBloom) {
// In the case that this is being routed via a remote URL
// ensure we preserve that for the zip endpoint.
const url = new URL(this.defaultFile.storage_url, window.location.href);
const baseurl = url.searchParams.get('baseurl');
storageUrl = urls.zipContentUrl(
this.defaultFile.checksum,
this.defaultFile.extension,
this.entry,
baseurl ? encodeURIComponent(baseurl) : undefined,
);
}
this.hashi.initialize(
(this.extraFields && this.extraFields.contentState) || {},
this.userData,
storageUrl,
this.defaultFile.storage_url,
this.defaultFile.checksum,
);
this.$emit('startTracking');
if (!this.isBloom) {
this.pollProgress();
}
},
beforeDestroy() {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.$emit('stopTracking');
},
methods: {
recordProgress() {
let progress;
if (this.forceDurationBasedProgress) {
progress = this.durationBasedProgress;
} else {
const hashiProgress = this.hashi ? this.hashi.getProgress() : null;
progress = hashiProgress === null ? this.durationBasedProgress : hashiProgress;
}
this.$emit('updateProgress', progress);
if (progress >= 1) {
this.$emit('finished');
}
this.pollProgress();
},
pollProgress() {
this.timeout = setTimeout(() => {
this.recordProgress();
}, 5000);
},
},
$trs: {
exitFullscreen: {
message: 'Exit fullscreen',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@
const hashiProgress = this.hashi.getProgress();
if (hashiProgress !== null && !this.forceDurationBasedProgress) {
this.$emit('updateProgress', hashiProgress);
if (hashiProgress >= 1) {
this.$emit('finished');
}
}
});
this.hashi.on('navigateTo', message => {
Expand Down
8 changes: 8 additions & 0 deletions packages/hashi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,11 @@ H5P Static Files
This code is currently generated from https://github.com/h5p/h5p-php-library

To update, update the `h5pCommit` variable in `downloadH5PVendor.js` to the desired tag and then run `yarn run build-h5p`.


Bloom Reader Static Files
-------------------------

This code is currently generated from https://github.com/learningequality/bloom-player (specifically the 'patched' default branch).

To regenerate, the repository should be cloned, and `yarn run build` run within the context of that repository to regenerate the new assets. All the files put into `dist` should then be copied into `kolibri/core/content/static/bloom` in the Kolibri repository. Any previously existing hash named files can be deleted and replaced by the new hash named files.
5 changes: 5 additions & 0 deletions packages/hashi/src/Bloom/BloomInterface.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default class Bloom extends BaseShim {
this.on(this.events.STATEUPDATE, this.__setData);
this.on(this.events.USERDATAUPDATE, this.__setUserData);
this.on(this.events.BLOOMPAGESREAD, this.__getProgress);
this._hasBeenFlaggedAsComplete = false;
}

init(iframe, filepath) {
Expand All @@ -39,6 +40,10 @@ export default class Bloom extends BaseShim {
let progress = this.userData.progress || 0;
if (data.totalNumberedPages) {
progress = (data.audioPages + data.nonAudioPages + data.videoPages) / data.totalNumberedPages;
if (!this._hasBeenFlaggedAsComplete && progress >= 1) {
progress = 0.95;
}
this._hasBeenFlaggedAsComplete = data.lastNumberedPageRead;
this.userData.progress = progress;
}
this.__mediator.sendMessage({
Expand Down
170 changes: 103 additions & 67 deletions packages/hashi/src/Bloom/BloomRunner.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,89 @@
import ZipFile from 'kolibri-zip';
import { strToU8 } from 'fflate';
import {
getAudioId,
getDOMPaths,
getStyleUrlPaths,
replaceAudioId,
replaceDOMPaths,
replaceStyleUrlPaths,
} from 'kolibri-zip/src/fileUtils';
import { DOMMapper, defaultFilePathMappers } from 'kolibri-zip/src/fileUtils';
import { events } from '../hashiBase';

const CONTENT_ID = '1234567890';
const domParser = new DOMParser();

const domSerializer = new XMLSerializer();

const audioSentenceSelector = '.audio-sentence';

const backgroundAudioSelector = '[data-backgroundaudio]';

function getAudioFiles(fileContents, mimeType) {
const dom = domParser.parseFromString(fileContents.trim(), mimeType);
const audioSentenceElements = dom.querySelectorAll(audioSentenceSelector);
const audioSentencePaths = Array.from(audioSentenceElements).map(element => {
const value = element.getAttribute('id');
// By convention all audio files are in a folder called "audio"
// and have a .mp3 extension.
return `audio/${value}.mp3`;
});
const backgroundAudioElements = dom.querySelectorAll(backgroundAudioSelector);
const backgroundAudioPaths = Array.from(backgroundAudioElements).map(element => {
const value = element.getAttribute('data-backgroundaudio');
// These files already have their extension, so we don't need to add it.
return `audio/${value}`;
});
return audioSentencePaths.concat(backgroundAudioPaths);
}

function _setDehydratedUrlAttribute(element, attributeName, url) {
// We have seen cases where the audio file simply isn't in the archive, so we need to check
// if the URL is null before setting it.
if (!url) {
return;
}
// We cannot set the fully qualified URL as the attribute here, as it breaks subsequent
// attempts to use the id as a DOM selector by Bloom player.
// The URL is rehydrated inside Bloom player, thanks to our code modifications there.
element.setAttribute(attributeName, `_${url.split('/').at(-1)}`);
}

function replaceAudioFiles(fileContents, packageFiles, mimeType) {
const dom = domParser.parseFromString(fileContents.trim(), mimeType);
const audioSentenceElements = dom.querySelectorAll(audioSentenceSelector);
for (const element of audioSentenceElements) {
const id = element.getAttribute('id');
const url = packageFiles[`audio/${id}.mp3`];
_setDehydratedUrlAttribute(element, 'id', url);
}
const backgroundAudioElements = dom.querySelectorAll(backgroundAudioSelector);
for (const element of backgroundAudioElements) {
const id = element.getAttribute('data-backgroundaudio');
const url = packageFiles[`audio/${id}`];
_setDehydratedUrlAttribute(element, 'data-backgroundaudio', url);
}
if (mimeType === 'text/html') {
// Remove the namespace attribute from the root element
// as serializeToString adds it by default and without this
// it gets repeated.
dom.documentElement.removeAttribute('xmlns');
}
return domSerializer.serializeToString(dom);
}

class BloomDOMMapper extends DOMMapper {
getPaths() {
const paths = super.getPaths();
return paths.concat(getAudioFiles(this.file.toString(), this.file.mimeType));
}

replacePaths(packageFiles) {
const newFileContents = super.replacePaths(packageFiles);
return replaceAudioFiles(newFileContents, packageFiles, this.file.mimeType);
}
}

// Override the default file path mappers to include our custom BloomDOMMapper
// which handles the special audio file references.
const filePathMappers = {
...defaultFilePathMappers,
htm: BloomDOMMapper,
html: BloomDOMMapper,
xml: BloomDOMMapper,
xhtml: BloomDOMMapper,
};

/*
* Class that manages loading, parsing, and running an Bloom file.
Expand Down Expand Up @@ -38,19 +111,12 @@ export default class BloomRunner {
this.iframe = iframe;
// This is the path to the Bloompub file which we load in its entirety.
this.filepath = filepath;
// A fallback URL to the zipcontent endpoint for this H5P file
this.zipcontentUrl = new URL(
`../../zipcontent/${this.filepath.substring(this.filepath.lastIndexOf('/') + 1)}`,
window.location,
).href;
// Callback to call when Bloom Player has finished loading
this.loaded = loaded;
// Callback to call when Bloom errors
this.errored = errored;
this.contentNamespace = CONTENT_ID;
this.zip = new ZipFile(this.filepath);
this.zip = new ZipFile(this.filepath, { filePathMappers });
return this.processFiles().then(() => {
this.processContent();
if (this.iframe.contentDocument && this.iframe.contentDocument.readyState === 'complete') {
return this.initBloom();
}
Expand All @@ -66,67 +132,37 @@ export default class BloomRunner {
initBloom() {
try {
this.loaded();
this.iframe.src = `../bloom/bloomplayer.htm?url=${this.contentUrl}&distributionUrl=${this.distributionUrl}&metaJsonUrl=${this.metaUrl}&independent=false`;
const options = new URLSearchParams({
url: this.contentUrl,
distributionUrl: this.distributionUrl,
metaJsonUrl: this.metaUrl,
independent: false,
hideFullScreenButton: true,
initiallyShowAppBar: true,
allowToggleAppBar: false,
});

this.iframe.src = `../bloom/bloomplayer.htm?${options.toString()}`;
} catch (e) {
this.errored(e);
}
}

processContent() {
const domPaths = getDOMPaths(this.contentfile.toString(), this.contentfile.mimeType).filter(
file => !file.startsWith('blob:'),
);
const stylePaths = getStyleUrlPaths(this.contentfile.toString(), this.contentfile.mimeType);
const files = [...new Set([...domPaths, ...stylePaths])];
const audioIds = getAudioId(this.contentfile.toString(), this.contentfile.mimeType);
const replacementFileMap = {};
if (files.length > 0 || audioIds.length > 0) {
for (const file of this.packageFiles) {
if (files.includes(file.name)) {
replacementFileMap[file.name] = file.toUrl();
}
if (files.includes(encodeURI(file.name))) {
replacementFileMap[encodeURI(file.name)] = file.toUrl();
}
const audioFile = file.name.substring(6);
const audioFileName = audioFile.split('.')[0];
const url = file.toUrl();
if (audioIds.includes(audioFileName)) {
replacementFileMap[audioFileName] = `_${url.split('/').at(-1)}`;
}
if (audioIds.includes(audioFile)) {
replacementFileMap[audioFile] = `_${url.split('/').at(-1)}`;
}
}
}
let newHtmlFile = replaceDOMPaths(
this.contentfile.toString(),
replacementFileMap,
this.contentfile.mimeType,
);
newHtmlFile = replaceStyleUrlPaths(newHtmlFile, replacementFileMap, this.contentfile.mimeType);
newHtmlFile = replaceAudioId(newHtmlFile, replacementFileMap, this.contentfile.mimeType);

this.contentfile.obj = strToU8(newHtmlFile);
this.contentUrl = this.contentfile.toUrl();
}

/*
* Process all files in the zip, content files and files in the packages
* Get the htm file, distribution file, and meta.json file from the zip.
*/
processFiles() {
return Promise.all([
this.zip.files('').then(files => {
this.packageFiles = files;
}),
// The htm file does not have a predictable name, so we find
// the first one in the zip.
this.zip.filesFromExtension('.htm').then(htmFile => {
this.contentfile = htmFile[0];
this.contentUrl = htmFile[0].toUrl();
}),
this.zip.filesFromExtension('.distribution').then(distributionFile => {
this.distributionUrl = distributionFile[0].toUrl();
this.zip.file('.distribution').then(distributionFile => {
this.distributionUrl = distributionFile.toUrl();
}),
this.zip.filesFromExtension('meta.json').then(meta => {
this.metaUrl = meta[0].toUrl();
this.zip.file('meta.json').then(meta => {
this.metaUrl = meta.toUrl();
}),
]);
}
Expand Down
1 change: 0 additions & 1 deletion packages/hashi/src/bloom.js

This file was deleted.

Loading

0 comments on commit 4efcfe7

Please sign in to comment.