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:
parent
89f52dcb93
commit
206a663dca
@ -12,6 +12,8 @@
|
|||||||
* Add additional fields to Common Media Client Data (CMCD) logging: next
|
* Add additional fields to Common Media Client Data (CMCD) logging: next
|
||||||
object request (`nor`) and next range request (`nrr`)
|
object request (`nor`) and next range request (`nrr`)
|
||||||
([#8699](https://github.com/google/ExoPlayer/issues/8699)).
|
([#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:
|
* Transformer:
|
||||||
* Changed `frameRate` and `durationUs` parameters of
|
* Changed `frameRate` and `durationUs` parameters of
|
||||||
`SampleConsumer.queueInputBitmap` to `TimestampIterator`.
|
`SampleConsumer.queueInputBitmap` to `TimestampIterator`.
|
||||||
|
@ -19,6 +19,7 @@ import static androidx.media3.common.util.Assertions.checkArgument;
|
|||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringDef;
|
import androidx.annotation.StringDef;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
@ -79,6 +80,13 @@ public final class CmcdConfiguration {
|
|||||||
@Target(TYPE_USE)
|
@Target(TYPE_USE)
|
||||||
public @interface CmcdKey {}
|
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. */
|
/** Maximum length for ID fields. */
|
||||||
public static final int MAX_ID_LENGTH = 64;
|
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_REQUEST = "CMCD-Request";
|
||||||
public static final String KEY_CMCD_SESSION = "CMCD-Session";
|
public static final String KEY_CMCD_SESSION = "CMCD-Session";
|
||||||
public static final String KEY_CMCD_STATUS = "CMCD-Status";
|
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_BITRATE = "br";
|
||||||
public static final String KEY_BUFFER_LENGTH = "bl";
|
public static final String KEY_BUFFER_LENGTH = "bl";
|
||||||
public static final String KEY_CONTENT_ID = "cid";
|
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_STARTUP = "su";
|
||||||
public static final String KEY_NEXT_OBJECT_REQUEST = "nor";
|
public static final String KEY_NEXT_OBJECT_REQUEST = "nor";
|
||||||
public static final String KEY_NEXT_RANGE_REQUEST = "nrr";
|
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.
|
* Factory for {@link CmcdConfiguration} instances.
|
||||||
@ -230,15 +241,28 @@ public final class CmcdConfiguration {
|
|||||||
/** Dynamic request specific configuration. */
|
/** Dynamic request specific configuration. */
|
||||||
public final RequestConfig requestConfig;
|
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(
|
public CmcdConfiguration(
|
||||||
@Nullable String sessionId, @Nullable String contentId, RequestConfig requestConfig) {
|
@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(sessionId == null || sessionId.length() <= MAX_ID_LENGTH);
|
||||||
checkArgument(contentId == null || contentId.length() <= MAX_ID_LENGTH);
|
checkArgument(contentId == null || contentId.length() <= MAX_ID_LENGTH);
|
||||||
checkNotNull(requestConfig);
|
checkNotNull(requestConfig);
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.contentId = contentId;
|
this.contentId = contentId;
|
||||||
this.requestConfig = requestConfig;
|
this.requestConfig = requestConfig;
|
||||||
|
this.dataTransmissionMode = dataTransmissionMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,8 +30,10 @@ import androidx.media3.common.MimeTypes;
|
|||||||
import androidx.media3.common.TrackGroup;
|
import androidx.media3.common.TrackGroup;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.datasource.DataSpec;
|
||||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableListMultimap;
|
import com.google.common.collect.ImmutableListMultimap;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
@ -41,62 +43,308 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class serves as a factory for generating Common Media Client Data (CMCD) HTTP request
|
* This class provides functionality for generating and adding Common Media Client Data (CMCD) data
|
||||||
* headers in adaptive streaming formats, DASH, HLS, and SmoothStreaming.
|
* to adaptive streaming formats, DASH, HLS, and SmoothStreaming.
|
||||||
*
|
*
|
||||||
* <p>It encapsulates the necessary attributes and information relevant to media content playback,
|
* <p>It encapsulates the necessary attributes and information relevant to media content playback,
|
||||||
* following the guidelines specified in the CMCD standard document <a
|
* 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>.
|
* href="https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf">CTA-5004</a>.
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class CmcdHeadersFactory {
|
public final class CmcdData {
|
||||||
|
|
||||||
private static final Joiner COMMA_JOINER = Joiner.on(",");
|
/** {@link CmcdData.Factory} for {@link CmcdData} instances. */
|
||||||
private static final Pattern CUSTOM_KEY_NAME_PATTERN =
|
public static final class Factory {
|
||||||
Pattern.compile("[a-zA-Z0-9]+(-[a-zA-Z0-9]+)+");
|
|
||||||
|
|
||||||
/**
|
/** Represents the Dynamic Adaptive Streaming over HTTP (DASH) format. */
|
||||||
* Retrieves the object type value from the given {@link ExoTrackSelection}.
|
public static final String STREAMING_FORMAT_DASH = "d";
|
||||||
*
|
|
||||||
* @param trackSelection The {@link ExoTrackSelection} from which to retrieve the object type.
|
/** Represents the HTTP Live Streaming (HLS) format. */
|
||||||
* @return The object type value as a String if {@link TrackType} can be mapped to one of the
|
public static final String STREAMING_FORMAT_HLS = "h";
|
||||||
* object types specified by {@link ObjectType} annotation, or {@code null}.
|
|
||||||
* @throws IllegalArgumentException if the provided {@link ExoTrackSelection} is {@code null}.
|
/** Represents the Smooth Streaming (SS) format. */
|
||||||
*/
|
public static final String STREAMING_FORMAT_SS = "s";
|
||||||
@Nullable
|
|
||||||
public static @ObjectType String getObjectType(ExoTrackSelection trackSelection) {
|
/** Represents the Video on Demand (VOD) stream type. */
|
||||||
checkArgument(trackSelection != null);
|
public static final String STREAM_TYPE_VOD = "v";
|
||||||
@C.TrackType
|
|
||||||
int trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType);
|
/** Represents the Live Streaming stream type. */
|
||||||
if (trackType == C.TRACK_TYPE_UNKNOWN) {
|
public static final String STREAM_TYPE_LIVE = "l";
|
||||||
trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().containerMimeType);
|
|
||||||
|
/** 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;
|
* Retrieves the object type value from the given {@link ExoTrackSelection}.
|
||||||
} else if (trackType == C.TRACK_TYPE_VIDEO) {
|
*
|
||||||
return OBJECT_TYPE_VIDEO_ONLY;
|
* @param trackSelection The {@link ExoTrackSelection} from which to retrieve the object type.
|
||||||
} else {
|
* @return The object type value as a String if {@link TrackType} can be mapped to one of the
|
||||||
// Track type cannot be mapped to a known object type.
|
* object types specified by {@link CmcdData.ObjectType} annotation, or {@code null}.
|
||||||
return 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. */
|
/** Indicates the streaming format used for media content. */
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@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
|
@Documented
|
||||||
@Target(TYPE_USE)
|
@Target(TYPE_USE)
|
||||||
public @interface StreamingFormat {}
|
public @interface StreamingFormat {}
|
||||||
|
|
||||||
/** Indicates the type of streaming for media content. */
|
/** Indicates the type of streaming for media content. */
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@StringDef({STREAM_TYPE_VOD, STREAM_TYPE_LIVE})
|
@StringDef({Factory.STREAM_TYPE_VOD, Factory.STREAM_TYPE_LIVE})
|
||||||
@Documented
|
@Documented
|
||||||
@Target(TYPE_USE)
|
@Target(TYPE_USE)
|
||||||
public @interface StreamType {}
|
public @interface StreamType {}
|
||||||
@ -104,251 +352,69 @@ public final class CmcdHeadersFactory {
|
|||||||
/** Indicates the media type of current object being requested. */
|
/** Indicates the media type of current object being requested. */
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@StringDef({
|
@StringDef({
|
||||||
OBJECT_TYPE_INIT_SEGMENT,
|
Factory.OBJECT_TYPE_INIT_SEGMENT,
|
||||||
OBJECT_TYPE_AUDIO_ONLY,
|
Factory.OBJECT_TYPE_AUDIO_ONLY,
|
||||||
OBJECT_TYPE_VIDEO_ONLY,
|
Factory.OBJECT_TYPE_VIDEO_ONLY,
|
||||||
OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
|
Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
|
||||||
})
|
})
|
||||||
@Documented
|
@Documented
|
||||||
@Target(TYPE_USE)
|
@Target(TYPE_USE)
|
||||||
public @interface ObjectType {}
|
public @interface ObjectType {}
|
||||||
|
|
||||||
/** Represents the Dynamic Adaptive Streaming over HTTP (DASH) format. */
|
private static final Joiner COMMA_JOINER = Joiner.on(",");
|
||||||
public static final String STREAMING_FORMAT_DASH = "d";
|
|
||||||
|
|
||||||
/** Represents the HTTP Live Streaming (HLS) format. */
|
private final CmcdObject cmcdObject;
|
||||||
public static final String STREAMING_FORMAT_HLS = "h";
|
private final CmcdRequest cmcdRequest;
|
||||||
|
private final CmcdSession cmcdSession;
|
||||||
|
private final CmcdStatus cmcdStatus;
|
||||||
|
private final @CmcdConfiguration.DataTransmissionMode int dataTransmissionMode;
|
||||||
|
|
||||||
/** Represents the Smooth Streaming (SS) format. */
|
private CmcdData(
|
||||||
public static final String STREAMING_FORMAT_SS = "s";
|
CmcdObject cmcdObject,
|
||||||
|
CmcdRequest cmcdRequest,
|
||||||
/** Represents the Video on Demand (VOD) stream type. */
|
CmcdSession cmcdSession,
|
||||||
public static final String STREAM_TYPE_VOD = "v";
|
CmcdStatus cmcdStatus,
|
||||||
|
@CmcdConfiguration.DataTransmissionMode int datatTransmissionMode) {
|
||||||
/** Represents the Live Streaming stream type. */
|
this.cmcdObject = cmcdObject;
|
||||||
public static final String STREAM_TYPE_LIVE = "l";
|
this.cmcdRequest = cmcdRequest;
|
||||||
|
this.cmcdSession = cmcdSession;
|
||||||
/** Represents the object type for an initialization segment in a media container. */
|
this.cmcdStatus = cmcdStatus;
|
||||||
public static final String OBJECT_TYPE_INIT_SEGMENT = "i";
|
this.dataTransmissionMode = datatTransmissionMode;
|
||||||
|
|
||||||
/** 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the duration of current media chunk being requested, in microseconds. The default value is
|
* Adds Common Media Client Data (CMCD) related information to the provided {@link DataSpec}
|
||||||
* {@link C#TIME_UNSET}.
|
* object.
|
||||||
*
|
|
||||||
* @throws IllegalArgumentException If {@code chunkDurationUs} is negative.
|
|
||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
public DataSpec addToDataSpec(DataSpec dataSpec) {
|
||||||
public CmcdHeadersFactory setChunkDurationUs(long chunkDurationUs) {
|
ArrayListMultimap<String, String> cmcdDataMap = ArrayListMultimap.create();
|
||||||
checkArgument(chunkDurationUs >= 0);
|
cmcdObject.populateCmcdDataMap(cmcdDataMap);
|
||||||
this.chunkDurationUs = chunkDurationUs;
|
cmcdRequest.populateCmcdDataMap(cmcdDataMap);
|
||||||
return this;
|
cmcdSession.populateCmcdDataMap(cmcdDataMap);
|
||||||
}
|
cmcdStatus.populateCmcdDataMap(cmcdDataMap);
|
||||||
|
|
||||||
/**
|
if (dataTransmissionMode == CmcdConfiguration.MODE_REQUEST_HEADER) {
|
||||||
* Sets the object type of the current object being requested. Must be one of the allowed object
|
ImmutableMap.Builder<String, String> httpRequestHeaders = ImmutableMap.builder();
|
||||||
* types specified by the {@link ObjectType} annotation.
|
for (String headerKey : cmcdDataMap.keySet()) {
|
||||||
*
|
List<String> headerValues = cmcdDataMap.get(headerKey);
|
||||||
* <p>Default is {@code null}.
|
Collections.sort(headerValues);
|
||||||
*/
|
httpRequestHeaders.put(headerKey, COMMA_JOINER.join(headerValues));
|
||||||
@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 (cmcdConfiguration.isTopBitrateLoggingAllowed()) {
|
return dataSpec.withAdditionalHeaders(httpRequestHeaders.buildOrThrow());
|
||||||
TrackGroup trackGroup = trackSelection.getTrackGroup();
|
} else {
|
||||||
int topBitrate = trackSelection.getSelectedFormat().bitrate;
|
List<String> keyValuePairs = new ArrayList<>();
|
||||||
for (int i = 0; i < trackGroup.length; i++) {
|
for (Collection<String> values : cmcdDataMap.asMap().values()) {
|
||||||
topBitrate = max(topBitrate, trackGroup.getFormat(i).bitrate);
|
keyValuePairs.addAll(values);
|
||||||
}
|
|
||||||
cmcdObject.setTopBitrateKbps(Util.ceilDivide(topBitrate, 1000));
|
|
||||||
}
|
}
|
||||||
if (cmcdConfiguration.isObjectDurationLoggingAllowed()) {
|
Collections.sort(keyValuePairs);
|
||||||
cmcdObject.setObjectDurationMs(Util.usToMs(chunkDurationUs));
|
Uri.Builder uriBuilder =
|
||||||
}
|
dataSpec
|
||||||
}
|
.uri
|
||||||
if (cmcdConfiguration.isObjectTypeLoggingAllowed()) {
|
.buildUpon()
|
||||||
cmcdObject.setObjectType(objectType);
|
.appendQueryParameter(
|
||||||
}
|
CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY,
|
||||||
if (customData.containsKey(CmcdConfiguration.KEY_CMCD_OBJECT)) {
|
Uri.encode(COMMA_JOINER.join(keyValuePairs)));
|
||||||
cmcdObject.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_OBJECT));
|
return dataSpec.buildUpon().setUri(uriBuilder.build()).build();
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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
|
* @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added.
|
||||||
* headers.
|
|
||||||
*/
|
*/
|
||||||
public void populateHttpRequestHeaders(
|
public void populateCmcdDataMap(
|
||||||
ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) {
|
ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
|
||||||
ArrayList<String> headerValueList = new ArrayList<>();
|
ArrayList<String> keyValuePairs = new ArrayList<>();
|
||||||
if (bitrateKbps != C.RATE_UNSET_INT) {
|
if (bitrateKbps != C.RATE_UNSET_INT) {
|
||||||
headerValueList.add(CmcdConfiguration.KEY_BITRATE + "=" + bitrateKbps);
|
keyValuePairs.add(CmcdConfiguration.KEY_BITRATE + "=" + bitrateKbps);
|
||||||
}
|
}
|
||||||
if (topBitrateKbps != C.RATE_UNSET_INT) {
|
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) {
|
if (objectDurationMs != C.TIME_UNSET) {
|
||||||
headerValueList.add(CmcdConfiguration.KEY_OBJECT_DURATION + "=" + objectDurationMs);
|
keyValuePairs.add(CmcdConfiguration.KEY_OBJECT_DURATION + "=" + objectDurationMs);
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(objectType)) {
|
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()) {
|
if (!keyValuePairs.isEmpty()) {
|
||||||
Collections.sort(headerValueList);
|
cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_OBJECT, keyValuePairs);
|
||||||
httpRequestHeaders.put(
|
|
||||||
CmcdConfiguration.KEY_CMCD_OBJECT, COMMA_JOINER.join(headerValueList));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -616,8 +679,8 @@ public final class CmcdHeadersFactory {
|
|||||||
* C#TIME_UNSET} if unset.
|
* C#TIME_UNSET} if unset.
|
||||||
*
|
*
|
||||||
* <p>This key SHOULD only be sent with an {@link CmcdObject#objectType} of {@link
|
* <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
|
* Factory#OBJECT_TYPE_AUDIO_ONLY}, {@link Factory#OBJECT_TYPE_VIDEO_ONLY} or {@link
|
||||||
* #OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO}.
|
* Factory#OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO}.
|
||||||
*
|
*
|
||||||
* <p>This value MUST be rounded to the nearest 100 ms.
|
* <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
|
* @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added.
|
||||||
* headers.
|
|
||||||
*/
|
*/
|
||||||
public void populateHttpRequestHeaders(
|
public void populateCmcdDataMap(
|
||||||
ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) {
|
ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
|
||||||
ArrayList<String> headerValueList = new ArrayList<>();
|
ArrayList<String> keyValuePairs = new ArrayList<>();
|
||||||
if (bufferLengthMs != C.TIME_UNSET) {
|
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) {
|
if (measuredThroughputInKbps != C.RATE_UNSET_INT) {
|
||||||
headerValueList.add(
|
keyValuePairs.add(
|
||||||
CmcdConfiguration.KEY_MEASURED_THROUGHPUT + "=" + measuredThroughputInKbps);
|
CmcdConfiguration.KEY_MEASURED_THROUGHPUT + "=" + measuredThroughputInKbps);
|
||||||
}
|
}
|
||||||
if (deadlineMs != C.TIME_UNSET) {
|
if (deadlineMs != C.TIME_UNSET) {
|
||||||
headerValueList.add(CmcdConfiguration.KEY_DEADLINE + "=" + deadlineMs);
|
keyValuePairs.add(CmcdConfiguration.KEY_DEADLINE + "=" + deadlineMs);
|
||||||
}
|
}
|
||||||
if (startup) {
|
if (startup) {
|
||||||
headerValueList.add(CmcdConfiguration.KEY_STARTUP);
|
keyValuePairs.add(CmcdConfiguration.KEY_STARTUP);
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(nextObjectRequest)) {
|
if (!TextUtils.isEmpty(nextObjectRequest)) {
|
||||||
headerValueList.add(
|
keyValuePairs.add(
|
||||||
Util.formatInvariant(
|
Util.formatInvariant(
|
||||||
"%s=\"%s\"", CmcdConfiguration.KEY_NEXT_OBJECT_REQUEST, nextObjectRequest));
|
"%s=\"%s\"", CmcdConfiguration.KEY_NEXT_OBJECT_REQUEST, nextObjectRequest));
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(nextRangeRequest)) {
|
if (!TextUtils.isEmpty(nextRangeRequest)) {
|
||||||
headerValueList.add(
|
keyValuePairs.add(
|
||||||
Util.formatInvariant(
|
Util.formatInvariant(
|
||||||
"%s=\"%s\"", CmcdConfiguration.KEY_NEXT_RANGE_REQUEST, nextRangeRequest));
|
"%s=\"%s\"", CmcdConfiguration.KEY_NEXT_RANGE_REQUEST, nextRangeRequest));
|
||||||
}
|
}
|
||||||
headerValueList.addAll(customDataList);
|
keyValuePairs.addAll(customDataList);
|
||||||
|
|
||||||
if (!headerValueList.isEmpty()) {
|
if (!keyValuePairs.isEmpty()) {
|
||||||
Collections.sort(headerValueList);
|
cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_REQUEST, keyValuePairs);
|
||||||
httpRequestHeaders.put(
|
|
||||||
CmcdConfiguration.KEY_CMCD_REQUEST, COMMA_JOINER.join(headerValueList));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
* @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added.
|
||||||
* headers.
|
|
||||||
*/
|
*/
|
||||||
public void populateHttpRequestHeaders(
|
public void populateCmcdDataMap(
|
||||||
ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) {
|
ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
|
||||||
ArrayList<String> headerValueList = new ArrayList<>();
|
ArrayList<String> keyValuePairs = new ArrayList<>();
|
||||||
if (!TextUtils.isEmpty(this.contentId)) {
|
if (!TextUtils.isEmpty(this.contentId)) {
|
||||||
headerValueList.add(
|
keyValuePairs.add(
|
||||||
Util.formatInvariant("%s=\"%s\"", CmcdConfiguration.KEY_CONTENT_ID, contentId));
|
Util.formatInvariant("%s=\"%s\"", CmcdConfiguration.KEY_CONTENT_ID, contentId));
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(this.sessionId)) {
|
if (!TextUtils.isEmpty(this.sessionId)) {
|
||||||
headerValueList.add(
|
keyValuePairs.add(
|
||||||
Util.formatInvariant("%s=\"%s\"", CmcdConfiguration.KEY_SESSION_ID, sessionId));
|
Util.formatInvariant("%s=\"%s\"", CmcdConfiguration.KEY_SESSION_ID, sessionId));
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(this.streamingFormat)) {
|
if (!TextUtils.isEmpty(this.streamingFormat)) {
|
||||||
headerValueList.add(CmcdConfiguration.KEY_STREAMING_FORMAT + "=" + streamingFormat);
|
keyValuePairs.add(CmcdConfiguration.KEY_STREAMING_FORMAT + "=" + streamingFormat);
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(this.streamType)) {
|
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) {
|
if (playbackRate != C.RATE_UNSET && playbackRate != 1.0f) {
|
||||||
headerValueList.add(
|
keyValuePairs.add(
|
||||||
Util.formatInvariant("%s=%.2f", CmcdConfiguration.KEY_PLAYBACK_RATE, playbackRate));
|
Util.formatInvariant("%s=%.2f", CmcdConfiguration.KEY_PLAYBACK_RATE, playbackRate));
|
||||||
}
|
}
|
||||||
if (VERSION != 1) {
|
if (VERSION != 1) {
|
||||||
headerValueList.add(CmcdConfiguration.KEY_VERSION + "=" + VERSION);
|
keyValuePairs.add(CmcdConfiguration.KEY_VERSION + "=" + VERSION);
|
||||||
}
|
}
|
||||||
headerValueList.addAll(customDataList);
|
keyValuePairs.addAll(customDataList);
|
||||||
|
|
||||||
if (!headerValueList.isEmpty()) {
|
if (!keyValuePairs.isEmpty()) {
|
||||||
Collections.sort(headerValueList);
|
cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_SESSION, keyValuePairs);
|
||||||
httpRequestHeaders.put(
|
|
||||||
CmcdConfiguration.KEY_CMCD_SESSION, COMMA_JOINER.join(headerValueList));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
* @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added.
|
||||||
* headers.
|
|
||||||
*/
|
*/
|
||||||
public void populateHttpRequestHeaders(
|
public void populateCmcdDataMap(
|
||||||
ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) {
|
ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) {
|
||||||
ArrayList<String> headerValueList = new ArrayList<>();
|
ArrayList<String> keyValuePairs = new ArrayList<>();
|
||||||
if (maximumRequestedThroughputKbps != C.RATE_UNSET_INT) {
|
if (maximumRequestedThroughputKbps != C.RATE_UNSET_INT) {
|
||||||
headerValueList.add(
|
keyValuePairs.add(
|
||||||
CmcdConfiguration.KEY_MAXIMUM_REQUESTED_BITRATE + "=" + maximumRequestedThroughputKbps);
|
CmcdConfiguration.KEY_MAXIMUM_REQUESTED_BITRATE + "=" + maximumRequestedThroughputKbps);
|
||||||
}
|
}
|
||||||
if (bufferStarvation) {
|
if (bufferStarvation) {
|
||||||
headerValueList.add(CmcdConfiguration.KEY_BUFFER_STARVATION);
|
keyValuePairs.add(CmcdConfiguration.KEY_BUFFER_STARVATION);
|
||||||
}
|
}
|
||||||
headerValueList.addAll(customDataList);
|
keyValuePairs.addAll(customDataList);
|
||||||
|
|
||||||
if (!headerValueList.isEmpty()) {
|
if (!keyValuePairs.isEmpty()) {
|
||||||
Collections.sort(headerValueList);
|
cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_STATUS, keyValuePairs);
|
||||||
httpRequestHeaders.put(
|
|
||||||
CmcdConfiguration.KEY_CMCD_STATUS, COMMA_JOINER.join(headerValueList));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -20,22 +20,23 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.TrackGroup;
|
import androidx.media3.common.TrackGroup;
|
||||||
|
import androidx.media3.datasource.DataSpec;
|
||||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableListMultimap;
|
import com.google.common.collect.ImmutableListMultimap;
|
||||||
import com.google.common.collect.ImmutableMap;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/** Tests for {@link CmcdHeadersFactory}. */
|
/** Tests for {@link CmcdData}. */
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class CmcdHeadersFactoryTest {
|
public class CmcdDataTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void createInstance_populatesCmcdHeaders() {
|
public void createInstance_populatesCmcdHttRequestHeaders() {
|
||||||
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
||||||
mediaItem ->
|
mediaItem ->
|
||||||
new CmcdConfiguration(
|
new CmcdConfiguration(
|
||||||
@ -66,21 +67,23 @@ public class CmcdHeadersFactoryTest {
|
|||||||
when(trackSelection.getTrackGroup())
|
when(trackSelection.getTrackGroup())
|
||||||
.thenReturn(new TrackGroup(format, new Format.Builder().setPeakBitrate(1_000_000).build()));
|
.thenReturn(new TrackGroup(format, new Format.Builder().setPeakBitrate(1_000_000).build()));
|
||||||
when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L);
|
when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L);
|
||||||
|
DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
|
||||||
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> requestHeaders =
|
CmcdData cmcdData =
|
||||||
new CmcdHeadersFactory(
|
new CmcdData.Factory(
|
||||||
cmcdConfiguration,
|
cmcdConfiguration,
|
||||||
trackSelection,
|
trackSelection,
|
||||||
/* bufferedDurationUs= */ 1_760_000,
|
/* bufferedDurationUs= */ 1_760_000,
|
||||||
/* playbackRate= */ 2.0f,
|
/* playbackRate= */ 2.0f,
|
||||||
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH,
|
/* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH,
|
||||||
/* isLive= */ true,
|
/* isLive= */ true,
|
||||||
/* didRebuffer= */ true,
|
/* didRebuffer= */ true,
|
||||||
/* isBufferEmpty= */ false)
|
/* isBufferEmpty= */ false)
|
||||||
.setChunkDurationUs(3_000_000)
|
.setChunkDurationUs(3_000_000)
|
||||||
.createHttpRequestHeaders();
|
.createCmcdData();
|
||||||
|
|
||||||
assertThat(requestHeaders)
|
dataSpec = cmcdData.addToDataSpec(dataSpec);
|
||||||
|
|
||||||
|
assertThat(dataSpec.httpRequestHeaders)
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
"CMCD-Object",
|
"CMCD-Object",
|
||||||
"br=840,d=3000,key-1=1,key-2-separated-by-multiple-hyphens=2,tb=1000",
|
"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");
|
"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
|
@Test
|
||||||
public void createInstance_withInvalidNonHyphenatedCustomKey_throwsIllegalStateException() {
|
public void createInstance_withInvalidNonHyphenatedCustomKey_throwsIllegalStateException() {
|
||||||
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
||||||
@ -115,15 +174,15 @@ public class CmcdHeadersFactoryTest {
|
|||||||
assertThrows(
|
assertThrows(
|
||||||
IllegalStateException.class,
|
IllegalStateException.class,
|
||||||
() ->
|
() ->
|
||||||
new CmcdHeadersFactory(
|
new CmcdData.Factory(
|
||||||
cmcdConfiguration,
|
cmcdConfiguration,
|
||||||
trackSelection,
|
trackSelection,
|
||||||
/* bufferedDurationUs= */ 0,
|
/* bufferedDurationUs= */ 0,
|
||||||
/* playbackRate= */ 1.0f,
|
/* playbackRate= */ 1.0f,
|
||||||
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH,
|
/* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH,
|
||||||
/* isLive= */ true,
|
/* isLive= */ true,
|
||||||
/* didRebuffer= */ true,
|
/* didRebuffer= */ true,
|
||||||
/* isBufferEmpty= */ false)
|
/* isBufferEmpty= */ false)
|
||||||
.createHttpRequestHeaders());
|
.createCmcdData());
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -56,7 +56,7 @@ import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
|
|||||||
import androidx.media3.exoplayer.source.chunk.SingleSampleMediaChunk;
|
import androidx.media3.exoplayer.source.chunk.SingleSampleMediaChunk;
|
||||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
|
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;
|
||||||
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
||||||
import androidx.media3.extractor.ChunkIndex;
|
import androidx.media3.extractor.ChunkIndex;
|
||||||
@ -387,15 +387,15 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||||||
int selectedTrackIndex = trackSelection.getSelectedIndex();
|
int selectedTrackIndex = trackSelection.getSelectedIndex();
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
CmcdHeadersFactory cmcdHeadersFactory =
|
CmcdData.Factory cmcdDataFactory =
|
||||||
cmcdConfiguration == null
|
cmcdConfiguration == null
|
||||||
? null
|
? null
|
||||||
: new CmcdHeadersFactory(
|
: new CmcdData.Factory(
|
||||||
cmcdConfiguration,
|
cmcdConfiguration,
|
||||||
trackSelection,
|
trackSelection,
|
||||||
bufferedDurationUs,
|
bufferedDurationUs,
|
||||||
/* playbackRate= */ loadingInfo.playbackSpeed,
|
/* playbackRate= */ loadingInfo.playbackSpeed,
|
||||||
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH,
|
/* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH,
|
||||||
/* isLive= */ manifest.dynamic,
|
/* isLive= */ manifest.dynamic,
|
||||||
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
|
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
|
||||||
/* isBufferEmpty= */ queue.isEmpty());
|
/* isBufferEmpty= */ queue.isEmpty());
|
||||||
@ -423,7 +423,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||||||
trackSelection.getSelectionData(),
|
trackSelection.getSelectionData(),
|
||||||
pendingInitializationUri,
|
pendingInitializationUri,
|
||||||
pendingIndexUri,
|
pendingIndexUri,
|
||||||
cmcdHeadersFactory);
|
cmcdDataFactory);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -501,7 +501,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||||||
maxSegmentCount,
|
maxSegmentCount,
|
||||||
seekTimeUs,
|
seekTimeUs,
|
||||||
nowPeriodTimeUs,
|
nowPeriodTimeUs,
|
||||||
cmcdHeadersFactory);
|
cmcdDataFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -684,7 +684,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||||||
* indexUri} is not {@code null}.
|
* indexUri} is not {@code null}.
|
||||||
* @param indexUri The URI pointing to index data. Can be {@code null} if {@code
|
* @param indexUri The URI pointing to index data. Can be {@code null} if {@code
|
||||||
* initializationUri} is not {@code null}.
|
* 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")
|
@RequiresNonNull("#1.chunkExtractor")
|
||||||
protected Chunk newInitializationChunk(
|
protected Chunk newInitializationChunk(
|
||||||
@ -695,7 +695,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||||||
@Nullable Object trackSelectionData,
|
@Nullable Object trackSelectionData,
|
||||||
@Nullable RangedUri initializationUri,
|
@Nullable RangedUri initializationUri,
|
||||||
@Nullable RangedUri indexUri,
|
@Nullable RangedUri indexUri,
|
||||||
@Nullable CmcdHeadersFactory cmcdHeadersFactory) {
|
@Nullable CmcdData.Factory cmcdDataFactory) {
|
||||||
Representation representation = representationHolder.representation;
|
Representation representation = representationHolder.representation;
|
||||||
RangedUri requestUri;
|
RangedUri requestUri;
|
||||||
if (initializationUri != null) {
|
if (initializationUri != null) {
|
||||||
@ -709,19 +709,19 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||||||
} else {
|
} else {
|
||||||
requestUri = checkNotNull(indexUri);
|
requestUri = checkNotNull(indexUri);
|
||||||
}
|
}
|
||||||
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
|
|
||||||
cmcdHeadersFactory == null
|
|
||||||
? ImmutableMap.of()
|
|
||||||
: cmcdHeadersFactory
|
|
||||||
.setObjectType(CmcdHeadersFactory.OBJECT_TYPE_INIT_SEGMENT)
|
|
||||||
.createHttpRequestHeaders();
|
|
||||||
DataSpec dataSpec =
|
DataSpec dataSpec =
|
||||||
DashUtil.buildDataSpec(
|
DashUtil.buildDataSpec(
|
||||||
representation,
|
representation,
|
||||||
representationHolder.selectedBaseUrl.url,
|
representationHolder.selectedBaseUrl.url,
|
||||||
requestUri,
|
requestUri,
|
||||||
/* flags= */ 0,
|
/* 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(
|
return new InitializationChunk(
|
||||||
dataSource,
|
dataSource,
|
||||||
dataSpec,
|
dataSpec,
|
||||||
@ -742,7 +742,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||||||
int maxSegmentCount,
|
int maxSegmentCount,
|
||||||
long seekTimeUs,
|
long seekTimeUs,
|
||||||
long nowPeriodTimeUs,
|
long nowPeriodTimeUs,
|
||||||
@Nullable CmcdHeadersFactory cmcdHeadersFactory) {
|
@Nullable CmcdData.Factory cmcdDataFactory) {
|
||||||
Representation representation = representationHolder.representation;
|
Representation representation = representationHolder.representation;
|
||||||
long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum);
|
long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum);
|
||||||
RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum);
|
RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum);
|
||||||
@ -753,29 +753,29 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||||||
firstSegmentNum, nowPeriodTimeUs)
|
firstSegmentNum, nowPeriodTimeUs)
|
||||||
? 0
|
? 0
|
||||||
: DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED;
|
: 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 =
|
DataSpec dataSpec =
|
||||||
DashUtil.buildDataSpec(
|
DashUtil.buildDataSpec(
|
||||||
representation,
|
representation,
|
||||||
representationHolder.selectedBaseUrl.url,
|
representationHolder.selectedBaseUrl.url,
|
||||||
segmentUri,
|
segmentUri,
|
||||||
flags,
|
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(
|
return new SingleSampleMediaChunk(
|
||||||
dataSource,
|
dataSource,
|
||||||
dataSpec,
|
dataSpec,
|
||||||
@ -812,29 +812,28 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||||||
representationHolder.isSegmentAvailableAtFullNetworkSpeed(segmentNum, nowPeriodTimeUs)
|
representationHolder.isSegmentAvailableAtFullNetworkSpeed(segmentNum, nowPeriodTimeUs)
|
||||||
? 0
|
? 0
|
||||||
: DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED;
|
: 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 =
|
DataSpec dataSpec =
|
||||||
DashUtil.buildDataSpec(
|
DashUtil.buildDataSpec(
|
||||||
representation,
|
representation,
|
||||||
representationHolder.selectedBaseUrl.url,
|
representationHolder.selectedBaseUrl.url,
|
||||||
segmentUri,
|
segmentUri,
|
||||||
flags,
|
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;
|
long sampleOffsetUs = -representation.presentationTimeOffsetUs;
|
||||||
return new ContainerMediaChunk(
|
return new ContainerMediaChunk(
|
||||||
dataSource,
|
dataSource,
|
||||||
|
@ -301,7 +301,7 @@ public class DefaultDashChunkSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdLoggingHeaders()
|
public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdHttpRequestHeaders()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
|
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
|
||||||
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
|
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
|
||||||
@ -381,7 +381,7 @@ public class DefaultDashChunkSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdLoggingHeaders()
|
public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdHttpRequestHeaders()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
||||||
mediaItem -> {
|
mediaItem -> {
|
||||||
@ -429,7 +429,7 @@ public class DefaultDashChunkSourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdLoggingHeaders()
|
getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpRequestHeaders()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
||||||
mediaItem -> {
|
mediaItem -> {
|
||||||
@ -475,6 +475,53 @@ public class DefaultDashChunkSourceTest {
|
|||||||
"key-4=5.0");
|
"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
|
@Test
|
||||||
public void
|
public void
|
||||||
getNextChunk_afterLastAvailableButBeforeEndOfLiveManifestWithKnownDuration_doesNotReturnEndOfStream()
|
getNextChunk_afterLastAvailableButBeforeEndOfLiveManifestWithKnownDuration_doesNotReturnEndOfStream()
|
||||||
|
@ -50,9 +50,8 @@ import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
|
|||||||
import androidx.media3.exoplayer.trackselection.BaseTrackSelection;
|
import androidx.media3.exoplayer.trackselection.BaseTrackSelection;
|
||||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
|
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.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.primitives.Ints;
|
import com.google.common.primitives.Ints;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -178,6 +177,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
* an infinite timeout.
|
* an infinite timeout.
|
||||||
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
|
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
|
||||||
* information is available in the multivariant playlist.
|
* 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(
|
public HlsChunkSource(
|
||||||
HlsExtractorFactory extractorFactory,
|
HlsExtractorFactory extractorFactory,
|
||||||
@ -488,22 +489,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
seenExpectedPlaylistError = false;
|
seenExpectedPlaylistError = false;
|
||||||
expectedPlaylistUrl = null;
|
expectedPlaylistUrl = null;
|
||||||
|
|
||||||
@Nullable CmcdHeadersFactory cmcdHeadersFactory = null;
|
@Nullable CmcdData.Factory cmcdDataFactory = null;
|
||||||
if (cmcdConfiguration != null) {
|
if (cmcdConfiguration != null) {
|
||||||
cmcdHeadersFactory =
|
cmcdDataFactory =
|
||||||
new CmcdHeadersFactory(
|
new CmcdData.Factory(
|
||||||
cmcdConfiguration,
|
cmcdConfiguration,
|
||||||
trackSelection,
|
trackSelection,
|
||||||
bufferedDurationUs,
|
bufferedDurationUs,
|
||||||
/* playbackRate= */ loadingInfo.playbackSpeed,
|
/* playbackRate= */ loadingInfo.playbackSpeed,
|
||||||
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_HLS,
|
/* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_HLS,
|
||||||
/* isLive= */ !playlist.hasEndTag,
|
/* isLive= */ !playlist.hasEndTag,
|
||||||
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
|
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
|
||||||
/* isBufferEmpty= */ queue.isEmpty())
|
/* isBufferEmpty= */ queue.isEmpty())
|
||||||
.setObjectType(
|
.setObjectType(
|
||||||
getIsMuxedAudioAndVideo()
|
getIsMuxedAudioAndVideo()
|
||||||
? CmcdHeadersFactory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
|
? CmcdData.Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
|
||||||
: CmcdHeadersFactory.getObjectType(trackSelection));
|
: CmcdData.Factory.getObjectType(trackSelection));
|
||||||
|
|
||||||
long nextChunkMediaSequence =
|
long nextChunkMediaSequence =
|
||||||
partIndex == C.LENGTH_UNSET
|
partIndex == C.LENGTH_UNSET
|
||||||
@ -515,7 +516,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
if (nextSegmentBaseHolder != null) {
|
if (nextSegmentBaseHolder != null) {
|
||||||
Uri uri = UriUtil.resolveToUri(playlist.baseUri, segmentBaseHolder.segmentBase.url);
|
Uri uri = UriUtil.resolveToUri(playlist.baseUri, segmentBaseHolder.segmentBase.url);
|
||||||
Uri nextUri = UriUtil.resolveToUri(playlist.baseUri, nextSegmentBaseHolder.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 + "-";
|
String nextRangeRequest = nextSegmentBaseHolder.segmentBase.byteRangeOffset + "-";
|
||||||
if (nextSegmentBaseHolder.segmentBase.byteRangeLength != C.LENGTH_UNSET) {
|
if (nextSegmentBaseHolder.segmentBase.byteRangeLength != C.LENGTH_UNSET) {
|
||||||
@ -523,7 +524,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
(nextSegmentBaseHolder.segmentBase.byteRangeOffset
|
(nextSegmentBaseHolder.segmentBase.byteRangeOffset
|
||||||
+ nextSegmentBaseHolder.segmentBase.byteRangeLength);
|
+ nextSegmentBaseHolder.segmentBase.byteRangeLength);
|
||||||
}
|
}
|
||||||
cmcdHeadersFactory.setNextRangeRequest(nextRangeRequest);
|
cmcdDataFactory.setNextRangeRequest(nextRangeRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();
|
lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();
|
||||||
@ -534,7 +535,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase.initializationSegment);
|
getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase.initializationSegment);
|
||||||
out.chunk =
|
out.chunk =
|
||||||
maybeCreateEncryptionChunkFor(
|
maybeCreateEncryptionChunkFor(
|
||||||
initSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ true, cmcdHeadersFactory);
|
initSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ true, cmcdDataFactory);
|
||||||
if (out.chunk != null) {
|
if (out.chunk != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -542,7 +543,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase);
|
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase);
|
||||||
out.chunk =
|
out.chunk =
|
||||||
maybeCreateEncryptionChunkFor(
|
maybeCreateEncryptionChunkFor(
|
||||||
mediaSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ false, cmcdHeadersFactory);
|
mediaSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ false, cmcdDataFactory);
|
||||||
if (out.chunk != null) {
|
if (out.chunk != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -578,7 +579,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri),
|
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri),
|
||||||
shouldSpliceIn,
|
shouldSpliceIn,
|
||||||
playerId,
|
playerId,
|
||||||
cmcdHeadersFactory);
|
cmcdDataFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean getIsMuxedAudioAndVideo() {
|
private boolean getIsMuxedAudioAndVideo() {
|
||||||
@ -896,7 +897,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
@Nullable Uri keyUri,
|
@Nullable Uri keyUri,
|
||||||
int selectedTrackIndex,
|
int selectedTrackIndex,
|
||||||
boolean isInitSegment,
|
boolean isInitSegment,
|
||||||
@Nullable CmcdHeadersFactory cmcdHeadersFactory) {
|
@Nullable CmcdData.Factory cmcdDataFactory) {
|
||||||
if (keyUri == null) {
|
if (keyUri == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -910,20 +911,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
return null;
|
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 =
|
DataSpec dataSpec =
|
||||||
new DataSpec.Builder()
|
new DataSpec.Builder().setUri(keyUri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build();
|
||||||
.setUri(keyUri)
|
if (cmcdDataFactory != null) {
|
||||||
.setFlags(DataSpec.FLAG_ALLOW_GZIP)
|
if (isInitSegment) {
|
||||||
.setHttpRequestHeaders(httpRequestHeaders)
|
cmcdDataFactory.setObjectType(CmcdData.Factory.OBJECT_TYPE_INIT_SEGMENT);
|
||||||
.build();
|
}
|
||||||
|
CmcdData cmcdData = cmcdDataFactory.createCmcdData();
|
||||||
|
dataSpec = cmcdData.addToDataSpec(dataSpec);
|
||||||
|
}
|
||||||
|
|
||||||
return new EncryptionKeyChunk(
|
return new EncryptionKeyChunk(
|
||||||
encryptionDataSource,
|
encryptionDataSource,
|
||||||
dataSpec,
|
dataSpec,
|
||||||
|
@ -33,15 +33,13 @@ import androidx.media3.datasource.DataSpec;
|
|||||||
import androidx.media3.exoplayer.analytics.PlayerId;
|
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
|
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
|
||||||
import androidx.media3.exoplayer.source.chunk.MediaChunk;
|
import androidx.media3.exoplayer.source.chunk.MediaChunk;
|
||||||
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
|
import androidx.media3.exoplayer.upstream.CmcdData;
|
||||||
import androidx.media3.exoplayer.upstream.CmcdHeadersFactory;
|
|
||||||
import androidx.media3.extractor.DefaultExtractorInput;
|
import androidx.media3.extractor.DefaultExtractorInput;
|
||||||
import androidx.media3.extractor.ExtractorInput;
|
import androidx.media3.extractor.ExtractorInput;
|
||||||
import androidx.media3.extractor.metadata.id3.Id3Decoder;
|
import androidx.media3.extractor.metadata.id3.Id3Decoder;
|
||||||
import androidx.media3.extractor.metadata.id3.PrivFrame;
|
import androidx.media3.extractor.metadata.id3.PrivFrame;
|
||||||
import com.google.common.base.Ascii;
|
import com.google.common.base.Ascii;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
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
|
* @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null
|
||||||
* otherwise.
|
* otherwise.
|
||||||
* @param shouldSpliceIn Whether samples for this chunk should be spliced into existing samples.
|
* @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(
|
public static HlsMediaChunk createInstance(
|
||||||
HlsExtractorFactory extractorFactory,
|
HlsExtractorFactory extractorFactory,
|
||||||
@ -103,23 +101,22 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||||||
@Nullable byte[] initSegmentKey,
|
@Nullable byte[] initSegmentKey,
|
||||||
boolean shouldSpliceIn,
|
boolean shouldSpliceIn,
|
||||||
PlayerId playerId,
|
PlayerId playerId,
|
||||||
@Nullable CmcdHeadersFactory cmcdHeadersFactory) {
|
@Nullable CmcdData.Factory cmcdDataFactory) {
|
||||||
// Media segment.
|
// Media segment.
|
||||||
HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase;
|
HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase;
|
||||||
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
|
|
||||||
cmcdHeadersFactory == null
|
|
||||||
? ImmutableMap.of()
|
|
||||||
: cmcdHeadersFactory
|
|
||||||
.setChunkDurationUs(mediaSegment.durationUs)
|
|
||||||
.createHttpRequestHeaders();
|
|
||||||
DataSpec dataSpec =
|
DataSpec dataSpec =
|
||||||
new DataSpec.Builder()
|
new DataSpec.Builder()
|
||||||
.setUri(UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url))
|
.setUri(UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url))
|
||||||
.setPosition(mediaSegment.byteRangeOffset)
|
.setPosition(mediaSegment.byteRangeOffset)
|
||||||
.setLength(mediaSegment.byteRangeLength)
|
.setLength(mediaSegment.byteRangeLength)
|
||||||
.setFlags(segmentBaseHolder.isPreload ? FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED : 0)
|
.setFlags(segmentBaseHolder.isPreload ? FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED : 0)
|
||||||
.setHttpRequestHeaders(httpRequestHeaders)
|
|
||||||
.build();
|
.build();
|
||||||
|
if (cmcdDataFactory != null) {
|
||||||
|
CmcdData cmcdData =
|
||||||
|
cmcdDataFactory.setChunkDurationUs(mediaSegment.durationUs).createCmcdData();
|
||||||
|
dataSpec = cmcdData.addToDataSpec(dataSpec);
|
||||||
|
}
|
||||||
|
|
||||||
boolean mediaSegmentEncrypted = mediaSegmentKey != null;
|
boolean mediaSegmentEncrypted = mediaSegmentKey != null;
|
||||||
@Nullable
|
@Nullable
|
||||||
byte[] mediaSegmentIv =
|
byte[] mediaSegmentIv =
|
||||||
@ -141,19 +138,20 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||||||
? getEncryptionIvArray(Assertions.checkNotNull(initSegment.encryptionIV))
|
? getEncryptionIvArray(Assertions.checkNotNull(initSegment.encryptionIV))
|
||||||
: null;
|
: null;
|
||||||
Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url);
|
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 =
|
initDataSpec =
|
||||||
new DataSpec.Builder()
|
new DataSpec.Builder()
|
||||||
.setUri(initSegmentUri)
|
.setUri(initSegmentUri)
|
||||||
.setPosition(initSegment.byteRangeOffset)
|
.setPosition(initSegment.byteRangeOffset)
|
||||||
.setLength(initSegment.byteRangeLength)
|
.setLength(initSegment.byteRangeLength)
|
||||||
.setHttpRequestHeaders(initHttpRequestHeaders)
|
|
||||||
.build();
|
.build();
|
||||||
|
if (cmcdDataFactory != null) {
|
||||||
|
CmcdData cmcdData =
|
||||||
|
cmcdDataFactory
|
||||||
|
.setObjectType(CmcdData.Factory.OBJECT_TYPE_INIT_SEGMENT)
|
||||||
|
.createCmcdData();
|
||||||
|
initDataSpec = cmcdData.addToDataSpec(dataSpec);
|
||||||
|
}
|
||||||
|
|
||||||
initDataSource = buildDataSource(dataSource, initSegmentKey, initSegmentIv);
|
initDataSource = buildDataSource(dataSource, initSegmentKey, initSegmentIv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,8 +195,7 @@ public class HlsChunkSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdLoggingHeaders()
|
public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdHttpRequestHeaders() {
|
||||||
throws Exception {
|
|
||||||
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
|
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
|
||||||
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
|
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
|
||||||
CmcdConfiguration cmcdConfiguration =
|
CmcdConfiguration cmcdConfiguration =
|
||||||
@ -238,8 +237,8 @@ public class HlsChunkSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCorrectBufferStarvationKey()
|
public void
|
||||||
throws Exception {
|
getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCorrectBufferStarvationKey() {
|
||||||
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
|
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
|
||||||
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
|
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
|
||||||
CmcdConfiguration cmcdConfiguration =
|
CmcdConfiguration cmcdConfiguration =
|
||||||
@ -289,8 +288,7 @@ public class HlsChunkSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdLoggingHeaders()
|
public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdHttpRequestHeaders() {
|
||||||
throws Exception {
|
|
||||||
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
||||||
mediaItem -> {
|
mediaItem -> {
|
||||||
CmcdConfiguration.RequestConfig cmcdRequestConfig =
|
CmcdConfiguration.RequestConfig cmcdRequestConfig =
|
||||||
@ -338,7 +336,7 @@ public class HlsChunkSourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdLoggingHeaders()
|
getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpRequestHeaders()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
||||||
mediaItem -> {
|
mediaItem -> {
|
||||||
@ -385,6 +383,53 @@ public class HlsChunkSourceTest {
|
|||||||
"key-4=5.0");
|
"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) {
|
private HlsChunkSource createHlsChunkSource(@Nullable CmcdConfiguration cmcdConfiguration) {
|
||||||
return new HlsChunkSource(
|
return new HlsChunkSource(
|
||||||
HlsExtractorFactory.DEFAULT,
|
HlsExtractorFactory.DEFAULT,
|
||||||
|
@ -43,14 +43,13 @@ import androidx.media3.exoplayer.source.chunk.MediaChunk;
|
|||||||
import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
|
import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
|
||||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
|
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;
|
||||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.FallbackSelection;
|
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.FallbackSelection;
|
||||||
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
|
||||||
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
||||||
import androidx.media3.extractor.mp4.Track;
|
import androidx.media3.extractor.mp4.Track;
|
||||||
import androidx.media3.extractor.mp4.TrackEncryptionBox;
|
import androidx.media3.extractor.mp4.TrackEncryptionBox;
|
||||||
import com.google.common.collect.ImmutableMap;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -291,24 +290,24 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
|||||||
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex);
|
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex);
|
||||||
Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex);
|
Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex);
|
||||||
|
|
||||||
@Nullable CmcdHeadersFactory cmcdHeadersFactory = null;
|
@Nullable CmcdData.Factory cmcdDataFactory = null;
|
||||||
if (cmcdConfiguration != null) {
|
if (cmcdConfiguration != null) {
|
||||||
cmcdHeadersFactory =
|
cmcdDataFactory =
|
||||||
new CmcdHeadersFactory(
|
new CmcdData.Factory(
|
||||||
cmcdConfiguration,
|
cmcdConfiguration,
|
||||||
trackSelection,
|
trackSelection,
|
||||||
bufferedDurationUs,
|
bufferedDurationUs,
|
||||||
/* playbackRate= */ loadingInfo.playbackSpeed,
|
/* playbackRate= */ loadingInfo.playbackSpeed,
|
||||||
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_SS,
|
/* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_SS,
|
||||||
/* isLive= */ manifest.isLive,
|
/* isLive= */ manifest.isLive,
|
||||||
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
|
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
|
||||||
/* isBufferEmpty= */ queue.isEmpty())
|
/* isBufferEmpty= */ queue.isEmpty())
|
||||||
.setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs)
|
.setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs)
|
||||||
.setObjectType(CmcdHeadersFactory.getObjectType(trackSelection));
|
.setObjectType(CmcdData.Factory.getObjectType(trackSelection));
|
||||||
|
|
||||||
if (chunkIndex + 1 < streamElement.chunkCount) {
|
if (chunkIndex + 1 < streamElement.chunkCount) {
|
||||||
Uri nextUri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex + 1);
|
Uri nextUri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex + 1);
|
||||||
cmcdHeadersFactory.setNextObjectRequest(UriUtil.getRelativePath(uri, nextUri));
|
cmcdDataFactory.setNextObjectRequest(UriUtil.getRelativePath(uri, nextUri));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();
|
lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();
|
||||||
@ -325,7 +324,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
|||||||
trackSelection.getSelectionReason(),
|
trackSelection.getSelectionReason(),
|
||||||
trackSelection.getSelectionData(),
|
trackSelection.getSelectionData(),
|
||||||
chunkExtractor,
|
chunkExtractor,
|
||||||
cmcdHeadersFactory);
|
cmcdDataFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -370,13 +369,13 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
|||||||
@C.SelectionReason int trackSelectionReason,
|
@C.SelectionReason int trackSelectionReason,
|
||||||
@Nullable Object trackSelectionData,
|
@Nullable Object trackSelectionData,
|
||||||
ChunkExtractor chunkExtractor,
|
ChunkExtractor chunkExtractor,
|
||||||
@Nullable CmcdHeadersFactory cmcdHeadersFactory) {
|
@Nullable CmcdData.Factory cmcdDataFactory) {
|
||||||
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
|
DataSpec dataSpec = new DataSpec.Builder().setUri(uri).build();
|
||||||
cmcdHeadersFactory == null
|
if (cmcdDataFactory != null) {
|
||||||
? ImmutableMap.of()
|
CmcdData cmcdData = cmcdDataFactory.createCmcdData();
|
||||||
: cmcdHeadersFactory.createHttpRequestHeaders();
|
dataSpec = cmcdData.addToDataSpec(dataSpec);
|
||||||
DataSpec dataSpec =
|
}
|
||||||
new DataSpec.Builder().setUri(uri).setHttpRequestHeaders(httpRequestHeaders).build();
|
|
||||||
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
|
// 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.
|
// To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs.
|
||||||
long sampleOffsetUs = chunkStartTimeUs;
|
long sampleOffsetUs = chunkStartTimeUs;
|
||||||
|
@ -51,7 +51,7 @@ public class DefaultSsChunkSourceTest {
|
|||||||
private static final String SAMPLE_ISMC_1 = "media/smooth-streaming/sample_ismc_1";
|
private static final String SAMPLE_ISMC_1 = "media/smooth-streaming/sample_ismc_1";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdLoggingHeaders()
|
public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdHttpRequestHeaders()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
|
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
|
||||||
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
|
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
|
||||||
@ -131,7 +131,7 @@ public class DefaultSsChunkSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdLoggingHeaders()
|
public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdHttpRequestHeaders()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
||||||
mediaItem -> {
|
mediaItem -> {
|
||||||
@ -179,7 +179,7 @@ public class DefaultSsChunkSourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdLoggingHeaders()
|
getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpRequestHeaders()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
CmcdConfiguration.Factory cmcdConfigurationFactory =
|
||||||
mediaItem -> {
|
mediaItem -> {
|
||||||
@ -225,6 +225,53 @@ public class DefaultSsChunkSourceTest {
|
|||||||
"key-4=5.0");
|
"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(
|
private SsChunkSource createSsChunkSource(
|
||||||
int numberOfTracks, @Nullable CmcdConfiguration cmcdConfiguration) throws IOException {
|
int numberOfTracks, @Nullable CmcdConfiguration cmcdConfiguration) throws IOException {
|
||||||
Assertions.checkArgument(numberOfTracks < 6);
|
Assertions.checkArgument(numberOfTracks < 6);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user