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 java.net.URI; 010import java.util.List; 011import java.util.regex.Pattern; 012import javax.annotation.CheckReturnValue; 013import net.bramp.ffmpeg.options.AudioEncodingOptions; 014import net.bramp.ffmpeg.options.EncodingOptions; 015import net.bramp.ffmpeg.options.MainEncodingOptions; 016import net.bramp.ffmpeg.options.VideoEncodingOptions; 017import net.bramp.ffmpeg.probe.FFmpegProbeResult; 018 019/** Builds a representation of a single output/encoding setting */ 020public class FFmpegOutputBuilder extends AbstractFFmpegStreamBuilder<FFmpegOutputBuilder> { 021 022 static final Pattern trailingZero = Pattern.compile("\\.0*$"); 023 024 public Double constantRateFactor; 025 026 public String audio_sample_format; 027 public long audio_bit_rate; 028 public Double audio_quality; 029 public String audio_bit_stream_filter; 030 public String audio_filter; 031 032 public long video_bit_rate; 033 public Double video_quality; 034 public String video_preset; 035 public String video_filter; 036 public String video_bit_stream_filter; 037 038 public FFmpegOutputBuilder() { 039 super(); 040 } 041 042 protected FFmpegOutputBuilder(FFmpegBuilder parent, String filename) { 043 super(parent, filename); 044 } 045 046 protected FFmpegOutputBuilder(FFmpegBuilder parent, URI uri) { 047 super(parent, uri); 048 } 049 050 public FFmpegOutputBuilder setConstantRateFactor(double factor) { 051 checkArgument(factor >= 0, "constant rate factor must be greater or equal to zero"); 052 this.constantRateFactor = factor; 053 return this; 054 } 055 056 public FFmpegOutputBuilder setVideoBitRate(long bit_rate) { 057 checkArgument(bit_rate > 0, "bit rate must be positive"); 058 this.video_enabled = true; 059 this.video_bit_rate = bit_rate; 060 return this; 061 } 062 063 public FFmpegOutputBuilder setVideoQuality(double quality) { 064 checkArgument(quality > 0, "quality must be positive"); 065 this.video_enabled = true; 066 this.video_quality = quality; 067 return this; 068 } 069 070 public FFmpegOutputBuilder setVideoBitStreamFilter(String filter) { 071 this.video_bit_stream_filter = checkNotEmpty(filter, "filter must not be empty"); 072 return this; 073 } 074 075 /** 076 * Sets a video preset to use. 077 * 078 * <p>Uses `-vpre`. 079 * 080 * @param preset the preset 081 * @return this 082 */ 083 public FFmpegOutputBuilder setVideoPreset(String preset) { 084 this.video_enabled = true; 085 this.video_preset = checkNotEmpty(preset, "video preset must not be empty"); 086 return this; 087 } 088 089 /** 090 * Sets Video Filter 091 * 092 * <p>TODO Build a fluent Filter builder 093 * 094 * @param filter The video filter. 095 * @return this 096 */ 097 public FFmpegOutputBuilder setVideoFilter(String filter) { 098 this.video_enabled = true; 099 this.video_filter = checkNotEmpty(filter, "filter must not be empty"); 100 return this; 101 } 102 103 /** 104 * Sets the audio bit depth. 105 * 106 * @param bit_depth The sample format, one of the net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_* constants. 107 * @return this 108 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_U8 109 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_S16 110 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_S32 111 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_FLT 112 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_DEPTH_DBL 113 * @deprecated use {@link #setAudioSampleFormat} instead. 114 */ 115 @Deprecated 116 public FFmpegOutputBuilder setAudioBitDepth(String bit_depth) { 117 return setAudioSampleFormat(bit_depth); 118 } 119 120 /** 121 * Sets the audio sample format. 122 * 123 * @param sample_format The sample format, one of the net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_* 124 * constants. 125 * @return this 126 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_U8 127 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_S16 128 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_S32 129 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_FLT 130 * @see net.bramp.ffmpeg.FFmpeg#AUDIO_FORMAT_DBL 131 */ 132 public FFmpegOutputBuilder setAudioSampleFormat(String sample_format) { 133 this.audio_enabled = true; 134 this.audio_sample_format = checkNotEmpty(sample_format, "sample format must not be empty"); 135 return this; 136 } 137 138 /** 139 * Sets the Audio bit rate 140 * 141 * @param bit_rate Audio bitrate in bits per second. 142 * @return this 143 */ 144 public FFmpegOutputBuilder setAudioBitRate(long bit_rate) { 145 checkArgument(bit_rate > 0, "bit rate must be positive"); 146 this.audio_enabled = true; 147 this.audio_bit_rate = bit_rate; 148 return this; 149 } 150 151 public FFmpegOutputBuilder setAudioQuality(double quality) { 152 checkArgument(quality > 0, "quality must be positive"); 153 this.audio_enabled = true; 154 this.audio_quality = quality; 155 return this; 156 } 157 158 public FFmpegOutputBuilder setAudioBitStreamFilter(String filter) { 159 this.audio_enabled = true; 160 this.audio_bit_stream_filter = checkNotEmpty(filter, "filter must not be empty"); 161 return this; 162 } 163 164 /** 165 * Sets Audio Filter 166 * 167 * <p>TODO Build a fluent Filter builder 168 * 169 * @param filter The audio filter. 170 * @return this 171 */ 172 public FFmpegOutputBuilder setAudioFilter(String filter) { 173 this.audio_enabled = true; 174 this.audio_filter = checkNotEmpty(filter, "filter must not be empty"); 175 return this; 176 } 177 178 /** 179 * Returns a representation of this Builder that can be safely serialised. 180 * 181 * <p>NOTE: This method is horribly out of date, and its use should be rethought. 182 * 183 * @return A new EncodingOptions capturing this Builder's state 184 */ 185 @CheckReturnValue 186 @Override 187 public EncodingOptions buildOptions() { 188 // TODO When/if modelmapper supports @ConstructorProperties, we map this 189 // object, instead of doing new XXX(...) 190 // https://github.com/jhalterman/modelmapper/issues/44 191 return new EncodingOptions( 192 new MainEncodingOptions(format, startOffset, duration), 193 new AudioEncodingOptions( 194 audio_enabled, 195 audio_codec, 196 audio_channels, 197 audio_sample_rate, 198 audio_sample_format, 199 audio_bit_rate, 200 audio_quality), 201 new VideoEncodingOptions( 202 video_enabled, 203 video_codec, 204 video_frame_rate, 205 video_width, 206 video_height, 207 video_bit_rate, 208 video_frames, 209 video_filter, 210 video_preset)); 211 } 212 213 @CheckReturnValue 214 @Override 215 protected List<String> build(int pass) { 216 Preconditions.checkState(parent != null, "Can not build without parent being set"); 217 return build(parent, pass); 218 } 219 220 /** 221 * Builds the arguments 222 * 223 * @param parent The parent FFmpegBuilder 224 * @param pass The particular pass. For one-pass this value will be zero, for multi-pass, it will 225 * be 1 for the first pass, 2 for the second, and so on. 226 * @return The arguments 227 */ 228 @CheckReturnValue 229 @Override 230 protected List<String> build(FFmpegBuilder parent, int pass) { 231 if (pass > 0) { 232 checkArgument( 233 targetSize != 0 || video_bit_rate != 0, 234 "Target size, or video bitrate must be specified when using two-pass"); 235 } 236 if (targetSize > 0) { 237 checkState(parent.inputs.size() == 1, "Target size does not support multiple inputs"); 238 239 checkArgument( 240 constantRateFactor == null, "Target size can not be used with constantRateFactor"); 241 242 String firstInput = parent.inputs.iterator().next(); 243 FFmpegProbeResult input = parent.inputProbes.get(firstInput); 244 245 checkState(input != null, "Target size must be used with setInput(FFmpegProbeResult)"); 246 247 // TODO factor in start time and/or number of frames 248 249 double durationInSeconds = input.format.duration; 250 long totalBitRate = 251 (long) Math.floor((targetSize * 8) / durationInSeconds) - pass_padding_bitrate; 252 253 // TODO Calculate audioBitRate 254 255 if (video_enabled && video_bit_rate == 0) { 256 // Video (and possibly audio) 257 long audioBitRate = audio_enabled ? audio_bit_rate : 0; 258 video_bit_rate = totalBitRate - audioBitRate; 259 } else if (audio_enabled && audio_bit_rate == 0) { 260 // Just Audio 261 audio_bit_rate = totalBitRate; 262 } 263 } 264 265 return super.build(parent, pass); 266 } 267 268 /** 269 * Returns a double formatted as a string. If the double is an integer, then trailing zeros are 270 * striped. 271 * 272 * @param d the double to format. 273 * @return The formatted double. 274 */ 275 protected static String formatDecimalInteger(double d) { 276 return trailingZero.matcher(String.valueOf(d)).replaceAll(""); 277 } 278 279 @Override 280 protected void addGlobalFlags(FFmpegBuilder parent, ImmutableList.Builder<String> args) { 281 super.addGlobalFlags(parent, args); 282 283 if (constantRateFactor != null) { 284 args.add("-crf", formatDecimalInteger(constantRateFactor)); 285 } 286 } 287 288 @Override 289 protected void addVideoFlags(FFmpegBuilder parent, ImmutableList.Builder<String> args) { 290 super.addVideoFlags(parent, args); 291 292 if (video_bit_rate > 0 && video_quality != null) { 293 // I'm not sure, but it seems video_quality overrides video_bit_rate, so don't allow both 294 throw new IllegalStateException("Only one of video_bit_rate and video_quality can be set"); 295 } 296 297 if (video_bit_rate > 0) { 298 args.add("-b:v", String.valueOf(video_bit_rate)); 299 } 300 301 if (video_quality != null) { 302 args.add("-qscale:v", formatDecimalInteger(video_quality)); 303 } 304 305 if (!Strings.isNullOrEmpty(video_preset)) { 306 args.add("-vpre", video_preset); 307 } 308 309 if (!Strings.isNullOrEmpty(video_filter)) { 310 checkState( 311 parent.inputs.size() == 1, 312 "Video filter only works with one input, instead use setComplexVideoFilter(..)"); 313 args.add("-vf", video_filter); 314 } 315 316 if (!Strings.isNullOrEmpty(video_bit_stream_filter)) { 317 args.add("-bsf:v", video_bit_stream_filter); 318 } 319 } 320 321 @Override 322 protected void addAudioFlags(ImmutableList.Builder<String> args) { 323 super.addAudioFlags(args); 324 325 if (!Strings.isNullOrEmpty(audio_sample_format)) { 326 args.add("-sample_fmt", audio_sample_format); 327 } 328 329 if (audio_bit_rate > 0 && audio_quality != null && throwWarnings) { 330 // I'm not sure, but it seems audio_quality overrides audio_bit_rate, so don't allow both 331 throw new IllegalStateException("Only one of audio_bit_rate and audio_quality can be set"); 332 } 333 334 if (audio_bit_rate > 0) { 335 args.add("-b:a", String.valueOf(audio_bit_rate)); 336 } 337 338 if (audio_quality != null) { 339 args.add("-qscale:a", formatDecimalInteger(audio_quality)); 340 } 341 342 if (!Strings.isNullOrEmpty(audio_bit_stream_filter)) { 343 args.add("-bsf:a", audio_bit_stream_filter); 344 } 345 346 if (!Strings.isNullOrEmpty(audio_filter)) { 347 args.add("-af", audio_filter); 348 } 349 } 350 351 @CheckReturnValue 352 @Override 353 protected FFmpegOutputBuilder getThis() { 354 return this; 355 } 356}