Commit 3083cfe8 authored by angermue's avatar angermue
Browse files

Improvement of PodcastCreator.java for providing smooth animations including annotations.

git-svn-id: https://www2.in.tum.de/repos/ttt/trunk@39 0463f305-d864-43cb-8a47-61cf597d4139
parent 46534b0a
// TeleTeachingTool - Presentation Recording With Automated Indexing
//
// Copyright (C) 2003-2008 Peter Ziewer - Technische Universitt Mnchen
// Copyright (C) 2003-2008 Peter Ziewer - Technische Universitt Mnchen
//
// This file is part of TeleTeachingTool.
//
......@@ -141,7 +141,37 @@ public class GraphicsContext extends JComponent implements MessageConsumer {
// does not allow getGraphics() :-(
return memImage.getScaledInstance(prefs.framebufferWidth, prefs.framebufferHeight, Image.SCALE_FAST);
}
/**
* Creates screenshot of current graphics contect including annotations, whiteboard pages, and the cursor.
*
* @see PodcastCreator#createPodcast
* @see GraphicsContext#getScreenshotWithoutAnnotations
*/
public BufferedImage getScreenshot() {
//Create a buffered image using the default color model
BufferedImage screenshot = new BufferedImage(prefs.framebufferWidth, prefs.framebufferHeight, BufferedImage.TYPE_INT_RGB);
Graphics g = screenshot.getGraphics();
if (isWhiteboardEnabled()) {
//Create whiteboard page
g.setColor(Color.WHITE);
g.fillRect(0, 0, prefs.framebufferWidth, prefs.framebufferHeight);
} else {
//Draw desktop
g.drawImage(memImage, 0, 0, null);
}
paintAnnotations((Graphics2D)g); //Paint annotation
// display cursor
if (showSoftCursor) {
int x0 = cursorX - hotX, y0 = cursorY - hotY;
Rectangle r = new Rectangle(x0, y0, cursorWidth, cursorHeight);
if (r.intersects(new Rectangle(0,0,screenshot.getWidth(),screenshot.getHeight()))) {
g.drawImage(softCursor, x0, y0, null);
}
}
return screenshot;
}
//MODMSG : changed return type to BufferedImage
public BufferedImage getScreenshotWithoutAnnotations() {
BufferedImage screenshot;
......
// TeleTeachingTool - Presentation Recording With Automated Indexing
//
// Copyright (C) 2003-2008 Peter Ziewer - Technische Universitt Mnchen
// Copyright (C) 2003-2008 Peter Ziewer - Technische Universitt Mnchen
//
// This file is part of TeleTeachingTool.
//
......@@ -23,7 +23,10 @@
package ttt;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
......@@ -155,7 +158,90 @@ public class ImageCreator {
return set.toArray(new String[0]);
}
/**
* Convenience method that returns a scaled instance of the provided {@code
* BufferedImage}.
*
* http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
*
* @param image
* the original image to be scaled
* @param targetWidth
* the desired width of the scaled instance, in pixels
* @param targetHeight
* the desired height of the scaled instance, in pixels
* @param hint
* one of the rendering hints that corresponds to {@code
* RenderingHints.KEY_INTERPOLATION} (e.g. {@code
* RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR}, {@code
* RenderingHints.VALUE_INTERPOLATION_BILINEAR}, {@code
* RenderingHints.VALUE_INTERPOLATION_BICUBIC})
* @param higherQuality
* if true, this method will use a multi-step scaling technique
* that provides higher quality than the usual one-step technique
* (only useful in downscaling cases, where {@code targetWidth}
* or {@code targetHeight} is smaller than the original
* dimensions, and generally only when the {@code BILINEAR} hint
* is specified)
* @return a scaled version of the original {@code BufferedImage}
*/
public static BufferedImage getScaledInstance(BufferedImage image,
int targetWidth, int targetHeight, Object hint,
boolean higherQuality)
{
int type = (image.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB
: BufferedImage.TYPE_INT_ARGB;
BufferedImage ret = (BufferedImage) image;
int w, h;
if (higherQuality)
{
// Use multi-step technique: start with original size, then
// scale down in multiple passes with drawImage()
// until the target size is reached
w = image.getWidth();
h = image.getHeight();
} else
{
// Use one-step technique: scale directly from original
// size to target size with a single drawImage() call
w = targetWidth;
h = targetHeight;
}
do
{
if (higherQuality && w > targetWidth)
{
w /= 2;
if (w < targetWidth)
{
w = targetWidth;
}
}
if (higherQuality && h > targetHeight)
{
h /= 2;
if (h < targetHeight)
{
h = targetHeight;
}
}
BufferedImage tmp = new BufferedImage(w, h, type);
Graphics2D g2 = tmp.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
g2.drawImage(ret, 0, 0, w, h, null);
g2.dispose();
ret = tmp;
} while (w != targetWidth || h != targetHeight);
return ret;
}
public static void main(String args[]) {
listFormats();
}
}
......@@ -51,16 +51,16 @@ public class LameEncoder {
}
/**Checks whether lame is available.
* @return True if lame is available.
/**Checks whether lame is available
* @return True if lame is available
* */
public static boolean isLameAvailable() {
return Exec.getCommand(LAME) != null;
}
/**Allows converting audio files using lame.
* @return True: Conversion succeeded.<br>False: Canceled by user.
/**Allows converting audio files using lame
* @return True: Conversion succeeded.<br>False: Canceled by user
*/
public static boolean convertAudioFile(File inFile, File outFile, String options, boolean batch) throws Exception {
......@@ -129,8 +129,8 @@ public class LameEncoder {
}
/**Allows converting audio files using lame determining suitable options automatically.
* @return True: Conversion succeeded.<br>False: Canceled by user.
/**Allows converting audio files using lame determining suitable options automatically
* @return True: Conversion succeeded.<br>False: Canceled by user
*/
public static boolean convertAudioFile(File inFile, File outFile, boolean batch) throws Exception {
......
......@@ -20,9 +20,10 @@
package ttt;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Scanner;
......@@ -39,10 +40,13 @@ import javax.swing.Timer;
*/
public class PodcastCreator {
private static final String RESOLUTION = "480x320"; //ouput resolution.
private static final int RESOLUTION_WIDTH = 480;
private static final int RESOLUTION_HEIGTH = 320;
private static final String FFMPEG = "ffmpeg";
private static final String MP4BOX = "MP4Box";
private static final double FRAMES_PER_SEC = 1;
public static void main(String[] args) throws Exception {
......@@ -56,31 +60,44 @@ public class PodcastCreator {
/**
* Checks whether it is possible to create a podcast.
* Checks whether it is possible to create a podcast
* @param recording
* @return True if ffmpeg, MP4Box, and an audio file is available for creating a podcast.
* @return True if ffmpeg, MP4Box, and an audio file is available for creating a podcast
* @throws IOException
*/
public static boolean isCreationPossible(Recording recording) throws IOException {
return Exec.getCommand(FFMPEG) != null && Exec.getCommand(MP4BOX) != null && (recording.getExistingFileBySuffix(new String[] {"wav","mp3","mp2"}).exists());
}
/**
* Creates podcast.
* Creates podcast using default parameters
* @param recording
* @param batch
* @return True: Podcast created successfully.<br>False: Canceled by user.
* @throws Exception
*/
public static boolean createPodcast(Recording recording, boolean batch) throws Exception {
return createPodcast(recording, RESOLUTION_WIDTH, RESOLUTION_HEIGTH, FRAMES_PER_SEC, batch);
}
/**
* Creates podcast
* @param recording
* @param resolutionWidth Podcast width
* @param resolutionHeight Podcast heigth
* @param framesPerSec Frames per second
* @param batch
* @return True: Podcast created successfully.<br>False: Canceled by user
* @throws Exception
*/
public static boolean createPodcast(Recording recording, int resolutionWidth, int resolutionHeight, double framesPerSec, boolean batch) throws Exception {
System.out.println("----------------------------------------------");
System.out.println("PodcastCreator");
System.out.println("----------------------------------------------");
System.out.println("Creating mp4 podcast");
//Check whether the necessary applications are available.
//check whether the necessary applications are available
String ffmpegCmd = Exec.getCommand(FFMPEG);
if (ffmpegCmd == null) {
throw new IOException("ffmpeg not found");
......@@ -89,118 +106,132 @@ public class PodcastCreator {
if (mp4BoxCmd == null) {
throw new IOException("MP4Box not found");
}
//Create html script containing images for creating the podcast if not available.
if (new File(recording.getDirectory() + recording.getFileBase() + ".html" + File.separator).exists() == false) {
if (recording.createScript(ScriptCreator.HTML_SCRIPT, batch) == false) {
return false;
}
}
//Get audio file
//get audio file
File audioFile = recording.getExistingFileBySuffix(new String[] {"wav","mp3","mp2"});
if (audioFile.exists() == false) {
throw new IOException("No audio file found");
}
//Initialization
//initialization
long startTime = System.currentTimeMillis();
File outMovieFile = recording.getFileBySuffix("mp4"); //final output
outMovieFile.delete();
File outMovieTmpFile = recording.getFileBySuffix("tmp.mp4"); //temporary output for joined slide movies
File slideMovieFile = File.createTempFile("tmpSlideMovie", ".mp4"); //slide movie created from png file
long slideMovieLength; //length of the slide movie
Index index = recording.index;
final ProgressMonitor progressMonitor = new ProgressMonitor(TTT.getRootComponent(), null, "building podcast from screenshots", 0, recording.getDuration()/1000*2); //time per frame is roughly the same for video and audio encoding
if (!batch) {
progressMonitor.setMillisToDecideToPopup(0);
progressMonitor.setMillisToPopup(0);
}
final Exec exec = new Exec();
File outMovieFile = File.createTempFile("tmpOutMovie",".mp4"); //final output
File outMovieTmpFile = File.createTempFile("tmpOutMovie",".mp4"); //temporary output for joined window movies
File windowMovieFile = File.createTempFile("tmpWindowMovie", ".mp4"); //window movie created from png file
File windowImageFile = File.createTempFile("tmpWindowImage", ".png"); //image of window movie
double frameLength = (double)1000/framesPerSec;
double outMovieLength = 0; //current length of outMovieFile
int vFrames; //number of video frames of window movie
int i = 0;
int j;
//Video encoding
for (int i = 0; i < index.size(); ++i) { //loop through all slides
System.out.println("Creating slide movie (" + (i+1) + "/" + index.size() + ")");
if (!batch) {
progressMonitor.setProgress(index.get(i).getTimestamp()/1000);
}
File imageFile = new File(recording.getDirectory() + recording.getFileBase() + ".html" + File.separator + "images" + File.separator + recording.getFileBase() + "." + ((i + 1) < 10 ? "0" : "") + (i + 1) + ".png");
if (imageFile.exists() == false) { //create png file from recording object if not available
recording.setTime(index.get(i).getTimestamp());
BufferedImage image = recording.graphicsContext.getScreenshotWithoutAnnotations();
ImageIO.write(image, "png", imageFile);
final ProgressMonitor progressMonitor = new ProgressMonitor(TTT.getRootComponent(), null, "building podcast video stream", 0, recording.messages.size()); //time per frame is roughly the same for video and audio encoding
final Exec exec = new Exec();
System.out.println("Building podcast video stream from messages");
recording.whiteOut();
while (i < recording.messages.size()) {
//draw all messages of the next frame
while (i < recording.messages.size() && recording.messages.get(i).getTimestamp() - outMovieLength <= frameLength) {
recording.deliverMessage(recording.messages.get(i++));
}
//get length of the slide movie
if (i == index.size() - 1) {
slideMovieLength = recording.getDuration()-index.get(i).getTimestamp();
//the number of video frames depends on the timestamp of the succeeding message
//if the next message occurs in x frames relative to the current frame, the next window lasts x-1 frames because nothing happens
if (i < recording.messages.size()) {
vFrames = (int)((recording.messages.get(i).getTimestamp() - outMovieLength) / frameLength);
} else {
slideMovieLength = index.get(i + 1).getTimestamp()-index.get(i).getTimestamp();
vFrames = (int)((recording.getDuration() - outMovieLength) / frameLength);
if (vFrames == 0) {
vFrames = 1;
}
}
outMovieLength += vFrames * frameLength;
if (!batch && i < recording.messages.size()) {
progressMonitor.setProgress(i);
}
slideMovieLength = Math.round((double) slideMovieLength / 1000);
//create the slide movie using ffmpeg
slideMovieFile.delete();
System.out.println(" Message (" + i + "/" + recording.messages.size() + ")");
//create window movie using ffmpeg
//write scaled window image
ImageIO.write(ImageCreator.getScaledInstance(recording.getGraphicsContext().getScreenshot(), resolutionWidth, resolutionHeight, RenderingHints.VALUE_INTERPOLATION_BICUBIC, true), "png", windowImageFile);
windowMovieFile.delete();
exec.createListenerStream();
j = exec.exec(new String[] {"ffmpeg", "-loop_input", "-r", "1", "-i", imageFile.getPath(), "-pix_fmt", "rgb24", "-vcodec", "mpeg4", "-vframes", String.valueOf(slideMovieLength), "-s", RESOLUTION,"-y", slideMovieFile.getPath()});
if (j != 0 || slideMovieFile.length() == 0) {
//error while creating the slide movie
System.out.println("Unable to create slide movie using ffmpeg:");
System.out.println(exec.getListenerStream());
slideMovieFile.delete();
j = exec.exec(new String[] {"ffmpeg", "-loop_input", "-r", String.valueOf(framesPerSec), "-i", windowImageFile.getPath(), "-pix_fmt", "rgb24", "-vcodec", "mpeg4", "-vframes", String.valueOf(vFrames), "-s", resolutionWidth + "x" + resolutionHeight,"-y", windowMovieFile.getPath()});
if (j != 0 || windowMovieFile.length() == 0) {
//error while creating window movie
windowMovieFile.delete();
outMovieFile.delete();
throw new IOException("unable to create slide movie using ffmpeg");
outMovieTmpFile.delete();
windowImageFile.delete();
System.out.println("Unable to create window movie using ffmpeg:");
System.out.println(exec.getListenerStream());
throw new IOException("unable to create window movie using ffmpeg");
}
if (!batch && progressMonitor.isCanceled()) {
//canceled by user
slideMovieFile.delete();
windowMovieFile.delete();
outMovieFile.delete();
outMovieTmpFile.delete();
windowImageFile.delete();
progressMonitor.close();
System.out.println("Canceled by user");
slideMovieFile.delete();
windowMovieFile.delete();
outMovieFile.delete();
outMovieTmpFile.delete();
return false;
}
//append the created slide movie (slideMovieFile) to the output file (outMovieFile) using MP4Box
//appending slideMovieFile to outMovieFile directly via "MP4Box -cat slideMovieFile.getPath() outMovieFile.getPath()" causes renaming problems in some cases. Thus outMovieTmpFile is used.
exec.createListenerStream();
j = exec.exec(new String[] { MP4BOX, "-cat", slideMovieFile.getPath(), outMovieFile.getPath(), "-out", outMovieTmpFile.getPath()});
if (j != 0 || outMovieTmpFile.length() == 0) {
//error while appending the slideMovie to the output file
System.out.println("Unable join slide movies using MP4Box:");
System.out.println(exec.getListenerStream());
outMovieFile.delete();
throw new IOException("unable join slide movies using MP4Box");
if (outMovieFile.length() == 0) {
//the first window movie can renamed directly to output movie.
//NOTE: MP4Box uses fps=1 for the container format when vFrames=1 whereby the container frame rate and codec frame rate can differ when using frameRate != 1. That causes a wrong synchronized video and audio stream
windowMovieFile.renameTo(outMovieTmpFile);
} else {
//append the created window movie (windowMovieFile) to the output movie (outMovieFile) using MP4Box
//NOTE: appending slideMovieFile to outMovieFile directly via "MP4Box -cat slideMovieFile.getPath() outMovieFile.getPath()" causes renaming problems in some cases. Thus outMovieTmpFile is used
exec.createListenerStream();
j = exec.exec(new String[] { MP4BOX, "-cat", windowMovieFile.getPath(), outMovieFile.getPath(), "-out", outMovieTmpFile.getPath()});
if (j != 0 || outMovieTmpFile.length() == 0) {
//error while appending the slideMovie to the output file
windowMovieFile.delete();
outMovieFile.delete();
outMovieTmpFile.delete();
windowImageFile.delete();
System.out.println("Unable join slide movies using MP4Box:");
System.out.println(exec.getListenerStream());
throw new IOException("unable join slide movies using MP4Box");
}
}
//replace outMovieFile by outMovieFileTmp
outMovieFile.delete();
if (i < index.size() - 1) {
if (i < recording.messages.size()) {
outMovieTmpFile.renameTo(outMovieFile);
}
}
slideMovieFile.delete(); //delete temporary file
windowMovieFile.delete();
outMovieFile.delete();
windowImageFile.delete();
//Audio encoding with ffmpeg. The audio stream must be converted via aac to achieve ipod compatibility
//audio encoding with ffmpeg. The audio stream must be converted via aac to achieve ipod compatibility
System.out.println("Adding audio stream to podcast");
Timer timer = null;
if (!batch) {
//The progress of the progress monitor is determined by the frame value ("frame= ") of the ffmpeg output
progressMonitor.setNote("adding audio stream to podcast");
//the progress of the progress monitor is determined by the frame value ("frame= ") of the ffmpeg output
final int nFrames = recording.getDuration()/1000;
progressMonitor.setNote("adding audio stream to podcast");
progressMonitor.setMaximum(nFrames);
progressMonitor.setProgress(0);
timer = new Timer(1000, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (progressMonitor.isCanceled()) {
exec.abort();
}
//Get the value after "frame=" of the last line
//get the value after "frame=" of the last line
String[] lines = exec.getListenerStream().toString().split("\n");
Scanner scanner = new Scanner(lines[lines.length-1]);
scanner.useDelimiter("[ ]+");
if (scanner.findInLine("frame=") != null && scanner.hasNextInt()){
int i = scanner.nextInt();
System.out.println("Adding audio stream on frame (" + i + "/" + nFrames + ")");
i+= nFrames;
if (i < progressMonitor.getMaximum()) {
progressMonitor.setProgress(i);
}
System.out.println(" Frame (" + i + "/" + nFrames + ")");
progressMonitor.setProgress(i);
}
exec.getListenerStream().reset();
}
......@@ -208,12 +239,17 @@ public class PodcastCreator {
timer.start();
}
exec.createListenerStream();
j = exec.exec(new String[] {ffmpegCmd, "-i",audioFile.getPath(),"-i",outMovieTmpFile.getPath(),"-acodec","libfaac","-ab" ,"128" ,"-ar","44100","-vcodec","copy","-y", outMovieFile.getPath()});
outMovieTmpFile.delete(); //delete temporary file
outMovieFile = recording.getFileBySuffix("mp4");
j = exec.exec(new String[] {ffmpegCmd,"-i",audioFile.getPath(),"-i",outMovieTmpFile.getPath(),"-acodec","libfaac","-ab" ,"128" ,"-ar","44100","-vcodec","copy","-y", outMovieFile.getPath()});
outMovieTmpFile.delete();
if (!batch) {
timer.stop();
if (progressMonitor.isCanceled()) {
//canceled by user
windowMovieFile.delete();
outMovieFile.delete();
outMovieTmpFile.delete();
windowImageFile.delete();
System.out.println("Canceled by user");
return false;
}
......@@ -230,4 +266,5 @@ public class PodcastCreator {
System.out.println("----------------------------------------------");
return true;
}
}
}
......@@ -142,22 +142,6 @@ public class PostProcessorPanel extends GradientPanel {
}
if (PodcastCreator.isCreationPossible(recording)) {
mp4CheckBox.setToolTipText("generate a mp4 podcast of this recording");
if (recording.getExistingFileBySuffix("html").isDirectory() == false) {
mp4CheckBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent event) {
try {
//if there is no html script it must be created before creating the podcast
if (mp4CheckBox.isSelected()) {
if (PostProcessorPanel.this.recording.getExistingFileBySuffix("html").isDirectory() == false) {
htmlCheckBox.setSelected(true);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
} else {
mp4CheckBox.setToolTipText("generating a mp4 podcast requires ffmpeg, mp4box, and an audio file");
mp4CheckBox.setSelected(false);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment