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