Class IcyInputStream

  • All Implemented Interfaces:
    java.io.Closeable, java.lang.AutoCloseable, MP3MetadataParser

    public class IcyInputStream
    extends java.io.BufferedInputStream
    implements MP3MetadataParser
    An BufferedInputStream that parses Shoutcast's "icy" metadata from the stream. Gets headers at the beginning and if the "icy-metaint" tag is found, it parses and strips in-stream metadata.

    The deal with metaint: Icy streams don't try to put tags between MP3 frames the way that ID3 does. Instead, it requires the client to strip metadata from the stream before it hits the decoder. You get an icy-metaint name/val in the beginning of the stream iff you sent "Icy-Metadata" with value "1" in the request headers (SimpleMP3DataSource does this if the "parseStreamMetadata" boolean is true). If this is the case then the value of icy-metaint is the amount of real data between metadata blocks. Each block begins with an int indicating how much metadata there is -- the block is this value times 16 (it can be, and often is, 0).

    Originally thought that "icy" implied Icecast, but this is completely wrong -- real Icecast servers, found through www.icecast.net and typified by URLs with a trailing directory (like CalArts School of Music - http://65.165.174.100:8000/som) do not have the "ICY 200 OK" magic string or any of the CRLF-separated headers. Apparently, "icy" means "Shoutcast". Yep, that's weird.

    • Field Summary

      Fields 
      Modifier and Type Field Description
      protected int bytesUntilNextMetadata
      how many bytes of real data remain before the next block of metadata.
      protected byte[] crlfBuffer
      Buffer for readCRLF line...
      static boolean DEBUG  
      protected static java.lang.String INLINE_TAG_SEPARATORS
      inline tags are delimited by ';', also filter out null bytes
      protected int metaint
      value of the "metaint" tag, which tells us how many bytes of real data are between the metadata tags.
      • Fields inherited from class java.io.BufferedInputStream

        buf, count, marklimit, markpos, pos
      • Fields inherited from class java.io.FilterInputStream

        in
    • Constructor Summary

      Constructors 
      Constructor Description
      IcyInputStream​(java.io.InputStream in)
      Reads the initial headers of the stream and adds tags appropriatly.
      IcyInputStream​(java.io.InputStream in, java.lang.String metaIntString)
      IcyInputStream constructor for know meta-interval (Icecast 2)
    • Method Summary

      All Methods Static Methods Instance Methods Concrete Methods 
      Modifier and Type Method Description
      protected void addTag​(IcyTag tag)
      adds the tag to the HashMap of tags we have encountered either in-stream or as headers, replacing any previous tag with this name.
      void addTagParseListener​(TagParseListener tpl)
      Adds a TagParseListener to be notified when this stream parses MP3Tags.
      MP3Tag getTag​(java.lang.String tagName)
      Get the named tag from the HashMap of headers and in-line tags.
      java.util.HashMap getTagHash()
      Returns a HashMap of all headers and in-stream tags parsed so far.
      MP3Tag[] getTags()
      Get all tags (headers or in-stream) encountered thus far.
      static void main​(java.lang.String[] args)
      Quickie unit-test.
      protected void parseInlineIcyTags​(byte[] tagBlock)
      Parse metadata from an in-stream "block" of bytes, add a tag for each one.
      int read()
      Reads and returns a single byte.
      int read​(byte[] buf)
      trivial return read (buf, 0, buf.length)
      int read​(byte[] buf, int offset, int length)
      Reads a block of bytes.
      protected java.lang.String readCRLFLine()
      Read everything up to the next CRLF, return it as a String.
      protected void readInitialHeaders()
      Assuming we're at the top of the stream, read lines one by one until we hit a completely blank \r\n.
      protected void readMetadata()
      Read the next segment of metadata.
      void removeTagParseListener​(TagParseListener tpl)
      Removes a TagParseListener, so it won't be notified when this stream parses MP3Tags.
      • Methods inherited from class java.io.BufferedInputStream

        available, close, mark, markSupported, reset, skip
      • Methods inherited from class java.io.InputStream

        nullInputStream, readAllBytes, readNBytes, readNBytes, transferTo
      • Methods inherited from class java.lang.Object

        clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
    • Field Detail

      • DEBUG

        public static boolean DEBUG
      • INLINE_TAG_SEPARATORS

        protected static final java.lang.String INLINE_TAG_SEPARATORS
        inline tags are delimited by ';', also filter out null bytes
        See Also:
        Constant Field Values
      • crlfBuffer

        protected byte[] crlfBuffer
        Buffer for readCRLF line... note this limits lines to 1024 chars (I've read that WinAmp barfs at 128, so this is generous)
      • metaint

        protected int metaint
        value of the "metaint" tag, which tells us how many bytes of real data are between the metadata tags. if -1, this stream does not have metadata after the header.
      • bytesUntilNextMetadata

        protected int bytesUntilNextMetadata
        how many bytes of real data remain before the next block of metadata. Only meaningful if metaint != -1.
    • Constructor Detail

      • IcyInputStream

        public IcyInputStream​(java.io.InputStream in)
                       throws java.io.IOException
        Reads the initial headers of the stream and adds tags appropriatly. Gets set up to find, read, and strip blocks of in-line metadata if the icy-metaint header is found.
        Throws:
        java.io.IOException
      • IcyInputStream

        public IcyInputStream​(java.io.InputStream in,
                              java.lang.String metaIntString)
                       throws java.io.IOException
        IcyInputStream constructor for know meta-interval (Icecast 2)
        Parameters:
        in -
        metaint -
        Throws:
        java.io.IOException
    • Method Detail

      • readInitialHeaders

        protected void readInitialHeaders()
                                   throws java.io.IOException
        Assuming we're at the top of the stream, read lines one by one until we hit a completely blank \r\n. Parse the data as IcyTags.
        Throws:
        java.io.IOException
      • readCRLFLine

        protected java.lang.String readCRLFLine()
                                         throws java.io.IOException
        Read everything up to the next CRLF, return it as a String.
        Throws:
        java.io.IOException
      • read

        public int read()
                 throws java.io.IOException
        Reads and returns a single byte. If the next byte is a metadata block, then that block is read, stripped, and parsed before reading and returning the first byte after the metadata block.
        Overrides:
        read in class java.io.BufferedInputStream
        Throws:
        java.io.IOException
      • read

        public int read​(byte[] buf,
                        int offset,
                        int length)
                 throws java.io.IOException
        Reads a block of bytes. If the next byte is known to be a block of metadata, then that is read, parsed, and stripped, and then a block of bytes is read and returned. Otherwise, it may read up to but not into the next metadata block if bytesUntilNextMetadata < length
        Overrides:
        read in class java.io.BufferedInputStream
        Throws:
        java.io.IOException
      • read

        public int read​(byte[] buf)
                 throws java.io.IOException
        trivial return read (buf, 0, buf.length)
        Overrides:
        read in class java.io.FilterInputStream
        Throws:
        java.io.IOException
      • readMetadata

        protected void readMetadata()
                             throws java.io.IOException
        Read the next segment of metadata. The stream must be right on the segment, ie, the next byte to read is the metadata block count. The metadata is parsed and new tags are added with addTag(), which fires events
        Throws:
        java.io.IOException
      • parseInlineIcyTags

        protected void parseInlineIcyTags​(byte[] tagBlock)
        Parse metadata from an in-stream "block" of bytes, add a tag for each one.

        Hilariously, the inline data format is totally different than the top-of-stream header. For example, here's a block I saw on "Final Fantasy Radio":

                StreamTitle='Final Fantasy 8 - Nobuo Uematsu - Blue Fields';StreamUrl='';
                
        In other words:
        1. Tags are delimited by semicolons
        2. Keys/values are delimited by equals-signs
        3. Values are wrapped in single-quotes
        4. Key names are in SentenceCase, not lowercase-dashed
      • addTag

        protected void addTag​(IcyTag tag)
        adds the tag to the HashMap of tags we have encountered either in-stream or as headers, replacing any previous tag with this name.
      • getTag

        public MP3Tag getTag​(java.lang.String tagName)
        Get the named tag from the HashMap of headers and in-line tags. Null if no such tag has been encountered.
      • getTags

        public MP3Tag[] getTags()
        Get all tags (headers or in-stream) encountered thus far.
        Specified by:
        getTags in interface MP3MetadataParser
      • getTagHash

        public java.util.HashMap getTagHash()
        Returns a HashMap of all headers and in-stream tags parsed so far.
      • main

        public static void main​(java.lang.String[] args)
        Quickie unit-test.