All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.fmj.media.parser.MultipartMixedReplaceParser Maven / Gradle / Ivy

There is a newer version: 1.0.2-jitsi
Show newest version
package net.sf.fmj.media.parser;

import java.awt.*;
import java.io.*;
import java.util.*;
import java.util.logging.*;

import javax.imageio.*;
import javax.media.*;
import javax.media.format.*;
import javax.media.protocol.*;

import net.sf.fmj.media.*;
import net.sf.fmj.media.format.*;
import net.sf.fmj.utility.*;

/**
 * Parser for multipart/x-mixed-replace - used in some cases for streaming
 * jpegs. TODO: check out the jipcam project, which has Mjpeg parsing info. This
 * project also has some ip camera info:
 * http://www.codeproject.com/cs/media/cameraviewer.asp?print=true Some camera
 * links from that project: Not responding:
 * http://chipmunk.uvm.edu/cgi-bin/webcam/nph-update.cgi?dummy=garb Works:
 * http://webcam-1.duesseldorf.it-on.net/cgi-bin/nph-update.cgi Works:
 * http://webcam-2.duesseldorf.it-on.net/cgi-bin/nph-update.cgi Works:
 * http://towercam.uu.edu/axis-cgi/mjpg/video.cgi Works:
 * http://136.165.99.86/axis-cgi/mjpg/video.cgi Works:
 * http://217.114.115.192/axis-cgi/mjpg/video.cgi Works:
 * http://129.78.249.81/axis-cgi/mjpg/video.cgi
 *
 * Others: Works:
 * http://camera.baywatch.tv/axis-cgi/mjpg/video.cgi?camera=1&resolution
 * =352x240&compression=50 Works:
 * http://www.surfshooterhawaii.com//cgi-bin/axispush555.cgi?dummy=garb
 *
 * More camera links: http://www.axis.com/solutions/video/gallery.htm
 *
 * TODO: support end-of-message, with 2 dashes after separator, see
 * http://wp.netscape.com/assist/net_sites/pushpull.html
 *
 * @author Ken Larson
 *
 */
public class MultipartMixedReplaceParser extends AbstractDemultiplexer
{
    private abstract class PullSourceStreamTrack extends AbstractTrack
    {
        public abstract void deallocate();

    }

    private class VideoTrack extends PullSourceStreamTrack
    {
        // TODO: track listener

        private class MaxLengthExceededException extends IOException
        {
            public MaxLengthExceededException(String s)
            {
                super(s);
            }
        }

        private final PullSourceStream stream;

        private final VideoFormat format;

        /** handles pushbacks. */
        private byte[] pushbackBuffer;

        private int pushbackBufferLen;

        private int pushbackBufferOffset;

        private static final int MAX_LINE_LENGTH = 255;

        // We put a limit on how much we will read, to prevent running out of
        // memory in case something goes wrong.
        private final int MAX_IMAGE_SIZE = 1000000;
        private String boundary; // content boundary
        private int framesRead; // full valid frames only

        private String frameContentType;

        public VideoTrack(PullSourceStream stream)
                throws ResourceUnavailableException
        {
            super();

            this.stream = stream;
            // set format

            // read first frame to determine format
            final Buffer buffer = new Buffer();
            readFrame(buffer);
            if (buffer.isDiscard() || buffer.isEOM())
                throw new ResourceUnavailableException(
                        "Unable to read first frame");
            // TODO: catch runtime exception too?

            // parse jpeg
            final java.awt.Image image;
            try
            {
                image = ImageIO.read(new ByteArrayInputStream((byte[]) buffer
                        .getData(), buffer.getOffset(), buffer.getLength()));
            } catch (IOException e)
            {
                logger.log(Level.WARNING, "" + e, e);
                throw new ResourceUnavailableException("Error reading image: "
                        + e);
            }

            if (image == null)
            {
                logger.log(Level.WARNING,
                        "Failed to read image (ImageIO.read returned null).");
                throw new ResourceUnavailableException();
            }

            if (frameContentType.equals("image/jpeg"))
                format = new JPEGFormat(new Dimension(image.getWidth(null),
                        image.getHeight(null)), Format.NOT_SPECIFIED,
                        Format.byteArray, -1.f, Format.NOT_SPECIFIED,
                        Format.NOT_SPECIFIED);
            else if (frameContentType.equals("image/gif"))
                format = new GIFFormat(new Dimension(image.getWidth(null),
                        image.getHeight(null)), Format.NOT_SPECIFIED,
                        Format.byteArray, -1.f);
            else if (frameContentType.equals("image/png"))
                format = new PNGFormat(new Dimension(image.getWidth(null),
                        image.getHeight(null)), Format.NOT_SPECIFIED,
                        Format.byteArray, -1.f);
            else
                throw new ResourceUnavailableException(
                        "Unsupported frame content type: " + frameContentType);
            // TODO: this discards first image. save and return first time
            // readFrame is called.

        }

        @Override
        public void deallocate()
        {
        }

        /**
         * Eat bytes until we see the specified boundary. Return -1 + -1 * num
         * bytes eaten on eos, num bytes eaten otherwise.
         */
        private int eatUntil(String boundary) throws IOException
        {
            int totalEaten = 0;
            // TODO: is there a blank line before the boundary?
            final byte[] boundaryBytes = boundary.getBytes();
            final byte[] matchBuffer = new byte[boundaryBytes.length];

            int matchOffset = 0; // will be nonzero when checking a potential
                                 // match
            while (true)
            {
                // TODO: read more efficiently, not 1 at a time.
                final int lenRead = read(matchBuffer, matchOffset, 1);
                if (lenRead < 0)
                    return -1 + -1 * totalEaten; // EOS
                ++totalEaten;
                if (matchBuffer[matchOffset] == boundaryBytes[matchOffset])
                {
                    if (matchOffset == boundaryBytes.length - 1)
                    { // found the boundary
                        pushback(matchBuffer, matchOffset + 1); // push it back
                                                                // to be read
                                                                // again
                        totalEaten -= matchOffset + 1;
                        break;
                    } else
                    {
                        // keep matching the boundary
                        ++matchOffset;
                    }

                } else
                {
                    if (matchOffset > 0)
                    { // we had a partial, but not full match
                        matchOffset = 0;
                    } else
                    { // completely nonmatching byte

                    }
                }
            }

            return totalEaten;
        }

        @Override
        public Time getDuration()
        {
            return Duration.DURATION_UNKNOWN; // TODO
        }

        @Override
        public Format getFormat()
        {
            return format;
        }

        @Override
        public Time mapFrameToTime(int frameNumber)
        {
            return TIME_UNKNOWN;
        }

        @Override
        public int mapTimeToFrame(Time t)
        {
            return FRAME_UNKNOWN;
        }

        /** Property keys converted to all uppercase. */
        private boolean parseProperty(String line, Properties properties)
        {
            final int index = line.indexOf(':');
            if (index < 0)
                return false;
            final String key = line.substring(0, index).trim();
            final String value = line.substring(index + 1).trim();
            properties.setProperty(key.toUpperCase(), value);
            return true;

        }

        /** push bytes back, to be read again later. */
        private void pushback(byte[] bytes, int len)
        {
            if (pushbackBufferLen == 0)
            {
                pushbackBuffer = bytes; // TODO: copy?
                pushbackBufferLen = len;
                pushbackBufferOffset = 0;
            } else
            {
                final byte[] newPushbackBuffer = new byte[pushbackBufferLen
                        + len];
                System.arraycopy(pushbackBuffer, 0, newPushbackBuffer, 0,
                        pushbackBufferLen);
                System.arraycopy(bytes, 0, newPushbackBuffer,
                        pushbackBufferLen, len);
                pushbackBuffer = newPushbackBuffer;
                pushbackBufferLen = pushbackBufferLen + len;
                pushbackBufferOffset = 0;
            }
        }

        // TODO: from JAVADOC:
        // This method might block if the data for a complete frame is not
        // available. It might also block if the stream contains intervening
        // data for a different interleaved Track. Once the other Track is read
        // by a readFrame call from a different thread, this method can read the
        // frame. If the intervening Track has been disabled, data for that
        // Track is read and discarded.
        //
        // Note: This scenario is necessary only if a PullDataSource
        // Demultiplexer implementation wants to avoid buffering data locally
        // and copying the data to the Buffer passed in as a parameter.
        // Implementations might decide to buffer data and not block (if
        // possible) and incur data copy overhead.

        /** supports pushback. */
        private int read(byte[] buffer, int offset, int length)
                throws IOException
        {
            if (pushbackBufferLen > 0)
            { // read from pushback buffer
                final int lenToCopy = length < pushbackBufferLen ? length
                        : pushbackBufferLen;
                System.arraycopy(pushbackBuffer, pushbackBufferOffset, buffer,
                        offset, lenToCopy);
                pushbackBufferLen -= lenToCopy;
                pushbackBufferOffset += lenToCopy;
                return lenToCopy;
            } else
            {
                return stream.read(buffer, offset, length);
            }
        }

        @Override
        public void readFrame(Buffer buffer)
        {
            // example data:
            // --ssBoundary8345
            // Content-Type: image/jpeg
            // Content-Length: 114587

            try
            {
                String line;
                // eat leading blank lines
                while (true)
                {
                    line = readLine(MAX_LINE_LENGTH);
                    if (line == null)
                    {
                        buffer.setEOM(true);
                        buffer.setLength(0);
                        return;
                    }

                    if (!line.trim().equals(""))
                        break; // end of header

                }

                if (boundary == null)
                {
                    boundary = line.trim(); // TODO: we should be able to get
                                            // this from the content type, but
                                            // the content type has this
                                            // stripped out. So we'll just take
                                            // the first nonblank line to be the
                                            // boundary.
                    // System.out.println("boundary: " + boundary);
                } else
                {
                    if (!line.trim().equals(boundary))
                    {
                        // throw new IOException("Expected boundary: " +
                        // toPrintable(line));
                        // TODO: why do we seem to get these when playing back
                        // mmr files recorded using FmjTranscode?
                        logger.warning("Expected boundary (frame " + framesRead
                                + "): " + toPrintable(line));

                        // handle streams that are truncated in the middle of a
                        // frame:
                        final int eatResult = eatUntil(boundary); // TODO: no
                                                                  // need to
                                                                  // store the
                                                                  // data

                        logger.info("Ignored bytes (eom after="
                                + (eatResult < 0)
                                + "): "
                                + (eatResult < 0 ? (-1 * eatResult - 1)
                                        : eatResult));
                        if (eatResult < 0)
                        {
                            buffer.setEOM(true);
                            buffer.setLength(0);
                            return;
                        }

                        // now read boundary
                        line = readLine(MAX_LINE_LENGTH);
                        if (!line.trim().equals(boundary))
                        {
                            throw new RuntimeException(
                                    "No boundary found after eatUntil(boundary)"); // should
                                                                                   // never
                                                                                   // happen
                        }

                    }
                }

                final Properties properties = new Properties();

                while (true)
                {
                    line = readLine(MAX_LINE_LENGTH);
                    if (line == null)
                    {
                        buffer.setEOM(true);
                        buffer.setLength(0);
                        return;
                    }

                    if (line.trim().equals(""))
                        break; // end of header

                    if (!parseProperty(line, properties))
                        throw new IOException("Expected property: "
                                + toPrintable(line));

                }

                final String contentType = properties
                        .getProperty("Content-Type".toUpperCase());
                if (contentType == null)
                {
                    logger.warning("Header properties: " + properties);
                    throw new IOException("Expected Content-Type in header");
                }

                // check supported content types:
                if (!isSupportedFrameContentType(contentType))
                {
                    throw new IOException("Unsupported Content-Type: "
                            + contentType);
                }

                if (frameContentType == null)
                {
                    frameContentType = contentType;
                } else
                {
                    if (!contentType.equals(frameContentType))
                        throw new IOException(
                                "Content type changed during stream from "
                                        + frameContentType + " to "
                                        + contentType);
                }

                // TODO: check that size doesn't change throughout

                final byte[] data;

                final String contentLenStr = properties
                        .getProperty("Content-Length".toUpperCase());
                if (contentLenStr != null)
                { // if we know the content length, use it
                    final int contentLen;
                    try
                    {
                        contentLen = Integer.parseInt(contentLenStr);
                    } catch (NumberFormatException e)
                    {
                        throw new IOException("Invalid content length: "
                                + contentLenStr);
                    }

                    // now, read the content-length bytes
                    data = readFully(contentLen); // TODO: don't realloc each
                                                  // time
                } else
                {
                    // if we don't know the content length, just read until we
                    // find the boundary.
                    // Some IP cameras don't specify it, like
                    // http://webcam-1.duesseldorf.it-on.net/cgi-bin/nph-update.cgi
                    data = readUntil(boundary);

                }

                // ext
                final String timestampStr = properties
                        .getProperty(TIMESTAMP_KEY.toUpperCase());
                if (timestampStr != null)
                {
                    try
                    {
                        final long timestamp = Long.parseLong(timestampStr);
                        buffer.setTimeStamp(timestamp);

                    } catch (NumberFormatException e)
                    {
                        logger.log(Level.WARNING, "" + e, e);
                    }
                }

                if (data == null)
                {
                    buffer.setEOM(true);
                    buffer.setLength(0);
                    return;
                }

                buffer.setData(data);
                buffer.setOffset(0);
                buffer.setLength(data.length);
                ++framesRead;

            } catch (IOException e)
            {
                throw new RuntimeException(e);
            }

        }

        private byte[] readFully(int bytes) throws IOException
        {
            final byte[] buffer = new byte[bytes];
            int offset = 0;
            int length = bytes;

            while (true)
            {
                final int lenRead = read(buffer, offset, length);
                if (lenRead < 0)
                    return null; // EOS
                if (lenRead == length)
                    return buffer;
                length -= lenRead;
                offset += lenRead;
            }
        }

        /** return null on eom */
        private String readLine(int max) throws IOException
        {
            final byte[] buffer = new byte[max];
            int offset = 0;
            final int length = 1;
            while (true)
            {
                if (offset >= max)
                    throw new MaxLengthExceededException("No newline found in "
                            + max + " bytes"); // no newline up to max.
                final int lenRead = read(buffer, offset, length);
                if (lenRead < 0)
                    return null; // EOS
                if (buffer[offset] == '\n')
                {
                    if (offset > 0 && buffer[offset - 1] == '\r')
                        offset -= 1; // don't include \r
                    return new String(buffer, 0, offset);
                }
                offset += 1;
            }
        }

        /** Read until we see the specified boundary. */
        private byte[] readUntil(String boundary) throws IOException
        {
            // TODO: is there a blank line before the boundary?
            final ByteArrayOutputStream os = new ByteArrayOutputStream();
            final byte[] boundaryBytes = boundary.getBytes();
            final byte[] matchBuffer = new byte[boundaryBytes.length];

            int matchOffset = 0; // will be nonzero when checking a potential
                                 // match
            while (true)
            {
                if (os.size() >= MAX_IMAGE_SIZE)
                    throw new IOException("No boundary found in "
                            + MAX_IMAGE_SIZE + " bytes.");
                // TODO: read more efficiently, not 1 at a time.
                final int lenRead = read(matchBuffer, matchOffset, 1);
                if (lenRead < 0)
                    return null; // EOS
                if (matchBuffer[matchOffset] == boundaryBytes[matchOffset])
                {
                    if (matchOffset == boundaryBytes.length - 1)
                    { // found the boundary
                        pushback(matchBuffer, matchOffset + 1); // push it back
                                                                // to be read
                                                                // again
                        break;
                    } else
                    {
                        // keep matching the boundary
                        ++matchOffset;
                    }

                } else
                {
                    if (matchOffset > 0)
                    { // we had a partial, but not full match - dump it all into
                      // the buffer
                        os.write(matchBuffer, 0, matchOffset + 1);
                        matchOffset = 0;
                    } else
                    {
                        // completely nonmatching byte
                        os.write(matchBuffer, 0, 1);
                    }
                }
            }

            final byte[] result = os.toByteArray();
            return result;

            // // the crlf before the boundary is part of the boundary, see
            // // http://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
            // // TODO: we should basically add this to the boundary
            //
            // System.out.println("\\r=" + Integer.toHexString('\r'));
            // System.out.println("\\n=" + Integer.toHexString('\n'));
            // System.out.println("" +
            // Integer.toHexString(result[result.length-3]));
            // System.out.println("" +
            // Integer.toHexString(result[result.length-2]));
            // System.out.println("" +
            // Integer.toHexString(result[result.length-1]));
            //
            // final int trim = 2;
            // final byte[] trimmedResult = new byte[result.length - trim];
            // System.arraycopy(result, 0, trimmedResult, 0,
            // trimmedResult.length);
            //
            //
            //
            // return trimmedResult;
        }
    }

    public static final String TIMESTAMP_KEY = "X-FMJ-Timestamp"; // will be
                                                                  // ignored by
                                                                  // most
                                                                  // recipients,
                                                                  // but with
                                                                  // FMJ we have
                                                                  // the option
                                                                  // of timing
                                                                  // the
                                                                  // playback
                                                                  // based on
                                                                  // this.

    private static final Logger logger = LoggerSingleton.logger;

    /**
     * Allows us to dump out arbitrary, possible binary data, that we were
     * expecting to be a certain string.
     */
    private static String toPrintable(String line)
    {
        return toPrintable(line, 32);
    }

    private static String toPrintable(String line, int max)
    {
        StringBuilder b = new StringBuilder();

        for (int i = 0; i < line.length(); ++i)
        {
            if (i >= max)
                break;
            char c = line.charAt(i);
            if (c >= ' ' && c <= '~')
                b.append(c);
            else
                b.append('.');

        }
        return b.toString();
    }

    private ContentDescriptor[] supportedInputContentDescriptors = new ContentDescriptor[] { new ContentDescriptor(
            "multipart.x_mixed_replace") };

    private static final String[] supportedFrameContentTypes = new String[] {
            "image/jpeg", "image/gif", "image/png" };

    private static final boolean isSupportedFrameContentType(String contentType)
    {
        for (String supported : supportedFrameContentTypes)
        {
            if (supported.equals(contentType.toLowerCase()))
                return true;
        }
        return false;
    }

    private PullDataSource source;

    private PullSourceStreamTrack[] tracks;

    public MultipartMixedReplaceParser()
    {
        super();
    }

    @Override
    public void close()
    {
        if (tracks != null)
        {
            for (int i = 0; i < tracks.length; ++i)
            {
                if (tracks[i] != null)
                {
                    tracks[i].deallocate();
                    tracks[i] = null;
                }
            }
            tracks = null;
        }

        super.close();
    }

    @Override
    public ContentDescriptor[] getSupportedInputContentDescriptors()
    {
        return supportedInputContentDescriptors;
    }

    // TODO: should we stop data source in stop?
    // // @Override
    // public void stop()
    // {
    // try
    // {
    // source.stop();
    // } catch (IOException e)
    // {
    // logger.log(Level.WARNING, "" + e, e);
    // }
    // }

    @Override
    public Track[] getTracks() throws IOException, BadHeaderException
    {
        return tracks;
    }

    // @Override
    // public Time setPosition(Time where, int rounding)
    // {
    // }

    @Override
    public boolean isPositionable()
    {
        return false; // TODO
    }

    @Override
    public boolean isRandomAccess()
    {
        return super.isRandomAccess(); // TODO: can we determine this from the
                                       // data source?
    }

    // @Override
    @Override
    public void open() throws ResourceUnavailableException
    {
        try
        {
            // source.connect(); // TODO: assume source is already connected
            source.start(); // TODO: stop/disconnect on stop/close.

            final PullSourceStream[] streams = source.getStreams();

            tracks = new PullSourceStreamTrack[streams.length];

            for (int i = 0; i < streams.length; ++i)
            {
                tracks[i] = new VideoTrack(streams[i]);

            }

        } catch (IOException e)
        {
            logger.log(Level.WARNING, "" + e, e);
            throw new ResourceUnavailableException("" + e);
        }

        super.open();

    }

    @Override
    public void setSource(DataSource source) throws IOException,
            IncompatibleSourceException
    {
        final String protocol = source.getLocator().getProtocol();

        if (!(source instanceof PullDataSource))
            throw new IncompatibleSourceException();

        this.source = (PullDataSource) source;

    }

    // @Override
    @Override
    public void start() throws IOException
    {
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy