1package com.jme3.app.state; 2 3import com.jme3.app.Application; 4import com.jme3.post.SceneProcessor; 5import com.jme3.renderer.Camera; 6import com.jme3.renderer.RenderManager; 7import com.jme3.renderer.Renderer; 8import com.jme3.renderer.ViewPort; 9import com.jme3.renderer.queue.RenderQueue; 10import com.jme3.system.NanoTimer; 11import com.jme3.texture.FrameBuffer; 12import com.jme3.util.BufferUtils; 13import com.jme3.util.Screenshots; 14import java.awt.image.BufferedImage; 15import java.io.File; 16import java.nio.ByteBuffer; 17import java.util.List; 18import java.util.concurrent.*; 19import java.util.logging.Level; 20import java.util.logging.Logger; 21 22/** 23 * A Video recording AppState that records the screen output into an AVI file with 24 * M-JPEG content. The file should be playable on any OS in any video player.<br/> 25 * The video recording starts when the state is attached and stops when it is detached 26 * or the application is quit. You can set the fileName of the file to be written when the 27 * state is detached, else the old file will be overwritten. If you specify no file 28 * the AppState will attempt to write a file into the user home directory, made unique 29 * by a timestamp. 30 * @author normenhansen, Robert McIntyre 31 */ 32public class VideoRecorderAppState extends AbstractAppState { 33 34 private int framerate = 30; 35 private VideoProcessor processor; 36 private File file; 37 private Application app; 38 private ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactory() { 39 40 public Thread newThread(Runnable r) { 41 Thread th = new Thread(r); 42 th.setName("jME Video Processing Thread"); 43 th.setDaemon(true); 44 return th; 45 } 46 }); 47 private int numCpus = Runtime.getRuntime().availableProcessors(); 48 private ViewPort lastViewPort; 49 50 public VideoRecorderAppState() { 51 Logger.getLogger(this.getClass().getName()).log(Level.INFO, "JME3 VideoRecorder running on {0} CPU's", numCpus); 52 } 53 54 public VideoRecorderAppState(File file) { 55 this.file = file; 56 Logger.getLogger(this.getClass().getName()).log(Level.INFO, "JME3 VideoRecorder running on {0} CPU's", numCpus); 57 } 58 59 public File getFile() { 60 return file; 61 } 62 63 public void setFile(File file) { 64 if (isInitialized()) { 65 throw new IllegalStateException("Cannot set file while attached!"); 66 } 67 this.file = file; 68 } 69 70 @Override 71 public void initialize(AppStateManager stateManager, Application app) { 72 super.initialize(stateManager, app); 73 this.app = app; 74 app.setTimer(new IsoTimer(framerate)); 75 if (file == null) { 76 String filename = System.getProperty("user.home") + File.separator + "jMonkey-" + System.currentTimeMillis() / 1000 + ".avi"; 77 file = new File(filename); 78 } 79 processor = new VideoProcessor(); 80 List<ViewPort> vps = app.getRenderManager().getPostViews(); 81 lastViewPort = vps.get(vps.size()-1); 82 lastViewPort.addProcessor(processor); 83 } 84 85 @Override 86 public void cleanup() { 87 lastViewPort.removeProcessor(processor); 88 app.setTimer(new NanoTimer()); 89 initialized = false; 90 file = null; 91 super.cleanup(); 92 } 93 94 private class WorkItem { 95 96 ByteBuffer buffer; 97 BufferedImage image; 98 byte[] data; 99 100 public WorkItem(int width, int height) { 101 image = new BufferedImage(width, height, 102 BufferedImage.TYPE_4BYTE_ABGR); 103 buffer = BufferUtils.createByteBuffer(width * height * 4); 104 } 105 } 106 107 private class VideoProcessor implements SceneProcessor { 108 109 private Camera camera; 110 private int width; 111 private int height; 112 private RenderManager renderManager; 113 private boolean isInitilized = false; 114 private LinkedBlockingQueue<WorkItem> freeItems; 115 private LinkedBlockingQueue<WorkItem> usedItems = new LinkedBlockingQueue<WorkItem>(); 116 private MjpegFileWriter writer; 117 118 public void addImage(Renderer renderer, FrameBuffer out) { 119 if (freeItems == null) { 120 return; 121 } 122 try { 123 final WorkItem item = freeItems.take(); 124 usedItems.add(item); 125 item.buffer.clear(); 126 renderer.readFrameBuffer(out, item.buffer); 127 executor.submit(new Callable<Void>() { 128 129 public Void call() throws Exception { 130 Screenshots.convertScreenShot(item.buffer, item.image); 131 item.data = writer.writeImageToBytes(item.image); 132 while (usedItems.peek() != item) { 133 Thread.sleep(1); 134 } 135 writer.addImage(item.data); 136 usedItems.poll(); 137 freeItems.add(item); 138 return null; 139 } 140 }); 141 } catch (InterruptedException ex) { 142 Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, null, ex); 143 } 144 } 145 146 public void initialize(RenderManager rm, ViewPort viewPort) { 147 this.camera = viewPort.getCamera(); 148 this.width = camera.getWidth(); 149 this.height = camera.getHeight(); 150 this.renderManager = rm; 151 this.isInitilized = true; 152 if (freeItems == null) { 153 freeItems = new LinkedBlockingQueue<WorkItem>(); 154 for (int i = 0; i < numCpus; i++) { 155 freeItems.add(new WorkItem(width, height)); 156 } 157 } 158 } 159 160 public void reshape(ViewPort vp, int w, int h) { 161 } 162 163 public boolean isInitialized() { 164 return this.isInitilized; 165 } 166 167 public void preFrame(float tpf) { 168 if (null == writer) { 169 try { 170 writer = new MjpegFileWriter(file, width, height, framerate); 171 } catch (Exception ex) { 172 Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error creating file writer: {0}", ex); 173 } 174 } 175 } 176 177 public void postQueue(RenderQueue rq) { 178 } 179 180 public void postFrame(FrameBuffer out) { 181 addImage(renderManager.getRenderer(), out); 182 } 183 184 public void cleanup() { 185 try { 186 while (freeItems.size() < numCpus) { 187 Thread.sleep(10); 188 } 189 writer.finishAVI(); 190 } catch (Exception ex) { 191 Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video: {0}", ex); 192 } 193 writer = null; 194 } 195 } 196 197 public static final class IsoTimer extends com.jme3.system.Timer { 198 199 private float framerate; 200 private int ticks; 201 private long lastTime = 0; 202 203 public IsoTimer(float framerate) { 204 this.framerate = framerate; 205 this.ticks = 0; 206 } 207 208 public long getTime() { 209 return (long) (this.ticks * (1.0f / this.framerate) * 1000f); 210 } 211 212 public long getResolution() { 213 return 1000000000L; 214 } 215 216 public float getFrameRate() { 217 return this.framerate; 218 } 219 220 public float getTimePerFrame() { 221 return (float) (1.0f / this.framerate); 222 } 223 224 public void update() { 225 long time = System.currentTimeMillis(); 226 long difference = time - lastTime; 227 lastTime = time; 228 if (difference < (1.0f / this.framerate) * 1000.0f) { 229 try { 230 Thread.sleep(difference); 231 } catch (InterruptedException ex) { 232 } 233 } 234 this.ticks++; 235 } 236 237 public void reset() { 238 this.ticks = 0; 239 } 240 } 241} 242