NexuizDemoRecorder: added ability to save and load templates (might make sense for...
[divverent/nexuiz.git] / misc / tools / NexuizDemoRecorder / main / src / main / java / com / nexuiz / demorecorder / application / DemoRecorderApplication.java
1 package com.nexuiz.demorecorder.application;\r
2 \r
3 import java.io.File;\r
4 import java.io.FileInputStream;\r
5 import java.io.FileNotFoundException;\r
6 import java.io.FileOutputStream;\r
7 import java.io.IOException;\r
8 import java.io.ObjectInputStream;\r
9 import java.io.ObjectOutputStream;\r
10 import java.net.MalformedURLException;\r
11 import java.net.URL;\r
12 import java.net.URLClassLoader;\r
13 import java.util.ArrayList;\r
14 import java.util.List;\r
15 import java.util.Properties;\r
16 import java.util.ServiceLoader;\r
17 import java.util.concurrent.CopyOnWriteArrayList;\r
18 \r
19 import com.nexuiz.demorecorder.application.jobs.EncoderJob;\r
20 import com.nexuiz.demorecorder.application.jobs.RecordJob;\r
21 import com.nexuiz.demorecorder.application.jobs.RecordsDoneJob;\r
22 import com.nexuiz.demorecorder.application.plugins.EncoderPlugin;\r
23 import com.nexuiz.demorecorder.ui.DemoRecorderUI;\r
24 \r
25 public class DemoRecorderApplication {\r
26         \r
27         public static class Preferences {\r
28                 public static final String OVERWRITE_VIDEO_FILE = "Overwrite final video destination file if it exists";\r
29                 public static final String DISABLE_RENDERING = "Disable rendering while fast-forwarding";\r
30                 public static final String DISABLE_SOUND = "Disable sound while fast-forwarding";\r
31                 public static final String FFW_SPEED_FIRST_STAGE = "Fast-forward speed (first stage)";\r
32                 public static final String FFW_SPEED_SECOND_STAGE = "Fast-forward speed (second stage)";\r
33                 public static final String DO_NOT_DELETE_CUT_DEMOS = "Do not delete cut demos";\r
34                 public static final String JOB_NAME_APPEND_DUPLICATE = "Append this suffix to job-name when duplicating jobs";\r
35                 \r
36                 public static final String[] PREFERENCES_ORDER = {\r
37                         OVERWRITE_VIDEO_FILE,\r
38                         DISABLE_RENDERING,\r
39                         DISABLE_SOUND,\r
40                         FFW_SPEED_FIRST_STAGE,\r
41                         FFW_SPEED_SECOND_STAGE,\r
42                         DO_NOT_DELETE_CUT_DEMOS,\r
43                         JOB_NAME_APPEND_DUPLICATE\r
44                 };\r
45         }\r
46         \r
47         public static final String PREFERENCES_DIRNAME = "settings";\r
48         public static final String LOGS_DIRNAME = "logs";\r
49         public static final String PLUGINS_DIRNAME = "plugins";\r
50         public static final String APP_PREFERENCES_FILENAME = "app_preferences.xml";\r
51         public static final String JOBQUEUE_FILENAME = "jobs.dat";\r
52         \r
53         public static final int STATE_WORKING = 0;\r
54         public static final int STATE_IDLE = 1;\r
55         \r
56         private RecorderJobPoolExecutor poolExecutor;\r
57         private List<RecordJob> jobs;\r
58         private NDRPreferences preferences = null;\r
59         private List<DemoRecorderUI> registeredUserInterfaces;\r
60         private List<EncoderPlugin> encoderPlugins;\r
61         private int state = STATE_IDLE;\r
62         \r
63         public DemoRecorderApplication() {\r
64                 poolExecutor = new RecorderJobPoolExecutor();\r
65                 jobs = new CopyOnWriteArrayList<RecordJob>();\r
66                 this.registeredUserInterfaces = new ArrayList<DemoRecorderUI>();\r
67                 this.encoderPlugins = new ArrayList<EncoderPlugin>();\r
68                 this.getPreferences();\r
69                 this.loadPlugins();\r
70                 this.configurePlugins();\r
71                 this.loadJobQueue();\r
72         }\r
73         \r
74         public void setPreference(String category, String preference, boolean value) {\r
75                 this.preferences.setProperty(category, preference, String.valueOf(value));\r
76         }\r
77         \r
78         public void setPreference(String category, String preference, int value) {\r
79                 this.preferences.setProperty(category, preference, String.valueOf(value));\r
80         }\r
81         \r
82         public void setPreference(String category, String preference, String value) {\r
83                 this.preferences.setProperty(category, preference, value);\r
84         }\r
85         \r
86         public NDRPreferences getPreferences() {\r
87                 if (this.preferences == null) {\r
88                         this.preferences = new NDRPreferences();\r
89                         this.createPreferenceDefaultValues();\r
90                         File preferencesFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, APP_PREFERENCES_FILENAME);\r
91                         if (preferencesFile.exists()) {\r
92                                 FileInputStream fis = null;\r
93                                 try {\r
94                                         fis = new FileInputStream(preferencesFile);\r
95                                         this.preferences.loadFromXML(fis);\r
96                                 } catch (Exception e) {\r
97                                         DemoRecorderUtils.showNonCriticalErrorDialog("Could not load the application preferences file!", e, true);\r
98                                 }\r
99                         }\r
100                 }\r
101                 \r
102                 return this.preferences;\r
103         }\r
104         \r
105         private void createPreferenceDefaultValues() {\r
106                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.OVERWRITE_VIDEO_FILE, "false");\r
107                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_RENDERING, "true");\r
108                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_SOUND, "true");\r
109                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_FIRST_STAGE, "100");\r
110                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_SECOND_STAGE, "10");\r
111                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DO_NOT_DELETE_CUT_DEMOS, "false");\r
112                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.JOB_NAME_APPEND_DUPLICATE, " duplicate");\r
113         }\r
114         \r
115         public void savePreferences() {\r
116                 File preferencesFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, APP_PREFERENCES_FILENAME);\r
117                 if (!preferencesFile.exists()) {\r
118                         try {\r
119                                 preferencesFile.createNewFile();\r
120                         } catch (IOException e) {\r
121                                 File parentDir = preferencesFile.getParentFile();\r
122                                 if (!parentDir.exists()) {\r
123                                         try {\r
124                                                 if (parentDir.mkdirs() == true) {\r
125                                                         try {\r
126                                                                 preferencesFile.createNewFile();\r
127                                                         } catch (Exception ex) {}\r
128                                                 }\r
129                                         } catch (Exception ex) {}\r
130                                 }\r
131                         }\r
132                 }\r
133                 \r
134                 if (!preferencesFile.exists()) {\r
135                         DemoRecorderException ex = new DemoRecorderException("Could not create the preferences file " + preferencesFile.getAbsolutePath());\r
136                         DemoRecorderUtils.showNonCriticalErrorDialog(ex);\r
137                         return;\r
138                 }\r
139                 \r
140                 FileOutputStream fos;\r
141                 try {\r
142                         fos = new FileOutputStream(preferencesFile);\r
143                 } catch (FileNotFoundException e) {\r
144                         DemoRecorderUtils.showNonCriticalErrorDialog("Could not create the preferences file " + preferencesFile.getAbsolutePath() + ". Unsufficient rights?", e, true);\r
145                         return;\r
146                 }\r
147                 try {\r
148                         this.preferences.storeToXML(fos, null);\r
149                 } catch (IOException e) {\r
150                         DemoRecorderUtils.showNonCriticalErrorDialog("Could not create the preferences file " + preferencesFile.getAbsolutePath(), e, true);\r
151                 }\r
152         }\r
153         \r
154         public List<RecordJob> getRecordJobs() {\r
155                 return new ArrayList<RecordJob>(this.jobs);\r
156         }\r
157         \r
158         public void startRecording() {\r
159                 if (this.state != STATE_WORKING) {\r
160                         this.state = STATE_WORKING;\r
161                         \r
162                         for (RecordJob currentJob : this.jobs) {\r
163                                 if (currentJob.getState() == RecordJob.State.WAITING) {\r
164                                         this.poolExecutor.runJob(currentJob);\r
165                                 }\r
166                         }\r
167                         \r
168                         //notify ourself when job is done\r
169                         this.poolExecutor.runJob(new RecordsDoneJob(this));\r
170                 }\r
171         }\r
172         \r
173         public void recordSelectedJobs(List<RecordJob> jobList) {\r
174                 if (this.state == STATE_IDLE) {\r
175                         this.state = STATE_WORKING;\r
176                         for (RecordJob currentJob : jobList) {\r
177                                 if (currentJob.getState() == RecordJob.State.WAITING) {\r
178                                         this.poolExecutor.runJob(currentJob);\r
179                                 }\r
180                         }\r
181                         \r
182                         //notify ourself when job is done\r
183                         this.poolExecutor.runJob(new RecordsDoneJob(this));\r
184                 }\r
185         }\r
186         \r
187         public void executePluginForSelectedJobs(EncoderPlugin plugin, List<RecordJob> jobList) {\r
188                 if (this.state == STATE_IDLE) {\r
189                         this.state = STATE_WORKING;\r
190                         for (RecordJob currentJob : jobList) {\r
191                                 if (currentJob.getState() == RecordJob.State.DONE) {\r
192                                         this.poolExecutor.runJob(new EncoderJob(currentJob, plugin));\r
193                                 }\r
194                         }\r
195                         \r
196                         //notify ourself when job is done\r
197                         this.poolExecutor.runJob(new RecordsDoneJob(this));\r
198                 }\r
199         }\r
200         \r
201         public void notifyAllJobsDone() {\r
202                 this.state = STATE_IDLE;\r
203                 \r
204                 //notify all UIs\r
205                 for (DemoRecorderUI currentUI : this.registeredUserInterfaces) {\r
206                         currentUI.recordingFinished();\r
207                 }\r
208         }\r
209         \r
210         public synchronized void stopRecording() {\r
211                 if (this.state == STATE_WORKING) {\r
212                         //clear the queue of the threadpoolexecutor and add the GUI/applayer notify job again\r
213                         this.poolExecutor.clearUnfinishedJobs();\r
214                         this.poolExecutor.runJob(new RecordsDoneJob(this));\r
215                 }\r
216         }\r
217         \r
218         public RecordJob createRecordJob(\r
219                 String name,\r
220                 File enginePath,\r
221                 String engineParameters,\r
222                 File demoFile,\r
223                 String relativeDemoPath,\r
224                 File dpVideoPath,\r
225                 File videoDestination,\r
226                 String executeBeforeCap,\r
227                 String executeAfterCap,\r
228                 float startSecond,\r
229                 float endSecond\r
230         ) {\r
231                 int jobIndex = -1;\r
232                 if (name == null || name.equals("")) {\r
233                         //we don't have a name, so use a generic one \r
234                         jobIndex = this.getNewJobIndex();\r
235                         name = "Job " + jobIndex;\r
236                 } else {\r
237                         //just use the name and keep jobIndex at -1. Jobs with real names don't need an index\r
238                 }\r
239                 \r
240                 \r
241                 \r
242                 RecordJob newJob = new RecordJob(\r
243                         this,\r
244                         name,\r
245                         jobIndex,\r
246                         enginePath,\r
247                         engineParameters,\r
248                         demoFile,\r
249                         relativeDemoPath,\r
250                         dpVideoPath,\r
251                         videoDestination,\r
252                         executeBeforeCap,\r
253                         executeAfterCap,\r
254                         startSecond,\r
255                         endSecond\r
256                 );\r
257                 this.jobs.add(newJob);\r
258                 this.fireUserInterfaceUpdate(newJob);\r
259                 \r
260                 return newJob;\r
261         }\r
262         \r
263         public synchronized boolean deleteRecordJob(RecordJob job) {\r
264                 if (!this.jobs.contains(job)) {\r
265                         return false;\r
266                 }\r
267                 \r
268                 //don't delete jobs that are scheduled for execution\r
269                 if (this.poolExecutor.getJobList().contains(job)) {\r
270                         return false;\r
271                 }\r
272                 \r
273                 this.jobs.remove(job);\r
274                 return true;\r
275         }\r
276         \r
277         public void addUserInterfaceListener(DemoRecorderUI ui) {\r
278                 this.registeredUserInterfaces.add(ui);\r
279         }\r
280         \r
281         /**\r
282          * Makes sure that all registered user interfaces can update their view/display.\r
283          * @param job either a job that's new to the UI, or one the UI already knows but of which details changed\r
284          */\r
285         public void fireUserInterfaceUpdate(RecordJob job) {\r
286                 for (DemoRecorderUI ui : this.registeredUserInterfaces) {\r
287                         ui.RecordJobPropertiesChange(job);\r
288                 }\r
289         }\r
290         \r
291         public int getNewJobIndex() {\r
292                 int jobIndex;\r
293                 if (this.jobs.size() == 0) {\r
294                         jobIndex = 1;\r
295                 } else {\r
296                         int greatestIndex = -1;\r
297                         for (RecordJob j : this.jobs) {\r
298                                 if (j.getJobIndex() > greatestIndex) {\r
299                                         greatestIndex = j.getJobIndex();\r
300                                 }\r
301                         }\r
302                         if (greatestIndex == -1) {\r
303                                 jobIndex = 1;\r
304                         } else {\r
305                                 jobIndex = greatestIndex + 1;\r
306                         }\r
307                 }\r
308                 \r
309                 return jobIndex;\r
310         }\r
311         \r
312         private void loadJobQueue() {\r
313                 File defaultFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, JOBQUEUE_FILENAME);\r
314                 this.loadJobQueue(defaultFile, true);\r
315         }\r
316         \r
317         /**\r
318          * Loads the jobs from the given file path. If override is enabled, the previous\r
319          * job list will be overwritten with the newly loaded list. Otherwise the loaded jobs\r
320          * are added to the already existing list.\r
321          * @param path\r
322          * @param override\r
323          * @return the number of jobs loaded from the file\r
324          */\r
325         @SuppressWarnings("unchecked")\r
326         public int loadJobQueue(File path, boolean override) {\r
327                 if (!path.exists()) {\r
328                         return 0;\r
329                 }\r
330                 \r
331                 try {\r
332                         FileInputStream fin = new FileInputStream(path);\r
333                         ObjectInputStream ois = new ObjectInputStream(fin);\r
334                         List<RecordJob> newList = (List<RecordJob>) ois.readObject();\r
335                         for (RecordJob currentJob : newList) {\r
336                                 currentJob.setAppLayer(this);\r
337                         }\r
338                         if (override) {\r
339                                 this.jobs = newList;\r
340                         } else {\r
341                                 this.jobs.addAll(newList);\r
342                         }\r
343                         return newList.size();\r
344                 } catch (Exception e) {\r
345                         DemoRecorderUtils.showNonCriticalErrorDialog("Could not load the job queue file " + path.getAbsolutePath(), e, true);\r
346                         return 0;\r
347                 }\r
348         }\r
349         \r
350         public void saveJobQueue() {\r
351                 File defaultFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, JOBQUEUE_FILENAME);\r
352                 this.saveJobQueue(defaultFile);\r
353         }\r
354         \r
355         public void saveJobQueue(File path) {\r
356                 if (!path.exists()) {\r
357                         try {\r
358                                 path.createNewFile();\r
359                         } catch (IOException e) {\r
360                                 File parentDir = path.getParentFile();\r
361                                 if (!parentDir.exists()) {\r
362                                         try {\r
363                                                 if (parentDir.mkdirs() == true) {\r
364                                                         try {\r
365                                                                 path.createNewFile();\r
366                                                         } catch (Exception ex) {}\r
367                                                 }\r
368                                         } catch (Exception ex) {}\r
369                                 }\r
370                         }\r
371                 }\r
372                 \r
373                 String exceptionMessage = "Could not save the job queue file " + path.getAbsolutePath();\r
374                 \r
375                 if (!path.exists()) {\r
376                         DemoRecorderException ex = new DemoRecorderException(exceptionMessage);\r
377                         DemoRecorderUtils.showNonCriticalErrorDialog(ex);\r
378                         return;\r
379                 }\r
380                 \r
381                 //make sure that for the next start of the program the state is set to waiting again\r
382                 for (RecordJob job : this.jobs) {\r
383                         if (job.getState() == RecordJob.State.PROCESSING) {\r
384                                 job.setState(RecordJob.State.WAITING);\r
385                         }\r
386                         job.setAppLayer(null); //we don't want to serialize the app layer!\r
387                 }\r
388                 \r
389                 try {\r
390                         FileOutputStream fout = new FileOutputStream(path);\r
391                         ObjectOutputStream oos = new ObjectOutputStream(fout);\r
392                         oos.writeObject(this.jobs);\r
393                         oos.close();\r
394                 } catch (Exception e) {\r
395                         DemoRecorderUtils.showNonCriticalErrorDialog(exceptionMessage, e, true);\r
396                 }\r
397                 \r
398                 //we sometimes also save the jobqueue and don't exit the program, so restore the applayer again\r
399                 for (RecordJob job : this.jobs) {\r
400                         job.setAppLayer(this);\r
401                 }\r
402         }\r
403         \r
404         public void shutDown() {\r
405                 this.poolExecutor.shutDown();\r
406                 this.savePreferences();\r
407                 this.saveJobQueue();\r
408         }\r
409         \r
410         public int getState() {\r
411                 return this.state;\r
412         }\r
413         \r
414         private void loadPlugins() {\r
415                 File pluginDir = DemoRecorderUtils.computeLocalFile(PLUGINS_DIRNAME, "");\r
416 \r
417                 if (!pluginDir.exists()) {\r
418                         pluginDir.mkdir();\r
419                 }\r
420 \r
421                 File[] jarFiles = pluginDir.listFiles();\r
422 \r
423                 List<URL> urlList = new ArrayList<URL>();\r
424                 for (File f : jarFiles) {\r
425                         try {\r
426                                 urlList.add(f.toURI().toURL());\r
427                         } catch (MalformedURLException ex) {}\r
428                 }\r
429                 ClassLoader parentLoader = Thread.currentThread().getContextClassLoader();\r
430                 URL[] urls = new URL[urlList.size()];\r
431                 urls = urlList.toArray(urls);\r
432                 URLClassLoader classLoader = new URLClassLoader(urls, parentLoader);\r
433                 \r
434                 ServiceLoader<EncoderPlugin> loader = ServiceLoader.load(EncoderPlugin.class, classLoader);\r
435                 for (EncoderPlugin implementation : loader) {\r
436                         this.encoderPlugins.add(implementation);\r
437                 }\r
438         }\r
439         \r
440         private void configurePlugins() {\r
441                 for (EncoderPlugin plugin : this.encoderPlugins) {\r
442                         plugin.setApplicationLayer(this);\r
443                         Properties pluginPreferences = plugin.getGlobalPreferences();\r
444                         for (Object preference : pluginPreferences.keySet()) {\r
445                                 String preferenceString = (String) preference;\r
446                                 \r
447                                 if (this.preferences.getProperty(plugin.getName(), preferenceString) == null) {\r
448                                         String defaultValue = pluginPreferences.getProperty(preferenceString);\r
449                                         this.preferences.setProperty(plugin.getName(), preferenceString, defaultValue);\r
450                                 }\r
451                         }\r
452                 }\r
453         }\r
454 \r
455         public List<EncoderPlugin> getEncoderPlugins() {\r
456                 return encoderPlugins;\r
457         }\r
458 }\r