Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get Current Song Metadata on Shoutcast stream #466

Closed
ericcbm opened this issue May 18, 2015 · 14 comments
Closed

Get Current Song Metadata on Shoutcast stream #466

ericcbm opened this issue May 18, 2015 · 14 comments

Comments

@ericcbm
Copy link

ericcbm commented May 18, 2015

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);
  }
}
@ojw28
Copy link
Contributor

ojw28 commented May 18, 2015

This is not currently supported.

@ericcbm
Copy link
Author

ericcbm commented May 18, 2015

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.

@Ood-Tsen
Copy link
Contributor

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

@Ood-Tsen
Copy link
Contributor

A reference site for SmackKu: shoutcast Metadata metadata.

@tlenclos
Copy link

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

@geolyth
Copy link

geolyth commented Sep 4, 2016

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

@ahmedarif193
Copy link

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

@y20k
Copy link

y20k commented Mar 27, 2017

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?

@y20k
Copy link

y20k commented Mar 30, 2017

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.

@asheeshs
Copy link

@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;
        }
    }

}

@y20k
Copy link

y20k commented Jan 30, 2018

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

@asheeshs
Copy link

Glad it helped @y20k

@y20k
Copy link

y20k commented Jan 30, 2018

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

}

@ojw28
Copy link
Contributor

ojw28 commented May 9, 2018

Now tracked by #3735.

@ojw28 ojw28 closed this as completed May 9, 2018
@ojw28 ojw28 added the duplicate label May 9, 2018
@google google locked and limited conversation to collaborators Sep 11, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

8 participants