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 java.net.URI;
010import java.util.List;
011import java.util.regex.Pattern;
012import javax.annotation.CheckReturnValue;
013import net.bramp.ffmpeg.options.AudioEncodingOptions;
014import net.bramp.ffmpeg.options.EncodingOptions;
015import net.bramp.ffmpeg.options.MainEncodingOptions;
016import net.bramp.ffmpeg.options.VideoEncodingOptions;
017import net.bramp.ffmpeg.probe.FFmpegProbeResult;
018
019/** Builds a representation of a single output/encoding setting */
020public class FFmpegOutputBuilder extends AbstractFFmpegStreamBuilder<FFmpegOutputBuilder> {
021
022  static final Pattern trailingZero = Pattern.compile("\\.0*$");
023
024  public Double constantRateFactor;
025
026  public String audio_sample_format;
027  public long audio_bit_rate;
028  public Double audio_quality;
029  public String audio_bit_stream_filter;
030  public String audio_filter;
031
032  public long video_bit_rate;
033  public Double video_quality;
034  public String video_preset;
035  public String video_filter;
036  public String video_bit_stream_filter;
037
038  public FFmpegOutputBuilder() {
039    super();
040  }
041
042  protected FFmpegOutputBuilder(FFmpegBuilder parent, String filename) {
043    super(parent, filename);
044  }
045
046  protected FFmpegOutputBuilder(FFmpegBuilder parent, URI uri) {
047    super(parent, uri);
048  }
049
050  public FFmpegOutputBuilder setConstantRateFactor(double factor) {
051    checkArgument(factor >= 0, "constant rate factor must be greater or equal to zero");
052    this.constantRateFactor = factor;
053    return this;
054  }
055
056  public FFmpegOutputBuilder setVideoBitRate(long bit_rate) {
057    checkArgument(bit_rate > 0, "bit rate must be positive");
058    this.video_enabled = true;
059    this.video_bit_rate = bit_rate;
060    return this;
061  }
062
063  public FFmpegOutputBuilder setVideoQuality(double quality) {
064    checkArgument(quality > 0, "quality must be positive");
065    this.video_enabled = true;
066    this.video_quality = quality;
067    return this;
068  }
069
070  public FFmpegOutputBuilder setVideoBitStreamFilter(String filter) {
071    this.video_bit_stream_filter = checkNotEmpty(filter, "filter must not be empty");
072    return this;
073  }
074
075  /**
076   * Sets a video preset to use.
077   *
078   * <p>Uses `-vpre`.
079   *
080   * @param preset the preset
081   * @return this
082   */
083  public FFmpegOutputBuilder setVideoPreset(String preset) {
084    this.video_enabled = true;
085    this.video_preset = checkNotEmpty(preset, "video preset must not be empty");
086    return this;
087  }
088
089  /**
090   * Sets Video Filter
091   *
092   * <p>TODO Build a fluent Filter builder
093   *
094   * @param filter The video filter.
095   * @return this
096   */
097  public FFmpegOutputBuilder setVideoFilter(String filter) {
098    this.video_enabled = true;
099    this.video_filter = checkNotEmpty(filter, "filter must not be empty");
100    return this;
101  }
102
103  /**
104   * Sets the audio bit depth.
105   *
106   * @param bit_depth The sample format, one of the net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_* constants.
107   * @return this
108   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_U8
109   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_S16
110   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_S32
111   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_FLT
112   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_DBL
113   * @deprecated use {@link #setAudioSampleFormat} instead.
114   */
115  @Deprecated
116  public FFmpegOutputBuilder setAudioBitDepth(String bit_depth) {
117    return setAudioSampleFormat(bit_depth);
118  }
119
120  /**
121   * Sets the audio sample format.
122   *
123   * @param sample_format The sample format, one of the net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_*
124   *     constants.
125   * @return this
126   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_U8
127   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_S16
128   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_S32
129   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_FLT
130   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_DBL
131   */
132  public FFmpegOutputBuilder setAudioSampleFormat(String sample_format) {
133    this.audio_enabled = true;
134    this.audio_sample_format = checkNotEmpty(sample_format, "sample format must not be empty");
135    return this;
136  }
137
138  /**
139   * Sets the Audio bit rate
140   *
141   * @param bit_rate Audio bitrate in bits per second.
142   * @return this
143   */
144  public FFmpegOutputBuilder setAudioBitRate(long bit_rate) {
145    checkArgument(bit_rate > 0, "bit rate must be positive");
146    this.audio_enabled = true;
147    this.audio_bit_rate = bit_rate;
148    return this;
149  }
150
151  public FFmpegOutputBuilder setAudioQuality(double quality) {
152    checkArgument(quality > 0, "quality must be positive");
153    this.audio_enabled = true;
154    this.audio_quality = quality;
155    return this;
156  }
157
158  public FFmpegOutputBuilder setAudioBitStreamFilter(String filter) {
159    this.audio_enabled = true;
160    this.audio_bit_stream_filter = checkNotEmpty(filter, "filter must not be empty");
161    return this;
162  }
163
164  /**
165   * Sets Audio Filter
166   *
167   * <p>TODO Build a fluent Filter builder
168   *
169   * @param filter The audio filter.
170   * @return this
171   */
172  public FFmpegOutputBuilder setAudioFilter(String filter) {
173    this.audio_enabled = true;
174    this.audio_filter = checkNotEmpty(filter, "filter must not be empty");
175    return this;
176  }
177
178  /**
179   * Returns a representation of this Builder that can be safely serialised.
180   *
181   * <p>NOTE: This method is horribly out of date, and its use should be rethought.
182   *
183   * @return A new EncodingOptions capturing this Builder's state
184   */
185  @CheckReturnValue
186  @Override
187  public EncodingOptions buildOptions() {
188    // TODO When/if modelmapper supports @ConstructorProperties, we map this
189    // object, instead of doing new XXX(...)
190    // https://github.com/jhalterman/modelmapper/issues/44
191    return new EncodingOptions(
192        new MainEncodingOptions(format, startOffset, duration),
193        new AudioEncodingOptions(
194            audio_enabled,
195            audio_codec,
196            audio_channels,
197            audio_sample_rate,
198            audio_sample_format,
199            audio_bit_rate,
200            audio_quality),
201        new VideoEncodingOptions(
202            video_enabled,
203            video_codec,
204            video_frame_rate,
205            video_width,
206            video_height,
207            video_bit_rate,
208            video_frames,
209            video_filter,
210            video_preset));
211  }
212
213  @CheckReturnValue
214  @Override
215  protected List<String> build(int pass) {
216    Preconditions.checkState(parent != null, "Can not build without parent being set");
217    return build(parent, pass);
218  }
219
220  /**
221   * Builds the arguments
222   *
223   * @param parent The parent FFmpegBuilder
224   * @param pass The particular pass. For one-pass this value will be zero, for multi-pass, it will
225   *     be 1 for the first pass, 2 for the second, and so on.
226   * @return The arguments
227   */
228  @CheckReturnValue
229  @Override
230  protected List<String> build(FFmpegBuilder parent, int pass) {
231    if (pass > 0) {
232      checkArgument(
233          targetSize != 0 || video_bit_rate != 0,
234          "Target size, or video bitrate must be specified when using two-pass");
235    }
236    if (targetSize > 0) {
237      checkState(parent.inputs.size() == 1, "Target size does not support multiple inputs");
238
239      checkArgument(
240          constantRateFactor == null, "Target size can not be used with constantRateFactor");
241
242      String firstInput = parent.inputs.iterator().next();
243      FFmpegProbeResult input = parent.inputProbes.get(firstInput);
244
245      checkState(input != null, "Target size must be used with setInput(FFmpegProbeResult)");
246
247      // TODO factor in start time and/or number of frames
248
249      double durationInSeconds = input.format.duration;
250      long totalBitRate =
251          (long) Math.floor((targetSize * 8) / durationInSeconds) - pass_padding_bitrate;
252
253      // TODO Calculate audioBitRate
254
255      if (video_enabled && video_bit_rate == 0) {
256        // Video (and possibly audio)
257        long audioBitRate = audio_enabled ? audio_bit_rate : 0;
258        video_bit_rate = totalBitRate - audioBitRate;
259      } else if (audio_enabled && audio_bit_rate == 0) {
260        // Just Audio
261        audio_bit_rate = totalBitRate;
262      }
263    }
264
265    return super.build(parent, pass);
266  }
267
268  /**
269   * Returns a double formatted as a string. If the double is an integer, then trailing zeros are
270   * striped.
271   *
272   * @param d the double to format.
273   * @return The formatted double.
274   */
275  protected static String formatDecimalInteger(double d) {
276    return trailingZero.matcher(String.valueOf(d)).replaceAll("");
277  }
278
279  @Override
280  protected void addGlobalFlags(FFmpegBuilder parent, ImmutableList.Builder<String> args) {
281    super.addGlobalFlags(parent, args);
282
283    if (constantRateFactor != null) {
284      args.add("-crf", formatDecimalInteger(constantRateFactor));
285    }
286  }
287
288  @Override
289  protected void addVideoFlags(FFmpegBuilder parent, ImmutableList.Builder<String> args) {
290    super.addVideoFlags(parent, args);
291
292    if (video_bit_rate > 0 && video_quality != null) {
293      // I'm not sure, but it seems video_quality overrides video_bit_rate, so don't allow both
294      throw new IllegalStateException("Only one of video_bit_rate and video_quality can be set");
295    }
296
297    if (video_bit_rate > 0) {
298      args.add("-b:v", String.valueOf(video_bit_rate));
299    }
300
301    if (video_quality != null) {
302      args.add("-qscale:v", formatDecimalInteger(video_quality));
303    }
304
305    if (!Strings.isNullOrEmpty(video_preset)) {
306      args.add("-vpre", video_preset);
307    }
308
309    if (!Strings.isNullOrEmpty(video_filter)) {
310      checkState(
311          parent.inputs.size() == 1,
312          "Video filter only works with one input, instead use setComplexVideoFilter(..)");
313      args.add("-vf", video_filter);
314    }
315
316    if (!Strings.isNullOrEmpty(video_bit_stream_filter)) {
317      args.add("-bsf:v", video_bit_stream_filter);
318    }
319  }
320
321  @Override
322  protected void addAudioFlags(ImmutableList.Builder<String> args) {
323    super.addAudioFlags(args);
324
325    if (!Strings.isNullOrEmpty(audio_sample_format)) {
326      args.add("-sample_fmt", audio_sample_format);
327    }
328
329    if (audio_bit_rate > 0 && audio_quality != null && throwWarnings) {
330      // I'm not sure, but it seems audio_quality overrides audio_bit_rate, so don't allow both
331      throw new IllegalStateException("Only one of audio_bit_rate and audio_quality can be set");
332    }
333
334    if (audio_bit_rate > 0) {
335      args.add("-b:a", String.valueOf(audio_bit_rate));
336    }
337
338    if (audio_quality != null) {
339      args.add("-qscale:a", formatDecimalInteger(audio_quality));
340    }
341
342    if (!Strings.isNullOrEmpty(audio_bit_stream_filter)) {
343      args.add("-bsf:a", audio_bit_stream_filter);
344    }
345
346    if (!Strings.isNullOrEmpty(audio_filter)) {
347      args.add("-af", audio_filter);
348    }
349  }
350
351  @CheckReturnValue
352  @Override
353  protected FFmpegOutputBuilder getThis() {
354    return this;
355  }
356}