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