From 206a663dca9b418e9b01b682f66d5ec0562674fb Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 31 Aug 2023 02:08:38 -0700 Subject: [PATCH] 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 --- RELEASENOTES.md | 2 + .../exoplayer/upstream/CmcdConfiguration.java | 26 +- ...{CmcdHeadersFactory.java => CmcdData.java} | 704 ++++++++++-------- ...dersFactoryTest.java => CmcdDataTest.java} | 85 ++- .../dash/DefaultDashChunkSource.java | 99 ++- .../dash/DefaultDashChunkSourceTest.java | 53 +- .../media3/exoplayer/hls/HlsChunkSource.java | 51 +- .../media3/exoplayer/hls/HlsMediaChunk.java | 36 +- .../exoplayer/hls/HlsChunkSourceTest.java | 59 +- .../smoothstreaming/DefaultSsChunkSource.java | 31 +- .../DefaultSsChunkSourceTest.java | 53 +- 11 files changed, 735 insertions(+), 464 deletions(-) rename libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/{CmcdHeadersFactory.java => CmcdData.java} (59%) rename libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/{CmcdHeadersFactoryTest.java => CmcdDataTest.java} (60%) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4289b972e9..acb85e4a18 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,6 +12,8 @@ * Add additional fields to Common Media Client Data (CMCD) logging: next object request (`nor`) and next range request (`nrr`) ([#8699](https://github.com/google/ExoPlayer/issues/8699)). + * Add functionality to transmit Common Media Client Data (CMCD) data using + query parameters ([#553](https://github.com/androidx/media/issues/553)). * Transformer: * Changed `frameRate` and `durationUs` parameters of `SampleConsumer.queueInputBitmap` to `TimestampIterator`. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java index e73adcec96..b302f0b60a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java @@ -19,6 +19,7 @@ import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static java.lang.annotation.ElementType.TYPE_USE; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.StringDef; import androidx.media3.common.C; @@ -79,6 +80,13 @@ public final class CmcdConfiguration { @Target(TYPE_USE) public @interface CmcdKey {} + /** Indicates the mode used for data transmission. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({MODE_REQUEST_HEADER, MODE_QUERY_PARAMETER}) + @Documented + @Target(TYPE_USE) + public @interface DataTransmissionMode {} + /** Maximum length for ID fields. */ public static final int MAX_ID_LENGTH = 64; @@ -86,6 +94,7 @@ public final class CmcdConfiguration { public static final String KEY_CMCD_REQUEST = "CMCD-Request"; public static final String KEY_CMCD_SESSION = "CMCD-Session"; public static final String KEY_CMCD_STATUS = "CMCD-Status"; + public static final String CMCD_QUERY_PARAMETER_KEY = "CMCD"; public static final String KEY_BITRATE = "br"; public static final String KEY_BUFFER_LENGTH = "bl"; public static final String KEY_CONTENT_ID = "cid"; @@ -104,6 +113,8 @@ public final class CmcdConfiguration { public static final String KEY_STARTUP = "su"; public static final String KEY_NEXT_OBJECT_REQUEST = "nor"; public static final String KEY_NEXT_RANGE_REQUEST = "nrr"; + public static final int MODE_REQUEST_HEADER = 0; + public static final int MODE_QUERY_PARAMETER = 1; /** * Factory for {@link CmcdConfiguration} instances. @@ -230,15 +241,28 @@ public final class CmcdConfiguration { /** Dynamic request specific configuration. */ public final RequestConfig requestConfig; - /** Creates an instance. */ + /** Mode used for data transmission. */ + public final @DataTransmissionMode int dataTransmissionMode; + + /** Creates an instance with {@link #dataTransmissionMode} set to {@link #MODE_REQUEST_HEADER}. */ public CmcdConfiguration( @Nullable String sessionId, @Nullable String contentId, RequestConfig requestConfig) { + this(sessionId, contentId, requestConfig, MODE_REQUEST_HEADER); + } + + /** Creates an instance. */ + public CmcdConfiguration( + @Nullable String sessionId, + @Nullable String contentId, + RequestConfig requestConfig, + @DataTransmissionMode int dataTransmissionMode) { checkArgument(sessionId == null || sessionId.length() <= MAX_ID_LENGTH); checkArgument(contentId == null || contentId.length() <= MAX_ID_LENGTH); checkNotNull(requestConfig); this.sessionId = sessionId; this.contentId = contentId; this.requestConfig = requestConfig; + this.dataTransmissionMode = dataTransmissionMode; } /** diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java similarity index 59% rename from libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java rename to libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java index f76807725e..35f3a1b53a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdData.java @@ -30,8 +30,10 @@ import androidx.media3.common.MimeTypes; import androidx.media3.common.TrackGroup; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import androidx.media3.datasource.DataSpec; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import com.google.common.base.Joiner; +import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; @@ -41,62 +43,308 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; /** - * This class serves as a factory for generating Common Media Client Data (CMCD) HTTP request - * headers in adaptive streaming formats, DASH, HLS, and SmoothStreaming. + * This class provides functionality for generating and adding Common Media Client Data (CMCD) data + * to adaptive streaming formats, DASH, HLS, and SmoothStreaming. * *

It encapsulates the necessary attributes and information relevant to media content playback, * following the guidelines specified in the CMCD standard document CTA-5004. */ @UnstableApi -public final class CmcdHeadersFactory { +public final class CmcdData { - private static final Joiner COMMA_JOINER = Joiner.on(","); - private static final Pattern CUSTOM_KEY_NAME_PATTERN = - Pattern.compile("[a-zA-Z0-9]+(-[a-zA-Z0-9]+)+"); + /** {@link CmcdData.Factory} for {@link CmcdData} instances. */ + public static final class Factory { - /** - * Retrieves the object type value from the given {@link ExoTrackSelection}. - * - * @param trackSelection The {@link ExoTrackSelection} from which to retrieve the object type. - * @return The object type value as a String if {@link TrackType} can be mapped to one of the - * object types specified by {@link ObjectType} annotation, or {@code null}. - * @throws IllegalArgumentException if the provided {@link ExoTrackSelection} is {@code null}. - */ - @Nullable - public static @ObjectType String getObjectType(ExoTrackSelection trackSelection) { - checkArgument(trackSelection != null); - @C.TrackType - int trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType); - if (trackType == C.TRACK_TYPE_UNKNOWN) { - trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().containerMimeType); + /** Represents the Dynamic Adaptive Streaming over HTTP (DASH) format. */ + public static final String STREAMING_FORMAT_DASH = "d"; + + /** Represents the HTTP Live Streaming (HLS) format. */ + public static final String STREAMING_FORMAT_HLS = "h"; + + /** Represents the Smooth Streaming (SS) format. */ + public static final String STREAMING_FORMAT_SS = "s"; + + /** Represents the Video on Demand (VOD) stream type. */ + public static final String STREAM_TYPE_VOD = "v"; + + /** Represents the Live Streaming stream type. */ + public static final String STREAM_TYPE_LIVE = "l"; + + /** Represents the object type for an initialization segment in a media container. */ + public static final String OBJECT_TYPE_INIT_SEGMENT = "i"; + + /** Represents the object type for audio-only content in a media container. */ + public static final String OBJECT_TYPE_AUDIO_ONLY = "a"; + + /** Represents the object type for video-only content in a media container. */ + public static final String OBJECT_TYPE_VIDEO_ONLY = "v"; + + /** Represents the object type for muxed audio and video content in a media container. */ + public static final String OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO = "av"; + + private static final Pattern CUSTOM_KEY_NAME_PATTERN = + Pattern.compile("[a-zA-Z0-9]+(-[a-zA-Z0-9]+)+"); + + private final CmcdConfiguration cmcdConfiguration; + private final ExoTrackSelection trackSelection; + private final long bufferedDurationUs; + private final float playbackRate; + private final @CmcdData.StreamingFormat String streamingFormat; + private final boolean isLive; + private final boolean didRebuffer; + private final boolean isBufferEmpty; + private long chunkDurationUs; + @Nullable private @CmcdData.ObjectType String objectType; + @Nullable private String nextObjectRequest; + @Nullable private String nextRangeRequest; + + /** + * Creates an instance. + * + * @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source. + * @param trackSelection The {@linkplain ExoTrackSelection track selection}. + * @param bufferedDurationUs The duration of media currently buffered from the current playback + * position, in microseconds. + * @param playbackRate The playback rate indicating the current speed of playback. + * @param streamingFormat The streaming format of the media content. Must be one of the allowed + * streaming formats specified by the {@link CmcdData.StreamingFormat} annotation. + * @param isLive {@code true} if the media content is being streamed live, {@code false} + * otherwise. + * @param didRebuffer {@code true} if a rebuffering event happened between the previous request + * and this one, {@code false} otherwise. + * @param isBufferEmpty {@code true} if the queue of buffered chunks is empty, {@code false} + * otherwise. + * @throws IllegalArgumentException If {@code bufferedDurationUs} is negative. + */ + public Factory( + CmcdConfiguration cmcdConfiguration, + ExoTrackSelection trackSelection, + long bufferedDurationUs, + float playbackRate, + @CmcdData.StreamingFormat String streamingFormat, + boolean isLive, + boolean didRebuffer, + boolean isBufferEmpty) { + checkArgument(bufferedDurationUs >= 0); + checkArgument(playbackRate > 0); + this.cmcdConfiguration = cmcdConfiguration; + this.trackSelection = trackSelection; + this.bufferedDurationUs = bufferedDurationUs; + this.playbackRate = playbackRate; + this.streamingFormat = streamingFormat; + this.isLive = isLive; + this.didRebuffer = didRebuffer; + this.isBufferEmpty = isBufferEmpty; + this.chunkDurationUs = C.TIME_UNSET; } - if (trackType == C.TRACK_TYPE_AUDIO) { - return OBJECT_TYPE_AUDIO_ONLY; - } else if (trackType == C.TRACK_TYPE_VIDEO) { - return OBJECT_TYPE_VIDEO_ONLY; - } else { - // Track type cannot be mapped to a known object type. - return null; + /** + * Retrieves the object type value from the given {@link ExoTrackSelection}. + * + * @param trackSelection The {@link ExoTrackSelection} from which to retrieve the object type. + * @return The object type value as a String if {@link TrackType} can be mapped to one of the + * object types specified by {@link CmcdData.ObjectType} annotation, or {@code null}. + * @throws IllegalArgumentException if the provided {@link ExoTrackSelection} is {@code null}. + */ + @Nullable + public static @CmcdData.ObjectType String getObjectType(ExoTrackSelection trackSelection) { + checkArgument(trackSelection != null); + @TrackType + int trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType); + if (trackType == C.TRACK_TYPE_UNKNOWN) { + trackType = MimeTypes.getTrackType(trackSelection.getSelectedFormat().containerMimeType); + } + + if (trackType == C.TRACK_TYPE_AUDIO) { + return OBJECT_TYPE_AUDIO_ONLY; + } else if (trackType == C.TRACK_TYPE_VIDEO) { + return OBJECT_TYPE_VIDEO_ONLY; + } else { + // Track type cannot be mapped to a known object type. + return null; + } + } + + /** + * Sets the duration of current media chunk being requested, in microseconds. The default value + * is {@link C#TIME_UNSET}. + * + * @throws IllegalArgumentException If {@code chunkDurationUs} is negative. + */ + @CanIgnoreReturnValue + public Factory setChunkDurationUs(long chunkDurationUs) { + checkArgument(chunkDurationUs >= 0); + this.chunkDurationUs = chunkDurationUs; + return this; + } + + /** + * Sets the object type of the current object being requested. Must be one of the allowed object + * types specified by the {@link CmcdData.ObjectType} annotation. + * + *

Default is {@code null}. + */ + @CanIgnoreReturnValue + public Factory setObjectType(@Nullable @CmcdData.ObjectType String objectType) { + this.objectType = objectType; + return this; + } + + /** + * Sets the relative path of the next object to be requested. This can be used to trigger + * pre-fetching by the CDN. + * + *

Default is {@code null}. + */ + @CanIgnoreReturnValue + public Factory setNextObjectRequest(@Nullable String nextObjectRequest) { + this.nextObjectRequest = nextObjectRequest; + return this; + } + + /** + * Sets the byte range representing the partial object request. This can be used to trigger + * pre-fetching by the CDN. + * + *

Default is {@code null}. + */ + @CanIgnoreReturnValue + public Factory setNextRangeRequest(@Nullable String nextRangeRequest) { + this.nextRangeRequest = nextRangeRequest; + return this; + } + + public CmcdData createCmcdData() { + ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> customData = + cmcdConfiguration.requestConfig.getCustomData(); + for (String headerKey : customData.keySet()) { + validateCustomDataListFormat(customData.get(headerKey)); + } + + int bitrateKbps = Util.ceilDivide(trackSelection.getSelectedFormat().bitrate, 1000); + + CmcdObject.Builder cmcdObject = new CmcdObject.Builder(); + if (!getIsInitSegment()) { + if (cmcdConfiguration.isBitrateLoggingAllowed()) { + cmcdObject.setBitrateKbps(bitrateKbps); + } + if (cmcdConfiguration.isTopBitrateLoggingAllowed()) { + TrackGroup trackGroup = trackSelection.getTrackGroup(); + int topBitrate = trackSelection.getSelectedFormat().bitrate; + for (int i = 0; i < trackGroup.length; i++) { + topBitrate = max(topBitrate, trackGroup.getFormat(i).bitrate); + } + cmcdObject.setTopBitrateKbps(Util.ceilDivide(topBitrate, 1000)); + } + if (cmcdConfiguration.isObjectDurationLoggingAllowed()) { + cmcdObject.setObjectDurationMs(Util.usToMs(chunkDurationUs)); + } + } + if (cmcdConfiguration.isObjectTypeLoggingAllowed()) { + cmcdObject.setObjectType(objectType); + } + if (customData.containsKey(CmcdConfiguration.KEY_CMCD_OBJECT)) { + cmcdObject.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_OBJECT)); + } + + CmcdRequest.Builder cmcdRequest = new CmcdRequest.Builder(); + if (!getIsInitSegment() && cmcdConfiguration.isBufferLengthLoggingAllowed()) { + cmcdRequest.setBufferLengthMs(Util.usToMs(bufferedDurationUs)); + } + if (cmcdConfiguration.isMeasuredThroughputLoggingAllowed() + && trackSelection.getLatestBitrateEstimate() != C.RATE_UNSET_INT) { + cmcdRequest.setMeasuredThroughputInKbps( + Util.ceilDivide(trackSelection.getLatestBitrateEstimate(), 1000)); + } + if (cmcdConfiguration.isDeadlineLoggingAllowed()) { + cmcdRequest.setDeadlineMs(Util.usToMs((long) (bufferedDurationUs / playbackRate))); + } + if (cmcdConfiguration.isStartupLoggingAllowed()) { + cmcdRequest.setStartup(didRebuffer || isBufferEmpty); + } + if (cmcdConfiguration.isNextObjectRequestLoggingAllowed()) { + cmcdRequest.setNextObjectRequest(nextObjectRequest); + } + if (cmcdConfiguration.isNextRangeRequestLoggingAllowed()) { + cmcdRequest.setNextRangeRequest(nextRangeRequest); + } + if (customData.containsKey(CmcdConfiguration.KEY_CMCD_REQUEST)) { + cmcdRequest.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_REQUEST)); + } + + CmcdSession.Builder cmcdSession = new CmcdSession.Builder(); + if (cmcdConfiguration.isContentIdLoggingAllowed()) { + cmcdSession.setContentId(cmcdConfiguration.contentId); + } + if (cmcdConfiguration.isSessionIdLoggingAllowed()) { + cmcdSession.setSessionId(cmcdConfiguration.sessionId); + } + if (cmcdConfiguration.isStreamingFormatLoggingAllowed()) { + cmcdSession.setStreamingFormat(streamingFormat); + } + if (cmcdConfiguration.isStreamTypeLoggingAllowed()) { + cmcdSession.setStreamType(isLive ? STREAM_TYPE_LIVE : STREAM_TYPE_VOD); + } + if (cmcdConfiguration.isPlaybackRateLoggingAllowed()) { + cmcdSession.setPlaybackRate(playbackRate); + } + if (customData.containsKey(CmcdConfiguration.KEY_CMCD_SESSION)) { + cmcdSession.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_SESSION)); + } + + CmcdStatus.Builder cmcdStatus = new CmcdStatus.Builder(); + if (cmcdConfiguration.isMaximumRequestThroughputLoggingAllowed()) { + cmcdStatus.setMaximumRequestedThroughputKbps( + cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps)); + } + if (cmcdConfiguration.isBufferStarvationLoggingAllowed()) { + cmcdStatus.setBufferStarvation(didRebuffer); + } + if (customData.containsKey(CmcdConfiguration.KEY_CMCD_STATUS)) { + cmcdStatus.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_STATUS)); + } + + return new CmcdData( + cmcdObject.build(), + cmcdRequest.build(), + cmcdSession.build(), + cmcdStatus.build(), + cmcdConfiguration.dataTransmissionMode); + } + + private boolean getIsInitSegment() { + return objectType != null && objectType.equals(OBJECT_TYPE_INIT_SEGMENT); + } + + private void validateCustomDataListFormat(List customDataList) { + for (String customData : customDataList) { + String key = Util.split(customData, "=")[0]; + checkState(CUSTOM_KEY_NAME_PATTERN.matcher(key).matches()); + } } } /** Indicates the streaming format used for media content. */ @Retention(RetentionPolicy.SOURCE) - @StringDef({STREAMING_FORMAT_DASH, STREAMING_FORMAT_HLS, STREAMING_FORMAT_SS}) + @StringDef({ + Factory.STREAMING_FORMAT_DASH, + Factory.STREAMING_FORMAT_HLS, + Factory.STREAMING_FORMAT_SS + }) @Documented @Target(TYPE_USE) public @interface StreamingFormat {} /** Indicates the type of streaming for media content. */ @Retention(RetentionPolicy.SOURCE) - @StringDef({STREAM_TYPE_VOD, STREAM_TYPE_LIVE}) + @StringDef({Factory.STREAM_TYPE_VOD, Factory.STREAM_TYPE_LIVE}) @Documented @Target(TYPE_USE) public @interface StreamType {} @@ -104,251 +352,69 @@ public final class CmcdHeadersFactory { /** Indicates the media type of current object being requested. */ @Retention(RetentionPolicy.SOURCE) @StringDef({ - OBJECT_TYPE_INIT_SEGMENT, - OBJECT_TYPE_AUDIO_ONLY, - OBJECT_TYPE_VIDEO_ONLY, - OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO + Factory.OBJECT_TYPE_INIT_SEGMENT, + Factory.OBJECT_TYPE_AUDIO_ONLY, + Factory.OBJECT_TYPE_VIDEO_ONLY, + Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO }) @Documented @Target(TYPE_USE) public @interface ObjectType {} - /** Represents the Dynamic Adaptive Streaming over HTTP (DASH) format. */ - public static final String STREAMING_FORMAT_DASH = "d"; + private static final Joiner COMMA_JOINER = Joiner.on(","); - /** Represents the HTTP Live Streaming (HLS) format. */ - public static final String STREAMING_FORMAT_HLS = "h"; + private final CmcdObject cmcdObject; + private final CmcdRequest cmcdRequest; + private final CmcdSession cmcdSession; + private final CmcdStatus cmcdStatus; + private final @CmcdConfiguration.DataTransmissionMode int dataTransmissionMode; - /** Represents the Smooth Streaming (SS) format. */ - public static final String STREAMING_FORMAT_SS = "s"; - - /** Represents the Video on Demand (VOD) stream type. */ - public static final String STREAM_TYPE_VOD = "v"; - - /** Represents the Live Streaming stream type. */ - public static final String STREAM_TYPE_LIVE = "l"; - - /** Represents the object type for an initialization segment in a media container. */ - public static final String OBJECT_TYPE_INIT_SEGMENT = "i"; - - /** Represents the object type for audio-only content in a media container. */ - public static final String OBJECT_TYPE_AUDIO_ONLY = "a"; - - /** Represents the object type for video-only content in a media container. */ - public static final String OBJECT_TYPE_VIDEO_ONLY = "v"; - - /** Represents the object type for muxed audio and video content in a media container. */ - public static final String OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO = "av"; - - private final CmcdConfiguration cmcdConfiguration; - private final ExoTrackSelection trackSelection; - private final long bufferedDurationUs; - private final float playbackRate; - private final @StreamingFormat String streamingFormat; - private final boolean isLive; - private final boolean didRebuffer; - private final boolean isBufferEmpty; - private long chunkDurationUs; - private @Nullable @ObjectType String objectType; - @Nullable private String nextObjectRequest; - @Nullable private String nextRangeRequest; - - /** - * Creates an instance. - * - * @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source. - * @param trackSelection The {@linkplain ExoTrackSelection track selection}. - * @param bufferedDurationUs The duration of media currently buffered from the current playback - * position, in microseconds. - * @param playbackRate The playback rate indicating the current speed of playback. - * @param streamingFormat The streaming format of the media content. Must be one of the allowed - * streaming formats specified by the {@link StreamingFormat} annotation. - * @param isLive {@code true} if the media content is being streamed live, {@code false} - * otherwise. - * @param didRebuffer {@code true} if a rebuffering event happened between the previous request - * and this one, {@code false} otherwise. - * @param isBufferEmpty {@code true} if the queue of buffered chunks is empty, {@code false} - * otherwise. - * @throws IllegalArgumentException If {@code bufferedDurationUs} is negative. - */ - public CmcdHeadersFactory( - CmcdConfiguration cmcdConfiguration, - ExoTrackSelection trackSelection, - long bufferedDurationUs, - float playbackRate, - @StreamingFormat String streamingFormat, - boolean isLive, - boolean didRebuffer, - boolean isBufferEmpty) { - checkArgument(bufferedDurationUs >= 0); - checkArgument(playbackRate > 0); - this.cmcdConfiguration = cmcdConfiguration; - this.trackSelection = trackSelection; - this.bufferedDurationUs = bufferedDurationUs; - this.playbackRate = playbackRate; - this.streamingFormat = streamingFormat; - this.isLive = isLive; - this.didRebuffer = didRebuffer; - this.isBufferEmpty = isBufferEmpty; - this.chunkDurationUs = C.TIME_UNSET; + private CmcdData( + CmcdObject cmcdObject, + CmcdRequest cmcdRequest, + CmcdSession cmcdSession, + CmcdStatus cmcdStatus, + @CmcdConfiguration.DataTransmissionMode int datatTransmissionMode) { + this.cmcdObject = cmcdObject; + this.cmcdRequest = cmcdRequest; + this.cmcdSession = cmcdSession; + this.cmcdStatus = cmcdStatus; + this.dataTransmissionMode = datatTransmissionMode; } /** - * Sets the duration of current media chunk being requested, in microseconds. The default value is - * {@link C#TIME_UNSET}. - * - * @throws IllegalArgumentException If {@code chunkDurationUs} is negative. + * Adds Common Media Client Data (CMCD) related information to the provided {@link DataSpec} + * object. */ - @CanIgnoreReturnValue - public CmcdHeadersFactory setChunkDurationUs(long chunkDurationUs) { - checkArgument(chunkDurationUs >= 0); - this.chunkDurationUs = chunkDurationUs; - return this; - } + public DataSpec addToDataSpec(DataSpec dataSpec) { + ArrayListMultimap cmcdDataMap = ArrayListMultimap.create(); + cmcdObject.populateCmcdDataMap(cmcdDataMap); + cmcdRequest.populateCmcdDataMap(cmcdDataMap); + cmcdSession.populateCmcdDataMap(cmcdDataMap); + cmcdStatus.populateCmcdDataMap(cmcdDataMap); - /** - * Sets the object type of the current object being requested. Must be one of the allowed object - * types specified by the {@link ObjectType} annotation. - * - *

Default is {@code null}. - */ - @CanIgnoreReturnValue - public CmcdHeadersFactory setObjectType(@Nullable @ObjectType String objectType) { - this.objectType = objectType; - return this; - } - - /** - * Sets the relative path of the next object to be requested. This can be used to trigger - * pre-fetching by the CDN. - * - *

Default is {@code null}. - */ - @CanIgnoreReturnValue - public CmcdHeadersFactory setNextObjectRequest(@Nullable String nextObjectRequest) { - this.nextObjectRequest = nextObjectRequest; - return this; - } - - /** - * Sets the byte range representing the partial object request. This can be used to trigger - * pre-fetching by the CDN. - * - *

Default is {@code null}. - */ - @CanIgnoreReturnValue - public CmcdHeadersFactory setNextRangeRequest(@Nullable String nextRangeRequest) { - this.nextRangeRequest = nextRangeRequest; - return this; - } - - /** Creates and returns a new {@link ImmutableMap} containing the CMCD HTTP request headers. */ - public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> createHttpRequestHeaders() { - ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> customData = - cmcdConfiguration.requestConfig.getCustomData(); - for (String headerKey : customData.keySet()) { - validateCustomDataListFormat(customData.get(headerKey)); - } - - int bitrateKbps = Util.ceilDivide(trackSelection.getSelectedFormat().bitrate, 1000); - - CmcdObject.Builder cmcdObject = new CmcdObject.Builder(); - if (!getIsInitSegment()) { - if (cmcdConfiguration.isBitrateLoggingAllowed()) { - cmcdObject.setBitrateKbps(bitrateKbps); + if (dataTransmissionMode == CmcdConfiguration.MODE_REQUEST_HEADER) { + ImmutableMap.Builder httpRequestHeaders = ImmutableMap.builder(); + for (String headerKey : cmcdDataMap.keySet()) { + List headerValues = cmcdDataMap.get(headerKey); + Collections.sort(headerValues); + httpRequestHeaders.put(headerKey, COMMA_JOINER.join(headerValues)); } - if (cmcdConfiguration.isTopBitrateLoggingAllowed()) { - TrackGroup trackGroup = trackSelection.getTrackGroup(); - int topBitrate = trackSelection.getSelectedFormat().bitrate; - for (int i = 0; i < trackGroup.length; i++) { - topBitrate = max(topBitrate, trackGroup.getFormat(i).bitrate); - } - cmcdObject.setTopBitrateKbps(Util.ceilDivide(topBitrate, 1000)); + return dataSpec.withAdditionalHeaders(httpRequestHeaders.buildOrThrow()); + } else { + List keyValuePairs = new ArrayList<>(); + for (Collection values : cmcdDataMap.asMap().values()) { + keyValuePairs.addAll(values); } - if (cmcdConfiguration.isObjectDurationLoggingAllowed()) { - cmcdObject.setObjectDurationMs(Util.usToMs(chunkDurationUs)); - } - } - if (cmcdConfiguration.isObjectTypeLoggingAllowed()) { - cmcdObject.setObjectType(objectType); - } - if (customData.containsKey(CmcdConfiguration.KEY_CMCD_OBJECT)) { - cmcdObject.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_OBJECT)); - } - - CmcdRequest.Builder cmcdRequest = new CmcdRequest.Builder(); - if (!getIsInitSegment() && cmcdConfiguration.isBufferLengthLoggingAllowed()) { - cmcdRequest.setBufferLengthMs(Util.usToMs(bufferedDurationUs)); - } - if (cmcdConfiguration.isMeasuredThroughputLoggingAllowed() - && trackSelection.getLatestBitrateEstimate() != C.RATE_UNSET_INT) { - cmcdRequest.setMeasuredThroughputInKbps( - Util.ceilDivide(trackSelection.getLatestBitrateEstimate(), 1000)); - } - if (cmcdConfiguration.isDeadlineLoggingAllowed()) { - cmcdRequest.setDeadlineMs(Util.usToMs((long) (bufferedDurationUs / playbackRate))); - } - if (cmcdConfiguration.isStartupLoggingAllowed()) { - cmcdRequest.setStartup(didRebuffer || isBufferEmpty); - } - if (cmcdConfiguration.isNextObjectRequestLoggingAllowed()) { - cmcdRequest.setNextObjectRequest(nextObjectRequest); - } - if (cmcdConfiguration.isNextRangeRequestLoggingAllowed()) { - cmcdRequest.setNextRangeRequest(nextRangeRequest); - } - if (customData.containsKey(CmcdConfiguration.KEY_CMCD_REQUEST)) { - cmcdRequest.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_REQUEST)); - } - - CmcdSession.Builder cmcdSession = new CmcdSession.Builder(); - if (cmcdConfiguration.isContentIdLoggingAllowed()) { - cmcdSession.setContentId(cmcdConfiguration.contentId); - } - if (cmcdConfiguration.isSessionIdLoggingAllowed()) { - cmcdSession.setSessionId(cmcdConfiguration.sessionId); - } - if (cmcdConfiguration.isStreamingFormatLoggingAllowed()) { - cmcdSession.setStreamingFormat(streamingFormat); - } - if (cmcdConfiguration.isStreamTypeLoggingAllowed()) { - cmcdSession.setStreamType(isLive ? STREAM_TYPE_LIVE : STREAM_TYPE_VOD); - } - if (cmcdConfiguration.isPlaybackRateLoggingAllowed()) { - cmcdSession.setPlaybackRate(playbackRate); - } - if (customData.containsKey(CmcdConfiguration.KEY_CMCD_SESSION)) { - cmcdSession.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_SESSION)); - } - - CmcdStatus.Builder cmcdStatus = new CmcdStatus.Builder(); - if (cmcdConfiguration.isMaximumRequestThroughputLoggingAllowed()) { - cmcdStatus.setMaximumRequestedThroughputKbps( - cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps)); - } - if (cmcdConfiguration.isBufferStarvationLoggingAllowed()) { - cmcdStatus.setBufferStarvation(didRebuffer); - } - if (customData.containsKey(CmcdConfiguration.KEY_CMCD_STATUS)) { - cmcdStatus.setCustomDataList(customData.get(CmcdConfiguration.KEY_CMCD_STATUS)); - } - - ImmutableMap.Builder httpRequestHeaders = ImmutableMap.builder(); - cmcdObject.build().populateHttpRequestHeaders(httpRequestHeaders); - cmcdRequest.build().populateHttpRequestHeaders(httpRequestHeaders); - cmcdSession.build().populateHttpRequestHeaders(httpRequestHeaders); - cmcdStatus.build().populateHttpRequestHeaders(httpRequestHeaders); - return httpRequestHeaders.buildOrThrow(); - } - - private boolean getIsInitSegment() { - return objectType != null && objectType.equals(OBJECT_TYPE_INIT_SEGMENT); - } - - private void validateCustomDataListFormat(List customDataList) { - for (String customData : customDataList) { - String key = Util.split(customData, "=")[0]; - checkState(CUSTOM_KEY_NAME_PATTERN.matcher(key).matches()); + Collections.sort(keyValuePairs); + Uri.Builder uriBuilder = + dataSpec + .uri + .buildUpon() + .appendQueryParameter( + CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY, + Uri.encode(COMMA_JOINER.join(keyValuePairs))); + return dataSpec.buildUpon().setUri(uriBuilder.build()).build(); } } @@ -477,32 +543,29 @@ public final class CmcdHeadersFactory { } /** - * Populates the HTTP request headers with {@link CmcdConfiguration#KEY_CMCD_OBJECT} values. + * Populates the {@code cmcdDataMap} with {@link CmcdConfiguration#KEY_CMCD_OBJECT} values. * - * @param httpRequestHeaders An {@link ImmutableMap.Builder} used to build the HTTP request - * headers. + * @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added. */ - public void populateHttpRequestHeaders( - ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) { - ArrayList headerValueList = new ArrayList<>(); + public void populateCmcdDataMap( + ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) { + ArrayList keyValuePairs = new ArrayList<>(); if (bitrateKbps != C.RATE_UNSET_INT) { - headerValueList.add(CmcdConfiguration.KEY_BITRATE + "=" + bitrateKbps); + keyValuePairs.add(CmcdConfiguration.KEY_BITRATE + "=" + bitrateKbps); } if (topBitrateKbps != C.RATE_UNSET_INT) { - headerValueList.add(CmcdConfiguration.KEY_TOP_BITRATE + "=" + topBitrateKbps); + keyValuePairs.add(CmcdConfiguration.KEY_TOP_BITRATE + "=" + topBitrateKbps); } if (objectDurationMs != C.TIME_UNSET) { - headerValueList.add(CmcdConfiguration.KEY_OBJECT_DURATION + "=" + objectDurationMs); + keyValuePairs.add(CmcdConfiguration.KEY_OBJECT_DURATION + "=" + objectDurationMs); } if (!TextUtils.isEmpty(objectType)) { - headerValueList.add(CmcdConfiguration.KEY_OBJECT_TYPE + "=" + objectType); + keyValuePairs.add(CmcdConfiguration.KEY_OBJECT_TYPE + "=" + objectType); } - headerValueList.addAll(customDataList); + keyValuePairs.addAll(customDataList); - if (!headerValueList.isEmpty()) { - Collections.sort(headerValueList); - httpRequestHeaders.put( - CmcdConfiguration.KEY_CMCD_OBJECT, COMMA_JOINER.join(headerValueList)); + if (!keyValuePairs.isEmpty()) { + cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_OBJECT, keyValuePairs); } } } @@ -616,8 +679,8 @@ public final class CmcdHeadersFactory { * C#TIME_UNSET} if unset. * *

This key SHOULD only be sent with an {@link CmcdObject#objectType} of {@link - * #OBJECT_TYPE_AUDIO_ONLY}, {@link #OBJECT_TYPE_VIDEO_ONLY} or {@link - * #OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO}. + * Factory#OBJECT_TYPE_AUDIO_ONLY}, {@link Factory#OBJECT_TYPE_VIDEO_ONLY} or {@link + * Factory#OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO}. * *

This value MUST be rounded to the nearest 100 ms. */ @@ -688,43 +751,40 @@ public final class CmcdHeadersFactory { } /** - * Populates the HTTP request headers with {@link CmcdConfiguration#KEY_CMCD_REQUEST} values. + * Populates the {@code cmcdDataMap} with {@link CmcdConfiguration#KEY_CMCD_REQUEST} values. * - * @param httpRequestHeaders An {@link ImmutableMap.Builder} used to build the HTTP request - * headers. + * @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added. */ - public void populateHttpRequestHeaders( - ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) { - ArrayList headerValueList = new ArrayList<>(); + public void populateCmcdDataMap( + ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) { + ArrayList keyValuePairs = new ArrayList<>(); if (bufferLengthMs != C.TIME_UNSET) { - headerValueList.add(CmcdConfiguration.KEY_BUFFER_LENGTH + "=" + bufferLengthMs); + keyValuePairs.add(CmcdConfiguration.KEY_BUFFER_LENGTH + "=" + bufferLengthMs); } if (measuredThroughputInKbps != C.RATE_UNSET_INT) { - headerValueList.add( + keyValuePairs.add( CmcdConfiguration.KEY_MEASURED_THROUGHPUT + "=" + measuredThroughputInKbps); } if (deadlineMs != C.TIME_UNSET) { - headerValueList.add(CmcdConfiguration.KEY_DEADLINE + "=" + deadlineMs); + keyValuePairs.add(CmcdConfiguration.KEY_DEADLINE + "=" + deadlineMs); } if (startup) { - headerValueList.add(CmcdConfiguration.KEY_STARTUP); + keyValuePairs.add(CmcdConfiguration.KEY_STARTUP); } if (!TextUtils.isEmpty(nextObjectRequest)) { - headerValueList.add( + keyValuePairs.add( Util.formatInvariant( "%s=\"%s\"", CmcdConfiguration.KEY_NEXT_OBJECT_REQUEST, nextObjectRequest)); } if (!TextUtils.isEmpty(nextRangeRequest)) { - headerValueList.add( + keyValuePairs.add( Util.formatInvariant( "%s=\"%s\"", CmcdConfiguration.KEY_NEXT_RANGE_REQUEST, nextRangeRequest)); } - headerValueList.addAll(customDataList); + keyValuePairs.addAll(customDataList); - if (!headerValueList.isEmpty()) { - Collections.sort(headerValueList); - httpRequestHeaders.put( - CmcdConfiguration.KEY_CMCD_REQUEST, COMMA_JOINER.join(headerValueList)); + if (!keyValuePairs.isEmpty()) { + cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_REQUEST, keyValuePairs); } } } @@ -870,41 +930,38 @@ public final class CmcdHeadersFactory { } /** - * Populates the HTTP request headers with {@link CmcdConfiguration#KEY_CMCD_SESSION} values. + * Populates the {@code cmcdDataMap} with {@link CmcdConfiguration#KEY_CMCD_SESSION} values. * - * @param httpRequestHeaders An {@link ImmutableMap.Builder} used to build the HTTP request - * headers. + * @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added. */ - public void populateHttpRequestHeaders( - ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) { - ArrayList headerValueList = new ArrayList<>(); + public void populateCmcdDataMap( + ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) { + ArrayList keyValuePairs = new ArrayList<>(); if (!TextUtils.isEmpty(this.contentId)) { - headerValueList.add( + keyValuePairs.add( Util.formatInvariant("%s=\"%s\"", CmcdConfiguration.KEY_CONTENT_ID, contentId)); } if (!TextUtils.isEmpty(this.sessionId)) { - headerValueList.add( + keyValuePairs.add( Util.formatInvariant("%s=\"%s\"", CmcdConfiguration.KEY_SESSION_ID, sessionId)); } if (!TextUtils.isEmpty(this.streamingFormat)) { - headerValueList.add(CmcdConfiguration.KEY_STREAMING_FORMAT + "=" + streamingFormat); + keyValuePairs.add(CmcdConfiguration.KEY_STREAMING_FORMAT + "=" + streamingFormat); } if (!TextUtils.isEmpty(this.streamType)) { - headerValueList.add(CmcdConfiguration.KEY_STREAM_TYPE + "=" + streamType); + keyValuePairs.add(CmcdConfiguration.KEY_STREAM_TYPE + "=" + streamType); } if (playbackRate != C.RATE_UNSET && playbackRate != 1.0f) { - headerValueList.add( + keyValuePairs.add( Util.formatInvariant("%s=%.2f", CmcdConfiguration.KEY_PLAYBACK_RATE, playbackRate)); } if (VERSION != 1) { - headerValueList.add(CmcdConfiguration.KEY_VERSION + "=" + VERSION); + keyValuePairs.add(CmcdConfiguration.KEY_VERSION + "=" + VERSION); } - headerValueList.addAll(customDataList); + keyValuePairs.addAll(customDataList); - if (!headerValueList.isEmpty()) { - Collections.sort(headerValueList); - httpRequestHeaders.put( - CmcdConfiguration.KEY_CMCD_SESSION, COMMA_JOINER.join(headerValueList)); + if (!keyValuePairs.isEmpty()) { + cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_SESSION, keyValuePairs); } } } @@ -991,27 +1048,24 @@ public final class CmcdHeadersFactory { } /** - * Populates the HTTP request headers with {@link CmcdConfiguration#KEY_CMCD_STATUS} values. + * Populates the {@code cmcdDataMap} with {@link CmcdConfiguration#KEY_CMCD_STATUS} values. * - * @param httpRequestHeaders An {@link ImmutableMap.Builder} used to build the HTTP request - * headers. + * @param cmcdDataMap An {@link ArrayListMultimap} to which CMCD data will be added. */ - public void populateHttpRequestHeaders( - ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) { - ArrayList headerValueList = new ArrayList<>(); + public void populateCmcdDataMap( + ArrayListMultimap<@CmcdConfiguration.HeaderKey String, String> cmcdDataMap) { + ArrayList keyValuePairs = new ArrayList<>(); if (maximumRequestedThroughputKbps != C.RATE_UNSET_INT) { - headerValueList.add( + keyValuePairs.add( CmcdConfiguration.KEY_MAXIMUM_REQUESTED_BITRATE + "=" + maximumRequestedThroughputKbps); } if (bufferStarvation) { - headerValueList.add(CmcdConfiguration.KEY_BUFFER_STARVATION); + keyValuePairs.add(CmcdConfiguration.KEY_BUFFER_STARVATION); } - headerValueList.addAll(customDataList); + keyValuePairs.addAll(customDataList); - if (!headerValueList.isEmpty()) { - Collections.sort(headerValueList); - httpRequestHeaders.put( - CmcdConfiguration.KEY_CMCD_STATUS, COMMA_JOINER.join(headerValueList)); + if (!keyValuePairs.isEmpty()) { + cmcdDataMap.putAll(CmcdConfiguration.KEY_CMCD_STATUS, keyValuePairs); } } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java similarity index 60% rename from libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java rename to libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java index ddd7503f4f..efd07f8ca3 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdDataTest.java @@ -20,22 +20,23 @@ import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.net.Uri; import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.TrackGroup; +import androidx.media3.datasource.DataSpec; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.ImmutableMap; import org.junit.Test; import org.junit.runner.RunWith; -/** Tests for {@link CmcdHeadersFactory}. */ +/** Tests for {@link CmcdData}. */ @RunWith(AndroidJUnit4.class) -public class CmcdHeadersFactoryTest { +public class CmcdDataTest { @Test - public void createInstance_populatesCmcdHeaders() { + public void createInstance_populatesCmcdHttRequestHeaders() { CmcdConfiguration.Factory cmcdConfigurationFactory = mediaItem -> new CmcdConfiguration( @@ -66,21 +67,23 @@ public class CmcdHeadersFactoryTest { when(trackSelection.getTrackGroup()) .thenReturn(new TrackGroup(format, new Format.Builder().setPeakBitrate(1_000_000).build())); when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L); - - ImmutableMap<@CmcdConfiguration.HeaderKey String, String> requestHeaders = - new CmcdHeadersFactory( + DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build(); + CmcdData cmcdData = + new CmcdData.Factory( cmcdConfiguration, trackSelection, /* bufferedDurationUs= */ 1_760_000, /* playbackRate= */ 2.0f, - /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH, + /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH, /* isLive= */ true, /* didRebuffer= */ true, /* isBufferEmpty= */ false) .setChunkDurationUs(3_000_000) - .createHttpRequestHeaders(); + .createCmcdData(); - assertThat(requestHeaders) + dataSpec = cmcdData.addToDataSpec(dataSpec); + + assertThat(dataSpec.httpRequestHeaders) .containsExactly( "CMCD-Object", "br=840,d=3000,key-1=1,key-2-separated-by-multiple-hyphens=2,tb=1000", @@ -92,6 +95,62 @@ public class CmcdHeadersFactoryTest { "bs,key-4=\"stringValue3=stringValue4\",rtp=1700"); } + @Test + public void createInstance_populatesCmcdHttpQueryParameters() { + CmcdConfiguration.Factory cmcdConfigurationFactory = + mediaItem -> + new CmcdConfiguration( + "sessionId", + mediaItem.mediaId, + new CmcdConfiguration.RequestConfig() { + @Override + public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> + getCustomData() { + return new ImmutableListMultimap.Builder() + .put("CMCD-Object", "key-1=1") + .put("CMCD-Request", "key-2=\"stringValue1,stringValue2\"") + .build(); + } + + @Override + public int getRequestedMaximumThroughputKbps(int throughputKbps) { + return 2 * throughputKbps; + } + }, + CmcdConfiguration.MODE_QUERY_PARAMETER); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); + ExoTrackSelection trackSelection = mock(ExoTrackSelection.class); + Format format = new Format.Builder().setPeakBitrate(840_000).build(); + when(trackSelection.getSelectedFormat()).thenReturn(format); + when(trackSelection.getTrackGroup()) + .thenReturn(new TrackGroup(format, new Format.Builder().setPeakBitrate(1_000_000).build())); + when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L); + DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build(); + CmcdData cmcdData = + new CmcdData.Factory( + cmcdConfiguration, + trackSelection, + /* bufferedDurationUs= */ 1_760_000, + /* playbackRate= */ 2.0f, + /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH, + /* isLive= */ true, + /* didRebuffer= */ true, + /* isBufferEmpty= */ false) + .setChunkDurationUs(3_000_000) + .createCmcdData(); + + dataSpec = cmcdData.addToDataSpec(dataSpec); + + assertThat( + Uri.decode(dataSpec.uri.getQueryParameter(CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY))) + .isEqualTo( + "bl=1800,br=840,bs,cid=\"mediaId\",d=3000,dl=900,key-1=1," + + "key-2=\"stringValue1,stringValue2\",mtp=500,pr=2.00,rtp=1700,sf=d," + + "sid=\"sessionId\",st=l,su,tb=1000"); + } + @Test public void createInstance_withInvalidNonHyphenatedCustomKey_throwsIllegalStateException() { CmcdConfiguration.Factory cmcdConfigurationFactory = @@ -115,15 +174,15 @@ public class CmcdHeadersFactoryTest { assertThrows( IllegalStateException.class, () -> - new CmcdHeadersFactory( + new CmcdData.Factory( cmcdConfiguration, trackSelection, /* bufferedDurationUs= */ 0, /* playbackRate= */ 1.0f, - /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH, + /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH, /* isLive= */ true, /* didRebuffer= */ true, /* isBufferEmpty= */ false) - .createHttpRequestHeaders()); + .createCmcdData()); } } diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java index 2e53d44aa2..7a0cd41f67 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java @@ -56,7 +56,7 @@ import androidx.media3.exoplayer.source.chunk.MediaChunkIterator; import androidx.media3.exoplayer.source.chunk.SingleSampleMediaChunk; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.CmcdConfiguration; -import androidx.media3.exoplayer.upstream.CmcdHeadersFactory; +import androidx.media3.exoplayer.upstream.CmcdData; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoaderErrorThrower; import androidx.media3.extractor.ChunkIndex; @@ -387,15 +387,15 @@ public class DefaultDashChunkSource implements DashChunkSource { int selectedTrackIndex = trackSelection.getSelectedIndex(); @Nullable - CmcdHeadersFactory cmcdHeadersFactory = + CmcdData.Factory cmcdDataFactory = cmcdConfiguration == null ? null - : new CmcdHeadersFactory( + : new CmcdData.Factory( cmcdConfiguration, trackSelection, bufferedDurationUs, /* playbackRate= */ loadingInfo.playbackSpeed, - /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH, + /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH, /* isLive= */ manifest.dynamic, /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs), /* isBufferEmpty= */ queue.isEmpty()); @@ -423,7 +423,7 @@ public class DefaultDashChunkSource implements DashChunkSource { trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri, - cmcdHeadersFactory); + cmcdDataFactory); return; } } @@ -501,7 +501,7 @@ public class DefaultDashChunkSource implements DashChunkSource { maxSegmentCount, seekTimeUs, nowPeriodTimeUs, - cmcdHeadersFactory); + cmcdDataFactory); } @Override @@ -684,7 +684,7 @@ public class DefaultDashChunkSource implements DashChunkSource { * indexUri} is not {@code null}. * @param indexUri The URI pointing to index data. Can be {@code null} if {@code * initializationUri} is not {@code null}. - * @param cmcdHeadersFactory The {@link CmcdHeadersFactory} for generating CMCD data. + * @param cmcdDataFactory The {@link CmcdData.Factory} for generating CMCD data. */ @RequiresNonNull("#1.chunkExtractor") protected Chunk newInitializationChunk( @@ -695,7 +695,7 @@ public class DefaultDashChunkSource implements DashChunkSource { @Nullable Object trackSelectionData, @Nullable RangedUri initializationUri, @Nullable RangedUri indexUri, - @Nullable CmcdHeadersFactory cmcdHeadersFactory) { + @Nullable CmcdData.Factory cmcdDataFactory) { Representation representation = representationHolder.representation; RangedUri requestUri; if (initializationUri != null) { @@ -709,19 +709,19 @@ public class DefaultDashChunkSource implements DashChunkSource { } else { requestUri = checkNotNull(indexUri); } - ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders = - cmcdHeadersFactory == null - ? ImmutableMap.of() - : cmcdHeadersFactory - .setObjectType(CmcdHeadersFactory.OBJECT_TYPE_INIT_SEGMENT) - .createHttpRequestHeaders(); DataSpec dataSpec = DashUtil.buildDataSpec( representation, representationHolder.selectedBaseUrl.url, requestUri, /* flags= */ 0, - httpRequestHeaders); + /* httpRequestHeaders= */ ImmutableMap.of()); + if (cmcdDataFactory != null) { + CmcdData cmcdData = + cmcdDataFactory.setObjectType(CmcdData.Factory.OBJECT_TYPE_INIT_SEGMENT).createCmcdData(); + dataSpec = cmcdData.addToDataSpec(dataSpec); + } + return new InitializationChunk( dataSource, dataSpec, @@ -742,7 +742,7 @@ public class DefaultDashChunkSource implements DashChunkSource { int maxSegmentCount, long seekTimeUs, long nowPeriodTimeUs, - @Nullable CmcdHeadersFactory cmcdHeadersFactory) { + @Nullable CmcdData.Factory cmcdDataFactory) { Representation representation = representationHolder.representation; long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum); @@ -753,29 +753,29 @@ public class DefaultDashChunkSource implements DashChunkSource { firstSegmentNum, nowPeriodTimeUs) ? 0 : DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED; - ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders = - ImmutableMap.of(); - if (cmcdHeadersFactory != null) { - cmcdHeadersFactory - .setChunkDurationUs(endTimeUs - startTimeUs) - .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection)); - @Nullable - Pair nextObjectAndRangeRequest = - getNextObjectAndRangeRequest(firstSegmentNum, segmentUri, representationHolder); - if (nextObjectAndRangeRequest != null) { - cmcdHeadersFactory - .setNextObjectRequest(nextObjectAndRangeRequest.first) - .setNextRangeRequest(nextObjectAndRangeRequest.second); - } - httpRequestHeaders = cmcdHeadersFactory.createHttpRequestHeaders(); - } DataSpec dataSpec = DashUtil.buildDataSpec( representation, representationHolder.selectedBaseUrl.url, segmentUri, flags, - httpRequestHeaders); + /* httpRequestHeaders= */ ImmutableMap.of()); + if (cmcdDataFactory != null) { + cmcdDataFactory + .setChunkDurationUs(endTimeUs - startTimeUs) + .setObjectType(CmcdData.Factory.getObjectType(trackSelection)); + @Nullable + Pair nextObjectAndRangeRequest = + getNextObjectAndRangeRequest(firstSegmentNum, segmentUri, representationHolder); + if (nextObjectAndRangeRequest != null) { + cmcdDataFactory + .setNextObjectRequest(nextObjectAndRangeRequest.first) + .setNextRangeRequest(nextObjectAndRangeRequest.second); + } + CmcdData cmcdData = cmcdDataFactory.createCmcdData(); + dataSpec = cmcdData.addToDataSpec(dataSpec); + } + return new SingleSampleMediaChunk( dataSource, dataSpec, @@ -812,29 +812,28 @@ public class DefaultDashChunkSource implements DashChunkSource { representationHolder.isSegmentAvailableAtFullNetworkSpeed(segmentNum, nowPeriodTimeUs) ? 0 : DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED; - ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders = - ImmutableMap.of(); - if (cmcdHeadersFactory != null) { - cmcdHeadersFactory - .setChunkDurationUs(endTimeUs - startTimeUs) - .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection)); - @Nullable - Pair nextObjectAndRangeRequest = - getNextObjectAndRangeRequest(firstSegmentNum, segmentUri, representationHolder); - if (nextObjectAndRangeRequest != null) { - cmcdHeadersFactory - .setNextObjectRequest(nextObjectAndRangeRequest.first) - .setNextRangeRequest(nextObjectAndRangeRequest.second); - } - httpRequestHeaders = cmcdHeadersFactory.createHttpRequestHeaders(); - } DataSpec dataSpec = DashUtil.buildDataSpec( representation, representationHolder.selectedBaseUrl.url, segmentUri, flags, - httpRequestHeaders); + /* httpRequestHeaders= */ ImmutableMap.of()); + if (cmcdDataFactory != null) { + cmcdDataFactory + .setChunkDurationUs(endTimeUs - startTimeUs) + .setObjectType(CmcdData.Factory.getObjectType(trackSelection)); + @Nullable + Pair nextObjectAndRangeRequest = + getNextObjectAndRangeRequest(firstSegmentNum, segmentUri, representationHolder); + if (nextObjectAndRangeRequest != null) { + cmcdDataFactory + .setNextObjectRequest(nextObjectAndRangeRequest.first) + .setNextRangeRequest(nextObjectAndRangeRequest.second); + } + CmcdData cmcdData = cmcdDataFactory.createCmcdData(); + dataSpec = cmcdData.addToDataSpec(dataSpec); + } long sampleOffsetUs = -representation.presentationTimeOffsetUs; return new ContainerMediaChunk( dataSource, diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java index 1d9895fa2e..46dcd4891f 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java @@ -301,7 +301,7 @@ public class DefaultDashChunkSourceTest { } @Test - public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdLoggingHeaders() + public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdHttpRequestHeaders() throws Exception { CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT; MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); @@ -381,7 +381,7 @@ public class DefaultDashChunkSourceTest { } @Test - public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdLoggingHeaders() + public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdHttpRequestHeaders() throws Exception { CmcdConfiguration.Factory cmcdConfigurationFactory = mediaItem -> { @@ -429,7 +429,7 @@ public class DefaultDashChunkSourceTest { @Test public void - getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdLoggingHeaders() + getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpRequestHeaders() throws Exception { CmcdConfiguration.Factory cmcdConfigurationFactory = mediaItem -> { @@ -475,6 +475,53 @@ public class DefaultDashChunkSourceTest { "key-4=5.0"); } + @Test + public void + getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpQueryParameters() + throws Exception { + CmcdConfiguration.Factory cmcdConfigurationFactory = + mediaItem -> { + CmcdConfiguration.RequestConfig cmcdRequestConfig = + new CmcdConfiguration.RequestConfig() { + @Override + public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> + getCustomData() { + return new ImmutableListMultimap.Builder< + @CmcdConfiguration.HeaderKey String, String>() + .put(CmcdConfiguration.KEY_CMCD_OBJECT, "key-1=1") + .put(CmcdConfiguration.KEY_CMCD_REQUEST, "key-2=\"stringValue\"") + .build(); + } + }; + + return new CmcdConfiguration( + /* sessionId= */ "sessionId", + /* contentId= */ mediaItem.mediaId, + cmcdRequestConfig, + CmcdConfiguration.MODE_QUERY_PARAMETER); + }; + MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); + DashChunkSource chunkSource = createDashChunkSource(/* numberOfTracks= */ 2, cmcdConfiguration); + ChunkHolder output = new ChunkHolder(); + + chunkSource.getNextChunk( + new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(), + /* loadPositionUs= */ 0, + /* queue= */ ImmutableList.of(), + output); + + assertThat( + Uri.decode( + output.chunk.dataSpec.uri.getQueryParameter( + CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY))) + .isEqualTo( + "bl=0,br=700,cid=\"mediaId\",d=4000,dl=0,key-1=1,key-2=\"stringValue\"," + + "mtp=1000,nor=\"..%2Fvideo_4000_700000.m4s\",nrr=\"0-\",ot=v,sf=d," + + "sid=\"sessionId\",st=v,su,tb=1300"); + } + @Test public void getNextChunk_afterLastAvailableButBeforeEndOfLiveManifestWithKnownDuration_doesNotReturnEndOfStream() diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java index 908a848c1e..56ead292c1 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java @@ -50,9 +50,8 @@ import androidx.media3.exoplayer.source.chunk.MediaChunkIterator; import androidx.media3.exoplayer.trackselection.BaseTrackSelection; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.CmcdConfiguration; -import androidx.media3.exoplayer.upstream.CmcdHeadersFactory; +import androidx.media3.exoplayer.upstream.CmcdData; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.primitives.Ints; import java.io.IOException; @@ -178,6 +177,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * an infinite timeout. * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption * information is available in the multivariant playlist. + * @param playerId The {@link PlayerId} of the player using this chunk source. + * @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source. */ public HlsChunkSource( HlsExtractorFactory extractorFactory, @@ -488,22 +489,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; seenExpectedPlaylistError = false; expectedPlaylistUrl = null; - @Nullable CmcdHeadersFactory cmcdHeadersFactory = null; + @Nullable CmcdData.Factory cmcdDataFactory = null; if (cmcdConfiguration != null) { - cmcdHeadersFactory = - new CmcdHeadersFactory( + cmcdDataFactory = + new CmcdData.Factory( cmcdConfiguration, trackSelection, bufferedDurationUs, /* playbackRate= */ loadingInfo.playbackSpeed, - /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_HLS, + /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_HLS, /* isLive= */ !playlist.hasEndTag, /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs), /* isBufferEmpty= */ queue.isEmpty()) .setObjectType( getIsMuxedAudioAndVideo() - ? CmcdHeadersFactory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO - : CmcdHeadersFactory.getObjectType(trackSelection)); + ? CmcdData.Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO + : CmcdData.Factory.getObjectType(trackSelection)); long nextChunkMediaSequence = partIndex == C.LENGTH_UNSET @@ -515,7 +516,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (nextSegmentBaseHolder != null) { Uri uri = UriUtil.resolveToUri(playlist.baseUri, segmentBaseHolder.segmentBase.url); Uri nextUri = UriUtil.resolveToUri(playlist.baseUri, nextSegmentBaseHolder.segmentBase.url); - cmcdHeadersFactory.setNextObjectRequest(UriUtil.getRelativePath(uri, nextUri)); + cmcdDataFactory.setNextObjectRequest(UriUtil.getRelativePath(uri, nextUri)); String nextRangeRequest = nextSegmentBaseHolder.segmentBase.byteRangeOffset + "-"; if (nextSegmentBaseHolder.segmentBase.byteRangeLength != C.LENGTH_UNSET) { @@ -523,7 +524,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; (nextSegmentBaseHolder.segmentBase.byteRangeOffset + nextSegmentBaseHolder.segmentBase.byteRangeLength); } - cmcdHeadersFactory.setNextRangeRequest(nextRangeRequest); + cmcdDataFactory.setNextRangeRequest(nextRangeRequest); } } lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime(); @@ -534,7 +535,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase.initializationSegment); out.chunk = maybeCreateEncryptionChunkFor( - initSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ true, cmcdHeadersFactory); + initSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ true, cmcdDataFactory); if (out.chunk != null) { return; } @@ -542,7 +543,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase); out.chunk = maybeCreateEncryptionChunkFor( - mediaSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ false, cmcdHeadersFactory); + mediaSegmentKeyUri, selectedTrackIndex, /* isInitSegment= */ false, cmcdDataFactory); if (out.chunk != null) { return; } @@ -578,7 +579,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /* initSegmentKey= */ keyCache.get(initSegmentKeyUri), shouldSpliceIn, playerId, - cmcdHeadersFactory); + cmcdDataFactory); } private boolean getIsMuxedAudioAndVideo() { @@ -896,7 +897,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Nullable Uri keyUri, int selectedTrackIndex, boolean isInitSegment, - @Nullable CmcdHeadersFactory cmcdHeadersFactory) { + @Nullable CmcdData.Factory cmcdDataFactory) { if (keyUri == null) { return null; } @@ -910,20 +911,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return null; } - ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders = - ImmutableMap.of(); - if (cmcdHeadersFactory != null) { - if (isInitSegment) { - cmcdHeadersFactory.setObjectType(CmcdHeadersFactory.OBJECT_TYPE_INIT_SEGMENT); - } - httpRequestHeaders = cmcdHeadersFactory.createHttpRequestHeaders(); - } DataSpec dataSpec = - new DataSpec.Builder() - .setUri(keyUri) - .setFlags(DataSpec.FLAG_ALLOW_GZIP) - .setHttpRequestHeaders(httpRequestHeaders) - .build(); + new DataSpec.Builder().setUri(keyUri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build(); + if (cmcdDataFactory != null) { + if (isInitSegment) { + cmcdDataFactory.setObjectType(CmcdData.Factory.OBJECT_TYPE_INIT_SEGMENT); + } + CmcdData cmcdData = cmcdDataFactory.createCmcdData(); + dataSpec = cmcdData.addToDataSpec(dataSpec); + } + return new EncryptionKeyChunk( encryptionDataSource, dataSpec, diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaChunk.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaChunk.java index 182b684c99..8b293dbc6e 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaChunk.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaChunk.java @@ -33,15 +33,13 @@ import androidx.media3.datasource.DataSpec; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist; import androidx.media3.exoplayer.source.chunk.MediaChunk; -import androidx.media3.exoplayer.upstream.CmcdConfiguration; -import androidx.media3.exoplayer.upstream.CmcdHeadersFactory; +import androidx.media3.exoplayer.upstream.CmcdData; import androidx.media3.extractor.DefaultExtractorInput; import androidx.media3.extractor.ExtractorInput; import androidx.media3.extractor.metadata.id3.Id3Decoder; import androidx.media3.extractor.metadata.id3.PrivFrame; import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import java.io.EOFException; import java.io.IOException; import java.io.InterruptedIOException; @@ -82,7 +80,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null * otherwise. * @param shouldSpliceIn Whether samples for this chunk should be spliced into existing samples. - * @param cmcdHeadersFactory The {@link CmcdHeadersFactory} for generating CMCD request headers. + * @param cmcdDataFactory The {@link CmcdData.Factory} for generating {@link CmcdData}. */ public static HlsMediaChunk createInstance( HlsExtractorFactory extractorFactory, @@ -103,23 +101,22 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Nullable byte[] initSegmentKey, boolean shouldSpliceIn, PlayerId playerId, - @Nullable CmcdHeadersFactory cmcdHeadersFactory) { + @Nullable CmcdData.Factory cmcdDataFactory) { // Media segment. HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase; - ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders = - cmcdHeadersFactory == null - ? ImmutableMap.of() - : cmcdHeadersFactory - .setChunkDurationUs(mediaSegment.durationUs) - .createHttpRequestHeaders(); DataSpec dataSpec = new DataSpec.Builder() .setUri(UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url)) .setPosition(mediaSegment.byteRangeOffset) .setLength(mediaSegment.byteRangeLength) .setFlags(segmentBaseHolder.isPreload ? FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED : 0) - .setHttpRequestHeaders(httpRequestHeaders) .build(); + if (cmcdDataFactory != null) { + CmcdData cmcdData = + cmcdDataFactory.setChunkDurationUs(mediaSegment.durationUs).createCmcdData(); + dataSpec = cmcdData.addToDataSpec(dataSpec); + } + boolean mediaSegmentEncrypted = mediaSegmentKey != null; @Nullable byte[] mediaSegmentIv = @@ -141,19 +138,20 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ? getEncryptionIvArray(Assertions.checkNotNull(initSegment.encryptionIV)) : null; Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url); - ImmutableMap<@CmcdConfiguration.HeaderKey String, String> initHttpRequestHeaders = - cmcdHeadersFactory == null - ? ImmutableMap.of() - : cmcdHeadersFactory - .setObjectType(CmcdHeadersFactory.OBJECT_TYPE_INIT_SEGMENT) - .createHttpRequestHeaders(); initDataSpec = new DataSpec.Builder() .setUri(initSegmentUri) .setPosition(initSegment.byteRangeOffset) .setLength(initSegment.byteRangeLength) - .setHttpRequestHeaders(initHttpRequestHeaders) .build(); + if (cmcdDataFactory != null) { + CmcdData cmcdData = + cmcdDataFactory + .setObjectType(CmcdData.Factory.OBJECT_TYPE_INIT_SEGMENT) + .createCmcdData(); + initDataSpec = cmcdData.addToDataSpec(dataSpec); + } + initDataSource = buildDataSource(dataSource, initSegmentKey, initSegmentIv); } diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java index 87d4aec93e..9d866b0ec4 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java @@ -195,8 +195,7 @@ public class HlsChunkSourceTest { } @Test - public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdLoggingHeaders() - throws Exception { + public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdHttpRequestHeaders() { CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT; MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); CmcdConfiguration cmcdConfiguration = @@ -238,8 +237,8 @@ public class HlsChunkSourceTest { } @Test - public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCorrectBufferStarvationKey() - throws Exception { + public void + getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCorrectBufferStarvationKey() { CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT; MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); CmcdConfiguration cmcdConfiguration = @@ -289,8 +288,7 @@ public class HlsChunkSourceTest { } @Test - public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdLoggingHeaders() - throws Exception { + public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdHttpRequestHeaders() { CmcdConfiguration.Factory cmcdConfigurationFactory = mediaItem -> { CmcdConfiguration.RequestConfig cmcdRequestConfig = @@ -338,7 +336,7 @@ public class HlsChunkSourceTest { @Test public void - getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdLoggingHeaders() + getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpRequestHeaders() throws Exception { CmcdConfiguration.Factory cmcdConfigurationFactory = mediaItem -> { @@ -385,6 +383,53 @@ public class HlsChunkSourceTest { "key-4=5.0"); } + @Test + public void + getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpQueryParameters() + throws Exception { + CmcdConfiguration.Factory cmcdConfigurationFactory = + mediaItem -> { + CmcdConfiguration.RequestConfig cmcdRequestConfig = + new CmcdConfiguration.RequestConfig() { + @Override + public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> + getCustomData() { + return new ImmutableListMultimap.Builder< + @CmcdConfiguration.HeaderKey String, String>() + .put(CmcdConfiguration.KEY_CMCD_OBJECT, "key-1=1") + .put(CmcdConfiguration.KEY_CMCD_REQUEST, "key-2=\"stringValue\"") + .build(); + } + }; + + return new CmcdConfiguration( + /* sessionId= */ "sessionId", + /* contentId= */ mediaItem.mediaId, + cmcdRequestConfig, + CmcdConfiguration.MODE_QUERY_PARAMETER); + }; + MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); + HlsChunkSource testChunkSource = createHlsChunkSource(cmcdConfiguration); + HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder(); + + testChunkSource.getNextChunk( + new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(), + /* loadPositionUs= */ 0, + /* queue= */ ImmutableList.of(), + /* allowEndOfStream= */ true, + output); + + assertThat( + Uri.decode( + output.chunk.dataSpec.uri.getQueryParameter( + CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY))) + .isEqualTo( + "bl=0,br=800,cid=\"mediaId\",d=4000,dl=0,key-1=1,key-2=\"stringValue\"," + + "nor=\"..%2F3.mp4\",nrr=\"0-\",ot=v,sf=h,sid=\"sessionId\",st=v,su,tb=800"); + } + private HlsChunkSource createHlsChunkSource(@Nullable CmcdConfiguration cmcdConfiguration) { return new HlsChunkSource( HlsExtractorFactory.DEFAULT, diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java index 7e4e51dae2..1ae37c5495 100644 --- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java +++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java @@ -43,14 +43,13 @@ import androidx.media3.exoplayer.source.chunk.MediaChunk; import androidx.media3.exoplayer.source.chunk.MediaChunkIterator; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.CmcdConfiguration; -import androidx.media3.exoplayer.upstream.CmcdHeadersFactory; +import androidx.media3.exoplayer.upstream.CmcdData; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.FallbackSelection; import androidx.media3.exoplayer.upstream.LoaderErrorThrower; import androidx.media3.extractor.mp4.FragmentedMp4Extractor; import androidx.media3.extractor.mp4.Track; import androidx.media3.extractor.mp4.TrackEncryptionBox; -import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.util.List; @@ -291,24 +290,24 @@ public class DefaultSsChunkSource implements SsChunkSource { int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex); Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); - @Nullable CmcdHeadersFactory cmcdHeadersFactory = null; + @Nullable CmcdData.Factory cmcdDataFactory = null; if (cmcdConfiguration != null) { - cmcdHeadersFactory = - new CmcdHeadersFactory( + cmcdDataFactory = + new CmcdData.Factory( cmcdConfiguration, trackSelection, bufferedDurationUs, /* playbackRate= */ loadingInfo.playbackSpeed, - /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_SS, + /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_SS, /* isLive= */ manifest.isLive, /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs), /* isBufferEmpty= */ queue.isEmpty()) .setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs) - .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection)); + .setObjectType(CmcdData.Factory.getObjectType(trackSelection)); if (chunkIndex + 1 < streamElement.chunkCount) { Uri nextUri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex + 1); - cmcdHeadersFactory.setNextObjectRequest(UriUtil.getRelativePath(uri, nextUri)); + cmcdDataFactory.setNextObjectRequest(UriUtil.getRelativePath(uri, nextUri)); } } lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime(); @@ -325,7 +324,7 @@ public class DefaultSsChunkSource implements SsChunkSource { trackSelection.getSelectionReason(), trackSelection.getSelectionData(), chunkExtractor, - cmcdHeadersFactory); + cmcdDataFactory); } @Override @@ -370,13 +369,13 @@ public class DefaultSsChunkSource implements SsChunkSource { @C.SelectionReason int trackSelectionReason, @Nullable Object trackSelectionData, ChunkExtractor chunkExtractor, - @Nullable CmcdHeadersFactory cmcdHeadersFactory) { - ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders = - cmcdHeadersFactory == null - ? ImmutableMap.of() - : cmcdHeadersFactory.createHttpRequestHeaders(); - DataSpec dataSpec = - new DataSpec.Builder().setUri(uri).setHttpRequestHeaders(httpRequestHeaders).build(); + @Nullable CmcdData.Factory cmcdDataFactory) { + DataSpec dataSpec = new DataSpec.Builder().setUri(uri).build(); + if (cmcdDataFactory != null) { + CmcdData cmcdData = cmcdDataFactory.createCmcdData(); + dataSpec = cmcdData.addToDataSpec(dataSpec); + } + // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs. long sampleOffsetUs = chunkStartTimeUs; diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java index 075ab15309..70ada1a5d2 100644 --- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java +++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java @@ -51,7 +51,7 @@ public class DefaultSsChunkSourceTest { private static final String SAMPLE_ISMC_1 = "media/smooth-streaming/sample_ismc_1"; @Test - public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdLoggingHeaders() + public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdHttpRequestHeaders() throws Exception { CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT; MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); @@ -131,7 +131,7 @@ public class DefaultSsChunkSourceTest { } @Test - public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdLoggingHeaders() + public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdHttpRequestHeaders() throws Exception { CmcdConfiguration.Factory cmcdConfigurationFactory = mediaItem -> { @@ -179,7 +179,7 @@ public class DefaultSsChunkSourceTest { @Test public void - getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdLoggingHeaders() + getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpRequestHeaders() throws Exception { CmcdConfiguration.Factory cmcdConfigurationFactory = mediaItem -> { @@ -225,6 +225,53 @@ public class DefaultSsChunkSourceTest { "key-4=5.0"); } + @Test + public void + getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdHttpQueryParameters() + throws Exception { + CmcdConfiguration.Factory cmcdConfigurationFactory = + mediaItem -> { + CmcdConfiguration.RequestConfig cmcdRequestConfig = + new CmcdConfiguration.RequestConfig() { + @Override + public ImmutableListMultimap<@CmcdConfiguration.HeaderKey String, String> + getCustomData() { + return new ImmutableListMultimap.Builder< + @CmcdConfiguration.HeaderKey String, String>() + .put(CmcdConfiguration.KEY_CMCD_OBJECT, "key-1=1") + .put(CmcdConfiguration.KEY_CMCD_REQUEST, "key-2=\"stringValue\"") + .build(); + } + }; + + return new CmcdConfiguration( + /* sessionId= */ "sessionId", + /* contentId= */ mediaItem.mediaId, + cmcdRequestConfig, + CmcdConfiguration.MODE_QUERY_PARAMETER); + }; + MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); + SsChunkSource chunkSource = createSsChunkSource(/* numberOfTracks= */ 2, cmcdConfiguration); + ChunkHolder output = new ChunkHolder(); + + chunkSource.getNextChunk( + new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(), + /* loadPositionUs= */ 0, + /* queue= */ ImmutableList.of(), + output); + + assertThat( + Uri.decode( + output.chunk.dataSpec.uri.getQueryParameter( + CmcdConfiguration.CMCD_QUERY_PARAMETER_KEY))) + .isEqualTo( + "bl=0,br=308,cid=\"mediaId\",d=1968,dl=0,key-1=1,key-2=\"stringValue\"," + + "mtp=1000,nor=\"..%2FFragments(video%3D19680000)\",ot=v,sf=s,sid=\"sessionId\"," + + "st=v,su,tb=1536"); + } + private SsChunkSource createSsChunkSource( int numberOfTracks, @Nullable CmcdConfiguration cmcdConfiguration) throws IOException { Assertions.checkArgument(numberOfTracks < 6);