diff --git a/jme3-android/src/main/java/com/jme3/app/state/MjpegFileWriter.java b/jme3-android/src/main/java/com/jme3/app/state/MjpegFileWriter.java new file mode 100644 index 000000000..9a8361f44 --- /dev/null +++ b/jme3-android/src/main/java/com/jme3/app/state/MjpegFileWriter.java @@ -0,0 +1,552 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.app.state; + +import android.graphics.Bitmap; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Released under BSD License + * @author monceaux, normenhansen, entrusC + */ +public class MjpegFileWriter { + private static final Logger logger = Logger.getLogger(MjpegFileWriter.class.getName()); + + int width = 0; + int height = 0; + double framerate = 0; + int numFrames = 0; + File aviFile = null; + FileOutputStream aviOutput = null; + FileChannel aviChannel = null; + long riffOffset = 0; + long aviMovieOffset = 0; + AVIIndexList indexlist = null; + + public MjpegFileWriter(File aviFile, int width, int height, double framerate) throws Exception { + this(aviFile, width, height, framerate, 0); + } + + public MjpegFileWriter(File aviFile, int width, int height, double framerate, int numFrames) throws Exception { + this.aviFile = aviFile; + this.width = width; + this.height = height; + this.framerate = framerate; + this.numFrames = numFrames; + aviOutput = new FileOutputStream(aviFile); + aviChannel = aviOutput.getChannel(); + + RIFFHeader rh = new RIFFHeader(); + aviOutput.write(rh.toBytes()); + aviOutput.write(new AVIMainHeader().toBytes()); + aviOutput.write(new AVIStreamList().toBytes()); + aviOutput.write(new AVIStreamHeader().toBytes()); + aviOutput.write(new AVIStreamFormat().toBytes()); + aviOutput.write(new AVIJunk().toBytes()); + aviMovieOffset = aviChannel.position(); + aviOutput.write(new AVIMovieList().toBytes()); + indexlist = new AVIIndexList(); + } + + public void addImage(Bitmap image) throws Exception { + addImage(image, 0.8f); + } + + public void addImage(Bitmap image, float quality) throws Exception { + addImage(writeImageToBytes(image, quality)); + } + + public void addImage(byte[] imagedata) throws Exception { + byte[] fcc = new byte[]{'0', '0', 'd', 'b'}; + int useLength = imagedata.length; + long position = aviChannel.position(); + int extra = (useLength + (int) position) % 4; + if (extra > 0) { + useLength = useLength + extra; + } + + indexlist.addAVIIndex((int) position, useLength); + + aviOutput.write(fcc); + aviOutput.write(intBytes(swapInt(useLength))); + aviOutput.write(imagedata); + if (extra > 0) { + for (int i = 0; i < extra; i++) { + aviOutput.write(0); + } + } + imagedata = null; + + numFrames++; //add a frame + } + + public void finishAVI() throws Exception { + logger.log(Level.INFO, "finishAVI"); + byte[] indexlistBytes = indexlist.toBytes(); + aviOutput.write(indexlistBytes); + aviOutput.close(); + int fileSize = (int)aviFile.length(); + logger.log(Level.INFO, "fileSize: {0}", fileSize); + int listSize = (int) (fileSize - 8 - aviMovieOffset - indexlistBytes.length); + logger.log(Level.INFO, "listSize: {0}", listSize); + logger.log(Level.INFO, "aviFile canWrite: {0}", aviFile.canWrite()); + logger.log(Level.INFO, "aviFile AbsolutePath: {0}", aviFile.getAbsolutePath()); + logger.log(Level.INFO, "aviFile numFrames: {0}", numFrames); + + RandomAccessFile raf = new RandomAccessFile(aviFile, "rw"); + + //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(); + } + + // public void writeAVI(File file) throws Exception + // { + // OutputStream os = new FileOutputStream(file); + // + // // RIFFHeader + // // AVIMainHeader + // // AVIStreamList + // // AVIStreamHeader + // // AVIStreamFormat + // // write 00db and image bytes... + // } + public static int swapInt(int v) { + return (v >>> 24) | (v << 24) | ((v << 8) & 0x00FF0000) | ((v >> 8) & 0x0000FF00); + } + + public static short swapShort(short v) { + return (short) ((v >>> 8) | (v << 8)); + } + + public static byte[] intBytes(int i) { + byte[] b = new byte[4]; + b[0] = (byte) (i >>> 24); + b[1] = (byte) ((i >>> 16) & 0x000000FF); + b[2] = (byte) ((i >>> 8) & 0x000000FF); + b[3] = (byte) (i & 0x000000FF); + + return b; + } + + public static byte[] shortBytes(short i) { + byte[] b = new byte[2]; + b[0] = (byte) (i >>> 8); + b[1] = (byte) (i & 0x000000FF); + + return b; + } + + private class RIFFHeader { + + public byte[] fcc = new byte[]{'R', 'I', 'F', 'F'}; + public int fileSize = 0; + public byte[] fcc2 = new byte[]{'A', 'V', 'I', ' '}; + public byte[] fcc3 = new byte[]{'L', 'I', 'S', 'T'}; + public int listSize = 200; + public byte[] fcc4 = new byte[]{'h', 'd', 'r', 'l'}; + + public RIFFHeader() { + } + + public RIFFHeader(int fileSize) { + this.fileSize = fileSize; + } + + public byte[] toBytes() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(fcc); + baos.write(intBytes(swapInt(fileSize))); + baos.write(fcc2); + baos.write(fcc3); + baos.write(intBytes(swapInt(listSize))); + baos.write(fcc4); + baos.close(); + + return baos.toByteArray(); + } + } + + private class AVIMainHeader { + /* + * + * FOURCC fcc; DWORD cb; DWORD dwMicroSecPerFrame; DWORD + * dwMaxBytesPerSec; DWORD dwPaddingGranularity; DWORD dwFlags; DWORD + * dwTotalFrames; DWORD dwInitialFrames; DWORD dwStreams; DWORD + * dwSuggestedBufferSize; DWORD dwWidth; DWORD dwHeight; DWORD + * dwReserved[4]; + */ + + public byte[] fcc = new byte[]{'a', 'v', 'i', 'h'}; + public int cb = 56; + public int dwMicroSecPerFrame = 0; // (1 + // / + // frames + // per + // sec) + // * + // 1,000,000 + public int dwMaxBytesPerSec = 10000000; + public int dwPaddingGranularity = 0; + public int dwFlags = 65552; + public int dwTotalFrames = 0; // replace + // with + // correct + // value + public int dwInitialFrames = 0; + public int dwStreams = 1; + public int dwSuggestedBufferSize = 0; + public int dwWidth = 0; // replace + // with + // correct + // value + public int dwHeight = 0; // replace + // with + // correct + // value + public int[] dwReserved = new int[4]; + + public AVIMainHeader() { + dwMicroSecPerFrame = (int) ((1.0 / framerate) * 1000000.0); + dwWidth = width; + dwHeight = height; + dwTotalFrames = numFrames; + } + + public byte[] toBytes() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(fcc); + baos.write(intBytes(swapInt(cb))); + baos.write(intBytes(swapInt(dwMicroSecPerFrame))); + baos.write(intBytes(swapInt(dwMaxBytesPerSec))); + baos.write(intBytes(swapInt(dwPaddingGranularity))); + baos.write(intBytes(swapInt(dwFlags))); + baos.write(intBytes(swapInt(dwTotalFrames))); + baos.write(intBytes(swapInt(dwInitialFrames))); + baos.write(intBytes(swapInt(dwStreams))); + baos.write(intBytes(swapInt(dwSuggestedBufferSize))); + baos.write(intBytes(swapInt(dwWidth))); + baos.write(intBytes(swapInt(dwHeight))); + baos.write(intBytes(swapInt(dwReserved[0]))); + baos.write(intBytes(swapInt(dwReserved[1]))); + baos.write(intBytes(swapInt(dwReserved[2]))); + baos.write(intBytes(swapInt(dwReserved[3]))); + baos.close(); + + return baos.toByteArray(); + } + } + + private class AVIStreamList { + + public byte[] fcc = new byte[]{'L', 'I', 'S', 'T'}; + public int size = 124; + public byte[] fcc2 = new byte[]{'s', 't', 'r', 'l'}; + + public AVIStreamList() { + } + + public byte[] toBytes() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(fcc); + baos.write(intBytes(swapInt(size))); + baos.write(fcc2); + baos.close(); + + return baos.toByteArray(); + } + } + + private class AVIStreamHeader { + /* + * FOURCC fcc; DWORD cb; FOURCC fccType; FOURCC fccHandler; DWORD + * dwFlags; WORD wPriority; WORD wLanguage; DWORD dwInitialFrames; DWORD + * dwScale; DWORD dwRate; DWORD dwStart; DWORD dwLength; DWORD + * dwSuggestedBufferSize; DWORD dwQuality; DWORD dwSampleSize; struct { + * short int left; short int top; short int right; short int bottom; } + * rcFrame; + */ + + public byte[] fcc = new byte[]{'s', 't', 'r', 'h'}; + public int cb = 64; + public byte[] fccType = new byte[]{'v', 'i', 'd', 's'}; + public byte[] fccHandler = new byte[]{'M', 'J', 'P', 'G'}; + public int dwFlags = 0; + public short wPriority = 0; + public short wLanguage = 0; + public int dwInitialFrames = 0; + public int dwScale = 0; // microseconds + // per + // frame + public int dwRate = 1000000; // dwRate + // / + // dwScale + // = + // frame + // rate + public int dwStart = 0; + public int dwLength = 0; // num + // frames + public int dwSuggestedBufferSize = 0; + public int dwQuality = -1; + public int dwSampleSize = 0; + public int left = 0; + public int top = 0; + public int right = 0; + public int bottom = 0; + + public AVIStreamHeader() { + dwScale = (int) ((1.0 / framerate) * 1000000.0); + dwLength = numFrames; + } + + public byte[] toBytes() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(fcc); + baos.write(intBytes(swapInt(cb))); + baos.write(fccType); + baos.write(fccHandler); + baos.write(intBytes(swapInt(dwFlags))); + baos.write(shortBytes(swapShort(wPriority))); + baos.write(shortBytes(swapShort(wLanguage))); + baos.write(intBytes(swapInt(dwInitialFrames))); + baos.write(intBytes(swapInt(dwScale))); + baos.write(intBytes(swapInt(dwRate))); + baos.write(intBytes(swapInt(dwStart))); + baos.write(intBytes(swapInt(dwLength))); + baos.write(intBytes(swapInt(dwSuggestedBufferSize))); + baos.write(intBytes(swapInt(dwQuality))); + baos.write(intBytes(swapInt(dwSampleSize))); + baos.write(intBytes(swapInt(left))); + baos.write(intBytes(swapInt(top))); + baos.write(intBytes(swapInt(right))); + baos.write(intBytes(swapInt(bottom))); + baos.close(); + + return baos.toByteArray(); + } + } + + private class AVIStreamFormat { + /* + * FOURCC fcc; DWORD cb; DWORD biSize; LONG biWidth; LONG biHeight; WORD + * biPlanes; WORD biBitCount; DWORD biCompression; DWORD biSizeImage; + * LONG biXPelsPerMeter; LONG biYPelsPerMeter; DWORD biClrUsed; DWORD + * biClrImportant; + */ + + public byte[] fcc = new byte[]{'s', 't', 'r', 'f'}; + public int cb = 40; + public int biSize = 40; // same + // as + // cb + public int biWidth = 0; + public int biHeight = 0; + public short biPlanes = 1; + public short biBitCount = 24; + public byte[] biCompression = new byte[]{'M', 'J', 'P', 'G'}; + public int biSizeImage = 0; // width + // x + // height + // in + // pixels + public int biXPelsPerMeter = 0; + public int biYPelsPerMeter = 0; + public int biClrUsed = 0; + public int biClrImportant = 0; + + public AVIStreamFormat() { + biWidth = width; + biHeight = height; + biSizeImage = width * height; + } + + public byte[] toBytes() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(fcc); + baos.write(intBytes(swapInt(cb))); + baos.write(intBytes(swapInt(biSize))); + baos.write(intBytes(swapInt(biWidth))); + baos.write(intBytes(swapInt(biHeight))); + baos.write(shortBytes(swapShort(biPlanes))); + baos.write(shortBytes(swapShort(biBitCount))); + baos.write(biCompression); + baos.write(intBytes(swapInt(biSizeImage))); + baos.write(intBytes(swapInt(biXPelsPerMeter))); + baos.write(intBytes(swapInt(biYPelsPerMeter))); + baos.write(intBytes(swapInt(biClrUsed))); + baos.write(intBytes(swapInt(biClrImportant))); + baos.close(); + + return baos.toByteArray(); + } + } + + private class AVIMovieList { + + public byte[] fcc = new byte[]{'L', 'I', 'S', 'T'}; + public int listSize = 0; + public byte[] fcc2 = new byte[]{'m', 'o', 'v', 'i'}; + + // 00db size jpg image data ... + public AVIMovieList() { + } + + public AVIMovieList(int listSize) { + this.listSize = listSize; + } + + public byte[] toBytes() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(fcc); + baos.write(intBytes(swapInt(listSize))); + baos.write(fcc2); + baos.close(); + + return baos.toByteArray(); + } + } + + private class AVIIndexList { + + public byte[] fcc = new byte[]{'i', 'd', 'x', '1'}; + public int cb = 0; + public List ind = new ArrayList(); + + public AVIIndexList() { + } + + @SuppressWarnings("unused") + public void addAVIIndex(AVIIndex ai) { + ind.add(ai); + } + + public void addAVIIndex(int dwOffset, int dwSize) { + ind.add(new AVIIndex(dwOffset, dwSize)); + } + + public byte[] toBytes() throws Exception { + cb = 16 * ind.size(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(fcc); + baos.write(intBytes(swapInt(cb))); + for (int i = 0; i < ind.size(); i++) { + AVIIndex in = (AVIIndex) ind.get(i); + baos.write(in.toBytes()); + } + + baos.close(); + + return baos.toByteArray(); + } + } + + private class AVIIndex { + + public byte[] fcc = new byte[]{'0', '0', 'd', 'b'}; + public int dwFlags = 16; + public int dwOffset = 0; + public int dwSize = 0; + + public AVIIndex(int dwOffset, int dwSize) { + this.dwOffset = dwOffset; + this.dwSize = dwSize; + } + + public byte[] toBytes() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(fcc); + baos.write(intBytes(swapInt(dwFlags))); + baos.write(intBytes(swapInt(dwOffset))); + baos.write(intBytes(swapInt(dwSize))); + baos.close(); + + return baos.toByteArray(); + } + } + + private class AVIJunk { + + public byte[] fcc = new byte[]{'J', 'U', 'N', 'K'}; + public int size = 1808; + public byte[] data = new byte[size]; + + public AVIJunk() { + Arrays.fill(data, (byte) 0); + } + + public byte[] toBytes() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(fcc); + baos.write(intBytes(swapInt(size))); + baos.write(data); + baos.close(); + + return baos.toByteArray(); + } + } + + public byte[] writeImageToBytes(Bitmap image, float quality) throws Exception { + Bitmap bi; + if (image.getConfig() == Bitmap.Config.RGB_565) { + bi = image; + } else { + bi = image.copy(Bitmap.Config.RGB_565, false); + if (bi == null) { + throw new IllegalStateException("Could not convert Bitmap to RGB_565"); + } + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + bi.compress(Bitmap.CompressFormat.JPEG, (int)(quality*100), baos); + baos.close(); + return baos.toByteArray(); + } +} diff --git a/jme3-android/src/main/java/com/jme3/app/state/VideoRecorderAppState.java b/jme3-android/src/main/java/com/jme3/app/state/VideoRecorderAppState.java new file mode 100644 index 000000000..6c9669df1 --- /dev/null +++ b/jme3-android/src/main/java/com/jme3/app/state/VideoRecorderAppState.java @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.app.state; + +import android.graphics.Bitmap; +import com.jme3.app.Application; +import com.jme3.post.SceneProcessor; +import com.jme3.renderer.Camera; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.Renderer; +import com.jme3.renderer.ViewPort; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.system.JmeSystem; +import com.jme3.system.Timer; +import com.jme3.texture.FrameBuffer; +import com.jme3.util.AndroidScreenshots; +import com.jme3.util.BufferUtils; +import java.io.File; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A Video recording AppState that records the screen output into an AVI file with + * M-JPEG content. The file should be playable on any OS in any video player.
+ * The video recording starts when the state is attached and stops when it is detached + * or the application is quit. You can set the fileName of the file to be written when the + * 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, entrusC + */ +public class VideoRecorderAppState extends AbstractAppState { + private static final Logger logger = Logger.getLogger(VideoRecorderAppState.class.getName()); + + private int numFrames = 0; + private int framerate = 30; + private VideoProcessor processor; + private File file; + private Application app; + private ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactory() { + + public Thread newThread(Runnable r) { + Thread th = new Thread(r); + th.setName("jME Video Processing Thread"); + th.setDaemon(true); + return th; + } + }); + private int numCpus = Runtime.getRuntime().availableProcessors(); + private ViewPort lastViewPort; + private float quality; + private Timer oldTimer; + + /** + * Using this constructor the video files will be written sequentially to the user's home directory with + * a quality of 0.8 and a framerate of 30fps. + */ + public VideoRecorderAppState() { + 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); + } + + /** + * 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) + * @param framerate the frame rate of the resulting video, the application will be locked to this framerate + */ + public VideoRecorderAppState(float quality, int framerate) { + this(null, quality, framerate); + } + + /** + * This constructor allows you to specify the output file of the video. The quality is set + * to 0.8 and framerate to 30 fps. + * @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) + * @param framerate the frame rate of the resulting video, the application will be locked to this framerate + */ + public VideoRecorderAppState(File file, float quality) { + this.file = file; + this.quality = quality; + Logger.getLogger(this.getClass().getName()).log(Level.FINE, "JME3 VideoRecorder running on {0} CPU's", numCpus); + } + + /** + * 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, int framerate) { + this.file = file; + this.quality = quality; + this.framerate = framerate; + Logger.getLogger(this.getClass().getName()).log(Level.FINE, "JME3 VideoRecorder running on {0} CPU's", numCpus); + } + + public File getFile() { + return file; + } + + public void setFile(File file) { + if (isInitialized()) { + throw new IllegalStateException("Cannot set file while attached!"); + } + 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); + this.app = app; + this.oldTimer = app.getTimer(); + app.setTimer(new IsoTimer(framerate)); + if (file == null) { + String filename = JmeSystem.getStorageFolder(JmeSystem.StorageFolderType.External) + File.separator + "jMonkey-" + System.currentTimeMillis() / 1000 + ".avi"; + logger.log(Level.INFO, "fileName: {0}", filename); + file = new File(filename); + } + processor = new VideoProcessor(); + List vps = app.getRenderManager().getPostViews(); + + for (int i = vps.size() - 1; i >= 0; i-- ) { + lastViewPort = vps.get(i); + if (lastViewPort.isEnabled()) { + break; + } + } + lastViewPort.addProcessor(processor); + } + + @Override + public void cleanup() { + logger.log(Level.INFO, "removing processor"); + lastViewPort.removeProcessor(processor); + app.setTimer(oldTimer); + initialized = false; + file = null; + super.cleanup(); + } + + private class WorkItem { + + ByteBuffer buffer; + Bitmap image; + byte[] data; + + public WorkItem(int width, int height) { + image = Bitmap.createBitmap(width, height, + Bitmap.Config.ARGB_8888); + buffer = BufferUtils.createByteBuffer(width * height * 4); + } + } + + private class VideoProcessor implements SceneProcessor { + + private Camera camera; + private int width; + private int height; + private RenderManager renderManager; + private boolean isInitilized = false; + private LinkedBlockingQueue freeItems; + private LinkedBlockingQueue usedItems = new LinkedBlockingQueue(); + private MjpegFileWriter writer; + private boolean fastMode = true; + + public void addImage(Renderer renderer, FrameBuffer out) { + if (freeItems == null) { + return; + } + try { + final WorkItem item = freeItems.take(); + usedItems.add(item); + item.buffer.clear(); + renderer.readFrameBuffer(out, item.buffer); + executor.submit(new Callable() { + + public Void call() throws Exception { + if (fastMode) { + item.data = item.buffer.array(); + } else { + AndroidScreenshots.convertScreenShot(item.buffer, item.image); + item.data = writer.writeImageToBytes(item.image, quality); + } + while (usedItems.peek() != item) { + Thread.sleep(1); + } + writer.addImage(item.data); + usedItems.poll(); + freeItems.add(item); + return null; + } + }); + } catch (InterruptedException ex) { + Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, null, ex); + } + } + + public void initialize(RenderManager rm, ViewPort viewPort) { + logger.log(Level.INFO, "initialize in VideoProcessor"); + this.camera = viewPort.getCamera(); + this.width = camera.getWidth(); + this.height = camera.getHeight(); + this.renderManager = rm; + this.isInitilized = true; + if (freeItems == null) { + freeItems = new LinkedBlockingQueue(); + for (int i = 0; i < numCpus; i++) { + freeItems.add(new WorkItem(width, height)); + } + } + } + + public void reshape(ViewPort vp, int w, int h) { + } + + public boolean isInitialized() { + return this.isInitilized; + } + + public void preFrame(float tpf) { + if (null == writer) { + try { + writer = new MjpegFileWriter(file, width, height, framerate); + } catch (Exception ex) { + Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error creating file writer: {0}", ex); + } + } + } + + public void postQueue(RenderQueue rq) { + } + + public void postFrame(FrameBuffer out) { + numFrames++; + addImage(renderManager.getRenderer(), out); + } + + public void cleanup() { + logger.log(Level.INFO, "cleanup in VideoProcessor"); + logger.log(Level.INFO, "VideoProcessor numFrames: {0}", numFrames); + try { + while (freeItems.size() < numCpus) { + Thread.sleep(10); + } + logger.log(Level.INFO, "finishAVI in VideoProcessor"); + writer.finishAVI(); + } catch (Exception ex) { + Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video: {0}", ex); + } + writer = null; + } + } + + public static final class IsoTimer extends com.jme3.system.Timer { + + private float framerate; + private int ticks; + private long lastTime = 0; + + public IsoTimer(float framerate) { + this.framerate = framerate; + this.ticks = 0; + } + + public long getTime() { + return (long) (this.ticks * (1.0f / this.framerate) * 1000f); + } + + public long getResolution() { + return 1000L; + } + + public float getFrameRate() { + return this.framerate; + } + + public float getTimePerFrame() { + return (float) (1.0f / this.framerate); + } + + public void update() { + long time = System.currentTimeMillis(); + long difference = time - lastTime; + lastTime = time; + if (difference < (1.0f / this.framerate) * 1000.0f) { + try { + Thread.sleep(difference); + } catch (InterruptedException ex) { + } + } else { + logger.log(Level.INFO, "actual tpf(ms): {0}, 1/framerate(ms): {1}", + new Object[]{difference, (1.0f / this.framerate) * 1000.0f}); + } + this.ticks++; + } + + public void reset() { + this.ticks = 0; + } + } +}