在 EPUB 或 Web 服务器中添加视频播放 - 代码行数 < 100
在 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。