/*
* Copyright 2023 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.exoplayer.upstream;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.Math.max;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.annotation.StringDef;
import androidx.media3.common.C;
import androidx.media3.common.C.TrackType;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
/**
* This class provides functionality for generating and adding Common Media Client Data (CMCD) data
* to adaptive streaming formats, DASH, HLS, and SmoothStreaming.
*
* <p>It encapsulates the necessary attributes and information relevant to media content playback,
* following the guidelines specified in the CMCD standard document <a
* href="https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf">CTA-5004</a>.
*/
@UnstableApi
public final class CmcdData {
/** {@link CmcdData.Factory} for {@link CmcdData} instances. */
public static final class Factory {
/** Represents the Dynamic Adaptive Streaming over HTTP (DASH) format. */
public static final String STREAMING_FORMAT_DASH = "d";
/** Represents the HTTP Live Streaming (HLS) format. */
public static final String STREAMING_FORMAT_HLS = "h";
/** Represents the Smooth Streaming (SS) format. */
public static final String STREAMING_FORMAT_SS = "s";
/** Represents the Video on Demand (VOD) stream type. */
public static final String STREAM_TYPE_VOD = "v";
/** Represents the Live Streaming stream type. */
public static final String STREAM_TYPE_LIVE = "l";
/** Represents the object type for an initialization segment in a media container. */
public static final String OBJECT_TYPE_INIT_SEGMENT = "i";
/** Represents the object type for audio-only content in a media container. */
public static final String OBJECT_TYPE_AUDIO_ONLY = "a";
/** Represents the object type for video-only content in a media container. */
public static final String OBJECT_TYPE_VIDEO_ONLY = "v";
/** Represents the object type for muxed audio and video content in a media container. */
public static final String OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO = "av";
private static final Pattern CUSTOM_KEY_NAME_PATTERN =
Pattern.compile("[a-zA-Z0-9]+(-[a-zA-Z0-9]+)+");
private final CmcdConfiguration cmcdConfiguration;
private final ExoTrackSelection trackSelection;
private final long bufferedDurationUs;
private final float playbackRate;
private final @CmcdData.StreamingFormat String streamingFormat;
private final boolean isLive;
private final boolean didRebuffer;
private final boolean isBufferEmpty;
private long chunkDurationUs;
@Nullable private @CmcdData.ObjectType String objectType;
@Nullable private String nextObjectRequest;
@Nullable private String nextRangeRequest;
/**
* Creates an instance.
*
* @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
* @param trackSelection The {@linkplain ExoTrackSelection track selection}.
* @param bufferedDurationUs The duration of media currently buffered from the current playback
* position, in microseconds.
* @param playbackRate The playback rate indicating the current speed of playback.
* @param streamingFormat The streaming format of the media content. Must be one of the allowed
* streaming formats specified by the {@link CmcdData.StreamingFormat} annotation.
* @param isLive {@code true} if the media content is being streamed live, {@code false}
* otherwise.
* @param didRebuffer {@code true} if a rebuffering event happened between the previous request
* and this one, {@code false} otherwise.
* @param isBufferEmpty {@code true} if the queue of buffered chunks is empty, {@code false}
* otherwise.
* @throws IllegalArgumentException If {@code bufferedDurationUs} is negative.
*/
public Factory(
CmcdConfiguration cmcdConfiguration,
ExoTrackSelection trackSelection,
long bufferedDurationUs,
float playbackRate,
@CmcdData.StreamingFormat String streamingFormat,
boolean isLive,
boolean didRebuffer,
boolean isBufferEmpty) {
checkArgument(bufferedDurationUs >= 0);
checkArgument(playbackRate > 0);
this.cmcdConfiguration = cmcdConfiguration;
this.trackSelection = trackSelection;
this.bufferedDurationUs = bufferedDurationUs;
this.playbackRate = playbackRate;
this.streamingFormat = streamingFormat;
this.isLive = isLive;
this.didRebuffer = didRebuffer;
this.isBufferEmpty = isBufferEmpty;
this.chunkDurationUs = C.TIME_UNSET;
}
/**
* Retrieves the object type value from the given {@link ExoTrackSelection}.
*
* @param trackSelection The {@link ExoTrackSelection} from which to retrieve the object type.
* @return The object type value as a String if {@link TrackType} can be mapped to one of the
* object types specified by {@link CmcdData.ObjectType} annotation, or {@code null}.
* @throws IllegalArgumentException if the provided {@link ExoTrackSelection} is {@code null}.
*/
@Nullable
public static @CmcdData.ObjectType String getObjectType(ExoTrackSelection trackSelection) {
checkArgument(trackSelection != null);
@TrackType
int trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType);
if (trackType == C.TRACK_TYPE_UNKNOWN) {
trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().containerMimeType);
}
if (trackType == C.TRACK_TYPE_AUDIO) {
return OBJECT_TYPE_AUDIO_ONLY;
} else if (trackType == C.TRACK_TYPE_VIDEO) {
return OBJECT_TYPE_VIDEO_ONLY;
} else {
// Track type cannot be mapped to a known object type.
return null;
}
}
/**
* Sets the duration of current media chunk being requested, in microseconds. The default value
* is {@link C#TIME_UNSET}.
*
* @throws IllegalArgumentException If {@code chunkDurationUs} is negative.
*/
@CanIgnoreReturnValue
public Factory setChunkDurationUs(long chunkDurationUs) {
checkArgument(chunkDurationUs >= 0);
this.chunkDurationUs = chunkDurationUs;
return this;
}
/**
* Sets the object type of the current object being requested. Must be one of the allowed object
* types specified by the {@link CmcdData.ObjectType} annotation.
*
* <p>Default is {@code null}.
*/
@CanIgnoreReturnValue
public Factory setObjectType(@Nullable @CmcdData.ObjectType String objectType) {
this.objectType = objectType;
return this;
}
/**
* Sets the relative path of the next object to be requested. This can be used to trigger
* pre-fetching by the CDN.
*
* <p>Default is {@code null}.
*/
@CanIgnoreReturnValue
public Factory setNextObjectRequest(@Nullable String nextObjectRequest) {
this.nextObjectRequest = nextObjectRequest;
return this;
}
/**
* Sets the byte range representing the partial object request. This can be used to trigger
* pre-fetching by the CDN.
*
* <p>Default is {@code null}.
*/
@CanIgnoreReturnValue
public Factory setNextRangeRequest(@Nullable String nextRangeRequest) {
this.nextRangeRequest = nextRangeRequest;
return this;
}
public CmcdData createCmcdData() {
ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> customData =
cmcdConfiguration.requestConfig.getCustomData();
for (String headerKey : customData.keySet()) {
validateCustomDataListFormat(customData.get(headerKey));
}
int bitrateKbps = Util.ceilDivide(trackSelection.getSelectedFormat().bitrate, 1000);
CmcdObject.Builder cmcdObject = new CmcdObject.Builder();
if (!getIsInitSegment()) {
if (cmcdConfiguration.isBitrateLoggingAllowed()) {
cmcdObject.setBitrateKbps(bitrateKbps);
}
if (cmcdConfiguration.isTopBitrateLoggingAllowed()) {
TrackGroup trackGroup = trackSelection.getTrackGroup();
int topBitrate = trackSelection.getSelectedFormat().bitrate;
for (int i = 0; i < trackGroup.length; i++) {
topBitrate = max(topBitrate, trackGroup.getFormat(i).bitrate);
}
cmcdObject.setTopBitrateKbps(Util.ceilDivide(topBitrate, 1000));
}
if (cmcdConfiguration.isObjectDurationLoggingAllowed()) {
cmcdObject.setObjectDurationMs(Util.usToMs(chunkDurationUs));
}
}
if (cmcdConfiguration.isObjectTypeLoggingAllowed()) {
cmcdObject.setObjectType(objectType);
}
if (customData.containsKey(CmcdConfiguration.KEY_CMCD_OBJECT)) {
cmcdObject.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_OBJECT));
}
CmcdRequest.Builder cmcdRequest = new CmcdRequest.Builder();
if (!getIsInitSegment() && cmcdConfiguration.isBufferLengthLoggingAllowed()) {
cmcdRequest.setBufferLengthMs(Util.usToMs(bufferedDurationUs));
}
if (cmcdConfiguration.isMeasuredThroughputLoggingAllowed()
&& trackSelection.getLatestBitrateEstimate() != C.RATE_UNSET_INT) {
cmcdRequest.setMeasuredThroughputInKbps(
Util.ceilDivide(trackSelection.getLatestBitrateEstimate(), 1000));
}
if (cmcdConfiguration.isDeadlineLoggingAllowed()) {
cmcdRequest.setDeadlineMs(Util.usToMs((long) (bufferedDurationUs / playbackRate)));
}
if (cmcdConfiguration.isStartupLoggingAllowed()) {
cmcdRequest.setStartup(didRebuffer || isBufferEmpty);
}
if (cmcdConfiguration.isNextObjectRequestLoggingAllowed()) {
cmcdRequest.setNextObjectRequest(nextObjectRequest);
}
if (cmcdConfiguration.isNextRangeRequestLoggingAllowed()) {
cmcdRequest.setNextRangeRequest(nextRangeRequest);
}
if (customData.containsKey(CmcdConfiguration.KEY_CMCD_REQUEST)) {
cmcdRequest.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_REQUEST));
}
CmcdSession.Builder cmcdSession = new CmcdSession.Builder();
if (cmcdConfiguration.isContentIdLoggingAllowed()) {
cmcdSession.setContentId(cmcdConfiguration.contentId);
}
if (cmcdConfiguration.isSessionIdLoggingAllowed()) {
cmcdSession.setSessionId(cmcdConfiguration.sessionId);
}
if (cmcdConfiguration.isStreamingFormatLoggingAllowed()) {
cmcdSession.setStreamingFormat(streamingFormat);
}
if (cmcdConfiguration.isStreamTypeLoggingAllowed()) {
cmcdSession.setStreamType(isLive ? STREAM_TYPE_LIVE : STREAM_TYPE_VOD);
}
if (cmcdConfiguration.isPlaybackRateLoggingAllowed()) {
cmcdSession.setPlaybackRate(playbackRate);
}
if (customData.containsKey(CmcdConfiguration.KEY_CMCD_SESSION)) {
cmcdSession.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_SESSION));
}
CmcdStatus.Builder cmcdStatus = new CmcdStatus.Builder();
if (cmcdConfiguration.isMaximumRequestThroughputLoggingAllowed()) {
cmcdStatus.setMaximumRequestedThroughputKbps(
cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps));
}
if (cmcdConfiguration.isBufferStarvationLoggingAllowed()) {
cmcdStatus.setBufferStarvation(didRebuffer);
}
if (customData.containsKey(CmcdConfiguration.KEY_CMCD_STATUS)) {
cmcdStatus.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_STATUS));
}
return new CmcdData(
cmcdObject.build(),
cmcdRequest.build(),
cmcdSession.build(),
cmcdStatus.build(),
cmcdConfiguration.dataTransmissionMode);
}
private boolean getIsInitSegment() {
return objectType != null && objectType.equals(OBJECT_TYPE_INIT_SEGMENT);
}
private void validateCustomDataListFormat(List<String> customDataList) {
for (String customData : customDataList) {
String key = Util.split(customData, "=")[0];
checkState(CUSTOM_KEY_NAME_PATTERN.matcher(key).matches());
}
}
}
/** Indicates the streaming format used for media content. */
@Retention(RetentionPolicy.SOURCE)
@StringDef({
Factory.STREAMING_FORMAT_DASH,
Factory.STREAMING_FORMAT_HLS,
Factory.STREAMING_FORMAT_SS
})
@Documented
@Target(TYPE_USE)
public @interface StreamingFormat {}
/** Indicates the type of streaming for media content. */
@Retention(RetentionPolicy.SOURCE)
@StringDef({Factory.STREAM_TYPE_VOD, Factory.STREAM_TYPE_LIVE})
@Documented
@Target(TYPE_USE)
public @interface StreamType {}
/** Indicates the media type of current object being requested. */
@Retention(RetentionPolicy.SOURCE)
@StringDef({
Factory.OBJECT_TYPE_INIT_SEGMENT,
Factory.OBJECT_TYPE_AUDIO_ONLY,
Factory.OBJECT_TYPE_VIDEO_ONLY,
Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
})
@Documented
@Target(TYPE_USE)
public @interface ObjectType {}
private static final Joiner COMMA_JOINER = Joiner.on(",");
private final CmcdObject cmcdObject;
private final CmcdRequest cmcdRequest;
private final CmcdSession cmcdSession;
private final CmcdStatus cmcdStatus;
private final @CmcdConfiguration.DataTransmissionMode int dataTransmissionMode;
private CmcdData(
CmcdObject cmcdObject,
CmcdRequest cmcdRequest,
CmcdSession cmcdSession,
CmcdStatus cmcdStatus,
@CmcdConfiguration.DataTransmissionMode int datatTransmissionMode) {
this.cmcdObject = cmcdObject;
this.cmcdRequest = cmcdRequest;
this.cmcdSession = cmcdSession;
this.cmcdStatus = cmcdStatus;
this.dataTransmissionMode = datatTransmissionMode;
}
/**
* Adds Common Media Client Data (CMCD) related information to the provided {@link DataSpec}
* object.
*/
public DataSpec addToDataSpec(DataSpec dataSpec) {
ArrayListMultimap<String, String> cmcdDataMap = ArrayListMultimap.create();
cmcdObject.populateCmcdDataMap(cmcdDataMap);
cmcdRequest.populateCmcdDataMap(cmcdDataMap);
cmcdSession.populateCmcdDataMap(cmcdDataMap);
cmcdStatus.populateCmcdDataMap(cmcdDataMap);
if (dataTransmissionMode == CmcdConfiguration.MODE_REQUEST_HEADER) {
ImmutableMap.Builder<String, String> httpRequestHeaders = ImmutableMap.builder();
for (String headerKey : cmcdDataMap.keySet()) {
List<String> headerValues = cmcdDataMap.get(headerKey);
Collections.sort(headerValues);
httpRequestHeaders.put(headerKey, COMMA_JOINER.join(headerValues));
}
return dataSpec.withAdditionalHeaders(httpRequestHeaders.buildOrThrow());
} else {
List<String> keyValuePairs = new ArrayList<>();
for (Collection<String> values : cmcdDataMap.asMap().values()) {
keyValuePairs.addAll(values);
}
Collections.sort(keyValuePairs);
Uri.Builder uriBuilder =
dataSpec
.uri
.buildUpon()
.appendQueryParameter(
CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY,
Uri.encode(COMMA_JOINER.join(keyValuePairs)));
return dataSpec.buildUpon().setUri(uriBuilder.build()).build();
}
}
/**
* Keys whose values vary with the object being requested. Contains CMCD fields: {@code br},
* {@code tb}, {@code d} and {@code ot}.
*/
private static final class CmcdObject {
/** Builder for {@link CmcdObject} instances. */
public static final class Builder {
private int bitrateKbps;
private int topBitrateKbps;
private long objectDurationMs;
@Nullable private @ObjectType String objectType;
private ImmutableList<String> customDataList;
/** Creates a new instance with default values. */
public Builder() {
this.bitrateKbps = C.RATE_UNSET_INT;
this.topBitrateKbps = C.RATE_UNSET_INT;
this.objectDurationMs = C.TIME_UNSET;
this.customDataList = ImmutableList.of();
}
/**
* Sets the {@link CmcdObject#bitrateKbps}. The default value is {@link C#RATE_UNSET_INT}.
*
* @throws IllegalArgumentException If {@code bitrateKbps} is not equal to {@link
* C#RATE_UNSET_INT} and is negative.
*/
@CanIgnoreReturnValue
public Builder setBitrateKbps(int bitrateKbps) {
checkArgument(bitrateKbps >= 0 || bitrateKbps == C.RATE_UNSET_INT);
this.bitrateKbps = bitrateKbps;
return this;
}
/**
* Sets the {@link CmcdObject#topBitrateKbps}. The default value is {@link C#RATE_UNSET_INT}.
*
* @throws IllegalArgumentException If {@code topBitrateKbps} is not equal to {@link
* C#RATE_UNSET_INT} and is negative.
*/
@CanIgnoreReturnValue
public Builder setTopBitrateKbps(int topBitrateKbps) {
checkArgument(topBitrateKbps >= 0 || topBitrateKbps == C.RATE_UNSET_INT);
this.topBitrateKbps = topBitrateKbps;
return this;
}
/**
* Sets the {@link CmcdObject#objectDurationMs}. The default value is {@link C#TIME_UNSET}.
*
* @throws IllegalArgumentException If {@code objectDurationMs} is not equal to {@link
* C#TIME_UNSET} and is negative.
*/
@CanIgnoreReturnValue
public Builder setObjectDurationMs(long objectDurationMs) {
checkArgument(objectDurationMs >= 0 || objectDurationMs == C.TIME_UNSET);
this.objectDurationMs = objectDurationMs;
return this;
}
/** Sets the {@link CmcdObject#objectType}. The default value is {@code null}. */
@CanIgnoreReturnValue
public Builder setObjectType(@Nullable @ObjectType String objectType) {
this.objectType = objectType;
return this;
}
/** Sets the {@link CmcdObject#customDataList}. The default value is an empty list. */
@CanIgnoreReturnValue
public Builder setCustomDataList(List<String> customDataList) {
this.customDataList = ImmutableList.copyOf(customDataList);
return this;
}
public CmcdObject build() {
return new CmcdObject(this);
}
}
/**
* The encoded bitrate in kbps of the audio or video object being requested, or {@link
* C#RATE_UNSET_INT} if unset.
*
* <p>This may not be known precisely by the player; however, it MAY be estimated based upon
* playlist/manifest declarations. If the playlist declares both peak and average bitrate
* values, the peak value should be transmitted.
*/
public final int bitrateKbps;
/**
* The highest bitrate rendition, in kbps, in the manifest or playlist that the client is
* allowed to play, given current codec, licensing and sizing constraints. If unset, it is
* represented by the value {@link C#RATE_UNSET_INT}.
*/
public final int topBitrateKbps;
/**
* The playback duration in milliseconds of the object being requested, or {@link C#TIME_UNSET}
* if unset. If a partial segment is being requested, then this value MUST indicate the playback
* duration of that part and not that of its parent segment. This value can be an approximation
* of the estimated duration if the explicit value is not known.
*/
public final long objectDurationMs;
/**
* The media type of the current object being requested , or {@code null} if unset. Must be one
* of the allowed object types specified by the {@link ObjectType} annotation.
*
* <p>If the object type being requested is unknown, then this key MUST NOT be used.
*/
@Nullable public final @ObjectType String objectType;
/** Custom data that vary based on the specific object being requested. */
public final ImmutableList<String> customDataList;
private CmcdObject(Builder builder) {
this.bitrateKbps = builder.bitrateKbps;
this.topBitrateKbps = builder.topBitrateKbps;
this.objectDurationMs = builder.objectDurationMs;
this.objectType = builder.objectType;
this.customDataList = builder.customDataList;
}
/**
* Populates the {@code cmcdDataMap} with {@link CmcdConfiguration#KEY_CMCD_OBJECT} values.
*
* @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added.
*/
public void populateCmcdDataMap(
ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
ArrayList<String> keyValuePairs = new ArrayList<>();
if (bitrateKbps != C.RATE_UNSET_INT) {
keyValuePairs.add(CmcdConfiguration.KEY_BITRATE + "=" + bitrateKbps);
}
if (topBitrateKbps != C.RATE_UNSET_INT) {
keyValuePairs.add(CmcdConfiguration.KEY_TOP_BITRATE + "=" + topBitrateKbps);
}
if (objectDurationMs != C.TIME_UNSET) {
keyValuePairs.add(CmcdConfiguration.KEY_OBJECT_DURATION + "=" + objectDurationMs);
}
if (!TextUtils.isEmpty(objectType)) {
keyValuePairs.add(CmcdConfiguration.KEY_OBJECT_TYPE + "=" + objectType);
}
keyValuePairs.addAll(customDataList);
if (!keyValuePairs.isEmpty()) {
cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_OBJECT, keyValuePairs);
}
}
}
/**
* Keys whose values vary with each request. Contains CMCD fields: {@code bl}, {@code mtp}, {@code
* dl}, {@code su}, {@code nor} and {@code nrr}.
*/
private static final class CmcdRequest {
/** Builder for {@link CmcdRequest} instances. */
public static final class Builder {
private long bufferLengthMs;
private long measuredThroughputInKbps;
private long deadlineMs;
private boolean startup;
@Nullable private String nextObjectRequest;
@Nullable private String nextRangeRequest;
private ImmutableList<String> customDataList;
/** Creates a new instance with default values. */
public Builder() {
this.bufferLengthMs = C.TIME_UNSET;
this.measuredThroughputInKbps = C.RATE_UNSET_INT;
this.deadlineMs = C.TIME_UNSET;
this.customDataList = ImmutableList.of();
}
/**
* Sets the {@link CmcdRequest#bufferLengthMs}. Rounded to nearest 100 ms. The default value
* is {@link C#TIME_UNSET}.
*
* @throws IllegalArgumentException If {@code bufferLengthMs} is not equal to {@link
* C#TIME_UNSET} and is negative.
*/
@CanIgnoreReturnValue
public Builder setBufferLengthMs(long bufferLengthMs) {
checkArgument(bufferLengthMs >= 0 || bufferLengthMs == C.TIME_UNSET);
this.bufferLengthMs = ((bufferLengthMs + 50) / 100) * 100;
return this;
}
/**
* Sets the {@link CmcdRequest#measuredThroughputInKbps}. Rounded to nearest 100 kbps. The
* default value is {@link C#RATE_UNSET_INT}.
*
* @throws IllegalArgumentException If {@code measuredThroughputInKbps} is not equal to {@link
* C#RATE_UNSET_INT} and is negative.
*/
@CanIgnoreReturnValue
public Builder setMeasuredThroughputInKbps(long measuredThroughputInKbps) {
checkArgument(
measuredThroughputInKbps >= 0 || measuredThroughputInKbps == C.RATE_UNSET_INT);
this.measuredThroughputInKbps = ((measuredThroughputInKbps + 50) / 100) * 100;
return this;
}
/**
* Sets the {@link CmcdRequest#deadlineMs}. Rounded to nearest 100 ms. The default value is
* {@link C#TIME_UNSET}.
*
* @throws IllegalArgumentException If {@code deadlineMs} is not equal to {@link C#TIME_UNSET}
* and is negative.
*/
@CanIgnoreReturnValue
public Builder setDeadlineMs(long deadlineMs) {
checkArgument(deadlineMs >= 0 || deadlineMs == C.TIME_UNSET);
this.deadlineMs = ((deadlineMs + 50) / 100) * 100;
return this;
}
/** Sets the {@link CmcdRequest#startup}. The default value is {@code false}. */
@CanIgnoreReturnValue
public Builder setStartup(boolean startup) {
this.startup = startup;
return this;
}
/**
* Sets the {@link CmcdRequest#nextObjectRequest}. This string is URL encoded. The default
* value is {@code null}.
*/
@CanIgnoreReturnValue
public Builder setNextObjectRequest(@Nullable String nextObjectRequest) {
this.nextObjectRequest = nextObjectRequest == null ? null : Uri.encode(nextObjectRequest);
return this;
}
/** Sets the {@link CmcdRequest#nextRangeRequest}. The default value is {@code null}. */
@CanIgnoreReturnValue
public Builder setNextRangeRequest(@Nullable String nextRangeRequest) {
this.nextRangeRequest = nextRangeRequest;
return this;
}
/** Sets the {@link CmcdRequest#customDataList}. The default value is an empty list. */
@CanIgnoreReturnValue
public Builder setCustomDataList(List<String> customDataList) {
this.customDataList = ImmutableList.copyOf(customDataList);
return this;
}
public CmcdRequest build() {
return new CmcdRequest(this);
}
}
/**
* The buffer length in milliseconds associated with the media object being requested, or {@link
* C#TIME_UNSET} if unset.
*
* <p>This key SHOULD only be sent with an {@link CmcdObject#objectType} of {@link
* Factory#OBJECT_TYPE_AUDIO_ONLY}, {@link Factory#OBJECT_TYPE_VIDEO_ONLY} or {@link
* Factory#OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO}.
*
* <p>This value MUST be rounded to the nearest 100 ms.
*/
public final long bufferLengthMs;
/**
* The throughput between client and server, as measured by the client, or {@link
* C#RATE_UNSET_INT} if unset.
*
* <p>This value MUST be rounded to the nearest 100 kbps. This value, however derived, SHOULD be
* the value that the client is using to make its next Adaptive Bitrate switching decision. If
* the client is connected to multiple servers concurrently, it must take care to report only
* the throughput measured against the receiving server. If the client has multiple concurrent
* connections to the server, then the intent is that this value communicates the aggregate
* throughput the client sees across all those connections.
*/
public final long measuredThroughputInKbps;
/**
* Deadline in milliseconds from the request time until the first sample of this Segment/Object
* needs to be available in order to not create a buffer underrun or any other playback
* problems, or {@link C#TIME_UNSET} if unset.
*
* <p>This value MUST be rounded to the nearest 100 ms. For a playback rate of 1, this may be
* equivalent to the player’s remaining buffer length.
*/
public final long deadlineMs;
/**
* A boolean indicating whether the chunk is needed urgently due to startup, seeking or recovery
* after a buffer-empty event, or {@code false} if unknown. The media SHOULD not be rendering
* when this request is made.
*/
public final boolean startup;
/**
* Relative path of the next object to be requested, or {@code null} if unset. This can be used
* to trigger pre-fetching by the CDN. This MUST be a path relative to the current request.
*
* <p>This string MUST be URL encoded.
*
* <p><b>Note:</b> The client SHOULD NOT depend upon any pre-fetch action being taken - it is
* merely a request for such a pre-fetch to take place.
*/
@Nullable public final String nextObjectRequest;
/**
* The byte range representing the partial object request, or {@code null} if unset. If the
* {@link #nextObjectRequest} field is not set, then the object is assumed to match the object
* currently being requested.
*
* <p><b>Note:</b> The client SHOULD NOT depend upon any pre-fetch action being taken - it is
* merely a request for such a pre-fetch to take place.
*/
@Nullable public final String nextRangeRequest;
/** Custom data that vary with each request. */
public final ImmutableList<String> customDataList;
private CmcdRequest(Builder builder) {
this.bufferLengthMs = builder.bufferLengthMs;
this.measuredThroughputInKbps = builder.measuredThroughputInKbps;
this.deadlineMs = builder.deadlineMs;
this.startup = builder.startup;
this.nextObjectRequest = builder.nextObjectRequest;
this.nextRangeRequest = builder.nextRangeRequest;
this.customDataList = builder.customDataList;
}
/**
* Populates the {@code cmcdDataMap} with {@link CmcdConfiguration#KEY_CMCD_REQUEST} values.
*
* @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added.
*/
public void populateCmcdDataMap(
ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
ArrayList<String> keyValuePairs = new ArrayList<>();
if (bufferLengthMs != C.TIME_UNSET) {
keyValuePairs.add(CmcdConfiguration.KEY_BUFFER_LENGTH + "=" + bufferLengthMs);
}
if (measuredThroughputInKbps != C.RATE_UNSET_INT) {
keyValuePairs.add(
CmcdConfiguration.KEY_MEASURED_THROUGHPUT + "=" + measuredThroughputInKbps);
}
if (deadlineMs != C.TIME_UNSET) {
keyValuePairs.add(CmcdConfiguration.KEY_DEADLINE + "=" + deadlineMs);
}
if (startup) {
keyValuePairs.add(CmcdConfiguration.KEY_STARTUP);
}
if (!TextUtils.isEmpty(nextObjectRequest)) {
keyValuePairs.add(
Util.formatInvariant(
"%s=\"%s\"", CmcdConfiguration.KEY_NEXT_OBJECT_REQUEST, nextObjectRequest));
}
if (!TextUtils.isEmpty(nextRangeRequest)) {
keyValuePairs.add(
Util.formatInvariant(
"%s=\"%s\"", CmcdConfiguration.KEY_NEXT_RANGE_REQUEST, nextRangeRequest));
}
keyValuePairs.addAll(customDataList);
if (!keyValuePairs.isEmpty()) {
cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_REQUEST, keyValuePairs);
}
}
}
/**
* Keys whose values are expected to be invariant over the life of the session. Contains CMCD
* fields: {@code cid}, {@code sid}, {@code sf}, {@code st}, {@code pr} and {@code v}.
*/
private static final class CmcdSession {
/** Builder for {@link CmcdSession} instances. */
public static final class Builder {
@Nullable private String contentId;
@Nullable private String sessionId;
@Nullable private @StreamingFormat String streamingFormat;
@Nullable private @StreamType String streamType;
private float playbackRate;
private ImmutableList<String> customDataList;
/** Creates a new instance with default values. */
public Builder() {
this.customDataList = ImmutableList.of();
}
/**
* Sets the {@link CmcdSession#contentId}. Maximum length allowed is 64 characters. The
* default value is {@code null}.
*
* @throws IllegalArgumentException If {@code contentId} is null or its length exceeds {@link
* CmcdConfiguration#MAX_ID_LENGTH}.
*/
@CanIgnoreReturnValue
public Builder setContentId(@Nullable String contentId) {
checkArgument(contentId == null || contentId.length() <= CmcdConfiguration.MAX_ID_LENGTH);
this.contentId = contentId;
return this;
}
/**
* Sets the {@link CmcdSession#sessionId}. Maximum length allowed is 64 characters. The
* default value is {@code null}.
*
* @throws IllegalArgumentException If {@code sessionId} is null or its length exceeds {@link
* CmcdConfiguration#MAX_ID_LENGTH}.
*/
@CanIgnoreReturnValue
public Builder setSessionId(@Nullable String sessionId) {
checkArgument(sessionId == null || sessionId.length() <= CmcdConfiguration.MAX_ID_LENGTH);
this.sessionId = sessionId;
return this;
}
/** Sets the {@link CmcdSession#streamingFormat}. The default value is {@code null}. */
@CanIgnoreReturnValue
public Builder setStreamingFormat(@Nullable @StreamingFormat String streamingFormat) {
this.streamingFormat = streamingFormat;
return this;
}
/** Sets the {@link CmcdSession#streamType}. The default value is {@code null}. */
@CanIgnoreReturnValue
public Builder setStreamType(@Nullable @StreamType String streamType) {
this.streamType = streamType;
return this;
}
/**
* Sets the {@link CmcdSession#playbackRate}. The default value is {@link C#RATE_UNSET}.
*
* @throws IllegalArgumentException If {@code playbackRate} is not equal to {@link
* C#RATE_UNSET} and is non-positive.
*/
@CanIgnoreReturnValue
public Builder setPlaybackRate(float playbackRate) {
checkArgument(playbackRate > 0 || playbackRate == C.RATE_UNSET);
this.playbackRate = playbackRate;
return this;
}
/** Sets the {@link CmcdSession#customDataList}. The default value is an empty list. */
@CanIgnoreReturnValue
public Builder setCustomDataList(List<String> customDataList) {
this.customDataList = ImmutableList.copyOf(customDataList);
return this;
}
public CmcdSession build() {
return new CmcdSession(this);
}
}
/**
* The version of this specification used for interpreting the defined key names and values. If
* this key is omitted, the client and server MUST interpret the values as being defined by
* version 1. Client SHOULD omit this field if the version is 1.
*/
public static final int VERSION = 1;
/**
* A GUID identifying the current content, or {@code null} if unset.
*
* <p>This value is consistent across multiple different sessions and devices and is defined and
* updated at the discretion of the service provider. Maximum length is 64 characters.
*/
@Nullable public final String contentId;
/**
* A GUID identifying the current playback session, or {@code null} if unset.
*
* <p>A playback session typically ties together segments belonging to a single media asset.
* Maximum length is 64 characters.
*/
@Nullable public final String sessionId;
/**
* The streaming format that defines the current request, or{@code null} if unset. Must be one
* of the allowed stream formats specified by the {@link StreamingFormat} annotation. If the
* streaming format being requested is unknown, then this key MUST NOT be used.
*/
@Nullable public final @StreamingFormat String streamingFormat;
/**
* Type of stream, or {@code null} if unset. Must be one of the allowed stream types specified
* by the {@link StreamType} annotation.
*/
@Nullable public final @StreamType String streamType;
/**
* The playback rate indicating the current rate of playback, or {@link C#RATE_UNSET} if unset.
*/
public final float playbackRate;
/** Custom data that is expected to be invariant over the life of the session. */
public final ImmutableList<String> customDataList;
private CmcdSession(Builder builder) {
this.contentId = builder.contentId;
this.sessionId = builder.sessionId;
this.streamingFormat = builder.streamingFormat;
this.streamType = builder.streamType;
this.playbackRate = builder.playbackRate;
this.customDataList = builder.customDataList;
}
/**
* Populates the {@code cmcdDataMap} with {@link CmcdConfiguration#KEY_CMCD_SESSION} values.
*
* @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added.
*/
public void populateCmcdDataMap(
ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
ArrayList<String> keyValuePairs = new ArrayList<>();
if (!TextUtils.isEmpty(this.contentId)) {
keyValuePairs.add(
Util.formatInvariant("%s=\"%s\"", CmcdConfiguration.KEY_CONTENT_ID, contentId));
}
if (!TextUtils.isEmpty(this.sessionId)) {
keyValuePairs.add(
Util.formatInvariant("%s=\"%s\"", CmcdConfiguration.KEY_SESSION_ID, sessionId));
}
if (!TextUtils.isEmpty(this.streamingFormat)) {
keyValuePairs.add(CmcdConfiguration.KEY_STREAMING_FORMAT + "=" + streamingFormat);
}
if (!TextUtils.isEmpty(this.streamType)) {
keyValuePairs.add(CmcdConfiguration.KEY_STREAM_TYPE + "=" + streamType);
}
if (playbackRate != C.RATE_UNSET && playbackRate != 1.0f) {
keyValuePairs.add(
Util.formatInvariant("%s=%.2f", CmcdConfiguration.KEY_PLAYBACK_RATE, playbackRate));
}
if (VERSION != 1) {
keyValuePairs.add(CmcdConfiguration.KEY_VERSION + "=" + VERSION);
}
keyValuePairs.addAll(customDataList);
if (!keyValuePairs.isEmpty()) {
cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_SESSION, keyValuePairs);
}
}
}
/**
* Keys whose values do not vary with every request or object. Contains CMCD fields: {@code rtp}
* and {@code bs}.
*/
private static final class CmcdStatus {
/** Builder for {@link CmcdStatus} instances. */
public static final class Builder {
private int maximumRequestedThroughputKbps;
private boolean bufferStarvation;
private ImmutableList<String> customDataList;
/** Creates a new instance with default values. */
public Builder() {
this.maximumRequestedThroughputKbps = C.RATE_UNSET_INT;
this.customDataList = ImmutableList.of();
}
/**
* Sets the {@link CmcdStatus#maximumRequestedThroughputKbps}. Rounded to nearest 100 kbps.
* The default value is {@link C#RATE_UNSET_INT}.
*
* @throws IllegalArgumentException If {@code maximumRequestedThroughputKbps} is not equal to
* {@link C#RATE_UNSET_INT} and is negative.
*/
@CanIgnoreReturnValue
public Builder setMaximumRequestedThroughputKbps(int maximumRequestedThroughputKbps) {
checkArgument(
maximumRequestedThroughputKbps >= 0
|| maximumRequestedThroughputKbps == C.RATE_UNSET_INT);
this.maximumRequestedThroughputKbps =
maximumRequestedThroughputKbps == C.RATE_UNSET_INT
? maximumRequestedThroughputKbps
: ((maximumRequestedThroughputKbps + 50) / 100) * 100;
return this;
}
/** Sets the {@link CmcdStatus#bufferStarvation}. The default value is {@code false}. */
@CanIgnoreReturnValue
public Builder setBufferStarvation(boolean bufferStarvation) {
this.bufferStarvation = bufferStarvation;
return this;
}
/** Sets the {@link CmcdStatus#customDataList}. The default value is an empty list. */
@CanIgnoreReturnValue
public Builder setCustomDataList(List<String> customDataList) {
this.customDataList = ImmutableList.copyOf(customDataList);
return this;
}
public CmcdStatus build() {
return new CmcdStatus(this);
}
}
/**
* The requested maximum throughput in kbps that the client considers sufficient for delivery of
* the asset, or {@link C#RATE_UNSET_INT} if unset. Values MUST be rounded to the nearest
* 100kbps.
*/
public final int maximumRequestedThroughputKbps;
/**
* A boolean indicating whether the buffer was starved at some point between the prior request
* and this chunk request, resulting in the player being in a rebuffering state and the video or
* audio playback being stalled, or {@code false} if unknown.
*/
public final boolean bufferStarvation;
/** Custom data that do not vary with every request or object. */
public final ImmutableList<String> customDataList;
private CmcdStatus(Builder builder) {
this.maximumRequestedThroughputKbps = builder.maximumRequestedThroughputKbps;
this.bufferStarvation = builder.bufferStarvation;
this.customDataList = builder.customDataList;
}
/**
* Populates the {@code cmcdDataMap} with {@link CmcdConfiguration#KEY_CMCD_STATUS} values.
*
* @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added.
*/
public void populateCmcdDataMap(
ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
ArrayList<String> keyValuePairs = new ArrayList<>();
if (maximumRequestedThroughputKbps != C.RATE_UNSET_INT) {
keyValuePairs.add(
CmcdConfiguration.KEY_MAXIMUM_REQUESTED_BITRATE + "=" + maximumRequestedThroughputKbps);
}
if (bufferStarvation) {
keyValuePairs.add(CmcdConfiguration.KEY_BUFFER_STARVATION);
}
keyValuePairs.addAll(customDataList);
if (!keyValuePairs.isEmpty()) {
cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_STATUS, keyValuePairs);
}
}
}
}