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}