VideoRecorderAppState enhancement (patch included)

Hi everyone,



I recorded some captures for my new game and noticed that the quality was not that good. I dove into the code and found that a simple ImageIO.write() was used to store the jpeg data. So I changed the VideoRecorderAppState and the MjpegFileWriter Class to include the possibility to set the quality of the jpegs within the video stream.



Additionally I fixed the problem that the video does not contain a proper length in the header.



All in all this is the patch - would really appreciate it if you would merge it into the trunk:



(also available as download: http://darkblue.no-ip.org/VideoRecorderAppState.diff)

[patch]

Index: MjpegFileWriter.java

===================================================================

— MjpegFileWriter.java (revision 9282)

+++ MjpegFileWriter.java (working copy)

@@ -11,11 +11,15 @@

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,8 +60,12 @@

      }



      public void addImage(Image image) throws Exception {
  •    addImage(writeImageToBytes(image));<br />
    
  •    addImage(image, 0.8f);<br />
    

}

+

  • public void addImage(Image image, float quality) throws Exception {
  •    addImage(writeImageToBytes(image, quality));<br />
    
  • }



    public void addImage(byte[] imagedata) throws Exception {

    byte[] fcc = new byte[]{‘0’, ‘0’, ‘d’, ‘b’};

    @@ -79,18 +87,29 @@

    }

    }

    imagedata = null;

    +
  •    numFrames++; //add a frame<br />
    

}



public void finishAVI() throws Exception {

byte[] indexlistBytes = indexlist.toBytes();

aviOutput.write(indexlistBytes);

aviOutput.close();

  •    long size = aviFile.length();<br />
    
  •    int fileSize = (int)aviFile.length();<br />
    
  •    int listSize = (int) (fileSize - 8 - aviMovieOffset - indexlistBytes.length);<br />
    

+

RandomAccessFile raf = new RandomAccessFile(aviFile, "rw");

  •    raf.seek(4);<br />
    
  •    raf.write(intBytes(swapInt((int) size - 8)));<br />
    
  •    raf.seek(aviMovieOffset + 4);<br />
    
  •    raf.write(intBytes(swapInt((int) (size - 8 - aviMovieOffset - indexlistBytes.length))));<br />
    

+

  •    //add header and length by writing the headers again<br />
    
  •    //with the now available information<br />
    
  •    raf.write(new RIFFHeader(fileSize).toBytes());<br />
    
  •    raf.write(new AVIMainHeader().toBytes());<br />
    
  •    raf.write(new AVIStreamList().toBytes());<br />
    
  •    raf.write(new AVIStreamHeader().toBytes());<br />
    
  •    raf.write(new AVIStreamFormat().toBytes());<br />
    
  •    raf.write(new AVIJunk().toBytes());<br />
    
  •    raf.write(new AVIMovieList(listSize).toBytes());<br />
    

+

raf.close();

}



@@ -142,6 +161,10 @@



public RIFFHeader() {

}

+

  •    public RIFFHeader(int fileSize) {<br />
    
  •        this.fileSize = fileSize;<br />
    
  •    }<br />
    

public byte[] toBytes() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -382,6 +405,10 @@
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 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 @@
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();
}
Index: VideoRecorderAppState.java
===================================================================
--- VideoRecorderAppState.java (revision 9282)
+++ VideoRecorderAppState.java (working copy)
@@ -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 @@
* 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 @@
});
private int numCpus = Runtime.getRuntime().availableProcessors();
private ViewPort lastViewPort;
+ private final 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);
}

@@ -128,7 +158,7 @@

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

[/patch]
6 Likes

Cool, thanks!

Nice patch, and great to see some documentation in there too :smiley:

@sbook said:
Nice patch, and great to see some documentation in there too :D

Thanks - I thought some documentation would be good as this class is really valuable :) I also added a few constructors to stay backward compatible.

Committed: http://code.google.com/p/jmonkeyengine/source/detail?r=9283

@normen said:
Committed: http://code.google.com/p/jmonkeyengine/source/detail?r=9283


cool :) - now I just need to get the people that update the maven repository to update it once in a while ;)

Why you not just include the ant build in your own maven project?