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