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.Preconditions.checkNotEmpty; 006 007import com.google.common.base.Ascii; 008import com.google.common.base.Preconditions; 009import com.google.common.base.Strings; 010import com.google.common.collect.ImmutableList; 011import java.io.File; 012import java.net.URI; 013import java.nio.file.Path; 014import java.nio.file.Paths; 015import java.util.ArrayList; 016import java.util.List; 017import java.util.Map; 018import java.util.TreeMap; 019import java.util.concurrent.TimeUnit; 020import javax.annotation.CheckReturnValue; 021import net.bramp.ffmpeg.FFmpegUtils; 022import net.bramp.ffmpeg.probe.FFmpegProbeResult; 023import org.slf4j.Logger; 024import org.slf4j.LoggerFactory; 025 026/** 027 * Builds a ffmpeg command line. 028 * 029 * @author bramp 030 */ 031public class FFmpegBuilder { 032 033 private static final Logger log = LoggerFactory.getLogger(FFmpegBuilder.class); 034 035 /** 036 * Log level options: <a href="https://ffmpeg.org/ffmpeg.html#Generic-options">ffmpeg 037 * documentation</a>. 038 */ 039 public enum Verbosity { 040 QUIET, 041 PANIC, 042 FATAL, 043 ERROR, 044 WARNING, 045 INFO, 046 VERBOSE, 047 DEBUG; 048 049 @Override 050 public String toString() { 051 // ffmpeg command line requires these options in lower case 052 return Ascii.toLowerCase(name()); 053 } 054 } 055 056 // Global Settings 057 boolean override = true; 058 int pass = 0; 059 String pass_directory = ""; 060 String pass_prefix; 061 Verbosity verbosity = Verbosity.ERROR; 062 URI progress; 063 String user_agent; 064 Integer qscale; 065 066 int threads; 067 // Input settings 068 String format; 069 Long startOffset; // in millis 070 boolean read_at_native_frame_rate = false; 071 final List<AbstractFFmpegInputBuilder<?>> inputs = new ArrayList<>(); 072 final Map<String, FFmpegProbeResult> inputProbes = new TreeMap<>(); 073 074 final List<String> extra_args = new ArrayList<>(); 075 076 // Output 077 final List<AbstractFFmpegOutputBuilder<?>> outputs = new ArrayList<>(); 078 079 protected Strict strict = Strict.NORMAL; 080 081 // Filters 082 String audioFilter; 083 String videoFilter; 084 String complexFilter; 085 086 /** Sets the strict standards compliance level. */ 087 public FFmpegBuilder setStrict(Strict strict) { 088 this.strict = checkNotNull(strict); 089 return this; 090 } 091 092 /** Sets whether to overwrite output files without asking. */ 093 public FFmpegBuilder overrideOutputFiles(boolean override) { 094 this.override = override; 095 return this; 096 } 097 098 /** Returns whether output files will be overwritten. */ 099 public boolean getOverrideOutputFiles() { 100 return this.override; 101 } 102 103 /** Sets the pass number for multi-pass encoding. */ 104 public FFmpegBuilder setPass(int pass) { 105 this.pass = pass; 106 return this; 107 } 108 109 /** Sets the directory for storing pass log files. */ 110 public FFmpegBuilder setPassDirectory(String directory) { 111 this.pass_directory = checkNotNull(directory); 112 return this; 113 } 114 115 /** Sets the directory for storing pass log files. */ 116 public FFmpegBuilder setPassDirectory(File directory) { 117 return setPassDirectory(checkNotNull(directory).getPath()); 118 } 119 120 /** Sets the directory for storing pass log files. */ 121 public FFmpegBuilder setPassDirectory(Path directory) { 122 return setPassDirectory(checkNotNull(directory).toString()); 123 } 124 125 /** Returns the pass log file directory. */ 126 public String getPassDirectory() { 127 return this.pass_directory; 128 } 129 130 /** Sets the pass log file prefix. */ 131 public FFmpegBuilder setPassPrefix(String prefix) { 132 this.pass_prefix = checkNotNull(prefix); 133 return this; 134 } 135 136 /** Returns the pass log file prefix. */ 137 public String getPassPrefix() { 138 return this.pass_prefix; 139 } 140 141 /** Sets the logging verbosity level. */ 142 public FFmpegBuilder setVerbosity(Verbosity verbosity) { 143 checkNotNull(verbosity); 144 this.verbosity = verbosity; 145 return this; 146 } 147 148 /** Sets the HTTP user agent string. */ 149 public FFmpegBuilder setUserAgent(String userAgent) { 150 this.user_agent = checkNotNull(userAgent); 151 return this; 152 } 153 154 /** 155 * Makes ffmpeg read the first input at the native frame read. 156 * 157 * @return this 158 * @deprecated Use {@link AbstractFFmpegInputBuilder#readAtNativeFrameRate()} instead 159 */ 160 @Deprecated 161 public FFmpegBuilder readAtNativeFrameRate() { 162 this.read_at_native_frame_rate = true; 163 return this; 164 } 165 166 /** Adds an input from a previously probed result. */ 167 public FFmpegFileInputBuilder addInput(FFmpegProbeResult result) { 168 checkNotNull(result); 169 String filename = checkNotNull(result.getFormat()).getFilename(); 170 171 return this.doAddInput(new FFmpegFileInputBuilder(this, filename, result)); 172 } 173 174 /** Adds an input by filename or URL. */ 175 public FFmpegFileInputBuilder addInput(String filename) { 176 checkNotNull(filename); 177 178 return this.doAddInput(new FFmpegFileInputBuilder(this, filename)); 179 } 180 181 /** Adds an input by file. */ 182 public FFmpegFileInputBuilder addInput(File file) { 183 return addInput(checkNotNull(file).getPath()); 184 } 185 186 /** Adds an input by path. */ 187 public FFmpegFileInputBuilder addInput(Path path) { 188 return addInput(checkNotNull(path).toString()); 189 } 190 191 /** Adds a pre-built input builder and finalizes it. */ 192 public <T extends AbstractFFmpegInputBuilder<T>> FFmpegBuilder addInput(T input) { 193 return this.doAddInput(input).done(); 194 } 195 196 /** Adds an input builder to the list and returns it for further configuration. */ 197 protected <T extends AbstractFFmpegInputBuilder<T>> T doAddInput(T input) { 198 checkNotNull(input); 199 200 inputs.add(input); 201 return input; 202 } 203 204 /** Clears all previously added inputs. */ 205 protected void clearInputs() { 206 inputs.clear(); 207 inputProbes.clear(); 208 } 209 210 /** Clears existing inputs and sets the input from a probed result. */ 211 public FFmpegFileInputBuilder setInput(FFmpegProbeResult result) { 212 clearInputs(); 213 return addInput(result); 214 } 215 216 /** Clears existing inputs and sets the input by filename or URL. */ 217 public FFmpegFileInputBuilder setInput(String filename) { 218 clearInputs(); 219 return addInput(filename); 220 } 221 222 /** Clears existing inputs and sets the input by file. */ 223 public FFmpegFileInputBuilder setInput(File file) { 224 clearInputs(); 225 return addInput(file); 226 } 227 228 /** Clears existing inputs and sets the input by path. */ 229 public FFmpegFileInputBuilder setInput(Path path) { 230 clearInputs(); 231 return addInput(path); 232 } 233 234 /** Sets the input using an input builder, replacing any previous inputs. */ 235 public <T extends AbstractFFmpegInputBuilder<T>> FFmpegBuilder setInput(T input) { 236 checkNotNull(input); 237 238 clearInputs(); 239 inputs.add(input); 240 241 return this; 242 } 243 244 /** Sets the number of threads to use for processing. */ 245 public FFmpegBuilder setThreads(int threads) { 246 checkArgument(threads > 0, "threads must be greater than zero"); 247 this.threads = threads; 248 return this; 249 } 250 251 /** 252 * Sets the format for the first input stream. 253 * 254 * @param format the format of this input stream, not null 255 * @return this 256 * @deprecated Specify this option on an input stream using {@link 257 * AbstractFFmpegStreamBuilder#setFormat(String)} 258 */ 259 @Deprecated 260 public FFmpegBuilder setFormat(String format) { 261 this.format = checkNotNull(format); 262 return this; 263 } 264 265 /** 266 * Sets the start offset for the first input stream. 267 * 268 * @param duration the amount of the offset, measured in terms of the unit 269 * @param units the unit that the duration is measured in, not null 270 * @return this 271 * @deprecated Specify this option on an input or output stream using {@link 272 * AbstractFFmpegStreamBuilder#setStartOffset(long, TimeUnit)} 273 */ 274 @Deprecated 275 public FFmpegBuilder setStartOffset(long duration, TimeUnit units) { 276 checkNotNull(units); 277 278 this.startOffset = units.toMillis(duration); 279 280 return this; 281 } 282 283 /** Sets the URI for progress reporting. */ 284 public FFmpegBuilder addProgress(URI uri) { 285 this.progress = checkNotNull(uri); 286 return this; 287 } 288 289 /** 290 * Sets the complex filter flag. 291 * 292 * @param filter the complex filter string 293 * @return this 294 * @deprecated Use {@link AbstractFFmpegOutputBuilder#setComplexFilter(String)} instead 295 */ 296 @Deprecated 297 public FFmpegBuilder setComplexFilter(String filter) { 298 this.complexFilter = checkNotEmpty(filter, "filter must not be empty"); 299 return this; 300 } 301 302 /** 303 * Sets the audio filter flag. 304 * 305 * @param filter the audio filter string 306 * @return this 307 */ 308 public FFmpegBuilder setAudioFilter(String filter) { 309 this.audioFilter = checkNotEmpty(filter, "filter must not be empty"); 310 return this; 311 } 312 313 /** 314 * Sets the video filter flag. 315 * 316 * @param filter the video filter string 317 * @return this 318 */ 319 public FFmpegBuilder setVideoFilter(String filter) { 320 this.videoFilter = checkNotEmpty(filter, "filter must not be empty"); 321 return this; 322 } 323 324 /** 325 * Sets vbr quality when decoding mp3 output. 326 * 327 * @param quality the quality between 0 and 9. Where 0 is best. 328 * @return FFmpegBuilder 329 */ 330 public FFmpegBuilder setVBR(Integer quality) { 331 Preconditions.checkArgument(quality > 0 && quality < 9, "vbr must be between 0 and 9"); 332 this.qscale = quality; 333 return this; 334 } 335 336 /** 337 * Add additional ouput arguments (for flags which aren't currently supported). 338 * 339 * @param values The extra arguments. 340 * @return this 341 */ 342 public FFmpegBuilder addExtraArgs(String... values) { 343 checkArgument(values.length > 0, "one or more values must be supplied"); 344 checkNotEmpty(values[0], "first extra arg may not be empty"); 345 346 for (String value : values) { 347 extra_args.add(checkNotNull(value)); 348 } 349 return this; 350 } 351 352 /** 353 * Adds new output file. 354 * 355 * @param filename output file path 356 * @return A new {@link FFmpegOutputBuilder} 357 */ 358 public FFmpegOutputBuilder addOutput(String filename) { 359 FFmpegOutputBuilder output = new FFmpegOutputBuilder(this, filename); 360 outputs.add(output); 361 return output; 362 } 363 364 /** Adds a new output file. */ 365 public FFmpegOutputBuilder addOutput(File file) { 366 return addOutput(checkNotNull(file).getPath()); 367 } 368 369 /** Adds a new output file. */ 370 public FFmpegOutputBuilder addOutput(Path path) { 371 return addOutput(checkNotNull(path).toString()); 372 } 373 374 /** 375 * Adds new output file. 376 * 377 * @param uri output file uri typically a stream 378 * @return A new {@link FFmpegOutputBuilder} 379 */ 380 public FFmpegOutputBuilder addOutput(URI uri) { 381 FFmpegOutputBuilder output = new FFmpegOutputBuilder(this, uri); 382 outputs.add(output); 383 return output; 384 } 385 386 /** 387 * Adds an existing FFmpegOutputBuilder. This is similar to calling the other addOuput methods but 388 * instead allows an existing FFmpegOutputBuilder to be used, and reused. 389 * 390 * <pre> 391 * <code>List<String> args = new FFmpegBuilder() 392 * .addOutput(new FFmpegOutputBuilder() 393 * .setFilename("output.flv") 394 * .setVideoCodec("flv") 395 * ) 396 * .build();</code> 397 * </pre> 398 * 399 * @param output FFmpegOutputBuilder to add 400 * @return this 401 */ 402 public FFmpegBuilder addOutput(FFmpegOutputBuilder output) { 403 outputs.add(output); 404 return this; 405 } 406 407 /** 408 * Adds new HLS(Http Live Streaming) output file. <br> 409 * 410 * <pre> 411 * <code>List<String> args = new FFmpegBuilder() 412 * .addHlsOutput("output.m3u8") 413 * .done().build();</code> 414 * </pre> 415 * 416 * @param filename output file path 417 * @return A new {@link FFmpegHlsOutputBuilder} 418 */ 419 public FFmpegHlsOutputBuilder addHlsOutput(String filename) { 420 FFmpegHlsOutputBuilder output = new FFmpegHlsOutputBuilder(this, filename); 421 outputs.add(output); 422 return output; 423 } 424 425 /** Adds a new HLS output file. */ 426 public FFmpegHlsOutputBuilder addHlsOutput(File file) { 427 return addHlsOutput(checkNotNull(file).getPath()); 428 } 429 430 /** Adds a new HLS output file. */ 431 public FFmpegHlsOutputBuilder addHlsOutput(Path path) { 432 return addHlsOutput(checkNotNull(path).toString()); 433 } 434 435 /** 436 * Create new output (to stdout). 437 * 438 * @return A new {@link FFmpegOutputBuilder} 439 */ 440 public FFmpegOutputBuilder addStdoutOutput() { 441 return addOutput("-"); 442 } 443 444 /** Builds and returns the list of command-line arguments for ffmpeg. */ 445 @CheckReturnValue 446 public List<String> build() { 447 ImmutableList.Builder<String> args = new ImmutableList.Builder<>(); 448 449 Preconditions.checkArgument(!inputs.isEmpty(), "At least one input must be specified"); 450 Preconditions.checkArgument(!outputs.isEmpty(), "At least one output must be specified"); 451 452 if (strict != Strict.NORMAL) { 453 args.add("-strict", strict.toString()); 454 } 455 456 args.add(override ? "-y" : "-n"); 457 args.add("-v", this.verbosity.toString()); 458 459 if (user_agent != null) { 460 args.add("-user_agent", user_agent); 461 } 462 463 if (startOffset != null) { 464 log.warn( 465 "Using FFmpegBuilder#setStartOffset is deprecated." 466 + " Specify it on the inputStream or outputStream instead"); 467 args.add("-ss", FFmpegUtils.toTimecode(startOffset, TimeUnit.MILLISECONDS)); 468 } 469 470 if (threads > 0) { 471 args.add("-threads", String.valueOf(threads)); 472 } 473 474 if (format != null) { 475 log.warn( 476 "Using FFmpegBuilder#setFormat is deprecated." 477 + " Specify it on the inputStream or outputStream instead"); 478 args.add("-f", format); 479 } 480 481 if (read_at_native_frame_rate) { 482 log.warn( 483 "Using FFmpegBuilder#readAtNativeFrameRate is deprecated." 484 + " Specify it on the inputStream instead"); 485 args.add("-re"); 486 } 487 488 if (progress != null) { 489 args.add("-progress", progress.toString()); 490 } 491 492 args.addAll(extra_args); 493 494 for (AbstractFFmpegInputBuilder<?> input : this.inputs) { 495 args.addAll(input.build(this, pass)); 496 } 497 498 if (pass > 0) { 499 args.add("-pass", Integer.toString(pass)); 500 501 if (pass_prefix != null) { 502 args.add("-passlogfile", Paths.get(pass_directory, pass_prefix).toString()); 503 } 504 } 505 506 if (!Strings.isNullOrEmpty(audioFilter)) { 507 args.add("-af", audioFilter); 508 } 509 510 if (!Strings.isNullOrEmpty(videoFilter)) { 511 args.add("-vf", videoFilter); 512 } 513 514 if (!Strings.isNullOrEmpty(complexFilter)) { 515 log.warn( 516 "Using FFmpegBuilder#setComplexFilter is deprecated." 517 + " Specify it on the outputStream instead"); 518 args.add("-filter_complex", complexFilter); 519 } 520 521 if (qscale != null) { 522 args.add("-qscale:a", qscale.toString()); 523 } 524 525 for (AbstractFFmpegOutputBuilder<?> output : this.outputs) { 526 args.addAll(output.build(this, pass)); 527 } 528 529 return args.build(); 530 } 531}