Inside Menestrello — Part 2: Architecture
RSS • Permalink • Created 26 Jun 2014 • Written by Alberto Pettarin
This is Part 2 of a "behind the scenes" tour of Menestrello app. In this post, I cover the high level architecture of Menestrello. In Part 1 I covered the motivation that made us develop this app.
The hybrid approach
Since we wanted to cover both iOS and Android, and we had no formal budget for developing the app, we immediately discarded the native code option, in favor of an hybrid approach.
Hybrid means that the app UI and logic is written in HTML5+CSS+JS, and this code is compiled into a native package by the platform. The resulting app essentially consists of a WebView/UIWebView instance (essentially, WebKit), which loads the initial page of the app and all the linked resources, including JS code.
We adopted the PhoneGap framework, compiling locally. (But soon we will switch to vanilla Cordova.)
The hybrid approach is ideal for a vast majority of apps, which have a lot of UI tricks but very simple logic, and more importantly, do not need to access low-level functions. But in our case the hybrid approach is taken to its limits, and we had to come up with a couple of clever tricks.
Before going into the details of the three main problems we faced while developing Menestrello, I want to briefly discuss the high level life cycle of the app.
The app life cycle
Ideally, we wanted to enable the user to:
- scan the device storage for EPUB files;
- see a Library of found books;
- open an Audio-eBook into a full-screen Reader;
- be free to customize her book experience;
- background the app or close the book and return to the Library.
Hence, the three key primitives are:
- Accessing the file system, looking for EPUB files and parsing their basic metadata and covers (to be displayed in the Library);
- Full parsing the EPUB assets and displaying them appropriately (when the book is open for reading+listening);
- Accurately managing the timings of audio and text highlighting (hey, this is the big point of Audio-eBooks!).
To start quickly, we assumed that the EPUB files were already present on the device. Since iOS apps are sandboxed, we associated the app with the EPUB media type, enabling the user to move an EPUB file downloaded with Safari or iTunes into the app bundle. The Android version assumes instead that the user has copied/downloaded the EPUB files to the device storage via a file or download manager. A very nice upgrade of Menestrello will consists in the ability of downloading EPUB files from the Web or storage services like Dropbox or Wuala.
The app architecture
Like many other eBook readers, the app consists of two main views: the Library and the Reader.
The Library shows a list of books, each with the cover thumbnail and its metadata. Beyond opening the desired book, from the Library view the user will be able to search for a book using the interactive search bar, change the sort criterion, and also access some additional views like the Book Metadata, the Guide, and the global Settings.
The Reader view is more interesting, as it is in charge of
- parsing the EPUB file,
- properly extracting its assets,
- feeding them to the WebView,
- managing all the user interactions, including those involving the audio rendition, backgrounding the app, and returning to the Library.
We wanted to keep the UI design minimal, elegant, and efficient. By using jQuery Mobile and the excellent nativeDroid theme by Raphael Wildhaber, we think we are really on the right path:
Three custom plugins: Librarian, Unzipper, MediaRB
As I mentioned above, the hybrid approach (more precisely, the JS part) is pushed to its limits when asked to deal with low-level functions, as is the case of a complex EPUB 3 reading system.
Luckily, PhoneGap (Cordova) allows the developer to add plugins to the app. A plugin consists of a common JS interface, and one or more native code classes for each of the platforms to be supported: in our case, Java code for Android, and ObjectiveC code for iOS. The idea is that, to perform low-level or computationally-intense tasks, your JS code calls the native code which executes on a separate thread. The output of the native code is then given back to the JS code with a callback.
In Menestrello, we have three main custom plugins:
- Librarian: scans the device storage for EPUB files, reads their metadata, and creates a JSON database that is used to populate the Library;
- Unzipper: manages the unzipping of assets from the EPUB (ZIP) container;
- MediaRB: provides control over the native audio APIs.
Besides performing the EPUB discovery, the Librarian also parses the OPF manifest of each EPUB file found, and retrieves the metadata items needed to properly display the book in the Library. These include basic metadata like title, author, language, etc., but also some specific to Audio-eBooks, like the narrator or the duration.
The Librarian does not perform a full parsing of each EPUB file, to keep the discovery phase reasonably fast. (Yet, we should improve it further.) Instead, the full EPUB parsing is done by a JS parser when the user opens a book and the Reader View is loaded. Additionally, we are working on caching the EPUB information, instead of parsing every time.
Moving to the second plugin,
We coded the Unzipper as a native plugin because
Audio-eBooks are much larger than common EPUB files
(due to the embedded audio),
and JS ZIP libraries are painfully slow.
Moreover, Menestrello extracts assets in a clever way,
to minimize the temporary storage required:
all the text and images are immediately extracted,
while each audio file is extracted only when needed.
For example, if you are on Chapter 1,
only the corresponding audio file (say, 01_Chapter_1.mp3
) is unpacked.
When you move to Chapter 2, 01_Chapter_1.mp3
is deleted,
and 02_Chapter_2.mp3
is extracted, and so on.
(A possible enhancement consists in pre-extracting the next audio file; however, we have noticed that on reasonably recent hardware the unzipping does not add a noticeable delay to the chapter transition, so we have skipped that for now.)
Note that this unzipping mechanism is needed because:
- unzipping the whole Audio-eBook might be problematic if the EPUB file is very large. For example, the EPUB 3 Audio-eBooks of the unabridged I Promessi Sposi or Moby Dick are well above 500 MB!
- we chose to keep the original EPUB file available to the user as any other file on the device storage (in Android). Had we chosen to maintain a "hidden" library, we could have simply unzipped each EPUB upon importing it, and kept the assets in unpacked form, like some iOS apps do with downloaded contents.
Finally, let's move to the third plugin. Early versions of Menestrello adopted the official Media PhoneGap plugin. Unfortunately, we discovered it was very buggy, and its code was "ugly" and "fat": for example, it had both the playback functions and the recording functions mixed together. Since we just needed the first ones, we wanted to get rid of the second ones (bonus points: doing so, we eliminated the need for the "Audio Recording" permission in Android).
So, we coded a replacement, unimaginatively called MediaRB,
removing all the unnecessary stuff,
and fixing a couple of bugs.
This turned out to be a good decision, because
when we wanted to support changing the audio playback speed,
feature which was not (and still is not) provided by the official PhoneGap plugin,
we managed to do it quickly and effectively.
(Sort of. iOS has a very simple API to change the rate of an audio stream,
while Android has not, and we ended up integrating an NDK library,
extending the stock MediaPlayer
class,
and dealing with an ordeal of firmware/manufacturers quirks.
Fun stuff, if you are really into that kind of things.)
In the next episode
In Part 3 I will describe some of the most interesting (and least known) features of Menestrello, including its support for footnotes and parallel texts.