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
-
-
@author monceaux, normenhansen, entrusC
*/
public class MjpegFileWriter {
@@ -56,8 +60,12 @@
}
public void addImage(Image image) throws Exception {
-
@author monceaux, normenhansen, entrusC
-
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]