1 package com.nexuiz.demorecorder.application.jobs;
3 import java.io.BufferedReader;
5 import java.io.IOException;
6 import java.io.InputStream;
7 import java.io.InputStreamReader;
8 import java.io.Serializable;
9 import java.util.ArrayList;
10 import java.util.HashMap;
11 import java.util.List;
13 import java.util.Properties;
15 import com.nexuiz.demorecorder.application.DemoRecorderApplication;
16 import com.nexuiz.demorecorder.application.DemoRecorderException;
17 import com.nexuiz.demorecorder.application.DemoRecorderUtils;
18 import com.nexuiz.demorecorder.application.NDRPreferences;
19 import com.nexuiz.demorecorder.application.DemoRecorderApplication.Preferences;
20 import com.nexuiz.demorecorder.application.democutter.DemoCutter;
21 import com.nexuiz.demorecorder.application.democutter.DemoCutterException;
22 import com.nexuiz.demorecorder.application.plugins.EncoderPlugin;
23 import com.nexuiz.demorecorder.application.plugins.EncoderPluginException;
25 public class RecordJob implements Runnable, Serializable {
27 private static final long serialVersionUID = -4585637490345587912L;
30 WAITING, PROCESSING, ERROR, ERROR_PLUGIN, DONE
33 public static final String CUT_DEMO_FILE_SUFFIX = "_autocut";
34 public static final String CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE = "autocap";
35 public static final String CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE = "1234567";
36 protected static final String[] VIDEO_FILE_ENDINGS = {"avi", "ogv"};
38 private DemoRecorderApplication appLayer;
39 protected String jobName;
41 protected File enginePath;
42 protected String engineParameters;
43 protected File demoFile;
44 protected String relativeDemoPath;
45 protected File dpVideoPath;
46 protected File videoDestination;
47 protected String executeBeforeCap;
48 protected String executeAfterCap;
49 protected float startSecond;
50 protected float endSecond;
51 protected State state = State.WAITING;
52 protected DemoRecorderException lastException = null;
55 * Points to the actual final file, including possible suffixes, e.g. _copy1, and the actualy ending
57 protected File actualVideoDestination = null;
59 * Map that identifies the plug-in by its name (String) and maps to the plug-in's job-specific settings
61 protected Map<String, Properties> encoderPluginSettings = new HashMap<String, Properties>();
63 private List<File> cleanUpFiles = null;
66 DemoRecorderApplication appLayer,
70 String engineParameters,
72 String relativeDemoPath,
74 File videoDestination,
75 String executeBeforeCap,
76 String executeAfterCap,
80 this.appLayer = appLayer;
81 this.jobName = jobName;
82 this.jobIndex = jobIndex;
84 this.setEnginePath(enginePath);
85 this.setEngineParameters(engineParameters);
86 this.setDemoFile(demoFile);
87 this.setRelativeDemoPath(relativeDemoPath);
88 this.setDpVideoPath(dpVideoPath);
89 this.setVideoDestination(videoDestination);
90 this.setExecuteBeforeCap(executeBeforeCap);
91 this.setExecuteAfterCap(executeAfterCap);
92 this.setStartSecond(startSecond);
93 this.setEndSecond(endSecond);
99 * Constructor that can be used by other classes such as job templates. Won't throw exceptions
100 * as it won't check the input for validity.
104 String engineParameters,
106 String relativeDemoPath,
108 File videoDestination,
109 String executeBeforeCap,
110 String executeAfterCap,
115 this.enginePath = enginePath;
116 this.engineParameters = engineParameters;
117 this.demoFile = demoFile;
118 this.relativeDemoPath = relativeDemoPath;
119 this.dpVideoPath = dpVideoPath;
120 this.videoDestination = videoDestination;
121 this.executeBeforeCap = executeBeforeCap;
122 this.executeAfterCap = executeAfterCap;
123 this.startSecond = startSecond;
124 this.endSecond = endSecond;
127 public void execute() {
128 if (this.state == State.PROCESSING) {
131 boolean errorOccurred = false;
132 this.setState(State.PROCESSING);
133 this.appLayer.fireUserInterfaceUpdate(this);
134 cleanUpFiles = new ArrayList<File>();
136 File cutDemo = computeCutDemoFile();
137 cutDemo.delete(); //delete possibly old cutDemoFile
139 EncoderPlugin recentEncoder = null;
142 this.cutDemo(cutDemo);
143 this.removeOldAutocaps();
144 this.recordClip(cutDemo);
145 this.moveRecordedClip();
146 for (EncoderPlugin plugin : this.appLayer.getEncoderPlugins()) {
147 recentEncoder = plugin;
148 plugin.executeEncoder(this);
150 } catch (DemoRecorderException e) {
151 errorOccurred = true;
152 this.lastException = e;
153 this.setState(State.ERROR);
154 } catch (EncoderPluginException e) {
155 errorOccurred = true;
156 this.lastException = new DemoRecorderException("Encoder plug-in " + recentEncoder.getName() + " failed: "
157 + e.getMessage(), e);
158 this.setState(State.ERROR_PLUGIN);
159 } catch (Exception e) {
160 errorOccurred = true;
161 this.lastException = new DemoRecorderException("Executing job failed, click on details for more info", e);
163 NDRPreferences preferences = this.appLayer.getPreferences();
164 if (!Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DO_NOT_DELETE_CUT_DEMOS))) {
165 cleanUpFiles.add(cutDemo);
167 if (!errorOccurred) {
168 this.setState(State.DONE);
171 this.appLayer.fireUserInterfaceUpdate(this);
172 this.appLayer.saveJobQueue();
177 * Will execute just the specified encoder plug-in on an already "done" job.
180 public void executePlugin(EncoderPlugin plugin) {
181 if (this.getState() != State.DONE) {
184 this.setState(State.PROCESSING);
185 this.appLayer.fireUserInterfaceUpdate(this);
188 plugin.executeEncoder(this);
189 this.setState(State.DONE);
190 } catch (EncoderPluginException e) {
191 this.lastException = new DemoRecorderException("Encoder plug-in " + plugin.getName() + " failed: "
192 + e.getMessage(), e);
193 this.setState(State.ERROR_PLUGIN);
196 this.appLayer.fireUserInterfaceUpdate(this);
199 private void cleanUpFiles() {
201 for (File f : this.cleanUpFiles) {
204 } catch (Exception e) {}
208 private void moveRecordedClip() {
209 //1. Figure out whether the file is .avi or .ogv
210 File sourceFile = null;
211 for (String videoExtension : VIDEO_FILE_ENDINGS) {
212 String fileString = this.dpVideoPath.getAbsolutePath() + File.separator + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE
213 + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE + "." + videoExtension;
214 File videoFile = new File(fileString);
215 if (videoFile.exists()) {
216 sourceFile = videoFile;
221 if (sourceFile == null) {
222 String p = this.dpVideoPath.getAbsolutePath() + File.separator + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE
223 + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE;
224 throw new DemoRecorderException("Could not locate the expected video file being generated by Nexuiz (should have been at "
227 cleanUpFiles.add(sourceFile);
229 File destinationFile = null;
230 NDRPreferences preferences = this.appLayer.getPreferences();
231 String sourceFileExtension = DemoRecorderUtils.getFileExtension(sourceFile);
232 String destinationFilePath = this.videoDestination + "." + sourceFileExtension;
233 destinationFile = new File(destinationFilePath);
234 if (destinationFile.exists()) {
235 if (Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.OVERWRITE_VIDEO_FILE))) {
236 if (!destinationFile.delete()) {
237 throw new DemoRecorderException("Could not delete the existing video destinatin file " + destinationFile.getAbsolutePath()
238 + " (application setting to overwrite existing video files is enabled!)");
241 destinationFilePath = this.videoDestination + "_copy" + this.getVideoDestinationCopyNr(sourceFileExtension) + "." + sourceFileExtension;
242 destinationFile = new File(destinationFilePath);
246 //finally move the file
247 if (!sourceFile.renameTo(destinationFile)) {
248 cleanUpFiles.add(destinationFile);
249 throw new DemoRecorderException("Could not move the video file from " + sourceFile.getAbsolutePath()
250 + " to " + destinationFile.getAbsolutePath());
253 this.actualVideoDestination = destinationFile;
257 * As destination video files, e.g. "test"[.avi] can already exist, we have to save the
258 * the video file to a file name such as test_copy1 or test_copy2.
259 * This function will figure out what the number (1, 2....) is.
262 private int getVideoDestinationCopyNr(String sourceFileExtension) {
266 lastFile = new File(this.videoDestination + "_copy" + i + "." + sourceFileExtension);
267 if (!lastFile.exists()) {
276 private File computeCutDemoFile() {
277 String origFileString = this.demoFile.getAbsolutePath();
278 int lastIndex = origFileString.lastIndexOf(File.separator);
279 String autoDemoFileName = origFileString.substring(lastIndex+1, origFileString.length());
281 autoDemoFileName = autoDemoFileName.substring(0, autoDemoFileName.length()-4);
282 autoDemoFileName = autoDemoFileName + CUT_DEMO_FILE_SUFFIX + ".dem";
283 String finalString = origFileString.substring(0, lastIndex) + File.separator + autoDemoFileName;
284 File f = new File(finalString);
289 private void cutDemo(File cutDemo) {
290 String injectAtStart = "";
291 String injectBeforeCap = "";
292 String injectAfterCap = "";
294 NDRPreferences preferences = this.appLayer.getPreferences();
295 if (Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_RENDERING))) {
296 injectAtStart += "r_render 0;";
297 injectBeforeCap += "r_render 1;";
299 if (Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_SOUND))) {
300 injectAtStart += "set _volume $volume;volume 0;";
301 injectBeforeCap += "set volume $_volume;";
303 injectBeforeCap += this.executeBeforeCap + "\n";
304 injectBeforeCap += "set _cl_capturevideo_nameformat $cl_capturevideo_nameformat;set _cl_capturevideo_number $cl_capturevideo_number;";
305 injectBeforeCap += "cl_capturevideo_nameformat " + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE + ";";
306 injectBeforeCap += "cl_capturevideo_number " + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE + ";";
308 injectAfterCap += this.executeAfterCap + "\n";
309 injectAfterCap += "cl_capturevideo_nameformat $_cl_capturevideo_nameformat;cl_capturevideo_number $_cl_capturevideo_number;";
312 DemoCutter cutter = new DemoCutter();
313 int fwdSpeedFirstStage, fwdSpeedSecondStage;
315 fwdSpeedFirstStage = Integer.parseInt(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_FIRST_STAGE));
316 fwdSpeedSecondStage = Integer.parseInt(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_SECOND_STAGE));
317 } catch (NumberFormatException e) {
318 throw new DemoRecorderException("Make sure that you specified valid numbers for the settings "
319 + Preferences.FFW_SPEED_FIRST_STAGE + " and " + Preferences.FFW_SPEED_SECOND_STAGE, e);
334 } catch (DemoCutterException e) {
335 throw new DemoRecorderException("Error occurred while trying to cut the demo: " + e.getMessage(), e);
340 private void removeOldAutocaps() {
341 for (String videoExtension : VIDEO_FILE_ENDINGS) {
342 String fileString = this.dpVideoPath.getAbsolutePath() + File.separator + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE
343 + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE + "." + videoExtension;
344 File videoFile = new File(fileString);
345 cleanUpFiles.add(videoFile);
346 if (videoFile.exists()) {
347 if (!videoFile.delete()) {
348 throw new DemoRecorderException("Could not delete old obsolete video file " + fileString);
354 private void recordClip(File cutDemo) {
356 String demoFileName = DemoRecorderUtils.getJustFileNameOfPath(cutDemo);
357 String execPath = this.enginePath.getAbsolutePath() + " " + this.engineParameters + " -demo "
358 + this.relativeDemoPath + "/" + demoFileName;
359 File engineDir = this.enginePath.getParentFile();
361 nexProc = Runtime.getRuntime().exec(execPath, null, engineDir);
362 nexProc.getErrorStream();
363 nexProc.getOutputStream();
364 InputStream is = nexProc.getInputStream();
365 InputStreamReader isr = new InputStreamReader(is);
366 BufferedReader br = new BufferedReader(isr);
367 while (br.readLine() != null) {
368 //System.out.println(line);
370 } catch (IOException e) {
371 throw new DemoRecorderException("I/O Exception occurred when trying to execute the Nexuiz binary", e);
379 public void setAppLayer(DemoRecorderApplication appLayer) {
380 this.appLayer = appLayer;
383 public int getJobIndex() {
387 public File getEnginePath() {
391 public void setEnginePath(File enginePath) {
392 this.checkForProcessingState();
393 if (enginePath == null || !enginePath.exists()) {
394 throw new DemoRecorderException("Could not locate engine binary!");
396 if (!enginePath.canExecute()) {
397 throw new DemoRecorderException("The file you specified is not executable!");
399 this.enginePath = enginePath.getAbsoluteFile();
402 public String getEngineParameters() {
403 return engineParameters;
406 public void setEngineParameters(String engineParameters) {
407 this.checkForProcessingState();
408 if (engineParameters == null) {
409 engineParameters = "";
411 this.engineParameters = engineParameters.trim();
414 public File getDemoFile() {
418 public void setDemoFile(File demoFile) {
419 this.checkForProcessingState();
420 if (demoFile == null) {
421 throw new DemoRecorderException("Could not locate demo file!");
423 if (!demoFile.exists()) {
424 throw new DemoRecorderException("Could not locate demo file!: " + demoFile.getAbsolutePath());
426 if (!doReadWriteTest(demoFile.getParentFile())) {
427 throw new DemoRecorderException("The directory you specified for the demo to be recorded is not writable!");
429 if (!demoFile.getAbsolutePath().endsWith(".dem")) {
430 throw new DemoRecorderException("The demo file you specified must have the ending .dem");
433 this.demoFile = demoFile.getAbsoluteFile();
436 public String getRelativeDemoPath() {
437 return relativeDemoPath;
440 public void setRelativeDemoPath(String relativeDemoPath) {
441 this.checkForProcessingState();
442 if (relativeDemoPath == null) {
443 relativeDemoPath = "";
446 //get rid of possible slashes
447 while (relativeDemoPath.startsWith("/") || relativeDemoPath.startsWith("\\")) {
448 relativeDemoPath = relativeDemoPath.substring(1, relativeDemoPath.length());
450 while (relativeDemoPath.endsWith("/") || relativeDemoPath.endsWith("\\")) {
451 relativeDemoPath = relativeDemoPath.substring(0, relativeDemoPath.length() - 1);
454 this.relativeDemoPath = relativeDemoPath.trim();
457 public File getDpVideoPath() {
461 public void setDpVideoPath(File dpVideoPath) {
462 this.checkForProcessingState();
463 if (dpVideoPath == null || !dpVideoPath.isDirectory()) {
464 throw new DemoRecorderException("Could not locate the specified DPVideo directory!");
467 if (!this.doReadWriteTest(dpVideoPath)) {
468 throw new DemoRecorderException("The DPVideo directory is not writable! It needs to be writable so that the file can be moved to its new location");
470 this.dpVideoPath = dpVideoPath.getAbsoluteFile();
473 public File getVideoDestination() {
474 return videoDestination;
477 public void setVideoDestination(File videoDestination) {
478 this.checkForProcessingState();
479 //keep in mind, the parameter videoDestination points to the final avi/ogg file w/o extension!
480 if (videoDestination == null || !videoDestination.getParentFile().isDirectory()) {
481 throw new DemoRecorderException("Could not locate the specified video destination");
484 if (!this.doReadWriteTest(videoDestination.getParentFile())) {
485 throw new DemoRecorderException("The video destination directory is not writable! It needs to be writable so that the file can be moved to its new location");
488 this.videoDestination = videoDestination.getAbsoluteFile();
491 public String getExecuteBeforeCap() {
492 return executeBeforeCap;
495 public void setExecuteBeforeCap(String executeBeforeCap) {
496 this.checkForProcessingState();
497 if (executeBeforeCap == null) {
498 executeBeforeCap = "";
500 executeBeforeCap = executeBeforeCap.trim();
501 while (executeBeforeCap.endsWith(";")) {
502 executeBeforeCap = executeBeforeCap.substring(0, executeBeforeCap.length()-1);
504 this.executeBeforeCap = executeBeforeCap;
507 public String getExecuteAfterCap() {
508 return executeAfterCap;
511 public void setExecuteAfterCap(String executeAfterCap) {
512 this.checkForProcessingState();
513 if (executeAfterCap == null) {
514 executeAfterCap = "";
516 executeAfterCap = executeAfterCap.trim();
517 while (executeAfterCap.endsWith(";")) {
518 executeAfterCap = executeAfterCap.substring(0, executeAfterCap.length()-1);
520 if (executeAfterCap.contains("cl_capturevideo_number") || executeAfterCap.contains("cl_capturevideo_nameformat")) {
521 throw new DemoRecorderException("Execute after String cannot contain cl_capturevideo_number or _nameformat changes!");
523 this.executeAfterCap = executeAfterCap;
526 public float getStartSecond() {
530 public void setStartSecond(float startSecond) {
531 this.checkForProcessingState();
532 if (startSecond < 0) {
533 throw new DemoRecorderException("Start second cannot be < 0");
535 this.startSecond = startSecond;
538 public float getEndSecond() {
542 public void setEndSecond(float endSecond) {
543 this.checkForProcessingState();
544 if (endSecond < this.startSecond) {
545 throw new DemoRecorderException("End second cannot be < start second");
547 this.endSecond = endSecond;
550 public State getState() {
554 public void setState(State state) {
556 this.appLayer.fireUserInterfaceUpdate(this);
559 public String getJobName() {
560 if (this.jobName == null || this.jobName.equals("")) {
561 return "Job " + this.jobIndex;
566 public void setJobName(String jobName) {
567 if (jobName == null || jobName.equals("")) {
568 this.jobIndex = appLayer.getNewJobIndex();
569 this.jobName = "Job " + this.jobIndex;
571 this.jobName = jobName;
575 public DemoRecorderException getLastException() {
576 return lastException;
580 * Tests whether the given directory is writable by creating a file in there and deleting
583 * @return true if directory is writable
585 protected boolean doReadWriteTest(File directory) {
586 boolean writable = false;
587 String fileName = "tmp." + Math.random()*10000 + ".dat";
588 File tempFile = new File(directory, fileName);
590 writable = tempFile.createNewFile();
594 } catch (IOException e) {
600 private void checkForProcessingState() {
601 if (this.state == State.PROCESSING) {
602 throw new DemoRecorderException("Cannot modify this job while it is processing!");
606 public Properties getEncoderPluginSettings(EncoderPlugin plugin) {
607 if (this.encoderPluginSettings.containsKey(plugin.getName())) {
608 return this.encoderPluginSettings.get(plugin.getName());
610 return new Properties();
614 public void setEncoderPluginSetting(String pluginName, String pluginSettingKey, String value) {
615 Properties p = this.encoderPluginSettings.get(pluginName);
617 p = new Properties();
618 this.encoderPluginSettings.put(pluginName, p);
621 p.put(pluginSettingKey, value);
624 public Map<String, Properties> getEncoderPluginSettings() {
625 return encoderPluginSettings;
628 public void setEncoderPluginSettings(Map<String, Properties> encoderPluginSettings) {
629 this.encoderPluginSettings = encoderPluginSettings;
632 public File getActualVideoDestination() {
633 return actualVideoDestination;
636 public void setActualVideoDestination(File actualVideoDestination) {
637 this.actualVideoDestination = actualVideoDestination;