001package net.bramp.ffmpeg.builder;
002
003import static com.google.common.base.Preconditions.checkArgument;
004import static com.google.common.base.Preconditions.checkNotNull;
005import static net.bramp.ffmpeg.FFmpegUtils.toTimecode;
006import static net.bramp.ffmpeg.Preconditions.checkNotEmpty;
007import static net.bramp.ffmpeg.Preconditions.checkValidStream;
008import static net.bramp.ffmpeg.builder.MetadataSpecifier.checkValidKey;
009
010import com.google.common.base.Preconditions;
011import com.google.common.base.Strings;
012import com.google.common.collect.ImmutableList;
013import java.io.File;
014import java.net.URI;
015import java.nio.file.Path;
016import java.util.ArrayList;
017import java.util.List;
018import java.util.concurrent.TimeUnit;
019import net.bramp.ffmpeg.modelmapper.Mapper;
020import net.bramp.ffmpeg.options.AudioEncodingOptions;
021import net.bramp.ffmpeg.options.EncodingOptions;
022import net.bramp.ffmpeg.options.MainEncodingOptions;
023import net.bramp.ffmpeg.options.VideoEncodingOptions;
024import org.apache.commons.lang3.SystemUtils;
025import org.apache.commons.lang3.math.Fraction;
026
027/**
028 * This abstract class holds flags that are both applicable to input and output streams in the
029 * ffmpeg command, while flags that apply to a particular direction (input/output) are located in
030 * {@link FFmpegOutputBuilder}. <br>
031 * <br>
032 * All possible flags can be found in the <a href="https://ffmpeg.org/ffmpeg.html#Options">official
033 * ffmpeg page</a> The discrimination criteria for flag location are the specifiers for each command
034 *
035 * <ul>
036 *   <li>AbstractFFmpegStreamBuilder
037 *       <ul>
038 *         <li>(input/output): <code>-t duration (input/output)</code>
039 *         <li>(input/output,per-stream): <code>
040 *             -codec[:stream_specifier] codec (input/output,per-stream)</code>
041 *         <li>(global): <code>-filter_threads nb_threads (global)</code>
042 *       </ul>
043 *   <li>FFmpegInputBuilder
044 *       <ul>
045 *         <li>(input): <code>-muxdelay seconds (input)</code>
046 *         <li>(input,per-stream): <code>-guess_layout_max channels (input,per-stream)</code>
047 *       </ul>
048 *   <li>FFmpegOutputBuilder
049 *       <ul>
050 *         <li>(output): <code>-atag fourcc/tag (output)</code>
051 *         <li>(output,per-stream): <code>
052 *             -bsf[:stream_specifier] bitstream_filters (output,per-stream)</code>
053 *       </ul>
054 * </ul>
055 *
056 * @param <T> A concrete class that extends from the AbstractFFmpegStreamBuilder
057 */
058public abstract class AbstractFFmpegStreamBuilder<T extends AbstractFFmpegStreamBuilder<T>> {
059
060  protected static final String DEVNULL = SystemUtils.IS_OS_WINDOWS ? "NUL" : "/dev/null";
061
062  final FFmpegBuilder parent;
063
064  /** Output filename or uri. Only one may be set. */
065  public String filename;
066
067  public URI uri;
068
069  public String format;
070
071  public Long startOffset; // in milliseconds
072  public Long duration; // in milliseconds
073
074  public final List<String> meta_tags = new ArrayList<>();
075  public final List<String> maps = new ArrayList<>();
076
077  public boolean audio_enabled = true;
078  public String audio_codec;
079  public int audio_channels;
080  public int audio_sample_rate;
081  public String audio_preset;
082
083  public boolean video_enabled = true;
084  public String video_codec;
085  public boolean video_copyinkf;
086  public Fraction video_frame_rate;
087  public int video_width;
088  public int video_height;
089  public String video_size;
090  public String video_movflags;
091  public Integer video_frames;
092  public String video_pixel_format;
093
094  public boolean subtitle_enabled = true;
095  public String subtitle_preset;
096  private String subtitle_codec;
097
098  public String preset;
099  public String presetFilename;
100  public final List<String> extra_args = new ArrayList<>();
101
102  public Strict strict = Strict.NORMAL;
103
104  public long targetSize = 0; // in bytes
105  public long pass_padding_bitrate = 1024; // in bits per second
106
107  public boolean throwWarnings = true; // TODO Either delete this, or apply it consistently
108
109  /** Constructs a stream builder with no parent. */
110  protected AbstractFFmpegStreamBuilder() {
111    this.parent = null;
112  }
113
114  /** Constructs a stream builder with the given parent and output filename. */
115  protected AbstractFFmpegStreamBuilder(FFmpegBuilder parent, String filename) {
116    this.parent = checkNotNull(parent);
117    this.filename = checkNotEmpty(filename, "filename must not be empty");
118  }
119
120  /** Constructs a stream builder with the given parent and output URI. */
121  protected AbstractFFmpegStreamBuilder(FFmpegBuilder parent, URI uri) {
122    this.parent = checkNotNull(parent);
123    this.uri = checkValidStream(uri);
124  }
125
126  /** Returns this instance for fluent API chaining. */
127  protected abstract T getThis();
128
129  /** Applies the given encoding options to this builder. */
130  public T useOptions(EncodingOptions opts) {
131    Mapper.map(opts, this);
132    return getThis();
133  }
134
135  /** Applies the given main encoding options to this builder. */
136  public T useOptions(MainEncodingOptions opts) {
137    Mapper.map(opts, this);
138    return getThis();
139  }
140
141  /** Applies the given audio encoding options to this builder. */
142  public T useOptions(AudioEncodingOptions opts) {
143    Mapper.map(opts, this);
144    return getThis();
145  }
146
147  /** Applies the given video encoding options to this builder. */
148  public T useOptions(VideoEncodingOptions opts) {
149    Mapper.map(opts, this);
150    return getThis();
151  }
152
153  /** Disables video output. */
154  public T disableVideo() {
155    this.video_enabled = false;
156    return getThis();
157  }
158
159  /** Disables audio output. */
160  public T disableAudio() {
161    this.audio_enabled = false;
162    return getThis();
163  }
164
165  /** Disables subtitle output. */
166  public T disableSubtitle() {
167    this.subtitle_enabled = false;
168    return getThis();
169  }
170
171  /**
172   * Sets a file to use containing presets.
173   *
174   * <p>Uses `-fpre`.
175   *
176   * @param presetFilename the preset by filename
177   * @return this
178   */
179  public T setPresetFilename(String presetFilename) {
180    this.presetFilename = checkNotEmpty(presetFilename, "file preset must not be empty");
181    return getThis();
182  }
183
184  /**
185   * Sets a file to use containing presets.
186   *
187   * <p>Uses `-fpre`.
188   *
189   * @param presetFile the preset by file
190   * @return this
191   */
192  public T setPresetFilename(File presetFile) {
193    return setPresetFilename(checkNotNull(presetFile).getPath());
194  }
195
196  /**
197   * Sets a file to use containing presets.
198   *
199   * <p>Uses `-fpre`.
200   *
201   * @param presetPath the preset by path
202   * @return this
203   */
204  public T setPresetFilename(Path presetPath) {
205    return setPresetFilename(checkNotNull(presetPath).toString());
206  }
207
208  /**
209   * Sets a preset by name (this only works with some codecs).
210   *
211   * <p>Uses `-preset`.
212   *
213   * @param preset the preset
214   * @return this
215   */
216  public T setPreset(String preset) {
217    this.preset = checkNotEmpty(preset, "preset must not be empty");
218    return getThis();
219  }
220
221  /** Sets the output filename. */
222  public T setFilename(String filename) {
223    this.filename = checkNotEmpty(filename, "filename must not be empty");
224    return getThis();
225  }
226
227  /**
228   * Sets the output filename.
229   *
230   * @param file The file
231   * @return this
232   */
233  public T setFilename(File file) {
234    return setFilename(checkNotNull(file).getPath());
235  }
236
237  /**
238   * Sets the output filename.
239   *
240   * @param path The path
241   * @return this
242   */
243  public T setFilename(Path path) {
244    return setFilename(checkNotNull(path).toString());
245  }
246
247  /** Returns the output filename. */
248  public String getFilename() {
249    return filename;
250  }
251
252  /** Sets the output URI. */
253  public T setUri(URI uri) {
254    this.uri = checkValidStream(uri);
255    return getThis();
256  }
257
258  public URI getUri() {
259    return uri;
260  }
261
262  /** Sets the output format. */
263  public T setFormat(String format) {
264    this.format = checkNotEmpty(format, "format must not be empty");
265    return getThis();
266  }
267
268  /** Sets the video codec. */
269  public T setVideoCodec(String codec) {
270    this.video_enabled = true;
271    this.video_codec = checkNotEmpty(codec, "codec must not be empty");
272    return getThis();
273  }
274
275  /** Sets whether to copy initial non-keyframes. */
276  public T setVideoCopyInkf(boolean copyinkf) {
277    this.video_enabled = true;
278    this.video_copyinkf = copyinkf;
279    return getThis();
280  }
281
282  /** Sets the MOV muxer flags. */
283  public T setVideoMovFlags(String movflags) {
284    this.video_enabled = true;
285    this.video_movflags = checkNotEmpty(movflags, "movflags must not be empty");
286    return getThis();
287  }
288
289  /**
290   * Sets the video's frame rate.
291   *
292   * @param frame_rate Frames per second
293   * @return this
294   * @see net.bramp.ffmpeg.FFmpeg#FPS_30
295   * @see net.bramp.ffmpeg.FFmpeg#FPS_29_97
296   * @see net.bramp.ffmpeg.FFmpeg#FPS_24
297   * @see net.bramp.ffmpeg.FFmpeg#FPS_23_976
298   */
299  public T setVideoFrameRate(Fraction frame_rate) {
300    this.video_enabled = true;
301    this.video_frame_rate = checkNotNull(frame_rate);
302    return getThis();
303  }
304
305  /**
306   * Set the video frame rate in terms of frames per interval. For example 24fps would be 24/1,
307   * however NTSC TV at 23.976fps would be 24000 per 1001.
308   *
309   * @param frames The number of frames within the given seconds
310   * @param per The number of seconds
311   * @return this
312   */
313  public T setVideoFrameRate(int frames, int per) {
314    return setVideoFrameRate(Fraction.getFraction(frames, per));
315  }
316
317  /** Sets the video frame rate. */
318  public T setVideoFrameRate(double frame_rate) {
319    return setVideoFrameRate(Fraction.getFraction(frame_rate));
320  }
321
322  /**
323   * Set the number of video frames to record.
324   *
325   * @param frames The number of frames
326   * @return this
327   */
328  public T setFrames(int frames) {
329    this.video_enabled = true;
330    this.video_frames = frames;
331    return getThis();
332  }
333
334  /** Checks if the given width or height value is valid. */
335  protected static boolean isValidSize(int widthOrHeight) {
336    return widthOrHeight > 0 || widthOrHeight == -1;
337  }
338
339  /** Sets the video width in pixels. */
340  public T setVideoWidth(int width) {
341    checkArgument(isValidSize(width), "Width must be -1 or greater than zero");
342
343    this.video_enabled = true;
344    this.video_width = width;
345    return getThis();
346  }
347
348  /** Sets the video height in pixels. */
349  public T setVideoHeight(int height) {
350    checkArgument(isValidSize(height), "Height must be -1 or greater than zero");
351
352    this.video_enabled = true;
353    this.video_height = height;
354    return getThis();
355  }
356
357  /** Sets the video resolution by width and height in pixels. */
358  public T setVideoResolution(int width, int height) {
359    checkArgument(
360        isValidSize(width) && isValidSize(height),
361        "Both width and height must be -1 or greater than zero");
362
363    this.video_enabled = true;
364    this.video_width = width;
365    this.video_height = height;
366    return getThis();
367  }
368
369  /**
370   * Sets video resolution based on an abbreviation, e.g. "ntsc" for 720x480, or "vga" for 640x480
371   *
372   * @see <a href="https://www.ffmpeg.org/ffmpeg-utils.html#Video-size">ffmpeg video size</a>
373   * @param abbreviation The abbreviation size. No validation is done, instead the value is passed
374   *     as is to ffmpeg.
375   * @return this
376   */
377  public T setVideoResolution(String abbreviation) {
378    this.video_enabled = true;
379    this.video_size = checkNotEmpty(abbreviation, "video abbreviation must not be empty");
380    return getThis();
381  }
382
383  /** Sets the video pixel format. */
384  public T setVideoPixelFormat(String format) {
385    this.video_enabled = true;
386    this.video_pixel_format = checkNotEmpty(format, "format must not be empty");
387    return getThis();
388  }
389
390  /**
391   * Add metadata on output streams. Which keys are possible depends on the used codec.
392   *
393   * @param key Metadata key, e.g. "comment"
394   * @param value Value to set for key
395   * @return this
396   */
397  public T addMetaTag(String key, String value) {
398    checkValidKey(key);
399    checkNotEmpty(value, "value must not be empty");
400    meta_tags.add("-metadata");
401    meta_tags.add(key + "=" + value);
402    return getThis();
403  }
404
405  /**
406   * Add metadata on output streams. Which keys are possible depends on the used codec.
407   *
408   * <pre>{@code
409   * import static net.bramp.ffmpeg.builder.MetadataSpecifier.*;
410   * import static net.bramp.ffmpeg.builder.StreamSpecifier.*;
411   * import static net.bramp.ffmpeg.builder.StreamSpecifierType.*;
412   *
413   * new FFmpegBuilder()
414   *   .addMetaTag("title", "Movie Title") // Annotate whole file
415   *   .addMetaTag(chapter(0), "author", "Bob") // Annotate first chapter
416   *   .addMetaTag(program(0), "comment", "Awesome") // Annotate first program
417   *   .addMetaTag(stream(0), "copyright", "Megacorp") // Annotate first stream
418   *   .addMetaTag(stream(Video), "framerate", "24fps") // Annotate all video streams
419   *   .addMetaTag(stream(Video, 0), "artist", "Joe") // Annotate first video stream
420   *   .addMetaTag(stream(Audio, 0), "language", "eng") // Annotate first audio stream
421   *   .addMetaTag(stream(Subtitle, 0), "language", "fre") // Annotate first subtitle stream
422   *   .addMetaTag(usable(), "year", "2010") // Annotate all streams with a usable configuration
423   * }</pre>
424   *
425   * @param spec Metadata specifier, e.g `MetadataSpec.stream(Audio, 0)`
426   * @param key Metadata key, e.g. "comment"
427   * @param value Value to set for key
428   * @return this
429   */
430  public T addMetaTag(MetadataSpecifier spec, String key, String value) {
431    checkValidKey(key);
432    checkNotEmpty(value, "value must not be empty");
433    meta_tags.add("-metadata:" + spec.spec());
434    meta_tags.add(key + "=" + value);
435    return getThis();
436  }
437
438  /** Adds a stream mapping by input index. */
439  public T addMap(int inputIndex) {
440    checkArgument(inputIndex >= 0, "inputIndex must be greater or equal to zero");
441    this.maps.add(String.valueOf(inputIndex));
442    return getThis();
443  }
444
445  /** Adds a stream mapping by input index and stream specifier. */
446  public T addMap(int inputIndex, StreamSpecifier spec) {
447    checkArgument(inputIndex >= 0, "inputIndex must be greater or equal to zero");
448    this.maps.add(inputIndex + ":" + spec.spec());
449    return getThis();
450  }
451
452  /** Sets the audio codec. */
453  public T setAudioCodec(String codec) {
454    this.audio_enabled = true;
455    this.audio_codec = checkNotEmpty(codec, "codec must not be empty");
456    return getThis();
457  }
458
459  /** Sets the subtitle codec. */
460  public T setSubtitleCodec(String codec) {
461    this.subtitle_enabled = true;
462    this.subtitle_codec = checkNotEmpty(codec, "codec must not be empty");
463    return getThis();
464  }
465
466  /**
467   * Sets the number of audio channels.
468   *
469   * @param channels Number of channels
470   * @return this
471   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_MONO
472   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_STEREO
473   */
474  public T setAudioChannels(int channels) {
475    checkArgument(channels > 0, "channels must be positive");
476    this.audio_enabled = true;
477    this.audio_channels = channels;
478    return getThis();
479  }
480
481  /**
482   * Sets the Audio sample rate, for example 44_000.
483   *
484   * @param sample_rate Samples measured in Hz
485   * @return this
486   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_8000
487   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_11025
488   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_12000
489   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_16000
490   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_22050
491   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_32000
492   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_44100
493   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_48000
494   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_96000
495   */
496  public T setAudioSampleRate(int sample_rate) {
497    checkArgument(sample_rate > 0, "sample rate must be positive");
498    this.audio_enabled = true;
499    this.audio_sample_rate = sample_rate;
500    return getThis();
501  }
502
503  /**
504   * Target output file size (in bytes).
505   *
506   * @param targetSize The target size in bytes
507   * @return this
508   */
509  public T setTargetSize(long targetSize) {
510    checkArgument(targetSize > 0, "target size must be positive");
511    this.targetSize = targetSize;
512    return getThis();
513  }
514
515  /**
516   * Decodes but discards input until the offset.
517   *
518   * @param offset The offset
519   * @param units The units the offset is in
520   * @return this
521   */
522  public T setStartOffset(long offset, TimeUnit units) {
523    checkNotNull(units);
524
525    this.startOffset = units.toMillis(offset);
526
527    return getThis();
528  }
529
530  /**
531   * Stop writing the output after duration is reached.
532   *
533   * @param duration The duration
534   * @param units The units the duration is in
535   * @return this
536   */
537  public T setDuration(long duration, TimeUnit units) {
538    checkNotNull(units);
539
540    this.duration = units.toMillis(duration);
541
542    return getThis();
543  }
544
545  /** Sets the strict mode for standards compliance. */
546  public T setStrict(Strict strict) {
547    this.strict = checkNotNull(strict);
548    return getThis();
549  }
550
551  /**
552   * When doing multi-pass we add a little extra padding, to ensure we reach our target.
553   *
554   * @param bitrate bit rate
555   * @return this
556   */
557  public T setPassPaddingBitrate(long bitrate) {
558    checkArgument(bitrate > 0, "bitrate must be positive");
559    this.pass_padding_bitrate = bitrate;
560    return getThis();
561  }
562
563  /**
564   * Sets a audio preset to use.
565   *
566   * <p>Uses `-apre`.
567   *
568   * @param preset the preset
569   * @return this
570   */
571  public T setAudioPreset(String preset) {
572    this.audio_enabled = true;
573    this.audio_preset = checkNotEmpty(preset, "audio preset must not be empty");
574    return getThis();
575  }
576
577  /**
578   * Sets a subtitle preset to use.
579   *
580   * <p>Uses `-spre`.
581   *
582   * @param preset the preset
583   * @return this
584   */
585  public T setSubtitlePreset(String preset) {
586    this.subtitle_enabled = true;
587    this.subtitle_preset = checkNotEmpty(preset, "subtitle preset must not be empty");
588    return getThis();
589  }
590
591  /**
592   * Add additional output arguments (for flags which aren't currently supported).
593   *
594   * @param values The extra arguments
595   * @return this
596   */
597  public T addExtraArgs(String... values) {
598    checkArgument(values.length > 0, "one or more values must be supplied");
599    checkNotEmpty(values[0], "first extra arg may not be empty");
600
601    for (String value : values) {
602      extra_args.add(checkNotNull(value));
603    }
604    return getThis();
605  }
606
607  /**
608   * Finished with this output.
609   *
610   * @return the parent FFmpegBuilder
611   */
612  public FFmpegBuilder done() {
613    Preconditions.checkState(parent != null, "Can not call done without parent being set");
614    return parent;
615  }
616
617  /**
618   * Returns a representation of this Builder that can be safely serialised.
619   *
620   * <p>NOTE: This method is horribly out of date, and its use should be rethought.
621   *
622   * @return A new EncodingOptions capturing this Builder's state
623   */
624  public abstract EncodingOptions buildOptions();
625
626  /** Builds the command-line arguments for the given pass using the parent builder. */
627  protected List<String> build(int pass) {
628    Preconditions.checkState(parent != null, "Can not build without parent being set");
629    return build(parent, pass);
630  }
631
632  /**
633   * Builds the arguments.
634   *
635   * @param parent The parent FFmpegBuilder
636   * @param pass The particular pass. For one-pass this value will be zero, for multi-pass, it will
637   *     be 1 for the first pass, 2 for the second, and so on.
638   * @return The arguments
639   */
640  protected List<String> build(FFmpegBuilder parent, int pass) {
641    checkNotNull(parent);
642
643    ImmutableList.Builder<String> args = new ImmutableList.Builder<>();
644
645    addGlobalFlags(parent, args);
646
647    if (video_enabled) {
648      addVideoFlags(parent, args);
649    } else {
650      args.add("-vn");
651    }
652
653    if (audio_enabled && pass != 1) {
654      addAudioFlags(args);
655    } else {
656      args.add("-an");
657    }
658
659    if (subtitle_enabled) {
660      if (!Strings.isNullOrEmpty(subtitle_codec)) {
661        args.add("-scodec", subtitle_codec);
662      }
663      if (!Strings.isNullOrEmpty(subtitle_preset)) {
664        args.add("-spre", subtitle_preset);
665      }
666    } else {
667      args.add("-sn");
668    }
669
670    addFormatArgs(args);
671
672    args.addAll(extra_args);
673
674    addSourceTarget(pass, args);
675
676    return args.build();
677  }
678
679  /** Adds source and target specific arguments for the given pass. */
680  protected abstract void addSourceTarget(int pass, ImmutableList.Builder<String> args);
681
682  /** Adds global flags such as format, preset, and time options to the arguments. */
683  protected void addGlobalFlags(FFmpegBuilder parent, ImmutableList.Builder<String> args) {
684    if (strict != Strict.NORMAL) {
685      args.add("-strict", strict.toString());
686    }
687
688    if (!Strings.isNullOrEmpty(format)) {
689      args.add("-f", format);
690    }
691
692    if (!Strings.isNullOrEmpty(preset)) {
693      args.add("-preset", preset);
694    }
695
696    if (!Strings.isNullOrEmpty(presetFilename)) {
697      args.add("-fpre", presetFilename);
698    }
699
700    if (startOffset != null) {
701      args.add("-ss", toTimecode(startOffset, TimeUnit.MILLISECONDS));
702    }
703
704    if (duration != null) {
705      args.add("-t", toTimecode(duration, TimeUnit.MILLISECONDS));
706    }
707
708    args.addAll(meta_tags);
709  }
710
711  /** Adds audio-related flags such as codec, channels, and sample rate to the arguments. */
712  protected void addAudioFlags(ImmutableList.Builder<String> args) {
713    if (!Strings.isNullOrEmpty(audio_codec)) {
714      args.add("-acodec", audio_codec);
715    }
716
717    if (audio_channels > 0) {
718      args.add("-ac", String.valueOf(audio_channels));
719    }
720
721    if (audio_sample_rate > 0) {
722      args.add("-ar", String.valueOf(audio_sample_rate));
723    }
724
725    if (!Strings.isNullOrEmpty(audio_preset)) {
726      args.add("-apre", audio_preset);
727    }
728  }
729
730  /** Adds video-related flags such as codec, frame rate, and resolution to the arguments. */
731  protected void addVideoFlags(FFmpegBuilder parent, ImmutableList.Builder<String> args) {
732    if (video_frames != null) {
733      args.add("-vframes", video_frames.toString());
734    }
735
736    if (!Strings.isNullOrEmpty(video_codec)) {
737      args.add("-vcodec", video_codec);
738    }
739
740    if (!Strings.isNullOrEmpty(video_pixel_format)) {
741      args.add("-pix_fmt", video_pixel_format);
742    }
743
744    if (video_copyinkf) {
745      args.add("-copyinkf");
746    }
747
748    if (!Strings.isNullOrEmpty(video_movflags)) {
749      args.add("-movflags", video_movflags);
750    }
751
752    if (video_size != null) {
753      checkArgument(
754          video_width == 0 && video_height == 0,
755          "Can not specific width or height, as well as an abbreviatied video size");
756      args.add("-s", video_size);
757
758    } else if (video_width != 0 && video_height != 0) {
759      args.add("-s", String.format("%dx%d", video_width, video_height));
760    }
761
762    // TODO What if width is set but heigh isn't. We don't seem to do anything
763
764    if (video_frame_rate != null) {
765      args.add("-r", video_frame_rate.toString());
766    }
767  }
768
769  /** Adds format-related arguments such as stream mappings. */
770  protected void addFormatArgs(ImmutableList.Builder<String> args) {
771    for (String map : maps) {
772      args.add("-map", map);
773    }
774  }
775}