-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;
+ }
+}