diff --git a/build.gradle b/build.gradle
index a013f4fb84..f4eb7704a2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,6 +20,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.novoda:bintray-release:0.8.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.0.3'
+ classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.2.71'
}
// Workaround for the following test coverage issue. Remove when fixed:
// https://code.google.com/p/android/issues/detail?id=226070
diff --git a/demos/icy/README.md b/demos/icy/README.md
new file mode 100644
index 0000000000..77a54098e2
--- /dev/null
+++ b/demos/icy/README.md
@@ -0,0 +1,4 @@
+# ICY demo application #
+
+This folder contains a demo application that showcases the ExoPlayer Shoutcast
+Metadata (ICY) extension.
diff --git a/demos/icy/build.gradle b/demos/icy/build.gradle
new file mode 100644
index 0000000000..9ae88c445e
--- /dev/null
+++ b/demos/icy/build.gradle
@@ -0,0 +1,62 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+ buildToolsVersion project.ext.buildToolsVersion
+
+ compileOptions {
+ sourceCompatibility 1.8
+ targetCompatibility 1.8
+ }
+
+ defaultConfig {
+ versionName project.ext.releaseVersion
+ versionCode project.ext.releaseVersionCode
+ minSdkVersion 23
+ targetSdkVersion project.ext.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ shrinkResources true
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ debug {
+ jniDebuggable = true
+ }
+ }
+
+ lintOptions {
+ // The demo app does not have translations.
+ disable 'MissingTranslation'
+ }
+}
+
+dependencies {
+ implementation project(modulePrefix + 'extension-icy')
+ implementation 'com.android.support:appcompat-v7:28.0.0'
+ implementation 'com.android.support.constraint:constraint-layout:1.1.3'
+ implementation 'com.android.support:design:28.0.0'
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.71"
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.26.1'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.26.1'
+}
+
+apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
\ No newline at end of file
diff --git a/demos/icy/src/main/AndroidManifest.xml b/demos/icy/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..69d64dbf96
--- /dev/null
+++ b/demos/icy/src/main/AndroidManifest.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/icy/src/main/java/com/google/android/exoplayer2/icydemo/MainActivity.kt b/demos/icy/src/main/java/com/google/android/exoplayer2/icydemo/MainActivity.kt
new file mode 100644
index 0000000000..643ee84ed7
--- /dev/null
+++ b/demos/icy/src/main/java/com/google/android/exoplayer2/icydemo/MainActivity.kt
@@ -0,0 +1,160 @@
+package com.google.android.exoplayer2.icydemo
+
+import android.net.Uri
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import android.util.Log
+import com.google.android.exoplayer2.*
+import com.google.android.exoplayer2.audio.AudioAttributes
+import com.google.android.exoplayer2.ext.icy.IcyHttpDataSourceFactory
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
+import com.google.android.exoplayer2.source.ExtractorMediaSource
+import com.google.android.exoplayer2.source.TrackGroupArray
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
+import com.google.android.exoplayer2.util.Util
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.coroutines.experimental.CoroutineStart
+import kotlinx.coroutines.experimental.Dispatchers
+import kotlinx.coroutines.experimental.GlobalScope
+import kotlinx.coroutines.experimental.async
+import okhttp3.OkHttpClient
+
+/**
+ * Test application, doesn't necessarily show the best way to do things.
+ */
+class MainActivity : AppCompatActivity() {
+ private var exoPlayer: SimpleExoPlayer? = null
+ private val exoPlayerEventListener = ExoPlayerEventListener()
+ private lateinit var userAgent: String
+ private var isPlaying = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ stream.setText(DEFAULT_STREAM)
+
+ userAgent = Util.getUserAgent(applicationContext, applicationContext.getString(R.string.app_name))
+
+ play_pause.setOnClickListener {
+ if (isPlaying) {
+ stop()
+ play_pause.setImageDrawable(resources.getDrawable(R.drawable.ic_play_arrow_black_24dp, null))
+ } else {
+ play()
+ play_pause.setImageDrawable(resources.getDrawable(R.drawable.ic_stop_black_24dp, null))
+ }
+ }
+ }
+
+ private fun play() {
+ GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT, null, {
+ if (exoPlayer == null) {
+ exoPlayer = ExoPlayerFactory.newSimpleInstance(applicationContext,
+ DefaultRenderersFactory(applicationContext),
+ DefaultTrackSelector(),
+ DefaultLoadControl()
+ )
+ exoPlayer?.addListener(exoPlayerEventListener)
+ }
+
+ val audioAttributes = AudioAttributes.Builder()
+ .setContentType(C.CONTENT_TYPE_MUSIC)
+ .setUsage(C.USAGE_MEDIA)
+ .build()
+ exoPlayer?.audioAttributes = audioAttributes
+
+ // Custom HTTP data source factory which requests Icy metadata and parses it if
+ // the stream server supports it
+ val client = OkHttpClient.Builder().build()
+ val icyHttpDataSourceFactory = IcyHttpDataSourceFactory.Builder(client)
+ .setUserAgent(userAgent)
+ .setIcyHeadersListener { icyHeaders ->
+ Log.d(TAG, "onIcyMetaData: icyHeaders=$icyHeaders")
+ }
+ .setIcyMetadataChangeListener { icyMetadata ->
+ Log.d(TAG, "onIcyMetaData: icyMetadata=$icyMetadata")
+ }
+ .build()
+
+ // Produces DataSource instances through which media data is loaded
+ val dataSourceFactory = DefaultDataSourceFactory(
+ applicationContext, null, icyHttpDataSourceFactory
+ )
+ // Produces Extractor instances for parsing the media data
+ val extractorsFactory = DefaultExtractorsFactory()
+
+ // The MediaSource represents the media to be played
+ val mediaSource = ExtractorMediaSource.Factory(dataSourceFactory)
+ .setExtractorsFactory(extractorsFactory)
+ .createMediaSource(Uri.parse(stream.text.toString()))
+
+ // Prepares media to play (happens on background thread) and triggers
+ // {@code onPlayerStateChanged} callback when the stream is ready to play
+ exoPlayer?.prepare(mediaSource)
+ })
+ }
+
+ private fun stop() {
+ releaseResources(true)
+ isPlaying = false
+ }
+
+ private fun releaseResources(releasePlayer: Boolean) {
+ Log.d(TAG, "releaseResources: releasePlayer=$releasePlayer")
+
+ // Stops and releases player (if requested and available).
+ if (releasePlayer && exoPlayer != null) {
+ exoPlayer?.release()
+ exoPlayer?.removeListener(exoPlayerEventListener)
+ exoPlayer = null
+ }
+ }
+
+ private inner class ExoPlayerEventListener : Player.EventListener {
+ override fun onTimelineChanged(timeline: Timeline, manifest: Any?, reason: Int) {
+ }
+
+ override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {
+ }
+
+ override fun onLoadingChanged(isLoading: Boolean) {
+ }
+
+ override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
+ Log.i(TAG, "onPlayerStateChanged: playWhenReady=$playWhenReady, playbackState=$playbackState")
+ when (playbackState) {
+ Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY ->
+ isPlaying = true
+ Player.STATE_ENDED ->
+ stop()
+ }
+ }
+
+ override fun onPlayerError(error: ExoPlaybackException) {
+ Log.e(TAG, "onPlayerStateChanged: error=$error")
+ }
+
+ override fun onPositionDiscontinuity(reason: Int) {
+ }
+
+ override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {
+ }
+
+ override fun onSeekProcessed() {
+ }
+
+ override fun onRepeatModeChanged(repeatMode: Int) {
+ }
+
+ override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
+ }
+ }
+
+ companion object {
+ private const val TAG = "MainActivity"
+ private const val DEFAULT_STREAM = "http://ice1.somafm.com/indiepop-128-mp3"
+ }
+}
diff --git a/demos/icy/src/main/res/drawable-v24/ic_launcher_foreground.xml b/demos/icy/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..1f6bb29060
--- /dev/null
+++ b/demos/icy/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/icy/src/main/res/drawable/ic_launcher_background.xml b/demos/icy/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000000..0d025f9bf6
--- /dev/null
+++ b/demos/icy/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/icy/src/main/res/drawable/ic_play_arrow_black_24dp.xml b/demos/icy/src/main/res/drawable/ic_play_arrow_black_24dp.xml
new file mode 100644
index 0000000000..bf9b895aca
--- /dev/null
+++ b/demos/icy/src/main/res/drawable/ic_play_arrow_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/demos/icy/src/main/res/drawable/ic_stop_black_24dp.xml b/demos/icy/src/main/res/drawable/ic_stop_black_24dp.xml
new file mode 100644
index 0000000000..c428d728dd
--- /dev/null
+++ b/demos/icy/src/main/res/drawable/ic_stop_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/demos/icy/src/main/res/layout/activity_main.xml b/demos/icy/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000000..14f3598479
--- /dev/null
+++ b/demos/icy/src/main/res/layout/activity_main.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/icy/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/demos/icy/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..eca70cfe52
--- /dev/null
+++ b/demos/icy/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/icy/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/demos/icy/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000..eca70cfe52
--- /dev/null
+++ b/demos/icy/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/icy/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/icy/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..898f3ed59a
Binary files /dev/null and b/demos/icy/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/icy/src/main/res/mipmap-hdpi/ic_launcher_round.png b/demos/icy/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..dffca3601e
Binary files /dev/null and b/demos/icy/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/demos/icy/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/icy/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..64ba76f75e
Binary files /dev/null and b/demos/icy/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/demos/icy/src/main/res/mipmap-mdpi/ic_launcher_round.png b/demos/icy/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..dae5e08234
Binary files /dev/null and b/demos/icy/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/demos/icy/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/icy/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..e5ed46597e
Binary files /dev/null and b/demos/icy/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/demos/icy/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/demos/icy/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..14ed0af350
Binary files /dev/null and b/demos/icy/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/demos/icy/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/icy/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..b0907cac3b
Binary files /dev/null and b/demos/icy/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/icy/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/demos/icy/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..d8ae031549
Binary files /dev/null and b/demos/icy/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/demos/icy/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/icy/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..2c18de9e66
Binary files /dev/null and b/demos/icy/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/demos/icy/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/demos/icy/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..beed3cdd2c
Binary files /dev/null and b/demos/icy/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/demos/icy/src/main/res/values/colors.xml b/demos/icy/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..e7185c4951
--- /dev/null
+++ b/demos/icy/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+
+
+
+ #008577
+ #00574B
+ #D81B60
+
+
diff --git a/demos/icy/src/main/res/values/strings.xml b/demos/icy/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..6f87ce75c7
--- /dev/null
+++ b/demos/icy/src/main/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Icy-Ext Example
+ Audio stream
+
+
diff --git a/demos/icy/src/main/res/values/styles.xml b/demos/icy/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..5885930df6
--- /dev/null
+++ b/demos/icy/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/settings.gradle b/settings.gradle
index d4530d67b7..2e0842fde2 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -20,10 +20,12 @@ if (gradle.ext.has('exoplayerModulePrefix')) {
include modulePrefix + 'demo'
include modulePrefix + 'demo-cast'
+include modulePrefix + 'demo-icy'
include modulePrefix + 'demo-ima'
include modulePrefix + 'playbacktests'
project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main')
project(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast')
+project(modulePrefix + 'demo-icy').projectDir = new File(rootDir, 'demos/icy')
project(modulePrefix + 'demo-ima').projectDir = new File(rootDir, 'demos/ima')
project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests')