diff --git a/core_settings.gradle b/core_settings.gradle index 4d90fa962a..df15edba29 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -28,6 +28,7 @@ include modulePrefix + 'testutils-robolectric' include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-flac' include modulePrefix + 'extension-gvr' +include modulePrefix + 'extension-icy' include modulePrefix + 'extension-ima' include modulePrefix + 'extension-cast' include modulePrefix + 'extension-cronet' @@ -50,6 +51,7 @@ project(modulePrefix + 'testutils-robolectric').projectDir = new File(rootDir, ' project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') +project(modulePrefix + 'extension-icy').projectDir = new File(rootDir, 'extensions/icy') project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima') project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast') project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet') diff --git a/extensions/icy/README.md b/extensions/icy/README.md new file mode 100644 index 0000000000..34ffe762e3 --- /dev/null +++ b/extensions/icy/README.md @@ -0,0 +1,67 @@ +# ExoPlayer Shoutcast Metadata Protocol (ICY) extension # +The Shoutcast Metadata Protocol extension provides **IcyHttpDataSource** and +**IcyHttpDataSourceFactory** which can parse ICY metadata information such as +stream name and genre as well as current song information from a music stream. + +You can find the protocol description here: + +- https://cast.readme.io/v1.0/docs/icy +- http://www.smackfu.com/stuff/programming/shoutcast.html + +## Getting the extension ## + +The easiest way to use the extension is to add it as a gradle dependency: + +```gradle +implementation 'com.google.android.exoplayer:extension-icy:2.X.X' +``` + +where `2.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Using the extension ## + +To receive information about the current music stream (such as name and genre, +see **IcyHeaders** class) as well as current song information (see +**IcyMetadata** class), pass an instance of **IcyHttpDataSourceFactory** instead +of an **DefaultHttpDataSourceFactory** like this (in Kotlin): + +```kotlin +// ... exoPlayer instance already created + +// 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("XXX", "onIcyHeaders: %s".format(icyHeaders.toString())) + } + .setIcyMetadataChangeListener { icyMetadata -> + Log.d("XXX", "onIcyMetaData: %s".format(icyMetadata.toString())) + } + .build() + +// Produces DataSource instances through which media data is loaded +val dataSourceFactory = DefaultDataSourceFactory(applicationContext, null, icyHttpDataSourceFactory) + +// The MediaSource represents the media to be played +val mediaSource = ExtractorMediaSource.Factory(dataSourceFactory) + .setExtractorsFactory(DefaultExtractorsFactory()) + .createMediaSource(sourceUri) + +// exoPlayer?.prepare(mediaSource) ... +``` + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.icy.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/icy/build.gradle b/extensions/icy/build.gradle new file mode 100644 index 0000000000..0bff281865 --- /dev/null +++ b/extensions/icy/build.gradle @@ -0,0 +1,52 @@ +// 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.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + consumerProguardFiles 'proguard-rules.txt' + } +} + +dependencies { + implementation project(modulePrefix + 'extension-okhttp') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + api 'com.google.android.exoplayer:extension-okhttp:' + releaseVersion + testImplementation 'junit:junit:' + junitVersion + testImplementation project(modulePrefix + 'testutils-robolectric') +} + +ext { + javadocTitle = 'Shoutcast Metadata Protocol (ICY) extension' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-icy' + releaseDescription = 'Shoutcast Metadata Protocol (ICY) extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/icy/proguard-rules.txt b/extensions/icy/proguard-rules.txt new file mode 100644 index 0000000000..950555d877 --- /dev/null +++ b/extensions/icy/proguard-rules.txt @@ -0,0 +1,2 @@ +# Proguard rules specific to the Icy extension. + diff --git a/extensions/icy/src/main/AndroidManifest.xml b/extensions/icy/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..4f19052a68 --- /dev/null +++ b/extensions/icy/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/extensions/icy/src/main/java/com/google/android/exoplayer2/ext/icy/IcyHttpDataSource.java b/extensions/icy/src/main/java/com/google/android/exoplayer2/ext/icy/IcyHttpDataSource.java new file mode 100644 index 0000000000..c6728219c9 --- /dev/null +++ b/extensions/icy/src/main/java/com/google/android/exoplayer2/ext/icy/IcyHttpDataSource.java @@ -0,0 +1,387 @@ +package com.google.android.exoplayer2.ext.icy; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.Predicate; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import okhttp3.CacheControl; +import okhttp3.Call; + +/** + * https://cast.readme.io/v1.0/docs/icy http://www.smackfu.com/stuff/programming/shoutcast.html + */ +public final class IcyHttpDataSource extends OkHttpDataSource { + + private static final String TAG = IcyHttpDataSource.class.getSimpleName(); + + private static final String REQUEST_HEADER_ICY_METAINT_KEY = "Icy-MetaData"; + private static final String REQUEST_HEADER_ICY_METAINT_VALUE = "1"; + + private static final String RESPONSE_HEADER_ICY_BR_KEY = "icy-br"; + private static final String RESPONSE_HEADER_ICY_GENRE_KEY = "icy-genre"; + private static final String RESPONSE_HEADER_ICY_NAME_KEY = "icy-name"; + private static final String RESPONSE_HEADER_ICY_URL_KEY = "icy-url"; + private static final String RESPONSE_HEADER_ICY_PUB_KEY = "icy-pub"; + private static final String RESPONSE_HEADER_ICY_METAINT_KEY = "icy-metaint"; + + private static final String ICY_METADATA_STREAM_TITLE_KEY = "StreamTitle"; + private static final String ICY_METADATA_STREAM_URL_KEY = "StreamUrl"; + + private IcyHeadersListener icyHeadersListener; + private IcyMetadataListener icyMetadataListener; + private int metaDataIntervalInBytes = -1; + private int remainingStreamDataUntilMetaDataBlock = -1; + private DataSpec dataSpec; + + public interface IcyHeadersListener { + + void onIcyHeaders(IcyHeaders icyHeaders); + } + + public interface IcyMetadataListener { + + void onIcyMetaData(IcyMetadata icyMetadata); + } + + private IcyHttpDataSource( + @NonNull Call.Factory callFactory, + @Nullable final String userAgent, + @Nullable final Predicate contentTypePredicate, + @Nullable CacheControl cacheControl, + @NonNull RequestProperties defaultRequestProperties) { + super(callFactory, userAgent, contentTypePredicate, cacheControl, defaultRequestProperties); + defaultRequestProperties.set(REQUEST_HEADER_ICY_METAINT_KEY, REQUEST_HEADER_ICY_METAINT_VALUE); + + // See class Builder + } + + @Override + public long open(DataSpec dataSpec) throws HttpDataSourceException { + this.dataSpec = dataSpec; + long bytesToRead = super.open(dataSpec); + + Map> responseHeaders = getResponseHeaders(); + if (responseHeaders != null) { + IcyHeaders icyHeaders = new IcyHeaders(); + + Log.d(TAG, "open: responseHeaders=" + responseHeaders.toString()); + List headers = responseHeaders.get(RESPONSE_HEADER_ICY_BR_KEY); + if (headers != null && headers.size() == 1) { + icyHeaders.bitRate = Integer.parseInt(headers.get(0)); + } + headers = responseHeaders.get(RESPONSE_HEADER_ICY_GENRE_KEY); + if (headers != null && headers.size() == 1) { + icyHeaders.genre = headers.get(0); + } + headers = responseHeaders.get(RESPONSE_HEADER_ICY_NAME_KEY); + if (headers != null && headers.size() == 1) { + icyHeaders.name = headers.get(0); + } + headers = responseHeaders.get(RESPONSE_HEADER_ICY_URL_KEY); + if (headers != null && headers.size() == 1) { + icyHeaders.url = headers.get(0); + } + headers = responseHeaders.get(RESPONSE_HEADER_ICY_PUB_KEY); + if (headers != null && headers.size() == 1) { + icyHeaders.isPublic = headers.get(0).equals("1"); + } + headers = responseHeaders.get(RESPONSE_HEADER_ICY_METAINT_KEY); + if (headers != null && headers.size() == 1) { + metaDataIntervalInBytes = Integer.parseInt(headers.get(0)); + remainingStreamDataUntilMetaDataBlock = metaDataIntervalInBytes; + } + + if (icyHeadersListener != null) { + icyHeadersListener.onIcyHeaders(icyHeaders); + } + } + return bytesToRead; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException { + int bytesRead; + + // Only read metadata if the server declared to send it... + if (metaDataIntervalInBytes < 0) { + bytesRead = super.read(buffer, offset, readLength); + } else { + bytesRead = super.read(buffer, offset, + remainingStreamDataUntilMetaDataBlock < readLength ? remainingStreamDataUntilMetaDataBlock + : readLength); + if (remainingStreamDataUntilMetaDataBlock == bytesRead) { + parseIcyMetadata(); + } else { + remainingStreamDataUntilMetaDataBlock -= bytesRead; + } + } + return bytesRead; + } + + private void parseIcyMetadata() throws HttpDataSourceException { + // We hit the metadata block, reset stream data counter + remainingStreamDataUntilMetaDataBlock = metaDataIntervalInBytes; + + byte[] metaDataBuffer = new byte[1]; + int bytesRead = super.read(metaDataBuffer, 0, 1); + if (bytesRead != 1) { + throw new HttpDataSourceException("parseIcyMetadata: Unable to read metadata length!", + dataSpec, HttpDataSourceException.TYPE_READ); + } + int metaDataBlockSize = metaDataBuffer[0]; + if (metaDataBlockSize < 1) { // Either no metadata or end of file + return; + } + metaDataBlockSize <<= 4; // Multiply by 16 to get actual size + + if (metaDataBuffer.length < metaDataBlockSize) { + metaDataBuffer = new byte[metaDataBlockSize]; // Make room for the full metadata block + } + + // Read entire metadata block into buffer + int offset = 0; + int readLength = metaDataBlockSize; + while (readLength > 0 && (bytesRead = super.read(metaDataBuffer, offset, readLength)) != -1) { + offset += bytesRead; + readLength -= bytesRead; + } + metaDataBlockSize = offset; + + // We read the metadata from the stream. Only parse it when we have a listener registered + // to return the contents. + if (icyMetadataListener != null) { + // Find null-terminator + for (int i = 0; i < metaDataBlockSize; i++) { + if (metaDataBuffer[i] == 0) { + metaDataBlockSize = i; + break; + } + } + + try { + final String metaDataString = new String(metaDataBuffer, 0, metaDataBlockSize, "utf-8"); + icyMetadataListener.onIcyMetaData(parseMetadata(metaDataString)); + } catch (Exception e) { + Log.e(TAG, "parseIcyMetadata: Cannot convert bytes to String"); + } + } + } + + private IcyMetadata parseMetadata(final String metaDataString) { + String[] keyAndValuePairs = metaDataString.split(";"); + IcyMetadata icyMetadata = new IcyMetadata(); + + for (String keyValuePair : keyAndValuePairs) { + int equalSignPosition = keyValuePair.indexOf('='); + if (equalSignPosition < 1) { + continue; + } + + boolean isString = equalSignPosition + 1 < keyValuePair.length() + && keyValuePair.charAt(keyValuePair.length() - 1) == '\'' + && keyValuePair.charAt(equalSignPosition + 1) == '\''; + + String key = keyValuePair.substring(0, equalSignPosition); + String value = isString ? + keyValuePair.substring(equalSignPosition + 2, keyValuePair.length() - 1) : + equalSignPosition + 1 < keyValuePair.length() ? + keyValuePair.substring(equalSignPosition + 1) : ""; + + switch (key) { + case ICY_METADATA_STREAM_TITLE_KEY: + icyMetadata.streamTitle = value; + case ICY_METADATA_STREAM_URL_KEY: + icyMetadata.streamUrl = value; + } + + icyMetadata.metadata.put(key, value); + } + + return icyMetadata; + } + + public final static class Builder { + + private Call.Factory callFactory; + private String userAgent; + private Predicate contentTypePredicate; + private CacheControl cacheControl; + private RequestProperties defaultRequestProperties = new RequestProperties(); + private IcyHeadersListener icyHeadersListener; + private IcyMetadataListener icyMetadataListener; + + public Builder(@NonNull Call.Factory callFactory) { + this.callFactory = callFactory; + } + + public Builder setUserAgent(@NonNull final String userAgent) { + this.userAgent = userAgent; + return this; + } + + public Builder setContentTypePredicate(@NonNull final Predicate contentTypePredicate) { + this.contentTypePredicate = contentTypePredicate; + return this; + } + + public Builder setCacheControl(@NonNull final CacheControl cacheControl) { + this.cacheControl = cacheControl; + return this; + } + + public Builder setDefaultRequestProperties( + @NonNull final RequestProperties defaultRequestProperties) { + this.defaultRequestProperties = defaultRequestProperties; + return this; + } + + public Builder setIcyHeadersListener( + @NonNull final IcyHttpDataSource.IcyHeadersListener icyHeadersListener) { + this.icyHeadersListener = icyHeadersListener; + return this; + } + + public Builder setIcyMetadataListener(@NonNull final IcyMetadataListener icyMetadataListener) { + this.icyMetadataListener = icyMetadataListener; + return this; + } + + IcyHttpDataSource build() { + final IcyHttpDataSource dataSource = + new IcyHttpDataSource(callFactory, + userAgent, + contentTypePredicate, + cacheControl, + defaultRequestProperties); + dataSource.icyHeadersListener = icyHeadersListener; + dataSource.icyMetadataListener = icyMetadataListener; + return dataSource; + } + } + + /** + * Container for Icy headers such as stream genre or name. + */ + public final class IcyHeaders { + + /** + * icy-br Bit rate in KB/s + */ + int bitRate; + /** + * icy-genre + */ + String genre; + /** + * icy-name + */ + String name; + /** + * icy-url + */ + String url; + /** + * icy-pub + */ + boolean isPublic; + + /** + * @return The bit rate in kilobits per second (KB/s) + */ + public int getBitRate() { + return bitRate; + } + + /** + * @return The musical genre of the stream + */ + public String getGenre() { + return genre; + } + + /** + * @return The stream name + */ + public String getName() { + return name; + } + + /** + * @return The URL of the music stream (can be a website or artwork) + */ + public String getUrl() { + return url; + } + + /** + * @return Determines if this stream is public or listed in a catalog + */ + public boolean isPublic() { + return isPublic; + } + + @Override + public String toString() { + return "IcyHeaders{" + + "bitRate='" + bitRate + '\'' + + ", genre='" + genre + '\'' + + ", name='" + name + '\'' + + ", url='" + url + '\'' + + ", isPublic=" + isPublic + + '}'; + } + } + + /** + * Container for stream title and URL. + *

+ * The exact contents isn't specified and implementation specific. It's therefore up to the user + * to figure what format a given stream returns. + */ + public final class IcyMetadata { + + String streamTitle; + String streamUrl; + HashMap metadata = new HashMap<>(); + + /** + * @return The song title. + */ + public String getStreamTitle() { + return streamTitle; + } + + /** + * @return Url to album artwork or more information about the current song. + */ + public String getStreamUrl() { + return streamUrl; + } + + /** + * Provides a map of all stream metadata. + * + * @return Complete metadata + */ + public HashMap getMetadata() { + return metadata; + } + + @Override + public String toString() { + return "IcyMetadata{" + + "streamTitle='" + streamTitle + '\'' + + ", streamUrl='" + streamUrl + '\'' + + ", metadata='" + metadata + '\'' + + '}'; + } + } +} diff --git a/extensions/icy/src/main/java/com/google/android/exoplayer2/ext/icy/IcyHttpDataSourceFactory.java b/extensions/icy/src/main/java/com/google/android/exoplayer2/ext/icy/IcyHttpDataSourceFactory.java new file mode 100644 index 0000000000..e396405f44 --- /dev/null +++ b/extensions/icy/src/main/java/com/google/android/exoplayer2/ext/icy/IcyHttpDataSourceFactory.java @@ -0,0 +1,85 @@ +package com.google.android.exoplayer2.ext.icy; + +import android.support.annotation.NonNull; + +import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.Predicate; + +import okhttp3.CacheControl; +import okhttp3.Call; + +/** + * A {@link HttpDataSource.Factory} that produces {@link IcyHttpDataSource} instances. + */ +public final class IcyHttpDataSourceFactory extends OkHttpDataSource.BaseFactory { + + private Call.Factory callFactory; + private String userAgent; + private Predicate contentTypePredicate; + private CacheControl cacheControl; + private IcyHttpDataSource.IcyHeadersListener icyHeadersListener; + private IcyHttpDataSource.IcyMetadataListener icyMetadataListener; + + private IcyHttpDataSourceFactory() { + // See class Builder + } + + /** + * Constructs a IcyHttpDataSourceFactory. + */ + public final static class Builder { + + private final IcyHttpDataSourceFactory factory; + + public Builder(@NonNull Call.Factory callFactory) { + // Apply defaults + factory = new IcyHttpDataSourceFactory(); + factory.callFactory = callFactory; + } + + public Builder setUserAgent(@NonNull final String userAgent) { + factory.userAgent = userAgent; + return this; + } + + public Builder setContentTypePredicate(@NonNull final Predicate contentTypePredicate) { + factory.contentTypePredicate = contentTypePredicate; + return this; + } + + public Builder setCacheControl(@NonNull final CacheControl cacheControl) { + factory.cacheControl = cacheControl; + return this; + } + + public Builder setIcyHeadersListener( + @NonNull final IcyHttpDataSource.IcyHeadersListener icyHeadersListener) { + factory.icyHeadersListener = icyHeadersListener; + return this; + } + + public Builder setIcyMetadataChangeListener( + @NonNull final IcyHttpDataSource.IcyMetadataListener icyMetadataListener) { + factory.icyMetadataListener = icyMetadataListener; + return this; + } + + public IcyHttpDataSourceFactory build() { + return factory; + } + } + + @Override + protected IcyHttpDataSource createDataSourceInternal( + @NonNull HttpDataSource.RequestProperties defaultRequestProperties) { + return new IcyHttpDataSource.Builder(callFactory) + .setUserAgent(userAgent) + .setContentTypePredicate(contentTypePredicate) + .setCacheControl(cacheControl) + .setDefaultRequestProperties(defaultRequestProperties) + .setIcyHeadersListener(icyHeadersListener) + .setIcyMetadataListener(icyMetadataListener) + .build(); + } +} diff --git a/extensions/icy/src/test/java/com/google/android/exoplayer2/ext/icy/IcyHttpDataSourceFactoryTest.java b/extensions/icy/src/test/java/com/google/android/exoplayer2/ext/icy/IcyHttpDataSourceFactoryTest.java new file mode 100644 index 0000000000..11f5e074d4 --- /dev/null +++ b/extensions/icy/src/test/java/com/google/android/exoplayer2/ext/icy/IcyHttpDataSourceFactoryTest.java @@ -0,0 +1,41 @@ +package com.google.android.exoplayer2.ext.icy; + +import com.google.android.exoplayer2.upstream.HttpDataSource; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import okhttp3.OkHttpClient; +import com.google.android.exoplayer2.ext.icy.test.Constants; + +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public final class IcyHttpDataSourceFactoryTest { + + private final IcyHttpDataSource.IcyHeadersListener TEST_ICY_HEADERS_LISTENER = icyHeaders -> { + }; + private final IcyHttpDataSource.IcyMetadataListener TEST_ICY_METADATA_LISTENER = icyMetadata -> { + }; + + @Test + public void createDataSourceViaFactoryFromFactoryBuilder() { + // Arrange + OkHttpClient client = new OkHttpClient.Builder().build(); + IcyHttpDataSourceFactory factory = new IcyHttpDataSourceFactory.Builder(client) + .setUserAgent(Constants.TEST_USER_AGENT) + .setIcyHeadersListener(TEST_ICY_HEADERS_LISTENER) + .setIcyMetadataChangeListener(TEST_ICY_METADATA_LISTENER) + .build(); + HttpDataSource.RequestProperties requestProperties = new HttpDataSource.RequestProperties(); + + // Act + IcyHttpDataSource source = factory.createDataSourceInternal(requestProperties); + + // Assert + assertNotNull(source); + } +} diff --git a/extensions/icy/src/test/java/com/google/android/exoplayer2/ext/icy/IcyHttpDataSourceTest.java b/extensions/icy/src/test/java/com/google/android/exoplayer2/ext/icy/IcyHttpDataSourceTest.java new file mode 100644 index 0000000000..b06bd78e65 --- /dev/null +++ b/extensions/icy/src/test/java/com/google/android/exoplayer2/ext/icy/IcyHttpDataSourceTest.java @@ -0,0 +1,29 @@ +package com.google.android.exoplayer2.ext.icy; + +import com.google.android.exoplayer2.ext.icy.IcyHttpDataSource; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import okhttp3.OkHttpClient; +import com.google.android.exoplayer2.ext.icy.test.Constants; + +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public final class IcyHttpDataSourceTest { + + @Test + public void createDataSourceFromBuilder() { + // Arrange, act + OkHttpClient client = new OkHttpClient.Builder().build(); + IcyHttpDataSource source = new IcyHttpDataSource.Builder(client) + .setUserAgent(Constants.TEST_USER_AGENT) + .build(); + + // Assert + assertNotNull(source); + } +} diff --git a/extensions/icy/src/test/java/com/google/android/exoplayer2/ext/icy/test/Constants.java b/extensions/icy/src/test/java/com/google/android/exoplayer2/ext/icy/test/Constants.java new file mode 100644 index 0000000000..6dec462829 --- /dev/null +++ b/extensions/icy/src/test/java/com/google/android/exoplayer2/ext/icy/test/Constants.java @@ -0,0 +1,6 @@ +package com.google.android.exoplayer2.ext.icy.test; + +public final class Constants { + + public static final String TEST_USER_AGENT = "test-agent"; +}