001package net.bramp.ffmpeg;
002
003import static java.util.concurrent.TimeUnit.HOURS;
004import static java.util.concurrent.TimeUnit.MILLISECONDS;
005import static java.util.concurrent.TimeUnit.MINUTES;
006import static java.util.concurrent.TimeUnit.SECONDS;
007import static net.bramp.ffmpeg.Preconditions.checkNotEmpty;
008
009import com.google.common.base.CharMatcher;
010import com.google.errorprone.annotations.InlineMe;
011import com.google.gson.Gson;
012import com.google.gson.GsonBuilder;
013import java.util.concurrent.TimeUnit;
014import java.util.regex.Matcher;
015import java.util.regex.Pattern;
016import net.bramp.commons.lang3.math.gson.FractionAdapter;
017import net.bramp.ffmpeg.adapter.FFmpegPacketsAndFramesAdapter;
018import net.bramp.ffmpeg.adapter.FFmpegStreamSideDataAdapter;
019import net.bramp.ffmpeg.gson.LowercaseEnumTypeAdapterFactory;
020import net.bramp.ffmpeg.probe.FFmpegFrameOrPacket;
021import net.bramp.ffmpeg.probe.FFmpegStream;
022import org.apache.commons.lang3.math.Fraction;
023
024/** Helper class with commonly used methods. */
025public final class FFmpegUtils {
026
027  static final Gson gson = FFmpegUtils.setupGson();
028  static final Pattern BITRATE_REGEX = Pattern.compile("(\\d+(?:\\.\\d+)?)kbits/s");
029  static final Pattern TIME_REGEX = Pattern.compile("(-?)(\\d+):(\\d+):(\\d+(?:\\.\\d+)?)");
030  static final CharMatcher ZERO = CharMatcher.is('0');
031
032  FFmpegUtils() {
033    throw new AssertionError("No instances for you!");
034  }
035
036  /**
037   * Convert milliseconds to "hh:mm:ss.ms" String representation.
038   *
039   * @param milliseconds time duration in milliseconds
040   * @return time duration in human-readable format
041   * @deprecated please use #toTimecode() instead.
042   */
043  @Deprecated
044  @InlineMe(
045      replacement = "FFmpegUtils.toTimecode(milliseconds, MILLISECONDS)",
046      imports = "net.bramp.ffmpeg.FFmpegUtils",
047      staticImports = "java.util.concurrent.TimeUnit.MILLISECONDS")
048  public static String millisecondsToString(long milliseconds) {
049    return toTimecode(milliseconds, MILLISECONDS);
050  }
051
052  /**
053   * Convert the duration to "hh:mm:ss" timecode representation, where ss (seconds) can be decimal.
054   *
055   * @param duration the duration.
056   * @param units the unit the duration is in.
057   * @return the timecode representation.
058   */
059  public static String toTimecode(long duration, TimeUnit units) {
060    String prefix = "";
061    if (duration < 0) {
062      prefix = "-";
063      duration = Math.abs(duration);
064    }
065
066    long nanoseconds = units.toNanos(duration); // TODO This will clip at Long.MAX_VALUE
067    long seconds = units.toSeconds(duration);
068    long ns = nanoseconds - SECONDS.toNanos(seconds);
069
070    long minutes = SECONDS.toMinutes(seconds);
071    seconds -= MINUTES.toSeconds(minutes);
072
073    long hours = MINUTES.toHours(minutes);
074    minutes -= HOURS.toMinutes(hours);
075
076    String result;
077    if (ns == 0) {
078      result = String.format("%02d:%02d:%02d", hours, minutes, seconds);
079    } else {
080      result =
081          ZERO.trimTrailingFrom(String.format("%02d:%02d:%02d.%09d", hours, minutes, seconds, ns));
082    }
083
084    return prefix + result;
085  }
086
087  /**
088   * Converts milliseconds to a seconds string representation. Uses integer format when there are no
089   * fractional seconds, otherwise uses decimal format with up to 3 decimal places and trailing
090   * zeros removed.
091   *
092   * @param millis duration in milliseconds
093   * @return seconds as a string (e.g. "5", "0.005", "1.5")
094   */
095  public static String millisToSeconds(long millis) {
096    if (millis % 1000 == 0) {
097      return String.valueOf(millis / 1000);
098    }
099    // Use up to 3 decimal places, trim trailing zeros
100    String s = String.format("%.3f", millis / 1000.0);
101    s = s.contains(".") ? s.replaceAll("0+$", "").replaceAll("\\.$", "") : s;
102    return s;
103  }
104
105  /**
106   * Returns the number of nanoseconds this timecode represents. The string is expected to be in the
107   * format "hour:minute:second", where second can be a decimal number.
108   *
109   * @param time the timecode to parse.
110   * @return the number of nanoseconds or -1 if time is 'N/A'
111   */
112  public static long fromTimecode(String time) {
113    checkNotEmpty(time, "time must not be empty string");
114
115    if (time.equals("N/A")) {
116      return -1;
117    }
118
119    Matcher m = TIME_REGEX.matcher(time);
120    if (!m.find()) {
121      throw new IllegalArgumentException("invalid time '" + time + "'");
122    }
123
124    long sign = m.group(1).equals("-") ? -1 : 1;
125    long hours = Long.parseLong(m.group(2));
126    long mins = Long.parseLong(m.group(3));
127    double secs = Double.parseDouble(m.group(4));
128
129    return sign
130        * (HOURS.toNanos(hours) + MINUTES.toNanos(mins) + (long) (SECONDS.toNanos(1) * secs));
131  }
132
133  /**
134   * Converts a string representation of bitrate to a long of bits per second.
135   *
136   * @param bitrate in the form of 12.3kbits/s
137   * @return the bitrate in bits per second or -1 if bitrate is 'N/A'
138   */
139  public static long parseBitrate(String bitrate) {
140    checkNotEmpty(bitrate, "bitrate must not be empty string");
141
142    if ("N/A".equals(bitrate)) {
143      return -1;
144    }
145    Matcher m = BITRATE_REGEX.matcher(bitrate);
146    if (!m.find()) {
147      throw new IllegalArgumentException("Invalid bitrate '" + bitrate + "'");
148    }
149
150    return (long) (Float.parseFloat(m.group(1)) * 1000);
151  }
152
153  static Gson getGson() {
154    return gson;
155  }
156
157  private static Gson setupGson() {
158    GsonBuilder builder = new GsonBuilder();
159
160    builder.registerTypeAdapterFactory(new LowercaseEnumTypeAdapterFactory());
161    builder.registerTypeAdapter(Fraction.class, new FractionAdapter());
162    builder.registerTypeAdapter(FFmpegFrameOrPacket.class, new FFmpegPacketsAndFramesAdapter());
163    builder.registerTypeAdapter(FFmpegStream.SideData.class, new FFmpegStreamSideDataAdapter());
164
165    return builder.create();
166  }
167}