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.List;
012import java.util.regex.Matcher;
013import java.util.regex.Pattern;
014import javax.annotation.CheckReturnValue;
015import javax.annotation.Nonnull;
016import javax.annotation.Nullable;
017import net.bramp.ffmpeg.builder.FFmpegBuilder;
018import net.bramp.ffmpeg.info.Codec;
019import net.bramp.ffmpeg.info.Format;
020import net.bramp.ffmpeg.info.PixelFormat;
021import net.bramp.ffmpeg.progress.ProgressListener;
022import net.bramp.ffmpeg.progress.ProgressParser;
023import net.bramp.ffmpeg.progress.TcpProgressParser;
024import org.apache.commons.lang3.math.Fraction;
025
026/**
027 * Wrapper around FFmpeg
028 *
029 * @author bramp
030 */
031public class FFmpeg extends FFcommon {
032
033  public static final String FFMPEG = "ffmpeg";
034  public static final String DEFAULT_PATH = firstNonNull(System.getenv("FFMPEG"), FFMPEG);
035
036  public static final Fraction FPS_30 = Fraction.getFraction(30, 1);
037  public static final Fraction FPS_29_97 = Fraction.getFraction(30000, 1001);
038  public static final Fraction FPS_24 = Fraction.getFraction(24, 1);
039  public static final Fraction FPS_23_976 = Fraction.getFraction(24000, 1001);
040
041  public static final int AUDIO_MONO = 1;
042  public static final int AUDIO_STEREO = 2;
043
044  public static final String AUDIO_FORMAT_U8 = "u8"; // 8
045  public static final String AUDIO_FORMAT_S16 = "s16"; // 16
046  public static final String AUDIO_FORMAT_S32 = "s32"; // 32
047  public static final String AUDIO_FORMAT_FLT = "flt"; // 32
048  public static final String AUDIO_FORMAT_DBL = "dbl"; // 64
049
050  @Deprecated public static final String AUDIO_DEPTH_U8 = AUDIO_FORMAT_U8;
051  @Deprecated public static final String AUDIO_DEPTH_S16 = AUDIO_FORMAT_S16;
052  @Deprecated public static final String AUDIO_DEPTH_S32 = AUDIO_FORMAT_S32;
053  @Deprecated public static final String AUDIO_DEPTH_FLT = AUDIO_FORMAT_FLT;
054  @Deprecated public static final String AUDIO_DEPTH_DBL = AUDIO_FORMAT_DBL;
055
056  public static final int AUDIO_SAMPLE_8000 = 8000;
057  public static final int AUDIO_SAMPLE_11025 = 11025;
058  public static final int AUDIO_SAMPLE_12000 = 12000;
059  public static final int AUDIO_SAMPLE_16000 = 16000;
060  public static final int AUDIO_SAMPLE_22050 = 22050;
061  public static final int AUDIO_SAMPLE_32000 = 32000;
062  public static final int AUDIO_SAMPLE_44100 = 44100;
063  public static final int AUDIO_SAMPLE_48000 = 48000;
064  public static final int AUDIO_SAMPLE_96000 = 96000;
065
066  static final Pattern CODECS_REGEX =
067      Pattern.compile("^ ([.D][.E][VASD][.I][.L][.S]) (\\S{2,})\\s+(.*)$");
068  static final Pattern FORMATS_REGEX = Pattern.compile("^ ([ D][ E]) (\\S+)\\s+(.*)$");
069  static final Pattern PIXEL_FORMATS_REGEX =
070      Pattern.compile("^([.I][.O][.H][.P][.B]) (\\S{2,})\\s+(\\d+)\\s+(\\d+)$");
071
072  /** Supported codecs */
073  List<Codec> codecs = null;
074
075  /** Supported formats */
076  List<Format> formats = null;
077
078  /** Supported pixel formats */
079  private List<PixelFormat> pixelFormats = null;
080
081  public FFmpeg() throws IOException {
082    this(DEFAULT_PATH, new RunProcessFunction());
083  }
084
085  public FFmpeg(@Nonnull ProcessFunction runFunction) throws IOException {
086    this(DEFAULT_PATH, runFunction);
087  }
088
089  public FFmpeg(@Nonnull String path) throws IOException {
090    this(path, new RunProcessFunction());
091  }
092
093  public FFmpeg(@Nonnull String path, @Nonnull ProcessFunction runFunction) throws IOException {
094    super(path, runFunction);
095    version();
096  }
097
098  /**
099   * Returns true if the binary we are using is the true ffmpeg. This is to avoid conflict with
100   * avconv (from the libav project), that some symlink to ffmpeg.
101   *
102   * @return true iff this is the official ffmpeg binary.
103   * @throws IOException If a I/O error occurs while executing ffmpeg.
104   */
105  public boolean isFFmpeg() throws IOException {
106    return version().startsWith("ffmpeg");
107  }
108
109  /**
110   * Throws an exception if this is an unsupported version of ffmpeg.
111   *
112   * @throws IllegalArgumentException if this is not the official ffmpeg binary.
113   * @throws IOException If a I/O error occurs while executing ffmpeg.
114   */
115  private void checkIfFFmpeg() throws IllegalArgumentException, IOException {
116    if (!isFFmpeg()) {
117      throw new IllegalArgumentException(
118          "This binary '" + path + "' is not a supported version of ffmpeg");
119    }
120  }
121
122  public synchronized @Nonnull List<Codec> codecs() throws IOException {
123    checkIfFFmpeg();
124
125    if (this.codecs == null) {
126      codecs = new ArrayList<>();
127
128      Process p = runFunc.run(ImmutableList.of(path, "-codecs"));
129      try {
130        BufferedReader r = wrapInReader(p);
131        String line;
132        while ((line = r.readLine()) != null) {
133          Matcher m = CODECS_REGEX.matcher(line);
134          if (!m.matches()) continue;
135
136          codecs.add(new Codec(m.group(2), m.group(3), m.group(1)));
137        }
138
139        throwOnError(p);
140        this.codecs = ImmutableList.copyOf(codecs);
141      } finally {
142        p.destroy();
143      }
144    }
145
146    return codecs;
147  }
148
149  public synchronized @Nonnull List<Format> formats() throws IOException {
150    checkIfFFmpeg();
151
152    if (this.formats == null) {
153      formats = new ArrayList<>();
154
155      Process p = runFunc.run(ImmutableList.of(path, "-formats"));
156      try {
157        BufferedReader r = wrapInReader(p);
158        String line;
159        while ((line = r.readLine()) != null) {
160          Matcher m = FORMATS_REGEX.matcher(line);
161          if (!m.matches()) continue;
162
163          formats.add(new Format(m.group(2), m.group(3), m.group(1)));
164        }
165
166        throwOnError(p);
167        this.formats = ImmutableList.copyOf(formats);
168      } finally {
169        p.destroy();
170      }
171    }
172    return formats;
173  }
174
175  public synchronized List<PixelFormat> pixelFormats() throws IOException {
176    checkIfFFmpeg();
177
178    if (this.pixelFormats == null) {
179      pixelFormats = new ArrayList<>();
180
181      Process p = runFunc.run(ImmutableList.of(path, "-pix_fmts"));
182      try {
183        BufferedReader r = wrapInReader(p);
184        String line;
185        while ((line = r.readLine()) != null) {
186          Matcher m = PIXEL_FORMATS_REGEX.matcher(line);
187          if (!m.matches()) continue;
188          String flags = m.group(1);
189
190          pixelFormats.add(
191              new PixelFormat(
192                  m.group(2), Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)), flags));
193        }
194
195        throwOnError(p);
196        this.pixelFormats = ImmutableList.copyOf(pixelFormats);
197      } finally {
198        p.destroy();
199      }
200    }
201
202    return pixelFormats;
203  }
204
205  protected ProgressParser createProgressParser(ProgressListener listener) throws IOException {
206    // TODO In future create the best kind for this OS, unix socket, named pipe, or TCP.
207    try {
208      // Default to TCP because it is supported across all OSes, and is better than UDP because it
209      // provides good properties such as in-order packets, reliability, error checking, etc.
210      return new TcpProgressParser(checkNotNull(listener));
211    } catch (URISyntaxException e) {
212      throw new IOException(e);
213    }
214  }
215
216  @Override
217  public void run(List<String> args) throws IOException {
218    checkIfFFmpeg();
219    super.run(args);
220  }
221
222  public void run(FFmpegBuilder builder) throws IOException {
223    run(builder, null);
224  }
225
226  public void run(FFmpegBuilder builder, @Nullable ProgressListener listener) throws IOException {
227    checkNotNull(builder);
228
229    if (listener != null) {
230      try (ProgressParser progressParser = createProgressParser(listener)) {
231        progressParser.start();
232        builder = builder.addProgress(progressParser.getUri());
233
234        run(builder.build());
235      }
236    } else {
237      run(builder.build());
238    }
239  }
240
241  @CheckReturnValue
242  public FFmpegBuilder builder() {
243    return new FFmpegBuilder();
244  }
245
246  @Override
247  public String getPath() {
248    return path;
249  }
250}