Package net.sf.fmj.media.parser

Source Code of net.sf.fmj.media.parser.MultipartMixedReplaceParser

package net.sf.fmj.media.parser;

import java.awt.Dimension;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.imageio.ImageIO;
import javax.media.BadHeaderException;
import javax.media.Buffer;
import javax.media.Duration;
import javax.media.Format;
import javax.media.IncompatibleSourceException;
import javax.media.ResourceUnavailableException;
import javax.media.Time;
import javax.media.Track;
import javax.media.format.JPEGFormat;
import javax.media.format.VideoFormat;
import javax.media.protocol.ContentDescriptor;
import javax.media.protocol.DataSource;
import javax.media.protocol.PullDataSource;
import javax.media.protocol.PullSourceStream;

import net.sf.fmj.media.AbstractDemultiplexer;
import net.sf.fmj.media.AbstractTrack;
import net.sf.fmj.media.format.GIFFormat;
import net.sf.fmj.media.format.PNGFormat;
import net.sf.fmj.utility.LoggerSingleton;

/**
* 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
{
  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;
 

  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 ContentDescriptor[] getSupportedInputContentDescriptors()
  {
    return supportedInputContentDescriptors;
  }

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

 
  @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 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 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
  @Override
  public void start() throws IOException
  {

  }
 
  // 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 boolean isPositionable()
  {
    return false// TODO
  }
 
//  @Override
//  public Time setPosition(Time where, int rounding)
//  {
//  }

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

  private abstract class PullSourceStreamTrack extends AbstractTrack
  {
    public abstract void deallocate();

  }
 


 
  private class VideoTrack extends PullSourceStreamTrack
  {
    // TODO: track listener
   
    private final PullSourceStream stream;
    private final VideoFormat format;
   
    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()
    {
    }

    /**
     *
     * @return nanos skipped, 0 if unable to skip.
     * @throws IOException
     */
    public long skipNanos(long nanos) throws IOException
    {
      return 0// TODO
     
    }
   
    public boolean canSkipNanos()
    {
      return false;
    }

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

    /** handles pushbacks. */
    private byte[] pushbackBuffer;
    private int pushbackBufferLen;
    private int pushbackBufferOffset;
   
    /** 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;
      }
    }
   
    /** 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);
      }
    }
   
    private static final int MAX_LINE_LENGTH = 255;
   
    private class MaxLengthExceededException extends IOException
    {

      public MaxLengthExceededException(String s)
      {
        super(s);
      }
    }
    /** 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;
      }
    }
   
    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;
      }
    }
   
    // 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
   
    /** 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;
    }
   
    /** 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;
    }
   
    /** 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;
     
    }
   
    private String boundary;  // content boundary
   
//      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.
    
    private int framesRead;  // full valid frames only
    private String frameContentType;
   
    @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);
      }
     

    }

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

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

  }

  /** 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();
  }
}
TOP

Related Classes of net.sf.fmj.media.parser.MultipartMixedReplaceParser

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.