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.Preconditions;
008import com.google.common.base.Strings;
009import com.google.common.collect.ImmutableList;
010import java.net.URI;
011import java.util.ArrayList;
012import java.util.List;
013import java.util.Map;
014import java.util.TreeMap;
015import java.util.concurrent.TimeUnit;
016import javax.annotation.CheckReturnValue;
017import net.bramp.ffmpeg.FFmpegUtils;
018import net.bramp.ffmpeg.probe.FFmpegProbeResult;
019
020/**
021 * Builds a ffmpeg command line
022 *
023 * @author bramp
024 */
025public class FFmpegBuilder {
026
027  public enum Strict {
028    VERY, // strictly conform to a older more strict version of the specifications or reference
029    // software
030    STRICT, // strictly conform to all the things in the specificiations no matter what consequences
031    NORMAL, // normal
032    UNOFFICIAL, // allow unofficial extensions
033    EXPERIMENTAL;
034
035    // ffmpeg command line requires these options in lower case
036    @Override
037    public String toString() {
038      return name().toLowerCase();
039    }
040  }
041
042  /** Log level options: https://ffmpeg.org/ffmpeg.html#Generic-options */
043  public enum Verbosity {
044    QUIET,
045    PANIC,
046    FATAL,
047    ERROR,
048    WARNING,
049    INFO,
050    VERBOSE,
051    DEBUG;
052
053    @Override
054    public String toString() {
055      return name().toLowerCase();
056    }
057  }
058
059  // Global Settings
060  boolean override = true;
061  int pass = 0;
062  String pass_directory = "";
063  String pass_prefix;
064  Verbosity verbosity = Verbosity.ERROR;
065  URI progress;
066  String user_agent;
067
068  // Input settings
069  String format;
070  Long startOffset; // in millis
071  boolean read_at_native_frame_rate = false;
072  final List<String> inputs = new ArrayList<>();
073  final Map<String, FFmpegProbeResult> inputProbes = new TreeMap<>();
074
075  final List<String> extra_args = new ArrayList<>();
076
077  // Output
078  final List<FFmpegOutputBuilder> outputs = new ArrayList<>();
079
080  // Filters
081  String audioFilter;
082  String videoFilter;
083  String complexFilter;
084
085  public FFmpegBuilder overrideOutputFiles(boolean override) {
086    this.override = override;
087    return this;
088  }
089
090  public boolean getOverrideOutputFiles() {
091    return this.override;
092  }
093
094  public FFmpegBuilder setPass(int pass) {
095    this.pass = pass;
096    return this;
097  }
098
099  public FFmpegBuilder setPassDirectory(String directory) {
100    this.pass_directory = checkNotNull(directory);
101    return this;
102  }
103
104  public FFmpegBuilder setPassPrefix(String prefix) {
105    this.pass_prefix = checkNotNull(prefix);
106    return this;
107  }
108
109  public FFmpegBuilder setVerbosity(Verbosity verbosity) {
110    checkNotNull(verbosity);
111    this.verbosity = verbosity;
112    return this;
113  }
114
115  public FFmpegBuilder setUserAgent(String userAgent) {
116    this.user_agent = checkNotNull(userAgent);
117    return this;
118  }
119
120  public FFmpegBuilder readAtNativeFrameRate() {
121    this.read_at_native_frame_rate = true;
122    return this;
123  }
124
125  public FFmpegBuilder addInput(FFmpegProbeResult result) {
126    checkNotNull(result);
127    String filename = checkNotNull(result.format).filename;
128    inputProbes.put(filename, result);
129    return addInput(filename);
130  }
131
132  public FFmpegBuilder addInput(String filename) {
133    checkNotNull(filename);
134    inputs.add(filename);
135    return this;
136  }
137
138  protected void clearInputs() {
139    inputs.clear();
140    inputProbes.clear();
141  }
142
143  public FFmpegBuilder setInput(FFmpegProbeResult result) {
144    clearInputs();
145    return addInput(result);
146  }
147
148  public FFmpegBuilder setInput(String filename) {
149    clearInputs();
150    return addInput(filename);
151  }
152
153  public FFmpegBuilder setFormat(String format) {
154    this.format = checkNotNull(format);
155    return this;
156  }
157
158  public FFmpegBuilder setStartOffset(long duration, TimeUnit units) {
159    checkNotNull(units);
160
161    this.startOffset = units.toMillis(duration);
162
163    return this;
164  }
165
166  public FFmpegBuilder addProgress(URI uri) {
167    this.progress = checkNotNull(uri);
168    return this;
169  }
170
171  /**
172   * Sets the complex filter flag.
173   *
174   * @param filter
175   * @return
176   */
177  public FFmpegBuilder setComplexFilter(String filter) {
178    this.complexFilter = checkNotEmpty(filter, "filter must not be empty");
179    return this;
180  }
181
182  /**
183   * Sets the audio filter flag.
184   *
185   * @param filter
186   * @return
187   */
188  public FFmpegBuilder setAudioFilter(String filter) {
189    this.audioFilter = checkNotEmpty(filter, "filter must not be empty");
190    return this;
191  }
192
193  /**
194   * Sets the video filter flag.
195   *
196   * @param filter
197   * @return
198   */
199  public FFmpegBuilder setVideoFilter(String filter) {
200    this.videoFilter = checkNotEmpty(filter, "filter must not be empty");
201    return this;
202  }
203
204  /**
205   * Add additional ouput arguments (for flags which aren't currently supported).
206   *
207   * @param values The extra arguments.
208   * @return this
209   */
210  public FFmpegBuilder addExtraArgs(String... values) {
211    checkArgument(values.length > 0, "one or more values must be supplied");
212    checkNotEmpty(values[0], "first extra arg may not be empty");
213
214    for (String value : values) {
215      extra_args.add(checkNotNull(value));
216    }
217    return this;
218  }
219
220  /**
221   * Adds new output file.
222   *
223   * @param filename output file path
224   * @return A new {@link FFmpegOutputBuilder}
225   */
226  public FFmpegOutputBuilder addOutput(String filename) {
227    FFmpegOutputBuilder output = new FFmpegOutputBuilder(this, filename);
228    outputs.add(output);
229    return output;
230  }
231
232  /**
233   * Adds new output file.
234   *
235   * @param uri output file uri typically a stream
236   * @return A new {@link FFmpegOutputBuilder}
237   */
238  public FFmpegOutputBuilder addOutput(URI uri) {
239    FFmpegOutputBuilder output = new FFmpegOutputBuilder(this, uri);
240    outputs.add(output);
241    return output;
242  }
243
244  /**
245   * Adds an existing FFmpegOutputBuilder. This is similar to calling the other addOuput methods but
246   * instead allows an existing FFmpegOutputBuilder to be used, and reused.
247   *
248   * <pre>
249   * <code>List&lt;String&gt; args = new FFmpegBuilder()
250   *   .addOutput(new FFmpegOutputBuilder()
251   *     .setFilename(&quot;output.flv&quot;)
252   *     .setVideoCodec(&quot;flv&quot;)
253   *   )
254   *   .build();</code>
255   * </pre>
256   *
257   * @param output FFmpegOutputBuilder to add
258   * @return this
259   */
260  public FFmpegBuilder addOutput(FFmpegOutputBuilder output) {
261    outputs.add(output);
262    return this;
263  }
264
265  /**
266   * Create new output (to stdout)
267   *
268   * @return A new {@link FFmpegOutputBuilder}
269   */
270  public FFmpegOutputBuilder addStdoutOutput() {
271    return addOutput("-");
272  }
273
274  @CheckReturnValue
275  public List<String> build() {
276    ImmutableList.Builder<String> args = new ImmutableList.Builder<String>();
277
278    Preconditions.checkArgument(!inputs.isEmpty(), "At least one input must be specified");
279    Preconditions.checkArgument(!outputs.isEmpty(), "At least one output must be specified");
280
281    args.add(override ? "-y" : "-n");
282    args.add("-v", this.verbosity.toString());
283
284    if (user_agent != null) {
285      args.add("-user_agent", user_agent);
286    }
287
288    if (startOffset != null) {
289      args.add("-ss", FFmpegUtils.toTimecode(startOffset, TimeUnit.MILLISECONDS));
290    }
291
292    if (format != null) {
293      args.add("-f", format);
294    }
295
296    if (read_at_native_frame_rate) {
297      args.add("-re");
298    }
299
300    if (progress != null) {
301      args.add("-progress", progress.toString());
302    }
303
304    args.addAll(extra_args);
305
306    for (String input : inputs) {
307      args.add("-i", input);
308    }
309
310    if (pass > 0) {
311      args.add("-pass", Integer.toString(pass));
312
313      if (pass_prefix != null) {
314        args.add("-passlogfile", pass_directory + pass_prefix);
315      }
316    }
317
318    if (!Strings.isNullOrEmpty(audioFilter)) {
319      args.add("-af", audioFilter);
320    }
321
322    if (!Strings.isNullOrEmpty(videoFilter)) {
323      args.add("-vf", videoFilter);
324    }
325
326    if (!Strings.isNullOrEmpty(complexFilter)) {
327      args.add("-filter_complex", complexFilter);
328    }
329
330    for (FFmpegOutputBuilder output : this.outputs) {
331      args.addAll(output.build(this, pass));
332    }
333
334    return args.build();
335  }
336}