]> git.xonotic.org Git - xonotic/xonotic.git/blobdiff - misc/tools/NexuizDemoRecorder/main/src/main/java/com/nexuiz/demorecorder/application/jobs/RecordJob.java
fix lots of CRLFs
[xonotic/xonotic.git] / misc / tools / NexuizDemoRecorder / main / src / main / java / com / nexuiz / demorecorder / application / jobs / RecordJob.java
index 32f663170b6c891c498bbb21b511b8672cf63b39..c56a7e44e57af8a6415b0975e9cbb2f8d942b6e2 100644 (file)
-package com.nexuiz.demorecorder.application.jobs;\r
-\r
-import java.io.BufferedReader;\r
-import java.io.File;\r
-import java.io.IOException;\r
-import java.io.InputStream;\r
-import java.io.InputStreamReader;\r
-import java.io.Serializable;\r
-import java.util.ArrayList;\r
-import java.util.HashMap;\r
-import java.util.List;\r
-import java.util.Map;\r
-import java.util.Properties;\r
-\r
-import com.nexuiz.demorecorder.application.DemoRecorderApplication;\r
-import com.nexuiz.demorecorder.application.DemoRecorderException;\r
-import com.nexuiz.demorecorder.application.DemoRecorderUtils;\r
-import com.nexuiz.demorecorder.application.NDRPreferences;\r
-import com.nexuiz.demorecorder.application.DemoRecorderApplication.Preferences;\r
-import com.nexuiz.demorecorder.application.democutter.DemoCutter;\r
-import com.nexuiz.demorecorder.application.democutter.DemoCutterException;\r
-import com.nexuiz.demorecorder.application.plugins.EncoderPlugin;\r
-import com.nexuiz.demorecorder.application.plugins.EncoderPluginException;\r
-\r
-public class RecordJob implements Runnable, Serializable {\r
-       \r
-       private static final long serialVersionUID = -4585637490345587912L;\r
-\r
-       public enum State {\r
-               WAITING, PROCESSING, ERROR, ERROR_PLUGIN, DONE\r
-       }\r
-       \r
-       public static final String CUT_DEMO_FILE_SUFFIX = "_autocut";\r
-       public static final String CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE = "autocap";\r
-       public static final String CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE = "1234567";\r
-       protected static final String[] VIDEO_FILE_ENDINGS = {"avi", "ogv"};\r
-       \r
-       private DemoRecorderApplication appLayer;\r
-       protected String jobName;\r
-       private int jobIndex;\r
-       protected File enginePath;\r
-       protected String engineParameters;\r
-       protected File demoFile;\r
-       protected String relativeDemoPath;\r
-       protected File dpVideoPath;\r
-       protected File videoDestination;\r
-       protected String executeBeforeCap;\r
-       protected String executeAfterCap;\r
-       protected float startSecond;\r
-       protected float endSecond;\r
-       protected State state = State.WAITING;\r
-       protected DemoRecorderException lastException = null;\r
-       \r
-       /**\r
-        * Points to the actual final file, including possible suffixes, e.g. _copy1, and the actualy ending\r
-        */\r
-       protected File actualVideoDestination = null;\r
-       /**\r
-        * Map that identifies the plug-in by its name (String) and maps to the plug-in's job-specific settings\r
-        */\r
-       protected Map<String, Properties> encoderPluginSettings = new HashMap<String, Properties>();\r
-       \r
-       private List<File> cleanUpFiles = null;\r
-       \r
-       public RecordJob(\r
-               DemoRecorderApplication appLayer,\r
-               String jobName,\r
-               int jobIndex,\r
-               File enginePath,\r
-               String engineParameters,\r
-               File demoFile,\r
-               String relativeDemoPath,\r
-               File dpVideoPath,\r
-               File videoDestination,\r
-               String executeBeforeCap,\r
-               String executeAfterCap,\r
-               float startSecond,\r
-               float endSecond\r
-       ) {\r
-               this.appLayer = appLayer;\r
-               this.jobName = jobName;\r
-               this.jobIndex = jobIndex;\r
-               \r
-               this.setEnginePath(enginePath);\r
-               this.setEngineParameters(engineParameters);\r
-               this.setDemoFile(demoFile);\r
-               this.setRelativeDemoPath(relativeDemoPath);\r
-               this.setDpVideoPath(dpVideoPath);\r
-               this.setVideoDestination(videoDestination);\r
-               this.setExecuteBeforeCap(executeBeforeCap);\r
-               this.setExecuteAfterCap(executeAfterCap);\r
-               this.setStartSecond(startSecond);\r
-               this.setEndSecond(endSecond);\r
-       }\r
-       \r
-       public RecordJob(){}\r
-       \r
-       /**\r
-        * Constructor that can be used by other classes such as job templates. Won't throw exceptions\r
-        * as it won't check the input for validity.\r
-        */\r
-       protected RecordJob(\r
-               File enginePath,\r
-               String engineParameters,\r
-               File demoFile,\r
-               String relativeDemoPath,\r
-               File dpVideoPath,\r
-               File videoDestination,\r
-               String executeBeforeCap,\r
-               String executeAfterCap,\r
-               float startSecond,\r
-               float endSecond\r
-               ) {\r
-               this.jobIndex = -1;\r
-               this.enginePath = enginePath;\r
-               this.engineParameters = engineParameters;\r
-               this.demoFile = demoFile;\r
-               this.relativeDemoPath = relativeDemoPath;\r
-               this.dpVideoPath = dpVideoPath;\r
-               this.videoDestination = videoDestination;\r
-               this.executeBeforeCap = executeBeforeCap;\r
-               this.executeAfterCap = executeAfterCap;\r
-               this.startSecond = startSecond;\r
-               this.endSecond = endSecond;\r
-       }\r
-       \r
-       public void execute() {\r
-               if (this.state == State.PROCESSING) {\r
-                       return;\r
-               }\r
-               boolean errorOccurred = false;\r
-               this.setState(State.PROCESSING);\r
-               this.appLayer.fireUserInterfaceUpdate(this);\r
-               cleanUpFiles = new ArrayList<File>();\r
-               \r
-               File cutDemo = computeCutDemoFile();\r
-               cutDemo.delete(); //delete possibly old cutDemoFile\r
-               \r
-               EncoderPlugin recentEncoder = null;\r
-               \r
-               try {\r
-                       this.cutDemo(cutDemo);\r
-                       this.removeOldAutocaps();\r
-                       this.recordClip(cutDemo);\r
-                       this.moveRecordedClip();\r
-                       for (EncoderPlugin plugin : this.appLayer.getEncoderPlugins()) {\r
-                               recentEncoder = plugin;\r
-                               plugin.executeEncoder(this);\r
-                       }\r
-               } catch (DemoRecorderException e) {\r
-                       errorOccurred = true;\r
-                       this.lastException = e;\r
-                       this.setState(State.ERROR);\r
-               } catch (EncoderPluginException e) {\r
-                       errorOccurred = true;\r
-                       this.lastException = new DemoRecorderException("Encoder plug-in " + recentEncoder.getName() + " failed: "\r
-                                       + e.getMessage(), e);\r
-                       this.setState(State.ERROR_PLUGIN);\r
-               } catch (Exception e) {\r
-                       errorOccurred = true;\r
-                       this.lastException = new DemoRecorderException("Executing job failed, click on details for more info", e);\r
-               } finally {\r
-                       NDRPreferences preferences = this.appLayer.getPreferences();\r
-                       if (!Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DO_NOT_DELETE_CUT_DEMOS))) {\r
-                               cleanUpFiles.add(cutDemo);\r
-                       }\r
-                       if (!errorOccurred) {\r
-                               this.setState(State.DONE);\r
-                       }\r
-                       this.cleanUpFiles();\r
-                       this.appLayer.fireUserInterfaceUpdate(this);\r
-                       this.appLayer.saveJobQueue();\r
-               }\r
-       }\r
-       \r
-       /**\r
-        * Will execute just the specified encoder plug-in on an already "done" job.\r
-        * @param pluginName\r
-        */\r
-       public void executePlugin(EncoderPlugin plugin) {\r
-               if (this.getState() != State.DONE) {\r
-                       return;\r
-               }\r
-               this.setState(State.PROCESSING);\r
-               this.appLayer.fireUserInterfaceUpdate(this);\r
-               \r
-               try {\r
-                       plugin.executeEncoder(this);\r
-                       this.setState(State.DONE);\r
-               } catch (EncoderPluginException e) {\r
-                       this.lastException = new DemoRecorderException("Encoder plug-in " + plugin.getName() + " failed: "\r
-                                       + e.getMessage(), e);\r
-                       this.setState(State.ERROR_PLUGIN);\r
-               }\r
-               \r
-               this.appLayer.fireUserInterfaceUpdate(this);\r
-       }\r
-       \r
-       private void cleanUpFiles() {\r
-               try {\r
-                       for (File f : this.cleanUpFiles) {\r
-                               f.delete();\r
-                       }\r
-               } catch (Exception e) {}\r
-               \r
-       }\r
-       \r
-       private void moveRecordedClip() {\r
-               //1. Figure out whether the file is .avi or .ogv\r
-               File sourceFile = null;\r
-               for (String videoExtension : VIDEO_FILE_ENDINGS) {\r
-                       String fileString = this.dpVideoPath.getAbsolutePath() + File.separator + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE\r
-                       + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE + "." + videoExtension;\r
-                       File videoFile = new File(fileString);\r
-                       if (videoFile.exists()) {\r
-                               sourceFile = videoFile;\r
-                               break;\r
-                       }\r
-               }\r
-               \r
-               if (sourceFile == null) {\r
-                       String p = this.dpVideoPath.getAbsolutePath() + File.separator + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE\r
-                       + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE;\r
-                       throw new DemoRecorderException("Could not locate the expected video file being generated by Nexuiz (should have been at "\r
-                                       + p + ".avi/.ogv");\r
-               }\r
-               cleanUpFiles.add(sourceFile);\r
-               \r
-               File destinationFile = null;\r
-               NDRPreferences preferences = this.appLayer.getPreferences();\r
-               String sourceFileExtension = DemoRecorderUtils.getFileExtension(sourceFile);\r
-               String destinationFilePath = this.videoDestination + "." + sourceFileExtension;\r
-               destinationFile = new File(destinationFilePath);\r
-               if (destinationFile.exists()) {\r
-                       if (Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.OVERWRITE_VIDEO_FILE))) {\r
-                               if (!destinationFile.delete()) {\r
-                                       throw new DemoRecorderException("Could not delete the existing video destinatin file " + destinationFile.getAbsolutePath()\r
-                                                       + " (application setting to overwrite existing video files is enabled!)");\r
-                               }\r
-                       } else {\r
-                               destinationFilePath = this.videoDestination + "_copy" + this.getVideoDestinationCopyNr(sourceFileExtension) + "." + sourceFileExtension;\r
-                               destinationFile = new File(destinationFilePath);\r
-                       }\r
-               }\r
-               \r
-               //finally move the file\r
-               if (!sourceFile.renameTo(destinationFile)) {\r
-                       cleanUpFiles.add(destinationFile);\r
-                       throw new DemoRecorderException("Could not move the video file from " + sourceFile.getAbsolutePath()\r
-                                       + " to " + destinationFile.getAbsolutePath());\r
-               }\r
-               \r
-               this.actualVideoDestination = destinationFile;\r
-       }\r
-       \r
-       /**\r
-        * As destination video files, e.g. "test"[.avi] can already exist, we have to save the\r
-        * the video file to a file name such as test_copy1 or test_copy2.\r
-        * This function will figure out what the number (1, 2....) is.\r
-        * @return\r
-        */\r
-       private int getVideoDestinationCopyNr(String sourceFileExtension) {\r
-               int i = 1;\r
-               File lastFile;\r
-               while (true) {\r
-                       lastFile = new File(this.videoDestination + "_copy" + i + "." + sourceFileExtension);\r
-                       if (!lastFile.exists()) {\r
-                               break;\r
-                       }\r
-                       \r
-                       i++;\r
-               }\r
-               return i;\r
-       }\r
-\r
-       private File computeCutDemoFile() {\r
-               String origFileString = this.demoFile.getAbsolutePath();\r
-               int lastIndex = origFileString.lastIndexOf(File.separator);\r
-               String autoDemoFileName = origFileString.substring(lastIndex+1, origFileString.length());\r
-               //strip .dem ending\r
-               autoDemoFileName = autoDemoFileName.substring(0, autoDemoFileName.length()-4);\r
-               autoDemoFileName = autoDemoFileName + CUT_DEMO_FILE_SUFFIX + ".dem";\r
-               String finalString = origFileString.substring(0, lastIndex) + File.separator + autoDemoFileName;\r
-               File f = new File(finalString);\r
-               \r
-               return f;\r
-       }\r
-       \r
-       private void cutDemo(File cutDemo) {\r
-               String injectAtStart = "";\r
-               String injectBeforeCap = "";\r
-               String injectAfterCap = "";\r
-               \r
-               NDRPreferences preferences = this.appLayer.getPreferences();\r
-               if (Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_RENDERING))) {\r
-                       injectAtStart += "r_render 0;";\r
-                       injectBeforeCap += "r_render 1;";\r
-               }\r
-               if (Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_SOUND))) {\r
-                       injectAtStart += "set _volume $volume;volume 0;";\r
-                       injectBeforeCap += "set volume $_volume;";\r
-               }\r
-               injectBeforeCap += this.executeBeforeCap + "\n";\r
-               injectBeforeCap += "set _cl_capturevideo_nameformat $cl_capturevideo_nameformat;set _cl_capturevideo_number $cl_capturevideo_number;";\r
-               injectBeforeCap += "cl_capturevideo_nameformat " + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE + ";";\r
-               injectBeforeCap += "cl_capturevideo_number " + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE + ";";\r
-               \r
-               injectAfterCap += this.executeAfterCap + "\n";\r
-               injectAfterCap += "cl_capturevideo_nameformat $_cl_capturevideo_nameformat;cl_capturevideo_number $_cl_capturevideo_number;";\r
-               \r
-               \r
-               DemoCutter cutter = new DemoCutter();\r
-               int fwdSpeedFirstStage, fwdSpeedSecondStage;\r
-               try {\r
-                       fwdSpeedFirstStage = Integer.parseInt(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_FIRST_STAGE));\r
-                       fwdSpeedSecondStage = Integer.parseInt(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_SECOND_STAGE));\r
-               } catch (NumberFormatException e) {\r
-                       throw new DemoRecorderException("Make sure that you specified valid numbers for the settings "\r
-                                       + Preferences.FFW_SPEED_FIRST_STAGE + " and " + Preferences.FFW_SPEED_SECOND_STAGE, e);\r
-               }\r
-               \r
-               try {\r
-                       cutter.cutDemo(\r
-                               this.demoFile,\r
-                               cutDemo,\r
-                               this.startSecond,\r
-                               this.endSecond,\r
-                               injectAtStart,\r
-                               injectBeforeCap,\r
-                               injectAfterCap,\r
-                               fwdSpeedFirstStage,\r
-                               fwdSpeedSecondStage\r
-                       );\r
-               } catch (DemoCutterException e) {\r
-                       throw new DemoRecorderException("Error occurred while trying to cut the demo: " + e.getMessage(), e);\r
-               }\r
-               \r
-       }\r
-       \r
-       private void removeOldAutocaps() {\r
-               for (String videoExtension : VIDEO_FILE_ENDINGS) {\r
-                       String fileString = this.dpVideoPath.getAbsolutePath() + File.separator + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE\r
-                       + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE + "." + videoExtension;\r
-                       File videoFile = new File(fileString);\r
-                       cleanUpFiles.add(videoFile);\r
-                       if (videoFile.exists()) {\r
-                               if (!videoFile.delete()) {\r
-                                       throw new DemoRecorderException("Could not delete old obsolete video file " + fileString);\r
-                               }\r
-                       }\r
-               }\r
-       }\r
-       \r
-       private void recordClip(File cutDemo) {\r
-               Process nexProc;\r
-               String demoFileName = DemoRecorderUtils.getJustFileNameOfPath(cutDemo);\r
-               String execPath = this.enginePath.getAbsolutePath() + " " + this.engineParameters + " -demo "\r
-                                               + this.relativeDemoPath + "/" + demoFileName;\r
-               File engineDir = this.enginePath.getParentFile();\r
-               try {\r
-                       nexProc = Runtime.getRuntime().exec(execPath, null, engineDir);\r
-                       nexProc.getErrorStream();\r
-                       nexProc.getOutputStream();\r
-                       InputStream is = nexProc.getInputStream();\r
-                       InputStreamReader isr = new InputStreamReader(is);\r
-                       BufferedReader br = new BufferedReader(isr);\r
-                       while (br.readLine() != null) {\r
-                               //System.out.println(line);\r
-                       }\r
-               } catch (IOException e) {\r
-                       throw new DemoRecorderException("I/O Exception occurred when trying to execute the Nexuiz binary", e);\r
-               }\r
-       }\r
-\r
-       public void run() {\r
-               this.execute();\r
-       }\r
-       \r
-       public void setAppLayer(DemoRecorderApplication appLayer) {\r
-               this.appLayer = appLayer;\r
-       }\r
-\r
-       public int getJobIndex() {\r
-               return jobIndex;\r
-       }\r
-\r
-       public File getEnginePath() {\r
-               return enginePath;\r
-       }\r
-\r
-       public void setEnginePath(File enginePath) {\r
-               this.checkForProcessingState();\r
-               if (enginePath == null || !enginePath.exists()) {\r
-                       throw new DemoRecorderException("Could not locate engine binary!");\r
-               }\r
-               if (!enginePath.canExecute()) {\r
-                       throw new DemoRecorderException("The file you specified is not executable!");\r
-               }\r
-               this.enginePath = enginePath.getAbsoluteFile();\r
-       }\r
-\r
-       public String getEngineParameters() {\r
-               return engineParameters;\r
-       }\r
-\r
-       public void setEngineParameters(String engineParameters) {\r
-               this.checkForProcessingState();\r
-               if (engineParameters == null) {\r
-                       engineParameters = "";\r
-               }\r
-               this.engineParameters = engineParameters.trim();\r
-       }\r
-\r
-       public File getDemoFile() {\r
-               return demoFile;\r
-       }\r
-\r
-       public void setDemoFile(File demoFile) {\r
-               this.checkForProcessingState();\r
-               if (demoFile == null) {\r
-                       throw new DemoRecorderException("Could not locate demo file!");\r
-               }\r
-               if (!demoFile.exists()) {\r
-                       throw new DemoRecorderException("Could not locate demo file!: " + demoFile.getAbsolutePath());\r
-               }\r
-               if (!doReadWriteTest(demoFile.getParentFile())) {\r
-                       throw new DemoRecorderException("The directory you specified for the demo to be recorded is not writable!");\r
-               }\r
-               if (!demoFile.getAbsolutePath().endsWith(".dem")) {\r
-                       throw new DemoRecorderException("The demo file you specified must have the ending .dem");\r
-               }\r
-               \r
-               this.demoFile = demoFile.getAbsoluteFile();\r
-       }\r
-\r
-       public String getRelativeDemoPath() {\r
-               return relativeDemoPath;\r
-       }\r
-\r
-       public void setRelativeDemoPath(String relativeDemoPath) {\r
-               this.checkForProcessingState();\r
-               if (relativeDemoPath == null) {\r
-                       relativeDemoPath = "";\r
-               }\r
-               \r
-               //get rid of possible slashes\r
-               while (relativeDemoPath.startsWith("/") || relativeDemoPath.startsWith("\\")) {\r
-                       relativeDemoPath = relativeDemoPath.substring(1, relativeDemoPath.length());\r
-               }\r
-               while (relativeDemoPath.endsWith("/") || relativeDemoPath.endsWith("\\")) {\r
-                       relativeDemoPath = relativeDemoPath.substring(0, relativeDemoPath.length() - 1);\r
-               }\r
-               \r
-               this.relativeDemoPath = relativeDemoPath.trim();\r
-       }\r
-\r
-       public File getDpVideoPath() {\r
-               return dpVideoPath;\r
-       }\r
-\r
-       public void setDpVideoPath(File dpVideoPath) {\r
-               this.checkForProcessingState();\r
-               if (dpVideoPath == null || !dpVideoPath.isDirectory()) {\r
-                       throw new DemoRecorderException("Could not locate the specified DPVideo directory!");\r
-               }\r
-               \r
-               if (!this.doReadWriteTest(dpVideoPath)) {\r
-                       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
-               }\r
-               this.dpVideoPath = dpVideoPath.getAbsoluteFile();\r
-       }\r
-\r
-       public File getVideoDestination() {\r
-               return videoDestination;\r
-       }\r
-\r
-       public void setVideoDestination(File videoDestination) {\r
-               this.checkForProcessingState();\r
-               //keep in mind, the parameter videoDestination points to the final avi/ogg file w/o extension!\r
-               if (videoDestination == null || !videoDestination.getParentFile().isDirectory()) {\r
-                       throw new DemoRecorderException("Could not locate the specified video destination");\r
-               }\r
-               \r
-               if (!this.doReadWriteTest(videoDestination.getParentFile())) {\r
-                       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
-               }\r
-               \r
-               this.videoDestination = videoDestination.getAbsoluteFile();\r
-       }\r
-\r
-       public String getExecuteBeforeCap() {\r
-               return executeBeforeCap;\r
-       }\r
-\r
-       public void setExecuteBeforeCap(String executeBeforeCap) {\r
-               this.checkForProcessingState();\r
-               if (executeBeforeCap == null) {\r
-                       executeBeforeCap = "";\r
-               }\r
-               executeBeforeCap = executeBeforeCap.trim();\r
-               while (executeBeforeCap.endsWith(";")) {\r
-                       executeBeforeCap = executeBeforeCap.substring(0, executeBeforeCap.length()-1);\r
-               }\r
-               this.executeBeforeCap = executeBeforeCap;\r
-       }\r
-\r
-       public String getExecuteAfterCap() {\r
-               return executeAfterCap;\r
-       }\r
-\r
-       public void setExecuteAfterCap(String executeAfterCap) {\r
-               this.checkForProcessingState();\r
-               if (executeAfterCap == null) {\r
-                       executeAfterCap = "";\r
-               }\r
-               executeAfterCap = executeAfterCap.trim();\r
-               while (executeAfterCap.endsWith(";")) {\r
-                       executeAfterCap = executeAfterCap.substring(0, executeAfterCap.length()-1);\r
-               }\r
-               if (executeAfterCap.contains("cl_capturevideo_number") || executeAfterCap.contains("cl_capturevideo_nameformat")) {\r
-                       throw new DemoRecorderException("Execute after String cannot contain cl_capturevideo_number or _nameformat changes!");\r
-               }\r
-               this.executeAfterCap = executeAfterCap;\r
-       }\r
-\r
-       public float getStartSecond() {\r
-               return startSecond;\r
-       }\r
-\r
-       public void setStartSecond(float startSecond) {\r
-               this.checkForProcessingState();\r
-               if (startSecond < 0) {\r
-                       throw new DemoRecorderException("Start second cannot be < 0");\r
-               }\r
-               this.startSecond = startSecond;\r
-       }\r
-\r
-       public float getEndSecond() {\r
-               return endSecond;\r
-       }\r
-\r
-       public void setEndSecond(float endSecond) {\r
-               this.checkForProcessingState();\r
-               if (endSecond < this.startSecond) {\r
-                       throw new DemoRecorderException("End second cannot be < start second");\r
-               }\r
-               this.endSecond = endSecond;\r
-       }\r
-\r
-       public State getState() {\r
-               return state;\r
-       }\r
-\r
-       public void setState(State state) {\r
-               this.state = state;\r
-               this.appLayer.fireUserInterfaceUpdate(this);\r
-       }\r
-\r
-       public String getJobName() {\r
-               if (this.jobName == null || this.jobName.equals("")) {\r
-                       return "Job " + this.jobIndex;\r
-               }\r
-               return this.jobName;\r
-       }\r
-       \r
-       public void setJobName(String jobName) {\r
-               if (jobName == null || jobName.equals("")) {\r
-                       this.jobIndex = appLayer.getNewJobIndex();\r
-                       this.jobName = "Job " + this.jobIndex;\r
-               } else {\r
-                       this.jobName = jobName;\r
-               }\r
-       }\r
-\r
-       public DemoRecorderException getLastException() {\r
-               return lastException;\r
-       }\r
-       \r
-       /**\r
-        * Tests whether the given directory is writable by creating a file in there and deleting\r
-        * it again.\r
-        * @param directory\r
-        * @return true if directory is writable\r
-        */\r
-       protected boolean doReadWriteTest(File directory) {\r
-               boolean writable = false;\r
-               String fileName = "tmp." + Math.random()*10000 + ".dat";\r
-               File tempFile = new File(directory, fileName);\r
-               try {\r
-                       writable = tempFile.createNewFile();\r
-                       if (writable) {\r
-                               tempFile.delete();\r
-                       }\r
-               } catch (IOException e) {\r
-                       writable = false;\r
-               }\r
-               return writable;\r
-       }\r
-       \r
-       private void checkForProcessingState() {\r
-               if (this.state == State.PROCESSING) {\r
-                       throw new DemoRecorderException("Cannot modify this job while it is processing!");\r
-               }\r
-       }\r
-\r
-       public Properties getEncoderPluginSettings(EncoderPlugin plugin) {\r
-               if (this.encoderPluginSettings.containsKey(plugin.getName())) {\r
-                       return this.encoderPluginSettings.get(plugin.getName());\r
-               } else {\r
-                       return new Properties();\r
-               }\r
-       }\r
-\r
-       public void setEncoderPluginSetting(String pluginName, String pluginSettingKey, String value) {\r
-               Properties p = this.encoderPluginSettings.get(pluginName);\r
-               if (p == null) {\r
-                       p = new Properties();\r
-                       this.encoderPluginSettings.put(pluginName, p);\r
-               }\r
-               \r
-               p.put(pluginSettingKey, value);\r
-       }\r
-\r
-       public Map<String, Properties> getEncoderPluginSettings() {\r
-               return encoderPluginSettings;\r
-       }\r
-\r
-       public void setEncoderPluginSettings(Map<String, Properties> encoderPluginSettings) {\r
-               this.encoderPluginSettings = encoderPluginSettings;\r
-       }\r
-\r
-       public File getActualVideoDestination() {\r
-               return actualVideoDestination;\r
-       }\r
-       \r
-       public void setActualVideoDestination(File actualVideoDestination) {\r
-               this.actualVideoDestination = actualVideoDestination;\r
-       }\r
-}\r
+package com.nexuiz.demorecorder.application.jobs;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import com.nexuiz.demorecorder.application.DemoRecorderApplication;
+import com.nexuiz.demorecorder.application.DemoRecorderException;
+import com.nexuiz.demorecorder.application.DemoRecorderUtils;
+import com.nexuiz.demorecorder.application.NDRPreferences;
+import com.nexuiz.demorecorder.application.DemoRecorderApplication.Preferences;
+import com.nexuiz.demorecorder.application.democutter.DemoCutter;
+import com.nexuiz.demorecorder.application.democutter.DemoCutterException;
+import com.nexuiz.demorecorder.application.plugins.EncoderPlugin;
+import com.nexuiz.demorecorder.application.plugins.EncoderPluginException;
+
+public class RecordJob implements Runnable, Serializable {
+       
+       private static final long serialVersionUID = -4585637490345587912L;
+
+       public enum State {
+               WAITING, PROCESSING, ERROR, ERROR_PLUGIN, DONE
+       }
+       
+       public static final String CUT_DEMO_FILE_SUFFIX = "_autocut";
+       public static final String CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE = "autocap";
+       public static final String CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE = "1234567";
+       protected static final String[] VIDEO_FILE_ENDINGS = {"avi", "ogv"};
+       
+       private DemoRecorderApplication appLayer;
+       protected String jobName;
+       private int jobIndex;
+       protected File enginePath;
+       protected String engineParameters;
+       protected File demoFile;
+       protected String relativeDemoPath;
+       protected File dpVideoPath;
+       protected File videoDestination;
+       protected String executeBeforeCap;
+       protected String executeAfterCap;
+       protected float startSecond;
+       protected float endSecond;
+       protected State state = State.WAITING;
+       protected DemoRecorderException lastException = null;
+       
+       /**
+        * Points to the actual final file, including possible suffixes, e.g. _copy1, and the actualy ending
+        */
+       protected File actualVideoDestination = null;
+       /**
+        * Map that identifies the plug-in by its name (String) and maps to the plug-in's job-specific settings
+        */
+       protected Map<String, Properties> encoderPluginSettings = new HashMap<String, Properties>();
+       
+       private List<File> cleanUpFiles = null;
+       
+       public RecordJob(
+               DemoRecorderApplication appLayer,
+               String jobName,
+               int jobIndex,
+               File enginePath,
+               String engineParameters,
+               File demoFile,
+               String relativeDemoPath,
+               File dpVideoPath,
+               File videoDestination,
+               String executeBeforeCap,
+               String executeAfterCap,
+               float startSecond,
+               float endSecond
+       ) {
+               this.appLayer = appLayer;
+               this.jobName = jobName;
+               this.jobIndex = jobIndex;
+               
+               this.setEnginePath(enginePath);
+               this.setEngineParameters(engineParameters);
+               this.setDemoFile(demoFile);
+               this.setRelativeDemoPath(relativeDemoPath);
+               this.setDpVideoPath(dpVideoPath);
+               this.setVideoDestination(videoDestination);
+               this.setExecuteBeforeCap(executeBeforeCap);
+               this.setExecuteAfterCap(executeAfterCap);
+               this.setStartSecond(startSecond);
+               this.setEndSecond(endSecond);
+       }
+       
+       public RecordJob(){}
+       
+       /**
+        * Constructor that can be used by other classes such as job templates. Won't throw exceptions
+        * as it won't check the input for validity.
+        */
+       protected RecordJob(
+               File enginePath,
+               String engineParameters,
+               File demoFile,
+               String relativeDemoPath,
+               File dpVideoPath,
+               File videoDestination,
+               String executeBeforeCap,
+               String executeAfterCap,
+               float startSecond,
+               float endSecond
+               ) {
+               this.jobIndex = -1;
+               this.enginePath = enginePath;
+               this.engineParameters = engineParameters;
+               this.demoFile = demoFile;
+               this.relativeDemoPath = relativeDemoPath;
+               this.dpVideoPath = dpVideoPath;
+               this.videoDestination = videoDestination;
+               this.executeBeforeCap = executeBeforeCap;
+               this.executeAfterCap = executeAfterCap;
+               this.startSecond = startSecond;
+               this.endSecond = endSecond;
+       }
+       
+       public void execute() {
+               if (this.state == State.PROCESSING) {
+                       return;
+               }
+               boolean errorOccurred = false;
+               this.setState(State.PROCESSING);
+               this.appLayer.fireUserInterfaceUpdate(this);
+               cleanUpFiles = new ArrayList<File>();
+               
+               File cutDemo = computeCutDemoFile();
+               cutDemo.delete(); //delete possibly old cutDemoFile
+               
+               EncoderPlugin recentEncoder = null;
+               
+               try {
+                       this.cutDemo(cutDemo);
+                       this.removeOldAutocaps();
+                       this.recordClip(cutDemo);
+                       this.moveRecordedClip();
+                       for (EncoderPlugin plugin : this.appLayer.getEncoderPlugins()) {
+                               recentEncoder = plugin;
+                               plugin.executeEncoder(this);
+                       }
+               } catch (DemoRecorderException e) {
+                       errorOccurred = true;
+                       this.lastException = e;
+                       this.setState(State.ERROR);
+               } catch (EncoderPluginException e) {
+                       errorOccurred = true;
+                       this.lastException = new DemoRecorderException("Encoder plug-in " + recentEncoder.getName() + " failed: "
+                                       + e.getMessage(), e);
+                       this.setState(State.ERROR_PLUGIN);
+               } catch (Exception e) {
+                       errorOccurred = true;
+                       this.lastException = new DemoRecorderException("Executing job failed, click on details for more info", e);
+               } finally {
+                       NDRPreferences preferences = this.appLayer.getPreferences();
+                       if (!Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DO_NOT_DELETE_CUT_DEMOS))) {
+                               cleanUpFiles.add(cutDemo);
+                       }
+                       if (!errorOccurred) {
+                               this.setState(State.DONE);
+                       }
+                       this.cleanUpFiles();
+                       this.appLayer.fireUserInterfaceUpdate(this);
+                       this.appLayer.saveJobQueue();
+               }
+       }
+       
+       /**
+        * Will execute just the specified encoder plug-in on an already "done" job.
+        * @param pluginName
+        */
+       public void executePlugin(EncoderPlugin plugin) {
+               if (this.getState() != State.DONE) {
+                       return;
+               }
+               this.setState(State.PROCESSING);
+               this.appLayer.fireUserInterfaceUpdate(this);
+               
+               try {
+                       plugin.executeEncoder(this);
+                       this.setState(State.DONE);
+               } catch (EncoderPluginException e) {
+                       this.lastException = new DemoRecorderException("Encoder plug-in " + plugin.getName() + " failed: "
+                                       + e.getMessage(), e);
+                       this.setState(State.ERROR_PLUGIN);
+               }
+               
+               this.appLayer.fireUserInterfaceUpdate(this);
+       }
+       
+       private void cleanUpFiles() {
+               try {
+                       for (File f : this.cleanUpFiles) {
+                               f.delete();
+                       }
+               } catch (Exception e) {}
+               
+       }
+       
+       private void moveRecordedClip() {
+               //1. Figure out whether the file is .avi or .ogv
+               File sourceFile = null;
+               for (String videoExtension : VIDEO_FILE_ENDINGS) {
+                       String fileString = this.dpVideoPath.getAbsolutePath() + File.separator + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE
+                       + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE + "." + videoExtension;
+                       File videoFile = new File(fileString);
+                       if (videoFile.exists()) {
+                               sourceFile = videoFile;
+                               break;
+                       }
+               }
+               
+               if (sourceFile == null) {
+                       String p = this.dpVideoPath.getAbsolutePath() + File.separator + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE
+                       + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE;
+                       throw new DemoRecorderException("Could not locate the expected video file being generated by Nexuiz (should have been at "
+                                       + p + ".avi/.ogv");
+               }
+               cleanUpFiles.add(sourceFile);
+               
+               File destinationFile = null;
+               NDRPreferences preferences = this.appLayer.getPreferences();
+               String sourceFileExtension = DemoRecorderUtils.getFileExtension(sourceFile);
+               String destinationFilePath = this.videoDestination + "." + sourceFileExtension;
+               destinationFile = new File(destinationFilePath);
+               if (destinationFile.exists()) {
+                       if (Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.OVERWRITE_VIDEO_FILE))) {
+                               if (!destinationFile.delete()) {
+                                       throw new DemoRecorderException("Could not delete the existing video destinatin file " + destinationFile.getAbsolutePath()
+                                                       + " (application setting to overwrite existing video files is enabled!)");
+                               }
+                       } else {
+                               destinationFilePath = this.videoDestination + "_copy" + this.getVideoDestinationCopyNr(sourceFileExtension) + "." + sourceFileExtension;
+                               destinationFile = new File(destinationFilePath);
+                       }
+               }
+               
+               //finally move the file
+               if (!sourceFile.renameTo(destinationFile)) {
+                       cleanUpFiles.add(destinationFile);
+                       throw new DemoRecorderException("Could not move the video file from " + sourceFile.getAbsolutePath()
+                                       + " to " + destinationFile.getAbsolutePath());
+               }
+               
+               this.actualVideoDestination = destinationFile;
+       }
+       
+       /**
+        * As destination video files, e.g. "test"[.avi] can already exist, we have to save the
+        * the video file to a file name such as test_copy1 or test_copy2.
+        * This function will figure out what the number (1, 2....) is.
+        * @return
+        */
+       private int getVideoDestinationCopyNr(String sourceFileExtension) {
+               int i = 1;
+               File lastFile;
+               while (true) {
+                       lastFile = new File(this.videoDestination + "_copy" + i + "." + sourceFileExtension);
+                       if (!lastFile.exists()) {
+                               break;
+                       }
+                       
+                       i++;
+               }
+               return i;
+       }
+
+       private File computeCutDemoFile() {
+               String origFileString = this.demoFile.getAbsolutePath();
+               int lastIndex = origFileString.lastIndexOf(File.separator);
+               String autoDemoFileName = origFileString.substring(lastIndex+1, origFileString.length());
+               //strip .dem ending
+               autoDemoFileName = autoDemoFileName.substring(0, autoDemoFileName.length()-4);
+               autoDemoFileName = autoDemoFileName + CUT_DEMO_FILE_SUFFIX + ".dem";
+               String finalString = origFileString.substring(0, lastIndex) + File.separator + autoDemoFileName;
+               File f = new File(finalString);
+               
+               return f;
+       }
+       
+       private void cutDemo(File cutDemo) {
+               String injectAtStart = "";
+               String injectBeforeCap = "";
+               String injectAfterCap = "";
+               
+               NDRPreferences preferences = this.appLayer.getPreferences();
+               if (Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_RENDERING))) {
+                       injectAtStart += "r_render 0;";
+                       injectBeforeCap += "r_render 1;";
+               }
+               if (Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_SOUND))) {
+                       injectAtStart += "set _volume $volume;volume 0;";
+                       injectBeforeCap += "set volume $_volume;";
+               }
+               injectBeforeCap += this.executeBeforeCap + "\n";
+               injectBeforeCap += "set _cl_capturevideo_nameformat $cl_capturevideo_nameformat;set _cl_capturevideo_number $cl_capturevideo_number;";
+               injectBeforeCap += "cl_capturevideo_nameformat " + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE + ";";
+               injectBeforeCap += "cl_capturevideo_number " + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE + ";";
+               
+               injectAfterCap += this.executeAfterCap + "\n";
+               injectAfterCap += "cl_capturevideo_nameformat $_cl_capturevideo_nameformat;cl_capturevideo_number $_cl_capturevideo_number;";
+               
+               
+               DemoCutter cutter = new DemoCutter();
+               int fwdSpeedFirstStage, fwdSpeedSecondStage;
+               try {
+                       fwdSpeedFirstStage = Integer.parseInt(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_FIRST_STAGE));
+                       fwdSpeedSecondStage = Integer.parseInt(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_SECOND_STAGE));
+               } catch (NumberFormatException e) {
+                       throw new DemoRecorderException("Make sure that you specified valid numbers for the settings "
+                                       + Preferences.FFW_SPEED_FIRST_STAGE + " and " + Preferences.FFW_SPEED_SECOND_STAGE, e);
+               }
+               
+               try {
+                       cutter.cutDemo(
+                               this.demoFile,
+                               cutDemo,
+                               this.startSecond,
+                               this.endSecond,
+                               injectAtStart,
+                               injectBeforeCap,
+                               injectAfterCap,
+                               fwdSpeedFirstStage,
+                               fwdSpeedSecondStage
+                       );
+               } catch (DemoCutterException e) {
+                       throw new DemoRecorderException("Error occurred while trying to cut the demo: " + e.getMessage(), e);
+               }
+               
+       }
+       
+       private void removeOldAutocaps() {
+               for (String videoExtension : VIDEO_FILE_ENDINGS) {
+                       String fileString = this.dpVideoPath.getAbsolutePath() + File.separator + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE
+                       + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE + "." + videoExtension;
+                       File videoFile = new File(fileString);
+                       cleanUpFiles.add(videoFile);
+                       if (videoFile.exists()) {
+                               if (!videoFile.delete()) {
+                                       throw new DemoRecorderException("Could not delete old obsolete video file " + fileString);
+                               }
+                       }
+               }
+       }
+       
+       private void recordClip(File cutDemo) {
+               Process nexProc;
+               String demoFileName = DemoRecorderUtils.getJustFileNameOfPath(cutDemo);
+               String execPath = this.enginePath.getAbsolutePath() + " " + this.engineParameters + " -demo "
+                                               + this.relativeDemoPath + "/" + demoFileName;
+               File engineDir = this.enginePath.getParentFile();
+               try {
+                       nexProc = Runtime.getRuntime().exec(execPath, null, engineDir);
+                       nexProc.getErrorStream();
+                       nexProc.getOutputStream();
+                       InputStream is = nexProc.getInputStream();
+                       InputStreamReader isr = new InputStreamReader(is);
+                       BufferedReader br = new BufferedReader(isr);
+                       while (br.readLine() != null) {
+                               //System.out.println(line);
+                       }
+               } catch (IOException e) {
+                       throw new DemoRecorderException("I/O Exception occurred when trying to execute the Nexuiz binary", e);
+               }
+       }
+
+       public void run() {
+               this.execute();
+       }
+       
+       public void setAppLayer(DemoRecorderApplication appLayer) {
+               this.appLayer = appLayer;
+       }
+
+       public int getJobIndex() {
+               return jobIndex;
+       }
+
+       public File getEnginePath() {
+               return enginePath;
+       }
+
+       public void setEnginePath(File enginePath) {
+               this.checkForProcessingState();
+               if (enginePath == null || !enginePath.exists()) {
+                       throw new DemoRecorderException("Could not locate engine binary!");
+               }
+               if (!enginePath.canExecute()) {
+                       throw new DemoRecorderException("The file you specified is not executable!");
+               }
+               this.enginePath = enginePath.getAbsoluteFile();
+       }
+
+       public String getEngineParameters() {
+               return engineParameters;
+       }
+
+       public void setEngineParameters(String engineParameters) {
+               this.checkForProcessingState();
+               if (engineParameters == null) {
+                       engineParameters = "";
+               }
+               this.engineParameters = engineParameters.trim();
+       }
+
+       public File getDemoFile() {
+               return demoFile;
+       }
+
+       public void setDemoFile(File demoFile) {
+               this.checkForProcessingState();
+               if (demoFile == null) {
+                       throw new DemoRecorderException("Could not locate demo file!");
+               }
+               if (!demoFile.exists()) {
+                       throw new DemoRecorderException("Could not locate demo file!: " + demoFile.getAbsolutePath());
+               }
+               if (!doReadWriteTest(demoFile.getParentFile())) {
+                       throw new DemoRecorderException("The directory you specified for the demo to be recorded is not writable!");
+               }
+               if (!demoFile.getAbsolutePath().endsWith(".dem")) {
+                       throw new DemoRecorderException("The demo file you specified must have the ending .dem");
+               }
+               
+               this.demoFile = demoFile.getAbsoluteFile();
+       }
+
+       public String getRelativeDemoPath() {
+               return relativeDemoPath;
+       }
+
+       public void setRelativeDemoPath(String relativeDemoPath) {
+               this.checkForProcessingState();
+               if (relativeDemoPath == null) {
+                       relativeDemoPath = "";
+               }
+               
+               //get rid of possible slashes
+               while (relativeDemoPath.startsWith("/") || relativeDemoPath.startsWith("\\")) {
+                       relativeDemoPath = relativeDemoPath.substring(1, relativeDemoPath.length());
+               }
+               while (relativeDemoPath.endsWith("/") || relativeDemoPath.endsWith("\\")) {
+                       relativeDemoPath = relativeDemoPath.substring(0, relativeDemoPath.length() - 1);
+               }
+               
+               this.relativeDemoPath = relativeDemoPath.trim();
+       }
+
+       public File getDpVideoPath() {
+               return dpVideoPath;
+       }
+
+       public void setDpVideoPath(File dpVideoPath) {
+               this.checkForProcessingState();
+               if (dpVideoPath == null || !dpVideoPath.isDirectory()) {
+                       throw new DemoRecorderException("Could not locate the specified DPVideo directory!");
+               }
+               
+               if (!this.doReadWriteTest(dpVideoPath)) {
+                       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");
+               }
+               this.dpVideoPath = dpVideoPath.getAbsoluteFile();
+       }
+
+       public File getVideoDestination() {
+               return videoDestination;
+       }
+
+       public void setVideoDestination(File videoDestination) {
+               this.checkForProcessingState();
+               //keep in mind, the parameter videoDestination points to the final avi/ogg file w/o extension!
+               if (videoDestination == null || !videoDestination.getParentFile().isDirectory()) {
+                       throw new DemoRecorderException("Could not locate the specified video destination");
+               }
+               
+               if (!this.doReadWriteTest(videoDestination.getParentFile())) {
+                       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");
+               }
+               
+               this.videoDestination = videoDestination.getAbsoluteFile();
+       }
+
+       public String getExecuteBeforeCap() {
+               return executeBeforeCap;
+       }
+
+       public void setExecuteBeforeCap(String executeBeforeCap) {
+               this.checkForProcessingState();
+               if (executeBeforeCap == null) {
+                       executeBeforeCap = "";
+               }
+               executeBeforeCap = executeBeforeCap.trim();
+               while (executeBeforeCap.endsWith(";")) {
+                       executeBeforeCap = executeBeforeCap.substring(0, executeBeforeCap.length()-1);
+               }
+               this.executeBeforeCap = executeBeforeCap;
+       }
+
+       public String getExecuteAfterCap() {
+               return executeAfterCap;
+       }
+
+       public void setExecuteAfterCap(String executeAfterCap) {
+               this.checkForProcessingState();
+               if (executeAfterCap == null) {
+                       executeAfterCap = "";
+               }
+               executeAfterCap = executeAfterCap.trim();
+               while (executeAfterCap.endsWith(";")) {
+                       executeAfterCap = executeAfterCap.substring(0, executeAfterCap.length()-1);
+               }
+               if (executeAfterCap.contains("cl_capturevideo_number") || executeAfterCap.contains("cl_capturevideo_nameformat")) {
+                       throw new DemoRecorderException("Execute after String cannot contain cl_capturevideo_number or _nameformat changes!");
+               }
+               this.executeAfterCap = executeAfterCap;
+       }
+
+       public float getStartSecond() {
+               return startSecond;
+       }
+
+       public void setStartSecond(float startSecond) {
+               this.checkForProcessingState();
+               if (startSecond < 0) {
+                       throw new DemoRecorderException("Start second cannot be < 0");
+               }
+               this.startSecond = startSecond;
+       }
+
+       public float getEndSecond() {
+               return endSecond;
+       }
+
+       public void setEndSecond(float endSecond) {
+               this.checkForProcessingState();
+               if (endSecond < this.startSecond) {
+                       throw new DemoRecorderException("End second cannot be < start second");
+               }
+               this.endSecond = endSecond;
+       }
+
+       public State getState() {
+               return state;
+       }
+
+       public void setState(State state) {
+               this.state = state;
+               this.appLayer.fireUserInterfaceUpdate(this);
+       }
+
+       public String getJobName() {
+               if (this.jobName == null || this.jobName.equals("")) {
+                       return "Job " + this.jobIndex;
+               }
+               return this.jobName;
+       }
+       
+       public void setJobName(String jobName) {
+               if (jobName == null || jobName.equals("")) {
+                       this.jobIndex = appLayer.getNewJobIndex();
+                       this.jobName = "Job " + this.jobIndex;
+               } else {
+                       this.jobName = jobName;
+               }
+       }
+
+       public DemoRecorderException getLastException() {
+               return lastException;
+       }
+       
+       /**
+        * Tests whether the given directory is writable by creating a file in there and deleting
+        * it again.
+        * @param directory
+        * @return true if directory is writable
+        */
+       protected boolean doReadWriteTest(File directory) {
+               boolean writable = false;
+               String fileName = "tmp." + Math.random()*10000 + ".dat";
+               File tempFile = new File(directory, fileName);
+               try {
+                       writable = tempFile.createNewFile();
+                       if (writable) {
+                               tempFile.delete();
+                       }
+               } catch (IOException e) {
+                       writable = false;
+               }
+               return writable;
+       }
+       
+       private void checkForProcessingState() {
+               if (this.state == State.PROCESSING) {
+                       throw new DemoRecorderException("Cannot modify this job while it is processing!");
+               }
+       }
+
+       public Properties getEncoderPluginSettings(EncoderPlugin plugin) {
+               if (this.encoderPluginSettings.containsKey(plugin.getName())) {
+                       return this.encoderPluginSettings.get(plugin.getName());
+               } else {
+                       return new Properties();
+               }
+       }
+
+       public void setEncoderPluginSetting(String pluginName, String pluginSettingKey, String value) {
+               Properties p = this.encoderPluginSettings.get(pluginName);
+               if (p == null) {
+                       p = new Properties();
+                       this.encoderPluginSettings.put(pluginName, p);
+               }
+               
+               p.put(pluginSettingKey, value);
+       }
+
+       public Map<String, Properties> getEncoderPluginSettings() {
+               return encoderPluginSettings;
+       }
+
+       public void setEncoderPluginSettings(Map<String, Properties> encoderPluginSettings) {
+               this.encoderPluginSettings = encoderPluginSettings;
+       }
+
+       public File getActualVideoDestination() {
+               return actualVideoDestination;
+       }
+       
+       public void setActualVideoDestination(File actualVideoDestination) {
+               this.actualVideoDestination = actualVideoDestination;
+       }
+}