1 package com.hammurapi.jcapture; 2 3 import java.awt.AlphaComposite; 4 import java.awt.BorderLayout; 5 import java.awt.Color; 6 import java.awt.Component; 7 import java.awt.Dimension; 8 import java.awt.Graphics; 9 import java.awt.Graphics2D; 10 import java.awt.GridBagConstraints; 11 import java.awt.GridBagLayout; 12 import java.awt.Image; 13 import java.awt.Insets; 14 import java.awt.Point; 15 import java.awt.Rectangle; 16 import java.awt.RenderingHints; 17 import java.awt.Toolkit; 18 import java.awt.event.ActionEvent; 19 import java.awt.event.ActionListener; 20 import java.awt.event.MouseAdapter; 21 import java.awt.event.MouseEvent; 22 import java.awt.event.WindowAdapter; 23 import java.awt.event.WindowEvent; 24 import java.awt.image.BufferedImage; 25 import java.io.File; 26 import java.io.IOException; 27 import java.lang.ref.Reference; 28 import java.lang.ref.SoftReference; 29 import java.util.ArrayList; 30 import java.util.Collections; 31 import java.util.List; 32 import java.util.concurrent.Executor; 33 34 import javax.sound.sampled.AudioFormat; 35 import javax.sound.sampled.AudioInputStream; 36 import javax.sound.sampled.AudioSystem; 37 import javax.sound.sampled.DataLine; 38 import javax.sound.sampled.SourceDataLine; 39 import javax.swing.AbstractAction; 40 import javax.swing.Action; 41 import javax.swing.JButton; 42 import javax.swing.JCheckBox; 43 import javax.swing.JCheckBoxMenuItem; 44 import javax.swing.JComponent; 45 import javax.swing.JFrame; 46 import javax.swing.JMenuItem; 47 import javax.swing.JOptionPane; 48 import javax.swing.JPanel; 49 import javax.swing.JPopupMenu; 50 import javax.swing.JScrollPane; 51 import javax.swing.JTable; 52 import javax.swing.JToolTip; 53 import javax.swing.ListSelectionModel; 54 import javax.swing.ProgressMonitor; 55 import javax.swing.SwingWorker; 56 import javax.swing.Timer; 57 import javax.swing.event.PopupMenuEvent; 58 import javax.swing.event.PopupMenuListener; 59 import javax.swing.table.DefaultTableModel; 60 import javax.swing.table.TableCellRenderer; 61 import javax.swing.table.TableModel; 62 63 import com.hammurapi.jcapture.ShapeImpl.ImageImpl; 64 import com.hammurapi.jcapture.VideoEncoder.Fragment; 65 import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame; 66 import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape; 67 import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.ImageReference; 68 import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.ShapeContent; 69 70 public class MovieEditorDialog extends javax.swing.JDialog { 71 72 private static final double DECIBELS_PER_PIXEL = 2.0; 73 private static final double NORMALIZED_LEVEL = 0.95; 74 private static final int AUDIO_CELL_HEIGHT = 50; 75 private static final int MEDIAN = AUDIO_CELL_HEIGHT/2; 76 int minCellDimension = 10; 77 int minToolTipImageDimension = 150; 78 79 int splashIndex = -1; 80 81 double coeff; 82 83 private static Color INACTIVE_COLOR = new Color(230, 230, 230); 84 private static Color ACTIVE_COLOR = Color.white; 85 private static Color SELECTED_COLOR = new Color(0, 0, 255, 70); 86 private static Color FOCUSED_COLOR = new Color(0, 0, 255, 100); 87 private static Color PLAYING_COLOR = new Color(255, 0, 0, 100); 88 89 private static Color SPLASH_COLOR = new Color(0, 255, 0, 127); 90 91 private static Color SOUND_COLOR = new Color(0, 0, 127); 92 private static Color DELETED_SOUND_COLOR = new Color(100, 100, 100); 93 94 private JButton saveButton; 95 private JPanel contentPanel; 96 private JScrollPane timeLineScrollPane; 97 private JCheckBox normalizeVolumeCheckBox; 98 private JPanel frameCanvas; 99 JTable timeLineTable; 100 private JButton discardButton; 101 private int focusColumn = 0; 102 private int playingColumn = -1; 103 private Image mouseImage; 104 private double maxVolume = -1; 105 FrameEntry[] frameEntries; 106 int cellWidth; 107 int cellHeight; 108 int toolTipImageWidth; 109 int toolTipImageHeight; 110 boolean hasAudio; 111 private Movie movie; 112 113 int numChannels; 114 int validBits; 115 long sampleRate; 116 Timer[] playTimera = {null}; 117 private Executor backgroundProcessor; 118 private double inactivityInterval; 119 private String imageFormat; 120 121 private class FrameEntry { 122 123 // Not null for first frames in fragments indicating that 124 // Indicating that it's time to open a new audio file. 125 File audioFile; 126 127 boolean mouseMoved; 128 129 // Scaled samples for painting - not real ones. 130 // idx, {min, max} 131 double[] audioSamples; 132 133 // Number of real samples falling to this frame. 134 int audioSamplesInFrame; 135 136 boolean isDeleted; 137 138 Reference<BufferedImage> toolTipImageRef; 139 140 Reference<BufferedImage> frameImageRef; 141 142 // row, selected, focus 143 private JPanel[][][] canvases = { 144 { {new FrameCellCanvas(false, false), new FrameCellCanvas(false, true)}, {new FrameCellCanvas(true, false), new FrameCellCanvas(true, true)} }, 145 { {new AudioCellCanvas(false, false), new AudioCellCanvas(false, true)}, {new AudioCellCanvas(true, false), new AudioCellCanvas(true, true)} } 146 }; 147 148 class CellCanvas extends JPanel { 149 150 boolean selected; 151 boolean hasFocus; 152 CellCanvas(boolean selected, boolean hasFocus)153 CellCanvas(boolean selected, boolean hasFocus) { 154 this.selected = selected; 155 this.hasFocus = hasFocus; 156 } 157 158 } 159 160 class FrameCellCanvas extends CellCanvas { 161 FrameCellCanvas(boolean selected, boolean hasFocus)162 FrameCellCanvas(boolean selected, boolean hasFocus) { 163 super(selected, hasFocus); 164 } 165 166 @Override paintComponent(Graphics g)167 public void paintComponent(Graphics g) { 168 super.paintComponent(g); 169 paintFrame(this, g, selected, hasFocus); 170 } 171 172 } 173 174 class AudioCellCanvas extends CellCanvas { 175 AudioCellCanvas(boolean selected, boolean hasFocus)176 AudioCellCanvas(boolean selected, boolean hasFocus) { 177 super(selected, hasFocus); 178 } 179 180 @Override paintComponent(Graphics g)181 public void paintComponent(Graphics g) { 182 super.paintComponent(g); 183 paintAudio(this, g, selected, hasFocus); 184 } 185 186 } 187 188 int idx; 189 int delta; 190 191 Frame frame; 192 getToolTipImage()193 BufferedImage getToolTipImage() throws IOException { 194 BufferedImage ret = toolTipImageRef==null ? null : toolTipImageRef.get(); 195 if (ret == null) { 196 BufferedImage image = getImage(); 197 ret = new BufferedImage(toolTipImageWidth, toolTipImageHeight, image.getType()); 198 Graphics2D g = ret.createGraphics(); 199 g.setComposite(AlphaComposite.Src); 200 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR); 201 g.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY); 202 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); 203 g.drawImage(image, 0, 0, ret.getWidth(), ret.getHeight(), null); 204 g.dispose(); 205 toolTipImageRef = new SoftReference<BufferedImage>(ret); 206 } 207 return ret; 208 209 } 210 paintFrame(FrameCellCanvas frameCellCanvas, Graphics g, boolean selected, boolean hasFocus)211 void paintFrame(FrameCellCanvas frameCellCanvas, Graphics g, boolean selected, boolean hasFocus) { 212 g.setColor(frame.isActive() ? ACTIVE_COLOR : INACTIVE_COLOR); 213 g.fillRect(0, 0, frameCellCanvas.getWidth(), frameCellCanvas.getHeight()); 214 215 if (idx==splashIndex) { 216 g.setColor(SPLASH_COLOR); 217 g.fillRect(1, 1, frameCellCanvas.getWidth()-2, frameCellCanvas.getHeight()-2); 218 219 } 220 221 if (frame.getMousePointer()!=null) { 222 int mx = (int) (frame.getMousePointer().getX()*(frameCellCanvas.getWidth()-3)/frame.getSize().getWidth())+1; 223 int my = (int) (frame.getMousePointer().getY()*(frameCellCanvas.getHeight()-3)/frame.getSize().getHeight())+1; 224 g.setColor(mouseMoved ? Color.BLACK : Color.GRAY); 225 g.fillRect(mx, my, 2, 2); 226 } 227 228 if (isDeleted) { 229 g.setColor(Color.RED); 230 g.drawLine(2, 2, frameCellCanvas.getWidth()-2, frameCellCanvas.getHeight()-2); 231 g.drawLine(frameCellCanvas.getWidth()-2, 2, 2, frameCellCanvas.getHeight()-2); 232 } 233 234 decorate(frameCellCanvas, g, selected, hasFocus); 235 } 236 paintAudio(AudioCellCanvas audioCellCanvas, Graphics g, boolean selected, boolean hasFocus)237 void paintAudio(AudioCellCanvas audioCellCanvas, Graphics g, boolean selected, boolean hasFocus) { 238 g.setColor(frame.isActive() ? ACTIVE_COLOR : INACTIVE_COLOR); 239 g.fillRect(0, 0, audioCellCanvas.getWidth(), audioCellCanvas.getHeight()); 240 241 if (audioSamples!=null) { 242 for (int i = 0; i<audioCellCanvas.getWidth(); ++i) { 243 g.setColor(isDeleted ? DELETED_SOUND_COLOR : SOUND_COLOR); 244 int volume = (int) (20.0*Math.log10(coeff*audioSamples[Math.min(i, audioSamples.length-1)]+1)/DECIBELS_PER_PIXEL); 245 g.drawLine(i, MEDIAN - volume, i, MEDIAN + volume); 246 } 247 } 248 249 decorate(audioCellCanvas, g, selected, hasFocus); 250 } 251 decorate(JComponent component, Graphics g, boolean selected, boolean hasFocus)252 private void decorate(JComponent component, Graphics g, boolean selected, boolean hasFocus) { 253 if (idx==playingColumn) { 254 g.setColor(PLAYING_COLOR); 255 Rectangle bounds = g.getClipBounds(); 256 g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); 257 } else if (hasFocus) { 258 g.setColor(FOCUSED_COLOR); 259 Rectangle bounds = g.getClipBounds(); 260 g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); 261 } else if (selected) { 262 g.setColor(SELECTED_COLOR); 263 Rectangle bounds = g.getClipBounds(); 264 g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); 265 } 266 } 267 getImage()268 BufferedImage getImage() throws IOException { 269 BufferedImage ret = frameImageRef==null ? null : frameImageRef.get(); 270 if (ret == null) { 271 int startIdx = idx; 272 while (startIdx>0 && !coversEverything(startIdx)) { 273 --startIdx; 274 } 275 int deltaArea = 0; 276 ret = new BufferedImage(frame.getSize().width, frame.getSize().height, shapeImage(frameEntries[startIdx].frame.getShapes().get(0)).getType()); 277 Graphics2D g = ret.createGraphics(); 278 for (int i=startIdx; i<=idx; ++i) { 279 for (Shape shape: frameEntries[i].frame.getShapes()) { 280 BufferedImage si = shapeImage(shape); 281 g.drawImage(si, shape.getLocation().x, shape.getLocation().y, null); 282 if (i==idx) { 283 deltaArea+=si.getWidth()*si.getHeight(); 284 } 285 } 286 } 287 delta = (int) (100.0*deltaArea/(frame.getSize().width * frame.getSize().height)); 288 if (frame.getMousePointer()!=null) { 289 g.drawImage(mouseImage, frame.getMousePointer().x, frame.getMousePointer().y, null); 290 } 291 frameImageRef = new SoftReference<BufferedImage>(ret); 292 } 293 return ret; 294 } 295 shapeImage(Shape shape)296 private BufferedImage shapeImage(Shape shape) throws IOException { 297 ShapeContent shapeContent = shape.getContent(); 298 if (shapeContent instanceof ImageReference) { 299 return ((ImageReference) shapeContent).getImage().getImage().getImage(); 300 } 301 return ((com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.Image) shapeContent).getImage().getImage(); 302 } 303 coversEverything(int entryIdx)304 boolean coversEverything(int entryIdx) { 305 for (Shape shape: frameEntries[entryIdx].frame.getShapes()) { 306 if (shape.getContent().coversEverything()) { 307 return true; 308 } 309 } 310 return false; 311 } 312 getCellRendererComponent(int row, boolean isSelected, boolean hasFocus)313 public Component getCellRendererComponent(int row, boolean isSelected, boolean hasFocus) { 314 return canvases[row][isSelected ? 1 : 0][hasFocus ? 1 : 0]; 315 } 316 317 } 318 MovieEditorDialog( final JFrame frame, final Movie movie, final Executor backgroundProcessor, double inactivityInterval, String imageFormat)319 public MovieEditorDialog( 320 final JFrame frame, 321 final Movie movie, 322 final Executor backgroundProcessor, 323 double inactivityInterval, 324 String imageFormat) { 325 326 super(frame, "Movie editor ("+movie+")"); 327 frame.setAlwaysOnTop(false); 328 frame.setVisible(false); 329 330 this.movie = movie; 331 this.backgroundProcessor = backgroundProcessor; 332 this.inactivityInterval = inactivityInterval; 333 this.imageFormat = imageFormat; 334 335 setModal(true); 336 setIconImage(frame.getIconImage()); 337 338 mouseImage = Toolkit.getDefaultToolkit().getImage(getClass().getResource("mouse.png")); 339 340 double aspectRatio = (double) movie.getFrameDimension().getWidth()/(double) movie.getFrameDimension().getHeight(); 341 342 if (aspectRatio>1) { 343 cellHeight = minCellDimension; 344 cellWidth = (int) Math.round(aspectRatio*cellHeight); 345 346 toolTipImageHeight = minToolTipImageDimension; 347 toolTipImageWidth = (int) Math.round(aspectRatio*toolTipImageHeight); 348 } else { 349 cellWidth = minCellDimension; 350 cellHeight = (int) Math.round((double) cellWidth/aspectRatio); 351 352 toolTipImageWidth = minToolTipImageDimension; 353 toolTipImageHeight = (int) Math.round((double) toolTipImageWidth/aspectRatio); 354 } 355 356 setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); 357 358 addWindowListener(new WindowAdapter() { 359 public void windowClosing(WindowEvent e) { 360 int confirmed = JOptionPane.showConfirmDialog(null, 361 "Are you sure you want to exit and discard the movie?", "User Confirmation", 362 JOptionPane.YES_NO_OPTION); 363 if (confirmed == JOptionPane.YES_OPTION) { 364 dispose(); 365 getOwner().setVisible(false); 366 } 367 } 368 }); 369 370 SwingWorker<Boolean, Long> loader = new SwingWorker<Boolean, Long>() { 371 372 @Override 373 protected Boolean doInBackground() throws Exception { 374 int totalFrames = 0; 375 for (Fragment fr: movie.getFragments()) { 376 if (fr.getAudio()!=null) { 377 hasAudio = true; 378 } 379 totalFrames+=fr.getFrames().size(); 380 } 381 382 ProgressMonitor progressMonitor = new ProgressMonitor(frame, "Loading frames", "Loading movie frames", 0, totalFrames); 383 384 try { 385 frameEntries = new FrameEntry[totalFrames]; 386 int idx = 0; 387 double audioSamplesPerFrame = -1; 388 Point prevMouse = null; 389 for (Fragment fr: movie.getFragments()) { 390 WavFile wavFile = fr.getAudio()==null ? null : WavFile.openWavFile(fr.getAudio()); 391 if (wavFile!=null) { 392 audioSamplesPerFrame = wavFile.getSampleRate()/movie.getFramesPerSecond(); 393 numChannels = wavFile.getNumChannels(); 394 validBits = wavFile.getValidBits(); 395 sampleRate = wavFile.getSampleRate(); 396 } 397 int audioFramesRead = 0; 398 int framePosition = 0; 399 for (Frame frm: fr.getFrames()) { 400 if (progressMonitor.isCanceled()) { 401 return false; 402 } 403 frameEntries[idx] = new FrameEntry(); 404 frameEntries[idx].frame = frm; 405 frameEntries[idx].idx = idx; 406 if (frm.getMousePointer()!=null) { 407 frameEntries[idx].mouseMoved = !frm.getMousePointer().equals(prevMouse); 408 } 409 prevMouse = frm.getMousePointer(); 410 411 if (framePosition == 0) { 412 frameEntries[idx].audioFile = fr.getAudio(); 413 } 414 415 if (wavFile!=null && wavFile.getFramesRemaining()>0) { 416 frameEntries[idx].audioSamplesInFrame = (int) ((framePosition+1)*audioSamplesPerFrame-audioFramesRead); 417 frameEntries[idx].audioSamples = new double[cellWidth]; 418 double[][] sampleBuffer = new double[wavFile.getNumChannels()][frameEntries[idx].audioSamplesInFrame]; 419 frameEntries[idx].audioSamplesInFrame=wavFile.readFrames(sampleBuffer, frameEntries[idx].audioSamplesInFrame); 420 audioFramesRead+=frameEntries[idx].audioSamplesInFrame; 421 for (int i=0; i<frameEntries[idx].audioSamplesInFrame; ++i) { 422 for (int ch=0; ch<wavFile.getNumChannels(); ++ch) { 423 maxVolume = Math.max(maxVolume, Math.abs(sampleBuffer[ch][i])); 424 int asidx = i*cellWidth/frameEntries[idx].audioSamplesInFrame; 425 frameEntries[idx].audioSamples[asidx] = Math.max(Math.abs(sampleBuffer[ch][i]), frameEntries[idx].audioSamples[asidx]); 426 } 427 } 428 } 429 430 ++idx; 431 ++framePosition; 432 progressMonitor.setProgress(idx); 433 } 434 if (wavFile!=null) { 435 wavFile.close(); 436 } 437 438 } 439 440 coeff = Math.pow(10.0, DECIBELS_PER_PIXEL*(MEDIAN-1)/20.0)/maxVolume; 441 442 return true; 443 } finally { 444 progressMonitor.close(); 445 } 446 } 447 448 protected void done() { 449 try { 450 if (get()) { 451 buildUI(); 452 setLocationRelativeTo(frame); 453 setVisible(true); 454 } else { 455 JOptionPane.showMessageDialog( 456 MovieEditorDialog.this, 457 "Loading operation was cancelled", 458 "Loading cancelled", 459 JOptionPane.ERROR_MESSAGE); 460 } 461 } catch (Exception e) { 462 e.printStackTrace(); 463 JOptionPane.showMessageDialog( 464 MovieEditorDialog.this, 465 e.toString(), 466 "Error loading frames", 467 JOptionPane.ERROR_MESSAGE); 468 } 469 470 }; 471 472 }; 473 474 loader.execute(); 475 476 } 477 buildUI()478 void buildUI() { 479 BorderLayout thisLayout = new BorderLayout(); 480 getContentPane().setLayout(thisLayout); 481 482 contentPanel = new JPanel(); 483 GridBagLayout contentPanelLayout = new GridBagLayout(); 484 getContentPane().add(contentPanel, BorderLayout.CENTER); 485 contentPanelLayout.rowWeights = new double[] { 0.1, 0.0, 0.0, 0.0, 0.0, 0.0 }; 486 contentPanelLayout.rowHeights = new int[] { movie.getFrameDimension().height, 7, cellHeight+(hasAudio? 23 + AUDIO_CELL_HEIGHT : 22), 7, 7, 7 }; 487 contentPanelLayout.columnWeights = new double[] { 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; 488 contentPanelLayout.columnWidths = new int[] { 7, 7, 7, 7, 7, 7, 7 }; 489 contentPanel.setLayout(contentPanelLayout); 490 491 saveButton = new JButton("Save"); 492 contentPanel.add(saveButton, new GridBagConstraints(3, 4, 1, 1, 0.0, 493 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, 494 new Insets(0, 0, 0, 0), 0, 0)); 495 saveButton.addActionListener(new ActionListener() { 496 public void actionPerformed(ActionEvent evt) { 497 SwingWorker<Movie, Long> encoder = new SwingWorker<Movie, Long>() { 498 499 @Override 500 protected Movie doInBackground() throws Exception { 501 ProgressMonitor progressMonitor = new ProgressMonitor(MovieEditorDialog.this, "Saving movie", "Composing movie", 0, frameEntries.length); 502 503 List<Frame> newFrames = new ArrayList<Frame>(); 504 505 if (splashIndex!=-1) { 506 newFrames.add(new FrameImpl( 507 Collections.singletonList((Shape) new ShapeImpl(new Point(0,0), new ImageImpl(new MappedImage(frameEntries[splashIndex].getImage(), imageFormat, null), true))) , 508 frameEntries[splashIndex].frame.getMousePointer(), 509 frameEntries[splashIndex].frame.getSize(), 510 false)); 511 } 512 513 File newAudio = hasAudio ? File.createTempFile("jCaptureAudioSink", ".wav") : null; 514 515 long numFrames=0; 516 for (FrameEntry fe: frameEntries) { 517 if (!fe.isDeleted) { 518 numFrames+=fe.audioSamplesInFrame; 519 } 520 } 521 WavFile newWavFile = newAudio==null ? null : WavFile.newWavFile(newAudio, numChannels, numFrames, validBits, sampleRate); 522 523 File currentAudio = null; 524 WavFile currentWav = null; 525 526 for (int i=0; i<frameEntries.length; ++i) { 527 if (frameEntries[i].audioFile!=null) { 528 if (currentWav!=null) { 529 currentWav.close(); 530 } 531 if (currentAudio!=null) { 532 if (!currentAudio.delete()) { 533 currentAudio.deleteOnExit(); 534 } 535 } 536 537 currentAudio = frameEntries[i].audioFile; 538 currentWav = WavFile.openWavFile(currentAudio); 539 } 540 541 if (currentWav!=null) { 542 if (normalizeVolumeCheckBox.isSelected()) { 543 double[][] buf = new double[numChannels][frameEntries[i].audioSamplesInFrame]; 544 int read = currentWav.readFrames(buf, frameEntries[i].audioSamplesInFrame); 545 if (read>0 && !frameEntries[i].isDeleted) { 546 // Normalization 547 for (double[] ch: buf) { 548 for (int j=0; j<ch.length; ++j) { 549 ch[j] = ch[j] * NORMALIZED_LEVEL / maxVolume; 550 } 551 } 552 newWavFile.writeFrames(buf, read); 553 } 554 } else { 555 long[][] buf = new long[numChannels][frameEntries[i].audioSamplesInFrame]; 556 int read = currentWav.readFrames(buf, frameEntries[i].audioSamplesInFrame); 557 if (read>0 && !frameEntries[i].isDeleted) { 558 newWavFile.writeFrames(buf, read); 559 } 560 } 561 } 562 563 if (frameEntries[i].isDeleted) { 564 if (i<frameEntries.length-1) { 565 ((FrameImpl) frameEntries[i+1].frame).merge(frameEntries[i].frame); 566 } 567 } else { 568 newFrames.add(frameEntries[i].frame); 569 } 570 571 progressMonitor.setProgress(i); 572 if (progressMonitor.isCanceled()) { 573 if (currentWav!=null) { 574 currentWav.close(); 575 } 576 if (currentAudio!=null) { 577 if (!currentAudio.delete()) { 578 currentAudio.deleteOnExit(); 579 } 580 } 581 if (newWavFile!=null) { 582 newWavFile.close(); 583 } 584 if (newAudio!=null) { 585 if (!newAudio.delete()) { 586 newAudio.deleteOnExit(); 587 } 588 } 589 return null; 590 } 591 } 592 593 if (currentWav!=null) { 594 currentWav.close(); 595 } 596 if (currentAudio!=null) { 597 if (!currentAudio.delete()) { 598 currentAudio.deleteOnExit(); 599 } 600 } 601 602 if (newWavFile!=null) { 603 newWavFile.close(); 604 } 605 606 return new Movie(movie.getFrameDimension(), movie.getFramesPerSecond(), Collections.singletonList((Fragment) new FragmentImpl(newFrames, newAudio)), movie); 607 } 608 609 @Override 610 protected void done() { 611 try { 612 MovieEditorDialog.this.setVisible(false); 613 ((RecordingControlsFrame) getOwner()).uploadMovie(get()); 614 MovieEditorDialog.this.dispose(); 615 } catch (Exception e) { 616 e.printStackTrace(); 617 JOptionPane.showMessageDialog( 618 MovieEditorDialog.this, e.toString(), 619 "Error saving recording", 620 JOptionPane.ERROR_MESSAGE); 621 MovieEditorDialog.this.setVisible(false); 622 MovieEditorDialog.this.getOwner().setVisible(false); 623 } 624 } 625 626 }; 627 628 encoder.execute(); 629 } 630 }); 631 632 discardButton = new JButton("Discard"); 633 contentPanel.add(discardButton, new GridBagConstraints(5, 4, 1, 1, 0.0, 634 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, 635 new Insets(0, 0, 0, 0), 0, 0)); 636 discardButton.addActionListener(new ActionListener() { 637 public void actionPerformed(ActionEvent evt) { 638 if (JOptionPane.showConfirmDialog(MovieEditorDialog.this, "Are you sure you want to discard the recording?", "Confirm discarding movie", JOptionPane.YES_NO_OPTION)==JOptionPane.YES_OPTION) { 639 MovieEditorDialog.this.setVisible(false); 640 MovieEditorDialog.this.dispose(); 641 MovieEditorDialog.this.getOwner().setVisible(false); 642 } 643 } 644 }); 645 646 timeLineScrollPane = new JScrollPane(); 647 timeLineScrollPane.setPreferredSize(new Dimension(movie.getFrameDimension().width, cellHeight+(hasAudio? 23 + AUDIO_CELL_HEIGHT : 22))); 648 contentPanel.add(timeLineScrollPane, new GridBagConstraints(0, 2, 7, 1, 649 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, 650 new Insets(0, 0, 0, 0), 0, 0)); 651 652 System.out.println("Loaded "+frameEntries.length+" frames"); 653 TableModel timeLineTableModel = new DefaultTableModel(hasAudio ? 2 : 1, frameEntries.length) { 654 655 @Override 656 public boolean isCellEditable(int row, int column) { 657 return false; 658 } 659 }; 660 timeLineTable = new JTable() { 661 662 @Override 663 public JToolTip createToolTip() { 664 Point p = getMousePosition(); 665 666 // Locate the renderer under the event location 667 int hitColumnIndex = columnAtPoint(p); 668 int hitRowIndex = rowAtPoint(p); 669 670 if ((hitColumnIndex != -1) && (hitRowIndex != -1)) { 671 try { 672 BufferedImage toolTipImage = frameEntries[hitColumnIndex].getToolTipImage(); 673 return new ImageToolTip("Frame "+(hitColumnIndex+1)+", delta "+frameEntries[hitColumnIndex].delta+"%", toolTipImage); 674 } catch (IOException e) { 675 e.printStackTrace(); 676 } 677 } 678 return super.createToolTip(); 679 } 680 681 }; 682 683 JPopupMenu popup = new JPopupMenu("Context"); 684 685 popup.addPopupMenuListener(new PopupMenuListener() { 686 687 @Override 688 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 689 if (playTimera[0]!=null) { 690 playTimera[0].stop(); 691 } 692 } 693 694 @Override 695 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 696 // TODO Auto-generated method stub 697 698 } 699 700 @Override 701 public void popupMenuCanceled(PopupMenuEvent e) { 702 // TODO Auto-generated method stub 703 704 } 705 }); 706 707 addDeleteFramesMenuItem(popup); 708 addUndeleteFramesMenuItem(popup); 709 addRemoveInactivityMenuItem(popup); 710 711 final JCheckBoxMenuItem splashMenuItem = new JCheckBoxMenuItem("Splash"); 712 713 splashMenuItem.addActionListener(new ActionListener() { 714 715 @Override 716 public void actionPerformed(ActionEvent e) { 717 splashIndex = splashMenuItem.isSelected() ? focusColumn : -1; 718 timeLineTable.repaint(); 719 } 720 }); 721 722 popup.add(splashMenuItem); 723 724 popup.addPopupMenuListener(new PopupMenuListener() { 725 726 @Override 727 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 728 splashMenuItem.setSelected(focusColumn!=-1 && focusColumn==splashIndex); 729 730 } 731 732 @Override 733 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 734 735 } 736 737 @Override 738 public void popupMenuCanceled(PopupMenuEvent e) { 739 // TODO Auto-generated method stub 740 741 } 742 }); 743 744 JMenuItem playMenuItem = new JMenuItem(); 745 Action playAction = new AbstractAction("Play") { 746 747 @Override 748 public void actionPerformed(ActionEvent e) { 749 if (playTimera[0]!=null) { 750 playTimera[0].stop(); 751 playTimera[0] = null; 752 } 753 754 final int range[] = {focusColumn, focusColumn}; 755 756 for (int i=focusColumn; i<frameEntries.length && timeLineTable.isColumnSelected(i); ++i) { 757 range[1] = i; 758 } 759 760 for (int i=focusColumn; i>=0 && timeLineTable.isColumnSelected(i); --i) { 761 range[0] = i; 762 } 763 764 if (range[0]==range[1]) { 765 range[1]=frameEntries.length-1; 766 } 767 768 playingColumn = range[0]; 769 770 if (hasAudio) { 771 try { 772 backgroundProcessor.execute(new SoundPlayer(range[0], range[1])); 773 } catch (Exception ex) { 774 ex.printStackTrace(); 775 JOptionPane.showMessageDialog( 776 MovieEditorDialog.this, 777 ex.toString(), 778 "Audio problem", 779 JOptionPane.ERROR_MESSAGE); 780 781 } 782 } 783 784 playTimera[0] = new Timer((int) ((double) 1000/movie.getFramesPerSecond()), new ActionListener() { 785 786 @Override 787 public void actionPerformed(ActionEvent e) { 788 while (frameEntries[playingColumn].isDeleted) { 789 ++playingColumn; 790 if (playingColumn>range[1]) { 791 ((Timer) e.getSource()).stop(); 792 return; 793 } 794 } 795 796 Rectangle visibleRect = timeLineTable.getVisibleRect(); 797 Rectangle playingRect = timeLineTable.getCellRect(0, playingColumn, true); 798 if (!visibleRect.contains(playingRect)) { 799 Rectangle scrollTo = new Rectangle(playingRect.x, playingRect.width, visibleRect.width-1, visibleRect.height-1); 800 timeLineTable.scrollRectToVisible(scrollTo); 801 } 802 803 frameCanvas.repaint(); 804 timeLineTable.repaint(); 805 806 ++playingColumn; 807 if (playingColumn>range[1]) { 808 ((Timer) e.getSource()).stop(); 809 return; 810 } 811 } 812 813 814 }) { 815 @Override 816 public void stop() { 817 super.stop(); 818 playingColumn=-1; 819 timeLineTable.scrollRectToVisible(timeLineTable.getCellRect(0, focusColumn, true)); 820 frameCanvas.repaint(); 821 timeLineTable.repaint(); 822 playTimera[0] = null; 823 } 824 }; 825 826 playTimera[0].start(); 827 synchronized (playTimera) { 828 playTimera.notifyAll(); 829 } 830 } 831 }; 832 playMenuItem.setAction(playAction); 833 834 popup.add(playMenuItem); 835 836 timeLineTable.setComponentPopupMenu(popup ); 837 838 timeLineTable.addMouseListener(new MouseAdapter() { 839 840 @Override 841 public void mouseClicked(MouseEvent e) { 842 if (playTimera[0]!=null) { 843 playTimera[0].stop(); 844 } 845 846 if (e.getClickCount()==2) { 847 int hitColumnIndex = timeLineTable.columnAtPoint(e.getPoint()); 848 if (hitColumnIndex!=-1) { 849 frameEntries[hitColumnIndex].isDeleted=!frameEntries[hitColumnIndex].isDeleted; 850 timeLineTable.repaint(); 851 } 852 } 853 } 854 }); 855 856 timeLineTable.setToolTipText("Movie timeline"); 857 timeLineScrollPane.setViewportView(timeLineTable); 858 timeLineTable.setModel(timeLineTableModel); 859 timeLineTable.setRowHeight(0, cellHeight+timeLineTable.getRowMargin()*2); 860 timeLineTable.setRowHeight(1, AUDIO_CELL_HEIGHT+timeLineTable.getRowMargin()*2); 861 for (int i=0; i<frameEntries.length; ++i) { 862 timeLineTable.getColumnModel().getColumn(i).setPreferredWidth(cellWidth); 863 timeLineTable.setValueAt(frameEntries[i], 0, i); 864 if (hasAudio) { 865 timeLineTable.setValueAt(frameEntries[i], 1, i); 866 } 867 } 868 timeLineTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); 869 timeLineTable.setTableHeader(null); 870 timeLineTable.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 871 timeLineTable.setColumnSelectionAllowed(true); 872 timeLineTable.setRowSelectionAllowed(false); 873 874 TableCellRenderer renderer = new TableCellRenderer() { 875 876 @Override 877 public Component getTableCellRendererComponent(JTable table, Object value, final boolean isSelected, final boolean hasFocus, int row, int column) { 878 879 if (hasFocus && column!=focusColumn) { 880 focusColumn = column; 881 frameCanvas.repaint(); 882 } 883 884 return frameEntries[column].getCellRendererComponent(row, isSelected, hasFocus); 885 } 886 }; 887 timeLineTable.setDefaultRenderer(Object.class, renderer); 888 889 frameCanvas = new JPanel() { 890 891 @Override 892 public void paintComponent(Graphics g) { 893 super.paintComponent(g); 894 Rectangle bounds = getBounds(); 895 896 try { 897 Image image = frameEntries[playingColumn==-1 ? focusColumn : playingColumn].getImage(); 898 double xScale = (double) bounds.width/(double) image.getWidth(null); 899 double yScale = (double) bounds.height/(double) image.getHeight(null); 900 double scale = Math.min(xScale, yScale); 901 int scaledWidth = (int) (image.getWidth(null)*scale); 902 int scaledHeight = (int) (image.getHeight(null)*scale); 903 if (g instanceof Graphics2D) { 904 ((Graphics2D) g).setComposite(AlphaComposite.Src); 905 ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR); 906 ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY); 907 ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); 908 } 909 g.drawImage(image, (bounds.width-scaledWidth)/2, (bounds.height-scaledHeight)/2, scaledWidth, scaledHeight, null); 910 } catch (Exception e) { 911 e.printStackTrace(); 912 g.clearRect(0, 0, bounds.width, bounds.height); 913 g.drawString(e.toString(), 10, 20); 914 } 915 } 916 917 }; 918 919 frameCanvas.addMouseListener(new MouseAdapter() { 920 921 @Override 922 public void mouseClicked(MouseEvent e) { 923 if (playTimera[0]!=null) { 924 playTimera[0].stop(); 925 } 926 } 927 }); 928 929 frameCanvas.setPreferredSize(movie.getFrameDimension()); 930 contentPanel.add(frameCanvas, new GridBagConstraints(0, 0, 7, 1, 0.0, 931 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, 932 new Insets(1, 1, 1, 1), 0, 0)); 933 934 normalizeVolumeCheckBox = new JCheckBox(); 935 contentPanel.add(normalizeVolumeCheckBox, new GridBagConstraints(1, 4, 936 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, 937 GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); 938 normalizeVolumeCheckBox.setText("Normalize volume (+"+Math.round(Math.log10(NORMALIZED_LEVEL/maxVolume)*20)+" dB)"); 939 940 timeLineTable.changeSelection(0, 0, false, false); 941 frameCanvas.repaint(); 942 943 pack(); 944 945 } 946 addUndeleteFramesMenuItem(JPopupMenu popup)947 void addUndeleteFramesMenuItem(JPopupMenu popup) { 948 JMenuItem unDeleteFrameMenuItem = new JMenuItem(); 949 Action unDeleteFrameAction = new AbstractAction("Undelete frame(s)") { 950 951 @Override 952 public void actionPerformed(ActionEvent e) { 953 for (int idx: timeLineTable.getSelectedColumns()) { 954 frameEntries[idx].isDeleted = false; 955 } 956 timeLineTable.repaint(); 957 } 958 }; 959 unDeleteFrameMenuItem.setAction(unDeleteFrameAction); 960 961 popup.add(unDeleteFrameMenuItem); 962 } 963 addDeleteFramesMenuItem(JPopupMenu popup)964 void addDeleteFramesMenuItem(JPopupMenu popup) { 965 JMenuItem deleteFrameMenuItem = new JMenuItem(); 966 Action deleteFrameAction = new AbstractAction("Delete frame(s)") { 967 968 @Override 969 public void actionPerformed(ActionEvent e) { 970 for (int idx: timeLineTable.getSelectedColumns()) { 971 frameEntries[idx].isDeleted = true; 972 } 973 timeLineTable.repaint(); 974 } 975 }; 976 977 deleteFrameMenuItem.setAction(deleteFrameAction); 978 popup.add(deleteFrameMenuItem); 979 } 980 addRemoveInactivityMenuItem(JPopupMenu popup)981 void addRemoveInactivityMenuItem(JPopupMenu popup) { 982 JMenuItem removeInactivityMenuItem = new JMenuItem(); 983 Action deleteFrameAction = new AbstractAction("Remove inactivity") { 984 985 @Override 986 public void actionPerformed(ActionEvent e) { 987 String msg = "Inactivity interval"; 988 while (true) { 989 String newVal = JOptionPane.showInputDialog(msg, String.valueOf(inactivityInterval)); 990 if (newVal==null) { 991 return; 992 } 993 994 try { 995 inactivityInterval = Double.parseDouble(newVal); 996 if (inactivityInterval > 0) { 997 break; 998 } 999 } catch (NumberFormatException nfe) { 1000 // NOP - loop 1001 } 1002 msg = "Invalid double value for inactivity interval: "+newVal+", enter valid value"; 1003 } 1004 int inactivityInFrames = (int) (inactivityInterval*movie.getFramesPerSecond()); 1005 int lastActivity = -inactivityInFrames-1; 1006 for (int idx: timeLineTable.getSelectedColumns()) { 1007 if (!frameEntries[idx].isDeleted && frameEntries[idx].frame.isActive()) { 1008 lastActivity = idx; 1009 } else if (idx - lastActivity > inactivityInFrames && !frameEntries[idx].frame.isActive()) { 1010 frameEntries[idx].isDeleted = true; 1011 } 1012 } 1013 timeLineTable.repaint(); 1014 } 1015 }; 1016 1017 removeInactivityMenuItem.setAction(deleteFrameAction); 1018 popup.add(removeInactivityMenuItem); 1019 } 1020 1021 private class SoundPlayer implements Runnable { 1022 1023 private final int BUFFER_SIZE; 1024 private AudioInputStream audioStream; 1025 private SourceDataLine sourceLine; 1026 private File audioFile; 1027 SoundPlayer(int start, int end)1028 public SoundPlayer(int start, int end) throws Exception { 1029 1030 audioFile = hasAudio ? File.createTempFile("jCaptureRangeAudio", ".wav") : null; 1031 1032 BUFFER_SIZE = (int) ((double) numChannels*sampleRate*validBits/(movie.getFramesPerSecond()*8)); // 1 frame buffer. 1033 1034 long numFrames=0; 1035 for (int i = start; i<=end; ++i) { 1036 if (!frameEntries[i].isDeleted) { 1037 numFrames+=frameEntries[i].audioSamplesInFrame; 1038 } 1039 } 1040 WavFile newWavFile = audioFile==null ? null : WavFile.newWavFile(audioFile, numChannels, numFrames, validBits, sampleRate); 1041 1042 File currentAudio = null; 1043 WavFile currentWav = null; 1044 1045 for (int i=0; i<=end; ++i) { 1046 if (frameEntries[i].audioFile!=null) { 1047 if (currentWav!=null) { 1048 currentWav.close(); 1049 } 1050 1051 currentAudio = frameEntries[i].audioFile; 1052 currentWav = WavFile.openWavFile(currentAudio); 1053 } 1054 1055 if (currentWav!=null) { 1056 if (normalizeVolumeCheckBox!=null && normalizeVolumeCheckBox.isSelected()) { 1057 double[][] buf = new double[numChannels][frameEntries[i].audioSamplesInFrame]; 1058 int read = currentWav.readFrames(buf, frameEntries[i].audioSamplesInFrame); 1059 if (read>0 && i>=start && !frameEntries[i].isDeleted) { 1060 // Normalization 1061 for (double[] ch: buf) { 1062 for (int j=0; j<ch.length; ++j) { 1063 ch[j] = ch[j] * NORMALIZED_LEVEL / maxVolume; 1064 } 1065 } 1066 newWavFile.writeFrames(buf, read); 1067 } 1068 } else { 1069 long[][] buf = new long[numChannels][frameEntries[i].audioSamplesInFrame]; 1070 int read = currentWav.readFrames(buf, frameEntries[i].audioSamplesInFrame); 1071 if (read>0 && i>=start && !frameEntries[i].isDeleted) { 1072 newWavFile.writeFrames(buf, read); 1073 } 1074 } 1075 } 1076 } 1077 1078 if (currentWav!=null) { 1079 currentWav.close(); 1080 } 1081 if (newWavFile!=null) { 1082 newWavFile.close(); 1083 } 1084 1085 if (audioFile!=null) { 1086 audioStream = AudioSystem.getAudioInputStream(audioFile); 1087 AudioFormat audioFormat = audioStream.getFormat(); 1088 DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat); 1089 sourceLine = (SourceDataLine) AudioSystem.getLine(info); 1090 sourceLine.open(audioFormat); 1091 } 1092 } 1093 1094 @Override run()1095 public void run() { 1096 try { 1097 sourceLine.start(); 1098 synchronized (playTimera) { 1099 if (playTimera[0] == null) { 1100 playTimera.wait(100); 1101 } 1102 } 1103 try { 1104 byte[] buf = new byte[BUFFER_SIZE]; 1105 int l; 1106 while (playTimera[0]!=null && (l=audioStream.read(buf))!=-1) { 1107 sourceLine.write(buf, 0, l); 1108 } 1109 } finally { 1110 audioStream.close(); 1111 sourceLine.drain(); 1112 sourceLine.close(); 1113 if (!audioFile.delete()) { 1114 audioFile.deleteOnExit(); 1115 } 1116 } 1117 } catch (Exception e) { 1118 e.printStackTrace(); 1119 } 1120 } 1121 1122 } 1123 1124 } 1125