作为客户端安装的媒体播放器,Java Media Framework并没有给人很深的印象。它仅仅支持一部分媒体类型,而这些类型只是其他播放系统比如说Windows Media Player和QuickTime支持类型的子集。
但是从内容供应商的观点来看,故事就变的有趣多了:JMF在所有的java模式下都可以适用,这样我们在部署媒体的时候客户端不需要任何媒体播放技术――只需要一个J2SE的运行环境就可以了。举例来说,工程巨人Robert X. Cringely最近宣布一项计划: 他们将提供一个被称为"NerdTV"的纯java的MPEG-4 系统,这个系统不需要任何客户端的预安装。
更进一步来说,我们还可以利用.jar格式来把解码器和媒体文件打包成一个文件,以此,从效果上来说创建了一个"自播放电影",与压缩系统WinZip和StuffIt可以创建自扩展归档文件的方式相似。
实现的策略分三步走:
1.使JMF可以播放在.jar文件里的媒体文件。
2.创建一个精简型的.jar 文件,只包括JMF里必须播放本地媒体文件的部分。
3.把代码和媒体文件都放进.jar里,创建一个合适的manifest 文件来支持双击。
JMF基本放像功能的关键在于得到一个可以解码和播放媒体文件的播放器。一般来说,这个动作通过使用管理器来得到一个合适的数据源,这个数据源在放像模式下同时提供媒体流和关于媒体流的元数据,比如说媒体文件的格式。管理器接着找到一个播放器来处理这个数据源。在两种情形下,管理器把一系列的包前缀(javax.media,com.ibm.media等等)绑定在反射机制上来寻找合适的类,在某种情形下抛出错误,比如说当找到的播放器不能接受提供的数据源。
接下来管理器没有更多的办法,通常只是查看URL里的协议和文件扩展名,所以它可以很容易的知道如何处理file&:///Users/cadamson/mymp3stash/some.mp3 ,却不知道如何处理jar:file&:/Users/cadamson/dev/jmftests/spmovie-old/src/gatsbymovie.jar!/movie/themovie.mov。
为了让事情变得更简单,我们来写一个数据源,或者更确切的说,写一个牵引式数据源。它的职责是为管理器描速数据源本身。JarEntryDataSource里的方法都很简单;但是有很多,因为我们提供的PullSourceStream需要使用很多接口的实现。
这种做法看起来有些不妥――类是通过文件的扩展名来返回"内容类型"的。它类似于MIME类型,只是它用句点而不是用斜杠来构建(MIME类型video/mpeg 转换为video.mpeg,这样管理器可以找到包com.sun.media.codec.video.mpeg )。下面是简单的实现:
public String getContentType() {
try {
URL url = getLocator().getURL();
String urlFile = url.getFile();
if (urlFile.endsWith(".mov"))
return "video.quicktime";
else if (urlFile.endsWith(".mpg"))
return "video.mpeg";
else if (urlFile.endsWith(".avi"))
// Manager needs '_' insted of '-'
return "video.x_msvideo";
else
return "unknown";
} catch (MalformedURLException murle) {
return "unknown";
}
}
另一个令人头疼的问题是JMF的源代码(目前从Sun的网站拿走了不过很快就会放上去)表明如果提供的流是Seekable,(一个提供随机访问方法seek()的接口) 缺省的播放器只能播放一个QuickTime的数据源。JarEntryDataSource的解决策略是在寻找点在媒介流前面的情况下使用InputStream.skip()。如果寻找点在当前读取点(被称为tellPoint因为这个值是由方法Seekable.tell()返回的)的后面,必须关闭InputStream,重开,然后跳到寻找点。它使用一个内部的thoroughSkip()方法来保证我们是真正的结束。
public long seek (long position) {
try {
if (position > tellPoint) {
thoroughSkip (position - tellPoint);
} else {
close();
open();
thoroughSkip (position);
}
return tellPoint;
} catch (IOException ioe) {
return 0; // bogus...
}
}
使用这个类,管理器可以找到一个可用的播放器来播放.jar里的.mov或者.avi 格式的文件。在我们的例子里TinyPlayer使用ClassLoader.getResource()来在classpath里找到movie/themovie.mov或者movie/themovie.avi。当classpath只包括.jar文件的时候,我们就实现了自播放。
准备一个合适的.jar文件的第一步是使用JMF的工具创建一个仅仅包含播放所必须的类的jar包,忽略那些流化,寻找,转码和其他任何不是骨架播放器所需要的功能。
不幸的是,Sun在纯java版本的JMF里没有包括jmfcustomizer的帮助文件,但是我们可以很容易的描速出定制所需要的一系列页面。
媒体源和媒体接收器: 选择"媒体文件"和"播放"
协议:"文件"
媒体格式:"QuickTime (.mov)" 和 "Avi."
解码器:不论你计划用什么,最可能的是"A-law," "U-law," 或者音频用"IMA4" ,视频用"H263"
处理:音频,我们需要"JavaSound"来支持java1.3或者更高版本,SunAudio来支持Sun 的pre-1.3 JVMs。视频,"AWT"就够了。
创建的结果是我们得到了一个精简的.jar文件――从普通1.9 MB的 jmf.jar 到我们定制的小于700K的jar包。
假定你已经编译了两个com.mac.invalidname.spmovie类,并把他们加到了定制的jar包里:
jar uf customized.jar com/mac/invalidname/spmovie/*.class
JMF的许可条例要求任何JMF的定制子集里都必须包括他的read-me文件。我已经在目录misc里提供了这个文件,TinyPlayer可以找到它。
jar uf customized.jar misc/
为了实现.jar的可双击,我们提供了一个manifest文件来告诉Java runtime 在双击或者使用-jar命令行参数调用的时候.jar里的哪个类包含可以调用的main()。Manifest还提供仅仅包括jar 自身的一个lasspath:
Main-Class: com.mac.invalidname.spmovie.TinyPlayer
Class-Path: .
用以下命令来加入manifest:
jar ufm customized.jar manifest-stub.txt
现在这个文件已经包含在jar播放电影所需要所有的代码了。为了将来的使用,把它保存为spmovie-engine.jar或者任何类似的文件。
现在引擎已经设置好了,所需要的就只剩下媒体文件。我们可以从JMF 支持类型页面可以看到,纯java版本的JMF只支持有限集合的类型。可能视频的最好选择是H.263,它在很宽的比特率范围里都表现的很好,但是对于很多老机器来说它显得过于臃肿,你可以通过减小视频或者保持一个较低的帧数率来解决。音频没有这么的约定俗成,但是我想IMA4:1表现的相当不错。把你的媒体文件通过合适的编码或者转码,拷贝到movie/themovie.mov或者movie/themovie.avi。如果需要你可以重命名.jar文件(我用的是spmovie.jar)然后通过下面的方式加入媒体文件:
jar u0f spmovie.jar media/
注意是"0"数字零,而不是字母0;表明我们不想压缩这个目录,因为媒体文件已经被压缩过了。
我们得到的结果就是自播放电影――一个在双击的时候知道该运行那个类,提供所有信号分离,解码,处理电影所需要的代码和电影本身。作为示例,这里有个小型的自播放电影,它是我4个月儿子Keagan在玩耍的时候录制的(使用了FreePlay Music免版税的音乐)。
把这个概念扩展到applet是一件很简单的事情,这样我们可以让媒体文件在所有支持的java的浏览器里播放。
可能有人会说我们解决错了问题――是java虚拟机而不是媒体播放器在客户端提供了支持。但是通过提供"一次创作,到处运行"的媒体文件,我们实现了java的初衷。