/* * Copyright (c) 2002 Francisco Javier Cabello * Copyright (c) 2004 Guilhem Tardy (www.salyens.com) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ * * $Id: NativeDecoder.java,v 1.1 2004/11/01 01:52:15 davidstuart Exp $ * * Description * ============ * This is a Decoder which takes H263 frames, or H263_RTP(rfc2190) packets * and generates either YUV420P or RGB output. This will eventually * also support other input format such as MPEG. *++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ /* * Revision 1.2 2004/02/19 Guilhem Tardy (gravsten@yahoo.com) * Major rewrite, now fully supports H.263/RTP (ffmpeg 0.4.7) and conversion to RGB24. */ package net.sourceforge.jffmpeg.ffmpegnative; import javax.media.Buffer; import javax.media.Codec; import javax.media.Format; import javax.media.ResourceUnavailableException; import javax.media.format.VideoFormat; import javax.media.format.RGBFormat; import javax.media.format.YUVFormat; import java.awt.Dimension; import com.sun.media.BasicCodec; import net.sourceforge.jffmpeg.JMFCodec; import net.sourceforge.jffmpeg.CodecManager; /** * A Codec to convert YUVFormat buffer to RGBFormat buffer. */ public class NativeDecoder implements JMFCodec { private boolean opened; private Format[] inputFormats; private Format[] outputFormats; private Format inputFormat; private Format outputFormat; private final static String PLUGIN_NAME = "FFMPEG Decoder"; private final static int DEF_WIDTH = 352; private final static int DEF_HEIGHT = 288; private final static int INPUT_BUFFER_PADDING_SIZE = 8; private int inputH263Length; private int outputLength; private final static int MAX_PAYLOAD_SIZE = 2048; private String encoding; /** * Video size */ private Dimension videoSize; // true if some parameter was changed and a reset is required, // false otherwise. protected boolean resetRequired = false; // If RTP is enabled, some portions of the decoding need to be // treated differently. Set this variable to true if we detect // an RTP protocol has been selected. private boolean rtpActive = false; /** * Do we need to provide the "Set Truncated" flag to FFMPEG? */ private boolean truncatedFlag = false; // Sequence number used for RTP. Gaps between the sequence numbers // of incoming buffer indicate a packet loss or reordering. private long seqNum = 0; // Frame number private long frameNum = 0; // Timestamp of the last incoming buffer, used in case of packet loss or reordering. private long timestamp = 0; private float frameRate = 0; // Variables used for YUV420P to RGB24 conversion private boolean yuv2rgb = false; private int depth; private int rMask, gMask, bMask; //-------------------------------------------------------------------------------- // NATIVE METHODS //-------------------------------------------------------------------------------- private native boolean open_decoder(String codec, boolean rtp, boolean setTruncted, boolean yuv2rgb, int depth, int rMask, int gMask, int bMask, int width, int height); private native boolean close_decoder(int peer); // returns the number of bytes consumed, or negative if error private native int convert(int peer, Object inData, int inBufSize, int inOffset, int inLength, Object outData, int outLength, int eof); private native float extractFrameRate( int peer ); //-------------------------------------------------------------------------------- // NATIVE VARIABLES //-------------------------------------------------------------------------------- // Native structure pointer public int peer = 0; /* Set to true if/when the native library is loaded */ private static boolean nativeLibraryLoaded = false; static { CodecManager codecManager = new CodecManager(); String libraryName = codecManager.getNativeLibraryName(); if ( libraryName == null ) libraryName = "jffmpeg"; try { System.loadLibrary( libraryName ); nativeLibraryLoaded = true; } catch (UnsatisfiedLinkError e) { nativeLibraryLoaded = false; } } /** * Returns false if the native library failed to load */ public boolean isCodecAvailable() { return nativeLibraryLoaded; } // Constructor public NativeDecoder() { // Specify the input formats. In this list we can be "general". The // framework will narrow down the options by itself and by use of the // other methods. inputFormats = new Format [] { new VideoFormat(VideoFormat.H263), new VideoFormat(VideoFormat.H263_RTP), new VideoFormat(VideoFormat.MPEG), new VideoFormat("mpeg video"), new VideoFormat("DIV3"), new VideoFormat("DIVX"), new VideoFormat("MP42"), new VideoFormat("MPG4"), new VideoFormat("WMV1"), new VideoFormat("WMV2"), new VideoFormat("MJPG"), }; // Specify the list of generic output formats. This list will also be // "general" in nature. outputFormats = new Format[] { new YUVFormat(YUVFormat.YUV_420), new RGBFormat() }; // Initialize parent members inputFormat = null; outputFormat = null; } public void setYuv2rgb( boolean yuv2rgb ) { this.yuv2rgb = yuv2rgb; } public void setEncoding( String encoding ) { this.encoding = encoding; } public void setVideoSize( Dimension videoSize ) { this.videoSize = videoSize; } public void setIsRtp( boolean isRtp ) { this.rtpActive = isRtp; } public void setIsTruncated( boolean truncatedFlag ) { this.truncatedFlag = truncatedFlag; } /** * JMF calls this method when it has decided on an output format. This * format will be the result of the call to getSupportedOutputFormats(). */ public Format setOutputFormat(Format out) { VideoFormat videoOut = (VideoFormat)out; Dimension outSize = videoOut.getSize(); // System.out.println("Decoder:: setOutputFormat(" + out.getEncoding() + ")"); // System.out.println("\tfrom: " + videoOut.toString() + " size=" + outSize); if (outSize == null) { Dimension inSize = videoSize; if (inSize == null) outSize = new Dimension(DEF_WIDTH, DEF_HEIGHT); else outSize = inSize; } if (out instanceof YUVFormat) { yuv2rgb = false; YUVFormat yuv = (YUVFormat) out; if (yuv.getYuvType() != YUVFormat.YUV_420) return null; if (yuv.getOffsetU() > yuv.getOffsetV()) return null; // TODO : Make other safety checks int strideY = outSize.width; int strideUV = strideY / 2; int offsetU = strideY * outSize.height; int offsetV = offsetU + strideUV * outSize.height / 2; outputLength = (outSize.width * outSize.height * 3) / 2; outputFormat = new YUVFormat(outSize, outputLength, Format.byteArray, videoOut.getFrameRate(), YUVFormat.YUV_420, strideY, strideUV, 0, offsetU, offsetV); } else if (out instanceof RGBFormat) { yuv2rgb = true; RGBFormat rgb = (RGBFormat) out; int bitsPerPixel = rgb.getBitsPerPixel(); Class dataType = rgb.getDataType(); int pixelStride = 1; switch (bitsPerPixel) { case 15: case 16: if (dataType != Format.byteArray && dataType != Format.shortArray) return null; if (dataType == Format.byteArray) pixelStride = 2; break; case 24: if (dataType != Format.byteArray) return null; pixelStride = 3; break; case 32: if (dataType != Format.byteArray && dataType != Format.intArray) return null; if (dataType == Format.byteArray) pixelStride = 4; break; default: return null; } // TODO : Make other safety checks if (dataType == Format.byteArray) { rMask = 0x000000ff << ((rgb.getRedMask() - 1) * 8); gMask = 0x000000ff << ((rgb.getGreenMask() - 1) * 8); bMask = 0x000000ff << ((rgb.getBlueMask() - 1) * 8); } else { rMask = rgb.getRedMask(); gMask = rgb.getGreenMask(); bMask = rgb.getBlueMask(); } depth = bitsPerPixel; int bytesPerPixel = (bitsPerPixel + 7) / 8; outputLength = outSize.width * outSize.height * bytesPerPixel; outputFormat = new RGBFormat(outSize, outputLength, dataType, videoOut.getFrameRate(), bitsPerPixel, rgb.getRedMask(), rgb.getGreenMask(), rgb.getBlueMask(), pixelStride, outSize.width * pixelStride, Format.FALSE, // flipped Format.NOT_SPECIFIED); // endian } else return null; // System.out.println("Decoder:: outputFormat is now: " + outputFormat.toString()); // Return the selected outputFormat return outputFormat; } /** * Get frame rate from Codec */ public float getFrameRate( int peer ) { if ( frameRate <= 0 ) frameRate = extractFrameRate( peer ); return frameRate; } /** * This flag can be used to allow data to start * in one buffer and end in the next. */ private boolean quirkIncompatibleBuffering = false; private byte[] leftOver = null; /** * The last timestamp and the relative frame number */ private long lastTime = 10; private int frames = 0; public int process(Buffer inBuffer, Buffer outBuffer) { int result; /* Do we need to set the time in the output buffer? */ outBuffer.setFlags( inBuffer.getFlags() ); if ( (inBuffer.getFlags() & Buffer.FLAG_NO_WAIT) == 0 ) { /* Has the input buffer specified a new timestamp */ if ( lastTime != inBuffer.getTimeStamp() ) { lastTime = inBuffer.getTimeStamp(); frames = 0; } else { /* Calculate time depending on frame rate */ outBuffer.setFlags( inBuffer.getFlags() | Buffer.FLAG_RELATIVE_TIME | Buffer.FLAG_NO_DROP ); } float currentFrameRate = ((VideoFormat)inBuffer.getFormat()).getFrameRate(); if ( currentFrameRate <=0 ) { currentFrameRate = getFrameRate(peer); } outBuffer.setTimeStamp( inBuffer.getTimeStamp() + (long)(((long)1000000000)/currentFrameRate) * frames); } if ( quirkIncompatibleBuffering && leftOver != null ) { byte[] data = new byte[ leftOver.length + inBuffer.getLength() ]; System.arraycopy( leftOver, 0, data, 0, leftOver.length ); byte[] in = (byte[])inBuffer.getData(); System.arraycopy( in, inBuffer.getOffset(), data, leftOver.length, inBuffer.getLength() ); inBuffer.setData( data ); inBuffer.setOffset( 0 ); inBuffer.setLength( data.length ); leftOver = null; } // System.out.println("#"+inBuffer.getSequenceNumber()+", timestamp="+inBuffer.getTimeStamp()); if ((inBuffer.getFlags() & Buffer.FLAG_EOM) != 0) { frames = 0; outBuffer.setFlags( outBuffer.getFlags() | Buffer.FLAG_EOM ); reset(); //System.out.println("return BUFFER_PROCESSED_OK"); return Codec.BUFFER_PROCESSED_OK; } if (inBuffer.isDiscard() ) { outBuffer.setDiscard(true); reset(); //System.out.println("return BUFFER_PROCESSED_OK"); return Codec.BUFFER_PROCESSED_OK; } Format inFormat = inBuffer.getFormat(); // if (inFormat != inputFormat && !(inFormat.matches(inputFormat))) { // System.out.println("format on inBuffer is " + inFormat.toString() + ", updating codec format"); // setInputFormat(inFormat); // } if (inBuffer.getLength() < 5) { outBuffer.setDiscard(true); reset(); //System.out.println("return BUFFER_PROCESSED_OK"); return Codec.BUFFER_PROCESSED_OK; } int inOffset = inBuffer.getOffset(); int inLength = inBuffer.getLength(); // The codec might read up to INPUT_BUFFER_PADDING_SIZE additional bytes // before checking for EOS, which can cause ArrayOutOfBounds in Java code. byte[] curArray = (byte[])inBuffer.getData(); int inBufSize = curArray.length; Object inData = (Object)inBuffer.getData(); //getInputData(inBuffer); // long inDataBytes = 0; //getNativeData(inData); /* TODO - check type is correct */ int[] outData = (int[])outBuffer.getData(); //getOutputData(outBuffer); if (outData == null || outData.length < outputLength || outBuffer.getFormat() != outputFormat || !outBuffer.getFormat().equals(outputFormat)) { // System.out.println("Decoder:: mismatch: " + (outData == null ? "NULL" : outData.toString()) + ", " + outBuffer.toString()); outData = new int[ outputLength ]; outBuffer.setLength(outputLength); outBuffer.setFormat(outputFormat); outBuffer.setData( outData ); } // outData = validateData(outBuffer, outputLength, true /*allow native*/); long outDataBytes = 0; //getNativeData(outData); int eof; // end of frame, used only if RTP mode if (rtpActive) { eof = inBuffer.getFlags() & Buffer.FLAG_RTP_MARKER; // Initialize seqNum on first use if (seqNum == 0) seqNum = inBuffer.getSequenceNumber(); // Try to detect loss of video packets by seqNum. Skip this // whole business if the timestamp is not set on the input // buffer. /** @todo find out how the timestamp is set on buffer in video decoder case */ if (seqNum != inBuffer.getSequenceNumber() && inBuffer.getTimeStamp() > 0 && timestamp > 0 ) { // System.out.println("Decoder:: packet loss, #" + seqNum + " ->#" + inBuffer.getSequenceNumber() + (timestamp != inBuffer.getTimeStamp() ? ", across frames (try to recover)" : ", within a single frame")); if (timestamp != inBuffer.getTimeStamp()) { // Try to recover last received frame result = convert(peer, inData, 0, 0, 0, outData, outputLength, 0); if ( result > 0 ) { // outBuffer.setTimeStamp(timestamp); outBuffer.setOffset(0); outBuffer.setLength(outputLength); frameNum++; // increment number of frames decoded // System.out.println("return INPUT_BUFFER_NOT_CONSUMED"); seqNum = inBuffer.getSequenceNumber(); // prevent packet loss code from being triggered a second time return Codec.INPUT_BUFFER_NOT_CONSUMED; } } } // Update seqNum to the values from inBuffer timestamp = inBuffer.getTimeStamp(); seqNum = inBuffer.getSequenceNumber(); } else { eof = 1; } result = convert(peer, inData, inBufSize, inOffset, inLength, outData, outputLength, eof); if ( result > 0 ) { inBuffer.setOffset( inOffset + result ); inBuffer.setLength( inLength - result ); } if ( result < 0 ) { //System.out.println("return BUFFER_PROCESSED_FAILED"); seqNum++; return Codec.BUFFER_PROCESSED_FAILED; } /** Not enough data */ if ( result == 0 ) { // System.out.println("Not enough data " + inLength ); if ( quirkIncompatibleBuffering ) { leftOver = new byte[ inLength ]; System.arraycopy( inData, inOffset, leftOver, 0, inLength ); } frames++; return Codec.BUFFER_PROCESSED_OK; } if (eof == 0) { // System.out.println("Decoder:: packet #" + seqNum + " (" + inLength + " bytes)"); seqNum++; outBuffer.setDiscard(true); //System.out.println("return BUFFER_PROCESSED_OK"); frames++; return Codec.BUFFER_PROCESSED_OK; } // System.out.println("Decoder:: packet #" + seqNum + " (" + inLength + " bytes), end of frame #" + frameNum); // outBuffer.setTimeStamp(inBuffer.getTimeStamp()); outBuffer.setOffset(0); outBuffer.setLength(outputLength); seqNum++; frameNum++; // increment number of frames decoded // At this point, the input and output frames buffers are done if (resetRequired && opened) reset(); frames++; if ( inBuffer.getLength() > 0 ) { return Codec.INPUT_BUFFER_NOT_CONSUMED; } //System.out.println("return BUFFER_PROCESSED_OK"); return Codec.BUFFER_PROCESSED_OK; } public synchronized void open() throws ResourceUnavailableException { if (!opened) { // System.out.println("Decoder:: open()"); synchronized (getClass()) { // if (inputFormat == null) // throw new ResourceUnavailableException("No input format selected"); if (outputFormat == null) throw new ResourceUnavailableException("No output format selected"); Dimension size = videoSize; if (!open_decoder(encoding, rtpActive, truncatedFlag, yuv2rgb, depth, rMask, gMask, bMask, (int)size.getWidth(), (int)size.getHeight())) throw new ResourceUnavailableException("Couldn't open codec for " + encoding); } resetRequired = false; } } public synchronized void close() { if (opened) { // System.out.println("Decoder:: close()"); close_decoder(peer); } } public synchronized void reset() { frames = 0; if (resetRequired && opened) { // System.out.println("Decoder:: reset()"); try { close(); open(); } catch(Exception e) { e.printStackTrace(); } } } public String getName() { return PLUGIN_NAME; } // Private method for encapsulating the RTP format check // Ideally each RTP protocol that we support should be // added here. private boolean isRTPFormat(Format format) { if (format.getEncoding().equals(VideoFormat.H263_RTP)) return true; return false; } // private method for finding the list of matching output formats // based on the input format. public Format [] getMatchingOutputFormats(Format in) { VideoFormat videoIn = (VideoFormat)in; Dimension inSize = videoIn.getSize(); // System.out.println("Decoder:: getMatchingOutputFormats(" + in.getEncoding() + ", size:" + inSize + ", fps:" + videoIn.getFrameRate() + ")"); int strideY = inSize.width; int strideUV = strideY / 2; int offsetU = strideY * inSize.height; int offsetV = offsetU + strideUV * inSize.height / 2; VideoFormat[] result = new VideoFormat [] { new YUVFormat(inSize, (strideY + strideUV) * inSize.height, Format.byteArray, videoIn.getFrameRate(), YUVFormat.YUV_420, strideY, strideUV, 0, offsetU, offsetV), new RGBFormat(inSize, inSize.width * inSize.height, Format.shortArray, videoIn.getFrameRate(), 15, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED, 1, inSize.width, Format.FALSE, Format.NOT_SPECIFIED), new RGBFormat(inSize, inSize.width * inSize.height, Format.shortArray, videoIn.getFrameRate(), 16, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED, 1, inSize.width, Format.FALSE, Format.NOT_SPECIFIED), new RGBFormat(inSize, inSize.width * inSize.height * 3, Format.byteArray, videoIn.getFrameRate(), 24, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED, 3, inSize.width * 3, Format.FALSE, Format.NOT_SPECIFIED), new RGBFormat(inSize, inSize.width * inSize.height, Format.intArray, videoIn.getFrameRate(), 32, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED, 1, inSize.width, Format.FALSE, Format.NOT_SPECIFIED), new RGBFormat(inSize, inSize.width * inSize.height, Format.shortArray, videoIn.getFrameRate(), 16, 0x7c00, 0x3e0, 0x1f, 1, inSize.width, Format.FALSE, Format.NOT_SPECIFIED), new RGBFormat(inSize, inSize.width * inSize.height, Format.shortArray, videoIn.getFrameRate(), 16, 0xf800, 0x3e0, 0x1f, 1, inSize.width, Format.FALSE, Format.NOT_SPECIFIED), new RGBFormat(inSize, inSize.width * inSize.height * 3, Format.byteArray, videoIn.getFrameRate(), 24, 3, 2, 1, 3, inSize.width * 3, Format.FALSE, Format.NOT_SPECIFIED), new RGBFormat(inSize, inSize.width * inSize.height, Format.intArray, videoIn.getFrameRate(), 32, 0xff0000, 0x00ff00, 0x0000ff, 1, inSize.width, Format.FALSE, Format.NOT_SPECIFIED) }; return result; } public Format[] getSupportedOutputFormats(Format i ) { return null; } }