001package net.bramp.ffmpeg.builder;
002
003import static com.google.common.base.Preconditions.checkArgument;
004import static com.google.common.base.Preconditions.checkState;
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 com.google.errorprone.annotations.InlineMe;
011import java.net.URI;
012import java.util.List;
013import java.util.regex.Pattern;
014import javax.annotation.CheckReturnValue;
015import net.bramp.ffmpeg.options.AudioEncodingOptions;
016import net.bramp.ffmpeg.options.EncodingOptions;
017import net.bramp.ffmpeg.options.MainEncodingOptions;
018import net.bramp.ffmpeg.options.VideoEncodingOptions;
019import net.bramp.ffmpeg.probe.FFmpegProbeResult;
020
021/** Builds a representation of a single output/encoding setting. */
022@SuppressWarnings({"DeprecatedIsStillUsed", "unchecked"})
023public abstract class AbstractFFmpegOutputBuilder<T extends AbstractFFmpegOutputBuilder<T>>
024    extends AbstractFFmpegStreamBuilder<T> {
025
026  static final Pattern trailingZero = Pattern.compile("\\.0*$");
027
028  /**
029   * The constant rate factor (CRF).
030   *
031   * @deprecated Use {@link #getConstantRateFactor()} instead.
032   */
033  @Deprecated public Double constantRateFactor;
034
035  /**
036   * The audio sample format.
037   *
038   * @deprecated Use {@link #getAudioSampleFormat()} instead.
039   */
040  @Deprecated public String audio_sample_format;
041
042  /**
043   * The audio bit rate.
044   *
045   * @deprecated Use {@link #getAudioBitRate()} instead.
046   */
047  @Deprecated public long audio_bit_rate;
048
049  /**
050   * The audio quality.
051   *
052   * @deprecated Use {@link #getAudioQuality()} instead.
053   */
054  @Deprecated public Double audio_quality;
055
056  /**
057   * The audio bitstream filter.
058   *
059   * @deprecated Use {@link #getVideoBitStreamFilter()} instead.
060   */
061  @Deprecated public String audio_bit_stream_filter;
062
063  /**
064   * The audio filter string.
065   *
066   * @deprecated Use {@link #getAudioFilter()} instead.
067   */
068  @Deprecated public String audio_filter;
069
070  /**
071   * The video bit rate.
072   *
073   * @deprecated Use {@link #getVideoBitRate()} instead.
074   */
075  @Deprecated public long video_bit_rate;
076
077  /**
078   * The video quality.
079   *
080   * @deprecated Use {@link #getVideoQuality()} instead.
081   */
082  @Deprecated public Double video_quality;
083
084  /**
085   * The video encoding preset.
086   *
087   * @deprecated Use {@link #getVideoPreset()} instead.
088   */
089  @Deprecated public String video_preset;
090
091  /**
092   * The video filter string.
093   *
094   * @deprecated Use {@link #getVideoFilter()} instead.
095   */
096  @Deprecated public String video_filter;
097
098  /**
099   * The video bitstream filter.
100   *
101   * @deprecated Use {@link #getVideoBitStreamFilter()} instead.
102   */
103  @Deprecated public String video_bit_stream_filter;
104
105  /**
106   * Specifies the number of b-frames ffmpeg is allowed to use. 0 will disable b-frames, null will
107   * let ffmpeg decide.
108   */
109  protected Integer bFrames;
110
111  protected String complexFilter;
112
113  /** Constructs a default output builder. */
114  public AbstractFFmpegOutputBuilder() {
115    super();
116  }
117
118  /** Constructs an output builder with the given parent and filename. */
119  protected AbstractFFmpegOutputBuilder(FFmpegBuilder parent, String filename) {
120    super(parent, filename);
121  }
122
123  /** Constructs an output builder with the given parent and URI. */
124  protected AbstractFFmpegOutputBuilder(FFmpegBuilder parent, URI uri) {
125    super(parent, uri);
126  }
127
128  /** Sets the constant rate factor (CRF) for encoding quality. */
129  public T setConstantRateFactor(double factor) {
130    checkArgument(factor >= 0, "constant rate factor must be greater or equal to zero");
131    this.constantRateFactor = factor;
132    return (T) this;
133  }
134
135  /** Sets the video bit rate in bits per second. */
136  public T setVideoBitRate(long bit_rate) {
137    checkArgument(bit_rate > 0, "bit rate must be positive");
138    this.video_enabled = true;
139    this.video_bit_rate = bit_rate;
140    return (T) this;
141  }
142
143  /** Sets the video quality factor. */
144  public T setVideoQuality(double quality) {
145    checkArgument(quality > 0, "quality must be positive");
146    this.video_enabled = true;
147    this.video_quality = quality;
148    return (T) this;
149  }
150
151  /** Sets the video bit stream filter. */
152  public T setVideoBitStreamFilter(String filter) {
153    this.video_bit_stream_filter = checkNotEmpty(filter, "filter must not be empty");
154    return (T) this;
155  }
156
157  /**
158   * Sets a video preset to use.
159   *
160   * <p>Uses `-vpre`.
161   *
162   * @param preset the preset
163   * @return this
164   */
165  public T setVideoPreset(String preset) {
166    this.video_enabled = true;
167    this.video_preset = checkNotEmpty(preset, "video preset must not be empty");
168    return (T) this;
169  }
170
171  /**
172   * Sets the number of b-frames ffmpeg is allowed to use. 0 means: Do not use b-frames at all
173   *
174   * @param bFrames number of b-frames
175   * @return this
176   */
177  public T setBFrames(int bFrames) {
178    this.bFrames = bFrames;
179    return (T) this;
180  }
181
182  /**
183   * Sets Video Filter.
184   *
185   * <p>TODO Build a fluent Filter builder
186   *
187   * @param filter The video filter.
188   * @return this
189   */
190  public T setVideoFilter(String filter) {
191    this.video_enabled = true;
192    this.video_filter = checkNotEmpty(filter, "filter must not be empty");
193    return (T) this;
194  }
195
196  /**
197   * Sets the audio bit depth.
198   *
199   * @param bit_depth The sample format, one of the net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_* constants.
200   * @return this
201   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_U8
202   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_S16
203   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_S32
204   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_FLT
205   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_DBL
206   * @deprecated use {@link #setAudioSampleFormat} instead.
207   */
208  @Deprecated
209  @InlineMe(replacement = "this.setAudioSampleFormat(bit_depth)")
210  public final T setAudioBitDepth(String bit_depth) {
211    return setAudioSampleFormat(bit_depth);
212  }
213
214  /**
215   * Sets the audio sample format.
216   *
217   * @param sample_format The sample format, one of the net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_*
218   *     constants.
219   * @return this
220   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_U8
221   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_S16
222   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_S32
223   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_FLT
224   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_DBL
225   */
226  public T setAudioSampleFormat(String sample_format) {
227    this.audio_enabled = true;
228    this.audio_sample_format = checkNotEmpty(sample_format, "sample format must not be empty");
229    return (T) this;
230  }
231
232  /**
233   * Sets the Audio bit rate.
234   *
235   * @param bit_rate Audio bitrate in bits per second.
236   * @return this
237   */
238  public T setAudioBitRate(long bit_rate) {
239    checkArgument(bit_rate > 0, "bit rate must be positive");
240    this.audio_enabled = true;
241    this.audio_bit_rate = bit_rate;
242    return (T) this;
243  }
244
245  /** Sets the audio quality factor. */
246  public T setAudioQuality(double quality) {
247    checkArgument(quality > 0, "quality must be positive");
248    this.audio_enabled = true;
249    this.audio_quality = quality;
250    return (T) this;
251  }
252
253  /** Sets the audio bitstream filter. */
254  public T setAudioBitStreamFilter(String filter) {
255    this.audio_enabled = true;
256    this.audio_bit_stream_filter = checkNotEmpty(filter, "filter must not be empty");
257    return (T) this;
258  }
259
260  /** Sets a complex filtergraph. */
261  public T setComplexFilter(String filter) {
262    this.complexFilter = checkNotEmpty(filter, "filter must not be empty");
263
264    return (T) this;
265  }
266
267  /**
268   * Sets Audio Filter.
269   *
270   * <p>TODO Build a fluent Filter builder
271   *
272   * @param filter The audio filter.
273   * @return this
274   */
275  public T setAudioFilter(String filter) {
276    this.audio_enabled = true;
277    this.audio_filter = checkNotEmpty(filter, "filter must not be empty");
278    return (T) this;
279  }
280
281  /**
282   * Returns a representation of this Builder that can be safely serialised.
283   *
284   * <p>NOTE: This method is horribly out of date, and its use should be rethought.
285   *
286   * @return A new EncodingOptions capturing this Builder's state
287   */
288  @CheckReturnValue
289  @Override
290  public EncodingOptions buildOptions() {
291    // TODO When/if modelmapper supports @ConstructorProperties, we map this
292    // object, instead of doing new XXX(...)
293    // https://github.com/jhalterman/modelmapper/issues/44
294    return new EncodingOptions(
295        new MainEncodingOptions(format, startOffset, duration),
296        new AudioEncodingOptions(
297            audio_enabled,
298            audio_codec,
299            audio_channels,
300            audio_sample_rate,
301            audio_sample_format,
302            audio_bit_rate,
303            audio_quality),
304        new VideoEncodingOptions(
305            video_enabled,
306            video_codec,
307            video_frame_rate,
308            video_width,
309            video_height,
310            video_bit_rate,
311            video_frames,
312            video_filter,
313            video_preset));
314  }
315
316  @CheckReturnValue
317  @Override
318  protected List<String> build(int pass) {
319    Preconditions.checkState(parent != null, "Can not build without parent being set");
320
321    return build(parent, pass);
322  }
323
324  /**
325   * Builds the arguments.
326   *
327   * @param parent The parent FFmpegBuilder
328   * @param pass The particular pass. For one-pass this value will be zero, for multi-pass, it will
329   *     be 1 for the first pass, 2 for the second, and so on.
330   * @return The arguments
331   */
332  @CheckReturnValue
333  @Override
334  protected List<String> build(FFmpegBuilder parent, int pass) {
335    if (pass > 0) {
336      checkArgument(
337          targetSize != 0 || video_bit_rate != 0,
338          "Target size, or video bitrate must be specified when using two-pass");
339
340      checkArgument(format != null, "Format must be specified when using two-pass");
341    }
342
343    if (targetSize > 0) {
344      checkState(parent.inputs.size() == 1, "Target size does not support multiple inputs");
345
346      checkArgument(
347          constantRateFactor == null, "Target size can not be used with constantRateFactor");
348
349      AbstractFFmpegInputBuilder<?> firstInput = parent.inputs.iterator().next();
350      FFmpegProbeResult input = firstInput.getProbeResult();
351
352      checkState(input != null, "Target size must be used with setInput(FFmpegProbeResult)");
353
354      // TODO factor in start time and/or number of frames
355
356      double durationInSeconds = input.format.duration;
357      long totalBitRate =
358          (long) Math.floor((targetSize * 8) / durationInSeconds) - pass_padding_bitrate;
359
360      // TODO Calculate audioBitRate
361
362      if (video_enabled && video_bit_rate == 0) {
363        // Video (and possibly audio)
364        long audioBitRate = audio_enabled ? audio_bit_rate : 0;
365        video_bit_rate = totalBitRate - audioBitRate;
366      } else if (audio_enabled && audio_bit_rate == 0) {
367        // Just Audio
368        audio_bit_rate = totalBitRate;
369      }
370    }
371
372    return super.build(parent, pass);
373  }
374
375  /**
376   * Returns a double formatted as a string. If the double is an integer, then trailing zeros are
377   * striped.
378   *
379   * @param d the double to format.
380   * @return The formatted double.
381   */
382  protected static String formatDecimalInteger(double d) {
383    return trailingZero.matcher(String.valueOf(d)).replaceAll("");
384  }
385
386  @Override
387  protected void addGlobalFlags(FFmpegBuilder parent, ImmutableList.Builder<String> args) {
388    super.addGlobalFlags(parent, args);
389
390    if (constantRateFactor != null) {
391      args.add("-crf", formatDecimalInteger(constantRateFactor));
392    }
393
394    if (complexFilter != null) {
395      args.add("-filter_complex", complexFilter);
396    }
397  }
398
399  @Override
400  protected void addVideoFlags(FFmpegBuilder parent, ImmutableList.Builder<String> args) {
401    super.addVideoFlags(parent, args);
402
403    if (video_bit_rate > 0 && video_quality != null) {
404      // I'm not sure, but it seems video_quality overrides video_bit_rate, so don't allow both
405      throw new IllegalStateException("Only one of video_bit_rate and video_quality can be set");
406    }
407
408    if (video_bit_rate > 0) {
409      args.add("-b:v", String.valueOf(video_bit_rate));
410    }
411
412    if (video_quality != null) {
413      args.add("-qscale:v", formatDecimalInteger(video_quality));
414    }
415
416    if (!Strings.isNullOrEmpty(video_preset)) {
417      args.add("-vpre", video_preset);
418    }
419
420    if (!Strings.isNullOrEmpty(video_filter)) {
421      checkState(
422          parent.inputs.size() == 1,
423          "Video filter only works with one input, instead use setComplexVideoFilter(..)");
424      args.add("-vf", video_filter);
425    }
426
427    if (!Strings.isNullOrEmpty(video_bit_stream_filter)) {
428      args.add("-bsf:v", video_bit_stream_filter);
429    }
430
431    if (bFrames != null) {
432      args.add("-bf", Integer.toString(bFrames));
433    }
434  }
435
436  @Override
437  protected void addAudioFlags(ImmutableList.Builder<String> args) {
438    super.addAudioFlags(args);
439
440    if (!Strings.isNullOrEmpty(audio_sample_format)) {
441      args.add("-sample_fmt", audio_sample_format);
442    }
443
444    if (audio_bit_rate > 0 && audio_quality != null && throwWarnings) {
445      // I'm not sure, but it seems audio_quality overrides audio_bit_rate, so don't allow both
446      throw new IllegalStateException("Only one of audio_bit_rate and audio_quality can be set");
447    }
448
449    if (audio_bit_rate > 0) {
450      args.add("-b:a", String.valueOf(audio_bit_rate));
451    }
452
453    if (audio_quality != null) {
454      args.add("-qscale:a", formatDecimalInteger(audio_quality));
455    }
456
457    if (!Strings.isNullOrEmpty(audio_bit_stream_filter)) {
458      args.add("-bsf:a", audio_bit_stream_filter);
459    }
460
461    if (!Strings.isNullOrEmpty(audio_filter)) {
462      args.add("-af", audio_filter);
463    }
464  }
465
466  @Override
467  protected void addSourceTarget(int pass, ImmutableList.Builder<String> args) {
468    if (filename != null && uri != null) {
469      throw new IllegalStateException("Only one of filename and uri can be set");
470    }
471
472    // Output
473    if (pass == 1) {
474      args.add(DEVNULL);
475    } else if (filename != null) {
476      args.add(filename);
477    } else if (uri != null) {
478      args.add(uri.toString());
479    } else {
480      assert false;
481    }
482  }
483
484  @CheckReturnValue
485  @Override
486  protected T getThis() {
487    return (T) this;
488  }
489
490  public Double getConstantRateFactor() {
491    return constantRateFactor;
492  }
493
494  public String getAudioSampleFormat() {
495    return audio_sample_format;
496  }
497
498  public long getAudioBitRate() {
499    return audio_bit_rate;
500  }
501
502  public Double getAudioQuality() {
503    return audio_quality;
504  }
505
506  public String getAudioBitStreamFilter() {
507    return audio_bit_stream_filter;
508  }
509
510  public String getAudioFilter() {
511    return audio_filter;
512  }
513
514  public long getVideoBitRate() {
515    return video_bit_rate;
516  }
517
518  public Double getVideoQuality() {
519    return video_quality;
520  }
521
522  public String getVideoPreset() {
523    return video_preset;
524  }
525
526  public String getVideoFilter() {
527    return video_filter;
528  }
529
530  public String getVideoBitStreamFilter() {
531    return video_bit_stream_filter;
532  }
533
534  public String getComplexFilter() {
535    return complexFilter;
536  }
537}