65.9K
CodeProject 正在变化。 阅读更多。
Home

在 EPUB 或 Web 服务器中添加视频播放 - 代码行数 < 100

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (11投票s)

2013年8月26日

CPOL

5分钟阅读

viewsIcon

25875

downloadIcon

349

在 EPUB 或 Web 服务器中添加视频播放,代码行数 < 100。

引言

我之前在 CodeProject 上的文章中,我展示了一个简单的 Android EPUB 阅读器。从那时起,我收到一个请求,希望 EPUB 阅读器能够显示嵌入在 EPUB 中的 MPEG-4 视频,这是 EPUB 3.0 规范的一项功能。本文将介绍如何实现这一点,但存在一些限制。

本文将涵盖的内容

  • WebView 控件显示视频时遇到的问题。
  • 构建一个托管在 EPUB 中的极简 Web 服务器。
  • Android MediaPlayer 的问题/限制以及如何检查 MPEG-4 视频的兼容性。

由于这是我上一篇文章的延续,我将假设您已经阅读了那篇文章。

在 WebView 中显示视频

根据 EPUB 3 规范,视频内容可以通过 HTML 5 的 <video> 标签包含在文档中。由于我提供的 EPUB 阅读器使用 Android WebView 控件来显示内容,所以它“应该可以正常工作”。但不幸的是,事实并非如此。

EPUB 阅读器存在两个问题。首先,WebView 未配置为显示视频。其次,WebView 不会调用 WebViewClient.shouldInterceptRequest() 来获取视频内容。

配置 WebView 以支持视频非常简单(至少对于 Android 4.1.2 来说)。只需为 WebView 提供一个 WebChromeClient 并将 PluginState 设置为 ON_DEMAND。这需要在 EpubWebView 构造函数中添加两行代码

    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());
    }

我相信 shouldInterceptRequest() 没有被调用是因为视频播放不是由实际的 WebView 完成的。相反,WebView 将此委托给 MediaPlayer,并将视频内容的 URI 传递给 MediaPlayer。

在旧的 EPUB 阅读器中,我们提供的是文件方案引用。因此,MediaPlayer 试图读取文件但失败了。因为文件“不存在于磁盘上”。可以通过让 EPUB 阅读器运行一个 Web 服务器并更改提供给 WebView 的 URI 以引用 Web 服务器来绕过不调用 shouldInterceptRequest() 的问题。这样,MediaPlayer 会向 EPUB 阅读器发出 HTTP 请求以获取视频内容。

修改后的代码用于提供 HTTP URI 是

    public static Uri resourceName2Url(String resourceName) {
        return new Uri.Builder().scheme("http")
            .encodedAuthority("localhost:" + Globals.WEB_SERVER_PORT)
            .appendEncodedPath(Uri.encode(resourceName, "/"))
            .build();
    }

唯一需要的更改是使用“http”方案并添加一个 localhost 授权,其中包含服务器正在监听的端口。

一个极简的 Web 服务器

Web 服务器必须执行以下操作

  • 监听传入的请求。
  • 解析请求以确定所请求的文件。
  • 返回所请求的文件。

可以使用 java.net.ServerSocket 监听网络请求。这个Oracle 教程详细介绍了如何使用 ServerSocket。如果忽略错误处理,代码如下所示

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();
    }
}

需要注意的关键一点是 accept() 是一个阻塞函数。在客户端连接之前,该调用不会返回。如果在应用程序的主线程上调用它,您的应用程序将停止。为避免这种情况,必须在自己的线程上创建 ServerSocket。要创建线程,可以从 java.lang.Thread 派生一个类并覆盖 run() 方法。

另一个需要注意的点是,Socket 不处理客户端的请求。请求被委托给传递给线程构造函数的“mWebServer”对象。

mWebServer”对象有三个职责

  • 解析客户端请求以确定客户端请求的操作。
  • 执行操作。
  • 将操作的详细信息返回给客户端。

org.apache.http.protocol.HttpService 类将处理大部分工作。该类的Apache 文档相当冗长。但基本步骤是

  • 创建 HttpService
  • 添加需要处理的 HTTP 标头信息的拦截器。
  • 注册处理程序以执行请求的操作。
  • 当收到来自客户端的请求时,调用 HttpService 的 handleRequest() 并传入客户端的套接字。

一个极简的 WebServer 看起来是这样的

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();
        }
    }
}

需要注意的主要几点: 

  • 我们只注册了一个处理程序,因为我们只执行一种操作:返回请求的文件。
  • 我们在构造函数中传入了这个处理程序。

处理程序需要返回与 URI 对应的“文件”。这听起来非常像我们的 Book 类所做的事情。实际上,我们通过添加一个函数,将我们的 Book 类变成了 HttpRequestHandler

    @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"); 
        }
    }

在我们的主 Activity 中,连接 Web 服务器并启动运行,如下所示

    private void createWebServer() {
        WebServer server = new WebServer(getBook());
        mWebServerThread = new ServerSocketThread(server, Globals.WEB_SERVER_PORT);
        mWebServerThread.startThread();
    }

MPEG-4 规范和 Android Media Player

您可能遇到的最后一个问题是,Android MediaPlayer 不支持所有 MPEG4 文件。具体来说,android 文档说“对于 3GPP 和 MPEG-4 容器,moov 原子必须出现在任何 mdat 原子之前,但必须出现在 ftyp 原子之后”。

这意味着(粗略简化):MPEG-4 由“原子”(早期规范)或“框”(当前规范)组成。moov 原子是文件中所有其他原子位置的索引。特别是,包含视频数据的 mdat 原子。MPEG-4 规范允许 moov 原子位于文件开头或结尾。然而,当 moov 原子位于 HTTP 流的末尾时,MediaPlayer 会出现问题,因为它在读取 moov 原子之前无法播放任何内容。

AtomicParsley 可用于检查 MPEG-4 文件,以查看原子是否按正确的顺序排列以供 MediaPlayer 使用。

如果 moov 原子位于文件末尾,那么可以使用“qt-faststart”之类的工具将“moov”原子移动到 MPEG-4 的开头。

源代码

源代码的最新版本可以从GitHub下载。

运行提供的代码

源代码中包含一个简单的 EPUB 文件,其中嵌入了一个视频文件(workingVideo.epub)。我从Creative Commons 示例 EPUB创建了这个文件,方法是删除除一页之外的所有页面,删除所有图像、音频和视频,然后添加一个单个视频文件。EPUB 阅读器可以在 Android 模拟器上使用 Android 4.1.2 版本运行时显示视频。EPUB 阅读器需要将此文件安装到 SD 卡上,放在名为“Download”的目录中。(在 DDMS 中,路径是“mnt/sdcard/Download”。)

当您的 EPUB 文件无法正常工作时该怎么办。

如果您有一个无法正常工作的 EPUB 文件并希望我提供帮助,请在您给我留言时附上您遇到的问题的 EPUB 文件的 URL。

© . All rights reserved.