001package net.bramp.ffmpeg; 002 003import static com.google.common.base.MoreObjects.firstNonNull; 004import static com.google.common.base.Preconditions.checkNotNull; 005 006import com.google.common.collect.ImmutableList; 007import java.io.BufferedReader; 008import java.io.IOException; 009import java.net.URISyntaxException; 010import java.util.ArrayList; 011import java.util.Collections; 012import java.util.List; 013import java.util.regex.Matcher; 014import java.util.regex.Pattern; 015import javax.annotation.CheckReturnValue; 016import javax.annotation.Nonnull; 017import javax.annotation.Nullable; 018import net.bramp.ffmpeg.builder.FFmpegBuilder; 019import net.bramp.ffmpeg.info.ChannelLayout; 020import net.bramp.ffmpeg.info.Codec; 021import net.bramp.ffmpeg.info.Filter; 022import net.bramp.ffmpeg.info.FilterPattern; 023import net.bramp.ffmpeg.info.Format; 024import net.bramp.ffmpeg.info.InfoParser; 025import net.bramp.ffmpeg.info.PixelFormat; 026import net.bramp.ffmpeg.progress.ProgressListener; 027import net.bramp.ffmpeg.progress.ProgressParser; 028import net.bramp.ffmpeg.progress.TcpProgressParser; 029import org.apache.commons.lang3.math.Fraction; 030 031/** 032 * Wrapper around FFmpeg. 033 * 034 * @author bramp 035 */ 036public class FFmpeg extends FFcommon { 037 038 public static final String FFMPEG = "ffmpeg"; 039 public static final String DEFAULT_PATH = firstNonNull(System.getenv("FFMPEG"), FFMPEG); 040 041 public static final Fraction FPS_30 = Fraction.getFraction(30, 1); 042 public static final Fraction FPS_29_97 = Fraction.getFraction(30000, 1001); 043 public static final Fraction FPS_24 = Fraction.getFraction(24, 1); 044 public static final Fraction FPS_23_976 = Fraction.getFraction(24000, 1001); 045 046 public static final int AUDIO_MONO = 1; 047 public static final int AUDIO_STEREO = 2; 048 049 public static final String AUDIO_FORMAT_U8 = "u8"; // 8 050 public static final String AUDIO_FORMAT_S16 = "s16"; // 16 051 public static final String AUDIO_FORMAT_S32 = "s32"; // 32 052 public static final String AUDIO_FORMAT_FLT = "flt"; // 32 053 public static final String AUDIO_FORMAT_DBL = "dbl"; // 64 054 055 @Deprecated public static final String AUDIO_DEPTH_U8 = AUDIO_FORMAT_U8; 056 @Deprecated public static final String AUDIO_DEPTH_S16 = AUDIO_FORMAT_S16; 057 @Deprecated public static final String AUDIO_DEPTH_S32 = AUDIO_FORMAT_S32; 058 @Deprecated public static final String AUDIO_DEPTH_FLT = AUDIO_FORMAT_FLT; 059 @Deprecated public static final String AUDIO_DEPTH_DBL = AUDIO_FORMAT_DBL; 060 061 public static final int AUDIO_SAMPLE_8000 = 8000; 062 public static final int AUDIO_SAMPLE_11025 = 11025; 063 public static final int AUDIO_SAMPLE_12000 = 12000; 064 public static final int AUDIO_SAMPLE_16000 = 16000; 065 public static final int AUDIO_SAMPLE_22050 = 22050; 066 public static final int AUDIO_SAMPLE_32000 = 32000; 067 public static final int AUDIO_SAMPLE_44100 = 44100; 068 public static final int AUDIO_SAMPLE_48000 = 48000; 069 public static final int AUDIO_SAMPLE_96000 = 96000; 070 071 static final Pattern CODECS_REGEX = 072 Pattern.compile("^ ([.D][.E][VASD][.I][.L][.S]) (\\S{2,})\\s+(.*)$"); 073 static final Pattern FORMATS_REGEX = Pattern.compile("^ ([ D][ E]) (\\S+)\\s+(.*)$"); 074 static final Pattern PIXEL_FORMATS_REGEX = 075 Pattern.compile("^([.I][.O][.H][.P][.B]) (\\S{2,})\\s+(\\d+)\\s+(\\d+)$"); 076 static final Pattern FILTERS_REGEX = 077 Pattern.compile( 078 "^\\s*(?<timelinesupport>[T.])(?<slicethreading>[S.])" 079 + "(?<commandsupport>[C.])\\s(?<name>[A-Za-z0-9_]+)" 080 + "\\s+(?<inputpattern>[AVN|]+)->(?<outputpattern>[AVN|]+)" 081 + "\\s+(?<description>.*)$"); 082 083 /** Supported codecs. */ 084 List<Codec> codecs = null; 085 086 /** Supported formats. */ 087 List<Format> formats = null; 088 089 /** Supported pixel formats. */ 090 private List<PixelFormat> pixelFormats = null; 091 092 /** Supported filters. */ 093 private List<Filter> filters = null; 094 095 /** Supported channel layouts. */ 096 private List<ChannelLayout> channelLayouts = null; 097 098 /** Constructs an FFmpeg instance using the default path. */ 099 public FFmpeg() throws IOException { 100 this(DEFAULT_PATH, new RunProcessFunction()); 101 } 102 103 /** Constructs an FFmpeg instance using the default path and the specified process function. */ 104 public FFmpeg(@Nonnull ProcessFunction runFunction) throws IOException { 105 this(DEFAULT_PATH, runFunction); 106 } 107 108 /** Constructs an FFmpeg instance using the specified path. */ 109 public FFmpeg(@Nonnull String path) throws IOException { 110 this(path, new RunProcessFunction()); 111 } 112 113 /** Constructs an FFmpeg instance using the specified path and process function. */ 114 @SuppressWarnings("this-escape") 115 public FFmpeg(@Nonnull String path, @Nonnull ProcessFunction runFunction) throws IOException { 116 super(path, runFunction); 117 version(); 118 } 119 120 /** 121 * Returns true if the binary we are using is the true ffmpeg. This is to avoid conflict with 122 * avconv (from the libav project), that some symlink to ffmpeg. 123 * 124 * @return true iff this is the official ffmpeg binary. 125 * @throws IOException If a I/O error occurs while executing ffmpeg. 126 */ 127 public boolean isFFmpeg() throws IOException { 128 return version().startsWith("ffmpeg"); 129 } 130 131 /** 132 * Throws an exception if this is an unsupported version of ffmpeg. 133 * 134 * @throws IllegalArgumentException if this is not the official ffmpeg binary. 135 * @throws IOException If a I/O error occurs while executing ffmpeg. 136 */ 137 private void checkIfFFmpeg() throws IllegalArgumentException, IOException { 138 if (!isFFmpeg()) { 139 throw new IllegalArgumentException( 140 "This binary '" + path + "' is not a supported version of ffmpeg"); 141 } 142 } 143 144 /** Returns the list of supported codecs. */ 145 public synchronized @Nonnull List<Codec> codecs() throws IOException { 146 checkIfFFmpeg(); 147 148 if (this.codecs == null) { 149 codecs = new ArrayList<>(); 150 151 Process p = runFunc.run(ImmutableList.of(path, "-codecs")); 152 try { 153 BufferedReader r = wrapInReader(p); 154 String line; 155 while ((line = r.readLine()) != null) { 156 Matcher m = CODECS_REGEX.matcher(line); 157 if (!m.matches()) { 158 continue; 159 } 160 161 codecs.add(new Codec(m.group(2), m.group(3), m.group(1))); 162 } 163 164 throwOnError(p); 165 this.codecs = ImmutableList.copyOf(codecs); 166 } finally { 167 p.destroy(); 168 } 169 } 170 171 return codecs; 172 } 173 174 /** Returns the list of supported filters. */ 175 public synchronized @Nonnull List<Filter> filters() throws IOException { 176 checkIfFFmpeg(); 177 178 if (this.filters == null) { 179 filters = new ArrayList<>(); 180 181 Process p = runFunc.run(ImmutableList.of(path, "-filters")); 182 try { 183 BufferedReader r = wrapInReader(p); 184 String line; 185 while ((line = r.readLine()) != null) { 186 Matcher m = FILTERS_REGEX.matcher(line); 187 if (!m.matches()) { 188 continue; 189 } 190 191 // (?<inputpattern>[AVN|]+)->(?<outputpattern>[AVN|]+)\s+(?<description>.*)$ 192 193 filters.add( 194 new Filter( 195 m.group("timelinesupport").equals("T"), 196 m.group("slicethreading").equals("S"), 197 m.group("commandsupport").equals("C"), 198 m.group("name"), 199 new FilterPattern(m.group("inputpattern")), 200 new FilterPattern(m.group("outputpattern")), 201 m.group("description"))); 202 } 203 204 throwOnError(p); 205 this.filters = ImmutableList.copyOf(filters); 206 } finally { 207 p.destroy(); 208 } 209 } 210 211 return this.filters; 212 } 213 214 /** Returns the list of supported formats. */ 215 public synchronized @Nonnull List<Format> formats() throws IOException { 216 checkIfFFmpeg(); 217 218 if (this.formats == null) { 219 formats = new ArrayList<>(); 220 221 Process p = runFunc.run(ImmutableList.of(path, "-formats")); 222 try { 223 BufferedReader r = wrapInReader(p); 224 String line; 225 while ((line = r.readLine()) != null) { 226 Matcher m = FORMATS_REGEX.matcher(line); 227 if (!m.matches()) { 228 continue; 229 } 230 231 formats.add(new Format(m.group(2), m.group(3), m.group(1))); 232 } 233 234 throwOnError(p); 235 this.formats = ImmutableList.copyOf(formats); 236 } finally { 237 p.destroy(); 238 } 239 } 240 return formats; 241 } 242 243 /** Returns the list of supported pixel formats. */ 244 public synchronized List<PixelFormat> pixelFormats() throws IOException { 245 checkIfFFmpeg(); 246 247 if (this.pixelFormats == null) { 248 pixelFormats = new ArrayList<>(); 249 250 Process p = runFunc.run(ImmutableList.of(path, "-pix_fmts")); 251 try { 252 BufferedReader r = wrapInReader(p); 253 String line; 254 while ((line = r.readLine()) != null) { 255 Matcher m = PIXEL_FORMATS_REGEX.matcher(line); 256 if (!m.matches()) { 257 continue; 258 } 259 String flags = m.group(1); 260 261 pixelFormats.add( 262 new PixelFormat( 263 m.group(2), Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)), flags)); 264 } 265 266 throwOnError(p); 267 this.pixelFormats = ImmutableList.copyOf(pixelFormats); 268 } finally { 269 p.destroy(); 270 } 271 } 272 273 return pixelFormats; 274 } 275 276 /** Returns the list of supported channel layouts. */ 277 public synchronized List<ChannelLayout> channelLayouts() throws IOException { 278 checkIfFFmpeg(); 279 280 if (this.channelLayouts == null) { 281 Process p = runFunc.run(ImmutableList.of(path, "-layouts")); 282 283 try { 284 BufferedReader r = wrapInReader(p); 285 this.channelLayouts = Collections.unmodifiableList(InfoParser.parseLayouts(r)); 286 } finally { 287 p.destroy(); 288 } 289 } 290 291 return this.channelLayouts; 292 } 293 294 /** Creates a progress parser for the given listener. */ 295 protected ProgressParser createProgressParser(ProgressListener listener) throws IOException { 296 // TODO In future create the best kind for this OS, unix socket, named pipe, or TCP. 297 try { 298 // Default to TCP because it is supported across all OSes, and is better than UDP because it 299 // provides good properties such as in-order packets, reliability, error checking, etc. 300 return new TcpProgressParser(checkNotNull(listener)); 301 } catch (URISyntaxException e) { 302 throw new IOException(e); 303 } 304 } 305 306 @Override 307 public void run(List<String> args) throws IOException { 308 checkIfFFmpeg(); 309 super.run(args); 310 } 311 312 /** Runs ffmpeg with the supplied builder. */ 313 public void run(FFmpegBuilder builder) throws IOException { 314 run(builder, null); 315 } 316 317 /** 318 * Runs ffmpeg with the supplied builder and an optional progress listener. 319 * 320 * @param builder the ffmpeg builder 321 * @param listener optional progress listener 322 * @throws IOException if an I/O error occurs 323 */ 324 public void run(FFmpegBuilder builder, @Nullable ProgressListener listener) throws IOException { 325 checkNotNull(builder); 326 327 if (listener != null) { 328 try (ProgressParser progressParser = createProgressParser(listener)) { 329 progressParser.start(); 330 builder = builder.addProgress(progressParser.getUri()); 331 332 run(builder.build()); 333 } 334 } else { 335 run(builder.build()); 336 } 337 } 338 339 /** Returns a new FFmpegBuilder instance. */ 340 @CheckReturnValue 341 public FFmpegBuilder builder() { 342 return new FFmpegBuilder(); 343 } 344 345 @Override 346 public String getPath() { 347 return path; 348 } 349}