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}