001package net.bramp.ffmpeg.progress; 002 003import static com.google.common.base.Preconditions.checkNotNull; 004import static net.bramp.ffmpeg.FFmpegUtils.fromTimecode; 005 006import com.google.common.base.MoreObjects; 007import java.util.Objects; 008import javax.annotation.CheckReturnValue; 009import net.bramp.ffmpeg.FFmpegUtils; 010import org.apache.commons.lang3.math.Fraction; 011import org.slf4j.Logger; 012import org.slf4j.LoggerFactory; 013 014// TODO Change to be immutable 015/** Represents progress data reported by FFmpeg during encoding. */ 016public class Progress { 017 static final Logger LOG = LoggerFactory.getLogger(Progress.class); 018 019 /** Enum representing the status of FFmpeg progress updates. */ 020 public enum Status { 021 CONTINUE("continue"), 022 END("end"); 023 024 private final String status; 025 026 Status(String status) { 027 this.status = status; 028 } 029 030 @Override 031 public String toString() { 032 return status; 033 } 034 035 /** 036 * Returns the canonical status for this String or throws a IllegalArgumentException. 037 * 038 * @param status the status to convert to a Status enum. 039 * @return the Status enum. 040 * @throws IllegalArgumentException if the status is unknown. 041 */ 042 public static Status of(String status) { 043 for (Status s : Status.values()) { 044 if (status.equalsIgnoreCase(s.status)) { 045 return s; 046 } 047 } 048 049 throw new IllegalArgumentException("invalid progress status '" + status + "'"); 050 } 051 } 052 053 /** The frame number being processed. */ 054 public long frame = 0; 055 056 /** The current frames per second. */ 057 public Fraction fps = Fraction.ZERO; 058 059 /** Current bitrate. */ 060 public long bitrate = 0; 061 062 /** Output file size (in bytes). */ 063 public long total_size = 0; 064 065 // TODO Change this to a java.time.Duration 066 /** Output time (in nanoseconds). */ 067 public long out_time_ns = 0; 068 069 public long dup_frames = 0; 070 071 /** Number of frames dropped. */ 072 public long drop_frames = 0; 073 074 /** Speed of transcoding. 1 means realtime, 2 means twice realtime. */ 075 public float speed = 0; 076 077 /** Current status, can be one of "continue", or "end". */ 078 public Status status = null; 079 080 /** Constructs a default empty progress instance. */ 081 public Progress() { 082 // Nothing 083 } 084 085 /** Constructs a progress instance with the specified values. */ 086 public Progress( 087 long frame, 088 float fps, 089 long bitrate, 090 long total_size, 091 long out_time_ns, 092 long dup_frames, 093 long drop_frames, 094 float speed, 095 Status status) { 096 this.frame = frame; 097 this.fps = Fraction.getFraction(fps); 098 this.bitrate = bitrate; 099 this.total_size = total_size; 100 this.out_time_ns = out_time_ns; 101 this.dup_frames = dup_frames; 102 this.drop_frames = drop_frames; 103 this.speed = speed; 104 this.status = status; 105 } 106 107 /** 108 * Parses values from the line, into this object. 109 * 110 * <p>The value options are defined in ffmpeg.c's print_report function 111 * https://github.com/FFmpeg/FFmpeg/blob/master/ffmpeg.c 112 * 113 * @param line A single line of output from ffmpeg 114 * @return true if the record is finished 115 */ 116 protected boolean parseLine(String line) { 117 line = checkNotNull(line).trim(); 118 if (line.isEmpty()) { 119 return false; // Skip empty lines 120 } 121 122 final String[] args = line.split("=", 2); 123 if (args.length != 2) { 124 // invalid argument, so skip 125 return false; 126 } 127 128 final String key = checkNotNull(args[0]); 129 final String value = checkNotNull(args[1]); 130 131 switch (key) { 132 case "frame": 133 frame = Long.parseLong(value); 134 return false; 135 136 case "fps": 137 fps = Fraction.getFraction(value); 138 return false; 139 140 case "bitrate": 141 if (value.equals("N/A")) { 142 bitrate = -1; 143 } else { 144 bitrate = FFmpegUtils.parseBitrate(value); 145 } 146 return false; 147 148 case "total_size": 149 if (value.equals("N/A")) { 150 total_size = -1; 151 } else { 152 total_size = Long.parseLong(value); 153 } 154 return false; 155 156 case "out_time_ms": 157 // This is a duplicate of the "out_time" field, but expressed as a int instead of string. 158 // Note this value is in microseconds, not milliseconds, and is based on AV_TIME_BASE which 159 // could change. 160 // out_time_ns = Long.parseLong(value) * 1000; 161 return false; 162 163 case "out_time_us": 164 return false; 165 166 case "out_time": 167 out_time_ns = fromTimecode(value); 168 return false; 169 170 case "dup_frames": 171 dup_frames = Long.parseLong(value); 172 return false; 173 174 case "drop_frames": 175 drop_frames = Long.parseLong(value); 176 return false; 177 178 case "speed": 179 if (value.equals("N/A")) { 180 speed = -1; 181 } else { 182 speed = Float.parseFloat(value.replace("x", "")); 183 } 184 return false; 185 186 case "progress": 187 // TODO After "end" stream is closed 188 status = Status.of(value); 189 return true; // The status field is always last in the record 190 191 default: 192 if (key.startsWith("stream_")) { 193 // TODO handle stream_0_0_q=0.0: 194 // stream_%d_%d_q= file_index, index, quality 195 // stream_%d_%d_psnr_%c=%2.2f, file_index, index, type{Y, U, V}, quality // Enable with 196 // AV_CODEC_FLAG_PSNR 197 // stream_%d_%d_psnr_all 198 } else { 199 LOG.warn("skipping unhandled key: {} = {}", key, value); 200 } 201 202 return false; // Either way, not supported 203 } 204 } 205 206 @CheckReturnValue 207 public boolean isEnd() { 208 return status == Status.END; 209 } 210 211 @Override 212 public boolean equals(Object o) { 213 if (this == o) { 214 return true; 215 } 216 if (!(o instanceof Progress)) { 217 return false; 218 } 219 220 Progress progress1 = (Progress) o; 221 return frame == progress1.frame 222 && bitrate == progress1.bitrate 223 && total_size == progress1.total_size 224 && out_time_ns == progress1.out_time_ns 225 && dup_frames == progress1.dup_frames 226 && drop_frames == progress1.drop_frames 227 && Float.compare(progress1.speed, speed) == 0 228 && Objects.equals(fps, progress1.fps) 229 && Objects.equals(status, progress1.status); 230 } 231 232 @Override 233 public int hashCode() { 234 return Objects.hash( 235 frame, fps, bitrate, total_size, out_time_ns, dup_frames, drop_frames, speed, status); 236 } 237 238 @Override 239 public String toString() { 240 return MoreObjects.toStringHelper(this) 241 .add("frame", frame) 242 .add("fps", fps) 243 .add("bitrate", bitrate) 244 .add("total_size", total_size) 245 .add("out_time_ns", out_time_ns) 246 .add("dup_frames", dup_frames) 247 .add("drop_frames", drop_frames) 248 .add("speed", speed) 249 .add("status", status) 250 .toString(); 251 } 252 253 public long getFrame() { 254 return frame; 255 } 256 257 public Fraction getFps() { 258 return fps; 259 } 260 261 public long getBitrate() { 262 return bitrate; 263 } 264 265 public long getTotalSize() { 266 return total_size; 267 } 268 269 public long getOutTimeNs() { 270 return out_time_ns; 271 } 272 273 public long getDupFrames() { 274 return dup_frames; 275 } 276 277 public long getDropFrames() { 278 return drop_frames; 279 } 280 281 public float getSpeed() { 282 return speed; 283 } 284 285 public Status getStatus() { 286 return status; 287 } 288}