Movie3D子类化了Jframe,而WrapMovie3D是Jpanel的子类化结果。WrapMovie3D创建了图2中的场景图,并在应用程序的JPanel上对其进行着色。它使用CheckerFloor类和ColouredTiles类来构造出跳棋盘地板效果。
JMFMovieScreen完成动画屏幕的创建,并把它添加到场景中去,然后通过创建一个JMFSnapper对象来启动动画的播放。TimeBehavior每40毫秒调用JMFMovieScreen中的nextFrame()方法一次,而nextFrame()又调用JMFSnapper中的getFrame()方法来检索当前帧。
所有该示例的代码以及本文的一个早期版本,都能在KGPJ website处找到。
3.观看动画播放
动画,动画屏幕以及用来更新屏幕的TimeBehavior对象都是由WrapMovie3D中的addMovieScreen()方法建立起来的://全局变量
private BranchGroup sceneBG;
private JMFMovieScreen ms; //动画屏幕
private TimeBehavior timer; //更新屏幕
private void addMovieScreen(String fnm)
{
//把fnm形式的动画投放到动画屏幕上
ms = new JMFMovieScreen(new Point3f(1.5f, 0, -1), 2.0f, fnm);
sceneBG.addChild(ms);
//为animating动画建立计时器对象
timer = new TimeBehavior(40, ms);
//更新动画每40ms一次(25帧/秒)
timer.setSchedulingBounds(bounds);
sceneBG.addChild(timer);
}
这里的两个Java 3D方法addChild()调用把JMFMovieScreen结点和TimeBehavior结点链接到场景图中。setSchedulingBounds()方法用于激活TimeBehavior结点(也就是说,使该结点开始计时)。
4.生成动画屏幕
JMFMovieScreen是Java 3D的Shape3D类的子类化,所以一定要为其指定一个几何形状和外观。
几何形状是一个四边形,每边与动画的图像尺寸成比例,但是用该四边形宽度与高度中的较大值用作类JMFMovieScreen的构造器的参数。该四边形是竖直的,朝向正Z轴方向,可被放于地板的任意位置。
该四边形是两面的,这样动画可以从屏幕的前后两个方向观看。纹理是使用双线性插补算法进行平滑处理,所以当从近处观看动画时,像素化现象大大减弱。
这里的大部分功能是从KGPJ一书第24章的第一人称射击程例中的ImageCsSeries类中复制过来的。ImageCsSeries类用于在一个四边形上显示一系列的GIF图像。为节省篇幅,我仅描述一下JMFMovieScreen区别于ImageCsSeries的几个特点。
实现图像的高效着色
从动画中提取的帧被转换成纹理后放于四边形之上;其实现方式分为两步:第一,给定的BufferedImage被传递给一个Java 3D ImageComponent2D对象,然后又传递给一个Java 3D Texture2D。
四边形的纹理更新是很迅速的:每秒可更新25帧,纹理需要变化25次。因此,实现高效地纹理化是十分重要的。这有可能通过把某些特定的格式指定给BufferedImage和ImageComponent2D对象来实现。
ImageComponent2D对象由JMFMovieScreen所使用,其声明如下:ImageComponent2D ic = new ImageComponent2D(
ImageComponent2D.FORMAT_RGB,
FORMAT_SIZE, FORMAT_SIZE, true, true);
构造器中的最后两个参数指出,它使用的是"by reference"和"Y-up"方式。由于Java 3D会避免把图像从应用程序空间复制到图像内存中去,所以这里指出的方式将减少存储纹理图像的内存需求。
在Windows操作系统环境下,Java 3D用OpenGL作基本的着色引擎,ImageComponent2D格式应该是ImageComponent2D.FORMAT_RGB(如上所述),BufferedImage格式应该是BufferedImage.TYPE_3BYTE_BGR。在JMFSnapper中BufferedImage格式是固定的。
要想对此和其它性能技巧有更多的了解,可以参见j3d.org。
把纹理链接到四边形上
把纹理图像平铺到一个四边形上的常用方法是,把纹理的左下角链接到四边形的左下角,并以反时针方向指定其它链接。这种方法显示在图4中。
纹理坐标的范围是0-1,沿着x轴和y轴,且y轴方向向上。例如,纹理左下角使用坐标(0,0),则右上角为(1,1)。
当使用"Y-up"方式时,y轴的纹理坐标是颠倒的,即指向下方。这就是说,坐标(0,0)对应纹理的左上角,而(1,1)对应纹理的右下角。
在使用"Y-up"方式情况下,纹理坐标一定要赋给四边形的不同点以取得图像的相同的方向。这种新的配置显示在图5中。
在JMFMovieScreen中,实现把四边形顶点与纹理坐标相链接的代码如下:TexCoord2f q = new TexCoord2f();
q.set(0.0f, 0.0f);
plane.setTextureCoordinate(0, 3, q);
//纹理坐标(0,0)-->四边形左上点(p3)
q.set(1.0f, 0.0f);
plane.setTextureCoordinate(0, 2, q);
// (1,0) -->右上(p2)
q.set(1.0f, 1.0f);
plane.setTextureCoordinate(0, 1, q);
// (1,1) -->右下(p1)
q.set(0.0f, 1.0f);
plane.setTextureCoordinate(0, 0, q);
// (0,1) -->左下(p0)
这里的平面对象代表了四边形。
更新图像
前面已经提到,一个TimeBehavior对象被建立以每隔40毫秒调用一次JMFMovieScreen的nextFrame()方法。而nextFrame()又调用JMFSnapper对象的getFrame()方法来以一个BufferedImage对象方式检索当前动画帧。该BufferedImage对象被指派给ImageComponent2D对象,然后用于四边形的材质。nextFrame()的代码如下所示://全局变量
private Texture2D texture; //由四边形使用
private ImageComponent2D ic;
private JMFSnapper snapper;
//快照该动画
private boolean isStopped = false;
//动画停止了吗?
public void nextFrame()
{
if (isStopped) //动画已经停止
return;
BufferedImage im = snapper.getFrame();
//获取当前帧
if (im != null) {
ic.set(im); //把该帧指派给ImageComponent2D
texture.setImage(0,ic);
//使成为该形状的材质
}
else
System.out.println("Null BufferedImage");
}
JMFSnapper对象snapper是在JMFMovieScreen的构造器中创建的://装载并播放动画
snapper = new JMFSnapper(movieFnm);
JMFSnapper中的简单接口隐藏了用于播放动画和从动画中提取帧的JMF代码的复杂性。在本文的第二部分里,JMFSnapper类为一个使用QuickTime for Java的版本所取代,且JMFMovieScreen类也作了最少的修改。
5.管理动画
JMF为存取特定的动画帧提供了一种高级存取方式。下面的代码片断显示了该高级方式的主要组成,我略去了其中有关错误检测及异常处理的部分。//在realized状态下,创建一个动画播放器
URL url = new URL("file:" + movieFnm);
Player p = Manager.createRealizedPlayer(url);
//生成一个帧放置器
FramePositioningControl fpc = (FramePositioningControl) p.getControl("javax.media.control. FramePositioningControl");
//创建一个帧抓取器
FrameGrabbingControl fg = (FrameGrabbingControl) p.getControl("javax.media.control.FrameGrabbingControl");
//要求改变到一个prefetched 态
p.prefetch();
//一直等待,直到播放器处于那种状态...
//移动到具体的某帧,例如第100帧
fpc.seek(100);
//取得当前帧的一个快照
Buffer buf = fg.grabFrame();
//取得它的视频格式细节
VideoFormat vf = (VideoFormat) buf.getFormat();
//用视频格式初始化BufferToImage
BufferToImage bufferToImage =new BufferToImage(vf);
//把缓冲区数据转化成一幅图像
Image im = bufferToImage.createImage(buf);
//指定想得到的BufferedImage的格式
BufferedImage formatImg =
new BufferedImage(
FORMAT_SIZE, FORMAT_SIZE,
BufferedImage.TYPE_3BYTE_BGR);
//把该图像转化成一个BufferedImage
Graphics g = formatImg.getGraphics();
g.drawImage(im, 0, 0,
FORMAT_SIZE, FORMAT_SIZE, null);
g.dispose();
一个媒体播放器从创建到开始播放共经历6种状态。处于realized态的播放器知道如何对其数据进行着色,所以在要求时可以提供可视化组件和控件。我用了两个控件:FramePositioningControl 和FrameGrabbingControl。FramePositioningControl提供seek()和skip()等方法,用于在一个动画中移动以查找一个特别的帧。FrameGrabbingControl提供了方法grabFrame(),它可以从动画的视频轨道中抓取当前帧。
为使这些控件工作,播放器必须实现从realized 态转入prefetched 态。这可以使播放器为进行媒体播放作好准备,并使媒体数据装入。
对于prefetch()的调用是异步的,这意味着我的代码必须包含一个等待周期,直到完成一个变换状态为止。标准的JMF编码方案是实现一个waitForState()方法,它可以停止代码的执行,直到一个状态改变事件唤醒它。
要抓取的帧可以用seek()方法在轨道中定位,然后调用grabFrame()方法实现帧的抓取。编码中必须经历多个转换步骤来把抓取的缓冲对象转怀蒍MFMovieScreen要求的BufferedImage对象。注意,BufferedImage对象使用了TYPE_3BYTE_BGR格式,这种格式对于该程序中的Java 3D部分通过引用方式使用纹理是必需的。
Sun的JMF站点 包括了一些有用的小例子,其中Seek.java一例说明了如何使用FramePositioningControl来遍历一个动画。
分三个步骤完成的技术攻关
不幸的是,上面概述编码方法是失败的,至少对于Windows 版的JMF性能包版本2.1.1e是这样。经过几番修改,我最后得到一个可以正常工作的版本JMFSnapper。
攻关1 上述的两个控件FramePositioningControl与FrameGrabbingControl,在JMF缺省的播放器模块中是难以得到(Solaris和Win32性能包均支持两种不同的MPEG播放器)的,要求用"native modular(本地组件)"播放器才行,其选取方式如下:Manager.setHint(Manager.PLUGIN_PLAYER, new Boolean(true));
该播放器是一个重量级的组件,其与轻量级的Swing GUI如JFrame 和JPanel交互性较差。不过,我不需要显示播放器的界面。使用本地组件播放器的一个更为严重的后果是,需要较长的时间装入媒体和出现一些不确定的播放结果(如播放快慢不一致且漏掉一些帧)。
攻关2 经过一番考虑,我定出最好的加速播放器的方式是让其承担较少量的工作。我从MPEG文件中提取出了音频轨道部分,并确保该文件以相对简单的MPEG-1格式存储。任何一些视频编辑工具都可以胜任这些工作。我使用了两个自由软件工具: MPEG Properties和 FlasKMPEG。前者用于提供动画格式信息,后者是一个不错的编辑器。
经提取加工后的动画播放速度快捷,帧速率稳定并且没有漏帧的情况发生。
然而,FramePositioningControl控件类并不可靠。在我的WinXP机器上,seek()方法几乎总是失败,skip()方法大约有百分之八十情况下工作正常。
攻关3 我只好对FramePositioningControl忍痛割爱。我运用的帧抓取算法依赖于调用FrameGrabbingControl的grabFrame()方法--当播放器播放动画时以常规的间隔时段调用该方法。
到此,我已有了可靠的从仅含视频的MPEG-1文件中抓取帧的代码。对于既有视频也有音频轨道的文件该代码也运行良好;但不足是,播放器启动很慢而且不确定的播放导致帧抓取的不确定性。
我在JMFSnapper 的开始加上了一些"等待"代码来处理既有视频也有音频的动画。JMFSnapper对象等待播放器的启动(即进入启动状态),并等待第一个动画帧可用。
等待第一帧
JMFSnapper类的构造器调用了方法waitForBufferToImage(),该方法反复地调用方法hasBufferToImage()直到它检测到第一个视频帧为止。
hasBufferToImage()调用了FrameGrabbingControl的方法grabFrame(),并检查是否返回的缓冲对象中包含视频格式数据。它使用该数据来初始化一个BufferToImag对象--该对象随后用于把每一个抓到帧转换成一幅图像。// 全局变量
private FrameGrabbingControl fg; //帧抓取器
private BufferToImage bufferToImage = null;
private int width, height; //帧尺寸
private boolean hasBufferToImage()
{
Buffer buf = fg.grabFrame(); //快照
if (buf == null) {
System.out.println("No grabbed frame");
return false;
}
//存在一个缓冲区,但要检查其是否为空
VideoFormat vf = (VideoFormat) buf.getFormat();
if (vf == null) {
System.out.println("No video format");
return false;
}
System.out.println("Video format: " + vf);
//提取图像的大小
width = vf.getSize().width;
height = vf.getSize().height;
// 用视频格式初始化bufferToImage
bufferToImage = new BufferToImage(vf);
return true;
}
这种编码方法的一个小缺点是,第一个视频帧(使得方法hasBufferToImage()返回true)在对象初始化后被放弃。该帧没有被转化为BufferedImage 并为JMFMovieScreen所用。
快照
JMFSnapper类中最主要的公共方法是getFrame(),它被周期性调用以取得正播放的动画的当前帧。// 全局变量
private BufferedImage formatImg; // 帧图像
synchronized public BufferedImage getFrame()
{
//以缓冲对象形式抓取当前帧
Buffer buf = fg.grabFrame();
if (buf == null) {
System.out.println("No grabbed buffer");
return null;
}
//把缓冲区数葑怀赏枷?
Image im = bufferToImage.createImage(buf);
if (im == null) {
System.out.println("No grabbed image");
return null;
}
//把该图像转换成一个缓冲图像
Graphics g = formatImg.getGraphics();
g.drawImage(im, 0, 0,FORMAT_SIZE, FORMAT_SIZE, null);
// Overlay current time on top of the image
g.setColor(Color.RED);
g.setFont(new Font("Helvetica",Font.BOLD,12));
g.drawString(timeNow(), 5, 14);
g.dispose();
return formatImg;
} //结束getFrame()
方法getFrame()和closeMovie()在JMFSnapper中都被同步处理。closeMovie()用于终止播放器,可以在任何时候调用。同步的目的是,为了确保提取一帧时,播放器不能被关闭。
缓冲图像对象formatImg在JMFSnapper的构造器中被初始化:formatImg = new BufferedImage( FORMAT_SIZE, FORMAT_SIZE, BufferedImage.TYPE_3BYTE_BGR);
6.抓取帧的另外一些方法
Sun的JMF示例站点提供了从动画中抓取帧的另外两种方法。
VideoRenderer接口
DemoJMFJ3D 示例结合了Java 3D和JMF两种技术,它显示了怎样把一个视频包装到一个圆柱体上。
例中的Java 3D部分其实与我前面讨论的技术相同-一个使用BufferedImage.TYPE_3BYTE_BGR格式的BufferedImage被传送给一个ImageComponent2D对象,然后该图像变成了圆柱体的纹理。该图像还可以使用BufferedImage.TYPE_4BYTE_ABGR格式,该格式在Solaris系统上使用以支持纹理的引用。
例中的JMF部分与我采用的技术有很大不同。一个JMF的VideoRenderer接口的实现被附着到动画视频轨道的TrackControl对象上。一旦TrackControl对象被启动,对于在视频中出现的每个帧VideoRenderer接口的process()方法被自动调用。process()方法的输入参数是缓冲区对象(也就是被抓取的帧)。不是采用我描述的Buffer-to-BufferedImage转换技术,DemoJMFJ3D是通过在缓冲区的原始数据和BufferedImage的像素映射之间执行一种低级的字节数组复制技术来构造实现的BufferedImage。
处理机Codec(多媒体数字信息编解码器)插件
FrameAccess示例中利用了一些高级的JMF成分,主要围绕着一个处理机Codec插件。
Processor类是Player的一个扩展版本,在处理媒体数据方面它提供了更多的能力。一个codec插件(其实是一个JMF接口Codec的执行)能够从轨道中读取帧,并对之进行任意的处理然后把它们写回到轨道中。特别地,每当在轨道中遇到一帧时,Codec的process()方法即被调用。该方法被提供给一个存有输入帧的缓冲对象和一个空缓冲对象用作输出。
FrameAccess例中把一个Codec插件附加到动画的视频轨道上,并使用传递到函数process()的输入帧缓冲对象来生成一些基本的有关该视频的统计数据。你可以轻易地修改这个例子以把缓冲对象转化成一个缓冲图像,为此,你既可以使用我介绍的方法也可以利用DemoJMFJ3D中的字节数组技术。
遗憾的是,要实现插件支持,Processor类不是必需的。结果,插件在JMF 1.0和基于2.0版本的一些JMF中并不工作。
在使用Sun的JMF示例前先搜一下JMF-兴趣邮件列表是个不错的注意,因为其中许多的程序都存在JMF版本不同带来的一些问题。下文将介绍该动画程序的另外一个版本-其中使用了Quicktime for Java技术。