Commit be2636c3 by Oliver Woodman

Merge pull request #4993 from saschpe:icy

PiperOrigin-RevId: 226031838
parents 0e8e9621 535b4053
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
* Support for playing spherical videos on Daydream. * Support for playing spherical videos on Daydream.
* Improve decoder re-use between playbacks. TODO: Write and link a blog post * Improve decoder re-use between playbacks. TODO: Write and link a blog post
here ([#2826](https://github.com/google/ExoPlayer/issues/2826)). here ([#2826](https://github.com/google/ExoPlayer/issues/2826)).
* Use the true bitrate for constant-bitrate MP3 seeking.
* Track selection: * Track selection:
* Add options for controlling audio track selections to `DefaultTrackSelector` * Add options for controlling audio track selections to `DefaultTrackSelector`
([#3314](https://github.com/google/ExoPlayer/issues/3314)). ([#3314](https://github.com/google/ExoPlayer/issues/3314)).
...@@ -36,8 +35,12 @@ ...@@ -36,8 +35,12 @@
* DownloadManager: * DownloadManager:
* Create only one task for all DownloadActions for the same content. * Create only one task for all DownloadActions for the same content.
* Rename TaskState to DownloadState. * Rename TaskState to DownloadState.
* MP3: Fix issue where streams would play twice on some Samsung devices * MP3:
([#4519](https://github.com/google/ExoPlayer/issues/4519)). * Use the true bitrate for constant-bitrate MP3 seeking.
* Fix issue where streams would play twice on some Samsung devices
([#4519](https://github.com/google/ExoPlayer/issues/4519)).
* Add support for SHOUTcast ICY metadata
([#3735](https://github.com/google/ExoPlayer/issues/3735)).
### 2.9.2 ### ### 2.9.2 ###
......
...@@ -20,6 +20,7 @@ import android.support.annotation.Nullable; ...@@ -20,6 +20,7 @@ import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
...@@ -493,6 +494,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -493,6 +494,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) { if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
throw new IOException("HTTP request with non-empty body must set Content-Type"); throw new IOException("HTTP request with non-empty body must set Content-Type");
} }
if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
requestBuilder.addHeader(
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
}
// Set the Range header. // Set the Range header.
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) { if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
StringBuilder rangeValue = new StringBuilder(); StringBuilder rangeValue = new StringBuilder();
......
...@@ -21,6 +21,7 @@ import android.net.Uri; ...@@ -21,6 +21,7 @@ import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
...@@ -263,7 +264,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { ...@@ -263,7 +264,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException { private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {
long position = dataSpec.position; long position = dataSpec.position;
long length = dataSpec.length; long length = dataSpec.length;
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString()); HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
if (url == null) { if (url == null) {
...@@ -293,10 +293,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { ...@@ -293,10 +293,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
if (userAgent != null) { if (userAgent != null) {
builder.addHeader("User-Agent", userAgent); builder.addHeader("User-Agent", userAgent);
} }
if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
if (!allowGzip) {
builder.addHeader("Accept-Encoding", "identity"); builder.addHeader("Accept-Encoding", "identity");
} }
if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
builder.addHeader(
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
}
RequestBody requestBody = null; RequestBody requestBody = null;
if (dataSpec.httpBody != null) { if (dataSpec.httpBody != null) {
requestBody = RequestBody.create(null, dataSpec.httpBody); requestBody = RequestBody.create(null, dataSpec.httpBody);
......
...@@ -1274,6 +1274,37 @@ public final class Format implements Parcelable { ...@@ -1274,6 +1274,37 @@ public final class Format implements Parcelable {
metadata); metadata);
} }
public Format copyWithBitrate(int bitrate) {
return new Format(
id,
label,
containerMimeType,
sampleMimeType,
codecs,
bitrate,
maxInputSize,
width,
height,
frameRate,
rotationDegrees,
pixelWidthHeightRatio,
projectionData,
stereoMode,
colorInfo,
channelCount,
sampleRate,
pcmEncoding,
encoderDelay,
encoderPadding,
selectionFlags,
language,
accessibilityChannel,
subsampleOffsetUs,
initializationData,
drmInitData,
metadata);
}
/** /**
* Returns the number of pixels if this is a video format whose {@link #width} and {@link #height} * Returns the number of pixels if this is a video format whose {@link #width} and {@link #height}
* are known, or {@link #NO_VALUE} otherwise * are known, or {@link #NO_VALUE} otherwise
......
...@@ -18,8 +18,10 @@ package com.google.android.exoplayer2.metadata; ...@@ -18,8 +18,10 @@ package com.google.android.exoplayer2.metadata;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /**
* A collection of metadata entries. * A collection of metadata entries.
...@@ -76,6 +78,18 @@ public final class Metadata implements Parcelable { ...@@ -76,6 +78,18 @@ public final class Metadata implements Parcelable {
return entries[index]; return entries[index];
} }
/**
* Returns a copy of this metadata with the specified entries appended.
*
* @param entriesToAppend The entries to append.
* @return The metadata instance with the appended entries.
*/
public Metadata copyWithAppendedEntries(Entry... entriesToAppend) {
@NullableType Entry[] merged = Arrays.copyOf(entries, entries.length + entriesToAppend.length);
System.arraycopy(entriesToAppend, 0, merged, entries.length, entriesToAppend.length);
return new Metadata(Util.castNonNullTypeArray(merged));
}
@Override @Override
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
import com.google.android.exoplayer2.metadata.icy.IcyDecoder;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder; import com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
...@@ -46,38 +47,43 @@ public interface MetadataDecoderFactory { ...@@ -46,38 +47,43 @@ public interface MetadataDecoderFactory {
/** /**
* Default {@link MetadataDecoder} implementation. * Default {@link MetadataDecoder} implementation.
* <p> *
* The formats supported by this factory are: * <p>The formats supported by this factory are:
*
* <ul> * <ul>
* <li>ID3 ({@link Id3Decoder})</li> * <li>ID3 ({@link Id3Decoder})
* <li>EMSG ({@link EventMessageDecoder})</li> * <li>EMSG ({@link EventMessageDecoder})
* <li>SCTE-35 ({@link SpliceInfoDecoder})</li> * <li>SCTE-35 ({@link SpliceInfoDecoder})
* <li>ICY ({@link IcyDecoder})
* </ul> * </ul>
*/ */
MetadataDecoderFactory DEFAULT = new MetadataDecoderFactory() { MetadataDecoderFactory DEFAULT =
new MetadataDecoderFactory() {
@Override
public boolean supportsFormat(Format format) {
String mimeType = format.sampleMimeType;
return MimeTypes.APPLICATION_ID3.equals(mimeType)
|| MimeTypes.APPLICATION_EMSG.equals(mimeType)
|| MimeTypes.APPLICATION_SCTE35.equals(mimeType);
}
@Override
public MetadataDecoder createDecoder(Format format) {
switch (format.sampleMimeType) {
case MimeTypes.APPLICATION_ID3:
return new Id3Decoder();
case MimeTypes.APPLICATION_EMSG:
return new EventMessageDecoder();
case MimeTypes.APPLICATION_SCTE35:
return new SpliceInfoDecoder();
default:
throw new IllegalArgumentException("Attempted to create decoder for unsupported format");
}
}
}; @Override
public boolean supportsFormat(Format format) {
String mimeType = format.sampleMimeType;
return MimeTypes.APPLICATION_ID3.equals(mimeType)
|| MimeTypes.APPLICATION_EMSG.equals(mimeType)
|| MimeTypes.APPLICATION_SCTE35.equals(mimeType)
|| MimeTypes.APPLICATION_ICY.equals(mimeType);
}
@Override
public MetadataDecoder createDecoder(Format format) {
switch (format.sampleMimeType) {
case MimeTypes.APPLICATION_ID3:
return new Id3Decoder();
case MimeTypes.APPLICATION_EMSG:
return new EventMessageDecoder();
case MimeTypes.APPLICATION_SCTE35:
return new SpliceInfoDecoder();
case MimeTypes.APPLICATION_ICY:
return new IcyDecoder();
default:
throw new IllegalArgumentException(
"Attempted to create decoder for unsupported format");
}
}
};
} }
/*
* 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.
*/
package com.google.android.exoplayer2.metadata.icy;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Decodes ICY stream information. */
public final class IcyDecoder implements MetadataDecoder {
private static final String TAG = "IcyDecoder";
private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.+?)';");
private static final String STREAM_KEY_NAME = "streamtitle";
private static final String STREAM_KEY_URL = "streamurl";
@Override
@Nullable
@SuppressWarnings("ByteBufferBackingArray")
public Metadata decode(MetadataInputBuffer inputBuffer) {
ByteBuffer buffer = inputBuffer.data;
byte[] data = buffer.array();
int length = buffer.limit();
return decode(Util.fromUtf8Bytes(data, 0, length));
}
@Nullable
@VisibleForTesting
/* package */ Metadata decode(String metadata) {
String name = null;
String url = null;
int index = 0;
Matcher matcher = METADATA_ELEMENT.matcher(metadata);
while (matcher.find(index)) {
String key = Util.toLowerInvariant(matcher.group(1));
String value = matcher.group(2);
switch (key) {
case STREAM_KEY_NAME:
name = value;
break;
case STREAM_KEY_URL:
url = value;
break;
default:
Log.w(TAG, "Unrecognized ICY tag: " + name);
break;
}
index = matcher.end();
}
return (name != null || url != null) ? new Metadata(new IcyInfo(name, url)) : null;
}
}
/*
* 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.
*/
package com.google.android.exoplayer2.metadata.icy;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.util.List;
import java.util.Map;
/** ICY headers. */
public final class IcyHeaders implements Metadata.Entry {
public static final String REQUEST_HEADER_ENABLE_METADATA_NAME = "Icy-MetaData";
public static final String REQUEST_HEADER_ENABLE_METADATA_VALUE = "1";
private static final String TAG = "IcyHeaders";
private static final String RESPONSE_HEADER_BITRATE = "icy-br";
private static final String RESPONSE_HEADER_GENRE = "icy-genre";
private static final String RESPONSE_HEADER_NAME = "icy-name";
private static final String RESPONSE_HEADER_URL = "icy-url";
private static final String RESPONSE_HEADER_PUB = "icy-pub";
private static final String RESPONSE_HEADER_METADATA_INTERVAL = "icy-metaint";
/**
* Parses {@link IcyHeaders} from response headers.
*
* @param responseHeaders The response headers.
* @return The parsed {@link IcyHeaders}, or {@code null} if no ICY headers were present.
*/
@Nullable
public static IcyHeaders parse(Map<String, List<String>> responseHeaders) {
boolean icyHeadersPresent = false;
int bitrate = Format.NO_VALUE;
String genre = null;
String name = null;
String url = null;
boolean isPublic = false;
int metadataInterval = C.LENGTH_UNSET;
List<String> headers = responseHeaders.get(RESPONSE_HEADER_BITRATE);
if (headers != null) {
String bitrateHeader = headers.get(0);
try {
bitrate = Integer.parseInt(bitrateHeader) * 1000;
if (bitrate > 0) {
icyHeadersPresent = true;
} else {
Log.w(TAG, "Invalid bitrate: " + bitrateHeader);
bitrate = Format.NO_VALUE;
}
} catch (NumberFormatException e) {
Log.w(TAG, "Invalid bitrate header: " + bitrateHeader);
}
}
headers = responseHeaders.get(RESPONSE_HEADER_GENRE);
if (headers != null) {
genre = headers.get(0);
icyHeadersPresent = true;
}
headers = responseHeaders.get(RESPONSE_HEADER_NAME);
if (headers != null) {
name = headers.get(0);
icyHeadersPresent = true;
}
headers = responseHeaders.get(RESPONSE_HEADER_URL);
if (headers != null) {
url = headers.get(0);
icyHeadersPresent = true;
}
headers = responseHeaders.get(RESPONSE_HEADER_PUB);
if (headers != null) {
isPublic = headers.get(0).equals("1");
icyHeadersPresent = true;
}
headers = responseHeaders.get(RESPONSE_HEADER_METADATA_INTERVAL);
if (headers != null) {
String metadataIntervalHeader = headers.get(0);
try {
metadataInterval = Integer.parseInt(metadataIntervalHeader);
if (metadataInterval > 0) {
icyHeadersPresent = true;
} else {
Log.w(TAG, "Invalid metadata interval: " + metadataIntervalHeader);
metadataInterval = C.LENGTH_UNSET;
}
} catch (NumberFormatException e) {
Log.w(TAG, "Invalid metadata interval: " + metadataIntervalHeader);
}
}
return icyHeadersPresent
? new IcyHeaders(bitrate, genre, name, url, isPublic, metadataInterval)
: null;
}
/**
* Bitrate in bits per second ({@code (icy-br * 1000)}), or {@link Format#NO_VALUE} if the header
* was not present.
*/
public final int bitrate;
/** The genre ({@code icy-genre}). */
@Nullable public final String genre;
/** The stream name ({@code icy-name}). */
@Nullable public final String name;
/** The URL of the radio station ({@code icy-url}). */
@Nullable public final String url;
/**
* Whether the radio station is listed ({@code icy-pub}), or {@code false} if the header was not
* present.
*/
public final boolean isPublic;
/**
* The interval in bytes between metadata chunks ({@code icy-metaint}), or {@link C#LENGTH_UNSET}
* if the header was not present.
*/
public final int metadataInterval;
/**
* @param bitrate See {@link #bitrate}.
* @param genre See {@link #genre}.
* @param name See {@link #name See}.
* @param url See {@link #url}.
* @param isPublic See {@link #isPublic}.
* @param metadataInterval See {@link #metadataInterval}.
*/
public IcyHeaders(
int bitrate,
@Nullable String genre,
@Nullable String name,
@Nullable String url,
boolean isPublic,
int metadataInterval) {
Assertions.checkArgument(metadataInterval == C.LENGTH_UNSET || metadataInterval > 0);
this.bitrate = bitrate;
this.genre = genre;
this.name = name;
this.url = url;
this.isPublic = isPublic;
this.metadataInterval = metadataInterval;
}
/* package */ IcyHeaders(Parcel in) {
bitrate = in.readInt();
genre = in.readString();
name = in.readString();
url = in.readString();
isPublic = Util.readBoolean(in);
metadataInterval = in.readInt();
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
IcyHeaders other = (IcyHeaders) obj;
return bitrate == other.bitrate
&& Util.areEqual(genre, other.genre)
&& Util.areEqual(name, other.name)
&& Util.areEqual(url, other.url)
&& isPublic == other.isPublic
&& metadataInterval == other.metadataInterval;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + bitrate;
result = 31 * result + (genre != null ? genre.hashCode() : 0);
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (url != null ? url.hashCode() : 0);
result = 31 * result + (isPublic ? 1 : 0);
result = 31 * result + metadataInterval;
return result;
}
@Override
public String toString() {
return "IcyHeaders: name=\""
+ name
+ "\", genre=\""
+ genre
+ "\", bitrate="
+ bitrate
+ ", metadataInterval="
+ metadataInterval;
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bitrate);
dest.writeString(genre);
dest.writeString(name);
dest.writeString(url);
Util.writeBoolean(dest, isPublic);
dest.writeInt(metadataInterval);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<IcyHeaders> CREATOR =
new Parcelable.Creator<IcyHeaders>() {
@Override
public IcyHeaders createFromParcel(Parcel in) {
return new IcyHeaders(in);
}
@Override
public IcyHeaders[] newArray(int size) {
return new IcyHeaders[size];
}
};
}
/*
* 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.
*/
package com.google.android.exoplayer2.metadata.icy;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Util;
/** ICY in-stream information. */
public final class IcyInfo implements Metadata.Entry {
/** The stream title if present, or {@code null}. */
@Nullable public final String title;
/** The stream title if present, or {@code null}. */
@Nullable public final String url;
/**
* @param title See {@link #title}.
* @param url See {@link #url}.
*/
public IcyInfo(@Nullable String title, @Nullable String url) {
this.title = title;
this.url = url;
}
/* package */ IcyInfo(Parcel in) {
title = in.readString();
url = in.readString();
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
IcyInfo other = (IcyInfo) obj;
return Util.areEqual(title, other.title) && Util.areEqual(url, other.url);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (title != null ? title.hashCode() : 0);
result = 31 * result + (url != null ? url.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "ICY: title=\"" + title + "\", url=\"" + url + "\"";
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(title);
dest.writeString(url);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<IcyInfo> CREATOR =
new Parcelable.Creator<IcyInfo>() {
@Override
public IcyInfo createFromParcel(Parcel in) {
return new IcyInfo(in);
}
@Override
public IcyInfo[] newArray(int size) {
return new IcyInfo[size];
}
};
}
/*
* 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.
*/
package com.google.android.exoplayer2.source;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* Splits ICY stream metadata out from a stream.
*
* <p>Note: {@link #open(DataSpec)} and {@link #close()} are not supported. This implementation is
* intended to wrap upstream {@link DataSource} instances that are opened and closed directly.
*/
/* package */ final class IcyDataSource implements DataSource {
public interface Listener {
/**
* Called when ICY stream metadata has been split from the stream.
*
* @param metadata The stream metadata in binary form.
*/
void onIcyMetadata(ParsableByteArray metadata);
}
private final DataSource upstream;
private final int metadataIntervalBytes;
private final Listener listener;
private final byte[] metadataLengthByteHolder;
private int bytesUntilMetadata;
/**
* @param upstream The upstream {@link DataSource}.
* @param metadataIntervalBytes The interval between ICY stream metadata, in bytes.
* @param listener A listener to which stream metadata is delivered.
*/
public IcyDataSource(DataSource upstream, int metadataIntervalBytes, Listener listener) {
Assertions.checkArgument(metadataIntervalBytes > 0);
this.upstream = upstream;
this.metadataIntervalBytes = metadataIntervalBytes;
this.listener = listener;
metadataLengthByteHolder = new byte[1];
bytesUntilMetadata = metadataIntervalBytes;
}
@Override
public void addTransferListener(TransferListener transferListener) {
upstream.addTransferListener(transferListener);
}
@Override
public long open(DataSpec dataSpec) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
if (bytesUntilMetadata == 0) {
if (readMetadata()) {
bytesUntilMetadata = metadataIntervalBytes;
} else {
return C.RESULT_END_OF_INPUT;
}
}
int bytesRead = upstream.read(buffer, offset, Math.min(bytesUntilMetadata, readLength));
if (bytesRead != C.RESULT_END_OF_INPUT) {
bytesUntilMetadata -= bytesRead;
}
return bytesRead;
}
@Nullable
@Override
public Uri getUri() {
return upstream.getUri();
}
@Override
public Map<String, List<String>> getResponseHeaders() {
return upstream.getResponseHeaders();
}
@Override
public void close() throws IOException {
throw new UnsupportedOperationException();
}
/**
* Reads an ICY stream metadata block, passing it to {@link #listener} unless the block is empty.
*
* @return True if the block was extracted, including if it's length byte indicated a length of
* zero. False if the end of the stream was reached.
* @throws IOException If an error occurs reading from the wrapped {@link DataSource}.
*/
private boolean readMetadata() throws IOException {
int bytesRead = upstream.read(metadataLengthByteHolder, 0, 1);
if (bytesRead == C.RESULT_END_OF_INPUT) {
return false;
}
int metadataLength = (metadataLengthByteHolder[0] & 0xFF) << 4;
if (metadataLength == 0) {
return true;
}
int offset = 0;
int lengthRemaining = metadataLength;
byte[] metadata = new byte[metadataLength];
while (lengthRemaining > 0) {
bytesRead = upstream.read(metadata, offset, lengthRemaining);
if (bytesRead == C.RESULT_END_OF_INPUT) {
return false;
}
offset += bytesRead;
lengthRemaining -= bytesRead;
}
// Discard trailing zero bytes.
while (metadataLength > 0 && metadata[metadataLength - 1] == 0) {
metadataLength--;
}
if (metadataLength > 0) {
listener.onIcyMetadata(new ParsableByteArray(metadata, metadataLength));
}
return true;
}
}
...@@ -31,14 +31,15 @@ import java.util.Arrays; ...@@ -31,14 +31,15 @@ import java.util.Arrays;
public final class DataSpec { public final class DataSpec {
/** /**
* The flags that apply to any request for data. Possible flag values are {@link #FLAG_ALLOW_GZIP} * The flags that apply to any request for data. Possible flag values are {@link
* and {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN}. * #FLAG_ALLOW_GZIP}, {@link #FLAG_ALLOW_ICY_METADATA} and {@link
* #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef( @IntDef(
flag = true, flag = true,
value = {FLAG_ALLOW_GZIP, FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN}) value = {FLAG_ALLOW_GZIP, FLAG_ALLOW_ICY_METADATA, FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN})
public @interface Flags {} public @interface Flags {}
/** /**
* Allows an underlying network stack to request that the server use gzip compression. * Allows an underlying network stack to request that the server use gzip compression.
...@@ -53,8 +54,11 @@ public final class DataSpec { ...@@ -53,8 +54,11 @@ public final class DataSpec {
*/ */
public static final int FLAG_ALLOW_GZIP = 1; public static final int FLAG_ALLOW_GZIP = 1;
/** Allows an underlying network stack to request that the stream contain ICY metadata. */
public static final int FLAG_ALLOW_ICY_METADATA = 1 << 1; // 2
/** Prevents caching if the length cannot be resolved when the {@link DataSource} is opened. */ /** Prevents caching if the length cannot be resolved when the {@link DataSource} is opened. */
public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 1; // 2 public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 2; // 4
/** /**
* The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link * The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link
......
...@@ -19,6 +19,7 @@ import android.net.Uri; ...@@ -19,6 +19,7 @@ import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
...@@ -429,12 +430,20 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou ...@@ -429,12 +430,20 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
long position = dataSpec.position; long position = dataSpec.position;
long length = dataSpec.length; long length = dataSpec.length;
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP); boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
boolean allowIcyMetadata = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA);
if (!allowCrossProtocolRedirects) { if (!allowCrossProtocolRedirects) {
// HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
// automatically. This is the behavior we want, so use it. // automatically. This is the behavior we want, so use it.
return makeConnection( return makeConnection(
url, httpMethod, httpBody, position, length, allowGzip, true /* followRedirects */); url,
httpMethod,
httpBody,
position,
length,
allowGzip,
allowIcyMetadata,
/* followRedirects= */ true);
} }
// We need to handle redirects ourselves to allow cross-protocol redirects. // We need to handle redirects ourselves to allow cross-protocol redirects.
...@@ -442,7 +451,14 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou ...@@ -442,7 +451,14 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
while (redirectCount++ <= MAX_REDIRECTS) { while (redirectCount++ <= MAX_REDIRECTS) {
HttpURLConnection connection = HttpURLConnection connection =
makeConnection( makeConnection(
url, httpMethod, httpBody, position, length, allowGzip, false /* followRedirects */); url,
httpMethod,
httpBody,
position,
length,
allowGzip,
allowIcyMetadata,
/* followRedirects= */ false);
int responseCode = connection.getResponseCode(); int responseCode = connection.getResponseCode();
String location = connection.getHeaderField("Location"); String location = connection.getHeaderField("Location");
if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD) if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD)
...@@ -482,6 +498,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou ...@@ -482,6 +498,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
* @param position The byte offset of the requested data. * @param position The byte offset of the requested data.
* @param length The length of the requested data, or {@link C#LENGTH_UNSET}. * @param length The length of the requested data, or {@link C#LENGTH_UNSET}.
* @param allowGzip Whether to allow the use of gzip. * @param allowGzip Whether to allow the use of gzip.
* @param allowIcyMetadata Whether to allow ICY metadata.
* @param followRedirects Whether to follow redirects. * @param followRedirects Whether to follow redirects.
*/ */
private HttpURLConnection makeConnection( private HttpURLConnection makeConnection(
...@@ -491,6 +508,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou ...@@ -491,6 +508,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
long position, long position,
long length, long length,
boolean allowGzip, boolean allowGzip,
boolean allowIcyMetadata,
boolean followRedirects) boolean followRedirects)
throws IOException { throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
...@@ -515,6 +533,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou ...@@ -515,6 +533,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
if (!allowGzip) { if (!allowGzip) {
connection.setRequestProperty("Accept-Encoding", "identity"); connection.setRequestProperty("Accept-Encoding", "identity");
} }
if (allowIcyMetadata) {
connection.setRequestProperty(
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
}
connection.setInstanceFollowRedirects(followRedirects); connection.setInstanceFollowRedirects(followRedirects);
connection.setDoOutput(httpBody != null); connection.setDoOutput(httpBody != null);
connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod)); connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod));
......
...@@ -92,6 +92,7 @@ public final class MimeTypes { ...@@ -92,6 +92,7 @@ public final class MimeTypes {
public static final String APPLICATION_EMSG = BASE_TYPE_APPLICATION + "/x-emsg"; public static final String APPLICATION_EMSG = BASE_TYPE_APPLICATION + "/x-emsg";
public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs"; public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs";
public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif";
public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy";
private static final ArrayList<CustomMimeType> customMimeTypes = new ArrayList<>(); private static final ArrayList<CustomMimeType> customMimeTypes = new ArrayList<>();
......
/*
* 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.
*/
package com.google.android.exoplayer2.metadata.icy;
import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.metadata.Metadata;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Test for {@link IcyDecoder}. */
@RunWith(RobolectricTestRunner.class)
public final class IcyDecoderTest {
@Test
public void decode() {
IcyDecoder decoder = new IcyDecoder();
Metadata metadata = decoder.decode("StreamTitle='test title';StreamURL='test_url';");
assertThat(metadata.length()).isEqualTo(1);
IcyInfo streamInfo = (IcyInfo) metadata.get(0);
assertThat(streamInfo.title).isEqualTo("test title");
assertThat(streamInfo.url).isEqualTo("test_url");
}
@Test
public void decode_titleOnly() {
IcyDecoder decoder = new IcyDecoder();
Metadata metadata = decoder.decode("StreamTitle='test title';");
assertThat(metadata.length()).isEqualTo(1);
IcyInfo streamInfo = (IcyInfo) metadata.get(0);
assertThat(streamInfo.title).isEqualTo("test title");
assertThat(streamInfo.url).isNull();
}
@Test
public void decode_semiColonInTitle() {
IcyDecoder decoder = new IcyDecoder();
Metadata metadata = decoder.decode("StreamTitle='test; title';StreamURL='test_url';");
assertThat(metadata.length()).isEqualTo(1);
IcyInfo streamInfo = (IcyInfo) metadata.get(0);
assertThat(streamInfo.title).isEqualTo("test; title");
assertThat(streamInfo.url).isEqualTo("test_url");
}
@Test
public void decode_quoteInTitle() {
IcyDecoder decoder = new IcyDecoder();
Metadata metadata = decoder.decode("StreamTitle='test' title';StreamURL='test_url';");
assertThat(metadata.length()).isEqualTo(1);
IcyInfo streamInfo = (IcyInfo) metadata.get(0);
assertThat(streamInfo.title).isEqualTo("test' title");
assertThat(streamInfo.url).isEqualTo("test_url");
}
@Test
public void decode_notIcy() {
IcyDecoder decoder = new IcyDecoder();
Metadata metadata = decoder.decode("NotIcyData");
assertThat(metadata).isNull();
}
}
/*
* 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.
*/
package com.google.android.exoplayer2.metadata.icy;
import static com.google.common.truth.Truth.assertThat;
import android.os.Parcel;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Test for {@link IcyHeaders}. */
@RunWith(RobolectricTestRunner.class)
public final class IcyHeadersTest {
@Test
public void parcelEquals() {
IcyHeaders icyHeaders =
new IcyHeaders(
/* bitrate= */ 1234,
"genre",
"name",
"url",
/* isPublic= */ true,
/* metadataInterval= */ 5678);
// Write to parcel.
Parcel parcel = Parcel.obtain();
icyHeaders.writeToParcel(parcel, 0);
// Create from parcel.
parcel.setDataPosition(0);
IcyHeaders fromParcelIcyHeaders = IcyHeaders.CREATOR.createFromParcel(parcel);
// Assert equals.
assertThat(fromParcelIcyHeaders).isEqualTo(icyHeaders);
}
}
/*
* 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.
*/
package com.google.android.exoplayer2.metadata.icy;
import static com.google.common.truth.Truth.assertThat;
import android.os.Parcel;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Test for {@link IcyInfo}. */
@RunWith(RobolectricTestRunner.class)
public final class IcyStreamInfoTest {
@Test
public void parcelEquals() {
IcyInfo streamInfo = new IcyInfo("name", "url");
// Write to parcel.
Parcel parcel = Parcel.obtain();
streamInfo.writeToParcel(parcel, 0);
// Create from parcel.
parcel.setDataPosition(0);
IcyInfo fromParcelStreamInfo = IcyInfo.CREATOR.createFromParcel(parcel);
// Assert equals.
assertThat(fromParcelStreamInfo).isEqualTo(streamInfo);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment