mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
commit
be2636c365
17 changed files with 984 additions and 67 deletions
|
|
@ -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 @@
|
||||||
* 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;
|
||||||
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 {
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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;
|
||||||
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 {
|
||||||
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;
|
||||||
|
|
||||||
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 {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
@Override
|
||||||
public boolean supportsFormat(Format format) {
|
public boolean supportsFormat(Format format) {
|
||||||
String mimeType = format.sampleMimeType;
|
String mimeType = format.sampleMimeType;
|
||||||
return MimeTypes.APPLICATION_ID3.equals(mimeType)
|
return MimeTypes.APPLICATION_ID3.equals(mimeType)
|
||||||
|| MimeTypes.APPLICATION_EMSG.equals(mimeType)
|
|| MimeTypes.APPLICATION_EMSG.equals(mimeType)
|
||||||
|| MimeTypes.APPLICATION_SCTE35.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();
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Attempted to create decoder for unsupported format");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
@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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
/*
|
||||||
|
* 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];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* 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];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
|
import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
|
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
|
||||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||||
import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
|
import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
|
|
@ -45,6 +47,7 @@ import com.google.android.exoplayer2.upstream.StatsDataSource;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.ConditionVariable;
|
import com.google.android.exoplayer2.util.ConditionVariable;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -79,13 +82,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
*/
|
*/
|
||||||
private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10000;
|
private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10000;
|
||||||
|
|
||||||
|
private static final Format ICY_FORMAT =
|
||||||
|
Format.createSampleFormat("icy", MimeTypes.APPLICATION_ICY, Format.OFFSET_SAMPLE_RELATIVE);
|
||||||
|
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||||
private final EventDispatcher eventDispatcher;
|
private final EventDispatcher eventDispatcher;
|
||||||
private final Listener listener;
|
private final Listener listener;
|
||||||
private final Allocator allocator;
|
private final Allocator allocator;
|
||||||
private final @Nullable String customCacheKey;
|
@Nullable private final String customCacheKey;
|
||||||
private final long continueLoadingCheckIntervalBytes;
|
private final long continueLoadingCheckIntervalBytes;
|
||||||
private final Loader loader;
|
private final Loader loader;
|
||||||
private final ExtractorHolder extractorHolder;
|
private final ExtractorHolder extractorHolder;
|
||||||
|
|
@ -94,14 +100,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
private final Runnable onContinueLoadingRequestedRunnable;
|
private final Runnable onContinueLoadingRequestedRunnable;
|
||||||
private final Handler handler;
|
private final Handler handler;
|
||||||
|
|
||||||
private @Nullable Callback callback;
|
@Nullable private Callback callback;
|
||||||
private @Nullable SeekMap seekMap;
|
@Nullable private SeekMap seekMap;
|
||||||
|
@Nullable private IcyHeaders icyHeaders;
|
||||||
private SampleQueue[] sampleQueues;
|
private SampleQueue[] sampleQueues;
|
||||||
private int[] sampleQueueTrackIds;
|
private TrackId[] sampleQueueTrackIds;
|
||||||
private boolean sampleQueuesBuilt;
|
private boolean sampleQueuesBuilt;
|
||||||
private boolean prepared;
|
private boolean prepared;
|
||||||
|
|
||||||
private @Nullable PreparedState preparedState;
|
@Nullable private PreparedState preparedState;
|
||||||
private boolean haveAudioVideoTracks;
|
private boolean haveAudioVideoTracks;
|
||||||
private int dataType;
|
private int dataType;
|
||||||
|
|
||||||
|
|
@ -134,7 +141,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
* invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
|
* invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
|
||||||
*/
|
*/
|
||||||
// maybeFinishPrepare is not posted to the handler until initialization completes.
|
// maybeFinishPrepare is not posted to the handler until initialization completes.
|
||||||
@SuppressWarnings("nullness:methodref.receiver.bound.invalid")
|
@SuppressWarnings({
|
||||||
|
"nullness:argument.type.incompatible",
|
||||||
|
"nullness:methodref.receiver.bound.invalid"
|
||||||
|
})
|
||||||
public ExtractorMediaPeriod(
|
public ExtractorMediaPeriod(
|
||||||
Uri uri,
|
Uri uri,
|
||||||
DataSource dataSource,
|
DataSource dataSource,
|
||||||
|
|
@ -164,7 +174,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
handler = new Handler();
|
handler = new Handler();
|
||||||
sampleQueueTrackIds = new int[0];
|
sampleQueueTrackIds = new TrackId[0];
|
||||||
sampleQueues = new SampleQueue[0];
|
sampleQueues = new SampleQueue[0];
|
||||||
pendingResetPositionUs = C.TIME_UNSET;
|
pendingResetPositionUs = C.TIME_UNSET;
|
||||||
length = C.LENGTH_UNSET;
|
length = C.LENGTH_UNSET;
|
||||||
|
|
@ -599,20 +609,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TrackOutput track(int id, int type) {
|
public TrackOutput track(int id, int type) {
|
||||||
int trackCount = sampleQueues.length;
|
return prepareTrackOutput(new TrackId(id, /* isIcyTrack= */ false));
|
||||||
for (int i = 0; i < trackCount; i++) {
|
|
||||||
if (sampleQueueTrackIds[i] == id) {
|
|
||||||
return sampleQueues[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SampleQueue trackOutput = new SampleQueue(allocator);
|
|
||||||
trackOutput.setUpstreamFormatChangeListener(this);
|
|
||||||
sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
|
|
||||||
sampleQueueTrackIds[trackCount] = id;
|
|
||||||
@NullableType SampleQueue[] sampleQueues = Arrays.copyOf(this.sampleQueues, trackCount + 1);
|
|
||||||
sampleQueues[trackCount] = trackOutput;
|
|
||||||
this.sampleQueues = Util.castNonNullTypeArray(sampleQueues);
|
|
||||||
return trackOutput;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -627,6 +624,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
handler.post(maybeFinishPrepareRunnable);
|
handler.post(maybeFinishPrepareRunnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Icy metadata. Called by the loading thread.
|
||||||
|
|
||||||
|
/* package */ TrackOutput icyTrack() {
|
||||||
|
return prepareTrackOutput(new TrackId(0, /* isIcyTrack= */ true));
|
||||||
|
}
|
||||||
|
|
||||||
// UpstreamFormatChangedListener implementation. Called by the loading thread.
|
// UpstreamFormatChangedListener implementation. Called by the loading thread.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -636,6 +639,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
|
private TrackOutput prepareTrackOutput(TrackId id) {
|
||||||
|
int trackCount = sampleQueues.length;
|
||||||
|
for (int i = 0; i < trackCount; i++) {
|
||||||
|
if (id.equals(sampleQueueTrackIds[i])) {
|
||||||
|
return sampleQueues[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SampleQueue trackOutput = new SampleQueue(allocator);
|
||||||
|
trackOutput.setUpstreamFormatChangeListener(this);
|
||||||
|
@NullableType
|
||||||
|
TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1);
|
||||||
|
sampleQueueTrackIds[trackCount] = id;
|
||||||
|
this.sampleQueueTrackIds = Util.castNonNullTypeArray(sampleQueueTrackIds);
|
||||||
|
@NullableType SampleQueue[] sampleQueues = Arrays.copyOf(this.sampleQueues, trackCount + 1);
|
||||||
|
sampleQueues[trackCount] = trackOutput;
|
||||||
|
this.sampleQueues = Util.castNonNullTypeArray(sampleQueues);
|
||||||
|
return trackOutput;
|
||||||
|
}
|
||||||
|
|
||||||
private void maybeFinishPrepare() {
|
private void maybeFinishPrepare() {
|
||||||
SeekMap seekMap = this.seekMap;
|
SeekMap seekMap = this.seekMap;
|
||||||
if (released || prepared || !sampleQueuesBuilt || seekMap == null) {
|
if (released || prepared || !sampleQueuesBuilt || seekMap == null) {
|
||||||
|
|
@ -653,11 +675,28 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
durationUs = seekMap.getDurationUs();
|
durationUs = seekMap.getDurationUs();
|
||||||
for (int i = 0; i < trackCount; i++) {
|
for (int i = 0; i < trackCount; i++) {
|
||||||
Format trackFormat = sampleQueues[i].getUpstreamFormat();
|
Format trackFormat = sampleQueues[i].getUpstreamFormat();
|
||||||
trackArray[i] = new TrackGroup(trackFormat);
|
|
||||||
String mimeType = trackFormat.sampleMimeType;
|
String mimeType = trackFormat.sampleMimeType;
|
||||||
boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType);
|
boolean isAudio = MimeTypes.isAudio(mimeType);
|
||||||
|
boolean isAudioVideo = isAudio || MimeTypes.isVideo(mimeType);
|
||||||
trackIsAudioVideoFlags[i] = isAudioVideo;
|
trackIsAudioVideoFlags[i] = isAudioVideo;
|
||||||
haveAudioVideoTracks |= isAudioVideo;
|
haveAudioVideoTracks |= isAudioVideo;
|
||||||
|
IcyHeaders icyHeaders = this.icyHeaders;
|
||||||
|
if (icyHeaders != null) {
|
||||||
|
if (isAudio || sampleQueueTrackIds[i].isIcyTrack) {
|
||||||
|
Metadata metadata = trackFormat.metadata;
|
||||||
|
trackFormat =
|
||||||
|
trackFormat.copyWithMetadata(
|
||||||
|
metadata == null
|
||||||
|
? new Metadata(icyHeaders)
|
||||||
|
: metadata.copyWithAppendedEntries(icyHeaders));
|
||||||
|
}
|
||||||
|
if (isAudio
|
||||||
|
&& trackFormat.bitrate == Format.NO_VALUE
|
||||||
|
&& icyHeaders.bitrate != Format.NO_VALUE) {
|
||||||
|
trackFormat = trackFormat.copyWithBitrate(icyHeaders.bitrate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackArray[i] = new TrackGroup(trackFormat);
|
||||||
}
|
}
|
||||||
dataType =
|
dataType =
|
||||||
length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET
|
length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET
|
||||||
|
|
@ -834,7 +873,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Loads the media stream and extracts sample data from it. */
|
/** Loads the media stream and extracts sample data from it. */
|
||||||
/* package */ final class ExtractingLoadable implements Loadable {
|
/* package */ final class ExtractingLoadable implements Loadable, IcyDataSource.Listener {
|
||||||
|
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
private final StatsDataSource dataSource;
|
private final StatsDataSource dataSource;
|
||||||
|
|
@ -849,6 +888,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
private long seekTimeUs;
|
private long seekTimeUs;
|
||||||
private DataSpec dataSpec;
|
private DataSpec dataSpec;
|
||||||
private long length;
|
private long length;
|
||||||
|
@Nullable private TrackOutput icyTrackOutput;
|
||||||
|
private boolean seenIcyMetadata;
|
||||||
|
|
||||||
@SuppressWarnings("method.invocation.invalid")
|
@SuppressWarnings("method.invocation.invalid")
|
||||||
public ExtractingLoadable(
|
public ExtractingLoadable(
|
||||||
|
|
@ -888,7 +929,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
length += position;
|
length += position;
|
||||||
}
|
}
|
||||||
Uri uri = Assertions.checkNotNull(dataSource.getUri());
|
Uri uri = Assertions.checkNotNull(dataSource.getUri());
|
||||||
input = new DefaultExtractorInput(dataSource, position, length);
|
icyHeaders = IcyHeaders.parse(dataSource.getResponseHeaders());
|
||||||
|
DataSource extractorDataSource = dataSource;
|
||||||
|
if (icyHeaders != null && icyHeaders.metadataInterval != C.LENGTH_UNSET) {
|
||||||
|
extractorDataSource = new IcyDataSource(dataSource, icyHeaders.metadataInterval, this);
|
||||||
|
icyTrackOutput = icyTrack();
|
||||||
|
icyTrackOutput.format(ICY_FORMAT);
|
||||||
|
}
|
||||||
|
input = new DefaultExtractorInput(extractorDataSource, position, length);
|
||||||
Extractor extractor = extractorHolder.selectExtractor(input, extractorOutput, uri);
|
Extractor extractor = extractorHolder.selectExtractor(input, extractorOutput, uri);
|
||||||
if (pendingExtractorSeek) {
|
if (pendingExtractorSeek) {
|
||||||
extractor.seek(position, seekTimeUs);
|
extractor.seek(position, seekTimeUs);
|
||||||
|
|
@ -914,6 +962,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IcyDataSource.Listener
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onIcyMetadata(ParsableByteArray metadata) {
|
||||||
|
// Always output the first ICY metadata at the start time. This helps minimize any delay
|
||||||
|
// between the start of playback and the first ICY metadata event.
|
||||||
|
long timeUs =
|
||||||
|
!seenIcyMetadata ? seekTimeUs : Math.max(getLargestQueuedTimestampUs(), seekTimeUs);
|
||||||
|
int length = metadata.bytesLeft();
|
||||||
|
TrackOutput icyTrackOutput = Assertions.checkNotNull(this.icyTrackOutput);
|
||||||
|
icyTrackOutput.sampleData(metadata, length);
|
||||||
|
icyTrackOutput.sampleMetadata(
|
||||||
|
timeUs, C.BUFFER_FLAG_KEY_FRAME, length, /* offset= */ 0, /* encryptionData= */ null);
|
||||||
|
seenIcyMetadata = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private DataSpec buildDataSpec(long position) {
|
private DataSpec buildDataSpec(long position) {
|
||||||
|
|
@ -924,13 +988,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
position,
|
position,
|
||||||
C.LENGTH_UNSET,
|
C.LENGTH_UNSET,
|
||||||
customCacheKey,
|
customCacheKey,
|
||||||
DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN);
|
DataSpec.FLAG_ALLOW_ICY_METADATA | DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setLoadPosition(long position, long timeUs) {
|
private void setLoadPosition(long position, long timeUs) {
|
||||||
positionHolder.position = position;
|
positionHolder.position = position;
|
||||||
seekTimeUs = timeUs;
|
seekTimeUs = timeUs;
|
||||||
pendingExtractorSeek = true;
|
pendingExtractorSeek = true;
|
||||||
|
seenIcyMetadata = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1014,4 +1079,33 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
this.trackNotifiedDownstreamFormats = new boolean[tracks.length];
|
this.trackNotifiedDownstreamFormats = new boolean[tracks.length];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Identifies a track. */
|
||||||
|
private static final class TrackId {
|
||||||
|
|
||||||
|
public final int id;
|
||||||
|
public final boolean isIcyTrack;
|
||||||
|
|
||||||
|
public TrackId(int id, boolean isIcyTrack) {
|
||||||
|
this.id = id;
|
||||||
|
this.isIcyTrack = isIcyTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TrackId other = (TrackId) obj;
|
||||||
|
return id == other.id && isIcyTrack == other.isIcyTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return 31 * id + (isIcyTrack ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
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 {
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
* @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
|
||||||
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
|
||||||
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 {
|
||||||
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<>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue