2

Since switching from Mediaplayer to a simple implementation Exoplayer I have noticed much improved load times but I'm wondering if there is any built in functionality such as a metadata change listener when streaming audio?

I have implemented Exoplayer using a simple example as below:

    Uri uri = Uri.parse(url);
    DefaultSampleSource sampleSource =
            new DefaultSampleSource(new FrameworkSampleExtractor(context, uri, null), 2);
    TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
    mExoPlayerInstance.prepare(audioRenderer);
    mExoPlayerInstance.setPlayWhenReady(true);
Jaz
  • 371
  • 1
  • 6
  • 20
  • hi Jaz, any way you can share what you did to upgrade from mediaplayer to exoplayer? thank you – alexistkd Jan 20 '16 at 16:30
  • Hi alexistkd, the I posted the code to use the Exoplayer below. No solution on the metadata for the stream but it works well as far as playing a stream. – Jaz Jan 20 '16 at 19:45
  • Would you kindly post the metadata in the header? Perhaps the metadata is in the stream and there's an indicator like 'icy-metaint:16000' which is what I have for an icecast stream URL - AACPlayer library can decode the song title from such metadata within stream sent every other 16000 bytes and I'm trying to figure out how to replicate that with ExoPlayer. – st_bk Sep 05 '16 at 14:24
  • Actually, it looks like someone made a library https://github.com/vsmaks/audiostream-metadata-retriever It'll create a separate URLConnection though. Perhaps it's possible to adapt some code into your ExoPlayer implementation and keep at one. But using this library would be a quick solution. – st_bk Sep 05 '16 at 15:11
  • Originally I had created another stream to read the data and extract the metadata but was concerned about data usage for users on non cellular connections. If that's not an issue then the second connection works. – Jaz Sep 05 '16 at 20:33

5 Answers5

6

Icy metadata support is now in exoplayer version 2.10:

ExoPlayerFactory.newSimpleInstance(this).apply {
    setAudioAttributes(
      AudioAttributes.Builder()
        .setContentType(C.CONTENT_TYPE_MUSIC)
        .setUsage(C.USAGE_MEDIA)
        .build(), true
    )
    addMetadataOutput(object : MetadataOutput {
      override fun onMetadata(metadata: Metadata) {
        for (n in 0 until metadata.length()) {
          when (val md = metadata[n]) {
            is com.google.android.exoplayer2.metadata.icy.IcyInfo -> {
              Log.d(TAG, "Title: ${md.title} URL: ${md.url}")
            }
            else -> {
              Log.d(TAG, "Some other sort of metadata: $md")
            }
          }
        }
      }
    })
  }
Dan Brough
  • 2,745
  • 1
  • 24
  • 24
4

I have an AsyncTask that starts ExoPlayer from an IceCast Stream:

OkHttpClient okHttpClient = new OkHttpClient();

UriDataSource uriDataSource = new OkHttpDataSource(okHttpClient, userAgent, null, null, CacheControl.FORCE_NETWORK);
((OkHttpDataSource) uriDataSource).setRequestProperty("Icy-MetaData", "1");
((OkHttpDataSource) uriDataSource).setPlayerCallback(mPlayerCallback);

DataSource dataSource = new DefaultUriDataSource(context, null, uriDataSource);

ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator,
                    BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE);


MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
MediaCodecSelector.DEFAULT, null, true, null, null,
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
mPlayerCallback.playerStarted();
exoPlayer.prepare(audioRenderer);

OkHttpDataSource is the class that implements HttpDataSource using OkHttpClient. It creates InputStream as a response from a request. I included this class from AACDecoder library https://github.com/vbartacek/aacdecoder-android/blob/master/decoder/src/com/spoledge/aacdecoder/IcyInputStream.java and replace InputStream with IcyInputStream depending on the response:

(In open() of OkHttpDataSource)

try {
  response = okHttpClient.newCall(request).execute();
  responseByteStream = response.body().byteStream();

  String icyMetaIntString = response.header("icy-metaint");
  int icyMetaInt = -1;
  if (icyMetaIntString != null) {
    try {
      icyMetaInt = Integer.parseInt(icyMetaIntString);
      if (icyMetaInt > 0)
        responseByteStream = new IcyInputStream(responseByteStream, icyMetaInt, playerCallback);
    } catch (Exception e) {
      Log.e(TAG, "The icy-metaint '" + icyMetaInt + "' cannot be parsed: '" + e);
    }
  }

} catch (IOException e) {
  throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
      dataSpec);
}

Now IcyInputStream can catch the medatada and invoke callback object (playerCallback here). PlayerCallback is also from the AACDecoder library: https://github.com/vbartacek/aacdecoder-android/blob/b58c519a341340a251f3291895c76ff63aef5b94/decoder/src/com/spoledge/aacdecoder/PlayerCallback.java

This way you are not making any duplicate stream and it is singular. If you don't want to have AACDecoder library in your project, then you can just copy needed files and include them directly in your project.

st_bk
  • 326
  • 4
  • 8
  • Thanks for posting st_bk. I willtry this when I get a chance. – Jaz Oct 03 '16 at 17:40
  • @st_bk could you post the full source for this somewhere? ((OkHttpDataSource) uriDataSource).setPlayerCallback is an unknown method when I try to replicate this. – codeman Oct 13 '16 at 16:34
  • @codeman It's just a setter for PlayerCallback object, that is a part of the AACDecoder library and it is just set to pass to IcyInputStream constructor. – st_bk Oct 17 '16 at 08:31
  • where's that setter defined? i don't see a setter in OkHttpDataSource. i'm getting it from here: compile 'com.google.android.exoplayer:extension-okhttp:r2.0.1' – codeman Oct 21 '16 at 14:10
  • nevermind i got it working. realized i had to modify the source of OkHttpDataSource to get this to work. was hoping i could just extend it but apparently response is private. – codeman Oct 22 '16 at 17:31
  • 1
    This doesn't seem to work with exoplayer2. I get errors with UriDataSource, ExtractorSampleSource and MediaCodecAudioTrackRenderer. Does anyone have en updated example for exoplayer2? – Johan Hovda Apr 17 '17 at 12:16
1

Parsing the Shoutcast Metadata Protocol consists of two parts:

  1. Telling the server that your client supports meta-data by sending the HTTP-Header Icy-Metadata:1, for example:

curl -v -H "Icy-MetaData:1" http://ice1.somafm.com/defcon-128-mp3

  1. Parsing the meta-data from the stream

Part one can be done without OkHttp based on ExoPlayer 2.6.1 (in Kotlin):

// Custom HTTP data source factory with IceCast metadata HTTP header set
val defaultHttpDataSourceFactory = DefaultHttpDataSourceFactory(userAgent, null)
defaultHttpDataSourceFactory.setDefaultRequestProperty("Icy-MetaData", "1")

// Produces DataSource instances through which media data is loaded.
val dataSourceFactory = DefaultDataSourceFactory(
    applicationContext, null, defaultHttpDataSourceFactory)

Part two is more involved and posting all the code is a little to much. You might want to take a look at the ExoPlayer2 extension I created instead:

github.com/saschpe/android-exoplayer2-ext-icy

It does not depend on OkHttp and is used in my Soma FM streaming radio application for Android called Alpha+ Player.

saschpe
  • 865
  • 8
  • 8
0

This will depend on a few factors (like stream format), but the short answer is no. Most browsers don't expose this. There is an out-of-band metadata approach though.

If the Icecast server from which you are getting this stream is running version 2.4.1 or newer, then you can query the metadata from its JSON API though. Basically by querying http://icecast.example.org/status.json or if you want info for only one specific stream: http://icecast.example.org/status.json?mount=/stream.ogg

This can be worked into older versions of Icecast, but then the API output needs to be cached by the webserver hosting the webpage/player or with CORS ACAO support.

TBR
  • 2,790
  • 1
  • 12
  • 22
  • Thanks for the reply. I am able to send the request for the metadata in the header to receive it in the stream which I can parse out of the stream but that also requires a separate call. I was hoping Exoplayer contained a separate listener which would be triggered on changes to the metadata in the stream. Separate calls to either the stream just for metadata or to the API eat up more of the user's data and I have found the API on the server I'm using can but a bit out of sync with the music being played. Not sure if including the metadata would cause a blip when it hits the metadata. – Jaz May 08 '15 at 14:52
  • I'm not familiar with that player in particular. If you are handling a legacy format stream with the shoutcast style metadata injection, then you'd need to make sure to remove the metadata from the stream before it gets decoded, else you'd likely end up with garbage. – TBR May 08 '15 at 14:58
  • Just noticed that this was a Java not Javascript question. Sorry for the confusion. – TBR May 08 '15 at 15:01
  • Thanks, your answer still applies for many aspects of the situation. I wasn't sure if Exoplayer can handle a stream containing metadata as similar to AVPlayer in IOS which contains a metadata object you have access to. – Jaz May 08 '15 at 15:33
0

Posting to show the implementation that worked for me. Just a Singleton with start and stop methods and some intents to update the UI.

private void startStation(Station station){
if(station!=null) {
  ExoPlayerSingleton.getInstance();
  ExoPlayerSingleton.playStation(station, getApplicationContext());
 }
}


public class ExoPlayerSingleton {

private static ExoPlayer mExoPlayerInstance;
private static MediaCodecAudioTrackRenderer audioRenderer;
private static final int BUFFER_SIZE = 10 * 1024 * 1024;
private static MediaPlayer mediaPlayer;
public static synchronized ExoPlayer getInstance() {


    if (mExoPlayerInstance == null) {
        mExoPlayerInstance = ExoPlayer.Factory.newInstance(1);
    }

    return mExoPlayerInstance;
}

 public static synchronized ExoPlayer getCurrentInstance() {
    return mExoPlayerInstance;
}

public static void  stopExoForStation(Context context){

    if(mExoPlayerInstance!=null) {
        try {

            mExoPlayerInstance.stop();
            mExoPlayerInstance.release();
            mExoPlayerInstance = null;
            Intent intent = new Intent();
            intent.setAction("com.zzz.now_playing_receiver");
            context.sendBroadcast(intent);
        } catch (Exception e) {
            Log.e("Exoplayer Error", e.toString());
        }

    }
}


public static boolean isPlaying(){

    if(mExoPlayerInstance!=null &&(mExoPlayerInstance.getPlaybackState()==       ExoPlayer.STATE_READY )){
        return true;
    }else{
        return false;
    }
}

public static boolean isBuffering(){

    if(mExoPlayerInstance!=null &&(mExoPlayerInstance.getPlaybackState()== ExoPlayer.STATE_BUFFERING)){
        return true;
    }else{
        return false;
    }
}

public static boolean isPreparing(){

    if(mExoPlayerInstance!=null &&( mExoPlayerInstance.getPlaybackState()== ExoPlayer.STATE_PREPARING)){
        return true;
    }else{
        return false;
    }
}

public static void playStation(Station station,final Context context){

    getInstance();
    url = station.getLow_Stream();

    if(url!=null) {
        Uri uri = Uri.parse(url);
        String userAgent = Util.getUserAgent(context, "SomeRadio");
        DataSource audioDataSource = new DefaultUriDataSource(context,userAgent);
        Mp3Extractor extractor = new Mp3Extractor();
                ExtractorSampleSource sampleSource = new ExtractorSampleSource(
                uri, audioDataSource,BUFFER_SIZE, extractor );

        audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);


        mExoPlayerInstance.addListener(new ExoPlayer.Listener() {
            @Override
            public void onPlayerStateChanged(boolean b, int i) {

                if (i == ExoPlayer.STATE_BUFFERING) {


                } else if (i == ExoPlayer.STATE_IDLE) {

                } else if (i == ExoPlayer.STATE_ENDED) {


                } else if (i == ExoPlayer.STATE_READY) {
                    Intent intent = new Intent();
                    intent.setAction("com.zzz.pause_play_update");
                    context.sendBroadcast(intent);

                    Intent progress_intent = new Intent();
                    progress_intent.putExtra("show_dialog", false);
                    progress_intent.setAction("com.zzz.load_progess");
                    context.sendBroadcast(progress_intent);
                }


            }

            @Override
            public void onPlayWhenReadyCommitted() {

                 }

            @Override
            public void onPlayerError(ExoPlaybackException e) {
                String excep =  e.toString();
                Log.e("ExoPlayer Error",excep);

            }
        });
        mExoPlayerInstance.prepare(audioRenderer);
        mExoPlayerInstance.setPlayWhenReady(true);

    }else{
        //send intent to raise no connection dialog
    }


}
Jaz
  • 371
  • 1
  • 6
  • 20