diff --git a/engine/src/desktop/com/jme3/app/state/MjpegFileWriter.java b/engine/src/desktop/com/jme3/app/state/MjpegFileWriter.java index e39bca16d..d63636d63 100644 --- a/engine/src/desktop/com/jme3/app/state/MjpegFileWriter.java +++ b/engine/src/desktop/com/jme3/app/state/MjpegFileWriter.java @@ -11,11 +11,15 @@ import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.imageio.IIOImage; import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageOutputStream; /** * Released under BSD License - * @author monceaux, normenhansen + * @author monceaux, normenhansen, entrusC */ public class MjpegFileWriter { @@ -56,7 +60,11 @@ public class MjpegFileWriter { } public void addImage(Image image) throws Exception { - addImage(writeImageToBytes(image)); + addImage(image, 0.8f); + } + + public void addImage(Image image, float quality) throws Exception { + addImage(writeImageToBytes(image, quality)); } public void addImage(byte[] imagedata) throws Exception { @@ -79,18 +87,29 @@ public class MjpegFileWriter { } } imagedata = null; + + numFrames++; //add a frame } public void finishAVI() throws Exception { byte[] indexlistBytes = indexlist.toBytes(); aviOutput.write(indexlistBytes); aviOutput.close(); - long size = aviFile.length(); + int fileSize = (int)aviFile.length(); + int listSize = (int) (fileSize - 8 - aviMovieOffset - indexlistBytes.length); + RandomAccessFile raf = new RandomAccessFile(aviFile, "rw"); - raf.seek(4); - raf.write(intBytes(swapInt((int) size - 8))); - raf.seek(aviMovieOffset + 4); - raf.write(intBytes(swapInt((int) (size - 8 - aviMovieOffset - indexlistBytes.length)))); + + //add header and length by writing the headers again + //with the now available information + raf.write(new RIFFHeader(fileSize).toBytes()); + raf.write(new AVIMainHeader().toBytes()); + raf.write(new AVIStreamList().toBytes()); + raf.write(new AVIStreamHeader().toBytes()); + raf.write(new AVIStreamFormat().toBytes()); + raf.write(new AVIJunk().toBytes()); + raf.write(new AVIMovieList(listSize).toBytes()); + raf.close(); } @@ -142,6 +161,10 @@ public class MjpegFileWriter { public RIFFHeader() { } + + public RIFFHeader(int fileSize) { + this.fileSize = fileSize; + } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -382,6 +405,10 @@ public class MjpegFileWriter { public AVIMovieList() { } + public AVIMovieList(int listSize) { + this.listSize = listSize; + } + public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); @@ -473,7 +500,7 @@ public class MjpegFileWriter { } } - public byte[] writeImageToBytes(Image image) throws Exception { + public byte[] writeImageToBytes(Image image, float quality) throws Exception { BufferedImage bi; if (image instanceof BufferedImage && ((BufferedImage) image).getType() == BufferedImage.TYPE_INT_RGB) { bi = (BufferedImage) image; @@ -483,7 +510,17 @@ public class MjpegFileWriter { g.drawImage(image, 0, 0, width, height, null); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ImageIO.write(bi, "jpg", baos); + + ImageWriter imgWrtr = ImageIO.getImageWritersByFormatName("jpg").next(); + ImageOutputStream imgOutStrm = ImageIO.createImageOutputStream(baos); + imgWrtr.setOutput(imgOutStrm); + + ImageWriteParam jpgWrtPrm = imgWrtr.getDefaultWriteParam(); + jpgWrtPrm.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + jpgWrtPrm.setCompressionQuality(quality); + imgWrtr.write(null, new IIOImage(bi, null, null), jpgWrtPrm); + imgOutStrm.close(); + baos.close(); return baos.toByteArray(); } diff --git a/engine/src/desktop/com/jme3/app/state/VideoRecorderAppState.java b/engine/src/desktop/com/jme3/app/state/VideoRecorderAppState.java index 6ae801953..4fac4a23c 100644 --- a/engine/src/desktop/com/jme3/app/state/VideoRecorderAppState.java +++ b/engine/src/desktop/com/jme3/app/state/VideoRecorderAppState.java @@ -1,6 +1,8 @@ package com.jme3.app.state; import com.jme3.app.Application; +import com.jme3.app.state.AbstractAppState; +import com.jme3.app.state.AppStateManager; import com.jme3.post.SceneProcessor; import com.jme3.renderer.Camera; import com.jme3.renderer.RenderManager; @@ -27,7 +29,7 @@ import java.util.logging.Logger; * state is detached, else the old file will be overwritten. If you specify no file * the AppState will attempt to write a file into the user home directory, made unique * by a timestamp. - * @author normenhansen, Robert McIntyre + * @author normenhansen, Robert McIntyre, entrusC */ public class VideoRecorderAppState extends AbstractAppState { @@ -46,13 +48,41 @@ public class VideoRecorderAppState extends AbstractAppState { }); private int numCpus = Runtime.getRuntime().availableProcessors(); private ViewPort lastViewPort; + private float quality; + /** + * Using this constructor the video files will be written sequentially to the user's home directory with + * a quality of 0.8 + */ public VideoRecorderAppState() { - Logger.getLogger(this.getClass().getName()).log(Level.INFO, "JME3 VideoRecorder running on {0} CPU's", numCpus); + this(null, 0.8f); + } + + /** + * Using this constructor the video files will be written sequentially to the user's home directory. + * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file) + */ + public VideoRecorderAppState(float quality) { + this(null, quality); } + /** + * This constructor allows you to specify the output file of the video. The quality is set + * to 0.8 + * @param file the video file + */ public VideoRecorderAppState(File file) { + this(file, 0.8f); + } + + /** + * This constructor allows you to specify the output file of the video as well as the quality + * @param file the video file + * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file) + */ + public VideoRecorderAppState(File file, float quality) { this.file = file; + this.quality = quality; Logger.getLogger(this.getClass().getName()).log(Level.INFO, "JME3 VideoRecorder running on {0} CPU's", numCpus); } @@ -67,6 +97,22 @@ public class VideoRecorderAppState extends AbstractAppState { this.file = file; } + /** + * Get the quality used to compress the video images. + * @return the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file) + */ + public float getQuality() { + return quality; + } + + /** + * Set the video image quality from 0(worst/smallest) to 1(best/largest). + * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file) + */ + public void setQuality(float quality) { + this.quality = quality; + } + @Override public void initialize(AppStateManager stateManager, Application app) { super.initialize(stateManager, app); @@ -128,7 +174,7 @@ public class VideoRecorderAppState extends AbstractAppState { public Void call() throws Exception { Screenshots.convertScreenShot(item.buffer, item.image); - item.data = writer.writeImageToBytes(item.image); + item.data = writer.writeImageToBytes(item.image, quality); while (usedItems.peek() != item) { Thread.sleep(1); }