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