• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
VideoRecorderAppState()50     public VideoRecorderAppState() {
51         Logger.getLogger(this.getClass().getName()).log(Level.INFO, "JME3 VideoRecorder running on {0} CPU's", numCpus);
52     }
53 
VideoRecorderAppState(File file)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 
getFile()59     public File getFile() {
60         return file;
61     }
62 
setFile(File file)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
initialize(AppStateManager stateManager, Application app)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
cleanup()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 
WorkItem(int width, int height)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 
addImage(Renderer renderer, FrameBuffer out)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 
initialize(RenderManager rm, ViewPort viewPort)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 
reshape(ViewPort vp, int w, int h)160         public void reshape(ViewPort vp, int w, int h) {
161         }
162 
isInitialized()163         public boolean isInitialized() {
164             return this.isInitilized;
165         }
166 
preFrame(float tpf)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 
postQueue(RenderQueue rq)177         public void postQueue(RenderQueue rq) {
178         }
179 
postFrame(FrameBuffer out)180         public void postFrame(FrameBuffer out) {
181             addImage(renderManager.getRenderer(), out);
182         }
183 
cleanup()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 
IsoTimer(float framerate)203         public IsoTimer(float framerate) {
204             this.framerate = framerate;
205             this.ticks = 0;
206         }
207 
getTime()208         public long getTime() {
209             return (long) (this.ticks * (1.0f / this.framerate) * 1000f);
210         }
211 
getResolution()212         public long getResolution() {
213             return 1000000000L;
214         }
215 
getFrameRate()216         public float getFrameRate() {
217             return this.framerate;
218         }
219 
getTimePerFrame()220         public float getTimePerFrame() {
221             return (float) (1.0f / this.framerate);
222         }
223 
update()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 
reset()237         public void reset() {
238             this.ticks = 0;
239         }
240     }
241 }
242