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);
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.
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.
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
}
}
Parsing the Shoutcast Metadata Protocol consists of two parts:
- 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
- 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.