/* ** AACDecoder - Freeware Advanced Audio (AAC) Decoder for Android ** Copyright (C) 2012 Spolecne s.r.o., http://www.spoledge.com ** ** This file is a part of AACDecoder. ** ** AACDecoder 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 3 of the License, ** or (at your option) any later version. ** ** This program 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 program. If not, see <http://www.gnu.org/licenses/>. */ package com.spoledge.aacdecoder; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import com.rubika.aotalk.util.Logging; /** * This is an InputStream which allows to fetch Icecast/Shoutcast metadata from. */ public class IcyInputStream extends FilterInputStream { private static final String APP_TAG = "--> The Leet :: IcyInputStream"; //////////////////////////////////////////////////////////////////////////// // Attributes //////////////////////////////////////////////////////////////////////////// /** * The period of metadata frame in bytes. */ protected int period; /** * The actual number of remaining bytes before the metadata. */ protected int remaining; /** * This is a temporary buffer used for fetching metadata bytes. */ protected byte[] mbuffer; /** * The callback - may be null. */ protected PlayerCallback playerCallback; //////////////////////////////////////////////////////////////////////////// // Constructors //////////////////////////////////////////////////////////////////////////// /** * Creates a new input stream. * @param in the underlying input stream * @param period the period of metadata frame is repeating (in bytes) */ public IcyInputStream( InputStream in, int period ) { this( in, period, null ); } /** * Creates a new input stream. * @param in the underlying input stream * @param period the period of metadata frame is repeating (in bytes) * @param playerCallback the callback - may be null */ public IcyInputStream( InputStream in, int period, PlayerCallback playerCallback ) { super( in ); this.period = period; this.playerCallback = playerCallback; remaining = period; mbuffer = new byte[128]; } //////////////////////////////////////////////////////////////////////////// // InputStream //////////////////////////////////////////////////////////////////////////// @Override public int read() throws IOException { int ret = super.read(); if (--remaining == 0) fetchMetadata(); return ret; } @Override public int read( byte[] buffer, int offset, int len ) throws IOException { int ret = in.read( buffer, offset, remaining < len ? remaining : len ); if (remaining == ret) fetchMetadata(); else remaining -= ret; return ret; } //////////////////////////////////////////////////////////////////////////// // Protected //////////////////////////////////////////////////////////////////////////// /** * This method reads the metadata string. * Actually it calls the method parseMetadata(). */ protected void fetchMetadata() throws IOException { remaining = period; int size = in.read(); // either no metadata or eof: if (size < 1) return; // size *= 16: size <<= 4; if (mbuffer.length < size) { mbuffer = null; mbuffer = new byte[ size ]; Logging.log(APP_TAG, "Enlarged metadata buffer to " + size + " bytes"); } size = readFully( mbuffer, 0, size ); // find the string end: for (int i=0; i < size; i++) { if (mbuffer[i] == 0) { size = i; break; } } String s; try { s = new String( mbuffer, 0, size, "UTF-8" ); } catch (Exception e) { Logging.log(APP_TAG, "Cannot convert bytes to String"); return; } Logging.log(APP_TAG, "Metadata string: " + s); parseMetadata( s ); } /** * Parses the metadata and sends them to PlayerCallback. * @param s the metadata string like: StreamTitle='...';StreamUrl='...'; */ protected void parseMetadata( String s ) { String[] kvs = s.split( ";" ); for (String kv : kvs) { int n = kv.indexOf( '=' ); if (n < 1) continue; boolean isString = n + 1 < kv.length() && kv.charAt( kv.length() - 1) == '\'' && kv.charAt( n + 1 ) == '\''; String key = kv.substring( 0, n ); String val = isString ? kv.substring( n+2, kv.length()-1) : n + 1 < kv.length() ? kv.substring( n+1 ) : ""; // yes - we should detect this earlier, but it will not be null in most cases: if (playerCallback != null) playerCallback.playerMetadata( key, val ); } } /** * Tries to read all bytes into the target buffer. * @param size the requested size * @return the number of really bytes read; if less than requested, then eof detected */ protected final int readFully( byte[] buffer, int offset, int size ) throws IOException { int n; int oo = offset; while (size > 0 && (n = in.read( buffer, offset, size )) != -1) { offset += n; size -= n; } return offset - oo; } }