package com.jme3.app.state; 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.NanoTimer; import com.jme3.texture.FrameBuffer; import com.jme3.util.BufferUtils; import com.jme3.util.Screenshots; import java.awt.image.BufferedImage; 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 */ public class VideoRecorderAppState extends AbstractAppState { 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; public VideoRecorderAppState() { Logger.getLogger(this.getClass().getName()).log(Level.INFO, "JME3 VideoRecorder running on {0} CPU's", numCpus); } public VideoRecorderAppState(File file) { this.file = file; Logger.getLogger(this.getClass().getName()).log(Level.INFO, "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; } @Override public void initialize(AppStateManager stateManager, Application app) { super.initialize(stateManager, app); this.app = app; app.setTimer(new IsoTimer(framerate)); if (file == null) { String filename = System.getProperty("user.home") + File.separator + "jMonkey-" + System.currentTimeMillis() / 1000 + ".avi"; file = new File(filename); } processor = new VideoProcessor(); List vps = app.getRenderManager().getPostViews(); lastViewPort = vps.get(vps.size()-1); lastViewPort.addProcessor(processor); } @Override public void cleanup() { lastViewPort.removeProcessor(processor); app.setTimer(new NanoTimer()); initialized = false; file = null; super.cleanup(); } private class WorkItem { ByteBuffer buffer; BufferedImage image; byte[] data; public WorkItem(int width, int height) { image = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); 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; 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 { Screenshots.convertScreenShot(item.buffer, item.image); item.data = writer.writeImageToBytes(item.image); 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) { 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) { addImage(renderManager.getRenderer(), out); } public void cleanup() { try { while (freeItems.size() < numCpus) { Thread.sleep(10); } 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 1000000000L; } 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) { } } this.ticks++; } public void reset() { this.ticks = 0; } } }