Extracting metadata from Icecast stream using Exop

2019-04-13 14:56发布

问题:

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

回答1:

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.



回答2:

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.



回答3:

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
    }


}


回答4:

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.