Home | History | Annotate | Download | only in state
      1 package com.jme3.app.state;
      2 
      3 import com.jme3.app.Application;
      4 import com.jme3.post.SceneProcessor;
      5 import com.jme3.renderer.Camera;
      6 import com.jme3.renderer.RenderManager;
      7 import com.jme3.renderer.Renderer;
      8 import com.jme3.renderer.ViewPort;
      9 import com.jme3.renderer.queue.RenderQueue;
     10 import com.jme3.system.NanoTimer;
     11 import com.jme3.texture.FrameBuffer;
     12 import com.jme3.util.BufferUtils;
     13 import com.jme3.util.Screenshots;
     14 import java.awt.image.BufferedImage;
     15 import java.io.File;
     16 import java.nio.ByteBuffer;
     17 import java.util.List;
     18 import java.util.concurrent.*;
     19 import java.util.logging.Level;
     20 import 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  */
     32 public 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