mirror of
https://github.com/samsonjs/media.git
synced 2026-04-26 14:57:47 +00:00
commit
be2636c365
17 changed files with 984 additions and 67 deletions
|
|
@ -7,7 +7,6 @@
|
|||
* Support for playing spherical videos on Daydream.
|
||||
* Improve decoder re-use between playbacks. TODO: Write and link a blog post
|
||||
here ([#2826](https://github.com/google/ExoPlayer/issues/2826)).
|
||||
* Use the true bitrate for constant-bitrate MP3 seeking.
|
||||
* Track selection:
|
||||
* Add options for controlling audio track selections to `DefaultTrackSelector`
|
||||
([#3314](https://github.com/google/ExoPlayer/issues/3314)).
|
||||
|
|
@ -36,8 +35,12 @@
|
|||
* DownloadManager:
|
||||
* Create only one task for all DownloadActions for the same content.
|
||||
* Rename TaskState to DownloadState.
|
||||
* MP3: Fix issue where streams would play twice on some Samsung devices
|
||||
([#4519](https://github.com/google/ExoPlayer/issues/4519)).
|
||||
* MP3:
|
||||
* 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 ###
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import android.support.annotation.Nullable;
|
|||
import android.text.TextUtils;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.DataSourceException;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
|
|
@ -493,6 +494,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
|
||||
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.
|
||||
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
||||
StringBuilder rangeValue = new StringBuilder();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import android.net.Uri;
|
|||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.DataSourceException;
|
||||
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 {
|
||||
long position = dataSpec.position;
|
||||
long length = dataSpec.length;
|
||||
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
|
||||
|
||||
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
|
||||
if (url == null) {
|
||||
|
|
@ -293,10 +293,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||
if (userAgent != null) {
|
||||
builder.addHeader("User-Agent", userAgent);
|
||||
}
|
||||
|
||||
if (!allowGzip) {
|
||||
if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
|
||||
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;
|
||||
if (dataSpec.httpBody != null) {
|
||||
requestBody = RequestBody.create(null, dataSpec.httpBody);
|
||||
|
|
|
|||
|
|
@ -1274,6 +1274,37 @@ public final class Format implements Parcelable {
|
|||
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}
|
||||
* are known, or {@link #NO_VALUE} otherwise
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ package com.google.android.exoplayer2.metadata;
|
|||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
|
||||
/**
|
||||
* A collection of metadata entries.
|
||||
|
|
@ -76,6 +78,18 @@ public final class Metadata implements Parcelable {
|
|||
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
|
||||
public boolean equals(@Nullable Object 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.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.scte35.SpliceInfoDecoder;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
|
@ -46,38 +47,43 @@ public interface MetadataDecoderFactory {
|
|||
|
||||
/**
|
||||
* Default {@link MetadataDecoder} implementation.
|
||||
* <p>
|
||||
* The formats supported by this factory are:
|
||||
*
|
||||
* <p>The formats supported by this factory are:
|
||||
*
|
||||
* <ul>
|
||||
* <li>ID3 ({@link Id3Decoder})</li>
|
||||
* <li>EMSG ({@link EventMessageDecoder})</li>
|
||||
* <li>SCTE-35 ({@link SpliceInfoDecoder})</li>
|
||||
* <li>ID3 ({@link Id3Decoder})
|
||||
* <li>EMSG ({@link EventMessageDecoder})
|
||||
* <li>SCTE-35 ({@link SpliceInfoDecoder})
|
||||
* <li>ICY ({@link IcyDecoder})
|
||||
* </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");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.SeekPoints;
|
||||
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.SampleQueue.UpstreamFormatChangedListener;
|
||||
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.ConditionVariable;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.EOFException;
|
||||
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 Format ICY_FORMAT =
|
||||
Format.createSampleFormat("icy", MimeTypes.APPLICATION_ICY, Format.OFFSET_SAMPLE_RELATIVE);
|
||||
|
||||
private final Uri uri;
|
||||
private final DataSource dataSource;
|
||||
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final Listener listener;
|
||||
private final Allocator allocator;
|
||||
private final @Nullable String customCacheKey;
|
||||
@Nullable private final String customCacheKey;
|
||||
private final long continueLoadingCheckIntervalBytes;
|
||||
private final Loader loader;
|
||||
private final ExtractorHolder extractorHolder;
|
||||
|
|
@ -94,14 +100,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
private final Runnable onContinueLoadingRequestedRunnable;
|
||||
private final Handler handler;
|
||||
|
||||
private @Nullable Callback callback;
|
||||
private @Nullable SeekMap seekMap;
|
||||
@Nullable private Callback callback;
|
||||
@Nullable private SeekMap seekMap;
|
||||
@Nullable private IcyHeaders icyHeaders;
|
||||
private SampleQueue[] sampleQueues;
|
||||
private int[] sampleQueueTrackIds;
|
||||
private TrackId[] sampleQueueTrackIds;
|
||||
private boolean sampleQueuesBuilt;
|
||||
private boolean prepared;
|
||||
|
||||
private @Nullable PreparedState preparedState;
|
||||
@Nullable private PreparedState preparedState;
|
||||
private boolean haveAudioVideoTracks;
|
||||
private int dataType;
|
||||
|
||||
|
|
@ -134,7 +141,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
* invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
|
||||
*/
|
||||
// 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(
|
||||
Uri uri,
|
||||
DataSource dataSource,
|
||||
|
|
@ -164,7 +174,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
}
|
||||
};
|
||||
handler = new Handler();
|
||||
sampleQueueTrackIds = new int[0];
|
||||
sampleQueueTrackIds = new TrackId[0];
|
||||
sampleQueues = new SampleQueue[0];
|
||||
pendingResetPositionUs = C.TIME_UNSET;
|
||||
length = C.LENGTH_UNSET;
|
||||
|
|
@ -599,20 +609,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
|
||||
@Override
|
||||
public TrackOutput track(int id, int type) {
|
||||
int trackCount = sampleQueues.length;
|
||||
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;
|
||||
return prepareTrackOutput(new TrackId(id, /* isIcyTrack= */ false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -627,6 +624,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
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.
|
||||
|
||||
@Override
|
||||
|
|
@ -636,6 +639,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
|
||||
// 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() {
|
||||
SeekMap seekMap = this.seekMap;
|
||||
if (released || prepared || !sampleQueuesBuilt || seekMap == null) {
|
||||
|
|
@ -653,11 +675,28 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
durationUs = seekMap.getDurationUs();
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
Format trackFormat = sampleQueues[i].getUpstreamFormat();
|
||||
trackArray[i] = new TrackGroup(trackFormat);
|
||||
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;
|
||||
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 =
|
||||
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. */
|
||||
/* package */ final class ExtractingLoadable implements Loadable {
|
||||
/* package */ final class ExtractingLoadable implements Loadable, IcyDataSource.Listener {
|
||||
|
||||
private final Uri uri;
|
||||
private final StatsDataSource dataSource;
|
||||
|
|
@ -849,6 +888,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
private long seekTimeUs;
|
||||
private DataSpec dataSpec;
|
||||
private long length;
|
||||
@Nullable private TrackOutput icyTrackOutput;
|
||||
private boolean seenIcyMetadata;
|
||||
|
||||
@SuppressWarnings("method.invocation.invalid")
|
||||
public ExtractingLoadable(
|
||||
|
|
@ -888,7 +929,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
length += position;
|
||||
}
|
||||
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);
|
||||
if (pendingExtractorSeek) {
|
||||
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.
|
||||
|
||||
private DataSpec buildDataSpec(long position) {
|
||||
|
|
@ -924,13 +988,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
position,
|
||||
C.LENGTH_UNSET,
|
||||
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) {
|
||||
positionHolder.position = position;
|
||||
seekTimeUs = timeUs;
|
||||
pendingExtractorSeek = true;
|
||||
seenIcyMetadata = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1014,4 +1079,33 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
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 {
|
||||
|
||||
/**
|
||||
* The flags that apply to any request for data. Possible flag values are {@link #FLAG_ALLOW_GZIP}
|
||||
* and {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN}.
|
||||
* The flags that apply to any request for data. Possible flag values are {@link
|
||||
* #FLAG_ALLOW_GZIP}, {@link #FLAG_ALLOW_ICY_METADATA} and {@link
|
||||
* #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(
|
||||
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 {}
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/** 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. */
|
||||
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
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import android.net.Uri;
|
|||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
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.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
|
|
@ -429,12 +430,20 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
|||
long position = dataSpec.position;
|
||||
long length = dataSpec.length;
|
||||
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
|
||||
boolean allowIcyMetadata = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA);
|
||||
|
||||
if (!allowCrossProtocolRedirects) {
|
||||
// HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
|
||||
// automatically. This is the behavior we want, so use it.
|
||||
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.
|
||||
|
|
@ -442,7 +451,14 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
|||
while (redirectCount++ <= MAX_REDIRECTS) {
|
||||
HttpURLConnection connection =
|
||||
makeConnection(
|
||||
url, httpMethod, httpBody, position, length, allowGzip, false /* followRedirects */);
|
||||
url,
|
||||
httpMethod,
|
||||
httpBody,
|
||||
position,
|
||||
length,
|
||||
allowGzip,
|
||||
allowIcyMetadata,
|
||||
/* followRedirects= */ false);
|
||||
int responseCode = connection.getResponseCode();
|
||||
String location = connection.getHeaderField("Location");
|
||||
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 length The length of the requested data, or {@link C#LENGTH_UNSET}.
|
||||
* @param allowGzip Whether to allow the use of gzip.
|
||||
* @param allowIcyMetadata Whether to allow ICY metadata.
|
||||
* @param followRedirects Whether to follow redirects.
|
||||
*/
|
||||
private HttpURLConnection makeConnection(
|
||||
|
|
@ -491,6 +508,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
|||
long position,
|
||||
long length,
|
||||
boolean allowGzip,
|
||||
boolean allowIcyMetadata,
|
||||
boolean followRedirects)
|
||||
throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
|
|
@ -515,6 +533,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
|||
if (!allowGzip) {
|
||||
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.setDoOutput(httpBody != null);
|
||||
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_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs";
|
||||
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<>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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