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 }