001package net.bramp.ffmpeg;
002
003import static com.google.common.base.Preconditions.checkArgument;
004import static java.util.concurrent.TimeUnit.*;
005import static net.bramp.ffmpeg.Preconditions.checkNotEmpty;
006
007import com.google.common.base.CharMatcher;
008import com.google.gson.Gson;
009import com.google.gson.GsonBuilder;
010import java.util.concurrent.TimeUnit;
011import java.util.regex.Matcher;
012import java.util.regex.Pattern;
013import net.bramp.commons.lang3.math.gson.FractionAdapter;
014import net.bramp.ffmpeg.gson.LowercaseEnumTypeAdapterFactory;
015import net.bramp.ffmpeg.gson.NamedBitsetAdapter;
016import net.bramp.ffmpeg.probe.FFmpegDisposition;
017import org.apache.commons.lang3.math.Fraction;
018
019/** Helper class with commonly used methods */
020public final class FFmpegUtils {
021
022  static final Gson gson = FFmpegUtils.setupGson();
023  static final Pattern BITRATE_REGEX = Pattern.compile("(\\d+(?:\\.\\d+)?)kbits/s");
024  static final Pattern TIME_REGEX = Pattern.compile("(\\d+):(\\d+):(\\d+(?:\\.\\d+)?)");
025  static final CharMatcher ZERO = CharMatcher.is('0');
026
027  FFmpegUtils() {
028    throw new AssertionError("No instances for you!");
029  }
030
031  /**
032   * Convert milliseconds to "hh:mm:ss.ms" String representation.
033   *
034   * @param milliseconds time duration in milliseconds
035   * @return time duration in human-readable format
036   * @deprecated please use #toTimecode() instead.
037   */
038  @Deprecated
039  public static String millisecondsToString(long milliseconds) {
040    return toTimecode(milliseconds, MILLISECONDS);
041  }
042
043  /**
044   * Convert the duration to "hh:mm:ss" timecode representation, where ss (seconds) can be decimal.
045   *
046   * @param duration the duration.
047   * @param units the unit the duration is in.
048   * @return the timecode representation.
049   */
050  public static String toTimecode(long duration, TimeUnit units) {
051    // FIXME Negative durations are also supported.
052    // https://www.ffmpeg.org/ffmpeg-utils.html#Time-duration
053    checkArgument(duration >= 0, "duration must be positive");
054
055    long nanoseconds = units.toNanos(duration); // TODO This will clip at Long.MAX_VALUE
056    long seconds = units.toSeconds(duration);
057    long ns = nanoseconds - SECONDS.toNanos(seconds);
058
059    long minutes = SECONDS.toMinutes(seconds);
060    seconds -= MINUTES.toSeconds(minutes);
061
062    long hours = MINUTES.toHours(minutes);
063    minutes -= HOURS.toMinutes(hours);
064
065    if (ns == 0) {
066      return String.format("%02d:%02d:%02d", hours, minutes, seconds);
067    }
068
069    return ZERO.trimTrailingFrom(String.format("%02d:%02d:%02d.%09d", hours, minutes, seconds, ns));
070  }
071
072  /**
073   * Returns the number of nanoseconds this timecode represents. The string is expected to be in the
074   * format "hour:minute:second", where second can be a decimal number.
075   *
076   * @param time the timecode to parse.
077   * @return the number of nanoseconds.
078   */
079  public static long fromTimecode(String time) {
080    checkNotEmpty(time, "time must not be empty string");
081    Matcher m = TIME_REGEX.matcher(time);
082    if (!m.find()) {
083      throw new IllegalArgumentException("invalid time '" + time + "'");
084    }
085
086    long hours = Long.parseLong(m.group(1));
087    long mins = Long.parseLong(m.group(2));
088    double secs = Double.parseDouble(m.group(3));
089
090    return HOURS.toNanos(hours) + MINUTES.toNanos(mins) + (long) (SECONDS.toNanos(1) * secs);
091  }
092
093  /**
094   * Converts a string representation of bitrate to a long of bits per second
095   *
096   * @param bitrate in the form of 12.3kbits/s
097   * @return the bitrate in bits per second or -1 if bitrate is 'N/A'
098   */
099  public static long parseBitrate(String bitrate) {
100    if ("N/A".equals(bitrate)) {
101      return -1;
102    }
103    Matcher m = BITRATE_REGEX.matcher(bitrate);
104    if (!m.find()) {
105      throw new IllegalArgumentException("Invalid bitrate '" + bitrate + "'");
106    }
107
108    return (long) (Float.parseFloat(m.group(1)) * 1000);
109  }
110
111  static Gson getGson() {
112    return gson;
113  }
114
115  private static Gson setupGson() {
116    GsonBuilder builder = new GsonBuilder();
117
118    builder.registerTypeAdapterFactory(new LowercaseEnumTypeAdapterFactory());
119    builder.registerTypeAdapter(Fraction.class, new FractionAdapter());
120    builder.registerTypeAdapter(
121        FFmpegDisposition.class, new NamedBitsetAdapter<>(FFmpegDisposition.class));
122
123    return builder.create();
124  }
125}