Java tutorial
/******************************************************************************* * 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.mediatool.demos; import java.util.List; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.GnuParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.xuggle.xuggler.Configuration; 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.IMetaData; import com.xuggle.xuggler.IPacket; import com.xuggle.xuggler.IRational; import com.xuggle.xuggler.IStream; import com.xuggle.xuggler.IStreamCoder; import com.xuggle.xuggler.IVideoPicture; import com.xuggle.xuggler.IVideoResampler; import com.xuggle.xuggler.Xuggler; 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 static final Logger LOG = LoggerFactory.getLogger(Converter.class); /** * 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) { LOG.error("AFSADF"); } } /** * * 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) { LOG.error("Error: %s\n", exception.getMessage()); } System.exit(0); } }