Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

Adding Video Playback to an EPUB or, a Web Server in < 100 Lines of Code

4.80/5 (11 votes)
26 Aug 2013CPOL5 min read 25.4K   311  
Adding video playback to an EPUB or, a Web Server in < 100 lines of code.

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

Java
public EpubWebView(Context context, AttributeSet attrs) {
    super(context, attrs);
    mGestureDetector = new GestureDetector(context, mGestureListener);
    WebSettings settings = getSettings();
    settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
    // these two lines enable Video
    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:

Java
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:

Java
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();
        
        // create socket, giving it port to listen for requests on
        ServerSocket serverSocket = new ServerSocket(mPort);
        serverSocket.setReuseAddress(true);
        while(isRunning) {
            // wait until a client makes a request.
            // will return with a clientSocket that can be used
            // to communicate with the client
            Socket clientSocket = serverSocket.accept();
            
            // pass socket on to "something else" that will
            // use it to communicate with client
            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:

Java
public class WebServer {
    private static final String MATCH_EVERYTING_PATTERN = "*";

    private BasicHttpContext mHttpContext = null;
    private HttpService mHttpService = null;

    /*
     * @handler that processes get requests
     */
    public WebServer(HttpRequestHandler handler){
        mHttpContext = new BasicHttpContext();

        // set up Interceptors.
        //... ResponseContent is required, or it doesn't work.
        //... Apache docs recommended the others be provided but
        //... they are not strictly needed in this case.
        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);
    }

    /*
     * Called when a client connects to server
     * @socket the client is using 
     */
    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:

Java
@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:

Java
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)