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}