Adding a VideoRecorderAppState for Android. Performance is really bad, but it will at least run and store the AVI. Not really intended for app usage, but it's been handy in a couple of places. Just committing to lot lose the changes necessary to make it run. More work required, for sure.

experimental
iwgeric 11 years ago
parent 37dd89c8f1
commit 7c8fa29b26
  1. 552
      jme3-android/src/main/java/com/jme3/app/state/MjpegFileWriter.java
  2. 364
      jme3-android/src/main/java/com/jme3/app/state/VideoRecorderAppState.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<AVIIndex> ind = new ArrayList<AVIIndex>();
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();
}
}

@ -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.<br/>
* 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<ViewPort> 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<WorkItem> freeItems;
private LinkedBlockingQueue<WorkItem> usedItems = new LinkedBlockingQueue<WorkItem>();
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<Void>() {
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<WorkItem>();
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;
}
}
}
Loading…
Cancel
Save