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