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.Preconditions; 008import com.google.common.base.Strings; 009import com.google.common.collect.ImmutableList; 010import java.net.URI; 011import java.util.ArrayList; 012import java.util.List; 013import java.util.Map; 014import java.util.TreeMap; 015import java.util.concurrent.TimeUnit; 016import javax.annotation.CheckReturnValue; 017import net.bramp.ffmpeg.FFmpegUtils; 018import net.bramp.ffmpeg.probe.FFmpegProbeResult; 019 020/** 021 * Builds a ffmpeg command line 022 * 023 * @author bramp 024 */ 025public class FFmpegBuilder { 026 027 public enum Strict { 028 VERY, // strictly conform to a older more strict version of the specifications or reference 029 // software 030 STRICT, // strictly conform to all the things in the specificiations no matter what consequences 031 NORMAL, // normal 032 UNOFFICIAL, // allow unofficial extensions 033 EXPERIMENTAL; 034 035 // ffmpeg command line requires these options in lower case 036 @Override 037 public String toString() { 038 return name().toLowerCase(); 039 } 040 } 041 042 /** Log level options: https://ffmpeg.org/ffmpeg.html#Generic-options */ 043 public enum Verbosity { 044 QUIET, 045 PANIC, 046 FATAL, 047 ERROR, 048 WARNING, 049 INFO, 050 VERBOSE, 051 DEBUG; 052 053 @Override 054 public String toString() { 055 return name().toLowerCase(); 056 } 057 } 058 059 // Global Settings 060 boolean override = true; 061 int pass = 0; 062 String pass_directory = ""; 063 String pass_prefix; 064 Verbosity verbosity = Verbosity.ERROR; 065 URI progress; 066 String user_agent; 067 068 // Input settings 069 String format; 070 Long startOffset; // in millis 071 boolean read_at_native_frame_rate = false; 072 final List<String> inputs = new ArrayList<>(); 073 final Map<String, FFmpegProbeResult> inputProbes = new TreeMap<>(); 074 075 final List<String> extra_args = new ArrayList<>(); 076 077 // Output 078 final List<FFmpegOutputBuilder> outputs = new ArrayList<>(); 079 080 // Filters 081 String audioFilter; 082 String videoFilter; 083 String complexFilter; 084 085 public FFmpegBuilder overrideOutputFiles(boolean override) { 086 this.override = override; 087 return this; 088 } 089 090 public boolean getOverrideOutputFiles() { 091 return this.override; 092 } 093 094 public FFmpegBuilder setPass(int pass) { 095 this.pass = pass; 096 return this; 097 } 098 099 public FFmpegBuilder setPassDirectory(String directory) { 100 this.pass_directory = checkNotNull(directory); 101 return this; 102 } 103 104 public FFmpegBuilder setPassPrefix(String prefix) { 105 this.pass_prefix = checkNotNull(prefix); 106 return this; 107 } 108 109 public FFmpegBuilder setVerbosity(Verbosity verbosity) { 110 checkNotNull(verbosity); 111 this.verbosity = verbosity; 112 return this; 113 } 114 115 public FFmpegBuilder setUserAgent(String userAgent) { 116 this.user_agent = checkNotNull(userAgent); 117 return this; 118 } 119 120 public FFmpegBuilder readAtNativeFrameRate() { 121 this.read_at_native_frame_rate = true; 122 return this; 123 } 124 125 public FFmpegBuilder addInput(FFmpegProbeResult result) { 126 checkNotNull(result); 127 String filename = checkNotNull(result.format).filename; 128 inputProbes.put(filename, result); 129 return addInput(filename); 130 } 131 132 public FFmpegBuilder addInput(String filename) { 133 checkNotNull(filename); 134 inputs.add(filename); 135 return this; 136 } 137 138 protected void clearInputs() { 139 inputs.clear(); 140 inputProbes.clear(); 141 } 142 143 public FFmpegBuilder setInput(FFmpegProbeResult result) { 144 clearInputs(); 145 return addInput(result); 146 } 147 148 public FFmpegBuilder setInput(String filename) { 149 clearInputs(); 150 return addInput(filename); 151 } 152 153 public FFmpegBuilder setFormat(String format) { 154 this.format = checkNotNull(format); 155 return this; 156 } 157 158 public FFmpegBuilder setStartOffset(long duration, TimeUnit units) { 159 checkNotNull(units); 160 161 this.startOffset = units.toMillis(duration); 162 163 return this; 164 } 165 166 public FFmpegBuilder addProgress(URI uri) { 167 this.progress = checkNotNull(uri); 168 return this; 169 } 170 171 /** 172 * Sets the complex filter flag. 173 * 174 * @param filter 175 * @return 176 */ 177 public FFmpegBuilder setComplexFilter(String filter) { 178 this.complexFilter = checkNotEmpty(filter, "filter must not be empty"); 179 return this; 180 } 181 182 /** 183 * Sets the audio filter flag. 184 * 185 * @param filter 186 * @return 187 */ 188 public FFmpegBuilder setAudioFilter(String filter) { 189 this.audioFilter = checkNotEmpty(filter, "filter must not be empty"); 190 return this; 191 } 192 193 /** 194 * Sets the video filter flag. 195 * 196 * @param filter 197 * @return 198 */ 199 public FFmpegBuilder setVideoFilter(String filter) { 200 this.videoFilter = checkNotEmpty(filter, "filter must not be empty"); 201 return this; 202 } 203 204 /** 205 * Add additional ouput arguments (for flags which aren't currently supported). 206 * 207 * @param values The extra arguments. 208 * @return this 209 */ 210 public FFmpegBuilder addExtraArgs(String... values) { 211 checkArgument(values.length > 0, "one or more values must be supplied"); 212 checkNotEmpty(values[0], "first extra arg may not be empty"); 213 214 for (String value : values) { 215 extra_args.add(checkNotNull(value)); 216 } 217 return this; 218 } 219 220 /** 221 * Adds new output file. 222 * 223 * @param filename output file path 224 * @return A new {@link FFmpegOutputBuilder} 225 */ 226 public FFmpegOutputBuilder addOutput(String filename) { 227 FFmpegOutputBuilder output = new FFmpegOutputBuilder(this, filename); 228 outputs.add(output); 229 return output; 230 } 231 232 /** 233 * Adds new output file. 234 * 235 * @param uri output file uri typically a stream 236 * @return A new {@link FFmpegOutputBuilder} 237 */ 238 public FFmpegOutputBuilder addOutput(URI uri) { 239 FFmpegOutputBuilder output = new FFmpegOutputBuilder(this, uri); 240 outputs.add(output); 241 return output; 242 } 243 244 /** 245 * Adds an existing FFmpegOutputBuilder. This is similar to calling the other addOuput methods but 246 * instead allows an existing FFmpegOutputBuilder to be used, and reused. 247 * 248 * <pre> 249 * <code>List<String> args = new FFmpegBuilder() 250 * .addOutput(new FFmpegOutputBuilder() 251 * .setFilename("output.flv") 252 * .setVideoCodec("flv") 253 * ) 254 * .build();</code> 255 * </pre> 256 * 257 * @param output FFmpegOutputBuilder to add 258 * @return this 259 */ 260 public FFmpegBuilder addOutput(FFmpegOutputBuilder output) { 261 outputs.add(output); 262 return this; 263 } 264 265 /** 266 * Create new output (to stdout) 267 * 268 * @return A new {@link FFmpegOutputBuilder} 269 */ 270 public FFmpegOutputBuilder addStdoutOutput() { 271 return addOutput("-"); 272 } 273 274 @CheckReturnValue 275 public List<String> build() { 276 ImmutableList.Builder<String> args = new ImmutableList.Builder<String>(); 277 278 Preconditions.checkArgument(!inputs.isEmpty(), "At least one input must be specified"); 279 Preconditions.checkArgument(!outputs.isEmpty(), "At least one output must be specified"); 280 281 args.add(override ? "-y" : "-n"); 282 args.add("-v", this.verbosity.toString()); 283 284 if (user_agent != null) { 285 args.add("-user_agent", user_agent); 286 } 287 288 if (startOffset != null) { 289 args.add("-ss", FFmpegUtils.toTimecode(startOffset, TimeUnit.MILLISECONDS)); 290 } 291 292 if (format != null) { 293 args.add("-f", format); 294 } 295 296 if (read_at_native_frame_rate) { 297 args.add("-re"); 298 } 299 300 if (progress != null) { 301 args.add("-progress", progress.toString()); 302 } 303 304 args.addAll(extra_args); 305 306 for (String input : inputs) { 307 args.add("-i", input); 308 } 309 310 if (pass > 0) { 311 args.add("-pass", Integer.toString(pass)); 312 313 if (pass_prefix != null) { 314 args.add("-passlogfile", pass_directory + pass_prefix); 315 } 316 } 317 318 if (!Strings.isNullOrEmpty(audioFilter)) { 319 args.add("-af", audioFilter); 320 } 321 322 if (!Strings.isNullOrEmpty(videoFilter)) { 323 args.add("-vf", videoFilter); 324 } 325 326 if (!Strings.isNullOrEmpty(complexFilter)) { 327 args.add("-filter_complex", complexFilter); 328 } 329 330 for (FFmpegOutputBuilder output : this.outputs) { 331 args.addAll(output.build(this, pass)); 332 } 333 334 return args.build(); 335 } 336}