From: Michael Zucchi Date: Sun, 12 May 2019 09:14:00 +0000 (+0930) Subject: VideoPlay demo enhancements X-Git-Url: https://code.zedzone.au/cvs?a=commitdiff_plain;h=813fb32b886b4c7c74ec23d7d0988d67b6992305;p=jjmpeg VideoPlay demo enhancements Make sure jni library is loaded for non-AVObject derived static methods. Checkpoint ongoing work on doc improvements Checkpoint ongoing work on accessor cleanup Auto-generate some of the AVIOContext flags AVIOContext improvements JJMediaWriter improvements Attach all threads as daemons in jni Change AVPacket interface for accessing data buffer --- diff --git a/src/notzed.jjmpeg.demo/classes/au/notzed/jjmpeg/demo/video/VideoPlay.java b/src/notzed.jjmpeg.demo/classes/au/notzed/jjmpeg/demo/video/VideoPlay.java index a2252d8..112c761 100644 --- a/src/notzed.jjmpeg.demo/classes/au/notzed/jjmpeg/demo/video/VideoPlay.java +++ b/src/notzed.jjmpeg.demo/classes/au/notzed/jjmpeg/demo/video/VideoPlay.java @@ -24,21 +24,25 @@ import au.notzed.jjmpeg.io.JJMediaReader; import java.io.FileNotFoundException; import java.io.IOException; import static java.lang.Double.min; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.time.Instant; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.List; -import java.util.TimeZone; import java.util.concurrent.CountDownLatch; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.geometry.Insets; +import javafx.geometry.Rectangle2D; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.image.ImageView; import javafx.scene.image.WritableImage; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; @@ -47,6 +51,7 @@ import javafx.scene.paint.Color; import javafx.scene.text.Text; import javafx.scene.transform.Scale; import javafx.scene.transform.Translate; +import javafx.stage.Screen; import javafx.stage.Stage; /** @@ -87,7 +92,7 @@ public class VideoPlay extends Application { WritableImage image; Text label; Stage stage; - SimpleDateFormat time; + DateTimeFormatter time; long time0 = -1; long stamp0; @@ -96,8 +101,7 @@ public class VideoPlay extends Application { height = vs.getHeight(); frameReader = new FXPixelReader(vs.getFrame().getPixelReader(width, height, 0)); - time = new SimpleDateFormat("HH:mm:ss.SSS"); - time.setTimeZone(TimeZone.getTimeZone("GMT")); + time = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); Platform.runLater(() -> { label = new Text(); @@ -115,7 +119,20 @@ public class VideoPlay extends Application { ap.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY))); - Scene scene = new Scene(ap, width, height); + Rectangle2D bounds = Screen.getPrimary().getVisualBounds(); + double swidth = bounds.getWidth() - 32; + double sheight = bounds.getHeight() - 32; + Scene scene; + if (width > swidth || height > sheight) { + double scalex = swidth / width; + double scaley = sheight / height; + + swidth = scalex < scaley ? swidth : width * scaley; + sheight = scalex < scaley ? height * scalex : sheight; + scene = new Scene(ap, swidth, sheight); + } else { + scene = new Scene(ap, width, height); + } stage = new Stage(); stage.setScene(scene); @@ -145,6 +162,12 @@ public class VideoPlay extends Application { ap.widthProperty().addListener(fitVideo); ap.heightProperty().addListener(fitVideo); + fitVideo.invalidated(null); + + scene.getAccelerators().put(new KeyCodeCombination(KeyCode.Q), () -> { + cancel = true; + stage.close(); + }); }); } @@ -161,7 +184,8 @@ public class VideoPlay extends Application { * @param stamp */ void videoFrame(JJMediaReader.JJReaderVideo vs, long stamp) { - String ts = time.format(new Date(stamp)); + Instant x = Instant.ofEpochMilli(stamp + vs.getStartMS()); + String ts = time.format(LocalTime.ofInstant(x, ZoneId.of("UTC"))); CountDownLatch latch = new CountDownLatch(1); // Display the frame. Must run on javafx thread. @@ -196,9 +220,11 @@ public class VideoPlay extends Application { * @param x */ void videoError(IOException x) { - Alert a = new Alert(Alert.AlertType.ERROR, x.getLocalizedMessage(), ButtonType.CLOSE); - a.showAndWait(); - Platform.exit(); + Platform.runLater(() -> { + Alert a = new Alert(Alert.AlertType.ERROR, x.getLocalizedMessage(), ButtonType.CLOSE); + a.showAndWait(); + Platform.exit(); + }); } volatile boolean cancel = false; @@ -226,6 +252,7 @@ public class VideoPlay extends Application { } } catch (AVIOException | FileNotFoundException ex) { + ex.printStackTrace(); videoError(ex); } } diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVChannelLayout.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVChannelLayout.java index aed143a..9a8bbd3 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVChannelLayout.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVChannelLayout.java @@ -20,7 +20,11 @@ package au.notzed.jjmpeg; /** */ -public class AVChannelLayout extends AVObject implements AVChannelLayoutBits { +public class AVChannelLayout implements AVChannelLayoutBits { + + static { + AVObject.init(); + } /** * Call av_get_channel_layout(name) @@ -48,7 +52,4 @@ public class AVChannelLayout extends AVObject implements AVChannelLayoutBits { */ public static native long getDefaultLayout(int nchannels); - private AVChannelLayout(long p) { - super(p); - } } diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVCodec.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVCodec.java index 38fe130..cd1356e 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVCodec.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVCodec.java @@ -51,15 +51,34 @@ public class AVCodec extends AVObject implements AVCodecBits, AVDiscardBits { public static native AVCodec findEncoder(String name); - /* - Accessors + /** + * Get AVCodec.name. + * + * @return */ public native String getName(); + /** + * Get AVCodec.long_name. + * + * @return + */ public native String getLongName(); + /** + * Get AVCodec.type. + * + * @return + * @see AVMediaType + */ public native int getType(); + /** + * Get AVCodec.id. + * + * @return + * @see AVCodecID + */ public native int getID(); public native int getCapabilities(); diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVCodecContext.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVCodecContext.java index 9e5e8e5..d3fae47 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVCodecContext.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVCodecContext.java @@ -21,6 +21,14 @@ package au.notzed.jjmpeg; /** * AVCodecContext accessors. *

+ * The accessors here are currently implemented as direct struct accessors but + * most fields may also be accessed by the {@link AVOptions} accessor methods. The + * AVOptions methods are somewhat inconvenient but provide additional + * validation checks for setting options. + *

+ * Note that there are many other options, these are documented in libavcodec/avcodec.h and the ffmpeg + * source in libavcodec/options_table.h. This can also be retrieved at runtime + * via */ public class AVCodecContext extends AVOptions implements AVCodecContextBits { @@ -28,46 +36,84 @@ public class AVCodecContext extends AVOptions implements AVCodecContextBits { super(p); } + /** + * Free the context. + *

+ * Calls avcodec_free_context(). + * + * @param p + */ private static native void release(long p); - /* - * Methods - */ /** + * Allocate and initialise an AVCodecContext. + *

+ * Calls avcodec_alloc_context3(). * - * @param codec + * @param codec Codec used to initialise parameters, may be null. * @return */ public static native AVCodecContext allocContext(AVCodec codec); - // avcodec_open2 + /** + * Open a context. + *

+ * Calls avcodec_open2(). + * + * @param codec Codec used. It must match the one used in allocContext(). + * @param options Codec options. + * @throws AVIOException is thrown iv avcodec_open2 returns an error. + */ public native void open(AVCodec codec, AVDictionary options) throws AVIOException; + /** + * Flush internal buffers. + *

+ * Used to reset codec state, for example after a seek. + *

+ * Calls avcodec_flush_buffers(). + */ public native void flushBuffers(); + /** + * Align dimensions suitable for codec. + *

+ * Calls avcodec_align_dimensions(). + * + * @param input Input dimensions. + * @return Modified dimensions. + */ public native AVSize alignDimensions(AVSize input); - + /** - * Calls avcodec_send_packet() + * Submit raw packet to a decoder. + *

+ * Packets are submitted to a decoder which generates output frames. *

* If the return value is EAGAIN or AVERROR_EOF then this function * returns false, for all other error values it throws an exception. + *

+ * Calls avcodec_send_packet(). * * @param pkt * @return true if the packet was accepted, false if a frame must be received first. * @throws AVIOException + * @see #receiveFrame(au.notzed.jjmpeg.AVFrame) */ public native boolean sendPacket(AVPacket pkt) throws AVIOException; /** - * Calls avcodec_receive_frame(). + * Retrieve a decoded frame from a decoder. *

- * If the return value is EAGAIN or AVERROR_EOF then this function - * returns false, for all other error values it throws an exception. + * If the return value from avcodec_receive_frame() is EAGAIN or AVERROR_EOF + * then this function returns false, for all other error values it throws an exception. + *

+ * Calls avcodec_receive_frame(). * * @param frame * @return Returns true if the frame is complete, false otherwise. * @throws AVIOException + * @see #sendPacket(au.notzed.jjmpeg.AVPacket) */ public native boolean receiveFrame(AVFrame frame) throws AVIOException; @@ -95,203 +141,346 @@ public class AVCodecContext extends AVOptions implements AVCodecContextBits { */ public native boolean receivePacket(AVPacket pkt) throws AVIOException; - /* - * Accessors + /** + * Get encoding/decoding AVCodecContext.codec_type. + * + * @return + * @see AVMediaType */ public native int getCodecType(); + /** + * Set encoding/decoding AVCodecContext.codec_type. + * + * @param val + * @see AVMediaType + */ public native void setCodecType(int val); + /** + * Get encoding/decoding AVCodecContext.codec_id. + * + * @return + * @see AVCodecID + */ public native int getCodecID(); + /** + * Set encoding/decoding AVCodecContext.codec_id. + * + * @param val + * @see AVCodecID + */ public native void setCodecID(int val); - public native int getCodecTag(); - - public native void setCodecTag(int val); - + /** + * Get encoding/decoding AVCodecContext.bit_rate. + *

+ * AVOption int "b" or "ab" + * + * @return + */ public native long getBitRate(); - public native void setBitRate(long val); - - public native int getBitRateTolerance(); - - public native void setBitRateTolerance(int val); + /** + * Set encoding/decoding AVCodecContext.bit_rate. + *

+ * AVOption int "b" or "ab" + * + * @param bitrate + */ + public native void setBitRate(long bitrate); + /** + * Get encoding AVCodecContext.global_quality. + *

+ * AVOption int "global_quality". + * + * @return + */ public native int getGlobalQuality(); - public native void setGlobalQuality(int val); - - public native int getCompressionLevel(); - - public native void setCompressionLevel(int val); + /** + * Set AVCodecContext.global_quality. + *

+ * AVOption int "global_quality". + * + * @param quality + */ + public native void setGlobalQuality(int quality); /** - * Get the contents of flags and flags2 together as a single 64-bit value. + * Get the encoding/decoding AVCodecContext.flags and AVCodecContext.flags2. + *

+ * This combines .flags and .flags2 into a single 64-bit flag set. The + * AV_CODE_FLAG2_* constants have been shifted appropriately. + *

+ * AVOption int "flag", also specific flags "unaligned", "mv4", and so on. * * @return */ public native long getFlags(); /** - * Set or clear flags and flags2. + * Adjust the encoding/decoding AVCodecContext.flags and AVCodecContext.flags2. + *

+ * This performs a combined get and set operation on the flags allowing any cobimation + * of flags to be set in a single call. + *

+ * AVOption int "flag", also specific flags "unaligned", "mv4", and so on. * - * @param mask Mask of bits to change. Only flags with a bit set here will be altered. + * @param mask Mask of bits to change. Flags with a bit set here will be set to the values in flags. * @param flags Value to set bits to. + * @see #getFlags() for an explanation of the flag values. */ public native void setFlags(long mask, long flags); /** - * Get the timebase. + * Get the encoding AVCodecContext.time_base. *

- * Note that the return value is a value type (copy) not a reference. + * AVOption rational (q) "time_base". * - * @return + * @return A copy of the time_base (adjustments will not be carried through). */ public native AVRational getTimeBase(); - public native void setTimeBase(AVRational val); - - public native int getTicksPerFrame(); - - public native void setTicksPerFrame(int val); + /** + * Set the encoding AVCodecContext.time_base. + *

+ * A time_base must always be set for encoding. + *

+ * AVOption rational (q) "time_base". + * + * @param timebase + */ + public native void setTimeBase(AVRational timebase); + /** + * Get the encoding/decoding AVCodecContext.delay. + *

+ * AVOption int "delay". + * + * @return + */ public native int getDelay(); - /* - Video only options + /** + * Get video encoding/decoding AVCodecContext.width. + *

+ * AVOption image size "video_size". + * + * @return */ public native int getWidth(); - public native void setWidth(int val); + /** + * Set video encoding/decoding AVCodecContext.width. + *

+ * AVOption image size "video_size". + * + * @param width + */ + public native void setWidth(int width); + /** + * Get video encoding/decoding AVCodecContext.height. + *

+ * AVOption image size "video_size". + * + * @return + */ public native int getHeight(); - public native void setHeight(int val); + /** + * Set video encoding/decoding AVCodecContext.width. + *

+ * AVOption image size "video_size". + * + * @param height + */ + public native void setHeight(int height); + /** + * Get video encoding/decoding AVCodecContext.sample_aspect_ratio. + *

+ * AVOption rational "aspect" or "sar". + * + * @return + */ public native AVRational getAspectRatio(); - public native void setAspectRatio(AVRational r); + /** + * Set video encoding/decoding AVCodecContext.sample_aspect_ratio. + *

+ * AVOption rational "aspect" or "sar". + * + * @param ratio + */ + public native void setAspectRatio(AVRational ratio); - // codedWidth, codedHeight + /** + * Get video encoding AVCodecContext.gop_size. + *

+ * AVOption int "g". + * + * @return + */ public native int getGOPSize(); + /** + * Set video encoding AVCodecContext.gop_size. + *

+ * AVOption int "g". + * + * @param val + */ public native void setGOPSize(int val); + /** + * Get video encoding/decoding AVCodecContext.pix_fmt. + *

+ * AVOption pixel format "pixel_format". + * + * @return + * @see AVPixelFormat + */ public native int getPixelFormat(); - public native void setPixelFormat(int val); - - // - public native int getMaxBFrames(); - - public native void setMaxBFrames(int val); - - public native float getBQuantFactor(); - - public native void setBQuantFactor(float val); - - public native float getBQuantOffset(); - - public native void setBQuantOffset(float val); - - public native boolean hasBFrames(); - - public native float getIQuantFactor(); - - public native void setIQuantFactor(float val); - - public native float getIQuantOffset(); - - public native void setIQuantOffset(float val); -// And a whole lot more ... -// - - public native int getMBDecision(); - - public native void setMBDecision(int val); -// ... + /** + * Set video encoding/decoding AVCodecContext.pix_fmt. + *

+ * AVOption pixel format "pixel_format". + * + * @param format + * @see AVPixelFormat + */ + public native void setPixelFormat(int format); - /* - Audtio only options + /** + * Get audio encoding/decoding AVCodecContext.sample_rate. + *

+ * AVOption int "ar". + * + * @return */ public native int getSampleRate(); - public native void setSampleRate(int val); + /** + * Set audio encoding/decoding AVCodecContext.sample_rate. + *

+ * AVOption int "ar". + * + * @param hertz + */ + public native void setSampleRate(int hertz); + /** + * Get audio encoding/decoding AVCodecContext.channels. + *

+ * AVOption int "ac". + * + * @return + */ public native int getNumChannels(); - public native void setNumChannels(int val); + /** + * Set audio encoding/decoding AVCodecContext.channels. + *

+ * AVOption int "ac". + * + * @param nchannels + */ + public native void setNumChannels(int nchannels); + /** + * Get audio encoding/decoding AVCodecContext.sample_fmt. + * + * @return + * @see AVSampleFormat + */ public native int getSampleFormat(); - public native void setSampleFormat(int val); + /** + * Set audio encoding/decoding AVCodecContext.sample_fmt. + * + * @param format + * @see AVSampleFormat + */ + public native void setSampleFormat(int format); + /** + * Get audio encoding/decoding AVCodecContext.frame_size. + *

+ * AVOption int "frame_size". + * + * @return + */ public native int getFrameSize(); - public native void setFrameSize(int val); - + /** + * Get audio encoding/decoding AVCodecContext.frame_number. + *

+ * AVOption int "frame_number". + * + * @return + */ public native int getFrameNumber(); -// int block_align -// int cutoff + /** + * Get audio encoding/decoding AVCodecContext.channel_layout. + *

+ * AVOption channel layout "channel_layout". + * + * @return + * @see AVChannelLayout + */ public native long getChannelLayout(); - public native void setChannelLayout(long val); + /** + * Set audio encoding/decoding AVCodecContext.channel_layout. + *

+ * AVOption channel layout "channel_layout". + * + * @param layout + * @see AVChannelLayout + */ + public native void setChannelLayout(long layout); + /** + * Get audio decoding AVCodecContext.request_channel_layout. + *

+ * AVOption channel layout "request_channel_layout". + * + * @return + * @see AVChannelLayout + */ public native long getRequestChannelLayout(); - public native void setRequestChannelLayout(long val); - -// enum AVAudioServiceType audio_service_type -// enum AVSampleFormat request_sample_fmt; - - /* - Encoding parameters + /** + * Set audio decoding AVCodecContext.request_channel_layout. + *

+ * AVOption channel layout "request_channel_layout". + * + * @param layout + * @see AVChannelLayout */ -// float qcompress -// float qblur -// float qmin -// float qmax -// float max_qdiff -// int rc_buffer_size -// int rc_override_count -// RCOverride *rc_override -// int64_t rc_max_rate -// int64_t rc_min_rate -// ... more rc stuff -// ... -// int trellis -// ... -// int workaround_bugs - public native int getStrictStdCompliance(); - - public native void setStrictStdCompliance(int val); - - public native int getErrorConcealment(); - - public native void setErrorConcealment(int val); -// int debug - - public native int getErrorRecognition(); - - public native void setErrorRecognition(int val); - - public native int getDCTAlgo(); - - public native void setDCTAlgo(int val); - - public native int getIDCTAlgo(); - - public native void setIDCTAlgo(int val); + public native void setRequestChannelLayout(long layout); + /** + * Get encoding/decoding AVCodecContext.thread_count. + *

+ * AVOption int "threads". + * + * @return + */ public native int getThreadCount(); - public native void setThreadCount(int val); -// ... - - public native int getProfile(); - - public native void setProfile(int val); -// ... + /** + * Set encoding/decoding AVCodecContext.thread_count. + *

+ * AVOption int "threads". + * + * @param nthreads + */ + public native void setThreadCount(int nthreads); } diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVCodecID.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVCodecID.java index 46b21b7..ab39bb8 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVCodecID.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVCodecID.java @@ -22,6 +22,10 @@ package au.notzed.jjmpeg; */ public class AVCodecID implements AVCodecIDBits { + static { + AVObject.init(); + } + /** * Call avcodec_get_name(value) */ diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVError.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVError.java index b374db7..7367b51 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVError.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVError.java @@ -22,6 +22,10 @@ package au.notzed.jjmpeg; * */ public class AVError implements AVErrorBits { - + + static { + AVObject.init(); + } + public static native String toString(int errno); } diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVFormatContext.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVFormatContext.java index 24eae74..f7f1d7e 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVFormatContext.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVFormatContext.java @@ -38,7 +38,6 @@ public class AVFormatContext extends AVOptions { // hmm? //public native void setOutputFormat(AVOutputFormat val); - public native AVIOContext getIOContext(); public native void setIOContext(AVIOContext val); @@ -132,7 +131,7 @@ public class AVFormatContext extends AVOptions { // returns false on end of file public native boolean readFrame(AVPacket pkt) throws AVIOException; - public native void seekFrame(int stream_index, long timestamp, int flags) throws AVIOException; + public native void seekFrame(int stream_index, long timestamp, int flags) throws AVIOException; public native void seekFile(int stream_index, long min_ts, long ts, long max_ts, int flags) throws AVIOException; @@ -153,6 +152,14 @@ public class AVFormatContext extends AVOptions { // returns true if flushed public native boolean writeFrame(AVPacket pktkt) throws AVIOException; + /** + * Write a frame to the output. + *

+ * Calls av_interleaved_write_frame(). + * + * @param pkt Packet containing data. + * @throws AVIOException on error. + */ public native void interleavedWriteFrame(AVPacket pkt) throws AVIOException; public native void writeUncodedFrame(int stream_index, AVFrame frame) throws AVIOException; diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVIOContext.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVIOContext.java index 3b0c499..ea1f893 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVIOContext.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVIOContext.java @@ -23,39 +23,25 @@ import java.nio.ByteBuffer; /** * */ -public class AVIOContext extends AVOptions { +public class AVIOContext extends AVOptions implements AVIOContextBits { + + /** + * Maintains references required for callbacks. + *

+ * This is a {@link au.notzed.nativez.ReferenceZ} object. + */ + Object opaque; - // TODO: autogen? - public static final int AVSEEK_SIZE = 0x10000; - public static final int AVSEEK_FORCE = 0x20000; // public static final int AVSEEK_SET = 0; public static final int AVSEEK_CUR = 1; public static final int AVSEEK_END = 2; - // - public static final int ALLOC_WRITE = 1; - public static final int ALLOC_STREAMED = 2; - // flags for open - public static final int AVIO_FLAG_READ = 1; - public static final int AVIO_FLAG_WRITE = 2; - public static final int AVIO_FLAG_READ_WRITE = 3; - // flags for alloc seekable - /** - * Seeking works like for a local file. - */ - public static final int AVIO_SEEKABLE_NORMAL = (1 << 0); - /** - * Seeking by timestamp with avio_seek_time() is possible. - */ - public static final int AVIO_SEEKABLE_TIME = (1 << 1); - /** - * pseudo-flag for alloc() which sets writable to avio_context_create. + * If set then alloc() will pass write_flag=1 to avio_alloc_context(). */ public static final int AVIO_WRITABLE = (1 << 31); /** - * pseudo-flaga for alloc() which sets the direct indicator on the - * created context. + * If set, then alloc() will set AVIOContext.direct=1. */ public static final int AVIO_DIRECT = (1 << 30); @@ -65,27 +51,58 @@ public class AVIOContext extends AVOptions { private static native void release(long p); - //?public native int getSeekable(); /** - * Calls avio_open. + * Create an AVIOContext backed by libavformat i/o. *

- * TODO: should it call avio_open2? + * Calls avio_open2(). * * @param url * @param avio_flags AVIO_FLAGS_*. + * @param interrupt_cb If non-null, supplies interrupt-check function. + * @param options + * @return + * @throws au.notzed.jjmpeg.AVIOException + */ + public static native AVIOContext open(String url, int avio_flags, AVIOInterrupt interrupt_cb, AVDictionary options) throws AVIOException; + + /** + * Create an AVIOContext backed by libavformat i/o with no callback or options. + * + * @param url + * @param avio_flags * @return + * @throws AVIOException */ - public static native AVIOContext open(String url, int avio_flags) throws AVIOException; + public static AVIOContext open(String url, int avio_flags) throws AVIOException { + return open(url, avio_flags, null, null); + } /** + * Create an AVIOContext backed by Java I/O. * * @param buffer_size * @param alloc_flags combination of AVIO_SEEKABLE_*, AVIO_WRITABLE, and AVIO_DIRECT. - * @param handler + * @param handler Functions which implement i/o. * @return */ public static native AVIOContext alloc(int buffer_size, int alloc_flags, AVIOHandler handler); + /** + * Callback hook for interrupt checking. + */ + public interface AVIOInterrupt { + + /** + * Called during blocking operations. + * + * @return true if the blocking i/o should abort. + */ + public boolean isInterrupted(); + } + + /** + * Callback hooks which allow Java I/O functions to provide I/O for FFmpeg. + */ public interface AVIOHandler { /** @@ -104,6 +121,13 @@ public class AVIOContext extends AVOptions { */ public int writePacket(ByteBuffer src); + /** + * Called to perform seek. + * + * @param offset + * @param whence AVSEEK_* value. + * @return + */ public long seek(long offset, int whence); } } diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVIOException.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVIOException.java index d31bb2e..e753419 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVIOException.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVIOException.java @@ -21,7 +21,9 @@ package au.notzed.jjmpeg; import java.io.IOException; /** - * + * General exception for FFmpeg errors. + *

+ * This contains the error code which triggered the exception. */ public class AVIOException extends IOException { diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVMediaType.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVMediaType.java index 9c88e45..0e32eba 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVMediaType.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVMediaType.java @@ -23,6 +23,10 @@ package au.notzed.jjmpeg; */ public class AVMediaType implements AVMediaTypeBits { + static { + AVObject.init(); + } + /** * Call av_get_media_type_string() */ diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVObject.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVObject.java index 63ff264..0bb6d50 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVObject.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVObject.java @@ -41,7 +41,17 @@ public abstract class AVObject extends NativeZ { AVUtil.setLogger(AVUtil.createAVLogger(Logger.getLogger("notzed.jjmpeg"))); } - + + /** + * Initialise AVObject. + *

+ * This function does nothing itself but ensures the native library is + * loaded and can be used by objects which DO NOT derive from AVObject + * but need to use native calls. + */ + public static void init() { + } + /** * Handy iterator for various uses in the library. */ diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVPacket.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVPacket.java index b218a38..50ff939 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVPacket.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVPacket.java @@ -45,7 +45,7 @@ public class AVPacket extends AVObject implements Cloneable { * Flag is used to discard packets which are required to maintain valid * decoder state but are not required for output and should be dropped * after decoding. - * + *

*/ public static final int AV_PKT_FLAG_DISCARD = 0x0004; /** @@ -74,14 +74,23 @@ public class AVPacket extends AVObject implements Cloneable { /** * Retrieve the current data buffer pointer and size. *

- * This value should not be saved between calls which might update the packet. - *

- * TODO: do i even need this? + * This value must not be saved between calls which might update the packet. * * @return */ public native ByteBuffer getData(); + /** + * Resize then retrieve the current data buffer pointer and size. + *

+ * This will call av_packet_grow() or av_packet_shrink() as necessary + * to set the data size, and will return a reference to it. + * + * @param size + * @return + */ + public native ByteBuffer getData(long size); + /** * Clear and free any data pointers and returns the packet to an initialised state. *

@@ -105,6 +114,10 @@ public class AVPacket extends AVObject implements Cloneable { public native void setDTS(long val); + public native long getDuration(); + + public native void setDuration(long val); + public native int getSize(); public native int getStreamIndex(); diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVPixelFormat.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVPixelFormat.java index 65166dc..8c578ce 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVPixelFormat.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVPixelFormat.java @@ -22,6 +22,10 @@ package au.notzed.jjmpeg; */ public class AVPixelFormat implements AVPixelFormatBits { + static { + AVObject.init(); + } + /** * Call av_get_pix_fmt(name) */ diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVRational.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVRational.java index 0dcac36..7dd1674 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVRational.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVRational.java @@ -35,6 +35,10 @@ public class AVRational extends Number implements Comparable { this.den = den; } + static { + AVObject.init(); + } + /** * Calls av_d2q * @@ -112,7 +116,7 @@ public class AVRational extends Number implements Comparable { * Compare two rationals. * * @param b Second rational - * + * * Taken from avutil/rational.c * * @return One of the following values: diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVSampleFormat.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVSampleFormat.java index c5bd2e8..5210857 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVSampleFormat.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVSampleFormat.java @@ -22,15 +22,17 @@ package au.notzed.jjmpeg; */ public class AVSampleFormat implements AVSampleFormatBits { + static { + AVObject.init(); + } + /** * Call av_get_sample_fmt(name) */ - public static native int valueOf(String name); - + /** * Call av_get_sample_fmt_name(value) */ public static native String toString(int value); } - diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVUtil.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVUtil.java index 5a42751..dce3c7d 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVUtil.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/AVUtil.java @@ -29,6 +29,10 @@ import java.util.logging.Logger; */ public class AVUtil { + static { + AVObject.init(); + } + /** * Call av_log_set_callback() with the provided logger. * diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/io/JJMediaReader.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/io/JJMediaReader.java index e891247..16aa7c5 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/io/JJMediaReader.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/io/JJMediaReader.java @@ -443,6 +443,15 @@ public class JJMediaReader implements AutoCloseable { return duration; } + /** + * Retrieve stream start time in milliseconds. + * + * @return + */ + public long getStartMS() { + return startms; + } + public boolean isOpened() { return opened; } diff --git a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/io/JJMediaWriter.java b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/io/JJMediaWriter.java index 66acaec..aff403c 100644 --- a/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/io/JJMediaWriter.java +++ b/src/notzed.jjmpeg/classes/au/notzed/jjmpeg/io/JJMediaWriter.java @@ -27,6 +27,7 @@ */ package au.notzed.jjmpeg.io; +import au.notzed.jjmpeg.AVChannelLayout; import au.notzed.jjmpeg.AVCodec; import au.notzed.jjmpeg.AVCodecContext; import au.notzed.jjmpeg.AVFormatContext; @@ -35,7 +36,6 @@ import au.notzed.jjmpeg.AVOutputFormat; import au.notzed.jjmpeg.AVPacket; import au.notzed.jjmpeg.AVRational; import au.notzed.jjmpeg.AVStream; -import au.notzed.jjmpeg.AVCodecID; import au.notzed.jjmpeg.AVError; import au.notzed.jjmpeg.AVIOContext; import au.notzed.jjmpeg.AVPixelFormat; @@ -47,16 +47,18 @@ import java.util.List; import java.util.logging.Logger; /** - * High level av file writer interface + * High level av file writer interface. *

- * @author notzed + * This is still work in progress and subject to change. */ -public class JJMediaWriter { +public class JJMediaWriter implements AutoCloseable { List streams = new ArrayList<>(); String filename; AVFormatContext oc; AVIOContext output; // need to keep ref around + // writeTrailer must only be called if writeHeader was successful. + boolean writeTrailer; /** * Create a new stream for writing to a file. @@ -66,16 +68,11 @@ public class JJMediaWriter { * and then call close(). * * @param filename - *

- * @throws AVInvalidFormatException */ public JJMediaWriter(String filename) throws AVIOException { this.filename = filename; - /* allocate the output media context */ oc = AVFormatContext.allocContext(null, null, filename); - - // Caller now calls addVideoStream, etc. } /** @@ -90,177 +87,170 @@ public class JJMediaWriter { /** * The stream must be opened after adding streams and before writing to them. *

- * @throws AVInvalidFormatException - * @throws AVInvalidCodecException * @throws AVIOException */ public void open() throws AVIOException { /* now that all the parameters are set, we can open the audio and video codecs and allocate the necessary encode buffers */ - for (JJWriterStream sd: streams) { + for (JJWriterStream sd : streams) { sd.open(); } /* open the output file, if needed */ //if (!(format->flags & AVFMT_NOFILE)) { - output = AVIOContext.open(filename, AVIOContext.AVIO_FLAG_WRITE); + output = AVIOContext.open(filename, AVIOContext.AVIO_FLAG_WRITE, null, null); oc.setIOContext(output); /* write the stream header, if any */ oc.writeHeader(null); - } - - /** - * Add a stream using the default video codec for this stream. - * The streamid is the index of the stream in the streams list. - *

- * @param width - * @param height - * @param frame_rate - * @param bit_rate - *

- * @return - */ - public JJWriterVideo addVideoStream(int width, int height, int frame_rate, long bit_rate) throws AVIOException { - return addVideoStream(getFormat().getVideoCodec(), streams.size(), width, height, 1, frame_rate, bit_rate); - } - - public JJWriterVideo addVideoStream(int width, int height, int frame_rate_num, int frame_rate_den, long bit_rate) throws AVIOException { - return addVideoStream(getFormat().getVideoCodec(), streams.size(), width, height, frame_rate_num, frame_rate_den, bit_rate); - } - - public JJWriterVideo addVideoStream(int codec_id, int streamid, int width, int height, int frame_rate_den, long bit_rate) throws AVIOException { - return addVideoStream(codec_id, streamid, width, height, 1, frame_rate_den, bit_rate); + writeTrailer = true; } /** * Add a video stream. *

- * @param codec_id - * @param width - * @param height - * @param frame_rate - * @param bit_rate - *

+ * Creates and adds a video stream. The video stream size, pixel format, and bitrate must still be initialised. + * + * @param codec If null then use the format default codec. If non-null must be a video codec. + * @param streamid StreamID to create, or -1 to use the next available slot. * @return - * @throws AVInvalidStreamException + * @throws AVIOException */ - public JJWriterVideo addVideoStream(int codec_id, int streamid, int width, int height, int frame_rate_num, int frame_rate_den, long bit_rate) throws AVIOException { + public JJWriterVideo addVideoStream(AVCodec codec, int streamid) throws AVIOException { AVCodecContext c; + JJWriterVideo vs; AVStream st; - // TODO: all this crap should probably go into JJWriterVideo constructor - AVCodec codec = AVCodec.findEncoder(codec_id); + if (codec == null) { + codec = AVCodec.findEncoder(getFormat().getVideoCodec()); + if (codec == null) + throw new AVIOException(AVError.AVERROR_STREAM_NOT_FOUND); + } else if (codec.getType() != AVMediaType.AVMEDIA_TYPE_VIDEO) + throw new AVIOException(AVError.AVERROR_STREAM_NOT_FOUND); c = AVCodecContext.allocContext(codec); - st = oc.newStream(codec); - if (st == null) { + if (st == null) throw new AVIOException(AVError.AVERROR_STREAM_NOT_FOUND); - } - st.setID(streams.size()); - c.setCodecID(codec_id); - c.setCodecType(AVMediaType.AVMEDIA_TYPE_VIDEO); - /* put sample parameters */ - c.setBitRate(bit_rate); + st.setID(streamid == -1 ? streams.size() : streamid); - /* resolution must be a multiple of two */ - c.setWidth(width); - c.setHeight(height); - /* time base: this is the fundamental unit of time (in seconds) in terms - of which frame timestamps are represented. for fixed-fps content, - timebase should be 1/framerate and timestamp increments should be - identically 1. */ - st.setTimeBase(new AVRational(frame_rate_num, frame_rate_den)); - c.setTimeBase(new AVRational(frame_rate_num, frame_rate_den)); - c.setGOPSize(12); - /* emit one intra frame every twelve frames at most */ - - c.setPixelFormat(AVPixelFormat.AV_PIX_FMT_YUV420P); - if (codec_id == AVCodecID.AV_CODEC_ID_MPEG2VIDEO) { - /* just for testing, we also add B frames */ - c.setMaxBFrames(2); - } - if (codec_id == AVCodecID.AV_CODEC_ID_MPEG1VIDEO) { - /* Needed to avoid using macroblocks in which some coeffs overflow. - This does not happen with normal video, it just happens here as - the motion of the chroma plane does not match the luma plane. */ - c.setMBDecision(2); - } - // FIXME: some formats want stream headers to be separate - if ((oc.getOutputFormat().getFlags() & AVOutputFormat.AVFMT_GLOBALHEADER) != 0) { - c.setFlags(AVCodecContext.AV_CODEC_FLAG_GLOBAL_HEADER, AVCodecContext.AV_CODEC_FLAG_GLOBAL_HEADER); - } + c.setCodecID(codec.getID()); + c.setCodecType(codec.getType()); - Logger.getLogger("jjmpeg.io") - .fine(() - -> String.format("add avideo stream %s %d [%dx%d %s aspect %s]\n", - codec.getName(), c.getBitRate(), - c.getWidth(), c.getHeight(), AVPixelFormat.toString(c.getPixelFormat()), - c.getAspectRatio())); + if ((oc.getOutputFormat().getFlags() & AVOutputFormat.AVFMT_GLOBALHEADER) != 0) + c.setFlags(AVCodecContext.AV_CODEC_FLAG_GLOBAL_HEADER, AVCodecContext.AV_CODEC_FLAG_GLOBAL_HEADER); - JJWriterVideo sd = new JJWriterVideo(st, c); - streams.add(sd); + vs = new JJWriterVideo(st, c); + streams.add(vs); - return sd; + return vs; } /** - * Add a new audio stream. + * Add an initialise a video stream. *

- * @param codec_id - * @param streamid - * @param fmt - * @param sample_rate - * @param bit_rate + * The supplied parameters are used to initialise the corresponding fields of the AVCodecContext. Other fields initialised + * are: + *

+ * + * @param codec If null then use the format default codec. If non-null must be a video codec. + * @param streamid StreamID to create, or -1 to use the next available slot. + * @param pixel_format AVPixelFormat. + * @param width Width of video stream. + * @param height Height of video stream. + * @param frame_rate Display frame rate. It's inverse is also set as the stream and codec timebase. + * @param bit_rate Bitrate of encoding. *

* @return - * @throws AVInvalidStreamException + * @throws au.notzed.jjmpeg.AVIOException on error */ - public JJWriterAudio addAudioStream(int codec_id, int streamid, int fmt, int sample_rate, int channels, long bit_rate) throws AVIOException { + public JJWriterVideo addVideoStream(AVCodec codec, int streamid, int pixel_format, int width, int height, AVRational frame_rate, long bit_rate) throws AVIOException { + JJWriterVideo sd = addVideoStream(codec, streamid); + + /* Initialise provided parameters */ + AVCodecContext c = sd.c; + AVStream st = sd.stream; + + c.setWidth(width); + c.setHeight(height); + + st.setAverageFrameRate(frame_rate); + sd.setTimeBase(new AVRational(frame_rate.den, frame_rate.num)); + + c.setBitRate(bit_rate); + c.setPixelFormat(pixel_format); + + return sd; + } + + public JJWriterAudio addAudioStream(AVCodec codec, int streamid) throws AVIOException { AVCodecContext c; + JJWriterAudio as; AVStream st; - AVCodec codec = AVCodec.findEncoder(codec_id); + if (codec == null) { + codec = AVCodec.findEncoder(getFormat().getAudioCodec()); + if (codec == null) + throw new AVIOException(AVError.AVERROR_STREAM_NOT_FOUND); + } else if (codec.getType() != AVMediaType.AVMEDIA_TYPE_AUDIO) + throw new AVIOException(AVError.AVERROR_STREAM_NOT_FOUND); c = AVCodecContext.allocContext(codec); - st = oc.newStream(codec); - if (st == null) { + if (st == null) throw new AVIOException(AVError.AVERROR_STREAM_NOT_FOUND); - } - - c.setCodecID(codec_id); - c.setCodecType(AVMediaType.AVMEDIA_TYPE_AUDIO); - c.setSampleFormat(fmt); - c.setBitRate(bit_rate); - c.setSampleRate(sample_rate); + st.setID(streamid == -1 ? streams.size() : streamid); - c.setNumChannels(channels); - c.setChannelLayout(4); + c.setCodecID(codec.getID()); + c.setCodecType(codec.getType()); - if ((oc.getOutputFormat().getFlags() & AVOutputFormat.AVFMT_GLOBALHEADER) != 0) { + if ((oc.getOutputFormat().getFlags() & AVOutputFormat.AVFMT_GLOBALHEADER) != 0) c.setFlags(AVCodecContext.AV_CODEC_FLAG_GLOBAL_HEADER, AVCodecContext.AV_CODEC_FLAG_GLOBAL_HEADER); - } - Logger.getLogger("jjmpeg.io") - .fine(() - -> String.format("add audio stream audio %s %d [x%d %dHz %s]\n", - codec.getName(), c.getBitRate(), - c.getNumChannels(), c.getSampleRate(), AVSampleFormat.toString(c.getSampleFormat()))); + as = new JJWriterAudio(st, c); + streams.add(as); - JJWriterAudio as = new JJWriterAudio(st, c); + return as; + } - streams.add(as); + /** + * Add an initialise an audio stream. + *

+ * @param codec If null then use the format default codec. If non-null must be a video codec. + * @param streamid StreamID to create, or -1 to use the next available slot. + * @param sample_format AVSampleFormat + * @param sample_rate Cycles per second. + * @param layout AVChannelLayout + * @param bit_rate Encoding bit rate. + *

+ * @return + * @throws au.notzed.jjmpeg.AVIOException + * @see AVSampleFormat + * @see AVChannelLayout + * @see AVChannelLayout#getDefaultLayout(int) + */ + public JJWriterAudio addAudioStream(AVCodec codec, int streamid, int sample_format, int sample_rate, long layout, long bit_rate) throws AVIOException { + JJWriterAudio as = addAudioStream(codec, streamid); + AVCodecContext c = as.c; + + c.setSampleFormat(sample_format); + c.setBitRate(bit_rate); + c.setSampleRate(sample_rate); + + c.setNumChannels(AVChannelLayout.getNumChannels(layout)); + c.setChannelLayout(layout); return as; } public void flush() throws AVIOException { - for (JJWriterStream sd: streams) { - sd.addFrame(null); + for (JJWriterStream sd : streams) { + sd.writeFrame(null); } } @@ -270,19 +260,17 @@ public class JJMediaWriter { public void close() throws AVIOException { flush(); - /* write the trailer, if any. the trailer must be written - * before you close the CodecContexts open when you wrote the - * header; otherwise write_trailer may try to use memory that - * was freed on av_codec_close() */ - oc.writeTrailer(); - - /* close each codec */ - for (JJWriterStream sd: streams) { - sd.close(); + if (writeTrailer) { + /* write the trailer, if any. the trailer must be written + * before you close the CodecContexts open when you wrote the + * header; otherwise write_trailer may try to use memory that + * was freed on av_codec_close() */ + oc.writeTrailer(); } - /* close the output file */ - //output.close(); + for (JJWriterStream sd : streams) { + sd.close(); + } /* free the stream */ oc.release(); @@ -322,6 +310,21 @@ public class JJMediaWriter { return c; } + public void setTimeBase(AVRational timeBase) { + stream.setTimeBase(timeBase); + c.setTimeBase(timeBase); + } + + public void setBitRate(long bit_rate) { + c.setBitRate(bit_rate); + } + + /** + * Open a write stream. + * This performs the step of set + * + * @throws AVIOException + */ public void open() throws AVIOException { AVCodec codec = AVCodec.findEncoder(c.getCodecID()); @@ -332,12 +335,38 @@ public class JJMediaWriter { opened = true; } - public void addFrame(AVFrame frame) throws AVIOException { + /** + * Write encoded packet. + * If the data is already in the correct format then use this to + * add a packet to the stream. + *

+ * This sets the packet streamIndex and rescales the timestamp and calls + * {@link AVFormatContext#interleavedWriteFrame(au.notzed.jjmpeg.AVPacket)} + * + * @param packet + * @throws AVIOException + */ + public void writePacket(AVPacket packet) throws AVIOException { + packet.rescaleTS(c.getTimeBase(), stream.getTimeBase()); + packet.setStreamIndex(stream.getIndex()); + oc.interleavedWriteFrame(packet); + } + + /** + * Write unencoded frame. + *

+ * The frame is passed through the + * {@link AVCodecContext#sendFrame(au.notzed.jjmpeg.AVFrame) / + * {@link AVCodecContext#receivePacket(au.notzed.jjmpeg.AVPacket) + * sequence and then written to the output. + * + * @param frame + * @throws AVIOException on error. + */ + public void writeFrame(AVFrame frame) throws AVIOException { if (c.sendFrame(frame)) { while (c.receivePacket(packet)) { - packet.rescaleTS(c.getTimeBase(), stream.getTimeBase()); - packet.setStreamIndex(stream.getIndex()); - oc.interleavedWriteFrame(packet); + writePacket(packet); } } } @@ -364,6 +393,30 @@ public class JJMediaWriter { super.open(); frame = AVFrame.alloc(c.getPixelFormat(), c.getWidth(), c.getHeight()); + + Logger.getLogger("jjmpeg.io") + .fine(() + -> String.format("open video stream %s %d [%dx%d %s aspect %s]\n", + AVCodec.findEncoder(c.getCodecID()).getName(), + c.getBitRate(), + c.getWidth(), c.getHeight(), AVPixelFormat.toString(c.getPixelFormat()), + c.getAspectRatio())); + } + + public void setWidth(int width) { + c.setWidth(width); + } + + public void setHeight(int height) { + c.setHeight(height); + } + + public void setPixelFormat(int format) { + c.setPixelFormat(format); + } + + public void setAverateFrameRate(AVRational frameRate) { + stream.setAverageFrameRate(frameRate); } @Override @@ -382,13 +435,13 @@ public class JJMediaWriter { *

* @param frame Video frame to write, or null to flush buffers. *

- * @throws AVEncodingError - * @throws AVIOException + * @throws AVIOException on error. */ - public void addFrame(AVFrame frame) throws AVIOException { - if (frame != null) - frame.setPTS(nextPTS++); - super.addFrame(frame); + @Override + public void writeFrame(AVFrame frame) throws AVIOException { + //if (frame != null) + // frame.setPTS(nextPTS++); + super.writeFrame(frame); } } @@ -399,6 +452,22 @@ public class JJMediaWriter { super(stream, c); } + public void setSampleFormat(int sample_format) { + c.setSampleFormat(sample_format); + } + + public void setSampleRate(int sample_rate) { + c.setSampleRate(sample_rate); + } + + public void setNumChannels(int channels) { + c.setNumChannels(channels); + } + + public void setChannelLayout(long layout) { + c.setChannelLayout(layout); + } + @Override public void open() throws AVIOException { super.open(); @@ -409,6 +478,13 @@ public class JJMediaWriter { // ??? unchecked frame = AVFrame.alloc(c.getSampleFormat(), c.getNumChannels(), 2048); + + Logger.getLogger("jjmpeg.io") + .fine(() + -> String.format("open audio stream %s %d [x%d %dHz %s]\n", + AVCodec.findEncoder(c.getCodecID()).getName(), + c.getBitRate(), + c.getNumChannels(), c.getSampleRate(), AVSampleFormat.toString(c.getSampleFormat()))); } @Override diff --git a/src/notzed.jjmpeg/gen/extract-defines.pl b/src/notzed.jjmpeg/gen/extract-defines.pl index cd82b46..cddf729 100644 --- a/src/notzed.jjmpeg/gen/extract-defines.pl +++ b/src/notzed.jjmpeg/gen/extract-defines.pl @@ -175,11 +175,13 @@ sub scan { my $def = $1; #print $1."\n"; if ($lastc ne "") { - #$lastc =~ s@\t+@\t@g; + $lastc =~ s@\"@\\\"@g; print C "\tprintf(\"%s\", \"$lastc\");\n"; } elsif (m@///< (.*)@ || m@/\*\|<(.*)\*@) { + my $com = $1; + $com =~ s@\"@\\\"@g; # handle single-line comments, perhaps these should override - print C "\tprintf(\"\\t/**\\n\\t * $1\\n\\t */\\n\");\n"; + print C "\tprintf(\"\\t/**\\n\\t * $com\\n\\t */\\n\");\n"; } print C "\tprintf(\"\\tpublic final static $o{type} $def = $o{cfmt};\\n\", $o{ctype}$def$o{shift});\n"; diff --git a/src/notzed.jjmpeg/gen/gen.make b/src/notzed.jjmpeg/gen/gen.make index 9bada1d..6281db8 100644 --- a/src/notzed.jjmpeg/gen/gen.make +++ b/src/notzed.jjmpeg/gen/gen.make @@ -28,6 +28,7 @@ notzed.jjmpeg_JAVA_GENERATED = \ au/notzed/jjmpeg/AVCodecIDBits.java \ au/notzed/jjmpeg/AVDiscardBits.java \ au/notzed/jjmpeg/AVErrorBits.java \ + au/notzed/jjmpeg/AVIOContextBits.java \ au/notzed/jjmpeg/AVMediaTypeBits.java \ au/notzed/jjmpeg/AVOptionsBits.java \ au/notzed/jjmpeg/AVPixelFormatBits.java \ @@ -87,6 +88,10 @@ $(tmp)/AVCodecBits-gen.c: $(extract_defines) $(dep) @install -d $(@D) perl $(extract_defines) -c -interface AVCodecBits -f $(FFMPEG_HOME) -header libavcodec/avcodec.h -d AV_CODEC_CAP_ $@ +$(tmp)/AVIOContextBits-gen.c: $(extract_defines) $(dep) + @install -d $(@D) + perl $(extract_defines) -c -interface AVIOContextBits -f $(FFMPEG_HOME) -header libavformat/avio.h -d AVSEEK_ -d AVIO_FLAG_ -d AVIO_SEEKABLE_ $@ + $(tmp)/AVOptionsBits-gen.c: $(extract_defines) $(dep) @install -d $(@D) perl $(extract_defines) -c -interface AVOptionsBits -f $(FFMPEG_HOME) -header libavutil/opt.h -d AV_OPT_ $@ diff --git a/src/notzed.jjmpeg/jni/jj-avcodeccontext.c b/src/notzed.jjmpeg/jni/jj-avcodeccontext.c index 78bda20..bd8eb93 100644 --- a/src/notzed.jjmpeg/jni/jj-avcodeccontext.c +++ b/src/notzed.jjmpeg/jni/jj-avcodeccontext.c @@ -35,13 +35,8 @@ jint AVCodecContext_OnLoad(JavaVM *vmi, JNIEnv *env) { GS_int(AVCodecContext, CodecType, codec_type) GS_int(AVCodecContext, CodecID, codec_id) -GS_int(AVCodecContext, CodecTag, codec_tag) GS_long(AVCodecContext, BitRate, bit_rate) -GS_int(AVCodecContext, BitRateTolerance, bit_rate_tolerance) GS_int(AVCodecContext, GlobalQuality, global_quality) -GS_int(AVCodecContext, CompressionLevel, compression_level) -//GS_int(AVCodecContext, Flags1, flags) -//GS_int(AVCodecContext, Flags2, flags2) JNIEXPORT jlong JNICALL MAKE_JJNAME(AVCodecContext, getFlags) (JNIEnv *env , jobject jo) { @@ -77,7 +72,6 @@ JNIEXPORT void JNICALL MAKE_JJNAME(AVCodecContext, setTimeBase) o->time_base = AVRational_get(env, jtb); } -GS_int(AVCodecContext, TicksPerFrame, ticks_per_frame) GET_prim(jint, AVCodecContext, Delay, delay) /* Video options */ @@ -88,35 +82,17 @@ GS_int(AVCodecContext, GOPSize, gop_size) GS_int(AVCodecContext, PixelFormat, pix_fmt) -GS_int(AVCodecContext, MaxBFrames, max_b_frames) -GS_float(AVCodecContext, BQuantFactor, b_quant_factor) -GS_float(AVCodecContext, BQuantOffset, b_quant_offset) - -GET_bool(AVCodecContext, hasBFrames, has_b_frames) - -GS_float(AVCodecContext, IQuantFactor, i_quant_factor) -GS_float(AVCodecContext, IQuantOffset, i_quant_offset) - -GS_int(AVCodecContext, MBDecision, mb_decision) - /* Audio options */ GS_int(AVCodecContext, SampleRate, sample_rate) GS_int(AVCodecContext, NumChannels, channels) GS_int(AVCodecContext, SampleFormat, sample_fmt) -GS_int(AVCodecContext, FrameSize, frame_size) +GET_prim(jint, AVCodecContext, FrameSize, frame_size) GET_prim(jint, AVCodecContext, FrameNumber, frame_number) GS_long(AVCodecContext, ChannelLayout, channel_layout) GS_long(AVCodecContext, RequestChannelLayout, request_channel_layout) -GS_int(AVCodecContext, StrictStdCompliance, strict_std_compliance) - -GS_int(AVCodecContext, ErrorConcealment, error_concealment) -GS_int(AVCodecContext, ErrorRecognition, err_recognition) // !! - -GS_int(AVCodecContext, DCTAlgo, dct_algo) -GS_int(AVCodecContext, IDCTAlgo, idct_algo) +/* other options */ GS_int(AVCodecContext, ThreadCount, thread_count) -GS_int(AVCodecContext, Profile, profile) /* ********************************************************************** */ /* Methods */ diff --git a/src/notzed.jjmpeg/jni/jj-aviocontext.c b/src/notzed.jjmpeg/jni/jj-aviocontext.c index 2b27ac2..51725b4 100644 --- a/src/notzed.jjmpeg/jni/jj-aviocontext.c +++ b/src/notzed.jjmpeg/jni/jj-aviocontext.c @@ -29,27 +29,28 @@ jint AVIOContext_OnLoad(JavaVM *vmi, JNIEnv *env) { /* ********************************************************************** */ +/* + * All the callbacks use this structure to find the java instance + * which implement it. + * + * AVIOContext.open() has a further complication in that there is + * nowhere to store a pointer to this. To get around this if an + * interrupt callback is supplied it is referenced in an instance of a + * nativez.ReferenceZ object and set on the java AVIOContext.opaque + * field. + */ struct jj_io { - // AVIOHandler global reference + // AVIOHandler or AVIOInterrupt depending on how it was created jobject jhandler; - // direct ByteBuffer for this data - // I don't think this will work looking at avio*.c - //jobject jbuffer; - //uint8_t *buffer; }; static int jj_read_packet(void *opaque, uint8_t *buf, int buf_size) { - JNIEnv *env = nativez_AttachCurrentThread(); + JNIEnv *env = nativez_AttachCurrentThreadAsDaemon(); if (env) { struct jj_io *jjio = opaque; - //size_t position = (buf - jjio->buffer); jvalue args[1]; - //jjbufferSetLimit(env, jjio->jbuffer, (int)(position + buf_size)); - //jjbufferSetPosition(env, jjio->jbuffer, (int)(position)); - //args[0].l = jjio->jbuffer; - args[0].l = nativez_NewDirectBuffer(env, buf, buf_size); return (*env)->CallIntMethodA(env, jjio->jhandler, AVIOHandler_readPacket_l, args); @@ -59,17 +60,12 @@ static int jj_read_packet(void *opaque, uint8_t *buf, int buf_size) { } static int jj_write_packet(void *opaque, uint8_t *buf, int buf_size) { - JNIEnv *env = nativez_AttachCurrentThread(); + JNIEnv *env = nativez_AttachCurrentThreadAsDaemon(); if (env) { struct jj_io *jjio = opaque; - //size_t position = (buf - jjio->buffer); jvalue args[1]; - //jjbufferSetLimit(env, jjio->jbuffer, (int)(position + buf_size)); - //jjbufferSetPosition(env, jjio->jbuffer, (int)(position)); - //args[0].l = jjio->jbuffer; - args[0].l = nativez_NewDirectBuffer(env, buf, buf_size); return (*env)->CallIntMethodA(env, jjio->jhandler, AVIOHandler_writePacket_l, args); @@ -79,7 +75,7 @@ static int jj_write_packet(void *opaque, uint8_t *buf, int buf_size) { } static int64_t jj_seek(void *opaque, int64_t offset, int whence) { - JNIEnv *env = nativez_AttachCurrentThread(); + JNIEnv *env = nativez_AttachCurrentThreadAsDaemon(); if (env) { struct jj_io *jjio = opaque; @@ -94,6 +90,18 @@ static int64_t jj_seek(void *opaque, int64_t offset, int whence) { } } +static int jj_interrupt(void *opaque) { + JNIEnv *env = nativez_AttachCurrentThreadAsDaemon(); + + if (env) { + struct jj_io *jjio = opaque; + + return (*env)->CallBooleanMethodA(env, jjio->jhandler, AVIOInterrupt_isInterrupted_, NULL); + } else { + return 1; + } +} + JNIEXPORT void JNICALL Java_au_notzed_jjmpeg_AVIOContext_release (JNIEnv *env, jclass jc, jlong jic) { AVIOContext *ic = (void *)(uintptr_t)jic; @@ -102,29 +110,63 @@ JNIEXPORT void JNICALL Java_au_notzed_jjmpeg_AVIOContext_release struct jj_io *jjio = ic->opaque; (*env)->DeleteGlobalRef(env, jjio->jhandler); - //(*env)->DeleteGlobalRef(env, jjio->jbuffer); - //DLCALL(av_free)(jjio->buffer); - free(jjio); - DLCALL(av_free)(ic->buffer); DLCALL(avio_context_free)(&ic); + free(jjio); } else { DLCALL(avio_close)(ic); } } JNIEXPORT jobject JNICALL Java_au_notzed_jjmpeg_AVIOContext_open -(JNIEnv *env, jclass jc, jstring jurl, jint flags) { - const char *url = nativez_GetString(env, jurl); - AVIOContext *ic = NULL; +(JNIEnv *env, jclass jc, jstring jurl, jint flags, jobject jcb, jobject joptions) { + AVIOContext *io = NULL; int res; + struct jj_io *jjio = NULL; + AVIOInterruptCB cb = { 0, 0 }; + jobject jio = NULL; + jobject jrefs = NULL; + + jjio = malloc(sizeof(*jjio)); + if (!jjio) { + nativez_ThrowOutOfMemoryError(env, "Allocating handler"); + return NULL; + } + + if (jcb) { + // FIXME: check returns + jrefs = ReferenceZ_create(env, &jcb, 1); + jjio->jhandler = (*env)->NewGlobalRef(env, jcb); + cb.callback = jj_interrupt; + cb.opaque = jjio; + } - res = DLCALL(avio_open)(&ic, url, flags); - if (res < 0) + AVDictionary *options = AVDictionary_get(env, joptions); + const char *url = nativez_GetString(env, jurl); + + res = DLCALL(avio_open2)(&io, url, flags, &cb, &options); + AVDictionary_set(env, joptions, options, 1); + + if (res < 0) { jjthrowAVIOException(env, res, url); + goto fail; + } + + jio = NativeZ_create(env, jc, io); + if (!jio) + goto fail; + + + (*env)->SetObjectField(env, jio, AVIOContext_opaque, jrefs); + + return jio; + fail: nativez_ReleaseString(env, jurl, url); + if (jjio->jhandler) + (*env)->DeleteGlobalRef(env, jjio->jhandler); + free(jjio); - return NativeZ_create(env, jc, ic); + return NULL; } JNIEXPORT jobject JNICALL Java_au_notzed_jjmpeg_AVIOContext_alloc @@ -132,35 +174,40 @@ JNIEXPORT jobject JNICALL Java_au_notzed_jjmpeg_AVIOContext_alloc AVIOContext *io = NULL; struct jj_io *jjio; uint8_t *buffer; + jobject jio = NULL; jjio = malloc(sizeof(*jjio)); - if (jjio) { - buffer = DLCALL(av_malloc)(buffer_size); - if (buffer) { - int write_flag = (jflags & au_notzed_jjmpeg_AVIOContext_AVIO_WRITABLE) != 0; + if (!jjio) + goto fail_nojjio; - jjio->jhandler = (*env)->NewGlobalRef(env, jhandler); - //jjio->jbuffer = jjnewDirectBuffer(env, jjio->buffer, buffer_size); + buffer = DLCALL(av_malloc)(buffer_size); + if (!buffer) + goto fail_nobuffer; + + jjio->jhandler = (*env)->NewGlobalRef(env, jhandler); - io = DLCALL(avio_alloc_context)(buffer, buffer_size, write_flag, jjio, - jj_read_packet, - jj_write_packet, - jj_seek); - if (io) { - io->seekable = jflags & 3; - io->direct = (jflags & au_notzed_jjmpeg_AVIOContext_AVIO_DIRECT) != 0; - } else { - nativez_ThrowOutOfMemoryError(env, "Allocating io context"); - free(buffer); - free(jjio); - } - } else { - nativez_ThrowOutOfMemoryError(env, "Allocating buffer"); - free(jjio); - } - } else { - nativez_ThrowOutOfMemoryError(env, "Allocating handler"); - } - - return NativeZ_create(env, jc, io); + io = DLCALL(avio_alloc_context)(buffer, buffer_size, + (jflags & au_notzed_jjmpeg_AVIOContext_AVIO_WRITABLE) != 0, + jjio, + jj_read_packet, + jj_write_packet, + jj_seek); + if (!io) + goto fail; + + io->seekable = jflags & (AVIO_SEEKABLE_NORMAL | AVIO_SEEKABLE_TIME); + io->direct = (jflags & au_notzed_jjmpeg_AVIOContext_AVIO_DIRECT) != 0; + jio = NativeZ_create(env, jc, io); + if (!jio) + goto fail; + + return jio; + fail: + (*env)->DeleteGlobalRef(env, jjio->jhandler); + free(buffer); + fail_nobuffer: + free(jjio); + fail_nojjio: + nativez_ThrowOutOfMemoryError(env, "Allocating io context"); + return NULL; } diff --git a/src/notzed.jjmpeg/jni/jj-aviocontext.def b/src/notzed.jjmpeg/jni/jj-aviocontext.def index f1a2fe7..fec52b1 100644 --- a/src/notzed.jjmpeg/jni/jj-aviocontext.def +++ b/src/notzed.jjmpeg/jni/jj-aviocontext.def @@ -3,7 +3,7 @@ header avformat libavformat/avio.h { avio_alloc_context avio_context_free - avio_open + avio_open2 avio_close } @@ -12,6 +12,14 @@ header avutil libavutil/mem.h { av_free } +java AVIOContext au/notzed/jjmpeg/AVIOContext { + opaque, Ljava/lang/Object; +} + +java AVIOInterrupt au/notzed/jjmpeg/AVIOContext$AVIOInterrupt { + isInterrupted, ()Z +} + java AVIOHandler au/notzed/jjmpeg/AVIOContext$AVIOHandler { readPacket, (Ljava/nio/ByteBuffer;)I writePacket, (Ljava/nio/ByteBuffer;)I diff --git a/src/notzed.jjmpeg/jni/jj-avpacket.c b/src/notzed.jjmpeg/jni/jj-avpacket.c index bd57fa4..fe2f3e1 100644 --- a/src/notzed.jjmpeg/jni/jj-avpacket.c +++ b/src/notzed.jjmpeg/jni/jj-avpacket.c @@ -34,6 +34,7 @@ jint AVPacket_OnLoad(JavaVM *vmi, JNIEnv *env) { GS_long(AVPacket, PTS, pts) GS_long(AVPacket, DTS, dts) +GS_long(AVPacket, Duration, duration) GET_prim(jint, AVPacket, Size, size) GS_int(AVPacket, StreamIndex, stream_index) GET_prim(jint, AVPacket, Flags, flags) @@ -61,14 +62,16 @@ JNIEXPORT jobject JNICALL Java_au_notzed_jjmpeg_AVPacket_alloc (JNIEnv *env, jclass jc) { AVPacket *packet = DLCALL(av_packet_alloc)(); - //if (!packet) // outofmemoryerror? - - return NativeZ_create(env, jc, packet); + if (nativez_NonNull(env, "Allocating AVPacket", packet)) { + DLCALL(av_init_packet)(packet); + return NativeZ_create(env, jc, packet); + } + return NULL; } /* **************************************** */ -JNIEXPORT jobject JNICALL Java_au_notzed_jjmpeg_AVPacket_getData +JNIEXPORT jobject JNICALL Java_au_notzed_jjmpeg_AVPacket_getData__ (JNIEnv *env, jobject jpacket) { AVPacket *packet = NativeZ_getP(env, jpacket); @@ -78,6 +81,25 @@ JNIEXPORT jobject JNICALL Java_au_notzed_jjmpeg_AVPacket_getData return NULL; } +JNIEXPORT jobject JNICALL Java_au_notzed_jjmpeg_AVPacket_getData__J +(JNIEnv *env, jobject jpacket, jlong size) { + AVPacket *packet = NativeZ_getP(env, jpacket); + + if (packet->size < size) { + if (av_grow_packet(packet, size - packet->size) != 0) { + nativez_ThrowOutOfMemoryError(env, "Unable to grow packet"); + return NULL; + } + } else { + av_shrink_packet(packet, size); + } + + if (nativez_NonNull(env, "No data pointer", packet->data)) + return nativez_NewDirectBuffer(env, packet->data, packet->size); + + return NULL; +} + JNIEXPORT void JNICALL Java_au_notzed_jjmpeg_AVPacket_clear (JNIEnv *env, jobject jpacket) { AVPacket *packet = NativeZ_getP(env, jpacket); @@ -90,8 +112,9 @@ JNIEXPORT jobject JNICALL Java_au_notzed_jjmpeg_AVPacket_clone AVPacket *packet = NativeZ_getP(env, jpacket); AVPacket *dst = DLCALL(av_packet_clone)(packet); - // if !dst ... - return NativeZ_create(env, AVPacket_classid, dst); + if (nativez_NonNull(env, "Cloning AVPacket", dst)) + return NativeZ_create(env, AVPacket_classid, dst); + return NULL; } JNIEXPORT void JNICALL Java_au_notzed_jjmpeg_AVPacket_rescaleTS diff --git a/src/notzed.jjmpeg/jni/jj-avpacket.def b/src/notzed.jjmpeg/jni/jj-avpacket.def index 3352b5a..92c42ae 100644 --- a/src/notzed.jjmpeg/jni/jj-avpacket.def +++ b/src/notzed.jjmpeg/jni/jj-avpacket.def @@ -4,6 +4,8 @@ header avcodec libavcodec/avcodec.h { av_packet_unref av_packet_clone av_packet_rescale_ts + + av_init_packet } java AVPacket au/notzed/jjmpeg/AVPacket { diff --git a/src/notzed.jjmpeg/jni/jj-avutil.c b/src/notzed.jjmpeg/jni/jj-avutil.c index a8d4bb7..8de4a15 100644 --- a/src/notzed.jjmpeg/jni/jj-avutil.c +++ b/src/notzed.jjmpeg/jni/jj-avutil.c @@ -30,7 +30,7 @@ jint AVUtil_OnLoad(JavaVM *vmi, JNIEnv *env) { static jobject jlogger_ref; static void log_callback(void*ptr, int level, const char*fmt, va_list vl) { - JNIEnv *env = nativez_AttachCurrentThread(); + JNIEnv *env = nativez_AttachCurrentThreadAsDaemon(); if (env) { char log[1024]; diff --git a/src/notzed.jjmpeg/jni/jjmpeg-jni.c b/src/notzed.jjmpeg/jni/jjmpeg-jni.c index a59381c..3c9a433 100644 --- a/src/notzed.jjmpeg/jni/jjmpeg-jni.c +++ b/src/notzed.jjmpeg/jni/jjmpeg-jni.c @@ -83,7 +83,7 @@ void jjthrowAVIOException(JNIEnv *env, int error, const char *msg) { /* ********************************************************************** */ -jstring jjnewIntArrayT(JNIEnv *env, const int *ap, int terminal) { +jintArray jjnewIntArrayT(JNIEnv *env, const int *ap, int terminal) { jintArray ret = NULL; if (ap) { @@ -94,6 +94,7 @@ jstring jjnewIntArrayT(JNIEnv *env, const int *ap, int terminal) { ret = (*env)->NewIntArray(env, len); (*env)->SetIntArrayRegion(env, ret, 0, len, (const jint *)ap); } + return ret; } diff --git a/src/notzed.jjmpeg/jni/jjmpeg.h b/src/notzed.jjmpeg/jni/jjmpeg.h index d29f641..be1aa4c 100644 --- a/src/notzed.jjmpeg/jni/jjmpeg.h +++ b/src/notzed.jjmpeg/jni/jjmpeg.h @@ -161,7 +161,7 @@ * @param ap Array pointer, may be NULL. * @param terminal Terminating value. */ -jstring jjnewIntArrayT(JNIEnv *env, const int *ap, int terminal); +jintArray jjnewIntArrayT(JNIEnv *env, const int *ap, int terminal); void jjthrowAVIOException(JNIEnv *env, int error, const char *msg);