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