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}