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}