001package net.bramp.ffmpeg.builder; 002 003import static com.google.common.base.Preconditions.checkArgument; 004import static com.google.common.base.Preconditions.checkState; 005import static net.bramp.ffmpeg.Preconditions.checkNotEmpty; 006 007import com.google.common.base.Preconditions; 008import com.google.common.base.Strings; 009import com.google.common.collect.ImmutableList; 010import com.google.errorprone.annotations.InlineMe; 011import java.net.URI; 012import java.util.List; 013import java.util.regex.Pattern; 014import javax.annotation.CheckReturnValue; 015import net.bramp.ffmpeg.options.AudioEncodingOptions; 016import net.bramp.ffmpeg.options.EncodingOptions; 017import net.bramp.ffmpeg.options.MainEncodingOptions; 018import net.bramp.ffmpeg.options.VideoEncodingOptions; 019import net.bramp.ffmpeg.probe.FFmpegProbeResult; 020 021/** Builds a representation of a single output/encoding setting. */ 022@SuppressWarnings({"DeprecatedIsStillUsed", "unchecked"}) 023public abstract class AbstractFFmpegOutputBuilder<T extends AbstractFFmpegOutputBuilder<T>> 024 extends AbstractFFmpegStreamBuilder<T> { 025 026 static final Pattern trailingZero = Pattern.compile("\\.0*$"); 027 028 /** 029 * The constant rate factor (CRF). 030 * 031 * @deprecated Use {@link #getConstantRateFactor()} instead. 032 */ 033 @Deprecated public Double constantRateFactor; 034 035 /** 036 * The audio sample format. 037 * 038 * @deprecated Use {@link #getAudioSampleFormat()} instead. 039 */ 040 @Deprecated public String audio_sample_format; 041 042 /** 043 * The audio bit rate. 044 * 045 * @deprecated Use {@link #getAudioBitRate()} instead. 046 */ 047 @Deprecated public long audio_bit_rate; 048 049 /** 050 * The audio quality. 051 * 052 * @deprecated Use {@link #getAudioQuality()} instead. 053 */ 054 @Deprecated public Double audio_quality; 055 056 /** 057 * The audio bitstream filter. 058 * 059 * @deprecated Use {@link #getVideoBitStreamFilter()} instead. 060 */ 061 @Deprecated public String audio_bit_stream_filter; 062 063 /** 064 * The audio filter string. 065 * 066 * @deprecated Use {@link #getAudioFilter()} instead. 067 */ 068 @Deprecated public String audio_filter; 069 070 /** 071 * The video bit rate. 072 * 073 * @deprecated Use {@link #getVideoBitRate()} instead. 074 */ 075 @Deprecated public long video_bit_rate; 076 077 /** 078 * The video quality. 079 * 080 * @deprecated Use {@link #getVideoQuality()} instead. 081 */ 082 @Deprecated public Double video_quality; 083 084 /** 085 * The video encoding preset. 086 * 087 * @deprecated Use {@link #getVideoPreset()} instead. 088 */ 089 @Deprecated public String video_preset; 090 091 /** 092 * The video filter string. 093 * 094 * @deprecated Use {@link #getVideoFilter()} instead. 095 */ 096 @Deprecated public String video_filter; 097 098 /** 099 * The video bitstream filter. 100 * 101 * @deprecated Use {@link #getVideoBitStreamFilter()} instead. 102 */ 103 @Deprecated public String video_bit_stream_filter; 104 105 /** 106 * Specifies the number of b-frames ffmpeg is allowed to use. 0 will disable b-frames, null will 107 * let ffmpeg decide. 108 */ 109 protected Integer bFrames; 110 111 protected String complexFilter; 112 113 /** Constructs a default output builder. */ 114 public AbstractFFmpegOutputBuilder() { 115 super(); 116 } 117 118 /** Constructs an output builder with the given parent and filename. */ 119 protected AbstractFFmpegOutputBuilder(FFmpegBuilder parent, String filename) { 120 super(parent, filename); 121 } 122 123 /** Constructs an output builder with the given parent and URI. */ 124 protected AbstractFFmpegOutputBuilder(FFmpegBuilder parent, URI uri) { 125 super(parent, uri); 126 } 127 128 /** Sets the constant rate factor (CRF) for encoding quality. */ 129 public T setConstantRateFactor(double factor) { 130 checkArgument(factor >= 0, "constant rate factor must be greater or equal to zero"); 131 this.constantRateFactor = factor; 132 return (T) this; 133 } 134 135 /** Sets the video bit rate in bits per second. */ 136 public T setVideoBitRate(long bit_rate) { 137 checkArgument(bit_rate > 0, "bit rate must be positive"); 138 this.video_enabled = true; 139 this.video_bit_rate = bit_rate; 140 return (T) this; 141 } 142 143 /** Sets the video quality factor. */ 144 public T setVideoQuality(double quality) { 145 checkArgument(quality > 0, "quality must be positive"); 146 this.video_enabled = true; 147 this.video_quality = quality; 148 return (T) this; 149 } 150 151 /** Sets the video bit stream filter. */ 152 public T setVideoBitStreamFilter(String filter) { 153 this.video_bit_stream_filter = checkNotEmpty(filter, "filter must not be empty"); 154 return (T) this; 155 } 156 157 /** 158 * Sets a video preset to use. 159 * 160 * <p>Uses `-vpre`. 161 * 162 * @param preset the preset 163 * @return this 164 */ 165 public T setVideoPreset(String preset) { 166 this.video_enabled = true; 167 this.video_preset = checkNotEmpty(preset, "video preset must not be empty"); 168 return (T) this; 169 } 170 171 /** 172 * Sets the number of b-frames ffmpeg is allowed to use. 0 means: Do not use b-frames at all 173 * 174 * @param bFrames number of b-frames 175 * @return this 176 */ 177 public T setBFrames(int bFrames) { 178 this.bFrames = bFrames; 179 return (T) this; 180 } 181 182 /** 183 * Sets Video Filter. 184 * 185 * <p>TODO Build a fluent Filter builder 186 * 187 * @param filter The video filter. 188 * @return this 189 */ 190 public T setVideoFilter(String filter) { 191 this.video_enabled = true; 192 this.video_filter = checkNotEmpty(filter, "filter must not be empty"); 193 return (T) this; 194 } 195 196 /** 197 * Sets the audio bit depth. 198 * 199 * @param bit_depth The sample format, one of the net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_* constants. 200 * @return this 201 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_U8 202 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_S16 203 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_S32 204 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_FLT 205 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_DBL 206 * @deprecated use {@link #setAudioSampleFormat} instead. 207 */ 208 @Deprecated 209 @InlineMe(replacement = "this.setAudioSampleFormat(bit_depth)") 210 public final T setAudioBitDepth(String bit_depth) { 211 return setAudioSampleFormat(bit_depth); 212 } 213 214 /** 215 * Sets the audio sample format. 216 * 217 * @param sample_format The sample format, one of the net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_* 218 * constants. 219 * @return this 220 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_U8 221 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_S16 222 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_S32 223 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_FLT 224 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_DBL 225 */ 226 public T setAudioSampleFormat(String sample_format) { 227 this.audio_enabled = true; 228 this.audio_sample_format = checkNotEmpty(sample_format, "sample format must not be empty"); 229 return (T) this; 230 } 231 232 /** 233 * Sets the Audio bit rate. 234 * 235 * @param bit_rate Audio bitrate in bits per second. 236 * @return this 237 */ 238 public T setAudioBitRate(long bit_rate) { 239 checkArgument(bit_rate > 0, "bit rate must be positive"); 240 this.audio_enabled = true; 241 this.audio_bit_rate = bit_rate; 242 return (T) this; 243 } 244 245 /** Sets the audio quality factor. */ 246 public T setAudioQuality(double quality) { 247 checkArgument(quality > 0, "quality must be positive"); 248 this.audio_enabled = true; 249 this.audio_quality = quality; 250 return (T) this; 251 } 252 253 /** Sets the audio bitstream filter. */ 254 public T setAudioBitStreamFilter(String filter) { 255 this.audio_enabled = true; 256 this.audio_bit_stream_filter = checkNotEmpty(filter, "filter must not be empty"); 257 return (T) this; 258 } 259 260 /** Sets a complex filtergraph. */ 261 public T setComplexFilter(String filter) { 262 this.complexFilter = checkNotEmpty(filter, "filter must not be empty"); 263 264 return (T) this; 265 } 266 267 /** 268 * Sets Audio Filter. 269 * 270 * <p>TODO Build a fluent Filter builder 271 * 272 * @param filter The audio filter. 273 * @return this 274 */ 275 public T setAudioFilter(String filter) { 276 this.audio_enabled = true; 277 this.audio_filter = checkNotEmpty(filter, "filter must not be empty"); 278 return (T) this; 279 } 280 281 /** 282 * Returns a representation of this Builder that can be safely serialised. 283 * 284 * <p>NOTE: This method is horribly out of date, and its use should be rethought. 285 * 286 * @return A new EncodingOptions capturing this Builder's state 287 */ 288 @CheckReturnValue 289 @Override 290 public EncodingOptions buildOptions() { 291 // TODO When/if modelmapper supports @ConstructorProperties, we map this 292 // object, instead of doing new XXX(...) 293 // https://github.com/jhalterman/modelmapper/issues/44 294 return new EncodingOptions( 295 new MainEncodingOptions(format, startOffset, duration), 296 new AudioEncodingOptions( 297 audio_enabled, 298 audio_codec, 299 audio_channels, 300 audio_sample_rate, 301 audio_sample_format, 302 audio_bit_rate, 303 audio_quality), 304 new VideoEncodingOptions( 305 video_enabled, 306 video_codec, 307 video_frame_rate, 308 video_width, 309 video_height, 310 video_bit_rate, 311 video_frames, 312 video_filter, 313 video_preset)); 314 } 315 316 @CheckReturnValue 317 @Override 318 protected List<String> build(int pass) { 319 Preconditions.checkState(parent != null, "Can not build without parent being set"); 320 321 return build(parent, pass); 322 } 323 324 /** 325 * Builds the arguments. 326 * 327 * @param parent The parent FFmpegBuilder 328 * @param pass The particular pass. For one-pass this value will be zero, for multi-pass, it will 329 * be 1 for the first pass, 2 for the second, and so on. 330 * @return The arguments 331 */ 332 @CheckReturnValue 333 @Override 334 protected List<String> build(FFmpegBuilder parent, int pass) { 335 if (pass > 0) { 336 checkArgument( 337 targetSize != 0 || video_bit_rate != 0, 338 "Target size, or video bitrate must be specified when using two-pass"); 339 340 checkArgument(format != null, "Format must be specified when using two-pass"); 341 } 342 343 if (targetSize > 0) { 344 checkState(parent.inputs.size() == 1, "Target size does not support multiple inputs"); 345 346 checkArgument( 347 constantRateFactor == null, "Target size can not be used with constantRateFactor"); 348 349 AbstractFFmpegInputBuilder<?> firstInput = parent.inputs.iterator().next(); 350 FFmpegProbeResult input = firstInput.getProbeResult(); 351 352 checkState(input != null, "Target size must be used with setInput(FFmpegProbeResult)"); 353 354 // TODO factor in start time and/or number of frames 355 356 double durationInSeconds = input.format.duration; 357 long totalBitRate = 358 (long) Math.floor((targetSize * 8) / durationInSeconds) - pass_padding_bitrate; 359 360 // TODO Calculate audioBitRate 361 362 if (video_enabled && video_bit_rate == 0) { 363 // Video (and possibly audio) 364 long audioBitRate = audio_enabled ? audio_bit_rate : 0; 365 video_bit_rate = totalBitRate - audioBitRate; 366 } else if (audio_enabled && audio_bit_rate == 0) { 367 // Just Audio 368 audio_bit_rate = totalBitRate; 369 } 370 } 371 372 return super.build(parent, pass); 373 } 374 375 /** 376 * Returns a double formatted as a string. If the double is an integer, then trailing zeros are 377 * striped. 378 * 379 * @param d the double to format. 380 * @return The formatted double. 381 */ 382 protected static String formatDecimalInteger(double d) { 383 return trailingZero.matcher(String.valueOf(d)).replaceAll(""); 384 } 385 386 @Override 387 protected void addGlobalFlags(FFmpegBuilder parent, ImmutableList.Builder<String> args) { 388 super.addGlobalFlags(parent, args); 389 390 if (constantRateFactor != null) { 391 args.add("-crf", formatDecimalInteger(constantRateFactor)); 392 } 393 394 if (complexFilter != null) { 395 args.add("-filter_complex", complexFilter); 396 } 397 } 398 399 @Override 400 protected void addVideoFlags(FFmpegBuilder parent, ImmutableList.Builder<String> args) { 401 super.addVideoFlags(parent, args); 402 403 if (video_bit_rate > 0 && video_quality != null) { 404 // I'm not sure, but it seems video_quality overrides video_bit_rate, so don't allow both 405 throw new IllegalStateException("Only one of video_bit_rate and video_quality can be set"); 406 } 407 408 if (video_bit_rate > 0) { 409 args.add("-b:v", String.valueOf(video_bit_rate)); 410 } 411 412 if (video_quality != null) { 413 args.add("-qscale:v", formatDecimalInteger(video_quality)); 414 } 415 416 if (!Strings.isNullOrEmpty(video_preset)) { 417 args.add("-vpre", video_preset); 418 } 419 420 if (!Strings.isNullOrEmpty(video_filter)) { 421 checkState( 422 parent.inputs.size() == 1, 423 "Video filter only works with one input, instead use setComplexVideoFilter(..)"); 424 args.add("-vf", video_filter); 425 } 426 427 if (!Strings.isNullOrEmpty(video_bit_stream_filter)) { 428 args.add("-bsf:v", video_bit_stream_filter); 429 } 430 431 if (bFrames != null) { 432 args.add("-bf", Integer.toString(bFrames)); 433 } 434 } 435 436 @Override 437 protected void addAudioFlags(ImmutableList.Builder<String> args) { 438 super.addAudioFlags(args); 439 440 if (!Strings.isNullOrEmpty(audio_sample_format)) { 441 args.add("-sample_fmt", audio_sample_format); 442 } 443 444 if (audio_bit_rate > 0 && audio_quality != null && throwWarnings) { 445 // I'm not sure, but it seems audio_quality overrides audio_bit_rate, so don't allow both 446 throw new IllegalStateException("Only one of audio_bit_rate and audio_quality can be set"); 447 } 448 449 if (audio_bit_rate > 0) { 450 args.add("-b:a", String.valueOf(audio_bit_rate)); 451 } 452 453 if (audio_quality != null) { 454 args.add("-qscale:a", formatDecimalInteger(audio_quality)); 455 } 456 457 if (!Strings.isNullOrEmpty(audio_bit_stream_filter)) { 458 args.add("-bsf:a", audio_bit_stream_filter); 459 } 460 461 if (!Strings.isNullOrEmpty(audio_filter)) { 462 args.add("-af", audio_filter); 463 } 464 } 465 466 @Override 467 protected void addSourceTarget(int pass, ImmutableList.Builder<String> args) { 468 if (filename != null && uri != null) { 469 throw new IllegalStateException("Only one of filename and uri can be set"); 470 } 471 472 // Output 473 if (pass == 1) { 474 args.add(DEVNULL); 475 } else if (filename != null) { 476 args.add(filename); 477 } else if (uri != null) { 478 args.add(uri.toString()); 479 } else { 480 assert false; 481 } 482 } 483 484 @CheckReturnValue 485 @Override 486 protected T getThis() { 487 return (T) this; 488 } 489 490 public Double getConstantRateFactor() { 491 return constantRateFactor; 492 } 493 494 public String getAudioSampleFormat() { 495 return audio_sample_format; 496 } 497 498 public long getAudioBitRate() { 499 return audio_bit_rate; 500 } 501 502 public Double getAudioQuality() { 503 return audio_quality; 504 } 505 506 public String getAudioBitStreamFilter() { 507 return audio_bit_stream_filter; 508 } 509 510 public String getAudioFilter() { 511 return audio_filter; 512 } 513 514 public long getVideoBitRate() { 515 return video_bit_rate; 516 } 517 518 public Double getVideoQuality() { 519 return video_quality; 520 } 521 522 public String getVideoPreset() { 523 return video_preset; 524 } 525 526 public String getVideoFilter() { 527 return video_filter; 528 } 529 530 public String getVideoBitStreamFilter() { 531 return video_bit_stream_filter; 532 } 533 534 public String getComplexFilter() { 535 return complexFilter; 536 } 537}