diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 4289b972e9..acb85e4a18 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -12,6 +12,8 @@
* Add additional fields to Common Media Client Data (CMCD) logging: next
object request (`nor`) and next range request (`nrr`)
([#8699](https://github.com/google/ExoPlayer/issues/8699)).
+ * Add functionality to transmit Common Media Client Data (CMCD) data using
+ query parameters ([#553](https://github.com/androidx/media/issues/553)).
* Transformer:
* Changed `frameRate` and `durationUs` parameters of
`SampleConsumer.queueInputBitmap` to `TimestampIterator`.
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java
index e73adcec96..b302f0b60a 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java
@@ -19,6 +19,7 @@ import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.annotation.ElementType.TYPE_USE;
+import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.StringDef;
import androidx.media3.common.C;
@@ -79,6 +80,13 @@ public final class CmcdConfiguration {
@Target(TYPE_USE)
public @interface CmcdKey {}
+ /** Indicates the mode used for data transmission. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({MODE_REQUEST_HEADER, MODE_QUERY_PARAMETER})
+ @Documented
+ @Target(TYPE_USE)
+ public @interface DataTransmissionMode {}
+
/** Maximum length for ID fields. */
public static final int MAX_ID_LENGTH = 64;
@@ -86,6 +94,7 @@ public final class CmcdConfiguration {
public static final String KEY_CMCD_REQUEST = "CMCD-Request";
public static final String KEY_CMCD_SESSION = "CMCD-Session";
public static final String KEY_CMCD_STATUS = "CMCD-Status";
+ public static final String CMCD_QUERY_PARAMETER_KEY = "CMCD";
public static final String KEY_BITRATE = "br";
public static final String KEY_BUFFER_LENGTH = "bl";
public static final String KEY_CONTENT_ID = "cid";
@@ -104,6 +113,8 @@ public final class CmcdConfiguration {
public static final String KEY_STARTUP = "su";
public static final String KEY_NEXT_OBJECT_REQUEST = "nor";
public static final String KEY_NEXT_RANGE_REQUEST = "nrr";
+ public static final int MODE_REQUEST_HEADER = 0;
+ public static final int MODE_QUERY_PARAMETER = 1;
/**
* Factory for {@link CmcdConfiguration} instances.
@@ -230,15 +241,28 @@ public final class CmcdConfiguration {
/** Dynamic request specific configuration. */
public final RequestConfig requestConfig;
- /** Creates an instance. */
+ /** Mode used for data transmission. */
+ public final @DataTransmissionMode int dataTransmissionMode;
+
+ /** Creates an instance with {@link #dataTransmissionMode} set to {@link #MODE_REQUEST_HEADER}. */
public CmcdConfiguration(
@Nullable String sessionId, @Nullable String contentId, RequestConfig requestConfig) {
+ this(sessionId, contentId, requestConfig, MODE_REQUEST_HEADER);
+ }
+
+ /** Creates an instance. */
+ public CmcdConfiguration(
+ @Nullable String sessionId,
+ @Nullable String contentId,
+ RequestConfig requestConfig,
+ @DataTransmissionMode int dataTransmissionMode) {
checkArgument(sessionId == null || sessionId.length() <= MAX_ID_LENGTH);
checkArgument(contentId == null || contentId.length() <= MAX_ID_LENGTH);
checkNotNull(requestConfig);
this.sessionId = sessionId;
this.contentId = contentId;
this.requestConfig = requestConfig;
+ this.dataTransmissionMode = dataTransmissionMode;
}
/**
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java
similarity index 59%
rename from libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java
rename to libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java
index f76807725e..35f3a1b53a 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java
@@ -30,8 +30,10 @@ 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;
@@ -41,62 +43,308 @@ 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 serves as a factory for generating Common Media Client Data (CMCD) HTTP request
- * headers in adaptive streaming formats, DASH, HLS, and SmoothStreaming.
+ * This class provides functionality for generating and adding Common Media Client Data (CMCD) data
+ * to adaptive streaming formats, DASH, HLS, and SmoothStreaming.
*
*
It encapsulates the necessary attributes and information relevant to media content playback,
* following the guidelines specified in the CMCD standard document CTA-5004.
*/
@UnstableApi
-public final class CmcdHeadersFactory {
+public final class CmcdData {
- private static final Joiner COMMA_JOINER = Joiner.on(",");
- private static final Pattern CUSTOM_KEY_NAME_PATTERN =
- Pattern.compile("[a-zA-Z0-9]+(-[a-zA-Z0-9]+)+");
+ /** {@link CmcdData.Factory} for {@link CmcdData} instances. */
+ public static final class Factory {
- /**
- * 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 ObjectType} annotation, or {@code null}.
- * @throws IllegalArgumentException if the provided {@link ExoTrackSelection} is {@code null}.
- */
- @Nullable
- public static @ObjectType String getObjectType(ExoTrackSelection trackSelection) {
- checkArgument(trackSelection != null);
- @C.TrackType
- int trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType);
- if (trackType == C.TRACK_TYPE_UNKNOWN) {
- trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().containerMimeType);
+ /** 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;
}
- 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;
+ /**
+ * 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.
+ *
+ *
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.
+ *
+ *
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.
+ *
+ *
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 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({STREAMING_FORMAT_DASH, STREAMING_FORMAT_HLS, STREAMING_FORMAT_SS})
+ @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({STREAM_TYPE_VOD, STREAM_TYPE_LIVE})
+ @StringDef({Factory.STREAM_TYPE_VOD, Factory.STREAM_TYPE_LIVE})
@Documented
@Target(TYPE_USE)
public @interface StreamType {}
@@ -104,251 +352,69 @@ public final class CmcdHeadersFactory {
/** Indicates the media type of current object being requested. */
@Retention(RetentionPolicy.SOURCE)
@StringDef({
- OBJECT_TYPE_INIT_SEGMENT,
- OBJECT_TYPE_AUDIO_ONLY,
- OBJECT_TYPE_VIDEO_ONLY,
- OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
+ 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 {}
- /** Represents the Dynamic Adaptive Streaming over HTTP (DASH) format. */
- public static final String STREAMING_FORMAT_DASH = "d";
+ private static final Joiner COMMA_JOINER = Joiner.on(",");
- /** Represents the HTTP Live Streaming (HLS) format. */
- public static final String STREAMING_FORMAT_HLS = "h";
+ private final CmcdObject cmcdObject;
+ private final CmcdRequest cmcdRequest;
+ private final CmcdSession cmcdSession;
+ private final CmcdStatus cmcdStatus;
+ private final @CmcdConfiguration.DataTransmissionMode int dataTransmissionMode;
- /** 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 final CmcdConfiguration cmcdConfiguration;
- private final ExoTrackSelection trackSelection;
- private final long bufferedDurationUs;
- private final float playbackRate;
- private final @StreamingFormat String streamingFormat;
- private final boolean isLive;
- private final boolean didRebuffer;
- private final boolean isBufferEmpty;
- private long chunkDurationUs;
- private @Nullable @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 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 CmcdHeadersFactory(
- CmcdConfiguration cmcdConfiguration,
- ExoTrackSelection trackSelection,
- long bufferedDurationUs,
- float playbackRate,
- @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;
+ 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;
}
/**
- * 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.
+ * Adds Common Media Client Data (CMCD) related information to the provided {@link DataSpec}
+ * object.
*/
- @CanIgnoreReturnValue
- public CmcdHeadersFactory setChunkDurationUs(long chunkDurationUs) {
- checkArgument(chunkDurationUs >= 0);
- this.chunkDurationUs = chunkDurationUs;
- return this;
- }
+ public DataSpec addToDataSpec(DataSpec dataSpec) {
+ ArrayListMultimap cmcdDataMap = ArrayListMultimap.create();
+ cmcdObject.populateCmcdDataMap(cmcdDataMap);
+ cmcdRequest.populateCmcdDataMap(cmcdDataMap);
+ cmcdSession.populateCmcdDataMap(cmcdDataMap);
+ cmcdStatus.populateCmcdDataMap(cmcdDataMap);
- /**
- * Sets the object type of the current object being requested. Must be one of the allowed object
- * types specified by the {@link ObjectType} annotation.
- *
- * Default is {@code null}.
- */
- @CanIgnoreReturnValue
- public CmcdHeadersFactory setObjectType(@Nullable @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.
- *
- *
Default is {@code null}.
- */
- @CanIgnoreReturnValue
- public CmcdHeadersFactory 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.
- *
- *
Default is {@code null}.
- */
- @CanIgnoreReturnValue
- public CmcdHeadersFactory setNextRangeRequest(@Nullable String nextRangeRequest) {
- this.nextRangeRequest = nextRangeRequest;
- return this;
- }
-
- /** Creates and returns a new {@link ImmutableMap} containing the CMCD HTTP request headers. */
- public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> createHttpRequestHeaders() {
- 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 (dataTransmissionMode == CmcdConfiguration.MODE_REQUEST_HEADER) {
+ ImmutableMap.Builder httpRequestHeaders = ImmutableMap.builder();
+ for (String headerKey : cmcdDataMap.keySet()) {
+ List headerValues = cmcdDataMap.get(headerKey);
+ Collections.sort(headerValues);
+ httpRequestHeaders.put(headerKey, COMMA_JOINER.join(headerValues));
}
- 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));
+ return dataSpec.withAdditionalHeaders(httpRequestHeaders.buildOrThrow());
+ } else {
+ List keyValuePairs = new ArrayList<>();
+ for (Collection values : cmcdDataMap.asMap().values()) {
+ keyValuePairs.addAll(values);
}
- 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));
- }
-
- ImmutableMap.Builder httpRequestHeaders = ImmutableMap.builder();
- cmcdObject.build().populateHttpRequestHeaders(httpRequestHeaders);
- cmcdRequest.build().populateHttpRequestHeaders(httpRequestHeaders);
- cmcdSession.build().populateHttpRequestHeaders(httpRequestHeaders);
- cmcdStatus.build().populateHttpRequestHeaders(httpRequestHeaders);
- return httpRequestHeaders.buildOrThrow();
- }
-
- private boolean getIsInitSegment() {
- return objectType != null && objectType.equals(OBJECT_TYPE_INIT_SEGMENT);
- }
-
- private void validateCustomDataListFormat(List customDataList) {
- for (String customData : customDataList) {
- String key = Util.split(customData, "=")[0];
- checkState(CUSTOM_KEY_NAME_PATTERN.matcher(key).matches());
+ 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();
}
}
@@ -477,32 +543,29 @@ public final class CmcdHeadersFactory {
}
/**
- * Populates the HTTP request headers with {@link CmcdConfiguration#KEY_CMCD_OBJECT} values.
+ * Populates the {@code cmcdDataMap} with {@link CmcdConfiguration#KEY_CMCD_OBJECT} values.
*
- * @param httpRequestHeaders An {@link ImmutableMap.Builder} used to build the HTTP request
- * headers.
+ * @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added.
*/
- public void populateHttpRequestHeaders(
- ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) {
- ArrayList headerValueList = new ArrayList<>();
+ public void populateCmcdDataMap(
+ ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
+ ArrayList keyValuePairs = new ArrayList<>();
if (bitrateKbps != C.RATE_UNSET_INT) {
- headerValueList.add(CmcdConfiguration.KEY_BITRATE + "=" + bitrateKbps);
+ keyValuePairs.add(CmcdConfiguration.KEY_BITRATE + "=" + bitrateKbps);
}
if (topBitrateKbps != C.RATE_UNSET_INT) {
- headerValueList.add(CmcdConfiguration.KEY_TOP_BITRATE + "=" + topBitrateKbps);
+ keyValuePairs.add(CmcdConfiguration.KEY_TOP_BITRATE + "=" + topBitrateKbps);
}
if (objectDurationMs != C.TIME_UNSET) {
- headerValueList.add(CmcdConfiguration.KEY_OBJECT_DURATION + "=" + objectDurationMs);
+ keyValuePairs.add(CmcdConfiguration.KEY_OBJECT_DURATION + "=" + objectDurationMs);
}
if (!TextUtils.isEmpty(objectType)) {
- headerValueList.add(CmcdConfiguration.KEY_OBJECT_TYPE + "=" + objectType);
+ keyValuePairs.add(CmcdConfiguration.KEY_OBJECT_TYPE + "=" + objectType);
}
- headerValueList.addAll(customDataList);
+ keyValuePairs.addAll(customDataList);
- if (!headerValueList.isEmpty()) {
- Collections.sort(headerValueList);
- httpRequestHeaders.put(
- CmcdConfiguration.KEY_CMCD_OBJECT, COMMA_JOINER.join(headerValueList));
+ if (!keyValuePairs.isEmpty()) {
+ cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_OBJECT, keyValuePairs);
}
}
}
@@ -616,8 +679,8 @@ public final class CmcdHeadersFactory {
* C#TIME_UNSET} if unset.
*
* This key SHOULD only be sent with an {@link CmcdObject#objectType} of {@link
- * #OBJECT_TYPE_AUDIO_ONLY}, {@link #OBJECT_TYPE_VIDEO_ONLY} or {@link
- * #OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO}.
+ * Factory#OBJECT_TYPE_AUDIO_ONLY}, {@link Factory#OBJECT_TYPE_VIDEO_ONLY} or {@link
+ * Factory#OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO}.
*
*
This value MUST be rounded to the nearest 100 ms.
*/
@@ -688,43 +751,40 @@ public final class CmcdHeadersFactory {
}
/**
- * Populates the HTTP request headers with {@link CmcdConfiguration#KEY_CMCD_REQUEST} values.
+ * Populates the {@code cmcdDataMap} with {@link CmcdConfiguration#KEY_CMCD_REQUEST} values.
*
- * @param httpRequestHeaders An {@link ImmutableMap.Builder} used to build the HTTP request
- * headers.
+ * @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added.
*/
- public void populateHttpRequestHeaders(
- ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) {
- ArrayList headerValueList = new ArrayList<>();
+ public void populateCmcdDataMap(
+ ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
+ ArrayList keyValuePairs = new ArrayList<>();
if (bufferLengthMs != C.TIME_UNSET) {
- headerValueList.add(CmcdConfiguration.KEY_BUFFER_LENGTH + "=" + bufferLengthMs);
+ keyValuePairs.add(CmcdConfiguration.KEY_BUFFER_LENGTH + "=" + bufferLengthMs);
}
if (measuredThroughputInKbps != C.RATE_UNSET_INT) {
- headerValueList.add(
+ keyValuePairs.add(
CmcdConfiguration.KEY_MEASURED_THROUGHPUT + "=" + measuredThroughputInKbps);
}
if (deadlineMs != C.TIME_UNSET) {
- headerValueList.add(CmcdConfiguration.KEY_DEADLINE + "=" + deadlineMs);
+ keyValuePairs.add(CmcdConfiguration.KEY_DEADLINE + "=" + deadlineMs);
}
if (startup) {
- headerValueList.add(CmcdConfiguration.KEY_STARTUP);
+ keyValuePairs.add(CmcdConfiguration.KEY_STARTUP);
}
if (!TextUtils.isEmpty(nextObjectRequest)) {
- headerValueList.add(
+ keyValuePairs.add(
Util.formatInvariant(
"%s=\"%s\"", CmcdConfiguration.KEY_NEXT_OBJECT_REQUEST, nextObjectRequest));
}
if (!TextUtils.isEmpty(nextRangeRequest)) {
- headerValueList.add(
+ keyValuePairs.add(
Util.formatInvariant(
"%s=\"%s\"", CmcdConfiguration.KEY_NEXT_RANGE_REQUEST, nextRangeRequest));
}
- headerValueList.addAll(customDataList);
+ keyValuePairs.addAll(customDataList);
- if (!headerValueList.isEmpty()) {
- Collections.sort(headerValueList);
- httpRequestHeaders.put(
- CmcdConfiguration.KEY_CMCD_REQUEST, COMMA_JOINER.join(headerValueList));
+ if (!keyValuePairs.isEmpty()) {
+ cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_REQUEST, keyValuePairs);
}
}
}
@@ -870,41 +930,38 @@ public final class CmcdHeadersFactory {
}
/**
- * Populates the HTTP request headers with {@link CmcdConfiguration#KEY_CMCD_SESSION} values.
+ * Populates the {@code cmcdDataMap} with {@link CmcdConfiguration#KEY_CMCD_SESSION} values.
*
- * @param httpRequestHeaders An {@link ImmutableMap.Builder} used to build the HTTP request
- * headers.
+ * @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added.
*/
- public void populateHttpRequestHeaders(
- ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) {
- ArrayList headerValueList = new ArrayList<>();
+ public void populateCmcdDataMap(
+ ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
+ ArrayList keyValuePairs = new ArrayList<>();
if (!TextUtils.isEmpty(this.contentId)) {
- headerValueList.add(
+ keyValuePairs.add(
Util.formatInvariant("%s=\"%s\"", CmcdConfiguration.KEY_CONTENT_ID, contentId));
}
if (!TextUtils.isEmpty(this.sessionId)) {
- headerValueList.add(
+ keyValuePairs.add(
Util.formatInvariant("%s=\"%s\"", CmcdConfiguration.KEY_SESSION_ID, sessionId));
}
if (!TextUtils.isEmpty(this.streamingFormat)) {
- headerValueList.add(CmcdConfiguration.KEY_STREAMING_FORMAT + "=" + streamingFormat);
+ keyValuePairs.add(CmcdConfiguration.KEY_STREAMING_FORMAT + "=" + streamingFormat);
}
if (!TextUtils.isEmpty(this.streamType)) {
- headerValueList.add(CmcdConfiguration.KEY_STREAM_TYPE + "=" + streamType);
+ keyValuePairs.add(CmcdConfiguration.KEY_STREAM_TYPE + "=" + streamType);
}
if (playbackRate != C.RATE_UNSET && playbackRate != 1.0f) {
- headerValueList.add(
+ keyValuePairs.add(
Util.formatInvariant("%s=%.2f", CmcdConfiguration.KEY_PLAYBACK_RATE, playbackRate));
}
if (VERSION != 1) {
- headerValueList.add(CmcdConfiguration.KEY_VERSION + "=" + VERSION);
+ keyValuePairs.add(CmcdConfiguration.KEY_VERSION + "=" + VERSION);
}
- headerValueList.addAll(customDataList);
+ keyValuePairs.addAll(customDataList);
- if (!headerValueList.isEmpty()) {
- Collections.sort(headerValueList);
- httpRequestHeaders.put(
- CmcdConfiguration.KEY_CMCD_SESSION, COMMA_JOINER.join(headerValueList));
+ if (!keyValuePairs.isEmpty()) {
+ cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_SESSION, keyValuePairs);
}
}
}
@@ -991,27 +1048,24 @@ public final class CmcdHeadersFactory {
}
/**
- * Populates the HTTP request headers with {@link CmcdConfiguration#KEY_CMCD_STATUS} values.
+ * Populates the {@code cmcdDataMap} with {@link CmcdConfiguration#KEY_CMCD_STATUS} values.
*
- * @param httpRequestHeaders An {@link ImmutableMap.Builder} used to build the HTTP request
- * headers.
+ * @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added.
*/
- public void populateHttpRequestHeaders(
- ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) {
- ArrayList headerValueList = new ArrayList<>();
+ public void populateCmcdDataMap(
+ ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
+ ArrayList keyValuePairs = new ArrayList<>();
if (maximumRequestedThroughputKbps != C.RATE_UNSET_INT) {
- headerValueList.add(
+ keyValuePairs.add(
CmcdConfiguration.KEY_MAXIMUM_REQUESTED_BITRATE + "=" + maximumRequestedThroughputKbps);
}
if (bufferStarvation) {
- headerValueList.add(CmcdConfiguration.KEY_BUFFER_STARVATION);
+ keyValuePairs.add(CmcdConfiguration.KEY_BUFFER_STARVATION);
}
- headerValueList.addAll(customDataList);
+ keyValuePairs.addAll(customDataList);
- if (!headerValueList.isEmpty()) {
- Collections.sort(headerValueList);
- httpRequestHeaders.put(
- CmcdConfiguration.KEY_CMCD_STATUS, COMMA_JOINER.join(headerValueList));
+ if (!keyValuePairs.isEmpty()) {
+ cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_STATUS, keyValuePairs);
}
}
}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java
similarity index 60%
rename from libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java
rename to libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java
index ddd7503f4f..efd07f8ca3 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java
@@ -20,22 +20,23 @@ import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import android.net.Uri;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.TrackGroup;
+import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ImmutableMap;
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Tests for {@link CmcdHeadersFactory}. */
+/** Tests for {@link CmcdData}. */
@RunWith(AndroidJUnit4.class)
-public class CmcdHeadersFactoryTest {
+public class CmcdDataTest {
@Test
- public void createInstance_populatesCmcdHeaders() {
+ public void createInstance_populatesCmcdHttRequestHeaders() {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem ->
new CmcdConfiguration(
@@ -66,21 +67,23 @@ public class CmcdHeadersFactoryTest {
when(trackSelection.getTrackGroup())
.thenReturn(new TrackGroup(format, new Format.Builder().setPeakBitrate(1_000_000).build()));
when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L);
-
- ImmutableMap<@CmcdConfiguration.HeaderKey String, String> requestHeaders =
- new CmcdHeadersFactory(
+ DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
+ CmcdData cmcdData =
+ new CmcdData.Factory(
cmcdConfiguration,
trackSelection,
/* bufferedDurationUs= */ 1_760_000,
/* playbackRate= */ 2.0f,
- /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH,
+ /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH,
/* isLive= */ true,
/* didRebuffer= */ true,
/* isBufferEmpty= */ false)
.setChunkDurationUs(3_000_000)
- .createHttpRequestHeaders();
+ .createCmcdData();
- assertThat(requestHeaders)
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+
+ assertThat(dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=840,d=3000,key-1=1,key-2-separated-by-multiple-hyphens=2,tb=1000",
@@ -92,6 +95,62 @@ public class CmcdHeadersFactoryTest {
"bs,key-4=\"stringValue3=stringValue4\",rtp=1700");
}
+ @Test
+ public void createInstance_populatesCmcdHttpQueryParameters() {
+ CmcdConfiguration.Factory cmcdConfigurationFactory =
+ mediaItem ->
+ new CmcdConfiguration(
+ "sessionId",
+ mediaItem.mediaId,
+ new CmcdConfiguration.RequestConfig() {
+ @Override
+ public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
+ getCustomData() {
+ return new ImmutableListMultimap.Builder()
+ .put("CMCD-Object", "key-1=1")
+ .put("CMCD-Request", "key-2=\"stringValue1,stringValue2\"")
+ .build();
+ }
+
+ @Override
+ public int getRequestedMaximumThroughputKbps(int throughputKbps) {
+ return 2 * throughputKbps;
+ }
+ },
+ CmcdConfiguration.MODE_QUERY_PARAMETER);
+ MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
+ CmcdConfiguration cmcdConfiguration =
+ cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
+ ExoTrackSelection trackSelection = mock(ExoTrackSelection.class);
+ Format format = new Format.Builder().setPeakBitrate(840_000).build();
+ when(trackSelection.getSelectedFormat()).thenReturn(format);
+ when(trackSelection.getTrackGroup())
+ .thenReturn(new TrackGroup(format, new Format.Builder().setPeakBitrate(1_000_000).build()));
+ when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L);
+ DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
+ CmcdData cmcdData =
+ new CmcdData.Factory(
+ cmcdConfiguration,
+ trackSelection,
+ /* bufferedDurationUs= */ 1_760_000,
+ /* playbackRate= */ 2.0f,
+ /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH,
+ /* isLive= */ true,
+ /* didRebuffer= */ true,
+ /* isBufferEmpty= */ false)
+ .setChunkDurationUs(3_000_000)
+ .createCmcdData();
+
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+
+ assertThat(
+ Uri.decode(dataSpec.uri.getQueryParameter(CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY)))
+ .isEqualTo(
+ "bl=1800,br=840,bs,cid=\"mediaId\",d=3000,dl=900,key-1=1,"
+ + "key-2=\"stringValue1,stringValue2\",mtp=500,pr=2.00,rtp=1700,sf=d,"
+ + "sid=\"sessionId\",st=l,su,tb=1000");
+ }
+
@Test
public void createInstance_withInvalidNonHyphenatedCustomKey_throwsIllegalStateException() {
CmcdConfiguration.Factory cmcdConfigurationFactory =
@@ -115,15 +174,15 @@ public class CmcdHeadersFactoryTest {
assertThrows(
IllegalStateException.class,
() ->
- new CmcdHeadersFactory(
+ new CmcdData.Factory(
cmcdConfiguration,
trackSelection,
/* bufferedDurationUs= */ 0,
/* playbackRate= */ 1.0f,
- /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH,
+ /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH,
/* isLive= */ true,
/* didRebuffer= */ true,
/* isBufferEmpty= */ false)
- .createHttpRequestHeaders());
+ .createCmcdData());
}
}
diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java
index 2e53d44aa2..7a0cd41f67 100644
--- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java
+++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java
@@ -56,7 +56,7 @@ import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
import androidx.media3.exoplayer.source.chunk.SingleSampleMediaChunk;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
-import androidx.media3.exoplayer.upstream.CmcdHeadersFactory;
+import androidx.media3.exoplayer.upstream.CmcdData;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.ChunkIndex;
@@ -387,15 +387,15 @@ public class DefaultDashChunkSource implements DashChunkSource {
int selectedTrackIndex = trackSelection.getSelectedIndex();
@Nullable
- CmcdHeadersFactory cmcdHeadersFactory =
+ CmcdData.Factory cmcdDataFactory =
cmcdConfiguration == null
? null
- : new CmcdHeadersFactory(
+ : new CmcdData.Factory(
cmcdConfiguration,
trackSelection,
bufferedDurationUs,
/* playbackRate= */ loadingInfo.playbackSpeed,
- /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH,
+ /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH,
/* isLive= */ manifest.dynamic,
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
/* isBufferEmpty= */ queue.isEmpty());
@@ -423,7 +423,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
trackSelection.getSelectionData(),
pendingInitializationUri,
pendingIndexUri,
- cmcdHeadersFactory);
+ cmcdDataFactory);
return;
}
}
@@ -501,7 +501,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
maxSegmentCount,
seekTimeUs,
nowPeriodTimeUs,
- cmcdHeadersFactory);
+ cmcdDataFactory);
}
@Override
@@ -684,7 +684,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
* indexUri} is not {@code null}.
* @param indexUri The URI pointing to index data. Can be {@code null} if {@code
* initializationUri} is not {@code null}.
- * @param cmcdHeadersFactory The {@link CmcdHeadersFactory} for generating CMCD data.
+ * @param cmcdDataFactory The {@link CmcdData.Factory} for generating CMCD data.
*/
@RequiresNonNull("#1.chunkExtractor")
protected Chunk newInitializationChunk(
@@ -695,7 +695,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
@Nullable Object trackSelectionData,
@Nullable RangedUri initializationUri,
@Nullable RangedUri indexUri,
- @Nullable CmcdHeadersFactory cmcdHeadersFactory) {
+ @Nullable CmcdData.Factory cmcdDataFactory) {
Representation representation = representationHolder.representation;
RangedUri requestUri;
if (initializationUri != null) {
@@ -709,19 +709,19 @@ public class DefaultDashChunkSource implements DashChunkSource {
} else {
requestUri = checkNotNull(indexUri);
}
- ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
- cmcdHeadersFactory == null
- ? ImmutableMap.of()
- : cmcdHeadersFactory
- .setObjectType(CmcdHeadersFactory.OBJECT_TYPE_INIT_SEGMENT)
- .createHttpRequestHeaders();
DataSpec dataSpec =
DashUtil.buildDataSpec(
representation,
representationHolder.selectedBaseUrl.url,
requestUri,
/* flags= */ 0,
- httpRequestHeaders);
+ /* httpRequestHeaders= */ ImmutableMap.of());
+ if (cmcdDataFactory != null) {
+ CmcdData cmcdData =
+ cmcdDataFactory.setObjectType(CmcdData.Factory.OBJECT_TYPE_INIT_SEGMENT).createCmcdData();
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+ }
+
return new InitializationChunk(
dataSource,
dataSpec,
@@ -742,7 +742,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
int maxSegmentCount,
long seekTimeUs,
long nowPeriodTimeUs,
- @Nullable CmcdHeadersFactory cmcdHeadersFactory) {
+ @Nullable CmcdData.Factory cmcdDataFactory) {
Representation representation = representationHolder.representation;
long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum);
RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum);
@@ -753,29 +753,29 @@ public class DefaultDashChunkSource implements DashChunkSource {
firstSegmentNum, nowPeriodTimeUs)
? 0
: DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED;
- ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
- ImmutableMap.of();
- if (cmcdHeadersFactory != null) {
- cmcdHeadersFactory
- .setChunkDurationUs(endTimeUs - startTimeUs)
- .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection));
- @Nullable
- Pair nextObjectAndRangeRequest =
- getNextObjectAndRangeRequest(firstSegmentNum, segmentUri, representationHolder);
- if (nextObjectAndRangeRequest != null) {
- cmcdHeadersFactory
- .setNextObjectRequest(nextObjectAndRangeRequest.first)
- .setNextRangeRequest(nextObjectAndRangeRequest.second);
- }
- httpRequestHeaders = cmcdHeadersFactory.createHttpRequestHeaders();
- }
DataSpec dataSpec =
DashUtil.buildDataSpec(
representation,
representationHolder.selectedBaseUrl.url,
segmentUri,
flags,
- httpRequestHeaders);
+ /* httpRequestHeaders= */ ImmutableMap.of());
+ if (cmcdDataFactory != null) {
+ cmcdDataFactory
+ .setChunkDurationUs(endTimeUs - startTimeUs)
+ .setObjectType(CmcdData.Factory.getObjectType(trackSelection));
+ @Nullable
+ Pair nextObjectAndRangeRequest =
+ getNextObjectAndRangeRequest(firstSegmentNum, segmentUri, representationHolder);
+ if (nextObjectAndRangeRequest != null) {
+ cmcdDataFactory
+ .setNextObjectRequest(nextObjectAndRangeRequest.first)
+ .setNextRangeRequest(nextObjectAndRangeRequest.second);
+ }
+ CmcdData cmcdData = cmcdDataFactory.createCmcdData();
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+ }
+
return new SingleSampleMediaChunk(
dataSource,
dataSpec,
@@ -812,29 +812,28 @@ public class DefaultDashChunkSource implements DashChunkSource {
representationHolder.isSegmentAvailableAtFullNetworkSpeed(segmentNum, nowPeriodTimeUs)
? 0
: DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED;
- ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
- ImmutableMap.of();
- if (cmcdHeadersFactory != null) {
- cmcdHeadersFactory
- .setChunkDurationUs(endTimeUs - startTimeUs)
- .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection));
- @Nullable
- Pair nextObjectAndRangeRequest =
- getNextObjectAndRangeRequest(firstSegmentNum, segmentUri, representationHolder);
- if (nextObjectAndRangeRequest != null) {
- cmcdHeadersFactory
- .setNextObjectRequest(nextObjectAndRangeRequest.first)
- .setNextRangeRequest(nextObjectAndRangeRequest.second);
- }
- httpRequestHeaders = cmcdHeadersFactory.createHttpRequestHeaders();
- }
DataSpec dataSpec =
DashUtil.buildDataSpec(
representation,
representationHolder.selectedBaseUrl.url,
segmentUri,
flags,
- httpRequestHeaders);
+ /* httpRequestHeaders= */ ImmutableMap.of());
+ if (cmcdDataFactory != null) {
+ cmcdDataFactory
+ .setChunkDurationUs(endTimeUs - startTimeUs)
+ .setObjectType(CmcdData.Factory.getObjectType(trackSelection));
+ @Nullable
+ Pair nextObjectAndRangeRequest =
+ getNextObjectAndRangeRequest(firstSegmentNum, segmentUri, representationHolder);
+ if (nextObjectAndRangeRequest != null) {
+ cmcdDataFactory
+ .setNextObjectRequest(nextObjectAndRangeRequest.first)
+ .setNextRangeRequest(nextObjectAndRangeRequest.second);
+ }
+ CmcdData cmcdData = cmcdDataFactory.createCmcdData();
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+ }
long sampleOffsetUs = -representation.presentationTimeOffsetUs;
return new ContainerMediaChunk(
dataSource,
diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java
index 1d9895fa2e..46dcd4891f 100644
--- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java
+++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java
@@ -301,7 +301,7 @@ public class DefaultDashChunkSourceTest {
}
@Test
- public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdLoggingHeaders()
+ public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdHttpRequestHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
@@ -381,7 +381,7 @@ public class DefaultDashChunkSourceTest {
}
@Test
- public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdLoggingHeaders()
+ public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdHttpRequestHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem -> {
@@ -429,7 +429,7 @@ public class DefaultDashChunkSourceTest {
@Test
public void
- getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdLoggingHeaders()
+ getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpRequestHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem -> {
@@ -475,6 +475,53 @@ public class DefaultDashChunkSourceTest {
"key-4=5.0");
}
+ @Test
+ public void
+ getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpQueryParameters()
+ throws Exception {
+ CmcdConfiguration.Factory cmcdConfigurationFactory =
+ mediaItem -> {
+ CmcdConfiguration.RequestConfig cmcdRequestConfig =
+ new CmcdConfiguration.RequestConfig() {
+ @Override
+ public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
+ getCustomData() {
+ return new ImmutableListMultimap.Builder<
+ @CmcdConfiguration.HeaderKey String, String>()
+ .put(CmcdConfiguration.KEY_CMCD_OBJECT, "key-1=1")
+ .put(CmcdConfiguration.KEY_CMCD_REQUEST, "key-2=\"stringValue\"")
+ .build();
+ }
+ };
+
+ return new CmcdConfiguration(
+ /* sessionId= */ "sessionId",
+ /* contentId= */ mediaItem.mediaId,
+ cmcdRequestConfig,
+ CmcdConfiguration.MODE_QUERY_PARAMETER);
+ };
+ MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
+ CmcdConfiguration cmcdConfiguration =
+ cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
+ DashChunkSource chunkSource = createDashChunkSource(/* numberOfTracks= */ 2, cmcdConfiguration);
+ ChunkHolder output = new ChunkHolder();
+
+ chunkSource.getNextChunk(
+ new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
+ /* loadPositionUs= */ 0,
+ /* queue= */ ImmutableList.of(),
+ output);
+
+ assertThat(
+ Uri.decode(
+ output.chunk.dataSpec.uri.getQueryParameter(
+ CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY)))
+ .isEqualTo(
+ "bl=0,br=700,cid=\"mediaId\",d=4000,dl=0,key-1=1,key-2=\"stringValue\","
+ + "mtp=1000,nor=\"..%2Fvideo_4000_700000.m4s\",nrr=\"0-\",ot=v,sf=d,"
+ + "sid=\"sessionId\",st=v,su,tb=1300");
+ }
+
@Test
public void
getNextChunk_afterLastAvailableButBeforeEndOfLiveManifestWithKnownDuration_doesNotReturnEndOfStream()
diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java
index 908a848c1e..56ead292c1 100644
--- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java
+++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java
@@ -50,9 +50,8 @@ import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
import androidx.media3.exoplayer.trackselection.BaseTrackSelection;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
-import androidx.media3.exoplayer.upstream.CmcdHeadersFactory;
+import androidx.media3.exoplayer.upstream.CmcdData;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
import java.io.IOException;
@@ -178,6 +177,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* an infinite timeout.
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
* information is available in the multivariant playlist.
+ * @param playerId The {@link PlayerId} of the player using this chunk source.
+ * @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
*/
public HlsChunkSource(
HlsExtractorFactory extractorFactory,
@@ -488,22 +489,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
seenExpectedPlaylistError = false;
expectedPlaylistUrl = null;
- @Nullable CmcdHeadersFactory cmcdHeadersFactory = null;
+ @Nullable CmcdData.Factory cmcdDataFactory = null;
if (cmcdConfiguration != null) {
- cmcdHeadersFactory =
- new CmcdHeadersFactory(
+ cmcdDataFactory =
+ new CmcdData.Factory(
cmcdConfiguration,
trackSelection,
bufferedDurationUs,
/* playbackRate= */ loadingInfo.playbackSpeed,
- /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_HLS,
+ /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_HLS,
/* isLive= */ !playlist.hasEndTag,
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
/* isBufferEmpty= */ queue.isEmpty())
.setObjectType(
getIsMuxedAudioAndVideo()
- ? CmcdHeadersFactory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
- : CmcdHeadersFactory.getObjectType(trackSelection));
+ ? CmcdData.Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
+ : CmcdData.Factory.getObjectType(trackSelection));
long nextChunkMediaSequence =
partIndex == C.LENGTH_UNSET
@@ -515,7 +516,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
if (nextSegmentBaseHolder != null) {
Uri uri = UriUtil.resolveToUri(playlist.baseUri, segmentBaseHolder.segmentBase.url);
Uri nextUri = UriUtil.resolveToUri(playlist.baseUri, nextSegmentBaseHolder.segmentBase.url);
- cmcdHeadersFactory.setNextObjectRequest(UriUtil.getRelativePath(uri, nextUri));
+ cmcdDataFactory.setNextObjectRequest(UriUtil.getRelativePath(uri, nextUri));
String nextRangeRequest = nextSegmentBaseHolder.segmentBase.byteRangeOffset + "-";
if (nextSegmentBaseHolder.segmentBase.byteRangeLength != C.LENGTH_UNSET) {
@@ -523,7 +524,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
(nextSegmentBaseHolder.segmentBase.byteRangeOffset
+ nextSegmentBaseHolder.segmentBase.byteRangeLength);
}
- cmcdHeadersFactory.setNextRangeRequest(nextRangeRequest);
+ cmcdDataFactory.setNextRangeRequest(nextRangeRequest);
}
}
lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();
@@ -534,7 +535,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase.initializationSegment);
out.chunk =
maybeCreateEncryptionChunkFor(
- initSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ true, cmcdHeadersFactory);
+ initSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ true, cmcdDataFactory);
if (out.chunk != null) {
return;
}
@@ -542,7 +543,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase);
out.chunk =
maybeCreateEncryptionChunkFor(
- mediaSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ false, cmcdHeadersFactory);
+ mediaSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ false, cmcdDataFactory);
if (out.chunk != null) {
return;
}
@@ -578,7 +579,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri),
shouldSpliceIn,
playerId,
- cmcdHeadersFactory);
+ cmcdDataFactory);
}
private boolean getIsMuxedAudioAndVideo() {
@@ -896,7 +897,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable Uri keyUri,
int selectedTrackIndex,
boolean isInitSegment,
- @Nullable CmcdHeadersFactory cmcdHeadersFactory) {
+ @Nullable CmcdData.Factory cmcdDataFactory) {
if (keyUri == null) {
return null;
}
@@ -910,20 +911,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return null;
}
- ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
- ImmutableMap.of();
- if (cmcdHeadersFactory != null) {
- if (isInitSegment) {
- cmcdHeadersFactory.setObjectType(CmcdHeadersFactory.OBJECT_TYPE_INIT_SEGMENT);
- }
- httpRequestHeaders = cmcdHeadersFactory.createHttpRequestHeaders();
- }
DataSpec dataSpec =
- new DataSpec.Builder()
- .setUri(keyUri)
- .setFlags(DataSpec.FLAG_ALLOW_GZIP)
- .setHttpRequestHeaders(httpRequestHeaders)
- .build();
+ new DataSpec.Builder().setUri(keyUri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build();
+ if (cmcdDataFactory != null) {
+ if (isInitSegment) {
+ cmcdDataFactory.setObjectType(CmcdData.Factory.OBJECT_TYPE_INIT_SEGMENT);
+ }
+ CmcdData cmcdData = cmcdDataFactory.createCmcdData();
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+ }
+
return new EncryptionKeyChunk(
encryptionDataSource,
dataSpec,
diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaChunk.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaChunk.java
index 182b684c99..8b293dbc6e 100644
--- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaChunk.java
+++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaChunk.java
@@ -33,15 +33,13 @@ import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
import androidx.media3.exoplayer.source.chunk.MediaChunk;
-import androidx.media3.exoplayer.upstream.CmcdConfiguration;
-import androidx.media3.exoplayer.upstream.CmcdHeadersFactory;
+import androidx.media3.exoplayer.upstream.CmcdData;
import androidx.media3.extractor.DefaultExtractorInput;
import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.metadata.id3.Id3Decoder;
import androidx.media3.extractor.metadata.id3.PrivFrame;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import java.io.EOFException;
import java.io.IOException;
import java.io.InterruptedIOException;
@@ -82,7 +80,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null
* otherwise.
* @param shouldSpliceIn Whether samples for this chunk should be spliced into existing samples.
- * @param cmcdHeadersFactory The {@link CmcdHeadersFactory} for generating CMCD request headers.
+ * @param cmcdDataFactory The {@link CmcdData.Factory} for generating {@link CmcdData}.
*/
public static HlsMediaChunk createInstance(
HlsExtractorFactory extractorFactory,
@@ -103,23 +101,22 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable byte[] initSegmentKey,
boolean shouldSpliceIn,
PlayerId playerId,
- @Nullable CmcdHeadersFactory cmcdHeadersFactory) {
+ @Nullable CmcdData.Factory cmcdDataFactory) {
// Media segment.
HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase;
- ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
- cmcdHeadersFactory == null
- ? ImmutableMap.of()
- : cmcdHeadersFactory
- .setChunkDurationUs(mediaSegment.durationUs)
- .createHttpRequestHeaders();
DataSpec dataSpec =
new DataSpec.Builder()
.setUri(UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url))
.setPosition(mediaSegment.byteRangeOffset)
.setLength(mediaSegment.byteRangeLength)
.setFlags(segmentBaseHolder.isPreload ? FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED : 0)
- .setHttpRequestHeaders(httpRequestHeaders)
.build();
+ if (cmcdDataFactory != null) {
+ CmcdData cmcdData =
+ cmcdDataFactory.setChunkDurationUs(mediaSegment.durationUs).createCmcdData();
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+ }
+
boolean mediaSegmentEncrypted = mediaSegmentKey != null;
@Nullable
byte[] mediaSegmentIv =
@@ -141,19 +138,20 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
? getEncryptionIvArray(Assertions.checkNotNull(initSegment.encryptionIV))
: null;
Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url);
- ImmutableMap<@CmcdConfiguration.HeaderKey String, String> initHttpRequestHeaders =
- cmcdHeadersFactory == null
- ? ImmutableMap.of()
- : cmcdHeadersFactory
- .setObjectType(CmcdHeadersFactory.OBJECT_TYPE_INIT_SEGMENT)
- .createHttpRequestHeaders();
initDataSpec =
new DataSpec.Builder()
.setUri(initSegmentUri)
.setPosition(initSegment.byteRangeOffset)
.setLength(initSegment.byteRangeLength)
- .setHttpRequestHeaders(initHttpRequestHeaders)
.build();
+ if (cmcdDataFactory != null) {
+ CmcdData cmcdData =
+ cmcdDataFactory
+ .setObjectType(CmcdData.Factory.OBJECT_TYPE_INIT_SEGMENT)
+ .createCmcdData();
+ initDataSpec = cmcdData.addToDataSpec(dataSpec);
+ }
+
initDataSource = buildDataSource(dataSource, initSegmentKey, initSegmentIv);
}
diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java
index 87d4aec93e..9d866b0ec4 100644
--- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java
+++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java
@@ -195,8 +195,7 @@ public class HlsChunkSourceTest {
}
@Test
- public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdLoggingHeaders()
- throws Exception {
+ public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdHttpRequestHeaders() {
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
CmcdConfiguration cmcdConfiguration =
@@ -238,8 +237,8 @@ public class HlsChunkSourceTest {
}
@Test
- public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCorrectBufferStarvationKey()
- throws Exception {
+ public void
+ getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCorrectBufferStarvationKey() {
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
CmcdConfiguration cmcdConfiguration =
@@ -289,8 +288,7 @@ public class HlsChunkSourceTest {
}
@Test
- public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdLoggingHeaders()
- throws Exception {
+ public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdHttpRequestHeaders() {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem -> {
CmcdConfiguration.RequestConfig cmcdRequestConfig =
@@ -338,7 +336,7 @@ public class HlsChunkSourceTest {
@Test
public void
- getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdLoggingHeaders()
+ getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpRequestHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem -> {
@@ -385,6 +383,53 @@ public class HlsChunkSourceTest {
"key-4=5.0");
}
+ @Test
+ public void
+ getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpQueryParameters()
+ throws Exception {
+ CmcdConfiguration.Factory cmcdConfigurationFactory =
+ mediaItem -> {
+ CmcdConfiguration.RequestConfig cmcdRequestConfig =
+ new CmcdConfiguration.RequestConfig() {
+ @Override
+ public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
+ getCustomData() {
+ return new ImmutableListMultimap.Builder<
+ @CmcdConfiguration.HeaderKey String, String>()
+ .put(CmcdConfiguration.KEY_CMCD_OBJECT, "key-1=1")
+ .put(CmcdConfiguration.KEY_CMCD_REQUEST, "key-2=\"stringValue\"")
+ .build();
+ }
+ };
+
+ return new CmcdConfiguration(
+ /* sessionId= */ "sessionId",
+ /* contentId= */ mediaItem.mediaId,
+ cmcdRequestConfig,
+ CmcdConfiguration.MODE_QUERY_PARAMETER);
+ };
+ MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
+ CmcdConfiguration cmcdConfiguration =
+ cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
+ HlsChunkSource testChunkSource = createHlsChunkSource(cmcdConfiguration);
+ HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder();
+
+ testChunkSource.getNextChunk(
+ new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
+ /* loadPositionUs= */ 0,
+ /* queue= */ ImmutableList.of(),
+ /* allowEndOfStream= */ true,
+ output);
+
+ assertThat(
+ Uri.decode(
+ output.chunk.dataSpec.uri.getQueryParameter(
+ CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY)))
+ .isEqualTo(
+ "bl=0,br=800,cid=\"mediaId\",d=4000,dl=0,key-1=1,key-2=\"stringValue\","
+ + "nor=\"..%2F3.mp4\",nrr=\"0-\",ot=v,sf=h,sid=\"sessionId\",st=v,su,tb=800");
+ }
+
private HlsChunkSource createHlsChunkSource(@Nullable CmcdConfiguration cmcdConfiguration) {
return new HlsChunkSource(
HlsExtractorFactory.DEFAULT,
diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java
index 7e4e51dae2..1ae37c5495 100644
--- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java
+++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java
@@ -43,14 +43,13 @@ import androidx.media3.exoplayer.source.chunk.MediaChunk;
import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
-import androidx.media3.exoplayer.upstream.CmcdHeadersFactory;
+import androidx.media3.exoplayer.upstream.CmcdData;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.FallbackSelection;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
import androidx.media3.extractor.mp4.Track;
import androidx.media3.extractor.mp4.TrackEncryptionBox;
-import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.util.List;
@@ -291,24 +290,24 @@ public class DefaultSsChunkSource implements SsChunkSource {
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex);
Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex);
- @Nullable CmcdHeadersFactory cmcdHeadersFactory = null;
+ @Nullable CmcdData.Factory cmcdDataFactory = null;
if (cmcdConfiguration != null) {
- cmcdHeadersFactory =
- new CmcdHeadersFactory(
+ cmcdDataFactory =
+ new CmcdData.Factory(
cmcdConfiguration,
trackSelection,
bufferedDurationUs,
/* playbackRate= */ loadingInfo.playbackSpeed,
- /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_SS,
+ /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_SS,
/* isLive= */ manifest.isLive,
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
/* isBufferEmpty= */ queue.isEmpty())
.setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs)
- .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection));
+ .setObjectType(CmcdData.Factory.getObjectType(trackSelection));
if (chunkIndex + 1 < streamElement.chunkCount) {
Uri nextUri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex + 1);
- cmcdHeadersFactory.setNextObjectRequest(UriUtil.getRelativePath(uri, nextUri));
+ cmcdDataFactory.setNextObjectRequest(UriUtil.getRelativePath(uri, nextUri));
}
}
lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();
@@ -325,7 +324,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
trackSelection.getSelectionReason(),
trackSelection.getSelectionData(),
chunkExtractor,
- cmcdHeadersFactory);
+ cmcdDataFactory);
}
@Override
@@ -370,13 +369,13 @@ public class DefaultSsChunkSource implements SsChunkSource {
@C.SelectionReason int trackSelectionReason,
@Nullable Object trackSelectionData,
ChunkExtractor chunkExtractor,
- @Nullable CmcdHeadersFactory cmcdHeadersFactory) {
- ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
- cmcdHeadersFactory == null
- ? ImmutableMap.of()
- : cmcdHeadersFactory.createHttpRequestHeaders();
- DataSpec dataSpec =
- new DataSpec.Builder().setUri(uri).setHttpRequestHeaders(httpRequestHeaders).build();
+ @Nullable CmcdData.Factory cmcdDataFactory) {
+ DataSpec dataSpec = new DataSpec.Builder().setUri(uri).build();
+ if (cmcdDataFactory != null) {
+ CmcdData cmcdData = cmcdDataFactory.createCmcdData();
+ dataSpec = cmcdData.addToDataSpec(dataSpec);
+ }
+
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
// To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs.
long sampleOffsetUs = chunkStartTimeUs;
diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java
index 075ab15309..70ada1a5d2 100644
--- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java
+++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java
@@ -51,7 +51,7 @@ public class DefaultSsChunkSourceTest {
private static final String SAMPLE_ISMC_1 = "media/smooth-streaming/sample_ismc_1";
@Test
- public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdLoggingHeaders()
+ public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdHttpRequestHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
@@ -131,7 +131,7 @@ public class DefaultSsChunkSourceTest {
}
@Test
- public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdLoggingHeaders()
+ public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdHttpRequestHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem -> {
@@ -179,7 +179,7 @@ public class DefaultSsChunkSourceTest {
@Test
public void
- getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdLoggingHeaders()
+ getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpRequestHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem -> {
@@ -225,6 +225,53 @@ public class DefaultSsChunkSourceTest {
"key-4=5.0");
}
+ @Test
+ public void
+ getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpQueryParameters()
+ throws Exception {
+ CmcdConfiguration.Factory cmcdConfigurationFactory =
+ mediaItem -> {
+ CmcdConfiguration.RequestConfig cmcdRequestConfig =
+ new CmcdConfiguration.RequestConfig() {
+ @Override
+ public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String>
+ getCustomData() {
+ return new ImmutableListMultimap.Builder<
+ @CmcdConfiguration.HeaderKey String, String>()
+ .put(CmcdConfiguration.KEY_CMCD_OBJECT, "key-1=1")
+ .put(CmcdConfiguration.KEY_CMCD_REQUEST, "key-2=\"stringValue\"")
+ .build();
+ }
+ };
+
+ return new CmcdConfiguration(
+ /* sessionId= */ "sessionId",
+ /* contentId= */ mediaItem.mediaId,
+ cmcdRequestConfig,
+ CmcdConfiguration.MODE_QUERY_PARAMETER);
+ };
+ MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
+ CmcdConfiguration cmcdConfiguration =
+ cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
+ SsChunkSource chunkSource = createSsChunkSource(/* numberOfTracks= */ 2, cmcdConfiguration);
+ ChunkHolder output = new ChunkHolder();
+
+ chunkSource.getNextChunk(
+ new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
+ /* loadPositionUs= */ 0,
+ /* queue= */ ImmutableList.of(),
+ output);
+
+ assertThat(
+ Uri.decode(
+ output.chunk.dataSpec.uri.getQueryParameter(
+ CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY)))
+ .isEqualTo(
+ "bl=0,br=308,cid=\"mediaId\",d=1968,dl=0,key-1=1,key-2=\"stringValue\","
+ + "mtp=1000,nor=\"..%2FFragments(video%3D19680000)\",ot=v,sf=s,sid=\"sessionId\","
+ + "st=v,su,tb=1536");
+ }
+
private SsChunkSource createSsChunkSource(
int numberOfTracks, @Nullable CmcdConfiguration cmcdConfiguration) throws IOException {
Assertions.checkArgument(numberOfTracks < 6);