1 package com.hammurapi.jcapture;
2 
3 import java.awt.Dimension;
4 import java.io.Closeable;
5 import java.io.File;
6 import java.io.IOException;
7 import java.io.RandomAccessFile;
8 import java.nio.channels.FileChannel;
9 import java.util.ArrayList;
10 import java.util.Collections;
11 import java.util.IdentityHashMap;
12 import java.util.LinkedList;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.concurrent.Future;
16 import java.util.zip.DataFormatException;
17 
18 import javax.sound.sampled.AudioFileFormat;
19 import javax.sound.sampled.AudioInputStream;
20 import javax.sound.sampled.AudioSystem;
21 import javax.sound.sampled.DataLine;
22 import javax.sound.sampled.Mixer;
23 import javax.sound.sampled.TargetDataLine;
24 import javax.swing.ProgressMonitor;
25 
26 import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape;
27 import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.Image;
28 import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.ShapeContent;
29 
30 /**
31  * Records screen into SWF movie.
32  * @author Pavel Vlasov
33  *
34  */
35 public class ScreenRecorder {
36 
37 	private CaptureConfig config;
38 	private Closeable imagesFileCloseable;
39 
40 	class Fragment {
41 
42 		private ScreenShot first;
43 
getActualFps()44 		float getActualFps() {
45 			return first.getFramesPerSecond();
46 		}
47 
48 		private class AudioRecordingThread extends SafeThread {
49 
AudioRecordingThread()50 			public AudioRecordingThread() {
51 				super("Audio recording thread");
52 			}
53 
54 			@Override
runInternal()55 			protected void runInternal() throws Exception {
56 				AudioSystem.write(new AudioInputStream(targetDataLine), AudioFileFormat.Type.WAVE, audioSink);
57 			}
58 
59 		}
60 
61 		private class ScreenCapturingThread  extends SafeThread {
62 
ScreenCapturingThread()63 			public ScreenCapturingThread() {
64 				super("Screen capturing thread");
65 			}
66 
67 			@Override
runInternal()68 			protected void runInternal() throws Exception {
69 				long start = System.currentTimeMillis();
70 				ScreenShot screenShot = null;
71 				for (int shot=0; !isDone; ++shot) {
72 
73 		       		long toSleep = (shot+1)*frameLength - (System.currentTimeMillis()-start);
74 		       		if (toSleep>0) {
75 		       			Thread.sleep(toSleep);
76 		       		}
77 
78 		       		screenShot = config.createScreenShot(screenShot, imagesChannel);
79 		       		if (first==null) {
80 		       			first = screenShot;
81 		       		}
82 		       		screenshots.add(config.submit(screenShot));
83 		       	}
84 
85 				System.out.println("Captured "+screenshots.size()+" screenshots");
86 			}
87 
88 		}
89 
Fragment()90 		public Fragment() throws Exception {
91 			if (targetDataLine!=null) {
92 	        	audioSink = File.createTempFile("jCaptureAudioSink", ".wav");
93 				targetDataLine.start();
94 				audioRecordingThread = new AudioRecordingThread();
95 				audioRecordingThread.start();
96 			}
97 
98 			screenCapturingThread = new ScreenCapturingThread();
99 			screenCapturingThread.start();
100 		}
101 
102 		File audioSink;
103 		List<Future<ScreenShot>> screenshots = new ArrayList<Future<ScreenShot>>();
104 
105 		AudioRecordingThread audioRecordingThread;
106 		ScreenCapturingThread screenCapturingThread;
107 
108 		volatile boolean isDone;
109 
stop()110 		void stop() throws Exception {
111 			if (targetDataLine!=null) {
112 				targetDataLine.stop();
113 			}
114 			isDone = true;
115 			if (audioRecordingThread!=null) {
116 				audioRecordingThread.join();
117 			}
118 			screenCapturingThread.join();
119 			if (screenCapturingThread.getException()!=null) {
120 				throw screenCapturingThread.getException();
121 			}
122 			if (audioRecordingThread!=null && audioRecordingThread.getException()!=null) {
123 				throw audioRecordingThread.getException();
124 			}
125 		}
126 
127 	}
128 
129 	LinkedList<Fragment> fragments = new LinkedList<Fragment>();
130 	private FileChannel imagesChannel;
131 
ScreenRecorder(CaptureConfig config, AbstractCaptureApplet applet)132 	public ScreenRecorder(CaptureConfig config, AbstractCaptureApplet applet) throws Exception {
133 		this.config = config;
134 		final File imagesFile = File.createTempFile("jCaptureImages", ".tmp");
135 		imagesFile.deleteOnExit();
136 		final RandomAccessFile raf = new RandomAccessFile(imagesFile, "rw");
137 		this.imagesChannel = raf.getChannel();
138 
139 		imagesFileCloseable = new Closeable() {
140 
141 			@Override
142 			public void close() throws IOException {
143 				imagesChannel.close();
144 				raf.close();
145 				if (!imagesFile.delete()) {
146 					imagesFile.deleteOnExit();
147 				}
148 			}
149 
150 		};
151 
152 		applet.addCloseable(imagesFileCloseable);
153 
154         if (config.isSound()) {
155 			DataLine.Info info = new DataLine.Info(TargetDataLine.class, config.getAudioFormat());
156 
157 			Mixer mixer = null;
158 			Mixer firstMixer = null;
159 			for (Mixer.Info mi: AudioSystem.getMixerInfo()) {
160 				Mixer mx = AudioSystem.getMixer(mi);
161 				if (mx.isLineSupported(info)) {
162 					if (firstMixer==null) {
163 						firstMixer = mx;
164 					}
165 					if (config.getMixerName()==null || mi.getName().equals(config.getMixerName())) {
166 						mixer = mx;
167 						break;
168 					}
169 				}
170 			}
171 
172 			if (mixer==null) {
173 				mixer = firstMixer;
174 			}
175 
176 			if (mixer!=null) {
177 				targetDataLine = (TargetDataLine) mixer.getLine(info);
178 				targetDataLine.open(config.getAudioFormat());
179 			}
180         }
181 
182 		frameLength = (long) (1000.0/config.getFramesPerSecond());
183 
184 		start();
185 	}
186 
start()187 	public synchronized void start() throws Exception {
188         fragments.add(new Fragment());
189 	}
190 
stop()191 	public void stop() throws Exception {
192 		fragments.getLast().stop();
193 	}
194 
195 	/**
196 	 * Recording is discarded if saveTo is null
197 	 * @param saveTo
198 	 * @return Movie size in pixels or null if saving was cancelled.
199 	 * @throws IOException
200 	 * @throws DataFormatException
201 	 */
getMovie()202 	public Movie getMovie() throws Exception {
203 		stop();
204 
205 		if (targetDataLine!=null) {
206 			targetDataLine.close();
207 		}
208 
209 		int totalWork = 3;
210 		for (Fragment f: fragments) {
211 			totalWork+=f.screenshots.size()+1;
212 		}
213 
214 		Map<Region, Image> imageCache = new IdentityHashMap<Region, VideoEncoder.Fragment.Frame.Shape.Image>();
215 
216 		Dimension frameDimension = null;
217 
218 		ProgressMonitor progressMonitor = new ProgressMonitor(config.getParentComponent(), "Encoding video", "Preparing frames", 0, totalWork+4);
219 		try {
220 			int progressCounter = 0;
221 
222 	        //In frames
223 	        int inactivityInterval = config.isRemoveInactivity() && !config.isSound() ? (int) (1000.0 * config.getInactivityInterval() / frameLength) : -1;
224 	        float fps = -1;
225 	        final List<VideoEncoder.Fragment> fragmentCollector = new ArrayList<VideoEncoder.Fragment>();
226 	        for (Fragment fragment: fragments) {
227 	 			if (progressMonitor.isCanceled()) {
228 	 				return null;
229 	 			}
230 
231 		        if (fps<0) {
232 		        	fps = config.isSound() ? fragment.getActualFps() : config.getSpeedScale()*fragment.getActualFps();
233 		        }
234 
235 		        progressMonitor.setProgress(++progressCounter);
236 
237 		        int lastActivity = -1;
238 		        List<VideoEncoder.Fragment.Frame> framesCollector = new ArrayList<VideoEncoder.Fragment.Frame>();
239 		 		for (Future<ScreenShot> sf: fragment.screenshots) {
240 
241 		 			if (progressMonitor.isCanceled()) {
242 		 				return null;
243 		 			}
244 
245 		 			ScreenShot screenShot = sf.get();
246 
247 			        if (inactivityInterval<0 || screenShot.isActive() || screenShot.getSecNo()-lastActivity<inactivityInterval) {
248 				        List<Shape> frameShapes = new ArrayList<VideoEncoder.Fragment.Frame.Shape>();
249 				        for (Region region: screenShot.getRegions()) {
250 				        	ShapeContent content;
251 				        	if (region.getMasterImageRegion()==null) {
252 				        		content = new ShapeImpl.ImageImpl(region.getImage(), region.coversEverything());
253 				        		imageCache.put(region, (Image) content);
254 				        		if (frameDimension==null && region.coversEverything()) {
255 				        			frameDimension = region.getSize();
256 				        		}
257 				        	} else {
258 				        		content = new ShapeImpl.ImageReferenceImpl(imageCache.get(region.getMasterImageRegion()));
259 				        	}
260 							frameShapes.add(new ShapeImpl(region.getImageLocation(), content));
261 				        }
262 						framesCollector.add(new FrameImpl(frameShapes, screenShot.getMousePosition(), screenShot.getSize(), screenShot.isActive()));
263 			        } else {
264 			 			progressMonitor.setProgress(++progressCounter);	// Skipping frame, report progress here.
265 			        }
266 
267 			        if (screenShot.isActive()) {
268 			        	lastActivity = screenShot.getSecNo();
269 			        }
270 
271 		 			progressMonitor.setProgress(++progressCounter);
272 		        }
273 
274 	 			fragmentCollector.add(new FragmentImpl(Collections.unmodifiableList(framesCollector), fragment.audioSink));
275 	        }
276 
277 			return new Movie(frameDimension, fps, fragmentCollector, imagesFileCloseable);
278 		} finally {
279 			progressMonitor.close();
280 		}
281 	}
282 
283 	private static abstract class SafeThread extends Thread {
284 		private Exception exception;
285 
SafeThread(String name)286 		public SafeThread(String name) {
287 			super(name);
288 		}
289 
290 		@Override
run()291 		public void run() {
292 			try {
293 				runInternal();
294 			} catch (Exception e) {
295 				this.exception = e;
296 				e.printStackTrace();
297 			}
298 		}
299 
runInternal()300 		protected abstract void runInternal() throws Exception;
301 
getException()302 		public Exception getException() {
303 			return exception;
304 		}
305 	}
306 
307 	long frameLength;
308 
309 
310 	private TargetDataLine targetDataLine;
311 
312 }