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