rlm@0
|
1 #+title: Capture Live Video Feeds from JMonkeyEngine
|
rlm@0
|
2 #+author: Robert McIntyre
|
rlm@0
|
3 #+email: rlm@mit.edu
|
rlm@0
|
4 #+description: Capture video from a JMonkeyEngine3 Application with Xuggle, and use gstreamer to compress the video to upload to YouTube.
|
rlm@0
|
5 #+keywords: JME3, video, Xuggle, JMonkeyEngine, youtube, capture video, Java
|
rlm@4
|
6 #+SETUPFILE: ../../aurellem/org/setup.org
|
rlm@4
|
7 #+INCLUDE: ../../aurellem/org/level-0.org
|
rlm@0
|
8
|
rlm@0
|
9
|
rlm@0
|
10 * The Problem
|
rlm@0
|
11 So you've made your cool new JMonkeyEngine3 game and you want to
|
rlm@0
|
12 create a demo video to show off your hard work. Screen capturing is
|
rlm@0
|
13 the most straightforward way to do this, but it can slow down your
|
rlm@0
|
14 game and produce low-quality video as a result. A better way is to
|
rlm@0
|
15 record a video feed directly from the game while it is
|
rlm@0
|
16 running.
|
rlm@0
|
17
|
rlm@0
|
18 In this post, I'll explain how you can alter your JMonkeyEngine3 game
|
rlm@0
|
19 to output video while it is running. The main trick is to alter the
|
rlm@0
|
20 pace of JMonkeyEngine3's in-game time: we allow the engine as much
|
rlm@0
|
21 time as it needs to compute complicated in-game events and to encode
|
rlm@0
|
22 video frames. As a result, the game appears to speed up and slow down
|
rlm@0
|
23 as the computational demands shift, but the end result is perfectly
|
rlm@0
|
24 smooth video output at a constant framerate.
|
rlm@0
|
25
|
rlm@0
|
26
|
rlm@0
|
27 * Game-time vs. User-time vs. Video-time
|
rlm@0
|
28
|
rlm@0
|
29 A standard JME3 application that extends =SimpleApplication= or
|
rlm@0
|
30 =Application= tries as hard as it can to keep in sync with
|
rlm@0
|
31 /user-time/. If a ball is rolling at 1 game-mile per game-hour in the
|
rlm@0
|
32 game, and you wait for one user-hour as measured by the clock on your
|
rlm@0
|
33 wall, then the ball should have traveled exactly one game-mile. In
|
rlm@0
|
34 order to keep sync with the real world, the game throttles its physics
|
rlm@0
|
35 engine and graphics display. If the computations involved in running
|
rlm@0
|
36 the game are too intense, then the game will first skip frames, then
|
rlm@0
|
37 sacrifice physics accuracy. If there are particuraly demanding
|
rlm@0
|
38 computations, then you may only get 1 fps, and the ball may tunnel
|
rlm@0
|
39 through the floor or obstacles due to inaccurate physics simulation,
|
rlm@0
|
40 but after the end of one user-hour, that ball will have traveled one
|
rlm@0
|
41 game-mile.
|
rlm@0
|
42
|
rlm@0
|
43 When we're recording video, we don't care if the game-time syncs with
|
rlm@0
|
44 user-time, but instead whether the time in the recorded video
|
rlm@0
|
45 (video-time) syncs with user-time. To continue the analogy, if we
|
rlm@0
|
46 recorded the ball rolling at 1 game-mile per game-hour and watched the
|
rlm@0
|
47 video later, we would want to see 30 fps video of the ball rolling at
|
rlm@0
|
48 1 video-mile per /user-hour/. It doesn't matter how much user-time it
|
rlm@0
|
49 took to simulate that hour of game-time to make the high-quality
|
rlm@0
|
50 recording.
|
rlm@0
|
51
|
rlm@0
|
52 * COMMENT Two examples to clarify the point:
|
rlm@0
|
53 ** Recording from a Simple Simulation
|
rlm@0
|
54
|
rlm@0
|
55 *** Without a Special Timer
|
rlm@0
|
56 You have a simulation of a ball rolling on an infinite empty plane at
|
rlm@0
|
57 one game-mile per game-hour, and a really good computer. Normally,
|
rlm@0
|
58 JME3 will throttle the physics engine and graphics display to sync the
|
rlm@0
|
59 game-time with user-time. If it takes one-thousandth of a second
|
rlm@0
|
60 user-time to simulate one-sixtieth of a second game time and another
|
rlm@0
|
61 one-thousandth of a second to draw to the screen, then JME3 will just
|
rlm@0
|
62 sit around for the remainder of $\frac{1}{60} - \frac{2}{1000}$
|
rlm@0
|
63 user-seconds, then calculate the next frame in $\frac{2}{1000}$
|
rlm@0
|
64 user-seconds, then wait, and so on. For every second of user time that
|
rlm@0
|
65 passes, one second of game-time passes, and the game will run at 60
|
rlm@0
|
66 frames per user-second.
|
rlm@0
|
67
|
rlm@0
|
68
|
rlm@0
|
69 *** With a Special Timer
|
rlm@0
|
70 Then, you change the game's timer so that user-time will be synced to
|
rlm@0
|
71 video-time. Assume that encoding a single frame takes 0 seconds
|
rlm@0
|
72 user-time to complete.
|
rlm@0
|
73
|
rlm@0
|
74 Now, JME3 takes advantage of all available resources. It still takes
|
rlm@0
|
75 one-thousandth of a second to calculate a physics tick, and another
|
rlm@0
|
76 one-thousandth to render to the screen. Then it takes 0 seconds to
|
rlm@0
|
77 write the video frame to disk and encode the video. In only one second
|
rlm@0
|
78 of user time, JME3 will complete 500 physics-tick/render/encode-video
|
rlm@0
|
79 cycles, and $\frac{500}{60}=8\frac{1}{3}$ seconds of game-time will
|
rlm@0
|
80 have passed. Game-time appears to dilate $8\frac{1}{3}\times$ with
|
rlm@0
|
81 respect to user-time, and in only 7.2 minutes user-time, one hour of
|
rlm@0
|
82 video will have been recorded. The game itself will run at 500 fps.
|
rlm@0
|
83 When someone watches the video, they will see 60 frames per
|
rlm@0
|
84 user-second, and $\frac{1}{60}$ video-seconds will pass each frame. It
|
rlm@0
|
85 will take exactly one hour user-time (and one hour video-time) for the
|
rlm@0
|
86 ball in the video to travel one video-mile.
|
rlm@0
|
87
|
rlm@0
|
88 ** Recording from a Complex Simulation
|
rlm@0
|
89
|
rlm@0
|
90 *** Without a Special Timer
|
rlm@0
|
91 You have a simulation of a ball rolling on an infinite empty plane at
|
rlm@0
|
92 one game-mile per game-hour accompanied by multiple explosions
|
rlm@0
|
93 involving thousands of nodes, particle effects, and complicated shadow
|
rlm@0
|
94 shaders to create realistic shadows. You also have a slow
|
rlm@0
|
95 laptop. Normally, JME3 must sacrifice rendering and physics simulation
|
rlm@0
|
96 to try to keep up. If it takes $\frac{1}{120}$ of a user-second to
|
rlm@0
|
97 calculate $\frac{1}{60}$ game-seconds, and an additional
|
rlm@0
|
98 $\frac{1}{60}$ of a user-second to render to screen, then JME3 has
|
rlm@0
|
99 it's work cut out for it. In order to render to the screen, it will
|
rlm@0
|
100 first step the game forward by up to four physics ticks before
|
rlm@0
|
101 rendering to the screen. If it still isn't fast enough then it will
|
rlm@0
|
102 decrease the accuracy of the physics engine until game-time and user
|
rlm@0
|
103 time are synched or a certain threshold is reached, at which point the
|
rlm@0
|
104 game visibly slows down. In this case, JME3 continuously repeat a
|
rlm@0
|
105 cycle of two physics ticks, and one screen render. For every
|
rlm@0
|
106 user-second that passes, one game-second will pass, but the game will
|
rlm@0
|
107 run at 30 fps instead of 60 fps like before.
|
rlm@0
|
108
|
rlm@0
|
109 *** With a Special Timer
|
rlm@0
|
110 Then, you change the game's timer so that user-time will be synced to
|
rlm@0
|
111 video-time. Once again, assume video encoding takes $\frac{1}{60}$ of
|
rlm@0
|
112 a user-second.
|
rlm@0
|
113
|
rlm@0
|
114 Now, JME3 will spend $\frac{1}{120}$ of a user-second to step the
|
rlm@0
|
115 physics tick $\frac{1}{60}$ game-seconds, $\frac{1}{60}$ to draw to
|
rlm@0
|
116 the screen, and an additional $\frac{1}{60}$ to encode the video and
|
rlm@0
|
117 write the frame to disk. This is a total of $\frac{1}{24}$
|
rlm@0
|
118 user-seconds for each $\frac{1}{60}$ game-seconds. It will take
|
rlm@0
|
119 $(\frac{60}{24} = 2.5)$ user-hours to record one game-hour and game-time
|
rlm@0
|
120 will appear to flow two-fifths as fast as user time while the game is
|
rlm@0
|
121 running. However, just as in example one, when all is said and done we
|
rlm@0
|
122 will have an hour long video at 60 fps.
|
rlm@0
|
123
|
rlm@0
|
124
|
rlm@0
|
125 * COMMENT proposed names for the new timer
|
rlm@0
|
126 # METRONOME
|
rlm@0
|
127 # IsoTimer
|
rlm@0
|
128 # EvenTimer
|
rlm@0
|
129 # PulseTimer
|
rlm@0
|
130 # FixedTimer
|
rlm@0
|
131 # RigidTimer
|
rlm@0
|
132 # FixedTempo
|
rlm@0
|
133 # RegularTimer
|
rlm@0
|
134 # MetronomeTimer
|
rlm@0
|
135 # ConstantTimer
|
rlm@0
|
136 # SteadyTimer
|
rlm@0
|
137
|
rlm@0
|
138
|
rlm@0
|
139 * =IsoTimer= records time like a metronome
|
rlm@0
|
140
|
rlm@0
|
141 The easiest way to achieve this special timing is to create a new
|
rlm@0
|
142 timer that always reports the same framerate to JME3 every time it is
|
rlm@0
|
143 called.
|
rlm@0
|
144
|
rlm@0
|
145
|
rlm@42
|
146 =./src/com/aurellem/capture/IsoTimer.java=
|
rlm@42
|
147 #+include ../../jmeCapture/src/com/aurellem/capture/IsoTimer.java src java
|
rlm@0
|
148
|
rlm@0
|
149 If an Application uses this =IsoTimer= instead of the normal one, we
|
rlm@0
|
150 can be sure that every call to =simpleUpdate=, for example, corresponds
|
rlm@0
|
151 to exactly $(\frac{1}{fps})$ seconds of game-time.
|
rlm@0
|
152
|
rlm@0
|
153 * Encoding to Video
|
rlm@0
|
154
|
rlm@0
|
155 Now that the issue of time is solved, we just need a function that
|
rlm@0
|
156 writes each frame to a video. We can put this function somewhere
|
rlm@0
|
157 where it will be called exactly one per frame.
|
rlm@0
|
158
|
rlm@42
|
159 The basic functions that a =VideoRecorder= should support are
|
rlm@42
|
160 recording, starting, stopping, and possibly a final finishing step
|
rlm@42
|
161 there it finilizes the recording (such as writing headers for a video
|
rlm@42
|
162 file).
|
rlm@42
|
163
|
rlm@42
|
164 An appropiate interface describing this behaviour could look like
|
rlm@42
|
165 this:
|
rlm@42
|
166
|
rlm@42
|
167 =./src/com/aurellem/capture/video/VideoRecorder.java=
|
rlm@42
|
168 #+include ../../jmeCapture/src/com/aurellem/capture/video/VideoRecorder.java src java
|
rlm@42
|
169
|
rlm@42
|
170
|
rlm@0
|
171 JME3 already provides exactly the class we need: the =SceneProcessor=
|
rlm@0
|
172 class can be attached to any viewport and the methods defined therein
|
rlm@0
|
173 will be called at the appropriate points in the rendering process.
|
rlm@0
|
174
|
rlm@42
|
175 However, it is also important to properly close the video stream and
|
rlm@42
|
176 write headers and such, and even though =SceneProcessor= has a
|
rlm@42
|
177 =.cleanup()= method, it is only called when the =SceneProcessor= is
|
rlm@42
|
178 removed from the =RenderManager=, not when the game is shutting down
|
rlm@42
|
179 when the user pressed ESC, for example. To obtain reliable shutdown
|
rlm@42
|
180 behaviour, we also have to implement =AppState=, which provides a
|
rlm@42
|
181 =.cleanup()= method that /is/ called on shutdown.
|
rlm@42
|
182
|
rlm@42
|
183 Here is an AbstractVideoRecorder class that takes care of the details
|
rlm@42
|
184 of setup and teardown.
|
rlm@42
|
185
|
rlm@42
|
186 =./src/com/aurellem/capture/video/AbstractVideoRecorder.java=
|
rlm@42
|
187 #+include ../../jmeCapture/src/com/aurellem/capture/video/AbstractVideoRecorder.java src java
|
rlm@42
|
188
|
rlm@0
|
189 If you want to generate video from Java, a great option is [[http://www.xuggle.com/][Xuggle]]. It
|
rlm@0
|
190 takes care of everything related to video encoding and decoding and
|
rlm@0
|
191 runs on Windows, Linux and Mac. Out of all the video frameworks for
|
rlm@0
|
192 Java I personally like this one the best.
|
rlm@0
|
193
|
rlm@42
|
194 Here is a =VideoRecorder= that uses [[http://www.xuggle.com/][Xuggle]] to write each frame to a
|
rlm@0
|
195 video file.
|
rlm@0
|
196
|
rlm@42
|
197 =./src/com/aurellem/capture/video/XuggleVideoRecorder.java=
|
rlm@42
|
198 #+include ../../jmeCapture/src/com/aurellem/capture/video/XuggleVideoRecorder.java src java
|
rlm@0
|
199
|
rlm@0
|
200 With this, we are able to record video!
|
rlm@0
|
201
|
rlm@42
|
202 However, it can be hard to properly install Xuggle. For those of you
|
rlm@42
|
203 who would rather not use Xuggle, here is an alternate class that uses
|
rlm@42
|
204 [[http://www.randelshofer.ch/blog/2008/08/writing-avi-videos-in-pure-java/][Werner Randelshofer's]] excellent pure Java AVI file writer.
|
rlm@42
|
205
|
rlm@42
|
206 =./src/com/aurellem/capture/video/AVIVideoRecorder.java=
|
rlm@42
|
207 #+include ../../jmeCapture/src/com/aurellem/capture/video/AVIVideoRecorder.java src java
|
rlm@42
|
208
|
rlm@42
|
209 This =AVIVideoRecorder= is more limited than the
|
rlm@42
|
210 =XuggleVideoRecorder=, but requires less external dependencies.
|
rlm@42
|
211
|
rlm@42
|
212 Finally, for those of you who prefer to create the final video from a
|
rlm@42
|
213 sequence of images, there is the =FileVideoRecorder=, which records
|
rlm@42
|
214 each frame to a folder as a sequentially numbered image file. Note
|
rlm@42
|
215 that you have to remember the FPS at which you recorded the video, as
|
rlm@42
|
216 this information is lost when saving each frame to a file.
|
rlm@42
|
217
|
rlm@42
|
218 =./src/com/aurellem/capture/video/FileVideoRecorder.java=
|
rlm@42
|
219 #+include ../../jmeCapture/src/com/aurellem/capture/video/FileVideoRecorder.java src java
|
rlm@42
|
220
|
rlm@42
|
221
|
rlm@42
|
222 * /Really/ Simple Video Recording
|
rlm@42
|
223
|
rlm@42
|
224 The most common case for recording a video is probably to just capture
|
rlm@42
|
225 whatever is on your screen exactly as you see it. In this case, this
|
rlm@42
|
226 method will do.
|
rlm@42
|
227
|
rlm@42
|
228 #+begin_src java
|
rlm@42
|
229 public static void captureVideo(final Application app,
|
rlm@42
|
230 final File file) throws IOException{
|
rlm@42
|
231 final AbstractVideoRecorder videoRecorder;
|
rlm@42
|
232 if (file.getCanonicalPath().endsWith(".avi")){
|
rlm@42
|
233 videoRecorder = new AVIVideoRecorder(file);}
|
rlm@42
|
234 else if (file.isDirectory()){
|
rlm@42
|
235 videoRecorder = new FileVideoRecorder(file);}
|
rlm@42
|
236 else { videoRecorder = new XuggleVideoRecorder(file);}
|
rlm@42
|
237
|
rlm@42
|
238 Callable<Object> thunk = new Callable<Object>(){
|
rlm@42
|
239 public Object call(){
|
rlm@42
|
240 ViewPort viewPort =
|
rlm@42
|
241 app.getRenderManager()
|
rlm@42
|
242 .createPostView("aurellem record", app.getCamera());
|
rlm@42
|
243 viewPort.setClearFlags(false, false, false);
|
rlm@42
|
244 // get GUI node stuff
|
rlm@42
|
245 for (Spatial s : app.getGuiViewPort().getScenes()){
|
rlm@42
|
246 viewPort.attachScene(s);
|
rlm@42
|
247 }
|
rlm@42
|
248 app.getStateManager().attach(videoRecorder);
|
rlm@42
|
249 viewPort.addProcessor(videoRecorder);
|
rlm@42
|
250 return null;
|
rlm@42
|
251 }
|
rlm@42
|
252 };
|
rlm@42
|
253 app.enqueue(thunk);
|
rlm@42
|
254 }
|
rlm@42
|
255 #+end_src
|
rlm@42
|
256
|
rlm@42
|
257 This will select the appropiate backend =VideoRecorder= class
|
rlm@42
|
258 depending on the file name you specify, and insturment your
|
rlm@42
|
259 application to record video to the file. You should still set the
|
rlm@42
|
260 game's timer to an =IsoTimer= with the desired fps.
|
rlm@42
|
261
|
rlm@42
|
262 This example will record video from the ocean scene from the
|
rlm@42
|
263 jMonkeyEngine test suite.
|
rlm@42
|
264 #+begin_src java
|
rlm@42
|
265 File video = File.createTempFile("JME-water-video", ".avi");
|
rlm@42
|
266 captureVideo(app, video);
|
rlm@42
|
267 app.start();
|
rlm@42
|
268 System.out.println(video.getCanonicalPath());
|
rlm@42
|
269 #+end_src
|
rlm@42
|
270
|
rlm@42
|
271
|
rlm@42
|
272 I've added support for this under a class called
|
rlm@42
|
273 =com.aurellem.capture.Capture=. You can get it [[http://hg.bortreb.com/jmeCapture/][here]].
|
rlm@42
|
274
|
rlm@42
|
275
|
rlm@0
|
276 * Hello Video!
|
rlm@0
|
277
|
rlm@0
|
278 I've taken [[http://code.google.com/p/jmonkeyengine/source/browse/trunk/engine/src/test/jme3test/helloworld/HelloLoop.java][=./jme3/src/test/jme3test/helloworld/HelloLoop.java=]] and
|
rlm@0
|
279 augmented it with video output as follows:
|
rlm@0
|
280
|
rlm@42
|
281 =./src/com/aurellem/capture/examples/HelloVideo.java=
|
rlm@42
|
282 #+include ../../src/com/aurellem/capture/examples/HelloVideo.java src java
|
rlm@0
|
283
|
rlm@0
|
284 The videos are created in the =hello-video= directory
|
rlm@0
|
285
|
rlm@42
|
286 #+begin_src sh :results verbatim :exports both
|
rlm@0
|
287 du -h hello-video/*
|
rlm@0
|
288 #+end_src
|
rlm@0
|
289
|
rlm@0
|
290 #+results:
|
rlm@0
|
291 : 932K hello-video/hello-video-moving.flv
|
rlm@0
|
292 : 640K hello-video/hello-video-static.flv
|
rlm@0
|
293
|
rlm@0
|
294 And can be immediately uploaded to youtube
|
rlm@0
|
295
|
rlm@0
|
296 - [[http://www.youtube.com/watch?v=C8gxVAySaPg][hello-video-moving.flv]]
|
rlm@0
|
297 #+BEGIN_HTML
|
rlm@0
|
298 <iframe width="425" height="349"
|
rlm@0
|
299 src="http://www.youtube.com/embed/C8gxVAySaPg"
|
rlm@0
|
300 frameborder="0" allowfullscreen>
|
rlm@0
|
301 </iframe>
|
rlm@0
|
302 #+END_HTML
|
rlm@0
|
303 - [[http://www.youtube.com/watch?v=pHcFOtIS07Q][hello-video-static.flv]]
|
rlm@0
|
304 #+BEGIN_HTML
|
rlm@0
|
305 <iframe width="425" height="349"
|
rlm@0
|
306 src="http://www.youtube.com/embed/pHcFOtIS07Q"
|
rlm@0
|
307 frameborder="0" allowfullscreen>
|
rlm@0
|
308 </iframe>
|
rlm@0
|
309
|
rlm@0
|
310 #+END_HTML
|
rlm@0
|
311
|
rlm@0
|
312
|
rlm@0
|
313 * Summary
|
rlm@0
|
314 It's quite easy to augment your own application to record video,
|
rlm@0
|
315 almost regardless of how complicated the actual application is. You
|
rlm@0
|
316 can also record from multiple ViewPorts as the above example shows.
|
rlm@0
|
317
|
rlm@0
|
318 The process for adding video recording to your application is as
|
rlm@0
|
319 follows:
|
rlm@0
|
320
|
rlm@0
|
321 Assuming you want to record at 30 fps, add:
|
rlm@0
|
322
|
rlm@0
|
323 #+begin_src java :exports code
|
rlm@0
|
324 this.setTimer(new IsoTimer(30));
|
rlm@0
|
325 #+end_src
|
rlm@0
|
326
|
rlm@42
|
327 Somewhere in the initialization of your Application.
|
rlm@0
|
328
|
rlm@0
|
329 If you want to record from the game's main =ViewPort= to a file called
|
rlm@0
|
330 =/home/r/record.flv=, then add:
|
rlm@0
|
331
|
rlm@0
|
332 #+begin_src java :exports code
|
rlm@42
|
333 Capture.captureVideo(app, new File("/home/r/record.flv"));
|
rlm@0
|
334 #+end_src
|
rlm@0
|
335
|
rlm@42
|
336 Before you call =app.start()=;
|
rlm@0
|
337
|
rlm@0
|
338 * More Examples
|
rlm@42
|
339 ** COMMENT Hello Physics
|
rlm@0
|
340 =HelloVideo= is boring. Let's add some video capturing to =HelloPhysics=
|
rlm@0
|
341 and create something fun!
|
rlm@0
|
342
|
rlm@0
|
343 This example is a modified version of =HelloPhysics= that creates four
|
rlm@0
|
344 simultaneous views of the same scene of cannonballs careening into a
|
rlm@0
|
345 brick wall.
|
rlm@0
|
346
|
rlm@0
|
347 =./jme3/src/test/jme3test/helloworld/HelloPhysicsWithVideo.java=
|
rlm@0
|
348 #+include ./jme3/src/test/jme3test/helloworld/HelloPhysicsWithVideo.java src java
|
rlm@0
|
349
|
rlm@0
|
350 Running the program outputs four videos into the =./physics-videos=
|
rlm@0
|
351 directory.
|
rlm@0
|
352
|
rlm@0
|
353 #+begin_src sh :exports both :results verbatim
|
rlm@0
|
354 ls ./physics-videos | grep -
|
rlm@0
|
355 #+end_src
|
rlm@0
|
356
|
rlm@0
|
357 #+results:
|
rlm@0
|
358 : lower-left.flv
|
rlm@0
|
359 : lower-right.flv
|
rlm@0
|
360 : upper-left.flv
|
rlm@0
|
361 : upper-right.flv
|
rlm@0
|
362
|
rlm@0
|
363 The videos are fused together with the following =gstreamer= commands:
|
rlm@0
|
364
|
rlm@0
|
365 #+begin_src sh :results silent
|
rlm@0
|
366 cd physics-videos
|
rlm@0
|
367
|
rlm@0
|
368 gst-launch-0.10 \
|
rlm@0
|
369 filesrc location=./upper-right.flv ! decodebin ! \
|
rlm@0
|
370 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
371 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
372 videobox border-alpha=0 left=-640 ! \
|
rlm@0
|
373 videomixer name=mix ! ffmpegcolorspace ! videorate ! \
|
rlm@0
|
374 video/x-raw-yuv, width=1280, height=480, framerate=25/1 ! \
|
rlm@0
|
375 jpegenc ! avimux ! filesink location=upper.flv \
|
rlm@0
|
376 \
|
rlm@0
|
377 filesrc location=./upper-left.flv ! decodebin ! \
|
rlm@0
|
378 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
379 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
380 videobox right=-640 ! mix.
|
rlm@0
|
381 #+end_src
|
rlm@0
|
382
|
rlm@0
|
383 #+begin_src sh :results silent
|
rlm@0
|
384 cd physics-videos
|
rlm@0
|
385
|
rlm@0
|
386 gst-launch-0.10 \
|
rlm@0
|
387 filesrc location=./lower-left.flv ! decodebin ! \
|
rlm@0
|
388 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
389 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
390 videobox border-alpha=0 left=-640 ! \
|
rlm@0
|
391 videomixer name=mix ! ffmpegcolorspace ! videorate ! \
|
rlm@0
|
392 video/x-raw-yuv, width=1280, height=480, framerate=25/1 ! \
|
rlm@0
|
393 jpegenc ! avimux ! filesink location=lower.flv \
|
rlm@0
|
394 \
|
rlm@0
|
395 filesrc location=./lower-right.flv ! decodebin ! \
|
rlm@0
|
396 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
397 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
398 videobox right=-640 ! mix.
|
rlm@0
|
399 #+end_src
|
rlm@0
|
400
|
rlm@0
|
401 #+begin_src sh :results silent
|
rlm@0
|
402 cd physics-videos
|
rlm@0
|
403
|
rlm@0
|
404 gst-launch-0.10 \
|
rlm@0
|
405 filesrc location=./upper.flv ! decodebin ! \
|
rlm@0
|
406 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
407 video/x-raw-yuv, width=1280, height=480, framerate=25/1 ! \
|
rlm@0
|
408 videobox border-alpha=0 bottom=-480 ! \
|
rlm@0
|
409 videomixer name=mix ! ffmpegcolorspace ! videorate ! \
|
rlm@0
|
410 video/x-raw-yuv, width=1280, height=960, framerate=25/1 ! \
|
rlm@0
|
411 jpegenc ! avimux ! filesink location=../youtube/helloPhysics.flv \
|
rlm@0
|
412 \
|
rlm@0
|
413 filesrc location=./lower.flv ! decodebin ! \
|
rlm@0
|
414 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
415 video/x-raw-yuv, width=1280, height=480, framerate=25/1 ! \
|
rlm@0
|
416 videobox top=-480 ! mix.
|
rlm@0
|
417 #+end_src
|
rlm@0
|
418
|
rlm@0
|
419 #+begin_src sh :results verbatim
|
rlm@0
|
420 du -h youtube/helloPhysics.flv
|
rlm@0
|
421 #+end_src
|
rlm@0
|
422
|
rlm@0
|
423 #+results:
|
rlm@0
|
424 : 180M physics-videos/helloPhysics.flv
|
rlm@0
|
425
|
rlm@0
|
426
|
rlm@0
|
427 Thats a terribly large size!
|
rlm@0
|
428 Let's compress it:
|
rlm@0
|
429
|
rlm@42
|
430 ** COMMENT Compressing the HelloPhysics Video
|
rlm@0
|
431 First, we'll scale the video, then, we'll decrease it's bitrate. The
|
rlm@0
|
432 end result will be perfect for upload to YouTube.
|
rlm@0
|
433
|
rlm@0
|
434 #+begin_src sh :results silent
|
rlm@0
|
435 cd youtube
|
rlm@0
|
436
|
rlm@0
|
437 gst-launch-0.10 \
|
rlm@0
|
438 filesrc location=./helloPhysics.flv ! decodebin ! \
|
rlm@0
|
439 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
440 `: # the original size is 1280 by 960` \
|
rlm@0
|
441 video/x-raw-yuv, width=1280, height=960, framerate=25/1 ! \
|
rlm@0
|
442 videoscale ! \
|
rlm@0
|
443 `: # here we scale the video down` \
|
rlm@0
|
444 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
445 `: # and here we limit the bitrate` \
|
rlm@0
|
446 theoraenc bitrate=1024 quality=30 ! \
|
rlm@0
|
447 oggmux ! progressreport update-freq=1 ! \
|
rlm@0
|
448 filesink location=./helloPhysics.ogg
|
rlm@0
|
449 #+end_src
|
rlm@0
|
450
|
rlm@0
|
451 #+begin_src sh :results verbatim
|
rlm@0
|
452 du -h youtube/helloPhysics.ogg
|
rlm@0
|
453 #+end_src
|
rlm@0
|
454
|
rlm@0
|
455 #+results:
|
rlm@0
|
456 : 13M youtube/helloPhysics.ogg
|
rlm@0
|
457
|
rlm@0
|
458 [[http://www.youtube.com/watch?v=WIJt9aRGusc][helloPhysics.ogg]]
|
rlm@0
|
459
|
rlm@0
|
460 #+begin_html
|
rlm@0
|
461 <iframe width="425" height="349"
|
rlm@0
|
462 src="http://www.youtube.com/embed/WIJt9aRGusc?hl=en&fs=1"
|
rlm@0
|
463 frameborder="0" allowfullscreen>
|
rlm@0
|
464 </iframe>
|
rlm@0
|
465 #+end_html
|
rlm@0
|
466
|
rlm@0
|
467
|
rlm@0
|
468 ** COMMENT failed attempts
|
rlm@0
|
469 Let's try the [[http://diracvideo.org/][Dirac]] video encoder.
|
rlm@0
|
470
|
rlm@0
|
471 #+begin_src sh :results verbatim
|
rlm@0
|
472 cd youtube
|
rlm@0
|
473 START=$(date +%s)
|
rlm@0
|
474 gst-launch-0.10 \
|
rlm@0
|
475 filesrc location=./helloPhysics.flv ! decodebin ! \
|
rlm@0
|
476 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
477 video/x-raw-yuv, width=1280, height=960, framerate=25/1 ! \
|
rlm@0
|
478 schroenc ! filesink location=./helloPhysics.drc > /dev/null
|
rlm@0
|
479 echo `expr $(( $(date +%s) - $START))`
|
rlm@0
|
480 #+end_src
|
rlm@0
|
481
|
rlm@0
|
482
|
rlm@0
|
483 #+results:
|
rlm@0
|
484 : 142
|
rlm@0
|
485
|
rlm@0
|
486 That took 142 seconds. Let's see how it does compression-wise:
|
rlm@0
|
487
|
rlm@0
|
488 #+begin_src sh :results verbatim
|
rlm@0
|
489 du -h ./youtube/helloPhysics.drc
|
rlm@0
|
490 #+end_src
|
rlm@0
|
491
|
rlm@0
|
492 #+results:
|
rlm@0
|
493 : 22M ./physics-videos/helloPhysics.drc
|
rlm@0
|
494
|
rlm@0
|
495
|
rlm@0
|
496 #+begin_src sh :results verbatim
|
rlm@0
|
497 cd youtube
|
rlm@0
|
498 START=$(date +%s)
|
rlm@0
|
499 gst-launch-0.10 \
|
rlm@0
|
500 filesrc location=./helloPhysics.flv ! decodebin ! \
|
rlm@0
|
501 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
502 video/x-raw-yuv, width=1280, height=960, framerate=25/1 ! \
|
rlm@0
|
503 theoraenc ! oggmux ! filesink location=./helloPhysics.ogg \
|
rlm@0
|
504 > /dev/null
|
rlm@0
|
505 echo `expr $(( $(date +%s) - $START))`
|
rlm@0
|
506 #+end_src
|
rlm@0
|
507
|
rlm@0
|
508 #+results:
|
rlm@0
|
509 : 123
|
rlm@0
|
510
|
rlm@0
|
511 #+begin_src sh :results verbatim
|
rlm@0
|
512 du -h youtube/helloPhysics.ogg
|
rlm@0
|
513 #+end_src
|
rlm@0
|
514
|
rlm@0
|
515 #+results:
|
rlm@0
|
516 : 59M physics-videos/helloPhysics.ogg
|
rlm@0
|
517
|
rlm@0
|
518
|
rlm@0
|
519 =*.drc= files can not be uploaded to YouTube, so I'll go for the
|
rlm@0
|
520 avi file.
|
rlm@0
|
521
|
rlm@0
|
522
|
rlm@0
|
523 ** COMMENT text for videos
|
rlm@0
|
524 Video output from JMonkeyEngine3 (www.jmonkeyengine.org/) using Xuggle
|
rlm@0
|
525 (www.xuggle.com/). Everything is explained at
|
rlm@0
|
526 http://aurellem.org/cortex/capture-video.html.
|
rlm@0
|
527
|
rlm@0
|
528
|
rlm@0
|
529 Video output from JMonkeyEngine3 (www.jmonkeyengine.org/) HelloPhysics
|
rlm@0
|
530 demo application using Xuggle (www.xuggle.com/). Everything is
|
rlm@0
|
531 explained at http://aurellem.org/cortex/capture-video.html. Here,
|
rlm@0
|
532 four points of view are simultaneously recorded and then glued
|
rlm@0
|
533 together later.
|
rlm@0
|
534
|
rlm@0
|
535 JME3 Xuggle Aurellem video capture
|
rlm@0
|
536
|
rlm@0
|
537
|
rlm@0
|
538 * Sample Videos
|
rlm@0
|
539 I encoded most of the original JME3 Hello demos for your viewing
|
rlm@42
|
540 pleasure, all using the =Capture= and =IsoTimer= classes.
|
rlm@0
|
541
|
rlm@0
|
542 ** HelloTerrain
|
rlm@0
|
543 [[http://youtu.be/5_4wyDFwrVQ][HelloTerrain.avi]]
|
rlm@0
|
544
|
rlm@0
|
545 #+begin_html
|
rlm@0
|
546 <iframe width="425" height="349"
|
rlm@0
|
547 src="http://www.youtube.com/embed/5_4wyDFwrVQ"
|
rlm@0
|
548 frameborder="0" allowfullscreen>
|
rlm@0
|
549 </iframe>
|
rlm@0
|
550 #+end_html
|
rlm@0
|
551
|
rlm@0
|
552 ** HelloAssets
|
rlm@0
|
553 [[http://www.youtube.com/watch?v=oGg-Q6k1BM4][HelloAssets.avi]]
|
rlm@0
|
554
|
rlm@0
|
555 #+begin_html
|
rlm@0
|
556 <iframe width="425" height="349"
|
rlm@0
|
557 src="http://www.youtube.com/embed/oGg-Q6k1BM4?hl=en&fs=1"
|
rlm@0
|
558 frameborder="0" allowfullscreen>
|
rlm@0
|
559 </iframe>
|
rlm@0
|
560 #+end_html
|
rlm@0
|
561
|
rlm@0
|
562 ** HelloEffects
|
rlm@0
|
563 [[http://www.youtube.com/watch?v=TuxlLMe53hA][HelloEffects]]
|
rlm@0
|
564
|
rlm@0
|
565 #+begin_html
|
rlm@0
|
566 <iframe width="425" height="349"
|
rlm@0
|
567 src="http://www.youtube.com/embed/TuxlLMe53hA?hl=en&fs=1"
|
rlm@0
|
568 frameborder="0" allowfullscreen>
|
rlm@0
|
569 </iframe>
|
rlm@0
|
570 #+end_html
|
rlm@0
|
571
|
rlm@0
|
572 ** HelloCollision
|
rlm@0
|
573 [[http://www.youtube.com/watch?v=GPlvJkiZfFw][HelloCollision.avi]]
|
rlm@0
|
574
|
rlm@0
|
575 #+begin_html
|
rlm@0
|
576 <iframe width="425" height="349"
|
rlm@0
|
577 src="http://www.youtube.com/embed/GPlvJkiZfFw?hl=en&fs=1"
|
rlm@0
|
578 frameborder="0" allowfullscreen>
|
rlm@0
|
579 </iframe>
|
rlm@0
|
580 #+end_html
|
rlm@0
|
581
|
rlm@0
|
582 ** HelloAnimation
|
rlm@0
|
583 [[http://www.youtube.com/watch?v=SDCfOSPYUkg][HelloAnimation.avi]]
|
rlm@0
|
584
|
rlm@0
|
585 #+begin_html
|
rlm@0
|
586 <iframe width="425" height="349"
|
rlm@0
|
587 src="http://www.youtube.com/embed/SDCfOSPYUkg?hl=en&fs=1"
|
rlm@0
|
588 frameborder="0" allowfullscreen>
|
rlm@0
|
589 </iframe>
|
rlm@0
|
590 #+end_html
|
rlm@0
|
591
|
rlm@0
|
592 ** HelloNode
|
rlm@0
|
593 [[http://www.youtube.com/watch?v=pL-0fR0-ilQ][HelloNode.avi]]
|
rlm@0
|
594
|
rlm@0
|
595 #+begin_html
|
rlm@0
|
596 <iframe width="425" height="349"
|
rlm@0
|
597 src="http://www.youtube.com/embed/pL-0fR0-ilQ?hl=en&fs=1"
|
rlm@0
|
598 frameborder="0" allowfullscreen>
|
rlm@0
|
599 </iframe>
|
rlm@0
|
600 #+end_html
|
rlm@0
|
601
|
rlm@0
|
602 ** HelloLoop
|
rlm@0
|
603 [[http://www.youtube.com/watch?v=mosZzzcdE5w][HelloLoop.avi]]
|
rlm@0
|
604
|
rlm@0
|
605 #+begin_html
|
rlm@0
|
606 <iframe width="425" height="349"
|
rlm@0
|
607 src="http://www.youtube.com/embed/mosZzzcdE5w?hl=en&fs=1"
|
rlm@0
|
608 frameborder="0" allowfullscreen>
|
rlm@0
|
609 </iframe>
|
rlm@0
|
610 #+end_html
|
rlm@0
|
611
|
rlm@0
|
612
|
rlm@0
|
613 *** COMMENT x-form the other stupid
|
rlm@0
|
614 progressreport update-freq=1
|
rlm@0
|
615
|
rlm@0
|
616 gst-launch-0.10 \
|
rlm@0
|
617 filesrc location=./helloPhy ! decodebin ! \
|
rlm@0
|
618 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
619 video/x-raw-yuv, width=1280, height=960, framerate=25/1 ! \
|
rlm@0
|
620 x264enc ! avimux ! filesink location=helloPhysics.avi \
|
rlm@0
|
621
|
rlm@0
|
622
|
rlm@0
|
623 gst-launch-0.10 \
|
rlm@0
|
624 filesrc location=./HelloAnimationStatic.flv ! decodebin ! \
|
rlm@0
|
625 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
626 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
627 videobox border-alpha=0 left=-640 ! \
|
rlm@0
|
628 videomixer name=mix ! ffmpegcolorspace ! videorate ! \
|
rlm@0
|
629 video/x-raw-yuv, width=1280, height=480, framerate=25/1 ! \
|
rlm@0
|
630 x264enc ! avimux ! progressreport update-freq=1 ! \
|
rlm@0
|
631 filesink location=../youtube/HelloAnimation.avi \
|
rlm@0
|
632 \
|
rlm@0
|
633 filesrc location=./HelloAnimationMotion.flv ! decodebin ! \
|
rlm@0
|
634 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
635 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
636 videobox right=-640 ! mix.
|
rlm@0
|
637
|
rlm@0
|
638 gst-launch-0.10 \
|
rlm@0
|
639 filesrc location=./HelloCollisionMotion.flv ! decodebin ! \
|
rlm@0
|
640 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
641 video/x-raw-yuv, width=800, height=600, framerate=25/1 ! \
|
rlm@0
|
642 x264enc bitrate=1024 ! avimux ! \
|
rlm@0
|
643 filesink location=../youtube/HelloCollision.avi
|
rlm@0
|
644
|
rlm@0
|
645 gst-launch-0.10 \
|
rlm@0
|
646 filesrc location=./HelloEffectsStatic.flv ! decodebin ! \
|
rlm@0
|
647 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
648 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
649 videobox border-alpha=0 left=-640 ! \
|
rlm@0
|
650 videomixer name=mix ! ffmpegcolorspace ! videorate ! \
|
rlm@0
|
651 video/x-raw-yuv, width=1280, height=480, framerate=25/1 ! \
|
rlm@0
|
652 x264enc bitrate=1024 ! avimux ! progressreport update-freq=1 ! \
|
rlm@0
|
653 filesink location=../youtube/HelloEffects.avi \
|
rlm@0
|
654 \
|
rlm@0
|
655 filesrc location=./HelloEffectsMotion.flv ! decodebin ! \
|
rlm@0
|
656 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
657 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
658 videobox right=-640 ! mix.
|
rlm@0
|
659
|
rlm@0
|
660 gst-launch-0.10 \
|
rlm@0
|
661 filesrc location=./HelloTerrainMotion.flv ! decodebin ! \
|
rlm@0
|
662 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
663 video/x-raw-yuv, width=800, height=600, framerate=25/1 ! \
|
rlm@0
|
664 x264enc bitrate=1024 ! avimux ! \
|
rlm@0
|
665 filesink location=../youtube/HelloTerrain.avi
|
rlm@0
|
666
|
rlm@0
|
667
|
rlm@0
|
668 gst-launch-0.10 \
|
rlm@0
|
669 filesrc location=./HelloAssetsStatic.flv ! decodebin ! \
|
rlm@0
|
670 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
671 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
672 videobox border-alpha=0 left=-640 ! \
|
rlm@0
|
673 videomixer name=mix ! ffmpegcolorspace ! videorate ! \
|
rlm@0
|
674 video/x-raw-yuv, width=1280, height=480, framerate=25/1 ! \
|
rlm@0
|
675 x264enc bitrate=1024 ! avimux ! progressreport update-freq=1 ! \
|
rlm@0
|
676 filesink location=../youtube/HelloAssets.avi \
|
rlm@0
|
677 \
|
rlm@0
|
678 filesrc location=./HelloAssetsMotion.flv ! decodebin ! \
|
rlm@0
|
679 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
680 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
681 videobox right=-640 ! mix.
|
rlm@0
|
682
|
rlm@0
|
683
|
rlm@0
|
684 gst-launch-0.10 \
|
rlm@0
|
685 filesrc location=./HelloNodeStatic.flv ! decodebin ! \
|
rlm@0
|
686 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
687 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
688 videobox border-alpha=0 left=-640 ! \
|
rlm@0
|
689 videomixer name=mix ! ffmpegcolorspace ! videorate ! \
|
rlm@0
|
690 video/x-raw-yuv, width=1280, height=480, framerate=25/1 ! \
|
rlm@0
|
691 x264enc bitrate=1024 ! avimux ! progressreport update-freq=1 ! \
|
rlm@0
|
692 filesink location=../youtube/HelloNode.avi \
|
rlm@0
|
693 \
|
rlm@0
|
694 filesrc location=./HelloNodeMotion.flv ! decodebin ! \
|
rlm@0
|
695 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
696 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
697 videobox right=-640 ! mix.
|
rlm@0
|
698
|
rlm@0
|
699 gst-launch-0.10 \
|
rlm@0
|
700 filesrc location=./HelloLoopStatic.flv ! decodebin ! \
|
rlm@0
|
701 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
702 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
703 videobox border-alpha=0 left=-640 ! \
|
rlm@0
|
704 videomixer name=mix ! ffmpegcolorspace ! videorate ! \
|
rlm@0
|
705 video/x-raw-yuv, width=1280, height=480, framerate=25/1 ! \
|
rlm@0
|
706 x264enc bitrate=1024 ! avimux ! progressreport update-freq=1 ! \
|
rlm@0
|
707 filesink location=../youtube/HelloLoop.avi \
|
rlm@0
|
708 \
|
rlm@0
|
709 filesrc location=./HelloLoopMotion.flv ! decodebin ! \
|
rlm@0
|
710 videoscale ! ffmpegcolorspace ! \
|
rlm@0
|
711 video/x-raw-yuv, width=640, height=480, framerate=25/1 ! \
|
rlm@0
|
712 videobox right=-640 ! mix.
|
rlm@0
|
713
|
rlm@32
|
714
|
rlm@32
|
715
|
rlm@32
|
716
|
rlm@32
|
717
|
rlm@32
|
718
|
rlm@32
|
719
|
rlm@32
|
720
|
rlm@32
|
721
|
rlm@32
|
722
|
rlm@32
|
723
|
rlm@32
|
724
|
rlm@32
|
725
|
rlm@32
|
726
|
rlm@32
|
727
|
rlm@32
|
728
|
rlm@32
|
729
|
rlm@32
|
730
|
rlm@32
|
731
|
rlm@32
|
732
|
rlm@32
|
733
|
rlm@32
|
734
|
rlm@32
|
735
|
rlm@32
|
736
|
rlm@32
|
737
|
rlm@32
|
738
|
rlm@32
|
739
|
rlm@42
|
740
|