Add functionality to transmit CMCD data using query parameters

Currently, we only support sending Common Media Client Data (CMCD) data through custom HTTP request headers, added capability to configure and transmit it as HTTP query parameters.

PiperOrigin-RevId: 561591246
This commit is contained in:
rohks 2023-08-31 02:08:38 -07:00 committed by Copybara-Service
parent 89f52dcb93
commit 206a663dca
11 changed files with 735 additions and 464 deletions

View File

@ -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`.

View File

@ -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;
}
/**

View File

@ -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.
*
* <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 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.
*
* <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({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<String, String> 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.
*
* <p>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.
*
* <p>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.
*
* <p>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<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));
}
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<String> keyValuePairs = new ArrayList<>();
for (Collection<String> 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<String, String> 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<String> 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<String> headerValueList = new ArrayList<>();
public void populateCmcdDataMap(
ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
ArrayList<String> 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.
*
* <p>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}.
*
* <p>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<String> headerValueList = new ArrayList<>();
public void populateCmcdDataMap(
ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
ArrayList<String> 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<String> headerValueList = new ArrayList<>();
public void populateCmcdDataMap(
ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
ArrayList<String> 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<String> headerValueList = new ArrayList<>();
public void populateCmcdDataMap(
ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
ArrayList<String> 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);
}
}
}

View File

@ -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<String, String>()
.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());
}
}

View File

@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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,

View File

@ -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()

View File

@ -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,

View File

@ -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);
}

View File

@ -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,

View File

@ -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;

View File

@ -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);