com.jme3.app.state.VideoRecorderAppState Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jme3-android Show documentation
Show all versions of jme3-android Show documentation
jMonkeyEngine is a 3D game engine for adventurous Java developers
/*
* Copyright (c) 2009-2021 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.profile.AppProfiler;
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.texture.Image;
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() {
@Override
public Thread newThread(Runnable r) {
Thread th = new Thread(r);
th.setName("jME3 Video Processor");
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)
*/
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)
* @param framerate the frame rate of the resulting video, the application
* will be locked to this framerate
*/
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 isInitialized = 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.readFrameBufferWithFormat(out, item.buffer, Image.Format.BGRA8);
executor.submit(new Callable() {
@Override
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);
}
}
@Override
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.isInitialized = true;
if (freeItems == null) {
freeItems = new LinkedBlockingQueue();
for (int i = 0; i < numCpus; i++) {
freeItems.add(new WorkItem(width, height));
}
}
}
@Override
public void reshape(ViewPort vp, int w, int h) {
}
@Override
public boolean isInitialized() {
return this.isInitialized;
}
@Override
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);
}
}
}
@Override
public void postQueue(RenderQueue rq) {
}
@Override
public void postFrame(FrameBuffer out) {
numFrames++;
addImage(renderManager.getRenderer(), out);
}
@Override
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;
}
@Override
public void setProfiler(AppProfiler profiler) {
// not implemented
}
}
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;
}
@Override
public long getTime() {
return (long) (this.ticks * (1.0f / this.framerate) * 1000f);
}
@Override
public long getResolution() {
return 1000L;
}
@Override
public float getFrameRate() {
return this.framerate;
}
@Override
public float getTimePerFrame() {
return 1.0f / this.framerate;
}
@Override
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 if (logger.isLoggable(Level.INFO)) {
logger.log(Level.INFO, "actual tpf(ms): {0}, 1/framerate(ms): {1}",
new Object[]{difference, (1.0f / this.framerate) * 1000.0f});
}
this.ticks++;
}
@Override
public void reset() {
this.ticks = 0;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy