/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.datasource;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.net.Uri;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/** Defines a region of data in a resource. */
@UnstableApi
public final class DataSpec {
static {
MediaLibraryInfo.registerModule("media3.datasource");
}
/**
* Builds {@link DataSpec} instances.
*
* <p>Use DataSpec#buildUpon() to obtain a builder representing an existing {@link DataSpec}.
*/
public static final class Builder {
@Nullable private Uri uri;
private long uriPositionOffset;
private @HttpMethod int httpMethod;
@Nullable private byte[] httpBody;
private Map<String, String> httpRequestHeaders;
private long position;
private long length;
@Nullable private String key;
private @Flags int flags;
@Nullable private Object customData;
/** Creates a new instance with default values. */
public Builder() {
httpMethod = HTTP_METHOD_GET;
httpRequestHeaders = Collections.emptyMap();
length = C.LENGTH_UNSET;
}
/**
* Creates a new instance to build upon the provided {@link DataSpec}.
*
* @param dataSpec The {@link DataSpec} to build upon.
*/
private Builder(DataSpec dataSpec) {
uri = dataSpec.uri;
uriPositionOffset = dataSpec.uriPositionOffset;
httpMethod = dataSpec.httpMethod;
httpBody = dataSpec.httpBody;
httpRequestHeaders = dataSpec.httpRequestHeaders;
position = dataSpec.position;
length = dataSpec.length;
key = dataSpec.key;
flags = dataSpec.flags;
customData = dataSpec.customData;
}
/**
* Sets {@link DataSpec#uri}.
*
* @param uriString The {@link DataSpec#uri}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setUri(String uriString) {
this.uri = Uri.parse(uriString);
return this;
}
/**
* Sets {@link DataSpec#uri}.
*
* @param uri The {@link DataSpec#uri}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setUri(Uri uri) {
this.uri = uri;
return this;
}
/**
* Sets the {@link DataSpec#uriPositionOffset}. The default value is 0.
*
* @param uriPositionOffset The {@link DataSpec#uriPositionOffset}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setUriPositionOffset(long uriPositionOffset) {
this.uriPositionOffset = uriPositionOffset;
return this;
}
/**
* Sets {@link DataSpec#httpMethod}. The default value is {@link #HTTP_METHOD_GET}.
*
* @param httpMethod The {@link DataSpec#httpMethod}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setHttpMethod(@HttpMethod int httpMethod) {
this.httpMethod = httpMethod;
return this;
}
/**
* Sets {@link DataSpec#httpBody}. The default value is {@code null}.
*
* @param httpBody The {@link DataSpec#httpBody}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setHttpBody(@Nullable byte[] httpBody) {
this.httpBody = httpBody;
return this;
}
/**
* Sets the {@link DataSpec#httpRequestHeaders}. The default value is an empty map.
*
* <p>Note: {@code Range}, {@code Accept-Encoding} and {@code User-Agent} should not be set with
* this method, since they are set directly by {@link HttpDataSource} implementations. See
* {@link DataSpec#httpRequestHeaders} for more details.
*
* @param httpRequestHeaders The {@link DataSpec#httpRequestHeaders}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setHttpRequestHeaders(Map<String, String> httpRequestHeaders) {
this.httpRequestHeaders = httpRequestHeaders;
return this;
}
/**
* Sets the {@link DataSpec#position}. The default value is 0.
*
* @param position The {@link DataSpec#position}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setPosition(long position) {
this.position = position;
return this;
}
/**
* Sets the {@link DataSpec#length}. The default value is {@link C#LENGTH_UNSET}.
*
* @param length The {@link DataSpec#length}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setLength(long length) {
this.length = length;
return this;
}
/**
* Sets the {@link DataSpec#key}. The default value is {@code null}.
*
* @param key The {@link DataSpec#key}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setKey(@Nullable String key) {
this.key = key;
return this;
}
/**
* Sets the {@link DataSpec#flags}. The default value is 0.
*
* @param flags The {@link DataSpec#flags}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setFlags(@Flags int flags) {
this.flags = flags;
return this;
}
/**
* Sets the {@link DataSpec#customData}. The default value is {@code null}.
*
* @param customData The {@link DataSpec#customData}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setCustomData(@Nullable Object customData) {
this.customData = customData;
return this;
}
/**
* Builds a {@link DataSpec} with the builder's current values.
*
* @return The build {@link DataSpec}.
* @throws IllegalStateException If {@link #setUri} has not been called.
*/
public DataSpec build() {
Assertions.checkStateNotNull(uri, "The uri must be set.");
return new DataSpec(
uri,
uriPositionOffset,
httpMethod,
httpBody,
httpRequestHeaders,
position,
length,
key,
flags,
customData);
}
}
/**
* The flags that apply to any request for data. Possible flag values are {@link
* #FLAG_ALLOW_GZIP}, {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN}, {@link
* #FLAG_ALLOW_CACHE_FRAGMENTATION}, and {@link #FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef(
flag = true,
value = {
FLAG_ALLOW_GZIP,
FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN,
FLAG_ALLOW_CACHE_FRAGMENTATION,
FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED
})
public @interface Flags {}
/**
* Allows an underlying network stack to request that the server use gzip compression.
*
* <p>Should not typically be set if the data being requested is already compressed (e.g. most
* audio and video requests). May be set when requesting other data.
*
* <p>When a {@link DataSource} is used to request data with this flag set, and if the {@link
* DataSource} does make a network request, then the value returned from {@link
* DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNSET}. The data read from {@link
* DataSource#read(byte[], int, int)} will be the decompressed data.
*/
public static final int FLAG_ALLOW_GZIP = 1;
/** 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;
/**
* Allows fragmentation of this request into multiple cache files, meaning a cache eviction policy
* will be able to evict individual fragments of the data. Depending on the cache implementation,
* setting this flag may also enable more concurrent access to the data (e.g. reading one fragment
* whilst writing another).
*/
public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 2;
/**
* Indicates there are known external factors that might prevent the data from being loaded at
* full network speed (e.g. server throttling or unfinished live media chunks).
*/
public static final int FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED = 1 << 3;
/**
* HTTP methods supported by ExoPlayer {@link HttpDataSource}s. One of {@link #HTTP_METHOD_GET},
* {@link #HTTP_METHOD_POST} or {@link #HTTP_METHOD_HEAD}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({HTTP_METHOD_GET, HTTP_METHOD_POST, HTTP_METHOD_HEAD})
public @interface HttpMethod {}
/** HTTP GET method. */
public static final int HTTP_METHOD_GET = 1;
/** HTTP POST method. */
public static final int HTTP_METHOD_POST = 2;
/** HTTP HEAD method. */
public static final int HTTP_METHOD_HEAD = 3;
/**
* Returns an uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the given
* {@link HttpMethod}.
*/
public static String getStringForHttpMethod(@HttpMethod int httpMethod) {
switch (httpMethod) {
case HTTP_METHOD_GET:
return "GET";
case HTTP_METHOD_POST:
return "POST";
case HTTP_METHOD_HEAD:
return "HEAD";
default:
// Never happens.
throw new IllegalStateException();
}
}
/** A {@link Uri} from which data belonging to the resource can be read. */
public final Uri uri;
/**
* The offset of the data located at {@link #uri} within the resource.
*
* <p>Equal to 0 unless {@link #uri} provides access to a subset of the resource. As an example,
* consider a resource that can be requested over the network and is 1000 bytes long. If {@link
* #uri} points to a local file that contains just bytes [200-300], then this field will be set to
* {@code 200}.
*
* <p>This field can be ignored except for in specific circumstances where the absolute position
* in the resource is required in a {@link DataSource} chain. One example is when a {@link
* DataSource} needs to decrypt the content as it's read. In this case the absolute position in
* the resource is typically needed to correctly initialize the decryption algorithm.
*/
public final long uriPositionOffset;
/**
* The HTTP method to use when requesting the data. This value will be ignored by non-HTTP {@link
* DataSource} implementations.
*/
public final @HttpMethod int httpMethod;
/**
* The HTTP request body, null otherwise. If the body is non-null, then {@code httpBody.length}
* will be non-zero.
*/
@Nullable public final byte[] httpBody;
/**
* Additional HTTP headers to use when requesting the data.
*
* <p>Note: This map is for additional headers specific to the data being requested. It does not
* include headers that are set directly by {@link HttpDataSource} implementations. In particular,
* this means the following headers are not included:
*
* <ul>
* <li>{@code Range}: {@link HttpDataSource} implementations derive the {@code Range} header
* from {@link #position} and {@link #length}.
* <li>{@code Accept-Encoding}: {@link HttpDataSource} implementations derive the {@code
* Accept-Encoding} header based on whether {@link #flags} includes {@link
* #FLAG_ALLOW_GZIP}.
* <li>{@code User-Agent}: {@link HttpDataSource} implementations set the {@code User-Agent}
* header directly.
* <li>Other headers set at the {@link HttpDataSource} layer. I.e., headers set using {@link
* HttpDataSource#setRequestProperty(String, String)}, and using {@link
* HttpDataSource.Factory#setDefaultRequestProperties(Map)}.
* </ul>
*/
public final Map<String, String> httpRequestHeaders;
/**
* The absolute position of the data in the resource.
*
* @deprecated Use {@link #position} except for specific use cases where the absolute position
* within the resource is required within a {@link DataSource} chain. Where the absolute
* position is required, use {@code uriPositionOffset + position}.
*/
@Deprecated public final long absoluteStreamPosition;
/** The position of the data when read from {@link #uri}. */
public final long position;
/** The length of the data, or {@link C#LENGTH_UNSET}. */
public final long length;
/**
* A key that uniquely identifies the resource. Used for cache indexing. May be null if the data
* spec is not intended to be used in conjunction with a cache.
*/
@Nullable public final String key;
/** Request {@link Flags flags}. */
public final @Flags int flags;
/**
* Application specific data.
*
* <p>This field is intended for advanced use cases in which applications require the ability to
* attach custom data to {@link DataSpec} instances. The custom data should be immutable.
*/
@Nullable public final Object customData;
/**
* Constructs an instance.
*
* @param uri {@link #uri}.
*/
public DataSpec(Uri uri) {
this(uri, /* position= */ 0, /* length= */ C.LENGTH_UNSET);
}
/**
* Constructs an instance.
*
* @param uri {@link #uri}.
* @param position {@link #position}.
* @param length {@link #length}.
*/
public DataSpec(Uri uri, long position, long length) {
this(
uri,
/* uriPositionOffset= */ 0,
HTTP_METHOD_GET,
/* httpBody= */ null,
/* httpRequestHeaders= */ Collections.emptyMap(),
position,
length,
/* key= */ null,
/* flags= */ 0,
/* customData= */ null);
}
/**
* Constructs an instance.
*
* @deprecated Use {@link Builder}.
* @param uri {@link #uri}.
* @param flags {@link #flags}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public DataSpec(Uri uri, @Flags int flags) {
this(uri, /* position= */ 0, C.LENGTH_UNSET, /* key= */ null, flags);
}
/**
* Constructs an instance.
*
* @deprecated Use {@link Builder}.
* @param uri {@link #uri}.
* @param position {@link #position}.
* @param length {@link #length}.
* @param key {@link #key}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public DataSpec(Uri uri, long position, long length, @Nullable String key) {
this(uri, position, position, length, key, /* flags= */ 0);
}
/**
* Constructs an instance.
*
* @deprecated Use {@link Builder}.
* @param uri {@link #uri}.
* @param position {@link #position}.
* @param length {@link #length}.
* @param key {@link #key}.
* @param flags {@link #flags}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public DataSpec(Uri uri, long position, long length, @Nullable String key, @Flags int flags) {
this(uri, position, position, length, key, flags);
}
/**
* Constructs an instance.
*
* @deprecated Use {@link Builder}.
* @param uri {@link #uri}.
* @param position {@link #position}, equal to {@link #position}.
* @param length {@link #length}.
* @param key {@link #key}.
* @param flags {@link #flags}.
* @param httpRequestHeaders {@link #httpRequestHeaders}
*/
@SuppressWarnings("deprecation")
@Deprecated
public DataSpec(
Uri uri,
long position,
long length,
@Nullable String key,
@Flags int flags,
Map<String, String> httpRequestHeaders) {
this(
uri,
HTTP_METHOD_GET,
/* httpBody= */ null,
position,
position,
length,
key,
flags,
httpRequestHeaders);
}
/**
* Constructs an instance where {@link #uriPositionOffset} may be non-zero.
*
* @deprecated Use {@link Builder}.
* @param uri {@link #uri}.
* @param absoluteStreamPosition The sum of {@link #uriPositionOffset} and {@link #position}.
* @param position {@link #position}.
* @param length {@link #length}.
* @param key {@link #key}.
* @param flags {@link #flags}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public DataSpec(
Uri uri,
long absoluteStreamPosition,
long position,
long length,
@Nullable String key,
@Flags int flags) {
this(uri, /* postBody= */ null, absoluteStreamPosition, position, length, key, flags);
}
/**
* Construct a instance where {@link #uriPositionOffset} may be non-zero. The {@link #httpMethod}
* is inferred from {@code postBody}. If {@code postBody} is non-null then {@link #httpMethod} is
* set to {@link #HTTP_METHOD_POST}. If {@code postBody} is null then {@link #httpMethod} is set
* to {@link #HTTP_METHOD_GET}.
*
* @deprecated Use {@link Builder}. Note that the httpMethod must be set explicitly for the
* Builder.
* @param uri {@link #uri}.
* @param postBody {@link #httpBody} The body of the HTTP request, which is also used to infer the
* {@link #httpMethod}.
* @param absoluteStreamPosition The sum of {@link #uriPositionOffset} and {@link #position}.
* @param position {@link #position}.
* @param length {@link #length}.
* @param key {@link #key}.
* @param flags {@link #flags}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public DataSpec(
Uri uri,
@Nullable byte[] postBody,
long absoluteStreamPosition,
long position,
long length,
@Nullable String key,
@Flags int flags) {
this(
uri,
/* httpMethod= */ postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET,
/* httpBody= */ postBody,
absoluteStreamPosition,
position,
length,
key,
flags);
}
/**
* Construct a instance where {@link #uriPositionOffset} may be non-zero.
*
* @deprecated Use {@link Builder}.
* @param uri {@link #uri}.
* @param httpMethod {@link #httpMethod}.
* @param httpBody {@link #httpBody}.
* @param absoluteStreamPosition The sum of {@link #uriPositionOffset} and {@link #position}.
* @param position {@link #position}.
* @param length {@link #length}.
* @param key {@link #key}.
* @param flags {@link #flags}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public DataSpec(
Uri uri,
@HttpMethod int httpMethod,
@Nullable byte[] httpBody,
long absoluteStreamPosition,
long position,
long length,
@Nullable String key,
@Flags int flags) {
this(
uri,
httpMethod,
httpBody,
absoluteStreamPosition,
position,
length,
key,
flags,
/* httpRequestHeaders= */ Collections.emptyMap());
}
/**
* Construct a instance where {@link #uriPositionOffset} may be non-zero.
*
* @deprecated Use {@link Builder}.
* @param uri {@link #uri}.
* @param httpMethod {@link #httpMethod}.
* @param httpBody {@link #httpBody}.
* @param absoluteStreamPosition The sum of {@link #uriPositionOffset} and {@link #position}.
* @param position {@link #position}.
* @param length {@link #length}.
* @param key {@link #key}.
* @param flags {@link #flags}.
* @param httpRequestHeaders {@link #httpRequestHeaders}.
*/
@Deprecated
public DataSpec(
Uri uri,
@HttpMethod int httpMethod,
@Nullable byte[] httpBody,
long absoluteStreamPosition,
long position,
long length,
@Nullable String key,
@Flags int flags,
Map<String, String> httpRequestHeaders) {
this(
uri,
/* uriPositionOffset= */ absoluteStreamPosition - position,
httpMethod,
httpBody,
httpRequestHeaders,
position,
length,
key,
flags,
/* customData= */ null);
}
@SuppressWarnings("deprecation")
private DataSpec(
Uri uri,
long uriPositionOffset,
@HttpMethod int httpMethod,
@Nullable byte[] httpBody,
Map<String, String> httpRequestHeaders,
long position,
long length,
@Nullable String key,
@Flags int flags,
@Nullable Object customData) {
// TODO: Replace this assertion with a stricter one checking "uriPositionOffset >= 0", after
// validating there are no violations in ExoPlayer and 1P apps.
Assertions.checkArgument(uriPositionOffset + position >= 0);
Assertions.checkArgument(position >= 0);
Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET);
this.uri = uri;
this.uriPositionOffset = uriPositionOffset;
this.httpMethod = httpMethod;
this.httpBody = httpBody != null && httpBody.length != 0 ? httpBody : null;
this.httpRequestHeaders = Collections.unmodifiableMap(new HashMap<>(httpRequestHeaders));
this.position = position;
this.absoluteStreamPosition = uriPositionOffset + position;
this.length = length;
this.key = key;
this.flags = flags;
this.customData = customData;
}
/**
* Returns whether the given flag is set.
*
* @param flag Flag to be checked if it is set.
*/
public boolean isFlagSet(@Flags int flag) {
return (this.flags & flag) == flag;
}
/**
* Returns the uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the
* {@link #httpMethod}.
*/
public final String getHttpMethodString() {
return getStringForHttpMethod(httpMethod);
}
/** Returns a {@link DataSpec.Builder} initialized with the values of this instance. */
public DataSpec.Builder buildUpon() {
return new Builder(this);
}
/**
* Returns a data spec that represents a subrange of the data defined by this DataSpec. The
* subrange includes data from the offset up to the end of this DataSpec.
*
* @param offset The offset of the subrange.
* @return A data spec that represents a subrange of the data defined by this DataSpec.
*/
public DataSpec subrange(long offset) {
return subrange(offset, length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length - offset);
}
/**
* Returns a data spec that represents a subrange of the data defined by this DataSpec.
*
* @param offset The offset of the subrange.
* @param length The length of the subrange.
* @return A data spec that represents a subrange of the data defined by this DataSpec.
*/
public DataSpec subrange(long offset, long length) {
if (offset == 0 && this.length == length) {
return this;
} else {
return new DataSpec(
uri,
uriPositionOffset,
httpMethod,
httpBody,
httpRequestHeaders,
position + offset,
length,
key,
flags,
customData);
}
}
/**
* Returns a copy of this data spec with the specified Uri.
*
* @param uri The new source {@link Uri}.
* @return The copied data spec with the specified Uri.
*/
public DataSpec withUri(Uri uri) {
return new DataSpec(
uri,
uriPositionOffset,
httpMethod,
httpBody,
httpRequestHeaders,
position,
length,
key,
flags,
customData);
}
/**
* Returns a copy of this data spec with the specified HTTP request headers. Headers already in
* the data spec are not copied to the new instance.
*
* @param httpRequestHeaders The HTTP request headers.
* @return The copied data spec with the specified HTTP request headers.
*/
public DataSpec withRequestHeaders(Map<String, String> httpRequestHeaders) {
return new DataSpec(
uri,
uriPositionOffset,
httpMethod,
httpBody,
httpRequestHeaders,
position,
length,
key,
flags,
customData);
}
/**
* Returns a copy this data spec with additional HTTP request headers. Headers in {@code
* additionalHttpRequestHeaders} will overwrite any headers already in the data spec that have the
* same keys.
*
* @param additionalHttpRequestHeaders The additional HTTP request headers.
* @return The copied data spec with the additional HTTP request headers.
*/
public DataSpec withAdditionalHeaders(Map<String, String> additionalHttpRequestHeaders) {
Map<String, String> httpRequestHeaders = new HashMap<>(this.httpRequestHeaders);
httpRequestHeaders.putAll(additionalHttpRequestHeaders);
return new DataSpec(
uri,
uriPositionOffset,
httpMethod,
httpBody,
httpRequestHeaders,
position,
length,
key,
flags,
customData);
}
@Override
public String toString() {
return "DataSpec["
+ getHttpMethodString()
+ " "
+ uri
+ ", "
+ position
+ ", "
+ length
+ ", "
+ key
+ ", "
+ flags
+ "]";
}
}