import au.notzed.jjmpeg.AVSampleFormat;
import au.notzed.jjmpeg.AVStream;
import java.io.FileNotFoundException;
+import java.util.AbstractList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;
/**
* High level interface for scanning audio and video frames.
* <p>
- * TODO: handle all frames.
- *
- * @author notzed
+ * <h2>Seeking</h2>
+ * <p>
+ * {@link AVFormatContext#seekFile} has unreliable behaviour
+ * depending on the codec and container. In general it seems
+ * to seek to the keyframe at or after the requested timestamp.
+ * As such, for reliable seeking the seek must be offset by a
+ * delay and then the correct frame found by decoding frames
+ * one by one.
+ * <p>
+ * Each JJReaderStream has a pts and millisecond offset which is
+ * estimated based on the frame-rate of the video file. A
+ * conservative estimate is used which means seeking may be slower
+ * than possible.
+ * For all-keyframe formats this shouldn't be required but it has not
+ * been special-cased.
+ * <p>
+ * An alternative is provided by the 2-argument {@link #seekMS}
+ * method which takes the timestamp of a keyframe together with
+ * the timestamp of a specific frame. This does the obvious and
+ * seeks to the keyframe and then single-steps to the desired frame
+ * and provides the highest performance general seek function but
+ * requires an external index.
+ * <p>
*/
public class JJMediaReader implements AutoCloseable {
}
public List<? extends JJReaderStream> getStreams() {
- return List.of(streams);
+ // Ideally List.of(streams) but that's java 9+
+ return new AbstractList<JJReaderStream>() {
+ @Override
+ public JJReaderStream get(int index) {
+ return streams[index];
+ }
+
+ @Override
+ public int size() {
+ return streams.length;
+ }
+ };
}
-
+
public Iterable<? extends JJReaderStream> streams() {
return () -> {
return new Iterator<JJReaderStream>() {
return pts;
}
+ /**
+ * Retrieve current timestamp against the given stream.
+ *
+ * @param rs
+ * @return time in milliseconds
+ */
+ public long convertPTS(JJReaderStream rs) {
+ return rs.convertPTS(pts);
+ }
+
/**
* call flushBuffers() on all opened streams codecs.
* <p>
}
}
- private long currentVideoMS() {
- for (JJReaderStream r: streams) {
- if (r instanceof JJReaderVideo) {
- return r.convertPTS(getPTS());
- }
- }
- return -1;
- }
-
/**
* Attempt to seek to the nearest millisecond.
* <p>
* The next frame will have pts (in milliseconds) ≥ stamp.
*
+ * @param rs Stream to seek against.
* @param stamp
* @throws AVIOException
*/
- public void seekMS(long stamp) throws AVIOException {
- // This is an attempt at an optimisation, for small seeks (< 1 second)
- // just parse/discard frames.
- // This is going to be faster on many codecs than a flush/seek which will probably
- // have to jump through more frames after a keyframe anyway
- long current = currentVideoMS();
- if (current != -1 && (stamp - current) >= 0 && (stamp - current) < 1000) {
- seekms = stamp;
- } else {
- format.seekFile(-1, 0, stamp * 1000, stamp * 1000, 0);
+ public void seekMS(JJReaderStream rs, long stamp) throws AVIOException {
+ long current = rs.convertPTS(getPTS());
+
+ if (current != -1 && (stamp - current) > 0 && (stamp - current) < 1000) {
seekms = stamp;
+ return;
+ }
+
+ format.seekFile(-1, 0, Math.max(0, stamp - rs.seekOffsetMS) * 1000, stamp * 1000, 0);
+ seekms = stamp;
+
+ flushCodec();
+ }
+
+ public void seekMS(JJReaderStream rs, long keyStamp, long stamp) throws AVIOException {
+ long current = rs.convertPTS(getPTS());
- flushCodec();
+ if (current != -1 && (stamp - current) > 0 && (stamp - current) < 2000) {
+ seekms = stamp;
+ return;
}
+
+ format.seekFile(-1, 0, keyStamp * 1000, keyStamp * 1000, 0);
+ seekms = stamp;
+
+ flushCodec();
}
/**
* <p>
* The next frame will have pts ≥ stamp
*
+ * @param rs Stream to seek against.
* @param stamp
* @throws AVIOException
*/
- public void seek(long stamp) throws AVIOException {
- format.seekFile(-1, 0, stamp, stamp, 0);
+ public void seek(JJReaderStream rs, long stamp) throws AVIOException {
+ format.seekFile(rs.stream.getIndex(), 0, Math.max(0, stamp - rs.seekOffset), stamp, 4);
seekid = stamp;
flushCodec();
int index = packet.getStreamIndex();
JJReaderStream ms = streams[index];
- if (ms != null) {
- if (ms.isOpened() && ms.decode(packet)) {
- pts = packet.getDTS();
+ if (ms != null && ms.isOpened() && ms.decode(packet)) {
+ pts = packet.getDTS();
- // If seeking, attempt to get to the exact frame
- if (seekid != -1
- && pts < seekid) {
+ // If seeking, attempt to get to the exact frame
+ if (seekid != -1) {
+ if (pts < seekid) {
+ Logger.getLogger("jjmpeg.io").fine(()
+ -> String.format("seek=%d but @ %d flags=%d", seekid, pts, packet.getFlags()));
continue;
- } else if (seekms != -1
- && ms.convertPTS(pts) < seekms) {
+ }
+ } else if (seekms != -1) {
+ long ptsms = ms.convertPTS(pts);
+
+ if (ptsms < seekms) {
+ Logger.getLogger("jjmpeg.io").fine(()
+ -> String.format("seekms=%d but @ %d flags=%d", seekms, ms.convertPTS(pts), packet.getFlags()));
continue;
}
- seekid = -1;
- seekms = -1;
- freePacket = true;
- return ms;
+ Logger.getLogger("jjmpeg.io").fine(()
+ -> String.format("seekms=%d now @ %d flags=%d *", seekms, ms.convertPTS(pts), packet.getFlags()));
}
+
+ seekid = -1;
+ seekms = -1;
+ freePacket = true;
+ return ms;
}
+
} finally {
if (!freePacket) {
packet.clear();
}
return null;
+
}
static public abstract class JJReaderStream {
AVStream stream;
AVCodecContext c;
AVCodecParameters cp;
- int streamID = -1;
protected AVCodec codec;
protected boolean opened = false;
// timebase
final AVFrame frame;
// Incremented every time a frame is decoded
long frameIndex;
+ // Some formats (mpeg?) always seek to the next keyframe, which is really not desirable.
+ long seekOffset;
+ long seekOffsetMS;
public JJReaderStream(AVStream stream, AVCodecParameters cp) throws AVIOException {
this.stream = stream;
startms = tb.rescale(startpts, 1000);
duration = stream.getDuration();
durationms = tb.rescale(duration, 1000);
+
+ // conservatively assume keyframe every 32 frames
+ seekOffsetMS = stream.getAverageFrameRate().inv().rescale(1000, 32);
+ seekOffset = (long) tb.inv().mul(new AVRational((int) seekOffsetMS, 1000)).doubleValue();
}
public void open() throws AVIOException {
codec.release();
c.release();
}
- frame.release();
+ if (frame != null)
+ frame.release();
stream.release();
}
}
/**
- * Convert the 'pts' provided to milliseconds relative to the start of the stream.
+ * Convert the 'pts' provided to milliseconds.
*
* @param pts
* @return
*/
public long convertPTS(long pts) {
- return tb.rescale(pts - startpts, 1000);
- }
-
- /**
- * Convert the 'pts' to milliseconds as presented in the stream.
- *
- * @param pts
- * @return
- */
- public long absolutePTS(long pts) {
return tb.rescale(pts, 1000);
}
boolean done = false;
if (isOpened()) {
if (packet != null && packet.getSize() == 0) {
- Logger.getLogger("jjmpeg.io")
- .fine(()
- -> String.format("stream %d packet size 0", streamID));
+ Logger.getLogger("jjmpeg.io").fine(()
+ -> String.format("stream %d packet size 0", stream.getIndex()));
return done;
}
// throw new AVIOException(AVError.AVERROR_INVALIDDATA, "No decodable video present");
//}
- Logger.getLogger("jjmpeg.io")
- .fine(() -> {
- return String.format("Open video reader\n"
- + " video %dx%d %s [aspect %s]\n"
- + " codec %s\n",
- cp.getWidth(),
- cp.getHeight(),
- AVPixelFormat.toString(cp.getPixelFormat()),
- cp.getAspectRatio(),
- AVCodecID.toString(cp.getCodecID()));
- });
+ Logger.getLogger("jjmpeg.io").fine(()
+ -> String.format("Open video reader\n"
+ + " video %dx%d %s [aspect %s]\n"
+ + " codec %s\n",
+ cp.getWidth(),
+ cp.getHeight(),
+ AVPixelFormat.toString(cp.getPixelFormat()),
+ cp.getAspectRatio(),
+ AVCodecID.toString(cp.getCodecID())));
super.open();
}
@Override
public void open() throws AVIOException {
- Logger.getLogger("jjmpeg.io")
- .fine(() -> {
- return String.format("Open audio reader\n"
- + " audio x%d %dHz %s\n"
- + " codec %s\n",
- cp.getNumChannels(),
- cp.getSampleRate(),
- AVSampleFormat.toString(cp.getSampleFormat()),
- AVCodecID.toString(cp.getCodecID()));
- });
+ Logger.getLogger("jjmpeg.io").fine(()
+ -> String.format("Open audio reader\n"
+ + " audio x%d %dHz %s\n"
+ + " codec %s\n",
+ cp.getNumChannels(),
+ cp.getSampleRate(),
+ AVSampleFormat.toString(cp.getSampleFormat()),
+ AVCodecID.toString(cp.getCodecID())));
super.open();
}