/*
* @(#)TrimDemoMain.java
*
* Copyright (c) 2012 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.iodemo;
import java.io.File;
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.ArrayList;
import org.monte.media.Buffer;
import org.monte.media.BufferFlag;
import org.monte.media.Codec;
import org.monte.media.Format;
import org.monte.media.FormatFormatter;
import org.monte.media.FormatKeys;
import static org.monte.media.FormatKeys.*;
import org.monte.media.MovieReader;
import org.monte.media.MovieWriter;
import org.monte.media.Registry;
import org.monte.media.converter.AdjustTimeCodec;
import org.monte.media.converter.CodecChain;
import org.monte.media.converter.TrimTimeCodec;
import org.monte.media.math.Rational;
/**
* Demonstrates how to trim a movie file without re-encoding the entire media
* data. <p> This demo is more complex than {@code ConcatDemoMain}, because we
* need to re-encode the first video frame, if the movie is cut at a
* non-keyframe.
*
* @author Werner Randelshofer
* @version $Id: TrimDemoMain.java 298 2013-01-03 07:39:43Z werner $
*/
public class TrimDemoMain {
/**
* Main function. <p> Takes one output file and one or more input files as
* arguments. Concatenates all input files into the output file.
* <pre>
* TrimDemo [-o outputfile] [-i inputfile ...] [-s rational] [-e rational]
* </pre>
*
* @param args the command line arguments
*/
public static void main(String[] args) {
// Use hardcoded arguments for debugging
/*
args = new String[]{//
"-i",
"/Users/werni/Movies/Poker.avi", //
"-o",
"/Users/werni/Movies/PokerTrim.avi",//
"-ss", "00:00:01",//
"-t", "00:05:00",//
};
*/
// Parse arguments
File outfile = null;
ArrayList<File> infiles = new ArrayList<File>();
Rational start = null;
Rational end = null;
String startString = null, durationString = null;
try {
char arg = ' ';
for (int i = 0; i < args.length; i++) {
if (args[i].length() > 1 && args[i].charAt(0) == '-') {
arg = args[i].charAt(1);
if (!args[i].matches("-i|-o|-ss|-t")) {
throw new IllegalArgumentException("error: illegal option: " + args[i]);
}
} else {
switch (arg) {
case 'o':
if (outfile != null) {
throw new IllegalArgumentException("error: only one outputfile allowed");
}
outfile = new File(args[i]);
break;
case 'i':
infiles.add(new File(args[i]));
break;
case 's':
startString = args[i];
break;
case 't':
durationString = args[i];
break;
default:
throw new IllegalArgumentException("error: illegal option: " + args[i]);
}
}
}
if (outfile == null) {
if (infiles.isEmpty()) {
throw new IllegalArgumentException("error: no inputfiles specified");
}
info(infiles);
return;
}
/*
if (outfile.exists()) {
throw new IllegalArgumentException("error: outputfile exists: " + outfile);
}*/
for (File f : infiles) {
if (!f.exists()) {
throw new IllegalArgumentException("error: inputfile does not exist: " + f);
}
}
{ // Parse the start time and duration values
// This may require reading the first movie file.
MovieReader[] r = new MovieReader[1];
try {
start = parseTime(startString, infiles, r);
Rational duration = parseTime(durationString, infiles, r);
if (duration != null) {
end = (start == null) ? duration : start.add(duration);
}
} catch (IOException ex) {
ex.printStackTrace();
} finally {
if (r[0] != null) {
try {
r[0].close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
if (start != null || end != null) {
System.out.println("Trimming input files at seconds. start:" + (start == null ? "start of movie" : start.toDescriptiveString()) + ", end:" + (end == null ? "end of movie" : end.toDescriptiveString()));
}
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
System.err.println("usage: java -jar TrimDemo.jar [-o outputfile] [-i inputfile ...] [-ss startTime] [-t duration]");
System.err.println(" -o Output file. Filename. e.g. movie.avi");
System.err.println(" -i Input file. More than one can be specified. e.g. movie1.avi movie2.avi");
System.err.println(" -ss Start time. Double, rational, or hh:mm:ss.frame. e.g. 1.5 or 3/2 or 00:00:01.15");
System.err.println(" -t Duration. Double, rational, or hh:mm:ss.frame");
String version = TrimDemoMain.class.getPackage().getImplementationVersion();
System.err.println("TrimDemo " + (version == null ? "" : version + " ") + "(c) Werner Randelshofer");
System.exit(10);
}
try {
concat(outfile, infiles, start, end);
} catch (Throwable e) {
e.printStackTrace();
System.err.println("error: " + e.getMessage());
}
}
/**
* Prints an info about each input file.
*/
private static void info(ArrayList<File> infiles) {
for (File f : infiles) {
System.out.println("Movie: " + f);
if (!f.exists()) {
System.out.println(" File does not exist.");
continue;
}
MovieReader in = Registry.getInstance().getReader(f);
try {
info(in);
} catch (IOException ex) {
ex.printStackTrace();
} finally {
try {
in.close();
} catch (IOException ex) {
//
}
}
System.out.println();
}
}
/**
* Prints an info about an output file.
*/
private static void info(MovieWriter w) throws IOException {
System.out.println(" Format: " + FormatFormatter.toString(w.getFileFormat()));
//System.out.println(" Duration: " + in.getDuration().toDescriptiveString() + " seconds");
for (int t = 0; t < w.getTrackCount(); t++) {
System.out.println(" Track " + t);
System.out.println(" Format: " + FormatFormatter.toString(w.getFormat(t)));
System.out.println(" Duration: " + w.getDuration(t).toDescriptiveString() + " seconds");
}
}
/**
* Prints an info about an input file.
*/
private static void info(MovieReader in) throws IOException {
System.out.println(" Format: " + FormatFormatter.toString(in.getFileFormat()));
System.out.println(" Duration: " + in.getDuration().toDescriptiveString() + " seconds");
for (int t = 0; t < in.getTrackCount(); t++) {
System.out.println(" Track " + t);
System.out.println(" Format: " + FormatFormatter.toString(in.getFormat(t)));
System.out.println(" Duration: " + in.getDuration(t).toDescriptiveString() + " seconds");
System.out.println(" Chunk Count: " + in.getChunkCount(t));
}
}
private static void concat(File outfile, ArrayList<File> infiles, Rational startTime, Rational endTime) throws IOException {
MovieWriter out = Registry.getInstance().getWriter(outfile);
if (out == null) {
throw new IOException("output file format not supported for: " + outfile);
}
try {
// -------------------
// Analyze input files
// -------------------
// For each input track in each input file, find a matching output track.
// If no matching output track has been found, create a new output track.
int[][] matchingT = new int[infiles.size()][0];
for (int i = 0, imax = infiles.size(); i < imax; i++) {
File infile = infiles.get(i);
MovieReader in = Registry.getInstance().getReader(infile);
if (in == null) {
throw new IOException("input file format not supported for: " + infile);
}
System.out.println(infile);
info(in);
try {
matchingT[i] = new int[in.getTrackCount()];
for (int t = 0, jmax = in.getTrackCount(); t < jmax; t++) {
matchingT[i][t] = -1;
for (int outputTrack = 0, nOutputTracks = out.getTrackCount(); outputTrack < nOutputTracks; outputTrack++) {
if (in.getFormat(t).matches(out.getFormat(outputTrack))) {
// if a movie has multiple tracks with the same format,
// we assign them to multiple output tracks
for (int tt = 0; tt < t; tt++) {
if (matchingT[i][tt] == outputTrack) {
continue;
}
}
matchingT[i][t] = outputTrack;
break;
}
}
if (matchingT[i][t] == -1) {
matchingT[i][t] = out.getTrackCount();
out.addTrack(in.getFormat(t));
}
}
} finally {
in.close();
}
}
// -----------------
// Set up the codecs
// -----------------
int trackCount = out.getTrackCount();
Codec[] ensureFirstFrameIsKeyframe = new Codec[trackCount];
Codec[] passThrough = new Codec[trackCount];
AdjustTimeCodec[] adjustTime = new AdjustTimeCodec[trackCount];
for (int t = 0; t < ensureFirstFrameIsKeyframe.length; t++) {
Codec decode = Registry.getInstance().getDecoder(out.getFormat(t));
decode.setOutputFormat(decode.getOutputFormats(out.getFormat(t))[0]);
Codec encode = Registry.getInstance().getCodec(decode.getOutputFormat(), out.getFormat(t));
adjustTime[t] = new AdjustTimeCodec();
TrimTimeCodec trimTime = new TrimTimeCodec();
trimTime.setStartTime(startTime);
trimTime.setEndTime(endTime);
ensureFirstFrameIsKeyframe[t] = CodecChain.createCodecChain(adjustTime[t], decode, trimTime, encode);
passThrough[t] = CodecChain.createCodecChain(adjustTime[t], trimTime);
}
// For each input file, write all media samples into the matching
// output tracks.
Buffer inBuf = new Buffer();
Buffer outBuf = new Buffer();
int tracksNeeded = (1 << trackCount) - 1;
int tracksDone = 0;
// -----------------------
// Process the input files
// -----------------------
for (int i = 0, imax = infiles.size(); i < imax; i++) {
File infile = infiles.get(i);
MovieReader in = Registry.getInstance().getReader(infile);
if (in == null) {
throw new IOException("input file format not supported for: " + infile);
}
// Speed up 1: Skip to start time on the first movie file
if (i == 0 && startTime != null) {
in.setMovieReadTime(startTime);
for (int t = 0; t < in.getTrackCount(); t++) {
int outTrack = matchingT[i][t];
adjustTime[outTrack].setMediaTime(in.getReadTime(t));
}
}
try {
for (int t = in.nextTrack(); t != -1; t = in.nextTrack()) {
int outTrack = matchingT[i][t];
in.read(t, inBuf);
int state;
if (out.isEmpty(outTrack)) {
state = ensureFirstFrameIsKeyframe[outTrack].process(inBuf, outBuf);
} else {
state = passThrough[outTrack].process(inBuf, outBuf);
}
// Speed up 2: Stop if all tracks are done
if (endTime != null && outBuf.timeStamp.compareTo(endTime) > 0) {
tracksDone |= 1 << outTrack;
if (tracksDone == tracksNeeded) {
break;
}
}
out.write(outTrack, outBuf);
}
} finally {
in.close();
}
}
System.out.println(outfile);
info(out);
} finally {
out.close();
}
}
private static Rational parseTime(String str, ArrayList<File> infiles, MovieReader[] r) throws IOException {
if (str != null) {
try {
return Rational.valueOf(str);
} catch (NumberFormatException e) {
if (r[0] == null && !infiles.isEmpty()) {
r[0] = Registry.getInstance().getReader(infiles.get(0));
}
if (r[0] != null) {
int t = r[0].findTrack(0, new Format(MediaTypeKey, MediaType.VIDEO));
if (t != -1) {
Format f = r[0].getFormat(t);
Rational frameRate = f.get(FrameRateKey);
long seconds = 0, frame = 0;
StreamTokenizer tt = new StreamTokenizer(new StringReader(str));
tt.resetSyntax();
tt.wordChars('0', '9');
if (tt.nextToken() != StreamTokenizer.TT_WORD) {
throw new NumberFormatException("hh:mm:ss.frame, hours missing: " + str);
}
seconds += Long.valueOf(tt.sval) * 3600;
if (tt.nextToken() != ':') {
throw new NumberFormatException("hh:mm:ss.frame, 1st ':' missing: " + str);
}
if (tt.nextToken() != StreamTokenizer.TT_WORD) {
throw new NumberFormatException("hh:mm:ss.frame, minutes missing: " + str);
}
seconds += Long.valueOf(tt.sval) * 60;
if (tt.nextToken() != ':') {
throw new NumberFormatException("hh:mm:ss.frame, 2nd ':' missing: " + str);
}
if (tt.nextToken() != StreamTokenizer.TT_WORD) {
throw new NumberFormatException("hh:mm:ss, seconds missing: " + str);
}
seconds += Long.valueOf(tt.sval);
if (tt.nextToken() == '.') {// frame number is optional
if (tt.nextToken() != StreamTokenizer.TT_WORD) {
throw new NumberFormatException("hh:mm:ss.frame, frames missing: " + str);
}
frame += Long.valueOf(tt.sval);
}
return new Rational(seconds).add(new Rational(frame).divide(frameRate));
}
}
}
}
return null;
}
}