Introduction
In my
previous article on CodeProject, I showed a simple EPUB viewer
for Android. Since then, I received a request to get the EPUB viewer
to show MPEG-4 video embedded in an EPUB which is a feature of the EPUB 3.0
specification. This article describes how this can be done, with some
limitations.
What this article will cover:
- Issues with getting a WebView
control to display video.
- Building a bare bones Web Server
hosted in the EPUB.
- Issues/limitations of the Android
MediaPlayer and how to check a MPEG-4 video for compatibility.
As this is a contination of my previous article I'm going to assume you've already read that.
Displaying Video in a WebView
From the EPUB 3 specification, video content can be included in a
document via the HTML 5 <video> tag. As the EPUB viewer I
provided used the Android WebView control to show the content, it
should have "just worked". Unfortunately, it didn't.
There were two problems with the EPUB viewer. First, the
WebView was not configured to show video. Second, WebView
does not not call WebViewClient.shouldInterceptRequest()
to obtain video
content.
Configuring the WebView for video is easy (at least for
Android 4.1.2). Just give the WebView a WebChromeClient and
set PluginState to ON_DEMAND
. Which requires adding
two lines to the EpubWebView
constructor
public EpubWebView(Context context, AttributeSet attrs) {
super(context, attrs);
mGestureDetector = new GestureDetector(context, mGestureListener);
WebSettings settings = getSettings();
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
settings.setPluginState(WebSettings.PluginState.ON_DEMAND);
setWebChromeClient(new WebChromeClient());
setWebViewClient(mWebViewClient = createWebViewClient());
}
I believe shouldInterceptRequest()
is not called because video playback is not done by the actual WebView.
Instead, the WebView delegates this to a MediaPlayer, passing the MediaPlayer the URI of the video content.
In the old EPUB viewer we were supplying the URIs as file scheme references. So the MediaPlayer was trying
to read the files and failing. Because the files don't exist "on disk".
Not calling shouldInterceptRequest()
can be worked around by having the EPUB viewer run a web server and changing the URIs supplied to the WebView to refer to the web server. Thus, the Media Player makes an HTTP request to the EPUB viwer to get the video content.
The revised code to give HTTP URIs is:
public static Uri resourceName2Url(String resourceName) {
return new Uri.Builder().scheme("http")
.encodedAuthority("localhost:" + Globals.WEB_SERVER_PORT)
.appendEncodedPath(Uri.encode(resourceName, "/"))
.build();
}
The only changes required are using the "http" scheme and adding a localhost Authority with the port the server is listening on.
A Bare Bones Web Server
The web server has to do the following:
- Listen for incoming requests.
- Parse the request to determine the requested file.
- Return the requested file.
Listening for network requests can be done with a java.net.ServerSocket
.
This
Oracle Tutorial
describes how to use a ServerSocket in detail. Stripped of error handling the code looks like this:
public class ServerSocketThread extends Thread {
private static final String THREAD_NAME = "ServerSocket";
private WebServer mWebServer;
private int mPort;
public ServerSocketThread(WebServer webServer, int port){
super(THREAD_NAME);
mWebServer = webServer;
mPort = port;
}
@Override
public void run() {
super.run();
ServerSocket serverSocket = new ServerSocket(mPort);
serverSocket.setReuseAddress(true);
while(isRunning) {
Socket clientSocket = serverSocket.accept();
mWebServer.processClientRequest(clientSocket);
}
}
public synchronized void stopThread(){
mIsRunning = false;
mServerSocket.close();
}
}
The vital point to note is accept()
is a blocking function. The call won't return
until a client connects. If you call it on your application's main thread, your application will halt. To avoid this, the ServerSocket must be created on its own thread.
To create a thread, derive a class from java.lang.Thread
and override the run()
method.
The other point of note is that the Socket doesn't process the client's request. The request is
delegated to the "mWebServer
" object which was supplied to the thread's constructor.
The "mWebServer
" object has three responsibilities:
- Parse the client's request to determine what action the client has requested.
- Perform the action.
- Return details of the action to the client.
The org.apache.http.protocol.HttpService
class will handle most of this work. The Apache documentation
for this class is quite lengthy. But the basic steps are:
- Create a
HttpService
. - Add Interceptors for the HTTP header information that needs to be processed.
- Register handlers to perform the requested actions.
- When receive a request from a client, call HttpService's
handleRequest()
with the client's socket.
A bare bones WebServer looks like this:
public class WebServer {
private static final String MATCH_EVERYTING_PATTERN = "*";
private BasicHttpContext mHttpContext = null;
private HttpService mHttpService = null;
public WebServer(HttpRequestHandler handler){
mHttpContext = new BasicHttpContext();
BasicHttpProcessor httpproc = new BasicHttpProcessor();
httpproc.addInterceptor(new ResponseContent());
httpproc.addInterceptor(new ResponseConnControl());
httpproc.addInterceptor(new ResponseDate());
httpproc.addInterceptor(new ResponseServer());
mHttpService = new HttpService(httpproc,
new DefaultConnectionReuseStrategy(),
new DefaultHttpResponseFactory());
HttpRequestHandlerRegistry registry = new HttpRequestHandlerRegistry();
registry.register(MATCH_EVERYTING_PATTERN, handler);
mHttpService.setHandlerResolver(registry);
}
public void processClientRequest(Socket socket) {
try {
DefaultHttpServerConnection serverConnection = new DefaultHttpServerConnection();
serverConnection.bind(socket, new BasicHttpParams());
mHttpService.handleRequest(serverConnection, mHttpContext);
serverConnection.shutdown();
} catch (IOException e) {
e.printStackTrace();
} catch (HttpException e) {
e.printStackTrace();
}
}
}
Major points to note:
- We only register one handler as there's only one action we're doing: return requested file.
- We pass in this handler in the constructor.
The handler needs to return the "file" corresponding to a URI. This sounds very much like what our Book
class does. In fact, we turn our Book class into a HttpRequestHandler
by adding a single function:
@Override
public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException {
String uriString = request.getRequestLine().getUri();
String resourceName = url2ResourceName(Uri.parse(uriString));
ZipEntry containerEntry = mZip.getEntry(resourceName);
if (containerEntry != null) {
InputStreamEntity entity = new InputStreamEntity(mZip.getInputStream(containerEntry), containerEntry.getSize());
entity.setContentType(mManifestMediaTypes.get(resourceName));
response.setEntity(entity);
} else {
response.setStatusLine(request.getProtocolVersion(), HttpStatus.SC_NOT_FOUND, "File Not Found");
}
}
In our main activity, wire up the Web server and start it running with:
private void createWebServer() {
WebServer server = new WebServer(getBook());
mWebServerThread = new ServerSocketThread(server, Globals.WEB_SERVER_PORT);
mWebServerThread.startThread();
}
MPEG-4 Specification and Android Media Player
A final problem you may run into is that the Android MediaPlayer does not support all MPEG4 files.
Specifically, the android docs say
"For 3GPP and MPEG-4 containers, the moov
atom must precede any mdat
atoms, but must succeed the ftyp
atom".
What this means (grossly simplifying): A MPEG-4 is made up of "atoms" (in the earlier spec) or "boxes"
(current spec). The moov
atom is the index of where all the other atoms are in the file. In particular, the mdat
atoms
that hold the video data. The MPEG-4 spec allows the moov
atom to be at the start or end of the file.
However, when the moov
atom is at the end of a HTTP stream, MediaPlayer has problems because it can't play anything until it reads the moov
atom.
AtomicParsley can be used to inspect an MPEG-4 file to see if the atoms are in the correct order for MediaPlayer.
If the moov
atom is at the end of the file, then a tool such as "qt-faststart" can be used to move the "moov
" atom to the start of the MPEG-4.
Source Code
The latest version of the source code can be downloaded from GitHub.
Running the supplied code
Included with the source code is a simple EPUB file with an embedded video file (workingVideo.epub). I created this file from the
Creative Commons sample EPUB.
by removing all but one page, all images audio and video and then adding a single video file
The EPUB viewer was able to show the video when run on the Android emulator with Android version 4.1.2.
The EPUB viewer needs this file to be installed onto the SD card, in a directory called "Download". (On DDMS, the path is "mnt/sdcard/Download".)
What to do when your EPUB file doesn't work.
If you've got an EPUB that doesn't work and you want my assistance, please include a URL to the EPUB file you're having problems with in your message to me.