Android Open Source - StreamingService A A C Player






From Project

Back to project page StreamingService.

License

The source code is released under:

Apache License

If you think the Android project StreamingService listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
** File modified from AACDecoder version 0.8
** by Commando Coder Ltd under LGPL.//w  w w .ja  va  2 s . com
*/

/*
** AACDecoder - Freeware Advanced Audio (AAC) Decoder for Android
** Copyright (C) 2011 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 android.util.Log;

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;

import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;


/**
 * This is the AAC Stream player class.
 * It uses Decoder to decode AAC stream into PCM samples.
 * This class is not thread safe.
 * <pre>
 *  AACPlayer player = new AACPlayer();
 *
 *  String url = ...;
 *  player.playAsync( url );
 * </pre>
 */
public class AACPlayer {

    /**
     * The default expected bitrate.
     * Used only if not specified in play() methods.
     */
    public static final int DEFAULT_EXPECTED_KBITSEC_RATE = 64;


    /**
     * The default capacity of the audio buffer (AudioTrack) in ms.
     * @see setAudioBufferCapacityMs(int)
     */
    public static final int DEFAULT_AUDIO_BUFFER_CAPACITY_MS = 1500;


    /**
     * The default capacity of the output buffer used for decoding in ms.
     * @see setDecodeBufferCapacityMs(int)
     */
    public static final int DEFAULT_DECODE_BUFFER_CAPACITY_MS = 700;


    private static final String LOG = "AACPlayer";


    ////////////////////////////////////////////////////////////////////////////
    // Attributes
    ////////////////////////////////////////////////////////////////////////////

    protected boolean stopped;
    protected boolean metadataEnabled = true;
    protected boolean responseCodeCheckEnabled = true;

    protected int audioBufferCapacityMs;
    protected int decodeBufferCapacityMs;
    protected PlayerCallback playerCallback;
    protected String metadataCharEnc;

    protected Decoder decoder;

    /**
     * The bit rate declared by the stream header - kb/s.
     */
    protected int declaredBitRate = -1;

    // variables used for computing average bitrate
    private int sumKBitSecRate = 0;
    private int countKBitSecRate = 0;
    private int avgKBitSecRate = 0;

    private PCMFeed pcmfeed = null;


  private float leftVolume = 1;
  private float rightVolume = 1;

    ////////////////////////////////////////////////////////////////////////////
    // Constructors
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Creates a new player.
     */
    public AACPlayer() {
        this( null );
    }


    /**
     * Creates a new player.
     * @param playerCallback the callback, can be null
     */
    public AACPlayer( PlayerCallback playerCallback ) {
        this( playerCallback, DEFAULT_AUDIO_BUFFER_CAPACITY_MS, DEFAULT_DECODE_BUFFER_CAPACITY_MS );
    }


    /**
     * Creates a new player.
     * @param playerCallback the callback, can be null
     * @param audioBufferCapacityMs the capacity of the audio buffer (AudioTrack) in ms
     * @param decodeBufferCapacityMs the capacity of the buffer used for decoding in ms
     * @see setAudioBufferCapacityMs(int)
     * @see setDecodeBufferCapacityMs(int)
     */
    public AACPlayer( PlayerCallback playerCallback, int audioBufferCapacityMs, int decodeBufferCapacityMs ) {
        setPlayerCallback( playerCallback );
        setAudioBufferCapacityMs( audioBufferCapacityMs );
        setDecodeBufferCapacityMs( decodeBufferCapacityMs );

        decoder = createDecoder();
    }


    ////////////////////////////////////////////////////////////////////////////
    // Public
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Returns the underlying decoder.
     */
    public Decoder getDecoder() {
        return decoder;
    }


    /**
     * Sets the custom decoder.
     */
    public void setDecoder( Decoder decoder ) {
        this.decoder = decoder;
    }


    /**
     * Sets the audio buffer (AudioTrack) capacity.
     * The capacity can be expressed in time of audio playing of such buffer.
     * For example 1 second buffer capacity is 88100 samples for 44kHz stereo.
     * By setting this the audio will start playing after the audio buffer is first filled.
     *
     * NOTE: this should be set BEFORE any of the play methods are called.
     *
     * @param audioBufferCapacityMs the capacity of the buffer in milliseconds
     */
    public void setAudioBufferCapacityMs( int audioBufferCapacityMs ) {
        this.audioBufferCapacityMs = audioBufferCapacityMs;
    }


    /**
     * Gets the audio buffer capacity as the audio playing time.
     * @return the capacity of the audio buffer in milliseconds
     */
    public int getAudioBufferCapacityMs() {
        return audioBufferCapacityMs;
    }


    /**
     * Sets the capacity of the output buffer used for decoding.
     * The capacity can be expressed in time of audio playing of such buffer.
     * For example 1 second buffer capacity is 88100 samples for 44kHz stereo.
     * Decoder tries to fill out the whole buffer in each round.
     *
     * NOTE: this should be set BEFORE any of the play methods are called.
     *
     * @param decodeBufferCapacityMs the capacity of the buffer in milliseconds
     */
    public void setDecodeBufferCapacityMs( int decodeBufferCapacityMs ) {
        this.decodeBufferCapacityMs = decodeBufferCapacityMs;
    }


    /**
     * Gets the capacity of the output buffer used for decoding as the audio playing time.
     * @return the capacity of the decoding buffer in milliseconds
     */
    public int getDecodeBufferCapacityMs() {
        return decodeBufferCapacityMs;
    }


    /**
     * Sets the PlayerCallback.
     * NOTE: this should be set BEFORE any of the play methods are called.
     */
    public void setPlayerCallback( PlayerCallback playerCallback ) {
        this.playerCallback = playerCallback;
    }


    /**
     * Returns the PlayerCallback or null if no PlayerCallback was set.
     */
    public PlayerCallback getPlayerCallback() {
        return playerCallback;
    }


    /**
     * Returns the flag if metadata information is enabeld / sent to PlayerCallback.
     */
    public boolean getMetadataEnabled() {
        return metadataEnabled;
    }


    /**
     * Sets the flag if metadata information is enabeld / sent to PlayerCallback.
     * This is enabled by default.
     */
    public void setMetadataEnabled( boolean metadataEnabled ) {
        this.metadataEnabled = metadataEnabled;
    }


    /**
     * Returns the flag if the HTTP / shoutcast response code should be checked or not.
     */
    public boolean getResponseCodeCheckEnabled() {
        return responseCodeCheckEnabled;
    }


    /**
     * Sets the flag if the HTTP / shoutcast response code should be checked or not.
     * This method was added for backward compatibility. By disabling the check
     * you also force pre-Kitkat devices to use original HttpURLConnection implementation
     * even for shoutcast streams.
     * This is enabled by default.
     * @since 0.8
     */
    public void setResponseCodeCheckEnabled( boolean responseCodeCheckEnabled ) {
        this.responseCodeCheckEnabled = responseCodeCheckEnabled;
    }


    /**
     * Sets the encoding for the metadata strings.
     * If not set, then UTF-8 is used.
     */
    public void setMetadataCharEnc( String metadataCharEnc ) {
        this.metadataCharEnc = metadataCharEnc;
    }


    /**
     * Returns the bit-rate as declared by the stream metadata.
     * @return the bitrate in kb/s or -1 if unknown
     * @since 0.8
     */
    public int getDeclaredBitRate() {
        return declaredBitRate;
    }


    /**
     * Plays a stream asynchronously.
     * This method starts a new thread.
     * @param url the URL of the stream or file
     */
    public void playAsync( final String url ) {
        playAsync( url, -1 );
    }


    /**
     * Plays a stream asynchronously.
     * This method starts a new thread.
     * @param url the URL of the stream or file
     * @param expectedKBitSecRate the expected average bitrate in kbit/sec; -1 means unknown
     */
    public void playAsync( final String url, final int expectedKBitSecRate ) {
        new Thread(new Runnable() {
            public void run() {
                try {
                    play( url, expectedKBitSecRate );
                }
                catch (Exception e) {
                    Log.e( LOG, "playAsync():", e);

                    if (playerCallback != null) playerCallback.playerException( e );
                }
            }
        }).start();
    }


    /**
     * Plays a stream synchronously.
     * @param url the URL of the stream or file
     */
    public void play( String url ) throws Exception {
        play( url, -1 );
    }


    /**
     * Plays a stream synchronously.
     * @param url the URL of the stream or file
     * @param expectedKBitSecRate the expected average bitrate in kbit/sec;
     *      -1 means unknown;
     *      when setting this parameter, then the declared bit-rate from the stream header is ignored
     */
    public void play( String url, int expectedKBitSecRate ) throws Exception {
        declaredBitRate = -1;

        if (url.indexOf( ':' ) > 0) {
            URLConnection cn = openConnection( url );
            InputStream is = null;

            try {
                if (responseCodeCheckEnabled) checkResponseCode( cn );
                processHeaders( cn );
                is = getInputStream( cn );

                // try to get the expectedKBitSecRate from headers
                // but if then expectedKBitSecRate is passed, then ignore the declared one:
                play( is, expectedKBitSecRate != -1 ? expectedKBitSecRate : declaredBitRate );
            }
            finally {
                try { is.close(); } catch (Throwable t) {}

                if (cn instanceof HttpURLConnection) {
                    try { ((HttpURLConnection)cn).disconnect(); } catch (Throwable t) {}
                }
            }
        }
        else {
            processFileType( url );
            InputStream is = new FileInputStream( url );

            try {
                play( is, expectedKBitSecRate );
            }
            finally {
                try { is.close(); } catch (Throwable t) {}
            }
        }
    }


    /**
     * Plays a stream synchronously.
     * @param is the input stream
     */
    public void play( InputStream is ) throws Exception {
        play( is, -1 );
    }


    /**
     * Plays a stream synchronously.
     * @param is the input stream
     * @param expectedKBitSecRate the expected average bitrate in kbit/sec; -1 means unknown
     */
    public final void play( InputStream is, int expectedKBitSecRate ) throws Exception {
        stopped = false;

        if (playerCallback != null) stopped = !playerCallback.playerStarted();

        if (expectedKBitSecRate <= 0) expectedKBitSecRate = DEFAULT_EXPECTED_KBITSEC_RATE;

        sumKBitSecRate = 0;
        countKBitSecRate = 0;

        playImpl( is, expectedKBitSecRate );
    }


    /**
     * Stops the execution thread.
     */
    public void stop() {
        stopped = true;
        if(pcmfeed != null) pcmfeed.stop();
    }


    ////////////////////////////////////////////////////////////////////////////
    // Protected
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Plays a stream synchronously.
     * This is the implementation method calle by every play() and playAsync() methods.
     * @param is the input stream
     * @param expectedKBitSecRate the expected average bitrate in kbit/sec
     */
    protected void playImpl( InputStream is, int expectedKBitSecRate ) throws Exception {
        BufferReader reader = new BufferReader(
                                        computeInputBufferSize( expectedKBitSecRate, decodeBufferCapacityMs ),
                                        is );
        new Thread( reader ).start();

        Thread pcmfeedThread = null;

        // profiling info
        long profMs = 0;
        long profSamples = 0;
        long profSampleRate = 0;
        int profCount = 0;

        try {
            Decoder.Info info = decoder.start( reader );

            Log.d( LOG, "play(): samplerate=" + info.getSampleRate() + ", channels=" + info.getChannels());

            profSampleRate = info.getSampleRate() * info.getChannels();

            if (info.getChannels() > 2) {
                throw new RuntimeException("Too many channels detected: " + info.getChannels());
            }

            // 3 buffers for result samples:
            //   - one is used by decoder
            //   - one is used by the PCMFeeder
            //   - one is enqueued / passed to PCMFeeder - non-blocking op
            short[][] decodeBuffers = createDecodeBuffers( 3, info );
            short[] decodeBuffer = decodeBuffers[0]; 
            int decodeBufferIndex = 0;

            pcmfeed = createPCMFeed( info );

            pcmfeed.setVolume(leftVolume, rightVolume);

            pcmfeedThread = new Thread( pcmfeed );
            pcmfeedThread.start();

            if (info.getFirstSamples() != null) {
                short[] firstSamples = info.getFirstSamples();
                Log.d( LOG, "First samples length: " + firstSamples.length );

                pcmfeed.feed( firstSamples, firstSamples.length );
                info.setFirstSamples( null );
            }

            do {
                long tsStart = System.currentTimeMillis();

                info = decoder.decode( decodeBuffer, decodeBuffer.length );
                int nsamp = info.getRoundSamples();

                profMs += System.currentTimeMillis() - tsStart;
                profSamples += nsamp;
                profCount++;

                Log.d( LOG, "play(): decoded " + nsamp + " samples" );

                if (nsamp == 0 || stopped) break;
                if (!pcmfeed.feed( decodeBuffer, nsamp ) || stopped) break;

                int kBitSecRate = computeAvgKBitSecRate( info );
                if (Math.abs(expectedKBitSecRate - kBitSecRate) > 1) {
                    Log.i( LOG, "play(): changing kBitSecRate: " + expectedKBitSecRate + " -> " + kBitSecRate );
                    reader.setCapacity( computeInputBufferSize( kBitSecRate, decodeBufferCapacityMs ));
                    expectedKBitSecRate = kBitSecRate;
                }

                if(true) {
//                    throw new Exception();
                }
                decodeBuffer = decodeBuffers[ ++decodeBufferIndex % 3 ];
            } while (!stopped);
        }
        finally {
            boolean stopImmediatelly = stopped;
            stopped = true;

            if (pcmfeed != null) pcmfeed.stop( !stopImmediatelly );
            decoder.stop();
            reader.stop();

            int perf = 0;

            if (profCount > 0) Log.i( LOG, "play(): average decoding time: " + profMs / profCount + " ms");

            if (profMs > 0) {
                perf = (int)((1000*profSamples / profMs - profSampleRate) * 100 / profSampleRate);

                Log.i( LOG, "play(): average rate (samples/sec): audio=" + profSampleRate
                    + ", decoding=" + (1000*profSamples / profMs)
                    + ", audio/decoding= " + perf
                    + " %  (the higher, the better; negative means that decoding is slower than needed by audio)");
            }

            if (pcmfeedThread != null) pcmfeedThread.join();

            // The other cause is an exception, handled in playAsync
            if(playerCallback != null) {
                if(stopImmediatelly) {
                    playerCallback.playerStopped( perf );
                }
                else {
                    playerCallback.playerException(new Exception("Streaming stopped unexpectedly"));
                }
            }
        }
    }


    protected Decoder createDecoder() {
        return Decoder.create();
    }


    protected short[][] createDecodeBuffers( int count, Decoder.Info info ) {
        int size = PCMFeed.msToSamples( decodeBufferCapacityMs, info.getSampleRate(), info.getChannels());

        short[][] ret = new short[ count ][];

        for (int i=0; i < ret.length; i++) {
            ret[i] = new short[ size ];
        }

        return ret;
    }


    protected PCMFeed createPCMFeed( Decoder.Info info ) {
        int size = PCMFeed.msToBytes( audioBufferCapacityMs, info.getSampleRate(), info.getChannels());

        PCMFeed newPcmFeed = new PCMFeed( info.getSampleRate(), info.getChannels(), size, playerCallback );
    return newPcmFeed;
    }



    /**
     * Opens connection.
     * Tries to recognize if the stream is a standard HTTP or SHOUTCAST.
     * Since Android 4.4 Kitkat the HttpURLConnection implementation is strict
     * and does not allow SHOUTCAST response "ICY 200 OK".
     * If we detect this, we try to use alternate protocol "icy" and 
     * our auxiliar implementation - IcyURLConnection.
     * NOTE: URL.setURLStreamHandlerFactory() must be called - this library does not call it
     * itself.
     */
    protected URLConnection openConnection( String url ) throws IOException {
        URLConnection conn = null;
        boolean close = true;

        while (true) {
            conn = new URL( url ).openConnection();

            prepareConnection( conn );
            conn.connect();

            try {
                if (conn instanceof HttpURLConnection) {
                    HttpURLConnection httpConn = (HttpURLConnection) conn;

                    try {
                        // pre-KitKat returns -1:
                        if (httpConn.getResponseCode() == -1) {
                            if (!responseCodeCheckEnabled) {
                                Log.w( LOG, "No response code, but ignoring - for url " + url );
                                close = false;
                                break;
                            }
                            else {
                                Log.w( LOG, "No response code for url " + url );
                            }
                        }
                        else {
                            // standard HTTP response / IcyURLConnection response
                            close = false;
                            break;
                        }
                    }
                    catch (Exception e) {
                        // KitKat throws exception:
                        // java.net.ProtocolException: Unexpected status line: ICY 200 OK
                        Log.w( LOG, "Invalid response code for url " + url + " - " + e );
                    }
                }
                else if (conn.getHeaderFields() == null) {
                    // sanity code
                    Log.w( LOG, "No header fields in response for url " + url );
                }
                else {
                    close = false;
                    break;
                }

                if (url.startsWith( "http:" )) {
                    url = "icy" + url.substring( 4 );
                    Log.i( LOG, "Trying to re-connect as ICY url " + url );
                }
                else throw new IOException( "Invalid response - no response code / headers detected" );
            }
            finally {
                if (close) {
                    if (conn instanceof HttpURLConnection) {
                        try { ((HttpURLConnection)conn).disconnect(); } catch (Throwable t) {}
                    }
                    conn = null;
                }
            }
        }

        return conn;
    }


    /**
     * Prepares the connection.
     * This method is called before a connection is opened.
     * Actually sets "Icy-MetaData" header to "1" if metadata are enabled.
     */
    protected void prepareConnection( URLConnection conn ) {
        // request for dynamic metadata:
        if (metadataEnabled) conn.setRequestProperty("Icy-MetaData", "1");
    }


    /**
     * Checks the response code.
     * Actually for HttpURLConnection it throws an exception
     * when the response code is not between 200 and 299.
     */
    protected void checkResponseCode( URLConnection conn ) throws Exception {
        if (conn instanceof HttpURLConnection) {
            HttpURLConnection httpConn = (HttpURLConnection) conn;

            int responseCode = httpConn.getResponseCode();

            if (responseCode == -1) {
                Log.w( LOG, "Empty response code: " + responseCode + " " + httpConn.getResponseMessage());
            }
            else if (responseCode < 200 || responseCode > 299) {
                Log.e( LOG, "Error response code: " + responseCode + " " + httpConn.getResponseMessage());
                throw new IOException( "Error response: " + responseCode + " " + httpConn.getResponseMessage());
            }
            else {
                Log.d( LOG, "Response: " + responseCode + " " + httpConn.getResponseMessage());
            }
        }
    }


    /**
     * Gets the input stream from the connection.
     * Actually returns the underlying stream or IcyInputStream.
     */
    protected InputStream getInputStream( URLConnection conn ) throws Exception {
        String smetaint = conn.getHeaderField( "icy-metaint" );
        InputStream ret = conn.getInputStream();

        if (!metadataEnabled) {
            Log.i( LOG, "Metadata not enabled" );
        }
        else if (smetaint != null) {
            int period = -1;
            try {
                period = Integer.parseInt( smetaint );
            }
            catch (Exception e) {
                Log.e( LOG, "The icy-metaint '" + smetaint + "' cannot be parsed: '" + e );
            }

            if (period > 0) {
                Log.i( LOG, "The dynamic metainfo is sent every " + period + " bytes" );

                ret = new IcyInputStream( ret, period, playerCallback, metadataCharEnc );
            }
        }
        else Log.i( LOG, "This stream does not provide dynamic metainfo" );

        return ret;
    }


    /**
     * This method is called after the connection is established.
     */
    protected void processHeaders( URLConnection cn ) {
        dumpHeaders( cn );

        String br = cn.getHeaderField( "icy-br" );

        if (br != null) {
            try {
                declaredBitRate = Integer.parseInt( br );

                if (declaredBitRate > 7) {
                    Log.d( LOG, "Declared bitrate is " + declaredBitRate + " kb/s" );
                }
                else {
                    Log.w( LOG, "Declared bitrate is too low - ignoring: " + declaredBitRate + " kb/s" );
                    declaredBitRate = -1;
                }
            }
            catch (Exception e) {
                Log.w( LOG, "Cannot parse declared bit-rate '" + br + "'" );
            }
        }

        if (playerCallback != null) {
            for (java.util.Map.Entry<String, java.util.List<String>> me : cn.getHeaderFields().entrySet()) {
                for (String s : me.getValue()) {
                    playerCallback.playerMetadata( me.getKey(), s );
                }
            }
        }
    }


    protected void dumpHeaders( URLConnection cn ) {
        if (cn.getHeaderFields() == null) {
            Log.d( LOG, "No headers - not an HTTP response ?" );
            return;
        }

        for (java.util.Map.Entry<String, java.util.List<String>> me : cn.getHeaderFields().entrySet()) {
            for (String s : me.getValue()) {
                Log.d( LOG, "header: key=" + me.getKey() + ", val=" + s);
            }
        }
    }


    /**
     * This method is called before opening the file.
     * Actually this method does nothing, but subclasses may override it.
     */
    protected void processFileType( String file ) {
    }


    protected int computeAvgKBitSecRate( Decoder.Info info ) {
        // do not change the value after a while - avoid changing of the out buffer:
        if (countKBitSecRate < 64) {
            int kBitSecRate = computeKBitSecRate( info );
            int frames = info.getRoundFrames();

            sumKBitSecRate += kBitSecRate * frames;
            countKBitSecRate += frames;
            avgKBitSecRate = sumKBitSecRate / countKBitSecRate;
        }

        return avgKBitSecRate;
    }


    protected static int computeKBitSecRate( Decoder.Info info ) {
        if (info.getRoundSamples() <= 0) return -1;

        return computeKBitSecRate( info.getRoundBytesConsumed(), info.getRoundSamples(),
                                   info.getSampleRate(), info.getChannels());
    }


    protected static int computeKBitSecRate( int bytesconsumed, int samples, int sampleRate, int channels ) {
        long ret = 8L * bytesconsumed * channels * sampleRate / samples;

        return (((int)ret) + 500) / 1000;
    }


    protected static int computeInputBufferSize( int kbitSec, int durationMs ) {
        return kbitSec * durationMs / 8;
    }


    protected static int computeInputBufferSize( Decoder.Info info, int durationMs ) {

        return computeInputBufferSize( info.getRoundBytesConsumed(), info.getRoundSamples(),
                                        info.getSampleRate(), info.getChannels(), durationMs );
    }


    protected static int computeInputBufferSize( int bytesconsumed, int samples,
                                                 int sampleRate, int channels, int durationMs ) {

        return (int)(((long) bytesconsumed) * channels * sampleRate * durationMs  / (1000L * samples));
    }

    public void setVolume(float leftVolume,float rightVolume) {
      if(pcmfeed != null) 
        pcmfeed.setVolume(leftVolume,rightVolume);
      this.leftVolume = leftVolume;
      this.rightVolume = rightVolume;
    }

}




Java Source Code List

com.commandocoder.streaming.StreamingException.java
com.commandocoder.streaming.StreamingListener.java
com.commandocoder.streaming.StreamingService.java
com.spoledge.aacdecoder.AACPlayer.java
com.spoledge.aacdecoder.BufferReader.java
com.spoledge.aacdecoder.Decoder.java
com.spoledge.aacdecoder.FlashAACInputStream.java
com.spoledge.aacdecoder.FlashAACPlayer.java
com.spoledge.aacdecoder.IcyInputStream.java
com.spoledge.aacdecoder.IcyURLConnection.java
com.spoledge.aacdecoder.IcyURLStreamHandler.java
com.spoledge.aacdecoder.MP3Player.java
com.spoledge.aacdecoder.MultiPlayer.java
com.spoledge.aacdecoder.PCMFeed.java
com.spoledge.aacdecoder.PlayerCallback.java