Exoplayer: Get Current Song Metadata on Shoutcast stream

Created on 18 May 2015  路  14Comments  路  Source: google/ExoPlayer

I'm trying to get current song metadata on a shoutcats stream.
http://mobil.metal-only.de:8000/
I've tryed with TextListner and Id3MetadataListener with no success. :(

Some advice?
There is my ExtractorRendererBuilder:

public class ExtractorRendererBuilder implements RendererBuilder {

  private static final int BUFFER_SIZE = 10 * 1024 * 1024;

  private final String userAgent;
  private final Uri uri;
  private final Extractor extractor;

  public ExtractorRendererBuilder(String userAgent, Uri uri,
                                  Extractor extractor) {
    this.userAgent = userAgent;
    this.uri = uri;
    this.extractor = extractor;
  }

  @Override
  public void buildRenderers(Player player, RendererBuilderCallback callback) {
    // Build the video and audio renderers.
    DataSource dataSource = new DefaultUriDataSource(userAgent, null);
    ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, extractor, 2,
        BUFFER_SIZE);
    MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
        null, true, player.getMainHandler(), player);

    // Invoke the callback.
    TrackRenderer[] renderers = new TrackRenderer[Player.RENDERER_COUNT];
    renderers[Player.TYPE_AUDIO] = audioRenderer;
    callback.onRenderers(null, null, renderers);
  }
}
duplicate enhancement low priority

Most helpful comment

PS. Had anyone here success with implementing the patch in an v2 based app?

UPDATE: Shoutcast extraction seems to work for me in a v2 based app. I had to create a CustomDefaultHttpDataSourceFactory based on the official DefaultHttpDataSourceFactory.

I was able to use IcyDataSource.java - the custom DefaultHttpDataSource from @Ood-Tsen that extracts Shoutcast Metadata using the IcyInputStream.java from AACDecoder.

The only problem with this: I had to make changes to the DefaultHttpDataSource.java - @Ood-Tsen did the same. These changes had to be made inside the ExoPlayer library itself, which makes me a bit uncomfortable.

I hope that in the future there is a better solution for this

  • be it in the form of official shoutcast support 馃憤
  • or in the form of a changed DefaultHttpDataSource.java that allows custom versions like IcyDataSource.java.

It would be great not to have to mess with the core code of ExoPlayer.

All 14 comments

This is not currently supported.

Thanks for fast answer and for added to enhancement label. I think it's a good feature for radio stream apps.
The aacdecoder-android library does that, maybe it helps.
https://github.com/vbartacek/aacdecoder-android

See: https://github.com/vbartacek/aacdecoder-android/blob/master/decoder/src/com/spoledge/aacdecoder/IcyInputStream.java
It calls fetchMetadata() method on IcyInputStream read() and read( byte[] buffer, int offset, int len ) methods.

I'm just a newbie, but i'm going to fork ExoPlayer and try that.

Hi @ericcbm ,
I just upload a patch for IcyDataSource

It seems doable to re-used the IcyInputStream you mentioned. This update will dump the meta into log.

But need to replace the IcyInputStream and PlayerCallback in your version

A reference site for SmackKu: shoutcast Metadata metadata.

Any progress of any kind of this ? @Ood-Tsen @ericcbm

So it is not possible to obtain MetaData for shoutcast stream?

yes it is possible to get metadata using aacdecoder-android but you will need to change the android Mediaplayer with the aac one, it worked for me
the next work for me is to extract this feature to be standalone

The patch @Ood-Tsen provided works with AAC and it also works great with MP3 streams (-> my use case). The patch is based on ExoPlayer v1 though. I tried to adapt the code to work with ExoPlayer v2, but I was not able to get it working.

Anyway. I would really like to see an official support for Shoutcast. Therefore: 馃憤 from me for this issue.

PS. Had anyone here success with implementing the patch in an v2 based app?

PS. Had anyone here success with implementing the patch in an v2 based app?

UPDATE: Shoutcast extraction seems to work for me in a v2 based app. I had to create a CustomDefaultHttpDataSourceFactory based on the official DefaultHttpDataSourceFactory.

I was able to use IcyDataSource.java - the custom DefaultHttpDataSource from @Ood-Tsen that extracts Shoutcast Metadata using the IcyInputStream.java from AACDecoder.

The only problem with this: I had to make changes to the DefaultHttpDataSource.java - @Ood-Tsen did the same. These changes had to be made inside the ExoPlayer library itself, which makes me a bit uncomfortable.

I hope that in the future there is a better solution for this

  • be it in the form of official shoutcast support 馃憤
  • or in the form of a changed DefaultHttpDataSource.java that allows custom versions like IcyDataSource.java.

It would be great not to have to mess with the core code of ExoPlayer.

@y20k I had the similar task to extract Shoutcast metadata in my project, below is how I achieved it.

Created IcyDataSource and then used it to create MediaSource

```
new ExtractorMediaSource.Factory(
new DefaultDataSourceFactory(
context,
new DefaultBandwidthMeter(),
() -> new IcyDataSource(new IcyPlayerCallback())))
.createMediaSource(uri, handler, listener);

Below is how I Implemented IcyDataSource

public class IcyDataSource implements DataSource {
private final static String TAG = IcyDataSource.class.getSimpleName();
private final PlayerCallback playerCallback;

private HttpURLConnection connection;
private InputStream inputStream;
boolean metadataEnabled = true;

public IcyDataSource(final PlayerCallback playerCallback) {
    this.playerCallback = playerCallback;
}

@Override public long open(final DataSpec dataSpec) throws IOException {
    Log.i(TAG, "open[" + dataSpec.position + "-" + dataSpec.length);
    URL url = new URL(dataSpec.uri.toString());
    connection = (HttpURLConnection) url.openConnection();
    connection.setRequestProperty("Icy-Metadata", "1");

    try {
        inputStream = getInputStream(connection);
    } catch (Exception e) {
        closeConnectionQuietly();
        throw new IOException(e.getMessage());
    }

    return dataSpec.length;
}

@Override public int read(final byte[] buffer, final int offset, final int readLength) throws
                                                                                       IOException {
    return inputStream.read(buffer, offset, readLength);
}

@Override public Uri getUri() {
    return connection == null ? null : Uri.parse(connection.getURL().toString());
}

@Override public void close() throws IOException {
    try {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                throw new IOException(e.getMessage());
            }
        }
    } finally {
        inputStream = null;
        closeConnectionQuietly();
    }
}

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

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

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

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

    return ret;
}

/**
 * Closes the current connection quietly, if there is one.
 */
private void closeConnectionQuietly() {
    if (connection != null) {
        try {
            connection.disconnect();
        } catch (Exception e) {
            Log.e(TAG, "Unexpected error while disconnecting", e);
        }
        connection = null;
    }
}

}
```

Thank you for posting your solution. That is way more elegant. No need to edit/customize Exoplayer'sDefaultHttpDataSource.java anymore.

Glad it helped @y20k

If that is helpful to anyone... Here is the IcyDataSourceFactory that I use with the IcyDataSource provided by @asheeshs. It is a slight modification of DefaultDataSourceFactory. It can be used as a replacement for DefaultDataSourceFactory when preparing the player as described in the ExoPlayer Developer guide. You would need to create a PlayerCallback that reacts to metadata changes to make this work.

import android.content.Context;

import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSource.Factory;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.TransferListener;


/**
 * IcyDataSourceFactory.class
 */
public final class IcyDataSourceFactory implements Factory {

    /* Main class variables */
    private final Context context;
    private final TransferListener<? super DataSource> listener;
    private final DataSource.Factory baseDataSourceFactory;
    private boolean enableShoutcast = false;
    private PlayerCallback playerCallback;


    /* Constructor */
    public IcyDataSourceFactory(Context context,
                                String userAgent,
                                boolean enableShoutcast,
                                PlayerCallback playerCallback) {
        // use next Constructor
        this(context,
             userAgent,
             null,
             enableShoutcast,
             playerCallback);
    }


    /* Constructor */
    public IcyDataSourceFactory(Context context,
                                String userAgent,
                                TransferListener<? super DataSource> listener,
                                boolean enableShoutcast,
                                PlayerCallback playerCallback) {
        // use next Constructor
        this(context,
             listener,
             new DefaultHttpDataSourceFactory(userAgent, listener),
             enableShoutcast,
             playerCallback);
    }


    /* Constructor */
    public IcyDataSourceFactory(Context context,
                                TransferListener<? super DataSource> listener,
                                DataSource.Factory baseDataSourceFactory,
                                boolean enableShoutcast,
                                PlayerCallback playerCallback) {
        this.context = context.getApplicationContext();
        this.listener = listener;
        this.baseDataSourceFactory = baseDataSourceFactory;
        this.enableShoutcast = enableShoutcast;
        this.playerCallback = playerCallback;
    }


    @Override
    public DataSource createDataSource() {
        // toggle Shoutcast extraction
        if (enableShoutcast) {
            return new IcyDataSource(playerCallback);
        } else {
            return new DefaultDataSource(context, listener, baseDataSourceFactory.createDataSource());
        }
    }

}
Was this page helpful?
0 / 5 - 0 ratings