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