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