*/
package au.notzed.jjmpeg;
-import java.util.Collection;
-import java.util.Map.Entry;
import java.util.TreeMap;
/**
- * AVDictionary is a remarkably shithouse "ADT" inherited by ffmpeg from libav.
+ * AVDictionary is a remarkably shithouse "ADT" used for configuration parameters.
* <p>
- * Because the pointer can be changed by any of the update functions this
- * doesn't bother trying to wrap the C api. Instead it maintains it's own
- * set of data and converts it to an array of strings on demand.
+ * This does not wrap it, it simply provides an equivalent functionality
+ * using a TreeMap.
+ * </p>
+ * <h3>Historical Note</h3>
* <p>
- * It's a tree because ... I don't know.
+ * AVDictionary was inherited from libav. It's a miserable
+ * design, it's simply a pointer to an array which updates itself <em>by copying the whole array</em>
+ * every time it's length is changed.
+ * </p>
*/
public class AVDictionary extends TreeMap<String, String> {
+ /**
+ * Convert the dictionary to an array of {@code Entry<String,String>}.
+ * <p>
+ * This is used by the native binding to read elements.
+ *
+ * @return An array containing {@link java.util.Map.Entry} objects.
+ */
public Object[] toArray() {
return entrySet().toArray();
}
-
- public void setAll(Collection set) {
- this.entrySet().retainAll(set);
- }
-
- public String[] toDictionary() {
- String[] list = new String[size() * 2];
- int i = 0;
-
- for (Entry<String, String> e: entrySet()) {
- list[i++] = e.getKey();
- list[i++] = e.getValue();
- }
-
- return list;
- }
-
- public void fromDictionary(String[] set) {
- clear();
- if (set != null) {
- for (int i = 0; i < set.length; i += 2) {
- put(set[i], set[i + 1]);
- }
- }
- }
}
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;
*/
public class JJMediaReader implements AutoCloseable {
- JJReaderStream[] streams;
+ List<JJReaderStream> streams;
AVFormatContext format;
private AVPacket packet;
private boolean freePacket = false;
- //
- long seekid = -1;
+ /**
+ * Last decoded pts.
+ */
+ long pts;
+ /**
+ * Tracks the last seek()
+ */
+ long seek = -1;
+ /**
+ * Tracks the last seekMS()
+ */
long seekms = -1;
- //
/**
* VIDEO Type indicator for openDefaultStream().
public static final Class<JJReaderAudio> TYPE_AUDIO = JJReaderAudio.class;
/**
- * Create a new media reader, will scan the file for available streams.
+ * Open a media file.
+ * <p>
+ * This opens a media file and scans it's format. After it has been opened
+ * one must open the individual streams of interest and then use readFrame
+ * to decode the media frames.
*
- * @param name
+ * @param name Filename.
* @throws AVIOException
+ * @throws java.io.FileNotFoundException
+ * @see #getStreams
+ * @see #readFrame
*/
public JJMediaReader(String name) throws AVIOException, FileNotFoundException {
this(name, null, null);
// find all streams and map to something
int nstreams = format.getNumStreams();
- streams = new JJReaderStream[nstreams];
+ JJReaderStream[] list = new JJReaderStream[nstreams];
for (int i = 0; i < nstreams; i++) {
AVStream s = format.getStream(i);
AVCodecParameters cp = s.getCodecParameters();
switch (cp.getCodecType()) {
case AVMediaType.AVMEDIA_TYPE_VIDEO:
- streams[i] = new JJReaderVideo(s, cp);
+ list[i] = new JJReaderVideo(s, cp);
break;
case AVMediaType.AVMEDIA_TYPE_AUDIO:
- streams[i] = new JJReaderAudio(s, cp);
+ list[i] = new JJReaderAudio(s, cp);
break;
default:
- streams[i] = new JJReaderUnknown(s, cp);
+ list[i] = new JJReaderUnknown(s, cp);
s.setDiscard(AVDiscardBits.AVDISCARD_ALL);
break;
}
}
+ // Ideally List.of(streams) but that's java 9+
+ streams = new AbstractList<JJReaderStream>() {
+ @Override
+ public JJReaderStream get(int index) {
+ return list[index];
+ }
+
+ @Override
+ public int size() {
+ return list.length;
+ }
+ };
packet = AVPacket.alloc();
}
return null;
}
+ /**
+ * Get the list of streams.
+ *
+ * @return
+ */
public List<? extends JJReaderStream> getStreams() {
- // 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>() {
- int i = 0;
-
- @Override
- public boolean hasNext() {
- return i < streams.length;
- }
-
- @Override
- public JJReaderStream next() {
- return streams[i++];
- }
- };
- };
+ return streams;
}
+ /**
+ * Release and close all resources.
+ * <p>
+ * Resources will be automatically reclaimed by the JVM but this can be used
+ * to control when they are released.
+ * <p>
+ * The object or any sourced from it of the package au.notzed.jjmpeg must not be accessed thereafter.
+ */
public void release() {
for (JJReaderStream m: streams) {
m.release();
packet.release();
}
+ /**
+ * Close the media reader.
+ * <p>
+ * Calls {@link #release}.
+ */
@Override
public void close() {
release();
}
/**
- * Get source AVFormatContext.
+ * Get underlying AVFormatContext object.
*
* @return
*/
public AVFormatContext getFormat() {
return format;
}
- long pts;
/**
- * Retrieve (calculated) pts of the last frame decoded.
+ * Get pts of the last frame decoded.
* <p>
* Well be -1 at EOF
*
/**
* Retrieve current timestamp against the given stream.
*
- * @param rs
+ * @param rs Stream of interest
* @return time in milliseconds
*/
public long convertPTS(JJReaderStream rs) {
}
/**
- * call flushBuffers() on all opened streams codecs.
+ * Call flushBuffers() on all opened streams codecs.
* <p>
- * e.g. after a seek.
+ * This is called automatically after a seek.
*/
public void flushCodec() {
for (JJReaderStream rs: streams) {
}
/**
- * Attempt to seek to the nearest millisecond.
+ * Seek in milliseconds.
* <p>
* The next frame will have pts (in milliseconds) ≥ stamp.
*
* @param rs Stream to seek against.
* @param stamp
* @throws AVIOException
+ * @see #seekMS(JJReaderStream,long,long)
*/
public void seekMS(JJReaderStream rs, long stamp) throws AVIOException {
long current = rs.convertPTS(getPTS());
flushCodec();
}
+ /**
+ * Seek in milliseconds via key-frame.
+ * <p>
+ * This will first seek to a key-frame whose time-stamp is ≥ <code>keyStamp</code>
+ * and then advance by single frames until pts (in milliseconds) ≥ stamp;
+ *
+ * @param rs Stream to seek against.
+ * @param keyStamp Timestamp of a keyframe.
+ * @param stamp Timestamp of desired frame.
+ * @throws AVIOException
+ */
public void seekMS(JJReaderStream rs, long keyStamp, long stamp) throws AVIOException {
long current = rs.convertPTS(getPTS());
*/
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;
+ seek = stamp;
flushCodec();
}
while (format.readFrame(packet)) {
try {
int index = packet.getStreamIndex();
- JJReaderStream ms = streams[index];
+ JJReaderStream ms = streams.get(index);
if (ms != null && ms.isOpened() && ms.decode(packet)) {
pts = packet.getDTS();
// If seeking, attempt to get to the exact frame
- if (seekid != -1) {
- if (pts < seekid) {
+ if (seek != -1) {
+ if (pts < seek) {
Logger.getLogger("jjmpeg.io").fine(()
- -> String.format("seek=%d but @ %d flags=%d", seekid, pts, packet.getFlags()));
+ -> String.format("seek=%d but @ %d flags=%d", seek, pts, packet.getFlags()));
continue;
}
} else if (seekms != -1) {
-> String.format("seekms=%d now @ %d flags=%d *", seekms, ms.convertPTS(pts), packet.getFlags()));
}
- seekid = -1;
+ seek = -1;
seekms = -1;
freePacket = true;
return ms;
return null;
}
- JJReaderStream last;
-
- public JJReaderStream readFrameX() throws AVIOException {
- if (freePacket) {
- packet.clear();
- }
- freePacket = false;
-
- // Drain all frames from last stream first
- if (last != null && last.c.receiveFrame(last.frame)) {
- last.frameIndex += 1;
- pts = packet.getDTS();
- return last;
- }
- last = null;
-
- while (format.readFrame(packet)) {
- try {
- int index = packet.getStreamIndex();
- JJReaderStream ms = streams[index];
-
- if (ms != null && ms.isOpened()) {
- ms.c.sendPacket(packet);
- while (ms.c.receiveFrame(ms.frame)) {
- ms.frameIndex += 1;
- pts = packet.getDTS();
-
- // If seeking, attempt to get to the exact frame
- if (seekid != -1
- && pts < seekid) {
- continue;
- } else if (seekms != -1
- && ms.convertPTS(pts) < seekms) {
- continue;
- }
- last = ms;
- seekid = -1;
- seekms = -1;
- freePacket = true;
- return ms;
- }
- }
- } finally {
- if (!freePacket) {
- packet.clear();
- }
- }
- }
-
- return null;
-
- }
-
static public abstract class JJReaderStream {
AVStream stream;
return cp;
}
+ /**
+ * Get the underlying AVStream object.
+ *
+ * @return
+ */
public AVStream getStream() {
return stream;
}
+ /**
+ * Get the underlying AVCodec object.
+ *
+ * @return
+ */
public AVCodec getCodec() {
return codec;
}
+ /**
+ * Get the underlying AVCodecContext object.
+ *
+ * @return
+ */
public AVCodecContext getContext() {
return c;
}
/**
- * Retrieve duration of sequence, in milliseconds.
+ * Get stream duration in milliseconds.
*
* @return
*/
}
/**
- * Get duration in timebase units (i.e. frames?)
+ * Get stream duration in timebase units.
*
* @return
*/
}
/**
- * Decode a packet. Returns true if data is now ready.
- * <p>
- * It is ok to call this on an unopened stream: return false.
+ * Decode a packet into the current frame.
* <p>
- * TODO: this is now common across different formats, up-class the code.
+ * It is ok to call this on an unopened stream.
*
* @param packet
- * @return
+ * @return <code>true</code> if the decoded data is now ready.
+ * @see #getFrame
*/
public boolean decode(AVPacket packet) throws AVIOException {
boolean done = false;
}
/**
- * Retrieve the decoded frame. This is only valid after a call to decode() returns true.
+ * Retrieve the decoded frame.
+ * <p>
+ * The frame is reused for each decoded frame so the value returned remains
+ * static for the lifetime of the JJReaderStream.
+ * <p>
+ * This is only valid after a call to decode() returns true.
*
* @return
*/