mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add Shoutcast Metadata Protocol (ICY) extension
Based on the OkHttp extension. Resolves #3735
This commit is contained in:
parent
1ef3efaa90
commit
95eff3b747
@ -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')
|
||||
|
67
extensions/icy/README.md
Normal file
67
extensions/icy/README.md
Normal file
@ -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
|
52
extensions/icy/build.gradle
Normal file
52
extensions/icy/build.gradle
Normal file
@ -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'
|
2
extensions/icy/proguard-rules.txt
Normal file
2
extensions/icy/proguard-rules.txt
Normal file
@ -0,0 +1,2 @@
|
||||
# Proguard rules specific to the Icy extension.
|
||||
|
17
extensions/icy/src/main/AndroidManifest.xml
Normal file
17
extensions/icy/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2016 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.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2.ext.icy"/>
|
@ -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<String> 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<String, List<String>> responseHeaders = getResponseHeaders();
|
||||
if (responseHeaders != null) {
|
||||
IcyHeaders icyHeaders = new IcyHeaders();
|
||||
|
||||
Log.d(TAG, "open: responseHeaders=" + responseHeaders.toString());
|
||||
List<String> 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<String> 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<String> 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.
|
||||
* <p>
|
||||
* 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<String, String> 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<String, String> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IcyMetadata{" +
|
||||
"streamTitle='" + streamTitle + '\'' +
|
||||
", streamUrl='" + streamUrl + '\'' +
|
||||
", metadata='" + metadata + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String> 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<String> 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user