/* * @(#)MovieMakerMain.java * * Copyright © 2010-2011 Werner Randelshofer, Goldau, Switzerland. * All rights reserved. * * You may not use, copy or modify this file, except in compliance with the * license agreement you entered into with Werner Randelshofer. * For details see accompanying license terms. */ package org.monte.moviemaker; import javax.swing.UIManager; import org.monte.media.math.Rational; import org.monte.media.Buffer; import org.monte.media.Format; import org.monte.media.gui.datatransfer.FileTextFieldTransferHandler; import org.monte.media.mp3.MP3AudioInputStream; import org.monte.media.quicktime.QuickTimeWriter; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.Arrays; import java.util.Enumeration; import java.util.LinkedList; import java.util.prefs.Preferences; import javax.imageio.ImageIO; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.UnsupportedAudioFileException; import javax.swing.AbstractButton; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.ProgressMonitor; import javax.swing.SwingWorker; import javax.swing.border.EmptyBorder; import javax.swing.filechooser.FileSystemView; import static java.lang.Math.*; import static org.monte.media.FormatKeys.*; import static org.monte.media.AudioFormatKeys.*; import static org.monte.media.VideoFormatKeys.*; /** * A demo for the {@link QuickTimeWriter} class. * * @author Werner Randelshofer * @version $Id: MovieMakerMain.java 301 2013-01-03 07:40:42Z werner $ */ public class MovieMakerMain extends javax.swing.JFrame { private JFileChooser imageFolderChooser; private JFileChooser soundFileChooser; private JFileChooser movieFileChooser; private Preferences prefs; enum WhatToDo { REPEAT_SHORTER_TRACK, STRETCH_AND_SQUASH_VIDEO_TRACK, CUT_LONGER_TRACK, DONT_CARE } private WhatToDo whatToDo = WhatToDo.STRETCH_AND_SQUASH_VIDEO_TRACK; /** Creates new form MovieMakerMain */ public MovieMakerMain() { initComponents(); String version = getClass().getPackage().getImplementationVersion(); if (version != null) { setTitle(getTitle() + " " + version); } ((JComponent) getContentPane()).setBorder(new EmptyBorder(12, 18, 18, 18)); imageFolderField.setTransferHandler(new FileTextFieldTransferHandler(JFileChooser.DIRECTORIES_ONLY)); soundFileField.setTransferHandler(new FileTextFieldTransferHandler()); JComponent[] smallComponents = {compressionBox, compressionLabel, fpsField, fpsLabel, heightField, heightLabel, widthField, widthLabel, passThroughCheckBox, noPreparationRadio, fastStartCompressedRadio, fastStartRadio}; for (JComponent c : smallComponents) { c.putClientProperty("JComponent.sizeVariant", "small"); } // Get Preferences prefs = Preferences.userNodeForPackage(MovieMakerMain.class); imageFolderField.setText(prefs.get("movie.imageFolder", "")); soundFileField.setText(prefs.get("movie.soundFile", "")); widthField.setText("" + prefs.getInt("movie.width", 320)); heightField.setText("" + prefs.getInt("movie.height", 240)); passThroughCheckBox.setSelected(prefs.getBoolean("movie.passThrough", false)); String fps = "" + prefs.getDouble("movie.fps", 30); if (fps.endsWith(".0")) { fps = fps.substring(0, fps.length() - 2); } fpsField.setText(fps); compressionBox.setSelectedIndex(Math.max(0, Math.min(compressionBox.getItemCount() - 1, prefs.getInt("movie.compression", 1)))); // String streaming = prefs.get("movie.streaming", "fastStartCompressed"); for (Enumeration<AbstractButton> i = streamingGroup.getElements(); i.hasMoreElements();) { AbstractButton btn = i.nextElement(); if (btn.getActionCommand().equals(streaming)) { btn.setSelected(true); break; } } } /** This method is called from within the constructor to * initialize the form. * WARNING: Do NOT modify this code. The content of this method is * always regenerated by the Form Editor. */ @SuppressWarnings("unchecked") // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents private void initComponents() { streamingGroup = new javax.swing.ButtonGroup(); aboutLabel = new javax.swing.JLabel(); imageFolderHelpLabel = new javax.swing.JLabel(); imageFolderField = new javax.swing.JTextField(); chooseImageFolderButton = new javax.swing.JButton(); soundFileHelpLable = new javax.swing.JLabel(); soundFileField = new javax.swing.JTextField(); chooseSoundFileButton = new javax.swing.JButton(); createMovieButton = new javax.swing.JButton(); widthLabel = new javax.swing.JLabel(); widthField = new javax.swing.JTextField(); heightLabel = new javax.swing.JLabel(); heightField = new javax.swing.JTextField(); compressionLabel = new javax.swing.JLabel(); compressionBox = new javax.swing.JComboBox(); fpsLabel = new javax.swing.JLabel(); fpsField = new javax.swing.JTextField(); passThroughCheckBox = new javax.swing.JCheckBox(); streamingLabel = new javax.swing.JLabel(); noPreparationRadio = new javax.swing.JRadioButton(); fastStartRadio = new javax.swing.JRadioButton(); fastStartCompressedRadio = new javax.swing.JRadioButton(); FormListener formListener = new FormListener(); setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); setTitle("QuickTime Movie Maker"); aboutLabel.setText("<html><b>This is a demo of the Monte Media library.</b><br>Copyright © 2010-2012 Werner Randelshofer. All rights reserved.<br> This software can be licensed under Creative Commons Atribution 3.0."); imageFolderHelpLabel.setText("Drag a folder with image files into the field below:"); chooseImageFolderButton.setText("Choose..."); chooseImageFolderButton.addActionListener(formListener); soundFileHelpLable.setText("Drag a sound file into the field below (.au, .aiff, .wav, .mp3):"); chooseSoundFileButton.setText("Choose..."); chooseSoundFileButton.addActionListener(formListener); createMovieButton.setText("Create QuickTime Movie..."); createMovieButton.addActionListener(formListener); widthLabel.setFont(new java.awt.Font("Lucida Grande", 0, 11)); widthLabel.setText("Width:"); widthField.setColumns(4); widthField.setFont(new java.awt.Font("Lucida Grande", 0, 11)); widthField.setText("320"); heightLabel.setFont(new java.awt.Font("Lucida Grande", 0, 11)); heightLabel.setText("Height:"); heightField.setColumns(4); heightField.setFont(new java.awt.Font("Lucida Grande", 0, 11)); heightField.setText("240"); compressionLabel.setFont(new java.awt.Font("Lucida Grande", 0, 11)); compressionLabel.setText("Compression:"); compressionBox.setFont(new java.awt.Font("Lucida Grande", 0, 11)); compressionBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "None", "Animation", "JPEG", "PNG" })); fpsLabel.setFont(new java.awt.Font("Lucida Grande", 0, 11)); fpsLabel.setText("FPS:"); fpsField.setColumns(4); fpsField.setFont(new java.awt.Font("Lucida Grande", 0, 11)); fpsField.setText("30"); passThroughCheckBox.setFont(new java.awt.Font("Lucida Grande", 0, 11)); passThroughCheckBox.setText("Pass through"); passThroughCheckBox.setToolTipText("Check this box if the folder contains already encoded video frames in the desired size."); streamingLabel.setText("Prepare for Internet Streaming"); streamingGroup.add(noPreparationRadio); noPreparationRadio.setFont(new java.awt.Font("Lucida Grande", 0, 11)); noPreparationRadio.setSelected(true); noPreparationRadio.setText("No preparation"); noPreparationRadio.setActionCommand("none"); noPreparationRadio.addActionListener(formListener); streamingGroup.add(fastStartRadio); fastStartRadio.setFont(new java.awt.Font("Lucida Grande", 0, 11)); fastStartRadio.setText("Fast Start"); fastStartRadio.setActionCommand("fastStart"); fastStartRadio.addActionListener(formListener); streamingGroup.add(fastStartCompressedRadio); fastStartCompressedRadio.setFont(new java.awt.Font("Lucida Grande", 0, 11)); fastStartCompressedRadio.setText("Fast Start - Compressed Header"); fastStartCompressedRadio.setActionCommand("fastStartCompressed"); fastStartCompressedRadio.addActionListener(formListener); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); getContentPane().setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addGap(61, 61, 61) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(widthLabel) .addComponent(fpsLabel)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addComponent(fpsField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(compressionLabel) .addGap(1, 1, 1) .addComponent(compressionBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(passThroughCheckBox)) .addGroup(layout.createSequentialGroup() .addComponent(widthField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(heightLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(heightField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) .addGap(41, 41, 41)) .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(aboutLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 484, Short.MAX_VALUE) .addComponent(imageFolderHelpLabel) .addComponent(soundFileHelpLable) .addGroup(layout.createSequentialGroup() .addComponent(soundFileField, javax.swing.GroupLayout.DEFAULT_SIZE, 372, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(chooseSoundFileButton)) .addComponent(createMovieButton, javax.swing.GroupLayout.Alignment.TRAILING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() .addComponent(imageFolderField, javax.swing.GroupLayout.DEFAULT_SIZE, 372, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(chooseImageFolderButton)))) .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(streamingLabel) .addGroup(layout.createSequentialGroup() .addGap(21, 21, 21) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(fastStartRadio) .addComponent(noPreparationRadio) .addComponent(fastStartCompressedRadio)))))) .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addContainerGap() .addComponent(aboutLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addGap(18, 18, 18) .addComponent(imageFolderHelpLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(imageFolderField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(chooseImageFolderButton)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(widthLabel) .addComponent(widthField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(heightLabel) .addComponent(heightField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(compressionBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(fpsLabel) .addComponent(fpsField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(compressionLabel) .addComponent(passThroughCheckBox)) .addGap(18, 18, 18) .addComponent(soundFileHelpLable) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(soundFileField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(chooseSoundFileButton)) .addGap(18, 18, 18) .addComponent(streamingLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(noPreparationRadio) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(fastStartRadio) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(fastStartCompressedRadio) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 18, Short.MAX_VALUE) .addComponent(createMovieButton) .addContainerGap()) ); pack(); } // Code for dispatching events from components to event handlers. private class FormListener implements java.awt.event.ActionListener { FormListener() {} public void actionPerformed(java.awt.event.ActionEvent evt) { if (evt.getSource() == chooseImageFolderButton) { MovieMakerMain.this.chooseImageFolder(evt); } else if (evt.getSource() == chooseSoundFileButton) { MovieMakerMain.this.chooseSoundFile(evt); } else if (evt.getSource() == createMovieButton) { MovieMakerMain.this.createMovie(evt); } else if (evt.getSource() == noPreparationRadio) { MovieMakerMain.this.streamingRadioPerformed(evt); } else if (evt.getSource() == fastStartRadio) { MovieMakerMain.this.streamingRadioPerformed(evt); } else if (evt.getSource() == fastStartCompressedRadio) { MovieMakerMain.this.streamingRadioPerformed(evt); } } }// </editor-fold>//GEN-END:initComponents private void chooseImageFolder(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chooseImageFolder if (imageFolderChooser == null) { imageFolderChooser = new JFileChooser(); imageFolderChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); if (imageFolderField.getText().length() > 0) { imageFolderChooser.setSelectedFile(new File(imageFolderField.getText())); } else if (soundFileField.getText().length() > 0) { imageFolderChooser.setCurrentDirectory(new File(soundFileField.getText()).getParentFile()); } } if (JFileChooser.APPROVE_OPTION == imageFolderChooser.showOpenDialog(this)) { imageFolderField.setText(imageFolderChooser.getSelectedFile().getPath()); } }//GEN-LAST:event_chooseImageFolder private void chooseSoundFile(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chooseSoundFile if (soundFileChooser == null) { soundFileChooser = new JFileChooser(); if (soundFileField.getText().length() > 0) { soundFileChooser.setSelectedFile(new File(soundFileField.getText())); } else if (imageFolderField.getText().length() > 0) { soundFileChooser.setCurrentDirectory(new File(imageFolderField.getText())); } } if (JFileChooser.APPROVE_OPTION == soundFileChooser.showOpenDialog(this)) { soundFileField.setText(soundFileChooser.getSelectedFile().getPath()); } }//GEN-LAST:event_chooseSoundFile private void createMovie(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_createMovie // --------------------------------- // Check input // --------------------------------- final File soundFile = soundFileField.getText().trim().length() == 0 ? null : new File(soundFileField.getText().trim()); final File imageFolder = imageFolderField.getText().trim().length() == 0 ? null : new File(imageFolderField.getText().trim()); final String streaming = prefs.get("movie.streaming", "fastStartCompressed"); if (soundFile == null && imageFolder == null) { JOptionPane.showMessageDialog(this, "<html>You need to specify a folder with<br>image files and/or a sound file."); return; } final int width, height; final double fps; try { width = Integer.parseInt(widthField.getText()); height = Integer.parseInt(heightField.getText()); fps = Double.parseDouble(fpsField.getText()); } catch (Throwable t) { JOptionPane.showMessageDialog(this, "<html>Width, Height and FPS must be numeric."); return; } if (fps!=fps) { JOptionPane.showMessageDialog(this, "<html>FPS must be a real number greater than zero."); return; } if (width <= 0 || height <= 0 || fps <= 0.0) { JOptionPane.showMessageDialog(this, "<html>Width, Height and FPS must be greater than zero."); return; } final Format videoFormat; switch (compressionBox.getSelectedIndex()) { case 0: videoFormat = QuickTimeWriter.VIDEO_RAW; break; case 1: videoFormat = QuickTimeWriter.VIDEO_ANIMATION; break; case 2: videoFormat = QuickTimeWriter.VIDEO_JPEG; break; case 3: default: videoFormat = QuickTimeWriter.VIDEO_PNG; break; } // --------------------------------- // Update Preferences // --------------------------------- prefs.put("movie.imageFolder", imageFolderField.getText()); prefs.put("movie.soundFile", soundFileField.getText()); prefs.putInt("movie.width", width); prefs.putInt("movie.height", height); prefs.putDouble("movie.fps", fps); prefs.putInt("movie.compression", compressionBox.getSelectedIndex()); prefs.putBoolean("movie.passThrough", passThroughCheckBox.isSelected()); // --------------------------------- // Choose an output file // --------------------------------- if (movieFileChooser == null) { movieFileChooser = new JFileChooser(); if (prefs.get("movie.outputFile", null) != null) { movieFileChooser.setSelectedFile(new File(prefs.get("movie.outputFile", null))); } else { if (imageFolderField.getText().length() > 0) { movieFileChooser.setCurrentDirectory(new File(imageFolderField.getText()).getParentFile()); } else if (soundFileField.getText().length() > 0) { movieFileChooser.setCurrentDirectory(new File(soundFileField.getText()).getParentFile()); } } } if (JFileChooser.APPROVE_OPTION != movieFileChooser.showSaveDialog(this)) { return; } final File movieFile = movieFileChooser.getSelectedFile().getPath().toLowerCase().endsWith(".mov")// ? movieFileChooser.getSelectedFile() : new File(movieFileChooser.getSelectedFile().getPath() + ".mov"); prefs.put("movie.outputFile", movieFile.getPath()); createMovieButton.setEnabled(false); final boolean passThrough = passThroughCheckBox.isSelected(); // --------------------------------- // Create the QuickTime movie // --------------------------------- SwingWorker w = new SwingWorker() { @Override protected Object doInBackground() { try { // Read image files File[] imgFiles = null; if (imageFolder != null) { imgFiles = imageFolder.listFiles(new FileFilter() { FileSystemView fsv = FileSystemView.getFileSystemView(); @Override public boolean accept(File f) { return f.isFile() && !fsv.isHiddenFile(f) && !f.getName().equals("Thumbs.db"); } }); if (imgFiles != null) { Arrays.sort(imgFiles); } } // FIXME Check on first image, if we can actually do pass through if (passThrough) { } // Delete movie file if it already exists. if (movieFile.exists()) { movieFile.delete(); } if (imgFiles != null && soundFile != null && imgFiles.length > 0) { writeVideoAndAudio(movieFile, imgFiles, soundFile, width, height, fps, videoFormat, passThrough, streaming); } else if (imgFiles != null && imgFiles.length > 0) { writeVideoOnlyVFR(movieFile, imgFiles, width, height, fps, videoFormat, passThrough, streaming); } else if (soundFile != null) { writeAudioOnly(movieFile, soundFile, streaming); } return null; } catch (Throwable t) { return t; } } @Override protected void done() { Object o; try { o = get(); } catch (Exception ex) { o = ex; } if (o instanceof Throwable) { Throwable t = (Throwable) o; t.printStackTrace(); JOptionPane.showMessageDialog(MovieMakerMain.this, "<html>Creating the QuickTime Movie failed.<br>" + (t.getMessage() == null ? t.toString() : t.getMessage()), "Sorry", JOptionPane.ERROR_MESSAGE); } createMovieButton.setEnabled(true); } }; w.execute(); }//GEN-LAST:event_createMovie private void streamingRadioPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_streamingRadioPerformed prefs.put("movie.streaming", evt.getActionCommand()); }//GEN-LAST:event_streamingRadioPerformed /** variable frame rate. */ private void writeVideoOnlyVFR(File movieFile, File[] imgFiles, int width, int height, double fps, Format videoFormat, boolean passThrough, String streaming) throws IOException { File tmpFile = streaming.equals("none") ? movieFile : new File(movieFile.getPath() + ".tmp"); ProgressMonitor p = new ProgressMonitor(MovieMakerMain.this, "Creating " + movieFile.getName(), "Creating Output File...", 0, imgFiles.length); Graphics2D g = null; BufferedImage img = null; BufferedImage prevImg = null; int[] data = null; int[] prevData = null; QuickTimeWriter qtOut = null; try { int timeScale = (int) (fps * 100.0); int duration = 100; qtOut = new QuickTimeWriter(tmpFile); int vt = qtOut.addVideoTrack(videoFormat, timeScale, width, height); qtOut.setSyncInterval(0, 30); if (!passThrough) { img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); data = ((DataBufferInt) img.getRaster().getDataBuffer()).getData(); prevImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); prevData = ((DataBufferInt) prevImg.getRaster().getDataBuffer()).getData(); g = img.createGraphics(); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); } int prevImgDuration = 0; Buffer buf = new Buffer(); for (int i = 0; i < imgFiles.length && !p.isCanceled(); i++) { File f = imgFiles[i]; p.setNote("Processing " + f.getName()); p.setProgress(i); if (passThrough) { qtOut.writeSample(vt, f, duration, true); } else { BufferedImage fImg = ImageIO.read(f); g.drawImage(fImg, 0, 0, width, height, null); if (i != 0 && Arrays.equals(data, prevData)) { prevImgDuration += duration; } else { if (prevImgDuration != 0) { qtOut.write(vt, prevImg, prevImgDuration); } prevImgDuration = duration; System.arraycopy(data, 0, prevData, 0, data.length); } } } if (prevImgDuration != 0) { qtOut.write(vt, prevImg, prevImgDuration); } if (streaming.equals("fastStart")) { qtOut.toWebOptimizedMovie(movieFile, false); tmpFile.delete(); } else if (streaming.equals("fastStartCompressed")) { qtOut.toWebOptimizedMovie(movieFile, true); tmpFile.delete(); } qtOut.close(); qtOut = null; } finally { p.close(); if (g != null) { g.dispose(); } if (img != null) { img.flush(); } if (qtOut != null) { qtOut.close(); } } } /** fixed framerate. */ private void writeVideoOnlyFFR(File movieFile, File[] imgFiles, int width, int height, double fps, Format videoFormat, boolean passThrough, String streaming) throws IOException { File tmpFile = streaming.equals("none") ? movieFile : new File(movieFile.getPath() + ".tmp"); ProgressMonitor p = new ProgressMonitor(MovieMakerMain.this, "Creating " + movieFile.getName(), "Creating Output File...", 0, imgFiles.length); Graphics2D g = null; BufferedImage imgBuffer = null; QuickTimeWriter qtOut = null; try { Rational duration = new Rational(1000,(int)(fps*1000)); qtOut = new QuickTimeWriter(tmpFile); int vt = qtOut.addTrack(videoFormat.append(WidthKey,width, HeightKey,height)); //qtOut.setSyncInterval(0,0); if (!passThrough) { imgBuffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); g = imgBuffer.createGraphics(); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); } Buffer buf = new Buffer(); for (int i = 0; i < imgFiles.length && !p.isCanceled(); i++) { File f = imgFiles[i]; p.setNote("Processing " + f.getName()); p.setProgress(i); if (passThrough) { qtOut.writeSample(vt, f, duration.floor(qtOut.getMediaTimeScale(vt)).getNumerator(), true); } else { BufferedImage fImg = ImageIO.read(f); if (fImg == null) { continue; } g.drawImage(fImg, 0, 0, width, height, null); buf.data = imgBuffer; buf.sampleDuration = duration; qtOut.write(vt, buf); } } if (streaming.equals("fastStart")) { qtOut.toWebOptimizedMovie(movieFile, false); tmpFile.delete(); } else if (streaming.equals("fastStartCompressed")) { qtOut.toWebOptimizedMovie(movieFile, true); tmpFile.delete(); } qtOut.close(); qtOut = null; } finally { p.close(); if (g != null) { g.dispose(); } if (imgBuffer != null) { imgBuffer.flush(); } if (qtOut != null) { qtOut.close(); } } } private void writeAudioOnly(File movieFile, File audioFile, String streaming) throws IOException { File tmpFile = streaming.equals("none") ? movieFile : new File(movieFile.getPath() + ".tmp"); int length = (int) Math.min(Integer.MAX_VALUE, audioFile.length()); // file length is used for a rough progress estimate. This will only work for uncompressed audio. ProgressMonitor p = new ProgressMonitor(MovieMakerMain.this, "Creating " + movieFile.getName(), "Initializing...", 0, length); AudioInputStream audioIn = null; QuickTimeWriter qtOut = null; try { qtOut = new QuickTimeWriter(tmpFile); if (audioFile.getName().toLowerCase().endsWith(".mp3")) { audioIn = new MP3AudioInputStream(audioFile); } else { audioIn = AudioSystem.getAudioInputStream(audioFile); } AudioFormat audioFormat = audioIn.getFormat(); //System.out.println("MovieMakerMain " + audioFormat); int at = qtOut.addAudioTrack(audioFormat); boolean isVBR = audioFormat.getProperty("vbr") != null && ((Boolean) audioFormat.getProperty("vbr")).booleanValue(); int asSize = audioFormat.getFrameSize(); int nbOfFramesInBuffer = isVBR ? 1 : Math.max(1, 1024 / asSize); int asDuration = (int) (audioFormat.getSampleRate() / audioFormat.getFrameRate()); //System.out.println(" frameDuration=" + asDuration); long count = 0; byte[] audioBuffer = new byte[asSize * nbOfFramesInBuffer]; for (int bytesRead = audioIn.read(audioBuffer); bytesRead != -1; bytesRead = audioIn.read(audioBuffer)) { if (bytesRead != 0) { int framesRead = bytesRead / asSize; qtOut.writeSamples(at, framesRead, audioBuffer, 0, bytesRead, asDuration); count += bytesRead; p.setProgress((int) count); } if (isVBR) { audioFormat = audioIn.getFormat(); if (audioFormat == null) { break; } asSize = audioFormat.getFrameSize(); asDuration = (int) (audioFormat.getSampleRate() / audioFormat.getFrameRate()); if (audioBuffer.length < asSize) { audioBuffer = new byte[asSize]; } } } audioIn.close(); audioIn = null; if (streaming.equals("fastStart")) { qtOut.toWebOptimizedMovie(movieFile, false); tmpFile.delete(); } else if (streaming.equals("fastStartCompressed")) { qtOut.toWebOptimizedMovie(movieFile, true); tmpFile.delete(); } qtOut.close(); qtOut = null; } catch (UnsupportedAudioFileException e) { IOException ioe = new IOException(e.getMessage()); ioe.initCause(e); throw ioe; } finally { p.close(); if (audioIn != null) { audioIn.close(); } if (qtOut != null) { qtOut.close(); } } } private void writeVideoAndAudio(File movieFile, File[] imgFiles, File audioFile, int width, int height, double fps, Format videoFormat, boolean passThrough, String streaming) throws IOException { File tmpFile = streaming.equals("none") ? movieFile : new File(movieFile.getPath() + ".tmp"); ProgressMonitor p = new ProgressMonitor(MovieMakerMain.this, "Creating " + movieFile.getName(), "Creating Output File...", 0, imgFiles.length); AudioInputStream audioIn = null; QuickTimeWriter qtOut = null; BufferedImage imgBuffer = null; Graphics2D g = null; try { // Determine audio format if (audioFile.getName().toLowerCase().endsWith(".mp3")) { audioIn = new MP3AudioInputStream(audioFile); } else { audioIn = AudioSystem.getAudioInputStream(audioFile); } AudioFormat audioFormat = audioIn.getFormat(); boolean isVBR = audioFormat.getProperty("vbr") != null && ((Boolean) audioFormat.getProperty("vbr")).booleanValue(); // Determine sampleDuration of a single sample int asDuration = (int) (audioFormat.getSampleRate() / audioFormat.getFrameRate()); int vsDuration = 100; // Create writer qtOut = new QuickTimeWriter(tmpFile); int at = qtOut.addAudioTrack(audioFormat); // audio in track 0 int vt = qtOut.addVideoTrack(videoFormat, (int) (fps * vsDuration), width, height); // video in track 1 qtOut.setCompressionQuality(vt, 0.95f); // Create audio buffer int asSize; byte[] audioBuffer; if (isVBR) { // => variable bit rate: create audio buffer for a single frame asSize = audioFormat.getFrameSize(); audioBuffer = new byte[asSize]; } else { // => fixed bit rate: create audio buffer for half a second asSize = audioFormat.getChannels() * audioFormat.getSampleSizeInBits() / 8; audioBuffer = new byte[(int) (qtOut.getMediaTimeScale(0) / 2 * asSize)]; } // Create video buffer if (!passThrough) { imgBuffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); g = imgBuffer.createGraphics(); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); } // Main loop int movieTime = 0; int imgIndex = 0; boolean isAudioDone = false; Buffer buf = new Buffer(); while ((imgIndex < imgFiles.length || !isAudioDone) && !p.isCanceled()) { // Advance movie time by half a second (we interleave twice per second) movieTime += qtOut.getMovieTimeScale() / 2; // Advance audio to movie time + 1 second (audio must be ahead of video by 1 second) while (!isAudioDone && qtOut.getTrackDuration(0) < movieTime + qtOut.getMovieTimeScale()) { int len = audioIn.read(audioBuffer); if (len == -1) { isAudioDone = true; } else { qtOut.writeSamples(at, len / asSize, audioBuffer, 0, len, asDuration); } if (isVBR) { // => variable bit rate: format can change at any time audioFormat = audioIn.getFormat(); if (audioFormat == null) { break; } asSize = audioFormat.getFrameSize(); asDuration = (int) (audioFormat.getSampleRate() / audioFormat.getFrameRate()); if (audioBuffer.length < asSize) { audioBuffer = new byte[asSize]; } } } // Advance video to movie time while (imgIndex < imgFiles.length && qtOut.getTrackDuration(1) < movieTime) { // catch up with video time p.setProgress(imgIndex); p.setNote("Processing " + imgFiles[imgIndex].getName()); if (passThrough) { qtOut.writeSample(vt, imgFiles[imgIndex], vsDuration, true); } else { BufferedImage fImg = ImageIO.read(imgFiles[imgIndex]); if (fImg == null) { continue; } g.drawImage(fImg, 0, 0, width, height, null); fImg.flush(); qtOut.write(vt, imgBuffer, vsDuration); } ++imgIndex; } } switch (whatToDo) { case CUT_LONGER_TRACK: { long d0 = qtOut.getTrackDuration(at); long d1 = qtOut.getTrackDuration(vt); int longerTrack = -1; int shorterDuration = -1; if (d0 != 0 && d1 != 0) { if (d0 > d1) { longerTrack = 0; shorterDuration = (int) d1; } else if (d1 > d0) { longerTrack = 1; shorterDuration = (int) d0; } } if (longerTrack != -1) { LinkedList<QuickTimeWriter.Edit> l = new LinkedList<QuickTimeWriter.Edit>(); l.add(new QuickTimeWriter.Edit(shorterDuration, 0, 1.0)); // sampleDuration, media time, media rate qtOut.setEditList(longerTrack, l.toArray(new QuickTimeWriter.Edit[l.size()])); } } break; case DONT_CARE: break; case REPEAT_SHORTER_TRACK: { long d0 = qtOut.getTrackDuration(at); long d1 = qtOut.getTrackDuration(vt); int shorterTrack = -1; int longerTrack = -1; int shorterDuration = -1; int longerDuration = -1; if (d0 != 0 && d1 != 0) { if (d0 > d1) { shorterTrack = 1; longerTrack = 0; shorterDuration = (int) d1; longerDuration = (int) d0; } else if (d1 > d0) { longerTrack = 1; shorterTrack = 1; shorterDuration = (int) d0; longerDuration = (int) d1; } } if (longerTrack != -1) { LinkedList<QuickTimeWriter.Edit> l = new LinkedList<QuickTimeWriter.Edit>(); for (; longerDuration > 0; longerDuration -= shorterDuration) { l.add(new QuickTimeWriter.Edit(min(shorterDuration, longerDuration), 0, 1.0)); // sampleDuration, media time, media rate } qtOut.setEditList(shorterTrack, l.toArray(new QuickTimeWriter.Edit[l.size()])); } } break; case STRETCH_AND_SQUASH_VIDEO_TRACK: { long d0 = qtOut.getTrackDuration(at); long d1 = qtOut.getTrackDuration(vt); if (d0 != d1 && d0 != 0 && d1 != 0) { LinkedList<QuickTimeWriter.Edit> l = new LinkedList<QuickTimeWriter.Edit>(); l.add(new QuickTimeWriter.Edit((int) d0, 0, d1 / (float) d0)); // sampleDuration, media time, media rate qtOut.setEditList(1, l.toArray(new QuickTimeWriter.Edit[l.size()])); } } break; } if (streaming.equals("fastStart")) { qtOut.toWebOptimizedMovie(movieFile, false); tmpFile.delete(); } else if (streaming.equals("fastStartCompressed")) { qtOut.toWebOptimizedMovie(movieFile, true); tmpFile.delete(); } qtOut.close(); qtOut = null; } catch (UnsupportedAudioFileException e) { IOException ioe = new IOException(e.getMessage()); ioe.initCause(e); throw ioe; } finally { p.close(); if (qtOut != null) { qtOut.close(); } if (audioIn != null) { audioIn.close(); } if (g != null) { g.dispose(); } if (imgBuffer != null) { imgBuffer.flush(); } } } /** * @param args the command line arguments */ public static void main(String args[]) { java.awt.EventQueue.invokeLater(new Runnable() { @Override public void run() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { //ignore } MovieMakerMain m = new MovieMakerMain(); m.setVisible(true); m.pack(); } }); } // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JLabel aboutLabel; private javax.swing.JButton chooseImageFolderButton; private javax.swing.JButton chooseSoundFileButton; private javax.swing.JComboBox compressionBox; private javax.swing.JLabel compressionLabel; private javax.swing.JButton createMovieButton; private javax.swing.JRadioButton fastStartCompressedRadio; private javax.swing.JRadioButton fastStartRadio; private javax.swing.JTextField fpsField; private javax.swing.JLabel fpsLabel; private javax.swing.JTextField heightField; private javax.swing.JLabel heightLabel; private javax.swing.JTextField imageFolderField; private javax.swing.JLabel imageFolderHelpLabel; private javax.swing.JRadioButton noPreparationRadio; private javax.swing.JCheckBox passThroughCheckBox; private javax.swing.JTextField soundFileField; private javax.swing.JLabel soundFileHelpLable; private javax.swing.ButtonGroup streamingGroup; private javax.swing.JLabel streamingLabel; private javax.swing.JTextField widthField; private javax.swing.JLabel widthLabel; // End of variables declaration//GEN-END:variables }