/*******************************************************************************
* Copyright (c) 2008, 2010 Xuggle Inc. All rights reserved.
*
* This file is part of Xuggle-Xuggler-Main.
*
* Xuggle-Xuggler-Main is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Xuggle-Xuggler-Main is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Xuggle-Xuggler-Main. If not, see <http://www.gnu.org/licenses/>.
*******************************************************************************/
package com.xuggle.xuggler;
import java.util.List;
import org.apache.commons.cli.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xuggle.xuggler.Global;
import com.xuggle.xuggler.IAudioResampler;
import com.xuggle.xuggler.IAudioSamples;
import com.xuggle.xuggler.IAudioSamples.Format;
import com.xuggle.xuggler.ICodec;
import com.xuggle.xuggler.IContainer;
import com.xuggle.xuggler.IContainerFormat;
import com.xuggle.xuggler.IVideoPicture;
import com.xuggle.xuggler.IPacket;
import com.xuggle.xuggler.IRational;
import com.xuggle.xuggler.IStream;
import com.xuggle.xuggler.IStreamCoder;
import com.xuggle.xuggler.IVideoResampler;
import com.xuggle.xuggler.io.URLProtocolManager;
/**
* An example class that shows how to use the Xuggler library to open, decode,
* re-sample, encode and write media files.
*
* <p>
*
* This class is called by the {@link Xuggler} class to do all the heavy
* lifting. It is meant as a Demonstration class and implements a small subset
* of the functionality that the (much more full-featured) <code>ffmpeg</code>
* command line tool implements. It is really meant to show people how the
* Xuggler library is used from java.
*
* </p>
* <p>
*
* Read <a href="{@docRoot}/src-html/com/xuggle/xuggler/Converter.html">the
* Converter.java source</a> for a good example of a class exercising this
* library.
*
* </p>
* <p>
*
* <strong>It is not our intent to replicate all features in the
* <code>ffmpeg</code> command line tool in this class.</strong>
*
* </p>
* <p>
*
* If you are just trying to convert pre-existing media files from one format to
* another with a batch-processing program we strongly recommend you use the
* <code>ffmpeg</code> command-line tool to do it. Look, here's even the <a
* href="http://ffmpeg.org/ffmpeg-doc.html">documentation</a> for that program.
*
* </p>
* <p>
*
* If on the other hand you need to programatically decide when and how you do
* video processing, or process only parts of files, or do transcoding live
* within a Java server without calling out to another process, then by all
* means use Xuggler and use this class as an example of how to do conversion.
* But please recognize you will likely need to implement code specific to your
* application. <strong>This class is no substitute for actually understanding
* the how to use the Xuggler API within your specific use-case</strong>
*
* </p>
* <p>
*
* And if you haven't gotten the impression, please stop asking us to support
* <code>ffmpeg</code> options like "-re" in this class. This class is only
* meant as a teaching-aide for the underlying Xuggler API.
*
* </p>
* <p>
*
* Instead implement your own Java class based off of this that does real-time
* playback yourself. Really. Please. We'd appreciate it very much.
*
* </p>
* <p>
* Now, all that said, here's how to create a main class that uses this
* Converter class:
* </p>
*
* <pre>
* public static void main(String[] args)
* {
* Converter converter = new Converter();
* try
* {
* // first define options
* Options options = converter.defineOptions();
* // And then parse them.
* CommandLine cmdLine = converter.parseOptions(options, args);
* // Finally, run the converter.
* converter.run(cmdLine);
* }
* catch (Exception exception)
* {
* System.err.printf("Error: %s\n", exception.getMessage());
* }
* }
* </pre>
*
* <p>
*
* Pass "--help" to your main class as the argument to see the list of
* accepted options.
*
* </p>
*
* @author aclarke
*
*/
public class Converter
{
static
{
// this forces the FFMPEG io library to be loaded which means we can bypass
// FFMPEG's file io if needed
URLProtocolManager.getManager();
}
/**
* Create a new Converter object.
*/
public Converter()
{
}
private final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* A container we'll use to read data from.
*/
private IContainer mIContainer = null;
/**
* A container we'll use to write data from.
*/
private IContainer mOContainer = null;
/**
* A set of {@link IStream} values for each stream in the input
* {@link IContainer}.
*/
private IStream[] mIStreams = null;
/**
* A set of {@link IStreamCoder} objects we'll use to decode audio and video.
*/
private IStreamCoder[] mICoders = null;
/**
* A set of {@link IStream} objects for each stream we'll output to the output
* {@link IContainer}.
*/
private IStream[] mOStreams = null;
/**
* A set of {@link IStreamCoder} objects we'll use to encode audio and video.
*/
private IStreamCoder[] mOCoders = null;
/**
* A set of {@link IVideoPicture} objects that we'll use to hold decoded video
* data.
*/
private IVideoPicture[] mIVideoPictures = null;
/**
* A set of {@link IVideoPicture} objects we'll use to hold
* potentially-resampled video data before we encode it.
*/
private IVideoPicture[] mOVideoPictures = null;
/**
* A set of {@link IAudioSamples} objects we'll use to hold decoded audio
* data.
*/
private IAudioSamples[] mISamples = null;
/**
* A set of {@link IAudioSamples} objects we'll use to hold
* potentially-resampled audio data before we encode it.
*/
private IAudioSamples[] mOSamples = null;
/**
* A set of {@link IAudioResampler} objects (one for each stream) we'll use to
* resample audio if needed.
*/
private IAudioResampler[] mASamplers = null;
/**
* A set of {@link IVideoResampler} objects (one for each stream) we'll use to
* resample video if needed.
*/
private IVideoResampler[] mVSamplers = null;
/**
* Should we convert audio
*/
private boolean mHasAudio = true;
/**
* Should we convert video
*/
private boolean mHasVideo = true;
/**
* Should we force an interleaving of the output
*/
private final boolean mForceInterleave = true;
/**
* Should we attempt to encode 'in real time'
*/
private boolean mRealTimeEncoder;
private Long mStartClockTime;
private Long mStartStreamTime;
/**
* Define all the command line options this program can take.
*
* @return The set of accepted options.
*/
public Options defineOptions()
{
Options options = new Options();
Option help = new Option("help", "print this message");
/*
* Output container options.
*/
OptionBuilder.withArgName("container-format");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("output container format to use (e.g. \"mov\")");
Option containerFormat = OptionBuilder.create("containerformat");
OptionBuilder.withArgName("icontainer-format");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("input container format to use (e.g. \"mov\")");
Option icontainerFormat = OptionBuilder.create("icontainerformat");
OptionBuilder.withArgName("file");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("container option presets file");
Option cpreset = OptionBuilder.create("cpreset");
/*
* Audio options
*/
OptionBuilder.hasArg(false);
OptionBuilder.withDescription("no audio");
Option ano = OptionBuilder.create("ano");
OptionBuilder.withArgName("file");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("audio option presets file");
Option apreset = OptionBuilder.create("apreset");
OptionBuilder.withArgName("codec");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("audio codec to encode with (e.g. \"libmp3lame\")");
Option acodec = OptionBuilder.create("acodec");
OptionBuilder.withArgName("icodec");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("input audio codec (e.g. \"libmp3lame\")");
Option iacodec = OptionBuilder.create("iacodec");
OptionBuilder.withArgName("sample-rate");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("audio sample rate to (up/down) encode with (in hz) (e.g. \"22050\")");
Option asamplerate = OptionBuilder.create("asamplerate");
OptionBuilder.withArgName("isample-rate");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("input audio sample rate to (up/down) encode with (in hz) (e.g. \"22050\")");
Option iasamplerate = OptionBuilder.create("iasamplerate");
OptionBuilder.withArgName("channels");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("number of audio channels (1 or 2) to encode with (e.g. \"2\")");
Option achannels = OptionBuilder.create("achannels");
OptionBuilder.withArgName("ichannels");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("input number of audio channels (1 or 2)");
Option iachannels = OptionBuilder.create("iachannels");
OptionBuilder.withArgName("abit-rate");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("bit rate to encode audio with (in bps) (e.g. \"60000\")");
Option abitrate = OptionBuilder.create("abitrate");
OptionBuilder.withArgName("stream");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("if multiple audio streams of a given type, this is the stream you want to output");
Option astream = OptionBuilder.create("astream");
OptionBuilder.withArgName("quality");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("quality setting to use for audio. 0 means same as source; higher numbers are (perversely) lower quality. Defaults to 0.");
Option aquality = OptionBuilder.create("aquality");
/*
* Video options
*/
OptionBuilder.hasArg(false);
OptionBuilder.withDescription("no video");
Option vno = OptionBuilder.create("vno");
OptionBuilder.withArgName("file");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("video option presets file");
Option vpreset = OptionBuilder.create("vpreset");
OptionBuilder.withArgName("codec");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("video codec to encode with (e.g. \"mpeg4\")");
Option vcodec = OptionBuilder.create("vcodec");
OptionBuilder.withArgName("factor");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("scaling factor to scale output video by (e.g. \"0.75\")");
Option vscaleFactor = OptionBuilder.create("vscalefactor");
OptionBuilder.withArgName("vbitrate");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("bit rate to encode video with (in bps) (e.g. \"60000\")");
Option vbitrate = OptionBuilder.create("vbitrate");
OptionBuilder.withArgName("vbitratetolerance");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("bit rate tolerance the bitstream is allowed to diverge from the reference (in bits) (e.g. \"1200000\")");
Option vbitratetolerance = OptionBuilder.create("vbitratetolerance");
OptionBuilder.withArgName("stream");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("if multiple video streams of a given type, this is the stream you want to output");
Option vstream = OptionBuilder.create("vstream");
OptionBuilder.withArgName("quality");
OptionBuilder.hasArg(true);
OptionBuilder
.withDescription("quality setting to use for video. 0 means same as source; higher numbers are (perversely) lower quality. Defaults to 0. If set, then bitrate flags are ignored.");
Option vquality = OptionBuilder.create("vquality");
OptionBuilder.withArgName("realtime");
OptionBuilder.hasArg(false);
OptionBuilder.withDescription("attempt to encode frames at the realtime rate -- i.e. it encodes when the picture should play");
Option realtime = OptionBuilder.create("realtime");
options.addOption(help);
options.addOption(containerFormat);
options.addOption(cpreset);
options.addOption(ano);
options.addOption(apreset);
options.addOption(acodec);
options.addOption(asamplerate);
options.addOption(achannels);
options.addOption(abitrate);
options.addOption(astream);
options.addOption(aquality);
options.addOption(vno);
options.addOption(vpreset);
options.addOption(vcodec);
options.addOption(vscaleFactor);
options.addOption(vbitrate);
options.addOption(vbitratetolerance);
options.addOption(vstream);
options.addOption(vquality);
options.addOption(icontainerFormat);
options.addOption(iacodec);
options.addOption(iachannels);
options.addOption(iasamplerate);
options.addOption(realtime);
return options;
}
/**
* Given a set of arguments passed into this object, return back a parsed
* command line.
*
* @param opt
* A set of options as defined by {@link #defineOptions()}.
* @param args
* A set of command line arguments passed into this class.
* @return A parsed command line.
* @throws ParseException
* If there is an error in the command line.
*/
public CommandLine parseOptions(Options opt, String[] args)
throws ParseException
{
CommandLine cmdLine = null;
CommandLineParser parser = new GnuParser();
cmdLine = parser.parse(opt, args);
if (cmdLine.hasOption("help"))
{
HelpFormatter help = new HelpFormatter();
help.printHelp("Xuggler [options] input_url output_url", opt);
System.exit(1);
}
// Make sure we have only two left over args
if (cmdLine.getArgs().length != 2)
throw new ParseException("missing input or output url");
return cmdLine;
}
/**
* Get an integer value from a command line argument.
*
* @param cmdLine
* A parsed command line (as returned from
* {@link #parseOptions(Options, String[])}
* @param key
* The key for an option you want.
* @param defaultVal
* The default value you want set if the key is not present in
* cmdLine.
* @return The value for the key in the cmdLine, or defaultVal if it's not
* there.
*/
private int getIntOptionValue(CommandLine cmdLine, String key, int defaultVal)
{
int retval = defaultVal;
String optValue = cmdLine.getOptionValue(key);
if (optValue != null)
{
try
{
retval = Integer.parseInt(optValue);
}
catch (Exception ex)
{
log
.warn(
"Option \"{}\" value \"{}\" cannot be converted to integer; using {} instead",
new Object[]
{
key, optValue, defaultVal
});
}
}
return retval;
}
/**
* Get a double value from a command line argument.
*
* @param cmdLine
* A parsed command line (as returned from
* {@link #parseOptions(Options, String[])}
* @param key
* The key for an option you want.
* @param defaultVal
* The default value you want set if the key is not present in
* cmdLine.
* @return The value for the key in the cmdLine, or defaultVal if it's not
* there.
*/
private double getDoubleOptionValue(CommandLine cmdLine, String key,
double defaultVal)
{
double retval = defaultVal;
String optValue = cmdLine.getOptionValue(key);
if (optValue != null)
{
try
{
retval = Double.parseDouble(optValue);
}
catch (Exception ex)
{
log
.warn(
"Option \"{}\" value \"{}\" cannot be converted to double; using {} instead",
new Object[]
{
key, optValue, defaultVal
});
}
}
return retval;
}
/**
* Open an initialize all Xuggler objects needed to encode and decode a video
* file.
*
* @param cmdLine
* A command line (as returned from
* {@link #parseOptions(Options, String[])}) that specifies what
* files we want to process and how to process them.
* @return Number of streams in the input file, or <= 0 on error.
*/
int setupStreams(CommandLine cmdLine)
{
String inputURL = cmdLine.getArgs()[0];
String outputURL = cmdLine.getArgs()[1];
mHasAudio = !cmdLine.hasOption("ano");
mHasVideo = !cmdLine.hasOption("vno");
mRealTimeEncoder = cmdLine.hasOption("realtime");
String acodec = cmdLine.getOptionValue("acodec");
String vcodec = cmdLine.getOptionValue("vcodec");
String containerFormat = cmdLine.getOptionValue("containerformat");
int astream = getIntOptionValue(cmdLine, "astream", -1);
int aquality = getIntOptionValue(cmdLine, "aquality", 0);
int sampleRate = getIntOptionValue(cmdLine, "asamplerate", 0);
int channels = getIntOptionValue(cmdLine, "achannels", 0);
int abitrate = getIntOptionValue(cmdLine, "abitrate", 0);
int vbitrate = getIntOptionValue(cmdLine, "vbitrate", 0);
int vbitratetolerance = getIntOptionValue(cmdLine, "vbitratetolerance", 0);
int vquality = getIntOptionValue(cmdLine, "vquality", -1);
int vstream = getIntOptionValue(cmdLine, "vstream", -1);
double vscaleFactor = getDoubleOptionValue(cmdLine, "vscalefactor", 1.0);
String icontainerFormat = cmdLine.getOptionValue("icontainerformat");
String iacodec = cmdLine.getOptionValue("iacodec");
int isampleRate = getIntOptionValue(cmdLine, "iasamplerate", 0);
int ichannels = getIntOptionValue(cmdLine, "iachannels", 0);
// Should have everything now!
int retval = 0;
/**
* Create one container for input, and one for output.
*/
mIContainer = IContainer.make();
mOContainer = IContainer.make();
String cpreset = cmdLine.getOptionValue("cpreset");
if (cpreset != null)
Configuration.configure(cpreset, mOContainer);
IContainerFormat iFmt = null;
IContainerFormat oFmt = null;
// override input format
if (icontainerFormat != null)
{
iFmt = IContainerFormat.make();
/**
* Try to find an output format based on what the user specified, or
* failing that, based on the outputURL (e.g. if it ends in .flv, we'll
* guess FLV).
*/
retval = iFmt.setInputFormat(icontainerFormat);
if (retval < 0)
throw new RuntimeException("could not find input container format: "
+ icontainerFormat);
}
// override the input codec
if (iacodec != null)
{
ICodec codec = null;
/**
* Looks like they did specify one; let's look it up by name.
*/
codec = ICodec.findDecodingCodecByName(iacodec);
if (codec == null || codec.getType() != ICodec.Type.CODEC_TYPE_AUDIO)
throw new RuntimeException("could not find decoder: " + iacodec);
/**
* Now, tell the output stream coder that it's to use that codec.
*/
mIContainer.setForcedAudioCodec(codec.getID());
}
/**
* Open the input container for Reading.
*/
IMetaData parameters = IMetaData.make();
if (isampleRate > 0)
parameters.setValue("sample_rate", ""+isampleRate);
if (ichannels > 0)
parameters.setValue("channels", ""+ichannels);
IMetaData rejectParameters = IMetaData.make();
retval = mIContainer.open(inputURL, IContainer.Type.READ, iFmt, false, true,
parameters, rejectParameters);
if (retval < 0)
throw new RuntimeException("could not open url: " + inputURL);
if (rejectParameters.getNumKeys() > 0)
throw new RuntimeException("some parameters were rejected: " + rejectParameters);
/**
* If the user EXPLICITLY asked for a output container format, we'll try to
* honor their request here.
*/
if (containerFormat != null)
{
oFmt = IContainerFormat.make();
/**
* Try to find an output format based on what the user specified, or
* failing that, based on the outputURL (e.g. if it ends in .flv, we'll
* guess FLV).
*/
retval = oFmt.setOutputFormat(containerFormat, outputURL, null);
if (retval < 0)
throw new RuntimeException("could not find output container format: "
+ containerFormat);
}
/**
* Open the output container for writing. If oFmt is null, we are telling
* Xuggler to guess the output container format based on the outputURL.
*/
retval = mOContainer.open(outputURL, IContainer.Type.WRITE, oFmt);
if (retval < 0)
throw new RuntimeException("could not open output url: " + outputURL);
/**
* Find out how many streams are there in the input container? For example,
* most FLV files will have 2 -- 1 audio stream and 1 video stream.
*/
int numStreams = mIContainer.getNumStreams();
if (numStreams <= 0)
throw new RuntimeException("not streams in input url: " + inputURL);
/**
* Here we create IStream, IStreamCoders and other objects for each input
* stream.
*
* We make parallel objects for each output stream as well.
*/
mIStreams = new IStream[numStreams];
mICoders = new IStreamCoder[numStreams];
mOStreams = new IStream[numStreams];
mOCoders = new IStreamCoder[numStreams];
mASamplers = new IAudioResampler[numStreams];
mVSamplers = new IVideoResampler[numStreams];
mIVideoPictures = new IVideoPicture[numStreams];
mOVideoPictures = new IVideoPicture[numStreams];
mISamples = new IAudioSamples[numStreams];
mOSamples = new IAudioSamples[numStreams];
/**
* Now let's go through the input streams one by one and explicitly set up
* our contexts.
*/
for (int i = 0; i < numStreams; i++)
{
/**
* Get the IStream for this input stream.
*/
IStream is = mIContainer.getStream(i);
/**
* And get the input stream coder. Xuggler will set up all sorts of
* defaults on this StreamCoder for you (such as the audio sample rate)
* when you open it.
*
* You can create IStreamCoders yourself using
* IStreamCoder#make(IStreamCoder.Direction), but then you have to set all
* parameters yourself.
*/
IStreamCoder ic = is.getStreamCoder();
/**
* Find out what Codec Xuggler guessed the input stream was encoded with.
*/
ICodec.Type cType = ic.getCodecType();
mIStreams[i] = is;
mICoders[i] = ic;
mOStreams[i] = null;
mOCoders[i] = null;
mASamplers[i] = null;
mVSamplers[i] = null;
mIVideoPictures[i] = null;
mOVideoPictures[i] = null;
mISamples[i] = null;
mOSamples[i] = null;
if (cType == ICodec.Type.CODEC_TYPE_AUDIO && mHasAudio
&& (astream == -1 || astream == i))
{
/**
* First, did the user specify an audio codec?
*/
ICodec codec = null;
if (acodec != null)
{
/**
* Looks like they did specify one; let's look it up by name.
*/
codec = ICodec.findEncodingCodecByName(acodec);
if (codec == null || codec.getType() != cType)
throw new RuntimeException("could not find encoder: " + acodec);
}
else
{
/**
* Looks like the user didn't specify an output coder for audio.
*
* So we ask Xuggler to guess an appropriate output coded based on the
* URL, container format, and that it's audio.
*/
codec = ICodec.guessEncodingCodec(oFmt, null, outputURL, null,
cType);
if (codec == null)
throw new RuntimeException("could not guess " + cType
+ " encoder for: " + outputURL);
}
/**
* So it looks like this stream as an audio stream. Now we add an audio
* stream to the output container that we will use to encode our
* resampled audio.
*/
IStream os = mOContainer.addNewStream(codec);
/**
* And we ask the IStream for an appropriately configured IStreamCoder
* for output.
*
* Unfortunately you still need to specify a lot of things for
* outputting (because we can't really guess what you want to encode
* as).
*/
IStreamCoder oc = os.getStreamCoder();
mOStreams[i] = os;
mOCoders[i] = oc;
/**
* Now let's see if the codec can support the input sample format; if not
* we pick the last sample format the codec supports.
*/
Format preferredFormat = ic.getSampleFormat();
List<Format> formats = codec.getSupportedAudioSampleFormats();
for(Format format : formats) {
oc.setSampleFormat(format);
if (format == preferredFormat)
break;
}
final String apreset = cmdLine.getOptionValue("apreset");
if (apreset != null)
Configuration.configure(apreset, oc);
/**
* In general a IStreamCoder encoding audio needs to know: 1) A ICodec
* to use. 2) The sample rate and number of channels of the audio. Most
* everything else can be defaulted.
*/
/**
* If the user didn't specify a sample rate to encode as, then just use
* the same sample rate as the input.
*/
if (sampleRate == 0)
sampleRate = ic.getSampleRate();
oc.setSampleRate(sampleRate);
/**
* If the user didn't specify a bit rate to encode as, then just use the
* same bit as the input.
*/
if (abitrate == 0)
abitrate = ic.getBitRate();
if (abitrate == 0)
// some containers don't give a bit-rate
abitrate = 64000;
oc.setBitRate(abitrate);
/**
* If the user didn't specify the number of channels to encode audio as,
* just assume we're keeping the same number of channels.
*/
if (channels == 0)
channels = ic.getChannels();
oc.setChannels(channels);
/**
* And set the quality (which defaults to 0, or highest, if the user
* doesn't tell us one).
*/
oc.setGlobalQuality(aquality);
/**
* Now check if our output channels or sample rate differ from our input
* channels or sample rate.
*
* If they do, we're going to need to resample the input audio to be in
* the right format to output.
*/
if (oc.getChannels() != ic.getChannels()
|| oc.getSampleRate() != ic.getSampleRate()
|| oc.getSampleFormat() != ic.getSampleFormat())
{
/**
* Create an audio resampler to do that job.
*/
mASamplers[i] = IAudioResampler.make(oc.getChannels(), ic
.getChannels(), oc.getSampleRate(), ic.getSampleRate(),
oc.getSampleFormat(), ic.getSampleFormat());
if (mASamplers[i] == null)
{
throw new RuntimeException(
"could not open audio resampler for stream: " + i);
}
}
else
{
mASamplers[i] = null;
}
/**
* Finally, create some buffers for the input and output audio
* themselves.
*
* We'll use these repeated during the #run(CommandLine) method.
*/
mISamples[i] = IAudioSamples.make(1024, ic.getChannels(), ic.getSampleFormat());
mOSamples[i] = IAudioSamples.make(1024, oc.getChannels(), oc.getSampleFormat());
}
else if (cType == ICodec.Type.CODEC_TYPE_VIDEO && mHasVideo
&& (vstream == -1 || vstream == i))
{
/**
* If you're reading these commends, this does the same thing as the
* above branch, only for video. I'm going to assume you read those
* comments and will only document something substantially different
* here.
*/
ICodec codec = null;
if (vcodec != null)
{
codec = ICodec.findEncodingCodecByName(vcodec);
if (codec == null || codec.getType() != cType)
throw new RuntimeException("could not find encoder: " + vcodec);
}
else
{
codec = ICodec.guessEncodingCodec(oFmt, null, outputURL, null,
cType);
if (codec == null)
throw new RuntimeException("could not guess " + cType
+ " encoder for: " + outputURL);
}
final IStream os = mOContainer.addNewStream(codec);
final IStreamCoder oc = os.getStreamCoder();
mOStreams[i] = os;
mOCoders[i] = oc;
// Set options AFTER selecting codec
final String vpreset = cmdLine.getOptionValue("vpreset");
if (vpreset != null)
Configuration.configure(vpreset, oc);
/**
* In general a IStreamCoder encoding video needs to know: 1) A ICodec
* to use. 2) The Width and Height of the Video 3) The pixel format
* (e.g. IPixelFormat.Type#YUV420P) of the video data. Most everything
* else can be defaulted.
*/
if (vbitrate == 0)
vbitrate = ic.getBitRate();
if (vbitrate == 0)
vbitrate = 250000;
oc.setBitRate(vbitrate);
if (vbitratetolerance > 0)
oc.setBitRateTolerance(vbitratetolerance);
int oWidth = ic.getWidth();
int oHeight = ic.getHeight();
if (oHeight <= 0 || oWidth <= 0)
throw new RuntimeException("could not find width or height in url: "
+ inputURL);
/**
* For this program we don't allow the user to specify the pixel format
* type; we force the output to be the same as the input.
*/
oc.setPixelType(ic.getPixelType());
if (vscaleFactor != 1.0)
{
/**
* In this case, it looks like the output video requires rescaling, so
* we create a IVideoResampler to do that dirty work.
*/
oWidth = (int) (oWidth * vscaleFactor);
oHeight = (int) (oHeight * vscaleFactor);
mVSamplers[i] = IVideoResampler
.make(oWidth, oHeight, oc.getPixelType(), ic.getWidth(), ic
.getHeight(), ic.getPixelType());
if (mVSamplers[i] == null)
{
throw new RuntimeException(
"This version of Xuggler does not support video resampling "
+ i);
}
}
else
{
mVSamplers[i] = null;
}
oc.setHeight(oHeight);
oc.setWidth(oWidth);
if (vquality >= 0)
{
oc.setFlag(IStreamCoder.Flags.FLAG_QSCALE, true);
oc.setGlobalQuality(vquality);
}
/**
* TimeBases are important, especially for Video. In general Audio
* encoders will assume that any new audio happens IMMEDIATELY after any
* prior audio finishes playing. But for video, we need to make sure
* it's being output at the right rate.
*
* In this case we make sure we set the same time base as the input, and
* then we don't change the time stamps of any IVideoPictures.
*
* But take my word that time stamps are tricky, and this only touches
* the envelope. The good news is, it's easier in Xuggler than some
* other systems.
*/
IRational num = null;
num = ic.getFrameRate();
oc.setFrameRate(num);
oc.setTimeBase(IRational.make(num.getDenominator(), num
.getNumerator()));
num = null;
/**
* And allocate buffers for us to store decoded and resample video
* pictures.
*/
mIVideoPictures[i] = IVideoPicture.make(ic.getPixelType(), ic
.getWidth(), ic.getHeight());
mOVideoPictures[i] = IVideoPicture.make(oc.getPixelType(), oc
.getWidth(), oc.getHeight());
}
else
{
log.warn("Ignoring input stream {} of type {}", i, cType);
}
/**
* Now, once you've set up all the parameters on the StreamCoder, you must
* open() them so they can do work.
*
* They will return an error if not configured correctly, so we check for
* that here.
*/
if (mOCoders[i] != null)
{
// some codecs require experimental mode to be set, and so we set it here.
retval = mOCoders[i].setStandardsCompliance(IStreamCoder.CodecStandardsCompliance.COMPLIANCE_EXPERIMENTAL);
if (retval < 0)
throw new RuntimeException ("could not set compliance mode to experimental");
retval = mOCoders[i].open(null, null);
if (retval < 0)
throw new RuntimeException(
"could not open output encoder for stream: " + i);
retval = mICoders[i].open(null, null);
if (retval < 0)
throw new RuntimeException(
"could not open input decoder for stream: " + i);
}
}
/**
* Pretty much every output container format has a header they need written,
* so we do that here.
*
* You must configure your output IStreams correctly before writing a
* header, and few formats deal nicely with key parameters changing (e.g.
* video width) after a header is written.
*/
retval = mOContainer.writeHeader();
if (retval < 0)
throw new RuntimeException("Could not write header for: " + outputURL);
/**
* That's it with setup; we're good to begin!
*/
return numStreams;
}
/**
* Close and release all resources we used to run this program.
*/
void closeStreams()
{
int numStreams = 0;
int i = 0;
numStreams = mIContainer.getNumStreams();
/**
* Some video coders (e.g. MP3) will often "read-ahead" in a stream and keep
* extra data around to get efficient compression. But they need some way to
* know they're never going to get more data. The convention for that case
* is to pass null for the IMediaData (e.g. IAudioSamples or IVideoPicture)
* in encodeAudio(...) or encodeVideo(...) once before closing the coder.
*
* In that case, the IStreamCoder will flush all data.
*/
for (i = 0; i < numStreams; i++)
{
if (mOCoders[i] != null)
{
IPacket oPacket = IPacket.make();
do {
if (mOCoders[i].getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO)
mOCoders[i].encodeAudio(oPacket, null, 0);
else
mOCoders[i].encodeVideo(oPacket, null, 0);
if (oPacket.isComplete())
mOContainer.writePacket(oPacket, mForceInterleave);
} while (oPacket.isComplete());
}
}
/**
* Some container formats require a trailer to be written to avoid a corrupt
* files.
*
* Others, such as the FLV container muxer, will take a writeTrailer() call
* to tell it to seek() back to the start of the output file and write the
* (now known) duration into the Meta Data.
*
* So trailers are required. In general if a format is a streaming format,
* then the writeTrailer() will never seek backwards.
*
* Make sure you don't close your codecs before you write your trailer, or
* we'll complain loudly and not actually write a trailer.
*/
int retval = mOContainer.writeTrailer();
if (retval < 0)
throw new RuntimeException("Could not write trailer to output file");
/**
* We do a nice clean-up here to show you how you should do it.
*
* That said, Xuggler goes to great pains to clean up after you if you
* forget to release things. But still, you should be a good boy or giral
* and clean up yourself.
*/
for (i = 0; i < numStreams; i++)
{
if (mOCoders[i] != null)
{
/**
* And close the input coder to tell Xuggler it can release all native
* memory.
*/
mOCoders[i].close();
}
mOCoders[i] = null;
if (mICoders[i] != null)
/**
* Close the input coder to tell Xuggler it can release all native
* memory.
*/
mICoders[i].close();
mICoders[i] = null;
}
/**
* Tell Xuggler it can close the output file, write all data, and free all
* relevant memory.
*/
mOContainer.close();
/**
* And do the same with the input file.
*/
mIContainer.close();
/**
* Technically setting everything to null here doesn't do anything but tell
* Java it can collect the memory it used.
*
* The interesting thing to note here is that if you forget to close() a
* Xuggler object, but also loose all references to it from Java, you won't
* leak the native memory. Instead, we'll clean up after you, but we'll
* complain LOUDLY in your logs, so you really don't want to do that.
*/
mOContainer = null;
mIContainer = null;
mISamples = null;
mOSamples = null;
mIVideoPictures = null;
mOVideoPictures = null;
mOCoders = null;
mICoders = null;
mASamplers = null;
mVSamplers = null;
}
/**
* Allow child class to override this method to alter the audio frame before
* it is rencoded and written. In this implementation the audio frame is
* passed through unmodified.
*
* @param audioFrame
* the source audio frame to be modified
*
* @return the modified audio frame
*/
protected IAudioSamples alterAudioFrame(IAudioSamples audioFrame)
{
return audioFrame;
}
/**
* Allow child class to override this method to alter the video frame before
* it is rencoded and written. In this implementation the video frame is
* passed through unmodified.
*
* @param videoFrame
* the source video frame to be modified
*
* @return the modified video frame
*/
protected IVideoPicture alterVideoFrame(IVideoPicture videoFrame)
{
return videoFrame;
}
/**
* Takes a given command line and decodes the input file, and encodes with new
* parameters to the output file.
*
* @param cmdLine
* A command line returned from
* {@link #parseOptions(Options, String[])}.
*/
public void run(CommandLine cmdLine)
{
/**
* Setup all our input and outputs
*/
setupStreams(cmdLine);
/**
* Create packet buffers for reading data from and writing data to the
* conatiners.
*/
IPacket iPacket = IPacket.make();
IPacket oPacket = IPacket.make();
/**
* Keep some "pointers' we'll use for the audio we're working with.
*/
IAudioSamples inSamples = null;
IAudioSamples outSamples = null;
IAudioSamples reSamples = null;
int retval = 0;
/**
* And keep some convenience pointers for the specific stream we're working
* on for a packet.
*/
IStreamCoder ic = null;
IStreamCoder oc = null;
IAudioResampler as = null;
IVideoResampler vs = null;
IVideoPicture inFrame = null;
IVideoPicture reFrame = null;
/**
* Now, we've already opened the files in #setupStreams(CommandLine). We
* just keep reading packets from it until the IContainer returns <0
*/
while (mIContainer.readNextPacket(iPacket) == 0)
{
/**
* Find out which stream this packet belongs to.
*/
int i = iPacket.getStreamIndex();
int offset = 0;
/**
* Find out if this stream has a starting timestamp
*/
IStream stream = mIContainer.getStream(i);
long tsOffset = 0;
if (stream.getStartTime() != Global.NO_PTS && stream.getStartTime() > 0
&& stream.getTimeBase() != null)
{
IRational defTimeBase = IRational.make(1,
(int) Global.DEFAULT_PTS_PER_SECOND);
tsOffset = defTimeBase.rescale(stream.getStartTime(), stream
.getTimeBase());
}
/**
* And look up the appropriate objects that are working on that stream.
*/
ic = mICoders[i];
oc = mOCoders[i];
as = mASamplers[i];
vs = mVSamplers[i];
inFrame = mIVideoPictures[i];
reFrame = mOVideoPictures[i];
inSamples = mISamples[i];
reSamples = mOSamples[i];
if (oc == null)
// we didn't set up this coder; ignore the packet
continue;
/**
* Find out if the stream is audio or video.
*/
ICodec.Type cType = ic.getCodecType();
if (cType == ICodec.Type.CODEC_TYPE_AUDIO && mHasAudio)
{
/**
* Decoding audio works by taking the data in the packet, and eating
* chunks from it to create decoded raw data.
*
* However, there may be more data in a packet than is needed to get one
* set of samples (or less), so you need to iterate through the byts to
* get that data.
*
* The following loop is the standard way of doing that.
*/
while (offset < iPacket.getSize())
{
retval = ic.decodeAudio(inSamples, iPacket, offset);
if (retval <= 0)
throw new RuntimeException("could not decode audio. stream: " + i);
if (inSamples.getTimeStamp() != Global.NO_PTS)
inSamples.setTimeStamp(inSamples.getTimeStamp() - tsOffset);
log.trace("packet:{}; samples:{}; offset:{}", new Object[]
{
iPacket, inSamples, tsOffset
});
/**
* If not an error, the decodeAudio returns the number of bytes it
* consumed. We use that so the next time around the loop we get new
* data.
*/
offset += retval;
int numSamplesConsumed = 0;
/**
* If as is not null then we know a resample was needed, so we do that
* resample now.
*/
if (as != null && inSamples.getNumSamples() > 0)
{
retval = as.resample(reSamples, inSamples, inSamples
.getNumSamples());
outSamples = reSamples;
}
else
{
outSamples = inSamples;
}
/**
* Include call a hook to derivied classes to allow them to alter the
* audio frame.
*/
outSamples = alterAudioFrame(outSamples);
/**
* Now that we've resampled, it's time to encode the audio.
*
* This workflow is similar to decoding; you may have more, less or
* just enough audio samples available to encode a packet. But you
* must iterate through.
*
* Unfortunately (don't ask why) there is a slight difference between
* encodeAudio and decodeAudio; encodeAudio returns the number of
* samples consumed, NOT the number of bytes. This can be confusing,
* and we encourage you to read the IAudioSamples documentation to
* find out what the difference is.
*
* But in any case, the following loop encodes the samples we have
* into packets.
*/
while (numSamplesConsumed < outSamples.getNumSamples())
{
retval = oc.encodeAudio(oPacket, outSamples, numSamplesConsumed);
if (retval <= 0)
throw new RuntimeException("Could not encode any audio: "
+ retval);
/**
* Increment the number of samples consumed, so that the next time
* through this loop we encode new audio
*/
numSamplesConsumed += retval;
log.trace("out packet:{}; samples:{}; offset:{}", new Object[]{
oPacket, outSamples, tsOffset
});
writePacket(oPacket);
}
}
}
else if (cType == ICodec.Type.CODEC_TYPE_VIDEO && mHasVideo)
{
/**
* This encoding workflow is pretty much the same as the for the audio
* above.
*
* The only major delta is that encodeVideo() will always consume one
* frame (whereas encodeAudio() might only consume some samples in an
* IAudioSamples buffer); it might not be able to output a packet yet,
* but you can assume that you it consumes the entire frame.
*/
IVideoPicture outFrame = null;
while (offset < iPacket.getSize())
{
retval = ic.decodeVideo(inFrame, iPacket, offset);
if (retval <= 0)
throw new RuntimeException("could not decode any video. stream: "
+ i);
log.trace("decoded vid ts: {}; pkts ts: {}", inFrame.getTimeStamp(),
iPacket.getTimeStamp());
if (inFrame.getTimeStamp() != Global.NO_PTS)
inFrame.setTimeStamp(inFrame.getTimeStamp() - tsOffset);
offset += retval;
if (inFrame.isComplete())
{
if (vs != null)
{
retval = vs.resample(reFrame, inFrame);
if (retval < 0)
throw new RuntimeException("could not resample video");
outFrame = reFrame;
}
else
{
outFrame = inFrame;
}
/**
* Include call a hook to derivied classes to allow them to alter
* the audio frame.
*/
outFrame = alterVideoFrame(outFrame);
outFrame.setQuality(0);
retval = oc.encodeVideo(oPacket, outFrame, 0);
if (retval < 0)
throw new RuntimeException("could not encode video");
writePacket(oPacket);
}
}
}
else
{
/**
* Just to be complete; there are other types of data that can show up
* in streams (e.g. SUB TITLE).
*
* Right now we don't support decoding and encoding that data, but youc
* could still decide to write out the packets if you wanted.
*/
log.trace("ignoring packet of type: {}", cType);
}
}
// and cleanup.
closeStreams();
}
private void writePacket(IPacket oPacket)
{
int retval;
if (oPacket.isComplete())
{
if (mRealTimeEncoder)
{
delayForRealTime(oPacket);
}
/**
* If we got a complete packet out of the encoder, then go ahead
* and write it to the container.
*/
retval = mOContainer.writePacket(oPacket, mForceInterleave);
if (retval < 0)
throw new RuntimeException("could not write output packet");
}
}
/**
* WARNING for those who want to copy this method and think it'll stream
* for them -- it won't. It doesn't interleave packets from non-interleaved
* containers, so instead it'll write chunky data. But it's useful if you
* have previously interleaved data that you want to write out slowly to
* a file, or, a socket.
* @param oPacket the packet about to be written.
*/
private void delayForRealTime(IPacket oPacket)
{
// convert packet timestamp to microseconds
final IRational timeBase = oPacket.getTimeBase();
if (timeBase == null || timeBase.getNumerator() == 0 ||
timeBase.getDenominator() == 0)
return;
long dts = oPacket.getDts();
if (dts == Global.NO_PTS)
return;
final long currStreamTime = IRational.rescale(dts,
1,
1000000,
timeBase.getNumerator(),
timeBase.getDenominator(),
IRational.Rounding.ROUND_NEAR_INF);
if (mStartStreamTime == null)
mStartStreamTime = currStreamTime;
// convert now to microseconds
final long currClockTime = System.nanoTime()/1000;
if (mStartClockTime == null)
mStartClockTime = currClockTime;
final long currClockDelta = currClockTime - mStartClockTime;
if (currClockDelta < 0)
return;
final long currStreamDelta = currStreamTime - mStartStreamTime;
if (currStreamDelta < 0)
return;
final long streamToClockDeltaMilliseconds = (currStreamDelta - currClockDelta)/1000;
if (streamToClockDeltaMilliseconds <= 0)
return;
try
{
Thread.sleep(streamToClockDeltaMilliseconds);
}
catch (InterruptedException e)
{
}
}
/**
*
* A simple test of xuggler, this program takes an input file, and outputs it
* as an output file.
*
* @param args
* The command line args passed to this program.
*/
public static void main(String[] args)
{
Converter converter = new Converter();
try
{
// first define options
Options options = converter.defineOptions();
// And then parse them.
CommandLine cmdLine = converter.parseOptions(options, args);
// Finally, run the converter.
converter.run(cmdLine);
}
catch (Exception exception)
{
System.err.printf("Error: %s\n", exception.getMessage());
}
}
}