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.FFmpegUtils.toTimecode;
006import static net.bramp.ffmpeg.Preconditions.checkNotEmpty;
007import static net.bramp.ffmpeg.Preconditions.checkValidStream;
008import static net.bramp.ffmpeg.builder.MetadataSpecifier.checkValidKey;
009
010import com.google.common.base.Preconditions;
011import com.google.common.base.Strings;
012import com.google.common.collect.ImmutableList;
013import java.net.URI;
014import java.util.ArrayList;
015import java.util.List;
016import java.util.concurrent.TimeUnit;
017import net.bramp.ffmpeg.modelmapper.Mapper;
018import net.bramp.ffmpeg.options.AudioEncodingOptions;
019import net.bramp.ffmpeg.options.EncodingOptions;
020import net.bramp.ffmpeg.options.MainEncodingOptions;
021import net.bramp.ffmpeg.options.VideoEncodingOptions;
022import org.apache.commons.lang3.SystemUtils;
023import org.apache.commons.lang3.math.Fraction;
024
025/**
026 * This abstract class holds flags that are both applicable to input and output streams in the
027 * ffmpeg command, while flags that apply to a particular direction (input/output) are located in
028 * {@link FFmpegOutputBuilder}. <br>
029 * <br>
030 * All possible flags can be found in the <a href="https://ffmpeg.org/ffmpeg.html#Options">official
031 * ffmpeg page</a> The discrimination criteria for flag location are the specifiers for each command
032 *
033 * <ul>
034 *   <li>AbstractFFmpegStreamBuilder
035 *       <ul>
036 *         <li>(input/output): <code>-t duration (input/output)</code>
037 *         <li>(input/output,per-stream): <code>
038 *             -codec[:stream_specifier] codec (input/output,per-stream)</code>
039 *         <li>(global): <code>-filter_threads nb_threads (global)</code>
040 *       </ul>
041 *   <li>FFmpegInputBuilder
042 *       <ul>
043 *         <li>(input): <code>-muxdelay seconds (input)</code>
044 *         <li>(input,per-stream): <code>-guess_layout_max channels (input,per-stream)</code>
045 *       </ul>
046 *   <li>FFmpegOutputBuilder
047 *       <ul>
048 *         <li>(output): <code>-atag fourcc/tag (output)</code>
049 *         <li>(output,per-stream): <code>
050 *             -bsf[:stream_specifier] bitstream_filters (output,per-stream)</code>
051 *       </ul>
052 * </ul>
053 *
054 * @param <T> A concrete class that extends from the AbstractFFmpegStreamBuilder
055 */
056public abstract class AbstractFFmpegStreamBuilder<T extends AbstractFFmpegStreamBuilder<T>> {
057
058  private static final String DEVNULL = SystemUtils.IS_OS_WINDOWS ? "NUL" : "/dev/null";
059
060  final FFmpegBuilder parent;
061
062  /** Output filename or uri. Only one may be set */
063  public String filename;
064
065  public URI uri;
066
067  public String format;
068
069  public Long startOffset; // in milliseconds
070  public Long duration; // in milliseconds
071
072  public final List<String> meta_tags = new ArrayList<>();
073
074  public boolean audio_enabled = true;
075  public String audio_codec;
076  public int audio_channels;
077  public int audio_sample_rate;
078  public String audio_preset;
079
080  public boolean video_enabled = true;
081  public String video_codec;
082  public boolean video_copyinkf;
083  public Fraction video_frame_rate;
084  public int video_width;
085  public int video_height;
086  public String video_size;
087  public String video_movflags;
088  public Integer video_frames;
089  public String video_pixel_format;
090
091  public boolean subtitle_enabled = true;
092  public String subtitle_preset;
093
094  public String preset;
095  public String presetFilename;
096  public final List<String> extra_args = new ArrayList<>();
097
098  public FFmpegBuilder.Strict strict = FFmpegBuilder.Strict.NORMAL;
099
100  public long targetSize = 0; // in bytes
101  public long pass_padding_bitrate = 1024; // in bits per second
102
103  public boolean throwWarnings = true; // TODO Either delete this, or apply it consistently
104
105  protected AbstractFFmpegStreamBuilder() {
106    this.parent = null;
107  }
108
109  protected AbstractFFmpegStreamBuilder(FFmpegBuilder parent, String filename) {
110    this.parent = checkNotNull(parent);
111    this.filename = checkNotEmpty(filename, "filename must not be empty");
112  }
113
114  protected AbstractFFmpegStreamBuilder(FFmpegBuilder parent, URI uri) {
115    this.parent = checkNotNull(parent);
116    this.uri = checkValidStream(uri);
117  }
118
119  protected abstract T getThis();
120
121  public T useOptions(EncodingOptions opts) {
122    Mapper.map(opts, this);
123    return getThis();
124  }
125
126  public T useOptions(MainEncodingOptions opts) {
127    Mapper.map(opts, this);
128    return getThis();
129  }
130
131  public T useOptions(AudioEncodingOptions opts) {
132    Mapper.map(opts, this);
133    return getThis();
134  }
135
136  public T useOptions(VideoEncodingOptions opts) {
137    Mapper.map(opts, this);
138    return getThis();
139  }
140
141  public T disableVideo() {
142    this.video_enabled = false;
143    return getThis();
144  }
145
146  public T disableAudio() {
147    this.audio_enabled = false;
148    return getThis();
149  }
150
151  public T disableSubtitle() {
152    this.subtitle_enabled = false;
153    return getThis();
154  }
155
156  /**
157   * Sets a file to use containing presets.
158   *
159   * <p>Uses `-fpre`.
160   *
161   * @param presetFilename the preset by filename
162   * @return this
163   */
164  public T setPresetFilename(String presetFilename) {
165    this.presetFilename = checkNotEmpty(presetFilename, "file preset must not be empty");
166    return getThis();
167  }
168
169  /**
170   * Sets a preset by name (this only works with some codecs).
171   *
172   * <p>Uses `-preset`.
173   *
174   * @param preset the preset
175   * @return this
176   */
177  public T setPreset(String preset) {
178    this.preset = checkNotEmpty(preset, "preset must not be empty");
179    return getThis();
180  }
181
182  public T setFilename(String filename) {
183    this.filename = checkNotEmpty(filename, "filename must not be empty");
184    return getThis();
185  }
186
187  public String getFilename() {
188    return filename;
189  }
190
191  public T setUri(URI uri) {
192    this.uri = checkValidStream(uri);
193    return getThis();
194  }
195
196  public URI getUri() {
197    return uri;
198  }
199
200  public T setFormat(String format) {
201    this.format = checkNotEmpty(format, "format must not be empty");
202    return getThis();
203  }
204
205  public T setVideoCodec(String codec) {
206    this.video_enabled = true;
207    this.video_codec = checkNotEmpty(codec, "codec must not be empty");
208    return getThis();
209  }
210
211  public T setVideoCopyInkf(boolean copyinkf) {
212    this.video_enabled = true;
213    this.video_copyinkf = copyinkf;
214    return getThis();
215  }
216
217  public T setVideoMovFlags(String movflags) {
218    this.video_enabled = true;
219    this.video_movflags = checkNotEmpty(movflags, "movflags must not be empty");
220    return getThis();
221  }
222
223  /**
224   * Sets the video's frame rate
225   *
226   * @param frame_rate Frames per second
227   * @return this
228   * @see net.bramp.ffmpeg.FFmpeg#FPS_30
229   * @see net.bramp.ffmpeg.FFmpeg#FPS_29_97
230   * @see net.bramp.ffmpeg.FFmpeg#FPS_24
231   * @see net.bramp.ffmpeg.FFmpeg#FPS_23_976
232   */
233  public T setVideoFrameRate(Fraction frame_rate) {
234    this.video_enabled = true;
235    this.video_frame_rate = checkNotNull(frame_rate);
236    return getThis();
237  }
238
239  /**
240   * Set the video frame rate in terms of frames per interval. For example 24fps would be 24/1,
241   * however NTSC TV at 23.976fps would be 24000 per 1001.
242   *
243   * @param frames The number of frames within the given seconds
244   * @param per The number of seconds
245   * @return this
246   */
247  public T setVideoFrameRate(int frames, int per) {
248    return setVideoFrameRate(Fraction.getFraction(frames, per));
249  }
250
251  public T setVideoFrameRate(double frame_rate) {
252    return setVideoFrameRate(Fraction.getFraction(frame_rate));
253  }
254
255  /**
256   * Set the number of video frames to record.
257   *
258   * @param frames The number of frames
259   * @return this
260   */
261  public T setFrames(int frames) {
262    this.video_enabled = true;
263    this.video_frames = frames;
264    return getThis();
265  }
266
267  protected static boolean isValidSize(int widthOrHeight) {
268    return widthOrHeight > 0 || widthOrHeight == -1;
269  }
270
271  public T setVideoWidth(int width) {
272    checkArgument(isValidSize(width), "Width must be -1 or greater than zero");
273
274    this.video_enabled = true;
275    this.video_width = width;
276    return getThis();
277  }
278
279  public T setVideoHeight(int height) {
280    checkArgument(isValidSize(height), "Height must be -1 or greater than zero");
281
282    this.video_enabled = true;
283    this.video_height = height;
284    return getThis();
285  }
286
287  public T setVideoResolution(int width, int height) {
288    checkArgument(
289        isValidSize(width) && isValidSize(height),
290        "Both width and height must be -1 or greater than zero");
291
292    this.video_enabled = true;
293    this.video_width = width;
294    this.video_height = height;
295    return getThis();
296  }
297
298  /**
299   * Sets video resolution based on an abbreviation, e.g. "ntsc" for 720x480, or "vga" for 640x480
300   *
301   * @see <a href="https://www.ffmpeg.org/ffmpeg-utils.html#Video-size">ffmpeg video size</a>
302   * @param abbreviation The abbreviation size. No validation is done, instead the value is passed
303   *     as is to ffmpeg.
304   * @return this
305   */
306  public T setVideoResolution(String abbreviation) {
307    this.video_enabled = true;
308    this.video_size = checkNotEmpty(abbreviation, "video abbreviation must not be empty");
309    return getThis();
310  }
311
312  public T setVideoPixelFormat(String format) {
313    this.video_enabled = true;
314    this.video_pixel_format = checkNotEmpty(format, "format must not be empty");
315    return getThis();
316  }
317
318  /**
319   * Add metadata on output streams. Which keys are possible depends on the used codec.
320   *
321   * @param key Metadata key, e.g. "comment"
322   * @param value Value to set for key
323   * @return this
324   */
325  public T addMetaTag(String key, String value) {
326    checkValidKey(key);
327    checkNotEmpty(value, "value must not be empty");
328    meta_tags.add("-metadata");
329    meta_tags.add(key + "=" + value);
330    return getThis();
331  }
332
333  /**
334   * Add metadata on output streams. Which keys are possible depends on the used codec.
335   *
336   * <pre>{@code
337   * import static net.bramp.ffmpeg.builder.MetadataSpecifier.*;
338   * import static net.bramp.ffmpeg.builder.StreamSpecifier.*;
339   * import static net.bramp.ffmpeg.builder.StreamSpecifierType.*;
340   *
341   * new FFmpegBuilder()
342   *   .addMetaTag("title", "Movie Title") // Annotate whole file
343   *   .addMetaTag(chapter(0), "author", "Bob") // Annotate first chapter
344   *   .addMetaTag(program(0), "comment", "Awesome") // Annotate first program
345   *   .addMetaTag(stream(0), "copyright", "Megacorp") // Annotate first stream
346   *   .addMetaTag(stream(Video), "framerate", "24fps") // Annotate all video streams
347   *   .addMetaTag(stream(Video, 0), "artist", "Joe") // Annotate first video stream
348   *   .addMetaTag(stream(Audio, 0), "language", "eng") // Annotate first audio stream
349   *   .addMetaTag(stream(Subtitle, 0), "language", "fre") // Annotate first subtitle stream
350   *   .addMetaTag(usable(), "year", "2010") // Annotate all streams with a usable configuration
351   * }</pre>
352   *
353   * @param spec Metadata specifier, e.g `MetadataSpec.stream(Audio, 0)`
354   * @param key Metadata key, e.g. "comment"
355   * @param value Value to set for key
356   * @return this
357   */
358  public T addMetaTag(MetadataSpecifier spec, String key, String value) {
359    checkValidKey(key);
360    checkNotEmpty(value, "value must not be empty");
361    meta_tags.add("-metadata:" + spec.spec());
362    meta_tags.add(key + "=" + value);
363    return getThis();
364  }
365
366  public T setAudioCodec(String codec) {
367    this.audio_enabled = true;
368    this.audio_codec = checkNotEmpty(codec, "codec must not be empty");
369    return getThis();
370  }
371
372  /**
373   * Sets the number of audio channels
374   *
375   * @param channels Number of channels
376   * @return this
377   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_MONO
378   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_STEREO
379   */
380  public T setAudioChannels(int channels) {
381    checkArgument(channels > 0, "channels must be positive");
382    this.audio_enabled = true;
383    this.audio_channels = channels;
384    return getThis();
385  }
386
387  /**
388   * Sets the Audio sample rate, for example 44_000.
389   *
390   * @param sample_rate Samples measured in Hz
391   * @return this
392   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_8000
393   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_11025
394   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_12000
395   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_16000
396   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_22050
397   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_32000
398   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_44100
399   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_48000
400   * @see net.bramp.ffmpeg.FFmpeg#AUDIO_SAMPLE_96000
401   */
402  public T setAudioSampleRate(int sample_rate) {
403    checkArgument(sample_rate > 0, "sample rate must be positive");
404    this.audio_enabled = true;
405    this.audio_sample_rate = sample_rate;
406    return getThis();
407  }
408
409  /**
410   * Target output file size (in bytes)
411   *
412   * @param targetSize The target size in bytes
413   * @return this
414   */
415  public T setTargetSize(long targetSize) {
416    checkArgument(targetSize > 0, "target size must be positive");
417    this.targetSize = targetSize;
418    return getThis();
419  }
420
421  /**
422   * Decodes but discards input until the offset.
423   *
424   * @param offset The offset
425   * @param units The units the offset is in
426   * @return this
427   */
428  public T setStartOffset(long offset, TimeUnit units) {
429    checkNotNull(units);
430
431    this.startOffset = units.toMillis(offset);
432
433    return getThis();
434  }
435
436  /**
437   * Stop writing the output after duration is reached.
438   *
439   * @param duration The duration
440   * @param units The units the duration is in
441   * @return this
442   */
443  public T setDuration(long duration, TimeUnit units) {
444    checkNotNull(units);
445
446    this.duration = units.toMillis(duration);
447
448    return getThis();
449  }
450
451  public T setStrict(FFmpegBuilder.Strict strict) {
452    this.strict = checkNotNull(strict);
453    return getThis();
454  }
455
456  /**
457   * When doing multi-pass we add a little extra padding, to ensure we reach our target
458   *
459   * @param bitrate bit rate
460   * @return this
461   */
462  public T setPassPaddingBitrate(long bitrate) {
463    checkArgument(bitrate > 0, "bitrate must be positive");
464    this.pass_padding_bitrate = bitrate;
465    return getThis();
466  }
467
468  /**
469   * Sets a audio preset to use.
470   *
471   * <p>Uses `-apre`.
472   *
473   * @param preset the preset
474   * @return this
475   */
476  public T setAudioPreset(String preset) {
477    this.audio_enabled = true;
478    this.audio_preset = checkNotEmpty(preset, "audio preset must not be empty");
479    return getThis();
480  }
481
482  /**
483   * Sets a subtitle preset to use.
484   *
485   * <p>Uses `-spre`.
486   *
487   * @param preset the preset
488   * @return this
489   */
490  public T setSubtitlePreset(String preset) {
491    this.subtitle_enabled = true;
492    this.subtitle_preset = checkNotEmpty(preset, "subtitle preset must not be empty");
493    return getThis();
494  }
495
496  /**
497   * Add additional output arguments (for flags which aren't currently supported).
498   *
499   * @param values The extra arguments
500   * @return this
501   */
502  public T addExtraArgs(String... values) {
503    checkArgument(values.length > 0, "one or more values must be supplied");
504    checkNotEmpty(values[0], "first extra arg may not be empty");
505
506    for (String value : values) {
507      extra_args.add(checkNotNull(value));
508    }
509    return getThis();
510  }
511
512  /**
513   * Finished with this output
514   *
515   * @return the parent FFmpegBuilder
516   */
517  public FFmpegBuilder done() {
518    Preconditions.checkState(parent != null, "Can not call done without parent being set");
519    return parent;
520  }
521
522  /**
523   * Returns a representation of this Builder that can be safely serialised.
524   *
525   * <p>NOTE: This method is horribly out of date, and its use should be rethought.
526   *
527   * @return A new EncodingOptions capturing this Builder's state
528   */
529  public abstract EncodingOptions buildOptions();
530
531  protected List<String> build(int pass) {
532    Preconditions.checkState(parent != null, "Can not build without parent being set");
533    return build(parent, pass);
534  }
535
536  /**
537   * Builds the arguments
538   *
539   * @param parent The parent FFmpegBuilder
540   * @param pass The particular pass. For one-pass this value will be zero, for multi-pass, it will
541   *     be 1 for the first pass, 2 for the second, and so on.
542   * @return The arguments
543   */
544  protected List<String> build(FFmpegBuilder parent, int pass) {
545    checkNotNull(parent);
546
547    if (pass > 0) {
548      // TODO Write a test for this:
549      checkArgument(format != null, "Format must be specified when using two-pass");
550    }
551
552    ImmutableList.Builder<String> args = new ImmutableList.Builder<>();
553
554    addGlobalFlags(parent, args);
555
556    if (video_enabled) {
557      addVideoFlags(parent, args);
558    } else {
559      args.add("-vn");
560    }
561
562    if (audio_enabled && pass != 1) {
563      addAudioFlags(args);
564    } else {
565      args.add("-an");
566    }
567
568    if (subtitle_enabled) {
569      if (!Strings.isNullOrEmpty(subtitle_preset)) {
570        args.add("-spre", subtitle_preset);
571      }
572    } else {
573      args.add("-sn");
574    }
575
576    args.addAll(extra_args);
577
578    if (filename != null && uri != null) {
579      throw new IllegalStateException("Only one of filename and uri can be set");
580    }
581
582    // Output
583    if (pass == 1) {
584      args.add(DEVNULL);
585    } else if (filename != null) {
586      args.add(filename);
587    } else if (uri != null) {
588      args.add(uri.toString());
589    } else {
590      assert (false);
591    }
592
593    return args.build();
594  }
595
596  protected void addGlobalFlags(FFmpegBuilder parent, ImmutableList.Builder<String> args) {
597    if (strict != FFmpegBuilder.Strict.NORMAL) {
598      args.add("-strict", strict.toString());
599    }
600
601    if (!Strings.isNullOrEmpty(format)) {
602      args.add("-f", format);
603    }
604
605    if (!Strings.isNullOrEmpty(preset)) {
606      args.add("-preset", preset);
607    }
608
609    if (!Strings.isNullOrEmpty(presetFilename)) {
610      args.add("-fpre", presetFilename);
611    }
612
613    if (startOffset != null) {
614      args.add("-ss", toTimecode(startOffset, TimeUnit.MILLISECONDS));
615    }
616
617    if (duration != null) {
618      args.add("-t", toTimecode(duration, TimeUnit.MILLISECONDS));
619    }
620
621    args.addAll(meta_tags);
622  }
623
624  protected void addAudioFlags(ImmutableList.Builder<String> args) {
625    if (!Strings.isNullOrEmpty(audio_codec)) {
626      args.add("-acodec", audio_codec);
627    }
628
629    if (audio_channels > 0) {
630      args.add("-ac", String.valueOf(audio_channels));
631    }
632
633    if (audio_sample_rate > 0) {
634      args.add("-ar", String.valueOf(audio_sample_rate));
635    }
636
637    if (!Strings.isNullOrEmpty(audio_preset)) {
638      args.add("-apre", audio_preset);
639    }
640  }
641
642  protected void addVideoFlags(FFmpegBuilder parent, ImmutableList.Builder<String> args) {
643    if (video_frames != null) {
644      args.add("-vframes", video_frames.toString());
645    }
646
647    if (!Strings.isNullOrEmpty(video_codec)) {
648      args.add("-vcodec", video_codec);
649    }
650
651    if (!Strings.isNullOrEmpty(video_pixel_format)) {
652      args.add("-pix_fmt", video_pixel_format);
653    }
654
655    if (video_copyinkf) {
656      args.add("-copyinkf");
657    }
658
659    if (!Strings.isNullOrEmpty(video_movflags)) {
660      args.add("-movflags", video_movflags);
661    }
662
663    if (video_size != null) {
664      checkArgument(
665          video_width == 0 && video_height == 0,
666          "Can not specific width or height, as well as an abbreviatied video size");
667      args.add("-s", video_size);
668
669    } else if (video_width != 0 && video_height != 0) {
670      args.add("-s", String.format("%dx%d", video_width, video_height));
671    }
672
673    // TODO What if width is set but heigh isn't. We don't seem to do anything
674
675    if (video_frame_rate != null) {
676      args.add("-r", video_frame_rate.toString());
677    }
678  }
679}