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,77 +43,24 @@ 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]+)+");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the object type value from the given {@link ExoTrackSelection}.
|
|
||||||
*
|
|
||||||
* @param trackSelection The {@link ExoTrackSelection} from which to retrieve the object type.
|
|
||||||
* @return The object type value as a String if {@link TrackType} can be mapped to one of the
|
|
||||||
* object types specified by {@link ObjectType} annotation, or {@code null}.
|
|
||||||
* @throws IllegalArgumentException if the provided {@link ExoTrackSelection} is {@code null}.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public static @ObjectType String getObjectType(ExoTrackSelection trackSelection) {
|
|
||||||
checkArgument(trackSelection != null);
|
|
||||||
@C.TrackType
|
|
||||||
int trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType);
|
|
||||||
if (trackType == C.TRACK_TYPE_UNKNOWN) {
|
|
||||||
trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().containerMimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Indicates the streaming format used for media content. */
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
|
||||||
@StringDef({STREAMING_FORMAT_DASH, STREAMING_FORMAT_HLS, STREAMING_FORMAT_SS})
|
|
||||||
@Documented
|
|
||||||
@Target(TYPE_USE)
|
|
||||||
public @interface StreamingFormat {}
|
|
||||||
|
|
||||||
/** Indicates the type of streaming for media content. */
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
|
||||||
@StringDef({STREAM_TYPE_VOD, STREAM_TYPE_LIVE})
|
|
||||||
@Documented
|
|
||||||
@Target(TYPE_USE)
|
|
||||||
public @interface StreamType {}
|
|
||||||
|
|
||||||
/** Indicates the media type of current object being requested. */
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
|
||||||
@StringDef({
|
|
||||||
OBJECT_TYPE_INIT_SEGMENT,
|
|
||||||
OBJECT_TYPE_AUDIO_ONLY,
|
|
||||||
OBJECT_TYPE_VIDEO_ONLY,
|
|
||||||
OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
|
|
||||||
})
|
|
||||||
@Documented
|
|
||||||
@Target(TYPE_USE)
|
|
||||||
public @interface ObjectType {}
|
|
||||||
|
|
||||||
/** Represents the Dynamic Adaptive Streaming over HTTP (DASH) format. */
|
/** Represents the Dynamic Adaptive Streaming over HTTP (DASH) format. */
|
||||||
public static final String STREAMING_FORMAT_DASH = "d";
|
public static final String STREAMING_FORMAT_DASH = "d";
|
||||||
@ -140,16 +89,19 @@ public final class CmcdHeadersFactory {
|
|||||||
/** Represents the object type for muxed audio and video content in a media container. */
|
/** 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";
|
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 CmcdConfiguration cmcdConfiguration;
|
||||||
private final ExoTrackSelection trackSelection;
|
private final ExoTrackSelection trackSelection;
|
||||||
private final long bufferedDurationUs;
|
private final long bufferedDurationUs;
|
||||||
private final float playbackRate;
|
private final float playbackRate;
|
||||||
private final @StreamingFormat String streamingFormat;
|
private final @CmcdData.StreamingFormat String streamingFormat;
|
||||||
private final boolean isLive;
|
private final boolean isLive;
|
||||||
private final boolean didRebuffer;
|
private final boolean didRebuffer;
|
||||||
private final boolean isBufferEmpty;
|
private final boolean isBufferEmpty;
|
||||||
private long chunkDurationUs;
|
private long chunkDurationUs;
|
||||||
private @Nullable @ObjectType String objectType;
|
@Nullable private @CmcdData.ObjectType String objectType;
|
||||||
@Nullable private String nextObjectRequest;
|
@Nullable private String nextObjectRequest;
|
||||||
@Nullable private String nextRangeRequest;
|
@Nullable private String nextRangeRequest;
|
||||||
|
|
||||||
@ -162,7 +114,7 @@ public final class CmcdHeadersFactory {
|
|||||||
* position, in microseconds.
|
* position, in microseconds.
|
||||||
* @param playbackRate The playback rate indicating the current speed of playback.
|
* @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
|
* @param streamingFormat The streaming format of the media content. Must be one of the allowed
|
||||||
* streaming formats specified by the {@link StreamingFormat} annotation.
|
* streaming formats specified by the {@link CmcdData.StreamingFormat} annotation.
|
||||||
* @param isLive {@code true} if the media content is being streamed live, {@code false}
|
* @param isLive {@code true} if the media content is being streamed live, {@code false}
|
||||||
* otherwise.
|
* otherwise.
|
||||||
* @param didRebuffer {@code true} if a rebuffering event happened between the previous request
|
* @param didRebuffer {@code true} if a rebuffering event happened between the previous request
|
||||||
@ -171,12 +123,12 @@ public final class CmcdHeadersFactory {
|
|||||||
* otherwise.
|
* otherwise.
|
||||||
* @throws IllegalArgumentException If {@code bufferedDurationUs} is negative.
|
* @throws IllegalArgumentException If {@code bufferedDurationUs} is negative.
|
||||||
*/
|
*/
|
||||||
public CmcdHeadersFactory(
|
public Factory(
|
||||||
CmcdConfiguration cmcdConfiguration,
|
CmcdConfiguration cmcdConfiguration,
|
||||||
ExoTrackSelection trackSelection,
|
ExoTrackSelection trackSelection,
|
||||||
long bufferedDurationUs,
|
long bufferedDurationUs,
|
||||||
float playbackRate,
|
float playbackRate,
|
||||||
@StreamingFormat String streamingFormat,
|
@CmcdData.StreamingFormat String streamingFormat,
|
||||||
boolean isLive,
|
boolean isLive,
|
||||||
boolean didRebuffer,
|
boolean didRebuffer,
|
||||||
boolean isBufferEmpty) {
|
boolean isBufferEmpty) {
|
||||||
@ -194,13 +146,40 @@ public final class CmcdHeadersFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the duration of current media chunk being requested, in microseconds. The default value is
|
* Retrieves the object type value from the given {@link ExoTrackSelection}.
|
||||||
* {@link C#TIME_UNSET}.
|
*
|
||||||
|
* @param trackSelection The {@link ExoTrackSelection} from which to retrieve the object type.
|
||||||
|
* @return The object type value as a String if {@link TrackType} can be mapped to one of the
|
||||||
|
* object types specified by {@link CmcdData.ObjectType} annotation, or {@code null}.
|
||||||
|
* @throws IllegalArgumentException if the provided {@link ExoTrackSelection} is {@code null}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static @CmcdData.ObjectType String getObjectType(ExoTrackSelection trackSelection) {
|
||||||
|
checkArgument(trackSelection != null);
|
||||||
|
@TrackType
|
||||||
|
int trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType);
|
||||||
|
if (trackType == C.TRACK_TYPE_UNKNOWN) {
|
||||||
|
trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().containerMimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackType == C.TRACK_TYPE_AUDIO) {
|
||||||
|
return OBJECT_TYPE_AUDIO_ONLY;
|
||||||
|
} else if (trackType == C.TRACK_TYPE_VIDEO) {
|
||||||
|
return OBJECT_TYPE_VIDEO_ONLY;
|
||||||
|
} else {
|
||||||
|
// Track type cannot be mapped to a known object type.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the duration of current media chunk being requested, in microseconds. The default value
|
||||||
|
* is {@link C#TIME_UNSET}.
|
||||||
*
|
*
|
||||||
* @throws IllegalArgumentException If {@code chunkDurationUs} is negative.
|
* @throws IllegalArgumentException If {@code chunkDurationUs} is negative.
|
||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public CmcdHeadersFactory setChunkDurationUs(long chunkDurationUs) {
|
public Factory setChunkDurationUs(long chunkDurationUs) {
|
||||||
checkArgument(chunkDurationUs >= 0);
|
checkArgument(chunkDurationUs >= 0);
|
||||||
this.chunkDurationUs = chunkDurationUs;
|
this.chunkDurationUs = chunkDurationUs;
|
||||||
return this;
|
return this;
|
||||||
@ -208,12 +187,12 @@ public final class CmcdHeadersFactory {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the object type of the current object being requested. Must be one of the allowed object
|
* Sets the object type of the current object being requested. Must be one of the allowed object
|
||||||
* types specified by the {@link ObjectType} annotation.
|
* types specified by the {@link CmcdData.ObjectType} annotation.
|
||||||
*
|
*
|
||||||
* <p>Default is {@code null}.
|
* <p>Default is {@code null}.
|
||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public CmcdHeadersFactory setObjectType(@Nullable @ObjectType String objectType) {
|
public Factory setObjectType(@Nullable @CmcdData.ObjectType String objectType) {
|
||||||
this.objectType = objectType;
|
this.objectType = objectType;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -225,7 +204,7 @@ public final class CmcdHeadersFactory {
|
|||||||
* <p>Default is {@code null}.
|
* <p>Default is {@code null}.
|
||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public CmcdHeadersFactory setNextObjectRequest(@Nullable String nextObjectRequest) {
|
public Factory setNextObjectRequest(@Nullable String nextObjectRequest) {
|
||||||
this.nextObjectRequest = nextObjectRequest;
|
this.nextObjectRequest = nextObjectRequest;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -237,13 +216,12 @@ public final class CmcdHeadersFactory {
|
|||||||
* <p>Default is {@code null}.
|
* <p>Default is {@code null}.
|
||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public CmcdHeadersFactory setNextRangeRequest(@Nullable String nextRangeRequest) {
|
public Factory setNextRangeRequest(@Nullable String nextRangeRequest) {
|
||||||
this.nextRangeRequest = nextRangeRequest;
|
this.nextRangeRequest = nextRangeRequest;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates and returns a new {@link ImmutableMap} containing the CMCD HTTP request headers. */
|
public CmcdData createCmcdData() {
|
||||||
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> createHttpRequestHeaders() {
|
|
||||||
ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> customData =
|
ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> customData =
|
||||||
cmcdConfiguration.requestConfig.getCustomData();
|
cmcdConfiguration.requestConfig.getCustomData();
|
||||||
for (String headerKey : customData.keySet()) {
|
for (String headerKey : customData.keySet()) {
|
||||||
@ -333,12 +311,12 @@ public final class CmcdHeadersFactory {
|
|||||||
cmcdStatus.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_STATUS));
|
cmcdStatus.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_STATUS));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImmutableMap.Builder<String, String> httpRequestHeaders = ImmutableMap.builder();
|
return new CmcdData(
|
||||||
cmcdObject.build().populateHttpRequestHeaders(httpRequestHeaders);
|
cmcdObject.build(),
|
||||||
cmcdRequest.build().populateHttpRequestHeaders(httpRequestHeaders);
|
cmcdRequest.build(),
|
||||||
cmcdSession.build().populateHttpRequestHeaders(httpRequestHeaders);
|
cmcdSession.build(),
|
||||||
cmcdStatus.build().populateHttpRequestHeaders(httpRequestHeaders);
|
cmcdStatus.build(),
|
||||||
return httpRequestHeaders.buildOrThrow();
|
cmcdConfiguration.dataTransmissionMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean getIsInitSegment() {
|
private boolean getIsInitSegment() {
|
||||||
@ -351,6 +329,94 @@ public final class CmcdHeadersFactory {
|
|||||||
checkState(CUSTOM_KEY_NAME_PATTERN.matcher(key).matches());
|
checkState(CUSTOM_KEY_NAME_PATTERN.matcher(key).matches());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Indicates the streaming format used for media content. */
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@StringDef({
|
||||||
|
Factory.STREAMING_FORMAT_DASH,
|
||||||
|
Factory.STREAMING_FORMAT_HLS,
|
||||||
|
Factory.STREAMING_FORMAT_SS
|
||||||
|
})
|
||||||
|
@Documented
|
||||||
|
@Target(TYPE_USE)
|
||||||
|
public @interface StreamingFormat {}
|
||||||
|
|
||||||
|
/** Indicates the type of streaming for media content. */
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@StringDef({Factory.STREAM_TYPE_VOD, Factory.STREAM_TYPE_LIVE})
|
||||||
|
@Documented
|
||||||
|
@Target(TYPE_USE)
|
||||||
|
public @interface StreamType {}
|
||||||
|
|
||||||
|
/** Indicates the media type of current object being requested. */
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@StringDef({
|
||||||
|
Factory.OBJECT_TYPE_INIT_SEGMENT,
|
||||||
|
Factory.OBJECT_TYPE_AUDIO_ONLY,
|
||||||
|
Factory.OBJECT_TYPE_VIDEO_ONLY,
|
||||||
|
Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
|
||||||
|
})
|
||||||
|
@Documented
|
||||||
|
@Target(TYPE_USE)
|
||||||
|
public @interface ObjectType {}
|
||||||
|
|
||||||
|
private static final Joiner COMMA_JOINER = Joiner.on(",");
|
||||||
|
|
||||||
|
private final CmcdObject cmcdObject;
|
||||||
|
private final CmcdRequest cmcdRequest;
|
||||||
|
private final CmcdSession cmcdSession;
|
||||||
|
private final CmcdStatus cmcdStatus;
|
||||||
|
private final @CmcdConfiguration.DataTransmissionMode int dataTransmissionMode;
|
||||||
|
|
||||||
|
private CmcdData(
|
||||||
|
CmcdObject cmcdObject,
|
||||||
|
CmcdRequest cmcdRequest,
|
||||||
|
CmcdSession cmcdSession,
|
||||||
|
CmcdStatus cmcdStatus,
|
||||||
|
@CmcdConfiguration.DataTransmissionMode int datatTransmissionMode) {
|
||||||
|
this.cmcdObject = cmcdObject;
|
||||||
|
this.cmcdRequest = cmcdRequest;
|
||||||
|
this.cmcdSession = cmcdSession;
|
||||||
|
this.cmcdStatus = cmcdStatus;
|
||||||
|
this.dataTransmissionMode = datatTransmissionMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds Common Media Client Data (CMCD) related information to the provided {@link DataSpec}
|
||||||
|
* object.
|
||||||
|
*/
|
||||||
|
public DataSpec addToDataSpec(DataSpec dataSpec) {
|
||||||
|
ArrayListMultimap<String, String> cmcdDataMap = ArrayListMultimap.create();
|
||||||
|
cmcdObject.populateCmcdDataMap(cmcdDataMap);
|
||||||
|
cmcdRequest.populateCmcdDataMap(cmcdDataMap);
|
||||||
|
cmcdSession.populateCmcdDataMap(cmcdDataMap);
|
||||||
|
cmcdStatus.populateCmcdDataMap(cmcdDataMap);
|
||||||
|
|
||||||
|
if (dataTransmissionMode == CmcdConfiguration.MODE_REQUEST_HEADER) {
|
||||||
|
ImmutableMap.Builder<String, String> httpRequestHeaders = ImmutableMap.builder();
|
||||||
|
for (String headerKey : cmcdDataMap.keySet()) {
|
||||||
|
List<String> headerValues = cmcdDataMap.get(headerKey);
|
||||||
|
Collections.sort(headerValues);
|
||||||
|
httpRequestHeaders.put(headerKey, COMMA_JOINER.join(headerValues));
|
||||||
|
}
|
||||||
|
return dataSpec.withAdditionalHeaders(httpRequestHeaders.buildOrThrow());
|
||||||
|
} else {
|
||||||
|
List<String> keyValuePairs = new ArrayList<>();
|
||||||
|
for (Collection<String> values : cmcdDataMap.asMap().values()) {
|
||||||
|
keyValuePairs.addAll(values);
|
||||||
|
}
|
||||||
|
Collections.sort(keyValuePairs);
|
||||||
|
Uri.Builder uriBuilder =
|
||||||
|
dataSpec
|
||||||
|
.uri
|
||||||
|
.buildUpon()
|
||||||
|
.appendQueryParameter(
|
||||||
|
CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY,
|
||||||
|
Uri.encode(COMMA_JOINER.join(keyValuePairs)));
|
||||||
|
return dataSpec.buildUpon().setUri(uriBuilder.build()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keys whose values vary with the object being requested. Contains CMCD fields: {@code br},
|
* Keys whose values vary with the object being requested. Contains CMCD fields: {@code br},
|
||||||
@ -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