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-ffmpeg'
|
||||||
include modulePrefix + 'extension-flac'
|
include modulePrefix + 'extension-flac'
|
||||||
include modulePrefix + 'extension-gvr'
|
include modulePrefix + 'extension-gvr'
|
||||||
|
include modulePrefix + 'extension-icy'
|
||||||
include modulePrefix + 'extension-ima'
|
include modulePrefix + 'extension-ima'
|
||||||
include modulePrefix + 'extension-cast'
|
include modulePrefix + 'extension-cast'
|
||||||
include modulePrefix + 'extension-cronet'
|
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-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
|
||||||
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
|
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
|
||||||
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
|
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-ima').projectDir = new File(rootDir, 'extensions/ima')
|
||||||
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
|
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
|
||||||
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
|
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