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.Preconditions.checkNotEmpty;
006
007import com.google.common.base.Ascii;
008import com.google.common.base.Preconditions;
009import com.google.common.base.Strings;
010import com.google.common.collect.ImmutableList;
011import java.io.File;
012import java.net.URI;
013import java.nio.file.Path;
014import java.nio.file.Paths;
015import java.util.ArrayList;
016import java.util.List;
017import java.util.Map;
018import java.util.TreeMap;
019import java.util.concurrent.TimeUnit;
020import javax.annotation.CheckReturnValue;
021import net.bramp.ffmpeg.FFmpegUtils;
022import net.bramp.ffmpeg.probe.FFmpegProbeResult;
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026/**
027 * Builds a ffmpeg command line.
028 *
029 * @author bramp
030 */
031public class FFmpegBuilder {
032
033  private static final Logger log = LoggerFactory.getLogger(FFmpegBuilder.class);
034
035  /**
036   * Log level options: <a href="https://ffmpeg.org/ffmpeg.html#Generic-options">ffmpeg
037   * documentation</a>.
038   */
039  public enum Verbosity {
040    QUIET,
041    PANIC,
042    FATAL,
043    ERROR,
044    WARNING,
045    INFO,
046    VERBOSE,
047    DEBUG;
048
049    @Override
050    public String toString() {
051      // ffmpeg command line requires these options in lower case
052      return Ascii.toLowerCase(name());
053    }
054  }
055
056  // Global Settings
057  boolean override = true;
058  int pass = 0;
059  String pass_directory = "";
060  String pass_prefix;
061  Verbosity verbosity = Verbosity.ERROR;
062  URI progress;
063  String user_agent;
064  Integer qscale;
065
066  int threads;
067  // Input settings
068  String format;
069  Long startOffset; // in millis
070  boolean read_at_native_frame_rate = false;
071  final List<AbstractFFmpegInputBuilder<?>> inputs = new ArrayList<>();
072  final Map<String, FFmpegProbeResult> inputProbes = new TreeMap<>();
073
074  final List<String> extra_args = new ArrayList<>();
075
076  // Output
077  final List<AbstractFFmpegOutputBuilder<?>> outputs = new ArrayList<>();
078
079  protected Strict strict = Strict.NORMAL;
080
081  // Filters
082  String audioFilter;
083  String videoFilter;
084  String complexFilter;
085
086  /** Sets the strict standards compliance level. */
087  public FFmpegBuilder setStrict(Strict strict) {
088    this.strict = checkNotNull(strict);
089    return this;
090  }
091
092  /** Sets whether to overwrite output files without asking. */
093  public FFmpegBuilder overrideOutputFiles(boolean override) {
094    this.override = override;
095    return this;
096  }
097
098  /** Returns whether output files will be overwritten. */
099  public boolean getOverrideOutputFiles() {
100    return this.override;
101  }
102
103  /** Sets the pass number for multi-pass encoding. */
104  public FFmpegBuilder setPass(int pass) {
105    this.pass = pass;
106    return this;
107  }
108
109  /** Sets the directory for storing pass log files. */
110  public FFmpegBuilder setPassDirectory(String directory) {
111    this.pass_directory = checkNotNull(directory);
112    return this;
113  }
114
115  /** Sets the directory for storing pass log files. */
116  public FFmpegBuilder setPassDirectory(File directory) {
117    return setPassDirectory(checkNotNull(directory).getPath());
118  }
119
120  /** Sets the directory for storing pass log files. */
121  public FFmpegBuilder setPassDirectory(Path directory) {
122    return setPassDirectory(checkNotNull(directory).toString());
123  }
124
125  /** Returns the pass log file directory. */
126  public String getPassDirectory() {
127    return this.pass_directory;
128  }
129
130  /** Sets the pass log file prefix. */
131  public FFmpegBuilder setPassPrefix(String prefix) {
132    this.pass_prefix = checkNotNull(prefix);
133    return this;
134  }
135
136  /** Returns the pass log file prefix. */
137  public String getPassPrefix() {
138    return this.pass_prefix;
139  }
140
141  /** Sets the logging verbosity level. */
142  public FFmpegBuilder setVerbosity(Verbosity verbosity) {
143    checkNotNull(verbosity);
144    this.verbosity = verbosity;
145    return this;
146  }
147
148  /** Sets the HTTP user agent string. */
149  public FFmpegBuilder setUserAgent(String userAgent) {
150    this.user_agent = checkNotNull(userAgent);
151    return this;
152  }
153
154  /**
155   * Makes ffmpeg read the first input at the native frame read.
156   *
157   * @return this
158   * @deprecated Use {@link AbstractFFmpegInputBuilder#readAtNativeFrameRate()} instead
159   */
160  @Deprecated
161  public FFmpegBuilder readAtNativeFrameRate() {
162    this.read_at_native_frame_rate = true;
163    return this;
164  }
165
166  /** Adds an input from a previously probed result. */
167  public FFmpegFileInputBuilder addInput(FFmpegProbeResult result) {
168    checkNotNull(result);
169    String filename = checkNotNull(result.getFormat()).getFilename();
170
171    return this.doAddInput(new FFmpegFileInputBuilder(this, filename, result));
172  }
173
174  /** Adds an input by filename or URL. */
175  public FFmpegFileInputBuilder addInput(String filename) {
176    checkNotNull(filename);
177
178    return this.doAddInput(new FFmpegFileInputBuilder(this, filename));
179  }
180
181  /** Adds an input by file. */
182  public FFmpegFileInputBuilder addInput(File file) {
183    return addInput(checkNotNull(file).getPath());
184  }
185
186  /** Adds an input by path. */
187  public FFmpegFileInputBuilder addInput(Path path) {
188    return addInput(checkNotNull(path).toString());
189  }
190
191  /** Adds a pre-built input builder and finalizes it. */
192  public <T extends AbstractFFmpegInputBuilder<T>> FFmpegBuilder addInput(T input) {
193    return this.doAddInput(input).done();
194  }
195
196  /** Adds an input builder to the list and returns it for further configuration. */
197  protected <T extends AbstractFFmpegInputBuilder<T>> T doAddInput(T input) {
198    checkNotNull(input);
199
200    inputs.add(input);
201    return input;
202  }
203
204  /** Clears all previously added inputs. */
205  protected void clearInputs() {
206    inputs.clear();
207    inputProbes.clear();
208  }
209
210  /** Clears existing inputs and sets the input from a probed result. */
211  public FFmpegFileInputBuilder setInput(FFmpegProbeResult result) {
212    clearInputs();
213    return addInput(result);
214  }
215
216  /** Clears existing inputs and sets the input by filename or URL. */
217  public FFmpegFileInputBuilder setInput(String filename) {
218    clearInputs();
219    return addInput(filename);
220  }
221
222  /** Clears existing inputs and sets the input by file. */
223  public FFmpegFileInputBuilder setInput(File file) {
224    clearInputs();
225    return addInput(file);
226  }
227
228  /** Clears existing inputs and sets the input by path. */
229  public FFmpegFileInputBuilder setInput(Path path) {
230    clearInputs();
231    return addInput(path);
232  }
233
234  /** Sets the input using an input builder, replacing any previous inputs. */
235  public <T extends AbstractFFmpegInputBuilder<T>> FFmpegBuilder setInput(T input) {
236    checkNotNull(input);
237
238    clearInputs();
239    inputs.add(input);
240
241    return this;
242  }
243
244  /** Sets the number of threads to use for processing. */
245  public FFmpegBuilder setThreads(int threads) {
246    checkArgument(threads > 0, "threads must be greater than zero");
247    this.threads = threads;
248    return this;
249  }
250
251  /**
252   * Sets the format for the first input stream.
253   *
254   * @param format the format of this input stream, not null
255   * @return this
256   * @deprecated Specify this option on an input stream using {@link
257   *     AbstractFFmpegStreamBuilder#setFormat(String)}
258   */
259  @Deprecated
260  public FFmpegBuilder setFormat(String format) {
261    this.format = checkNotNull(format);
262    return this;
263  }
264
265  /**
266   * Sets the start offset for the first input stream.
267   *
268   * @param duration the amount of the offset, measured in terms of the unit
269   * @param units the unit that the duration is measured in, not null
270   * @return this
271   * @deprecated Specify this option on an input or output stream using {@link
272   *     AbstractFFmpegStreamBuilder#setStartOffset(long, TimeUnit)}
273   */
274  @Deprecated
275  public FFmpegBuilder setStartOffset(long duration, TimeUnit units) {
276    checkNotNull(units);
277
278    this.startOffset = units.toMillis(duration);
279
280    return this;
281  }
282
283  /** Sets the URI for progress reporting. */
284  public FFmpegBuilder addProgress(URI uri) {
285    this.progress = checkNotNull(uri);
286    return this;
287  }
288
289  /**
290   * Sets the complex filter flag.
291   *
292   * @param filter the complex filter string
293   * @return this
294   * @deprecated Use {@link AbstractFFmpegOutputBuilder#setComplexFilter(String)} instead
295   */
296  @Deprecated
297  public FFmpegBuilder setComplexFilter(String filter) {
298    this.complexFilter = checkNotEmpty(filter, "filter must not be empty");
299    return this;
300  }
301
302  /**
303   * Sets the audio filter flag.
304   *
305   * @param filter the audio filter string
306   * @return this
307   */
308  public FFmpegBuilder setAudioFilter(String filter) {
309    this.audioFilter = checkNotEmpty(filter, "filter must not be empty");
310    return this;
311  }
312
313  /**
314   * Sets the video filter flag.
315   *
316   * @param filter the video filter string
317   * @return this
318   */
319  public FFmpegBuilder setVideoFilter(String filter) {
320    this.videoFilter = checkNotEmpty(filter, "filter must not be empty");
321    return this;
322  }
323
324  /**
325   * Sets vbr quality when decoding mp3 output.
326   *
327   * @param quality the quality between 0 and 9. Where 0 is best.
328   * @return FFmpegBuilder
329   */
330  public FFmpegBuilder setVBR(Integer quality) {
331    Preconditions.checkArgument(quality > 0 && quality < 9, "vbr must be between 0 and 9");
332    this.qscale = quality;
333    return this;
334  }
335
336  /**
337   * Add additional ouput arguments (for flags which aren't currently supported).
338   *
339   * @param values The extra arguments.
340   * @return this
341   */
342  public FFmpegBuilder addExtraArgs(String... values) {
343    checkArgument(values.length > 0, "one or more values must be supplied");
344    checkNotEmpty(values[0], "first extra arg may not be empty");
345
346    for (String value : values) {
347      extra_args.add(checkNotNull(value));
348    }
349    return this;
350  }
351
352  /**
353   * Adds new output file.
354   *
355   * @param filename output file path
356   * @return A new {@link FFmpegOutputBuilder}
357   */
358  public FFmpegOutputBuilder addOutput(String filename) {
359    FFmpegOutputBuilder output = new FFmpegOutputBuilder(this, filename);
360    outputs.add(output);
361    return output;
362  }
363
364  /** Adds a new output file. */
365  public FFmpegOutputBuilder addOutput(File file) {
366    return addOutput(checkNotNull(file).getPath());
367  }
368
369  /** Adds a new output file. */
370  public FFmpegOutputBuilder addOutput(Path path) {
371    return addOutput(checkNotNull(path).toString());
372  }
373
374  /**
375   * Adds new output file.
376   *
377   * @param uri output file uri typically a stream
378   * @return A new {@link FFmpegOutputBuilder}
379   */
380  public FFmpegOutputBuilder addOutput(URI uri) {
381    FFmpegOutputBuilder output = new FFmpegOutputBuilder(this, uri);
382    outputs.add(output);
383    return output;
384  }
385
386  /**
387   * Adds an existing FFmpegOutputBuilder. This is similar to calling the other addOuput methods but
388   * instead allows an existing FFmpegOutputBuilder to be used, and reused.
389   *
390   * <pre>
391   * <code>List&lt;String&gt; args = new FFmpegBuilder()
392   *   .addOutput(new FFmpegOutputBuilder()
393   *     .setFilename(&quot;output.flv&quot;)
394   *     .setVideoCodec(&quot;flv&quot;)
395   *   )
396   *   .build();</code>
397   * </pre>
398   *
399   * @param output FFmpegOutputBuilder to add
400   * @return this
401   */
402  public FFmpegBuilder addOutput(FFmpegOutputBuilder output) {
403    outputs.add(output);
404    return this;
405  }
406
407  /**
408   * Adds new HLS(Http Live Streaming) output file. <br>
409   *
410   * <pre>
411   * <code>List&lt;String&gt; args = new FFmpegBuilder()
412   *   .addHlsOutput(&quot;output.m3u8&quot;)
413   *   .done().build();</code>
414   * </pre>
415   *
416   * @param filename output file path
417   * @return A new {@link FFmpegHlsOutputBuilder}
418   */
419  public FFmpegHlsOutputBuilder addHlsOutput(String filename) {
420    FFmpegHlsOutputBuilder output = new FFmpegHlsOutputBuilder(this, filename);
421    outputs.add(output);
422    return output;
423  }
424
425  /** Adds a new HLS output file. */
426  public FFmpegHlsOutputBuilder addHlsOutput(File file) {
427    return addHlsOutput(checkNotNull(file).getPath());
428  }
429
430  /** Adds a new HLS output file. */
431  public FFmpegHlsOutputBuilder addHlsOutput(Path path) {
432    return addHlsOutput(checkNotNull(path).toString());
433  }
434
435  /**
436   * Create new output (to stdout).
437   *
438   * @return A new {@link FFmpegOutputBuilder}
439   */
440  public FFmpegOutputBuilder addStdoutOutput() {
441    return addOutput("-");
442  }
443
444  /** Builds and returns the list of command-line arguments for ffmpeg. */
445  @CheckReturnValue
446  public List<String> build() {
447    ImmutableList.Builder<String> args = new ImmutableList.Builder<>();
448
449    Preconditions.checkArgument(!inputs.isEmpty(), "At least one input must be specified");
450    Preconditions.checkArgument(!outputs.isEmpty(), "At least one output must be specified");
451
452    if (strict != Strict.NORMAL) {
453      args.add("-strict", strict.toString());
454    }
455
456    args.add(override ? "-y" : "-n");
457    args.add("-v", this.verbosity.toString());
458
459    if (user_agent != null) {
460      args.add("-user_agent", user_agent);
461    }
462
463    if (startOffset != null) {
464      log.warn(
465          "Using FFmpegBuilder#setStartOffset is deprecated."
466              + " Specify it on the inputStream or outputStream instead");
467      args.add("-ss", FFmpegUtils.toTimecode(startOffset, TimeUnit.MILLISECONDS));
468    }
469
470    if (threads > 0) {
471      args.add("-threads", String.valueOf(threads));
472    }
473
474    if (format != null) {
475      log.warn(
476          "Using FFmpegBuilder#setFormat is deprecated."
477              + " Specify it on the inputStream or outputStream instead");
478      args.add("-f", format);
479    }
480
481    if (read_at_native_frame_rate) {
482      log.warn(
483          "Using FFmpegBuilder#readAtNativeFrameRate is deprecated."
484              + " Specify it on the inputStream instead");
485      args.add("-re");
486    }
487
488    if (progress != null) {
489      args.add("-progress", progress.toString());
490    }
491
492    args.addAll(extra_args);
493
494    for (AbstractFFmpegInputBuilder<?> input : this.inputs) {
495      args.addAll(input.build(this, pass));
496    }
497
498    if (pass > 0) {
499      args.add("-pass", Integer.toString(pass));
500
501      if (pass_prefix != null) {
502        args.add("-passlogfile", Paths.get(pass_directory, pass_prefix).toString());
503      }
504    }
505
506    if (!Strings.isNullOrEmpty(audioFilter)) {
507      args.add("-af", audioFilter);
508    }
509
510    if (!Strings.isNullOrEmpty(videoFilter)) {
511      args.add("-vf", videoFilter);
512    }
513
514    if (!Strings.isNullOrEmpty(complexFilter)) {
515      log.warn(
516          "Using FFmpegBuilder#setComplexFilter is deprecated."
517              + " Specify it on the outputStream instead");
518      args.add("-filter_complex", complexFilter);
519    }
520
521    if (qscale != null) {
522      args.add("-qscale:a", qscale.toString());
523    }
524
525    for (AbstractFFmpegOutputBuilder<?> output : this.outputs) {
526      args.addAll(output.build(this, pass));
527    }
528
529    return args.build();
530  }
531}