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}