1 package com.nexuiz.demorecorder.application;
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;
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;
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;
25 public class DemoRecorderApplication {
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";
36 public static final String[] PREFERENCES_ORDER = {
40 FFW_SPEED_FIRST_STAGE,
41 FFW_SPEED_SECOND_STAGE,
42 DO_NOT_DELETE_CUT_DEMOS,
43 JOB_NAME_APPEND_DUPLICATE
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";
53 public static final int STATE_WORKING = 0;
54 public static final int STATE_IDLE = 1;
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;
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();
70 this.configurePlugins();
74 public void setPreference(String category, String preference, boolean value) {
75 this.preferences.setProperty(category, preference, String.valueOf(value));
78 public void setPreference(String category, String preference, int value) {
79 this.preferences.setProperty(category, preference, String.valueOf(value));
82 public void setPreference(String category, String preference, String value) {
83 this.preferences.setProperty(category, preference, value);
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;
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);
102 return this.preferences;
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");
115 public void savePreferences() {
116 File preferencesFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, APP_PREFERENCES_FILENAME);
117 if (!preferencesFile.exists()) {
119 preferencesFile.createNewFile();
120 } catch (IOException e) {
121 File parentDir = preferencesFile.getParentFile();
122 if (!parentDir.exists()) {
124 if (parentDir.mkdirs() == true) {
126 preferencesFile.createNewFile();
127 } catch (Exception ex) {}
129 } catch (Exception ex) {}
134 if (!preferencesFile.exists()) {
135 DemoRecorderException ex = new DemoRecorderException("Could not create the preferences file " + preferencesFile.getAbsolutePath());
136 DemoRecorderUtils.showNonCriticalErrorDialog(ex);
140 FileOutputStream fos;
142 fos = new FileOutputStream(preferencesFile);
143 } catch (FileNotFoundException e) {
144 DemoRecorderUtils.showNonCriticalErrorDialog("Could not create the preferences file " + preferencesFile.getAbsolutePath() + ". Unsufficient rights?", e, true);
148 this.preferences.storeToXML(fos, null);
149 } catch (IOException e) {
150 DemoRecorderUtils.showNonCriticalErrorDialog("Could not create the preferences file " + preferencesFile.getAbsolutePath(), e, true);
154 public List<RecordJob> getRecordJobs() {
155 return new ArrayList<RecordJob>(this.jobs);
158 public void startRecording() {
159 if (this.state != STATE_WORKING) {
160 this.state = STATE_WORKING;
162 for (RecordJob currentJob : this.jobs) {
163 if (currentJob.getState() == RecordJob.State.WAITING) {
164 this.poolExecutor.runJob(currentJob);
168 //notify ourself when job is done
169 this.poolExecutor.runJob(new RecordsDoneJob(this));
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);
182 //notify ourself when job is done
183 this.poolExecutor.runJob(new RecordsDoneJob(this));
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));
196 //notify ourself when job is done
197 this.poolExecutor.runJob(new RecordsDoneJob(this));
201 public void notifyAllJobsDone() {
202 this.state = STATE_IDLE;
205 for (DemoRecorderUI currentUI : this.registeredUserInterfaces) {
206 currentUI.recordingFinished();
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));
218 public RecordJob createRecordJob(
221 String engineParameters,
223 String relativeDemoPath,
225 File videoDestination,
226 String executeBeforeCap,
227 String executeAfterCap,
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;
237 //just use the name and keep jobIndex at -1. Jobs with real names don't need an index
242 RecordJob newJob = new RecordJob(
257 this.jobs.add(newJob);
258 this.fireUserInterfaceUpdate(newJob);
263 public synchronized boolean deleteRecordJob(RecordJob job) {
264 if (!this.jobs.contains(job)) {
268 //don't delete jobs that are scheduled for execution
269 if (this.poolExecutor.getJobList().contains(job)) {
273 this.jobs.remove(job);
277 public void addUserInterfaceListener(DemoRecorderUI ui) {
278 this.registeredUserInterfaces.add(ui);
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
285 public void fireUserInterfaceUpdate(RecordJob job) {
286 for (DemoRecorderUI ui : this.registeredUserInterfaces) {
287 ui.RecordJobPropertiesChange(job);
291 public int getNewJobIndex() {
293 if (this.jobs.size() == 0) {
296 int greatestIndex = -1;
297 for (RecordJob j : this.jobs) {
298 if (j.getJobIndex() > greatestIndex) {
299 greatestIndex = j.getJobIndex();
302 if (greatestIndex == -1) {
305 jobIndex = greatestIndex + 1;
312 private void loadJobQueue() {
313 File defaultFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, JOBQUEUE_FILENAME);
314 this.loadJobQueue(defaultFile, true);
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.
323 * @return the number of jobs loaded from the file
325 @SuppressWarnings("unchecked")
326 public int loadJobQueue(File path, boolean override) {
327 if (!path.exists()) {
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);
341 this.jobs.addAll(newList);
343 return newList.size();
344 } catch (Exception e) {
345 DemoRecorderUtils.showNonCriticalErrorDialog("Could not load the job queue file " + path.getAbsolutePath(), e, true);
350 public void saveJobQueue() {
351 File defaultFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, JOBQUEUE_FILENAME);
352 this.saveJobQueue(defaultFile);
355 public void saveJobQueue(File path) {
356 if (!path.exists()) {
358 path.createNewFile();
359 } catch (IOException e) {
360 File parentDir = path.getParentFile();
361 if (!parentDir.exists()) {
363 if (parentDir.mkdirs() == true) {
365 path.createNewFile();
366 } catch (Exception ex) {}
368 } catch (Exception ex) {}
373 String exceptionMessage = "Could not save the job queue file " + path.getAbsolutePath();
375 if (!path.exists()) {
376 DemoRecorderException ex = new DemoRecorderException(exceptionMessage);
377 DemoRecorderUtils.showNonCriticalErrorDialog(ex);
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);
386 job.setAppLayer(null); //we don't want to serialize the app layer!
390 FileOutputStream fout = new FileOutputStream(path);
391 ObjectOutputStream oos = new ObjectOutputStream(fout);
392 oos.writeObject(this.jobs);
394 } catch (Exception e) {
395 DemoRecorderUtils.showNonCriticalErrorDialog(exceptionMessage, e, true);
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);
404 public void shutDown() {
405 this.poolExecutor.shutDown();
406 this.savePreferences();
410 public int getState() {
414 private void loadPlugins() {
415 File pluginDir = DemoRecorderUtils.computeLocalFile(PLUGINS_DIRNAME, "");
417 if (!pluginDir.exists()) {
421 File[] jarFiles = pluginDir.listFiles();
423 List<URL> urlList = new ArrayList<URL>();
424 for (File f : jarFiles) {
426 urlList.add(f.toURI().toURL());
427 } catch (MalformedURLException ex) {}
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);
434 ServiceLoader<EncoderPlugin> loader = ServiceLoader.load(EncoderPlugin.class, classLoader);
435 for (EncoderPlugin implementation : loader) {
436 this.encoderPlugins.add(implementation);
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;
447 if (this.preferences.getProperty(plugin.getName(), preferenceString) == null) {
448 String defaultValue = pluginPreferences.getProperty(preferenceString);
449 this.preferences.setProperty(plugin.getName(), preferenceString, defaultValue);
455 public List<EncoderPlugin> getEncoderPlugins() {
456 return encoderPlugins;