Implement logging support for Common Media Client Data (CMCD)

Add support for including Common Media Client Data (CMCD) in the outgoing requests of adaptive streaming formats DASH, HLS, and SmoothStreaming.

API structure and API methods:
   *   CMCD logging is disabled by default, use `MediaSource.Factory.setCmcdConfigurationFactory(CmcdConfiguration.Factory cmcdConfigurationFactory)` to enable it.
   *   All keys are enabled by default, override `CmcdConfiguration.RequestConfig.isKeyAllowed(String key)` to filter out which keys are logged.
   *  Override `CmcdConfiguration.RequestConfig.getCustomData()` to enable custom key logging.

NOTE: Only the following fields have been implemented: `br`, `bl`, `cid`, `rtp`, and `sid`.

Issue: google/ExoPlayer#8699

#minor-release

PiperOrigin-RevId: 539021056
This commit is contained in:
rohks 2023-06-09 09:14:10 +00:00 committed by Tofunmi Adigun-Hameed
parent a67ce066df
commit b55ddf12b4
24 changed files with 1465 additions and 45 deletions

View File

@ -10,6 +10,20 @@
information if desired (possibly using
`Logger.appendThrowableString(String, Throwable)`).
* ExoPlayer:
* Add support for including Common Media Client Data (CMCD) in the
outgoing requests of adaptive streaming formats DASH, HLS, and
SmoothStreaming. The following fields, `br`, `bl`, `cid`, `rtp`, and
`sid`, have been incorporated
([#8699](https://github.com/google/ExoPlayer/issues/8699)). API
structure and API methods:
* CMCD logging is disabled by default, use
`MediaSource.Factory.setCmcdConfigurationFactory(CmcdConfiguration.Factory
cmcdConfigurationFactory)` to enable it.
* All keys are enabled by default, override
`CmcdConfiguration.RequestConfig.isKeyAllowed(String key)` to filter
out which keys are logged.
* Override `CmcdConfiguration.RequestConfig.getCustomData()` to enable
custom key logging.
* Add additional action to manifest of main demo for making it easier to
start the demo app with a custom `*.exolist.json` file
([#439](https://github.com/androidx/media/pull/439)).

View File

@ -38,6 +38,7 @@ import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.source.ads.AdsLoader;
import androidx.media3.exoplayer.source.ads.AdsMediaSource;
import androidx.media3.exoplayer.text.SubtitleDecoderFactory;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.extractor.DefaultExtractorsFactory;
import androidx.media3.extractor.Extractor;
@ -381,6 +382,15 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
return this;
}
@CanIgnoreReturnValue
@UnstableApi
@Override
public DefaultMediaSourceFactory setCmcdConfigurationFactory(
CmcdConfiguration.Factory cmcdConfigurationFactory) {
delegateFactoryLoader.setCmcdConfigurationFactory(checkNotNull(cmcdConfigurationFactory));
return this;
}
@CanIgnoreReturnValue
@UnstableApi
@Override
@ -566,6 +576,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
private final Map<Integer, MediaSource.Factory> mediaSourceFactories;
private DataSource.@MonotonicNonNull Factory dataSourceFactory;
@Nullable private CmcdConfiguration.Factory cmcdConfigurationFactory;
@Nullable private DrmSessionManagerProvider drmSessionManagerProvider;
@Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
@ -595,6 +606,9 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
}
mediaSourceFactory = mediaSourceFactorySupplier.get();
if (cmcdConfigurationFactory != null) {
mediaSourceFactory.setCmcdConfigurationFactory(cmcdConfigurationFactory);
}
if (drmSessionManagerProvider != null) {
mediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
}
@ -615,6 +629,13 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
}
}
public void setCmcdConfigurationFactory(CmcdConfiguration.Factory cmcdConfigurationFactory) {
this.cmcdConfigurationFactory = cmcdConfigurationFactory;
for (MediaSource.Factory mediaSourceFactory : mediaSourceFactories.values()) {
mediaSourceFactory.setCmcdConfigurationFactory(cmcdConfigurationFactory);
}
}
public void setDrmSessionManagerProvider(DrmSessionManagerProvider drmSessionManagerProvider) {
this.drmSessionManagerProvider = drmSessionManagerProvider;
for (MediaSource.Factory mediaSourceFactory : mediaSourceFactories.values()) {

View File

@ -28,6 +28,7 @@ import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import java.io.IOException;
@ -67,6 +68,19 @@ public interface MediaSource {
@SuppressWarnings("deprecation")
Factory UNSUPPORTED = MediaSourceFactory.UNSUPPORTED;
/**
* Sets the {@link CmcdConfiguration.Factory} used to obtain a {@link CmcdConfiguration} for a
* {@link MediaItem}.
*
* @return This factory, for convenience.
*/
@UnstableApi
default Factory setCmcdConfigurationFactory(
CmcdConfiguration.Factory cmcdConfigurationFactory) {
// do nothing
return this;
}
/**
* Sets the {@link DrmSessionManagerProvider} used to obtain a {@link DrmSessionManager} for a
* {@link MediaItem}.

View File

@ -0,0 +1,240 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.upstream;
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.Nullable;
import androidx.annotation.StringDef;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableMap;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.UUID;
/** Represents a configuration for the Common Media Client Data (CMCD) logging. */
@UnstableApi
public final class CmcdConfiguration {
/**
* Header keys SHOULD be allocated to one of the four defined header names based upon their
* expected level of variability:
*
* <ul>
* <li>CMCD-Object: keys whose values vary with the object being requested.
* <li>CMCD-Request: keys whose values vary with each request.
* <li>CMCD-Session: keys whose values are expected to be invariant over the life of the
* session.
* <li>CMCD-Status: keys whose values do not vary with every request or object.
* </ul>
*/
@Retention(RetentionPolicy.SOURCE)
@StringDef({KEY_CMCD_OBJECT, KEY_CMCD_REQUEST, KEY_CMCD_SESSION, KEY_CMCD_STATUS})
@Documented
@Target(TYPE_USE)
public @interface HeaderKey {}
/** Indicates that the annotated element represents a CMCD key. */
@Retention(RetentionPolicy.SOURCE)
@StringDef({
KEY_BITRATE,
KEY_BUFFER_LENGTH,
KEY_CONTENT_ID,
KEY_SESSION_ID,
KEY_MAXIMUM_REQUESTED_BITRATE
})
@Documented
@Target(TYPE_USE)
public @interface CmcdKey {}
/** Maximum length for ID fields. */
public static final int MAX_ID_LENGTH = 64;
public static final String KEY_CMCD_OBJECT = "CMCD-Object";
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 KEY_BITRATE = "br";
public static final String KEY_BUFFER_LENGTH = "bl";
public static final String KEY_CONTENT_ID = "cid";
public static final String KEY_SESSION_ID = "sid";
public static final String KEY_MAXIMUM_REQUESTED_BITRATE = "rtp";
/**
* Factory for {@link CmcdConfiguration} instances.
*
* <p>Implementations must not make assumptions about which thread called their methods; and must
* be thread-safe.
*/
public interface Factory {
/**
* Creates a {@link CmcdConfiguration} based on the provided {@link MediaItem}.
*
* @param mediaItem The {@link MediaItem} from which to create the CMCD configuration.
* @return A {@link CmcdConfiguration} instance.
*/
CmcdConfiguration createCmcdConfiguration(MediaItem mediaItem);
/**
* The default factory implementation.
*
* <p>It creates a {@link CmcdConfiguration} by generating a random session ID and using the
* content ID from {@link MediaItem#mediaId} (or {@link MediaItem#DEFAULT_MEDIA_ID} if the media
* item does not have a {@link MediaItem#mediaId} defined).
*
* <p>It also utilises a default {@link RequestConfig} implementation that enables all available
* keys, provides empty custom data, and sets the maximum requested bitrate to {@link
* C#RATE_UNSET_INT}.
*/
CmcdConfiguration.Factory DEFAULT =
mediaItem ->
new CmcdConfiguration(
/* sessionId= */ UUID.randomUUID().toString(),
/* contentId= */ mediaItem.mediaId != null
? mediaItem.mediaId
: MediaItem.DEFAULT_MEDIA_ID,
new RequestConfig() {});
}
/**
* Represents configuration which can vary on each request.
*
* <p>Implementations must not make assumptions about which thread called their methods; and must
* be thread-safe.
*/
public interface RequestConfig {
/**
* Checks whether the specified key is allowed in CMCD logging. By default, all keys are
* allowed.
*
* @param key The key to check.
* @return Whether the key is allowed.
*/
default boolean isKeyAllowed(@CmcdKey String key) {
return true;
}
/**
* Retrieves the custom data associated with CMCD logging.
*
* <p>By default, no custom data is provided.
*
* <p>The key should belong to the {@link HeaderKey}. The value should consist of key-value
* pairs separated by commas. If the value contains one of the keys defined in the {@link
* CmcdKey} list, then this key should not be {@linkplain #isKeyAllowed(String) allowed},
* otherwise the key could be included twice in the produced log.
*
* <p>Example:
*
* <ul>
* <li>CMCD-Request:customField1=25400
* <li>CMCD-Object:customField2=3200,customField3=4004,customField4=v,customField5=6000
* <li>CMCD-Status:customField6,customField7=15000
* <li>CMCD-Session:customField8="6e2fb550-c457-11e9-bb97-0800200c9a66"
* </ul>
*
* @return An {@link ImmutableMap} containing the custom data.
*/
default ImmutableMap<@HeaderKey String, String> getCustomData() {
return ImmutableMap.of();
}
/**
* Returns the maximum throughput requested in kbps, or {@link C#RATE_UNSET_INT} if the maximum
* throughput is unknown in which case the maximum throughput will not be logged upstream.
*
* @param throughputKbps The throughput in kbps of the audio or video object being requested.
* @return The maximum throughput requested in kbps.
*/
default int getRequestedMaximumThroughputKbps(int throughputKbps) {
return C.RATE_UNSET_INT;
}
}
/**
* A GUID identifying the current playback session, or {@code null} if unset.
*
* <p>A playback session typically ties together segments belonging to a single media asset.
* Maximum length is 64 characters.
*/
@Nullable public final String sessionId;
/**
* A GUID identifying the current content, or {@code null} if unset.
*
* <p>This value is consistent across multiple different sessions and devices and is defined and
* updated at the discretion of the service provider. Maximum length is 64 characters.
*/
@Nullable public final String contentId;
/** Dynamic request specific configuration. */
public final RequestConfig requestConfig;
/** Creates an instance. */
public CmcdConfiguration(
@Nullable String sessionId, @Nullable String contentId, RequestConfig requestConfig) {
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;
}
/**
* Whether logging bitrate is allowed based on the {@linkplain RequestConfig request
* configuration}.
*/
public boolean isBitrateLoggingAllowed() {
return requestConfig.isKeyAllowed(KEY_BITRATE);
}
/**
* Whether logging buffer length is allowed based on the {@linkplain RequestConfig request
* configuration}.
*/
public boolean isBufferLengthLoggingAllowed() {
return requestConfig.isKeyAllowed(KEY_BUFFER_LENGTH);
}
/**
* Whether logging content ID is allowed based on the {@linkplain RequestConfig request
* configuration}.
*/
public boolean isContentIdLoggingAllowed() {
return requestConfig.isKeyAllowed(KEY_CONTENT_ID);
}
/**
* Whether logging session ID is allowed based on the {@linkplain RequestConfig request
* configuration}.
*/
public boolean isSessionIdLoggingAllowed() {
return requestConfig.isKeyAllowed(KEY_SESSION_ID);
}
/**
* Whether logging maximum requested throughput is allowed based on the {@linkplain RequestConfig
* request configuration}.
*/
public boolean isMaximumRequestThroughputLoggingAllowed() {
return requestConfig.isKeyAllowed(KEY_MAXIMUM_REQUESTED_BITRATE);
}
}

View File

@ -0,0 +1,480 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.upstream;
import static androidx.media3.common.util.Assertions.checkArgument;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/**
* Represents the data for CMCD (Common Media Client Data) in adaptive streaming formats DASH, HLS,
* and SmoothStreaming.
*
* <p>It holds various attributes related to the playback of media content according to the
* specifications outlined in the CMCD standard document <a
* href="https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf">CTA-5004</a>.
*/
@UnstableApi
public final class CmcdLog {
/**
* Creates a new instance.
*
* @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
* @param trackSelection The {@linkplain ExoTrackSelection track selection}.
* @param playbackPositionUs The current playback position in microseconds.
* @param loadPositionUs The current load position in microseconds.
*/
public static CmcdLog createInstance(
CmcdConfiguration cmcdConfiguration,
ExoTrackSelection trackSelection,
long playbackPositionUs,
long loadPositionUs) {
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> customData =
cmcdConfiguration.requestConfig.getCustomData();
int bitrateKbps = trackSelection.getSelectedFormat().bitrate / 1000;
CmcdLog.CmcdObject.Builder cmcdObject =
new CmcdLog.CmcdObject.Builder()
.setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_OBJECT));
if (cmcdConfiguration.isBitrateLoggingAllowed()) {
cmcdObject.setBitrateKbps(bitrateKbps);
}
CmcdLog.CmcdRequest.Builder cmcdRequest =
new CmcdLog.CmcdRequest.Builder()
.setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_REQUEST));
if (cmcdConfiguration.isBufferLengthLoggingAllowed()) {
cmcdRequest.setBufferLengthMs(
loadPositionUs == C.TIME_UNSET ? 0 : (loadPositionUs - playbackPositionUs) / 1000);
}
CmcdLog.CmcdSession.Builder cmcdSession =
new CmcdLog.CmcdSession.Builder()
.setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_SESSION));
if (cmcdConfiguration.isContentIdLoggingAllowed()) {
cmcdSession.setContentId(cmcdConfiguration.contentId);
}
if (cmcdConfiguration.isSessionIdLoggingAllowed()) {
cmcdSession.setSessionId(cmcdConfiguration.sessionId);
}
CmcdLog.CmcdStatus.Builder cmcdStatus =
new CmcdLog.CmcdStatus.Builder()
.setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_STATUS));
if (cmcdConfiguration.isMaximumRequestThroughputLoggingAllowed()) {
cmcdStatus.setMaximumRequestedThroughputKbps(
cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps));
}
return new CmcdLog(
cmcdObject.build(), cmcdRequest.build(), cmcdSession.build(), cmcdStatus.build());
}
private final CmcdObject cmcdObject;
private final CmcdRequest cmcdRequest;
private final CmcdSession cmcdSession;
private final CmcdStatus cmcdStatus;
private CmcdLog(
CmcdObject cmcdObject,
CmcdRequest cmcdRequest,
CmcdSession cmcdSession,
CmcdStatus cmcdStatus) {
this.cmcdObject = cmcdObject;
this.cmcdRequest = cmcdRequest;
this.cmcdSession = cmcdSession;
this.cmcdStatus = cmcdStatus;
}
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> getHttpRequestHeaders() {
ImmutableMap.Builder<String, String> httpRequestHeaders = ImmutableMap.builder();
this.cmcdObject.populateHttpRequestHeaders(httpRequestHeaders);
this.cmcdRequest.populateHttpRequestHeaders(httpRequestHeaders);
this.cmcdSession.populateHttpRequestHeaders(httpRequestHeaders);
this.cmcdStatus.populateHttpRequestHeaders(httpRequestHeaders);
return httpRequestHeaders.buildOrThrow();
}
/** Keys whose values vary with the object being requested. Contains CMCD fields: {@code br}. */
private static final class CmcdObject {
/** Builder for {@link CmcdObject} instances. */
public static final class Builder {
private int bitrateKbps;
@Nullable private String customData;
/** Creates a new instance with default values. */
public Builder() {
this.bitrateKbps = C.RATE_UNSET_INT;
}
/** Sets the {@link CmcdObject#bitrateKbps}. The default value is {@link C#RATE_UNSET_INT}. */
@CanIgnoreReturnValue
public Builder setBitrateKbps(int bitrateKbps) {
this.bitrateKbps = bitrateKbps;
return this;
}
/** Sets the {@link CmcdObject#customData}. The default value is {@code null}. */
@CanIgnoreReturnValue
public Builder setCustomData(@Nullable String customData) {
this.customData = customData;
return this;
}
public CmcdObject build() {
return new CmcdObject(this);
}
}
/**
* The encoded bitrate in kbps of the audio or video object being requested, or {@link
* C#RATE_UNSET_INT} if unset.
*
* <p>This may not be known precisely by the player; however, it MAY be estimated based upon
* playlist/manifest declarations. If the playlist declares both peak and average bitrate
* values, the peak value should be transmitted.
*/
public final int bitrateKbps;
/**
* Custom data where the values of the keys vary with the object being requested, or {@code
* null} if unset.
*
* <p>The String consists of key-value pairs separated by commas.<br>
* Example: {@code key1=intValue,key2="stringValue"}.
*/
@Nullable public final String customData;
private CmcdObject(Builder builder) {
this.bitrateKbps = builder.bitrateKbps;
this.customData = builder.customData;
}
/**
* Populates the HTTP request headers with {@link CmcdConfiguration#KEY_CMCD_OBJECT} values.
*
* @param httpRequestHeaders An {@link ImmutableMap.Builder} used to build the HTTP request
* headers.
*/
public void populateHttpRequestHeaders(
ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) {
StringBuilder headerValue = new StringBuilder();
if (bitrateKbps != C.RATE_UNSET_INT) {
headerValue.append(
Util.formatInvariant("%s=%d,", CmcdConfiguration.KEY_BITRATE, bitrateKbps));
}
if (!TextUtils.isEmpty(customData)) {
headerValue.append(Util.formatInvariant("%s,", customData));
}
if (headerValue.length() == 0) {
return;
}
// Remove the trailing comma as headerValue is not empty
headerValue.setLength(headerValue.length() - 1);
httpRequestHeaders.put(CmcdConfiguration.KEY_CMCD_OBJECT, headerValue.toString());
}
}
/** Keys whose values vary with each request. Contains CMCD fields: {@code bl}. */
private static final class CmcdRequest {
/** Builder for {@link CmcdRequest} instances. */
public static final class Builder {
private long bufferLengthMs;
@Nullable private String customData;
/** Creates a new instance with default values. */
public Builder() {
this.bufferLengthMs = C.TIME_UNSET;
}
/**
* Sets the {@link CmcdRequest#bufferLengthMs}. Rounded to nearest 100 ms. The default value
* is {@link C#TIME_UNSET}.
*/
@CanIgnoreReturnValue
public Builder setBufferLengthMs(long bufferLengthMs) {
checkArgument(bufferLengthMs == C.TIME_UNSET || bufferLengthMs >= 0);
this.bufferLengthMs =
bufferLengthMs == C.TIME_UNSET ? bufferLengthMs : ((bufferLengthMs + 50) / 100) * 100;
return this;
}
/** Sets the {@link CmcdRequest#customData}. The default value is {@code null}. */
@CanIgnoreReturnValue
public CmcdRequest.Builder setCustomData(@Nullable String customData) {
this.customData = customData;
return this;
}
public CmcdRequest build() {
return new CmcdRequest(this);
}
}
/**
* The buffer length in milliseconds associated with the media object being requested, or {@link
* C#TIME_UNSET} if unset.
*
* <p>This value MUST be rounded to the nearest 100 ms.
*/
public final long bufferLengthMs;
/**
* Custom data where the values of the keys vary with each request, or {@code null} if unset.
*
* <p>The String consists of key-value pairs separated by commas.<br>
* Example: {@code key1=intValue, key2="stringValue"}.
*/
@Nullable public final String customData;
private CmcdRequest(Builder builder) {
this.bufferLengthMs = builder.bufferLengthMs;
this.customData = builder.customData;
}
/**
* Populates the HTTP request headers with {@link CmcdConfiguration#KEY_CMCD_REQUEST} values.
*
* @param httpRequestHeaders An {@link ImmutableMap.Builder} used to build the HTTP request
* headers.
*/
public void populateHttpRequestHeaders(
ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) {
StringBuilder headerValue = new StringBuilder();
if (bufferLengthMs != C.TIME_UNSET) {
headerValue.append(
Util.formatInvariant("%s=%d,", CmcdConfiguration.KEY_BUFFER_LENGTH, bufferLengthMs));
}
if (!TextUtils.isEmpty(customData)) {
headerValue.append(Util.formatInvariant("%s,", customData));
}
if (headerValue.length() == 0) {
return;
}
// Remove the trailing comma as headerValue is not empty
headerValue.setLength(headerValue.length() - 1);
httpRequestHeaders.put(CmcdConfiguration.KEY_CMCD_REQUEST, headerValue.toString());
}
}
/**
* Keys whose values are expected to be invariant over the life of the session. Contains CMCD
* fields: {@code cid} and {@code sid}.
*/
private static final class CmcdSession {
/** Builder for {@link CmcdSession} instances. */
public static final class Builder {
@Nullable private String contentId;
@Nullable private String sessionId;
@Nullable private String customData;
/**
* Sets the {@link CmcdSession#contentId}. Maximum length allowed is 64 characters. The
* default value is {@code null}.
*/
@CanIgnoreReturnValue
public Builder setContentId(@Nullable String contentId) {
checkArgument(contentId == null || contentId.length() <= CmcdConfiguration.MAX_ID_LENGTH);
this.contentId = contentId;
return this;
}
/**
* Sets the {@link CmcdSession#sessionId}. Maximum length allowed is 64 characters. The
* default value is {@code null}.
*/
@CanIgnoreReturnValue
public Builder setSessionId(@Nullable String sessionId) {
checkArgument(sessionId == null || sessionId.length() <= CmcdConfiguration.MAX_ID_LENGTH);
this.sessionId = sessionId;
return this;
}
/** Sets the {@link CmcdSession#customData}. The default value is {@code null}. */
@CanIgnoreReturnValue
public CmcdSession.Builder setCustomData(@Nullable String customData) {
this.customData = customData;
return this;
}
public CmcdSession build() {
return new CmcdSession(this);
}
}
/**
* A GUID identifying the current content, or {@code null} if unset.
*
* <p>This value is consistent across multiple different sessions and devices and is defined and
* updated at the discretion of the service provider. Maximum length is 64 characters.
*/
@Nullable public final String contentId;
/**
* A GUID identifying the current playback session, or {@code null} if unset.
*
* <p>A playback session typically ties together segments belonging to a single media asset.
* Maximum length is 64 characters.
*/
@Nullable public final String sessionId;
/**
* Custom data where the values of the keys are expected to be invariant over the life of the
* session, or {@code null} if unset.
*
* <p>The String consists of key-value pairs separated by commas.<br>
* Example: {@code key1=intValue, key2="stringValue"}.
*/
@Nullable public final String customData;
private CmcdSession(Builder builder) {
this.contentId = builder.contentId;
this.sessionId = builder.sessionId;
this.customData = builder.customData;
}
/**
* Populates the HTTP request headers with {@link CmcdConfiguration#KEY_CMCD_SESSION} values.
*
* @param httpRequestHeaders An {@link ImmutableMap.Builder} used to build the HTTP request
* headers.
*/
public void populateHttpRequestHeaders(
ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) {
StringBuilder headerValue = new StringBuilder();
if (!TextUtils.isEmpty(this.contentId)) {
headerValue.append(
Util.formatInvariant("%s=\"%s\",", CmcdConfiguration.KEY_CONTENT_ID, contentId));
}
if (!TextUtils.isEmpty(this.sessionId)) {
headerValue.append(
Util.formatInvariant("%s=\"%s\",", CmcdConfiguration.KEY_SESSION_ID, sessionId));
}
if (!TextUtils.isEmpty(customData)) {
headerValue.append(Util.formatInvariant("%s,", customData));
}
if (headerValue.length() == 0) {
return;
}
// Remove the trailing comma as headerValue is not empty
headerValue.setLength(headerValue.length() - 1);
httpRequestHeaders.put(CmcdConfiguration.KEY_CMCD_SESSION, headerValue.toString());
}
}
/**
* Keys whose values do not vary with every request or object. Contains CMCD fields: {@code rtp}.
*/
private static final class CmcdStatus {
/** Builder for {@link CmcdStatus} instances. */
public static final class Builder {
private int maximumRequestedThroughputKbps;
@Nullable private String customData;
/** Creates a new instance with default values. */
public Builder() {
this.maximumRequestedThroughputKbps = C.RATE_UNSET_INT;
}
/**
* Sets the {@link CmcdStatus#maximumRequestedThroughputKbps}. Rounded to nearest 100 kbps.
* The default value is {@link C#RATE_UNSET_INT}.
*/
@CanIgnoreReturnValue
public Builder setMaximumRequestedThroughputKbps(int maximumRequestedThroughputKbps) {
checkArgument(
maximumRequestedThroughputKbps == C.RATE_UNSET_INT
|| maximumRequestedThroughputKbps >= 0);
this.maximumRequestedThroughputKbps =
maximumRequestedThroughputKbps == C.RATE_UNSET_INT
? maximumRequestedThroughputKbps
: ((maximumRequestedThroughputKbps + 50) / 100) * 100;
return this;
}
/** Sets the {@link CmcdStatus#customData}. The default value is {@code null}. */
@CanIgnoreReturnValue
public CmcdStatus.Builder setCustomData(@Nullable String customData) {
this.customData = customData;
return this;
}
public CmcdStatus build() {
return new CmcdStatus(this);
}
}
/**
* The requested maximum throughput in kbps that the client considers sufficient for delivery of
* the asset, or {@link C#RATE_UNSET_INT} if unset. Values MUST be rounded to the nearest
* 100kbps.
*/
public final int maximumRequestedThroughputKbps;
/**
* Custom data where the values of the keys do not vary with every request or object, or {@code
* null} if unset.
*
* <p>The String consists of key-value pairs separated by commas.<br>
* Example: {@code key1=intValue, key2="stringValue"}.
*/
@Nullable public final String customData;
private CmcdStatus(Builder builder) {
this.maximumRequestedThroughputKbps = builder.maximumRequestedThroughputKbps;
this.customData = builder.customData;
}
/**
* Populates the HTTP request headers with {@link CmcdConfiguration#KEY_CMCD_STATUS} values.
*
* @param httpRequestHeaders An {@link ImmutableMap.Builder} used to build the HTTP request
* headers.
*/
public void populateHttpRequestHeaders(
ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders) {
StringBuilder headerValue = new StringBuilder();
if (maximumRequestedThroughputKbps != C.RATE_UNSET_INT) {
headerValue.append(
Util.formatInvariant(
"%s=%d,",
CmcdConfiguration.KEY_MAXIMUM_REQUESTED_BITRATE, maximumRequestedThroughputKbps));
}
if (!TextUtils.isEmpty(customData)) {
headerValue.append(Util.formatInvariant("%s,", customData));
}
if (headerValue.length() == 0) {
return;
}
// Remove the trailing comma as headerValue is not empty
headerValue.setLength(headerValue.length() - 1);
httpRequestHeaders.put(CmcdConfiguration.KEY_CMCD_STATUS, headerValue.toString());
}
}
}

View File

@ -0,0 +1,131 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.upstream;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableMap;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link CmcdConfiguration}. */
@RunWith(AndroidJUnit4.class)
public class CmcdConfigurationTest {
private static final String TEST_CONTENT_ID = "contentId";
private static final String TEST_MEDIA_ID = "mediaId";
private static final String TEST_SESSION_ID = "sessionId";
private static final String LONG_INVALID_ID =
"9haaks0aousdjts41iczi1ilmkzxrbwf7hkuesvzt2ib44s8cmjtzfcmenzy3ozp67890qwertyuiopasd";
@Test
public void invalidSessionId_throwsError() {
assertThrows(
IllegalArgumentException.class,
() ->
new CmcdConfiguration(
/* sessionId= */ LONG_INVALID_ID,
/* contentId= */ null,
new CmcdConfiguration.RequestConfig() {}));
}
@Test
public void invalidContentId_throwsError() {
assertThrows(
IllegalArgumentException.class,
() ->
new CmcdConfiguration(
/* sessionId= */ null,
/* contentId= */ LONG_INVALID_ID,
new CmcdConfiguration.RequestConfig() {}));
}
@Test
public void nullRequestConfig_throwsError() {
assertThrows(
NullPointerException.class,
() ->
new CmcdConfiguration(
/* sessionId= */ null, /* contentId= */ null, /* requestConfig= */ null));
}
@Test
public void defaultFactory_createsInstance() {
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
MediaItem mediaItem = new MediaItem.Builder().setMediaId(TEST_MEDIA_ID).build();
CmcdConfiguration cmcdConfiguration =
cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
assertThat(cmcdConfiguration.contentId).isEqualTo(TEST_MEDIA_ID);
assertThat(cmcdConfiguration.isBitrateLoggingAllowed()).isTrue();
assertThat(cmcdConfiguration.requestConfig.getCustomData()).isEmpty();
assertThat(
cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(
/* throughputKbps= */ 0))
.isEqualTo(C.RATE_UNSET_INT);
}
@Test
public void customFactory_createsInstance() {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem ->
new CmcdConfiguration(
TEST_SESSION_ID,
TEST_CONTENT_ID,
new CmcdConfiguration.RequestConfig() {
@Override
public boolean isKeyAllowed(@CmcdConfiguration.CmcdKey String key) {
return key.equals("br") || key.equals("rtp");
}
@Override
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> getCustomData() {
return new ImmutableMap.Builder<String, String>()
.put("CMCD-Object", "key1=value1")
.put("CMCD-Request", "key2=\"stringValue\"")
.buildOrThrow();
}
@Override
public int getRequestedMaximumThroughputKbps(int throughputKbps) {
return 2 * throughputKbps;
}
});
MediaItem mediaItem = new MediaItem.Builder().setMediaId(TEST_MEDIA_ID).build();
CmcdConfiguration cmcdConfiguration =
cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
assertThat(cmcdConfiguration.sessionId).isEqualTo(TEST_SESSION_ID);
assertThat(cmcdConfiguration.contentId).isEqualTo(TEST_CONTENT_ID);
assertThat(cmcdConfiguration.isBitrateLoggingAllowed()).isTrue();
assertThat(cmcdConfiguration.isBufferLengthLoggingAllowed()).isFalse();
assertThat(cmcdConfiguration.isContentIdLoggingAllowed()).isFalse();
assertThat(cmcdConfiguration.isSessionIdLoggingAllowed()).isFalse();
assertThat(cmcdConfiguration.isMaximumRequestThroughputLoggingAllowed()).isTrue();
assertThat(cmcdConfiguration.requestConfig.getCustomData())
.containsExactly("CMCD-Object", "key1=value1", "CMCD-Request", "key2=\"stringValue\"");
assertThat(
cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(
/* throughputKbps= */ 100))
.isEqualTo(200);
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.upstream;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableMap;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link CmcdLog}. */
@RunWith(AndroidJUnit4.class)
public class CmcdLogTest {
@Test
public void createInstance_populatesCmcdHeaders() {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem ->
new CmcdConfiguration(
"sessionId",
mediaItem.mediaId,
new CmcdConfiguration.RequestConfig() {
@Override
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> getCustomData() {
return new ImmutableMap.Builder<String, String>()
.put("CMCD-Object", "key1=value1")
.put("CMCD-Request", "key2=\"stringValue\"")
.buildOrThrow();
}
@Override
public int getRequestedMaximumThroughputKbps(int throughputKbps) {
return 2 * throughputKbps;
}
});
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
CmcdConfiguration cmcdConfiguration =
cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
ExoTrackSelection trackSelection = mock(ExoTrackSelection.class);
when(trackSelection.getSelectedFormat())
.thenReturn(new Format.Builder().setPeakBitrate(840_000).build());
CmcdLog cmcdLog =
CmcdLog.createInstance(
cmcdConfiguration,
trackSelection,
/* playbackPositionUs= */ 1_000_000,
/* loadPositionUs= */ 2_760_000);
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> requestHeaders =
cmcdLog.getHttpRequestHeaders();
assertThat(requestHeaders)
.containsExactly(
"CMCD-Object",
"br=840,key1=value1",
"CMCD-Request",
"bl=1800,key2=\"stringValue\"",
"CMCD-Session",
"cid=\"mediaId\",sid=\"sessionId\"",
"CMCD-Status",
"rtp=1700");
}
}

View File

@ -26,6 +26,7 @@ import androidx.media3.exoplayer.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler;
import androidx.media3.exoplayer.dash.manifest.DashManifest;
import androidx.media3.exoplayer.source.chunk.ChunkSource;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import java.util.List;
@ -55,6 +56,7 @@ public interface DashChunkSource extends ChunkSource {
* @param transferListener The transfer listener which should be informed of any data transfers.
* May be null if no listener is available.
* @param playerId The {@link PlayerId} of the player using this chunk source.
* @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
* @return The created {@link DashChunkSource}.
*/
DashChunkSource createDashChunkSource(
@ -70,7 +72,8 @@ public interface DashChunkSource extends ChunkSource {
List<Format> closedCaptionFormats,
@Nullable PlayerTrackEmsgHandler playerEmsgHandler,
@Nullable TransferListener transferListener,
PlayerId playerId);
PlayerId playerId,
@Nullable CmcdConfiguration cmcdConfiguration);
}
/**

View File

@ -54,6 +54,7 @@ import androidx.media3.exoplayer.source.chunk.ChunkSampleStream;
import androidx.media3.exoplayer.source.chunk.ChunkSampleStream.EmbeddedSampleStream;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import com.google.common.primitives.Ints;
@ -85,6 +86,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* package */ final int id;
private final DashChunkSource.Factory chunkSourceFactory;
@Nullable private final TransferListener transferListener;
@Nullable private final CmcdConfiguration cmcdConfiguration;
private final DrmSessionManager drmSessionManager;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final BaseUrlExclusionList baseUrlExclusionList;
@ -116,6 +118,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int periodIndex,
DashChunkSource.Factory chunkSourceFactory,
@Nullable TransferListener transferListener,
@Nullable CmcdConfiguration cmcdConfiguration,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
@ -132,6 +135,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
this.periodIndex = periodIndex;
this.chunkSourceFactory = chunkSourceFactory;
this.transferListener = transferListener;
this.cmcdConfiguration = cmcdConfiguration;
this.drmSessionManager = drmSessionManager;
this.drmEventDispatcher = drmEventDispatcher;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
@ -791,7 +795,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
embeddedClosedCaptionTrackFormats,
trackPlayerEmsgHandler,
transferListener,
playerId);
playerId,
cmcdConfiguration);
ChunkSampleStream<DashChunkSource> stream =
new ChunkSampleStream<>(
trackGroupInfo.trackType,

View File

@ -67,6 +67,7 @@ import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher
import androidx.media3.exoplayer.source.MediaSourceFactory;
import androidx.media3.exoplayer.source.SequenceableLoader;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
@ -106,6 +107,7 @@ public final class DashMediaSource extends BaseMediaSource {
private final DashChunkSource.Factory chunkSourceFactory;
@Nullable private final DataSource.Factory manifestDataSourceFactory;
private CmcdConfiguration.Factory cmcdConfigurationFactory;
private DrmSessionManagerProvider drmSessionManagerProvider;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
@ -161,6 +163,13 @@ public final class DashMediaSource extends BaseMediaSource {
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
}
@CanIgnoreReturnValue
@Override
public Factory setCmcdConfigurationFactory(CmcdConfiguration.Factory cmcdConfigurationFactory) {
this.cmcdConfigurationFactory = checkNotNull(cmcdConfigurationFactory);
return this;
}
@CanIgnoreReturnValue
@Override
public Factory setDrmSessionManagerProvider(
@ -290,6 +299,11 @@ public final class DashMediaSource extends BaseMediaSource {
mediaItemBuilder.setUri(Uri.EMPTY);
}
mediaItem = mediaItemBuilder.build();
@Nullable
CmcdConfiguration cmcdConfiguration =
cmcdConfigurationFactory == null
? null
: cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
return new DashMediaSource(
mediaItem,
manifest,
@ -297,6 +311,7 @@ public final class DashMediaSource extends BaseMediaSource {
/* manifestParser= */ null,
chunkSourceFactory,
compositeSequenceableLoaderFactory,
cmcdConfiguration,
drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
fallbackTargetLiveOffsetMs,
@ -321,6 +336,11 @@ public final class DashMediaSource extends BaseMediaSource {
if (!streamKeys.isEmpty()) {
manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys);
}
@Nullable
CmcdConfiguration cmcdConfiguration =
cmcdConfigurationFactory == null
? null
: cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
return new DashMediaSource(
mediaItem,
@ -329,6 +349,7 @@ public final class DashMediaSource extends BaseMediaSource {
manifestParser,
chunkSourceFactory,
compositeSequenceableLoaderFactory,
cmcdConfiguration,
drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
fallbackTargetLiveOffsetMs,
@ -373,6 +394,7 @@ public final class DashMediaSource extends BaseMediaSource {
private final DataSource.Factory manifestDataSourceFactory;
private final DashChunkSource.Factory chunkSourceFactory;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
@Nullable private final CmcdConfiguration cmcdConfiguration;
private final DrmSessionManager drmSessionManager;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final BaseUrlExclusionList baseUrlExclusionList;
@ -416,6 +438,7 @@ public final class DashMediaSource extends BaseMediaSource {
@Nullable ParsingLoadable.Parser<? extends DashManifest> manifestParser,
DashChunkSource.Factory chunkSourceFactory,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
@Nullable CmcdConfiguration cmcdConfiguration,
DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
long fallbackTargetLiveOffsetMs,
@ -428,6 +451,7 @@ public final class DashMediaSource extends BaseMediaSource {
this.manifestDataSourceFactory = manifestDataSourceFactory;
this.manifestParser = manifestParser;
this.chunkSourceFactory = chunkSourceFactory;
this.cmcdConfiguration = cmcdConfiguration;
this.drmSessionManager = drmSessionManager;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.fallbackTargetLiveOffsetMs = fallbackTargetLiveOffsetMs;
@ -507,6 +531,7 @@ public final class DashMediaSource extends BaseMediaSource {
periodIndex,
chunkSourceFactory,
mediaTransferListener,
cmcdConfiguration,
drmSessionManager,
drmEventDispatcher,
loadErrorHandlingPolicy,

View File

@ -50,9 +50,12 @@ import androidx.media3.exoplayer.source.chunk.MediaChunk;
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.CmcdLog;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.ChunkIndex;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@ -114,7 +117,8 @@ public class DefaultDashChunkSource implements DashChunkSource {
List<Format> closedCaptionFormats,
@Nullable PlayerTrackEmsgHandler playerEmsgHandler,
@Nullable TransferListener transferListener,
PlayerId playerId) {
PlayerId playerId,
@Nullable CmcdConfiguration cmcdConfiguration) {
DataSource dataSource = dataSourceFactory.createDataSource();
if (transferListener != null) {
dataSource.addTransferListener(transferListener);
@ -134,7 +138,8 @@ public class DefaultDashChunkSource implements DashChunkSource {
enableEventMessageTrack,
closedCaptionFormats,
playerEmsgHandler,
playerId);
playerId,
cmcdConfiguration);
}
}
@ -146,6 +151,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
private final long elapsedRealtimeOffsetMs;
private final int maxSegmentsPerLoad;
@Nullable private final PlayerTrackEmsgHandler playerTrackEmsgHandler;
@Nullable private final CmcdConfiguration cmcdConfiguration;
protected final RepresentationHolder[] representationHolders;
@ -177,6 +183,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
* @param playerTrackEmsgHandler The {@link PlayerTrackEmsgHandler} instance to handle emsg
* messages targeting the player. Maybe null if this is not necessary.
* @param playerId The {@link PlayerId} of the player using this chunk source.
* @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
*/
public DefaultDashChunkSource(
ChunkExtractor.Factory chunkExtractorFactory,
@ -193,7 +200,8 @@ public class DefaultDashChunkSource implements DashChunkSource {
boolean enableEventMessageTrack,
List<Format> closedCaptionFormats,
@Nullable PlayerTrackEmsgHandler playerTrackEmsgHandler,
PlayerId playerId) {
PlayerId playerId,
@Nullable CmcdConfiguration cmcdConfiguration) {
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.manifest = manifest;
this.baseUrlExclusionList = baseUrlExclusionList;
@ -205,6 +213,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;
this.maxSegmentsPerLoad = maxSegmentsPerLoad;
this.playerTrackEmsgHandler = playerTrackEmsgHandler;
this.cmcdConfiguration = cmcdConfiguration;
long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
@ -436,6 +445,13 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
}
@Nullable
CmcdLog cmcdLog =
cmcdConfiguration == null
? null
: CmcdLog.createInstance(
cmcdConfiguration, trackSelection, playbackPositionUs, loadPositionUs);
long seekTimeUs = queue.isEmpty() ? loadPositionUs : C.TIME_UNSET;
out.chunk =
newMediaChunk(
@ -448,7 +464,8 @@ public class DefaultDashChunkSource implements DashChunkSource {
segmentNum,
maxSegmentCount,
seekTimeUs,
nowPeriodTimeUs);
nowPeriodTimeUs,
cmcdLog);
}
@Override
@ -658,10 +675,13 @@ public class DefaultDashChunkSource implements DashChunkSource {
long firstSegmentNum,
int maxSegmentCount,
long seekTimeUs,
long nowPeriodTimeUs) {
long nowPeriodTimeUs,
@Nullable CmcdLog cmcdLog) {
Representation representation = representationHolder.representation;
long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum);
RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum);
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
cmcdLog == null ? ImmutableMap.of() : cmcdLog.getHttpRequestHeaders();
if (representationHolder.chunkExtractor == null) {
long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum);
int flags =
@ -672,6 +692,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
DataSpec dataSpec =
DashUtil.buildDataSpec(
representation, representationHolder.selectedBaseUrl.url, segmentUri, flags);
dataSpec = dataSpec.buildUpon().setHttpRequestHeaders(httpRequestHeaders).build();
return new SingleSampleMediaChunk(
dataSource,
dataSpec,
@ -711,6 +732,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
DataSpec dataSpec =
DashUtil.buildDataSpec(
representation, representationHolder.selectedBaseUrl.url, segmentUri, flags);
dataSpec = dataSpec.buildUpon().setHttpRequestHeaders(httpRequestHeaders).build();
long sampleOffsetUs = -representation.presentationTimeOffsetUs;
return new ContainerMediaChunk(
dataSource,

View File

@ -211,6 +211,7 @@ public final class DashMediaPeriodTest {
periodIndex,
mock(DashChunkSource.Factory.class),
mock(TransferListener.class),
/* cmcdConfiguration= */ null,
DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId),

View File

@ -24,6 +24,7 @@ import android.os.SystemClock;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Util;
@ -39,6 +40,7 @@ import androidx.media3.exoplayer.source.chunk.Chunk;
import androidx.media3.exoplayer.source.chunk.ChunkHolder;
import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection;
import androidx.media3.exoplayer.trackselection.FixedTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter;
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
@ -97,7 +99,8 @@ public class DefaultDashChunkSourceTest {
/* enableEventMessageTrack= */ false,
/* closedCaptionFormats= */ ImmutableList.of(),
/* playerTrackEmsgHandler= */ null,
PlayerId.UNSET);
PlayerId.UNSET,
/* cmcdConfiguration= */ null);
long nowInPeriodUs = Util.msToUs(nowMs - manifest.availabilityStartTimeMs);
ChunkHolder output = new ChunkHolder();
@ -146,7 +149,8 @@ public class DefaultDashChunkSourceTest {
/* enableEventMessageTrack= */ false,
/* closedCaptionFormats= */ ImmutableList.of(),
/* playerTrackEmsgHandler= */ null,
PlayerId.UNSET);
PlayerId.UNSET,
/* cmcdConfiguration= */ null);
ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk(
@ -171,7 +175,8 @@ public class DefaultDashChunkSourceTest {
}
};
List<Chunk> chunks = new ArrayList<>();
DashChunkSource chunkSource = createDashChunkSource(/* numberOfTracks= */ 1);
DashChunkSource chunkSource =
createDashChunkSource(/* numberOfTracks= */ 1, /* cmcdConfiguration= */ null);
ChunkHolder output = new ChunkHolder();
boolean requestReplacementChunk = true;
@ -228,7 +233,8 @@ public class DefaultDashChunkSourceTest {
FALLBACK_TYPE_TRACK, DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_EXCLUSION_MS);
}
};
DashChunkSource chunkSource = createDashChunkSource(/* numberOfTracks= */ 4);
DashChunkSource chunkSource =
createDashChunkSource(/* numberOfTracks= */ 4, /* cmcdConfiguration= */ null);
ChunkHolder output = new ChunkHolder();
List<Chunk> chunks = new ArrayList<>();
boolean requestReplacementChunk = true;
@ -269,7 +275,8 @@ public class DefaultDashChunkSourceTest {
return null;
}
};
DashChunkSource chunkSource = createDashChunkSource(/* numberOfTracks= */ 2);
DashChunkSource chunkSource =
createDashChunkSource(/* numberOfTracks= */ 2, /* cmcdConfiguration= */ null);
ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk(
/* playbackPositionUs= */ 0,
@ -288,7 +295,127 @@ public class DefaultDashChunkSourceTest {
assertThat(requestReplacementChunk).isFalse();
}
private DashChunkSource createDashChunkSource(int numberOfTracks) throws IOException {
@Test
public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdLoggingHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
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(
/* playbackPositionUs= */ 0,
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
output);
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=700",
"CMCD-Request",
"bl=0",
"CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\"");
}
@Test
public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdLoggingHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem -> {
CmcdConfiguration.RequestConfig cmcdRequestConfig =
new CmcdConfiguration.RequestConfig() {
@Override
public boolean isKeyAllowed(String key) {
return !key.equals(CmcdConfiguration.KEY_SESSION_ID);
}
@Override
public int getRequestedMaximumThroughputKbps(int throughputKbps) {
return 5 * throughputKbps;
}
};
return new CmcdConfiguration(
/* sessionId= */ "sessionId",
/* contentId= */ mediaItem.mediaId + "contentIdSuffix",
cmcdRequestConfig);
};
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(
/* playbackPositionUs= */ 0,
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
output);
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=700",
"CMCD-Request",
"bl=0",
"CMCD-Session",
"cid=\"mediaIdcontentIdSuffix\"",
"CMCD-Status",
"rtp=3500");
}
@Test
public void
getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdLoggingHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem -> {
CmcdConfiguration.RequestConfig cmcdRequestConfig =
new CmcdConfiguration.RequestConfig() {
@Override
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> getCustomData() {
return new ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String>()
.put(CmcdConfiguration.KEY_CMCD_OBJECT, "key1=value1")
.put(CmcdConfiguration.KEY_CMCD_REQUEST, "key2=\"stringValue\"")
.put(CmcdConfiguration.KEY_CMCD_SESSION, "key3=1")
.put(CmcdConfiguration.KEY_CMCD_STATUS, "key4=5.0")
.buildOrThrow();
}
};
return new CmcdConfiguration(
/* sessionId= */ "sessionId", /* contentId= */ mediaItem.mediaId, cmcdRequestConfig);
};
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(
/* playbackPositionUs= */ 0,
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
output);
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=700,key1=value1",
"CMCD-Request",
"bl=0,key2=\"stringValue\"",
"CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",key3=1",
"CMCD-Status",
"key4=5.0");
}
private DashChunkSource createDashChunkSource(
int numberOfTracks, @Nullable CmcdConfiguration cmcdConfiguration) throws IOException {
Assertions.checkArgument(numberOfTracks < 6);
DashManifest manifest =
new DashManifestParser()
@ -330,7 +457,8 @@ public class DefaultDashChunkSourceTest {
/* enableEventMessageTrack= */ false,
/* closedCaptionFormats= */ ImmutableList.of(),
/* playerTrackEmsgHandler= */ null,
PlayerId.UNSET);
PlayerId.UNSET,
cmcdConfiguration);
}
private LoadErrorHandlingPolicy.LoadErrorInfo createFakeLoadErrorInfo(

View File

@ -47,6 +47,8 @@ import androidx.media3.exoplayer.source.chunk.MediaChunk;
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.CmcdLog;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
@ -130,6 +132,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable private final List<Format> muxedCaptionFormats;
private final FullSegmentEncryptionKeyCache keyCache;
private final PlayerId playerId;
@Nullable private final CmcdConfiguration cmcdConfiguration;
private boolean isPrimaryTimestampSource;
private byte[] scratchSpace;
@ -170,7 +173,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable TransferListener mediaTransferListener,
TimestampAdjusterProvider timestampAdjusterProvider,
@Nullable List<Format> muxedCaptionFormats,
PlayerId playerId) {
PlayerId playerId,
@Nullable CmcdConfiguration cmcdConfiguration) {
this.extractorFactory = extractorFactory;
this.playlistTracker = playlistTracker;
this.playlistUrls = playlistUrls;
@ -178,6 +182,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.timestampAdjusterProvider = timestampAdjusterProvider;
this.muxedCaptionFormats = muxedCaptionFormats;
this.playerId = playerId;
this.cmcdConfiguration = cmcdConfiguration;
keyCache = new FullSegmentEncryptionKeyCache(KEY_CACHE_SIZE);
scratchSpace = Util.EMPTY_BYTE_ARRAY;
liveEdgeInPeriodTimeUs = C.TIME_UNSET;
@ -493,6 +498,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return;
}
@Nullable
CmcdLog cmcdLog =
cmcdConfiguration == null
? null
: CmcdLog.createInstance(
cmcdConfiguration, trackSelection, playbackPositionUs, loadPositionUs);
out.chunk =
HlsMediaChunk.createInstance(
extractorFactory,
@ -511,7 +523,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri),
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri),
shouldSpliceIn,
playerId);
playerId,
cmcdLog);
}
@Nullable

View File

@ -33,12 +33,15 @@ 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.CmcdLog;
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;
@ -93,15 +96,19 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable byte[] mediaSegmentKey,
@Nullable byte[] initSegmentKey,
boolean shouldSpliceIn,
PlayerId playerId) {
PlayerId playerId,
@Nullable CmcdLog cmcdLog) {
// Media segment.
HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase;
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
cmcdLog == null ? ImmutableMap.of() : cmcdLog.getHttpRequestHeaders();
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();
boolean mediaSegmentEncrypted = mediaSegmentKey != null;
@Nullable

View File

@ -47,6 +47,7 @@ import androidx.media3.exoplayer.source.SequenceableLoader;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.extractor.Extractor;
import com.google.common.primitives.Ints;
@ -69,6 +70,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsPlaylistTracker.Pla
private final HlsPlaylistTracker playlistTracker;
private final HlsDataSourceFactory dataSourceFactory;
@Nullable private final TransferListener mediaTransferListener;
@Nullable private final CmcdConfiguration cmcdConfiguration;
private final DrmSessionManager drmSessionManager;
private final DrmSessionEventListener.EventDispatcher drmEventDispatcher;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
@ -102,6 +104,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsPlaylistTracker.Pla
* and keys.
* @param mediaTransferListener The transfer listener to inform of any media data transfers. May
* be null if no listener is available.
* @param cmcdConfiguration The {@link CmcdConfiguration} for the period.
* @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession
* DrmSessions} with.
* @param drmEventDispatcher A {@link DrmSessionEventListener.EventDispatcher} used to distribute
@ -121,6 +124,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsPlaylistTracker.Pla
HlsPlaylistTracker playlistTracker,
HlsDataSourceFactory dataSourceFactory,
@Nullable TransferListener mediaTransferListener,
@Nullable CmcdConfiguration cmcdConfiguration,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
@ -135,6 +139,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsPlaylistTracker.Pla
this.playlistTracker = playlistTracker;
this.dataSourceFactory = dataSourceFactory;
this.mediaTransferListener = mediaTransferListener;
this.cmcdConfiguration = cmcdConfiguration;
this.drmSessionManager = drmSessionManager;
this.drmEventDispatcher = drmEventDispatcher;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
@ -777,7 +782,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsPlaylistTracker.Pla
mediaTransferListener,
timestampAdjusterProvider,
muxedCaptionFormats,
playerId);
playerId,
cmcdConfiguration);
return new HlsSampleStreamWrapper(
uid,
trackType,

View File

@ -53,6 +53,7 @@ import androidx.media3.exoplayer.source.MediaSourceFactory;
import androidx.media3.exoplayer.source.SequenceableLoader;
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.extractor.Extractor;
@ -105,6 +106,7 @@ public final class HlsMediaSource extends BaseMediaSource
private HlsPlaylistParserFactory playlistParserFactory;
private HlsPlaylistTracker.Factory playlistTrackerFactory;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
@Nullable private CmcdConfiguration.Factory cmcdConfigurationFactory;
private DrmSessionManagerProvider drmSessionManagerProvider;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private boolean allowChunklessPreparation;
@ -300,6 +302,13 @@ public final class HlsMediaSource extends BaseMediaSource
return this;
}
@CanIgnoreReturnValue
@Override
public Factory setCmcdConfigurationFactory(CmcdConfiguration.Factory cmcdConfigurationFactory) {
this.cmcdConfigurationFactory = checkNotNull(cmcdConfigurationFactory);
return this;
}
@CanIgnoreReturnValue
@Override
public Factory setDrmSessionManagerProvider(
@ -344,12 +353,18 @@ public final class HlsMediaSource extends BaseMediaSource
playlistParserFactory =
new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys);
}
@Nullable
CmcdConfiguration cmcdConfiguration =
cmcdConfigurationFactory == null
? null
: cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
return new HlsMediaSource(
mediaItem,
hlsDataSourceFactory,
extractorFactory,
compositeSequenceableLoaderFactory,
cmcdConfiguration,
drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
playlistTrackerFactory.createTracker(
@ -370,6 +385,7 @@ public final class HlsMediaSource extends BaseMediaSource
private final MediaItem.LocalConfiguration localConfiguration;
private final HlsDataSourceFactory dataSourceFactory;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
@Nullable private final CmcdConfiguration cmcdConfiguration;
private final DrmSessionManager drmSessionManager;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final boolean allowChunklessPreparation;
@ -387,6 +403,7 @@ public final class HlsMediaSource extends BaseMediaSource
HlsDataSourceFactory dataSourceFactory,
HlsExtractorFactory extractorFactory,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
@Nullable CmcdConfiguration cmcdConfiguration,
DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
HlsPlaylistTracker playlistTracker,
@ -400,6 +417,7 @@ public final class HlsMediaSource extends BaseMediaSource
this.dataSourceFactory = dataSourceFactory;
this.extractorFactory = extractorFactory;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
this.cmcdConfiguration = cmcdConfiguration;
this.drmSessionManager = drmSessionManager;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.playlistTracker = playlistTracker;
@ -440,6 +458,7 @@ public final class HlsMediaSource extends BaseMediaSource
playlistTracker,
dataSourceFactory,
mediaTransferListener,
cmcdConfiguration,
drmSessionManager,
drmEventDispatcher,
loadErrorHandlingPolicy,

View File

@ -21,19 +21,24 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.exoplayer.SeekParameters;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser;
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistTracker;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.test.utils.ExoPlayerTestRunner;
import androidx.media3.test.utils.FakeDataSource;
import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.io.InputStream;
import org.junit.Before;
@ -63,7 +68,6 @@ public class HlsChunkSourceTest {
.build();
@Mock private HlsPlaylistTracker mockPlaylistTracker;
private HlsChunkSource testChunkSource;
@Before
public void setup() throws IOException {
@ -77,18 +81,6 @@ public class HlsChunkSourceTest {
when(mockPlaylistTracker.getPlaylistSnapshot(eq(PLAYLIST_URI), anyBoolean()))
.thenReturn(playlist);
testChunkSource =
new HlsChunkSource(
HlsExtractorFactory.DEFAULT,
mockPlaylistTracker,
new Uri[] {IFRAME_URI, PLAYLIST_URI},
new Format[] {IFRAME_FORMAT, ExoPlayerTestRunner.VIDEO_FORMAT},
new DefaultHlsDataSourceFactory(new FakeDataSource.Factory()),
/* mediaTransferListener= */ null,
new TimestampAdjusterProvider(),
/* muxedCaptionFormats= */ null,
PlayerId.UNSET);
when(mockPlaylistTracker.isSnapshotValid(eq(PLAYLIST_URI))).thenReturn(true);
// Mock that segments totalling PLAYLIST_START_PERIOD_OFFSET_US in duration have been removed
// from the start of the playlist.
@ -97,7 +89,9 @@ public class HlsChunkSourceTest {
}
@Test
public void getAdjustedSeekPositionUs_previousSync() {
public void getAdjustedSeekPositionUs_previousSync() throws IOException {
HlsChunkSource testChunkSource = createHlsChunkSource(/* cmcdConfiguration= */ null);
long adjustedPositionUs =
testChunkSource.getAdjustedSeekPositionUs(
playlistTimeToPeriodTimeUs(17_000_000), SeekParameters.PREVIOUS_SYNC);
@ -106,7 +100,9 @@ public class HlsChunkSourceTest {
}
@Test
public void getAdjustedSeekPositionUs_nextSync() {
public void getAdjustedSeekPositionUs_nextSync() throws IOException {
HlsChunkSource testChunkSource = createHlsChunkSource(/* cmcdConfiguration= */ null);
long adjustedPositionUs =
testChunkSource.getAdjustedSeekPositionUs(
playlistTimeToPeriodTimeUs(17_000_000), SeekParameters.NEXT_SYNC);
@ -115,7 +111,9 @@ public class HlsChunkSourceTest {
}
@Test
public void getAdjustedSeekPositionUs_nextSyncAtEnd() {
public void getAdjustedSeekPositionUs_nextSyncAtEnd() throws IOException {
HlsChunkSource testChunkSource = createHlsChunkSource(/* cmcdConfiguration= */ null);
long adjustedPositionUs =
testChunkSource.getAdjustedSeekPositionUs(
playlistTimeToPeriodTimeUs(24_000_000), SeekParameters.NEXT_SYNC);
@ -124,7 +122,9 @@ public class HlsChunkSourceTest {
}
@Test
public void getAdjustedSeekPositionUs_closestSyncBefore() {
public void getAdjustedSeekPositionUs_closestSyncBefore() throws IOException {
HlsChunkSource testChunkSource = createHlsChunkSource(/* cmcdConfiguration= */ null);
long adjustedPositionUs =
testChunkSource.getAdjustedSeekPositionUs(
playlistTimeToPeriodTimeUs(17_000_000), SeekParameters.CLOSEST_SYNC);
@ -133,7 +133,9 @@ public class HlsChunkSourceTest {
}
@Test
public void getAdjustedSeekPositionUs_closestSyncAfter() {
public void getAdjustedSeekPositionUs_closestSyncAfter() throws IOException {
HlsChunkSource testChunkSource = createHlsChunkSource(/* cmcdConfiguration= */ null);
long adjustedPositionUs =
testChunkSource.getAdjustedSeekPositionUs(
playlistTimeToPeriodTimeUs(19_000_000), SeekParameters.CLOSEST_SYNC);
@ -142,7 +144,9 @@ public class HlsChunkSourceTest {
}
@Test
public void getAdjustedSeekPositionUs_exact() {
public void getAdjustedSeekPositionUs_exact() throws IOException {
HlsChunkSource testChunkSource = createHlsChunkSource(/* cmcdConfiguration= */ null);
long adjustedPositionUs =
testChunkSource.getAdjustedSeekPositionUs(
playlistTimeToPeriodTimeUs(17_000_000), SeekParameters.EXACT);
@ -152,6 +156,8 @@ public class HlsChunkSourceTest {
@Test
public void getAdjustedSeekPositionUs_noIndependentSegments() throws IOException {
HlsChunkSource testChunkSource = createHlsChunkSource(/* cmcdConfiguration= */ null);
InputStream inputStream =
TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), PLAYLIST);
HlsMediaPlaylist playlist =
@ -168,6 +174,8 @@ public class HlsChunkSourceTest {
@Test
public void getAdjustedSeekPositionUs_emptyPlaylist() throws IOException {
HlsChunkSource testChunkSource = createHlsChunkSource(/* cmcdConfiguration= */ null);
InputStream inputStream =
TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), PLAYLIST_EMPTY);
HlsMediaPlaylist playlist =
@ -182,6 +190,142 @@ public class HlsChunkSourceTest {
assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(100_000_000);
}
@Test
public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdLoggingHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
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(
/* playbackPositionUs= */ 0,
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
/* allowEndOfStream= */ true,
output);
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=800",
"CMCD-Request",
"bl=0",
"CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\"");
}
@Test
public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdLoggingHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem -> {
CmcdConfiguration.RequestConfig cmcdRequestConfig =
new CmcdConfiguration.RequestConfig() {
@Override
public boolean isKeyAllowed(String key) {
return !key.equals(CmcdConfiguration.KEY_SESSION_ID);
}
@Override
public int getRequestedMaximumThroughputKbps(int throughputKbps) {
return 5 * throughputKbps;
}
};
return new CmcdConfiguration(
/* sessionId= */ "sessionId",
/* contentId= */ mediaItem.mediaId + "contentIdSuffix",
cmcdRequestConfig);
};
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(
/* playbackPositionUs= */ 0,
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
/* allowEndOfStream= */ true,
output);
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=800",
"CMCD-Request",
"bl=0",
"CMCD-Session",
"cid=\"mediaIdcontentIdSuffix\"",
"CMCD-Status",
"rtp=4000");
}
@Test
public void
getNextChunk_chunkSourceWithCustomCmcdConfigurationAndCustomData_setsCmcdLoggingHeaders()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory =
mediaItem -> {
CmcdConfiguration.RequestConfig cmcdRequestConfig =
new CmcdConfiguration.RequestConfig() {
@Override
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> getCustomData() {
return new ImmutableMap.Builder<@CmcdConfiguration.HeaderKey String, String>()
.put(CmcdConfiguration.KEY_CMCD_OBJECT, "key1=value1")
.put(CmcdConfiguration.KEY_CMCD_REQUEST, "key2=\"stringValue\"")
.put(CmcdConfiguration.KEY_CMCD_SESSION, "key3=1")
.put(CmcdConfiguration.KEY_CMCD_STATUS, "key4=5.0")
.buildOrThrow();
}
};
return new CmcdConfiguration(
/* sessionId= */ "sessionId", /* contentId= */ mediaItem.mediaId, cmcdRequestConfig);
};
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(
/* playbackPositionUs= */ 0,
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
/* allowEndOfStream= */ true,
output);
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=800,key1=value1",
"CMCD-Request",
"bl=0,key2=\"stringValue\"",
"CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",key3=1",
"CMCD-Status",
"key4=5.0");
}
private HlsChunkSource createHlsChunkSource(@Nullable CmcdConfiguration cmcdConfiguration) {
return new HlsChunkSource(
HlsExtractorFactory.DEFAULT,
mockPlaylistTracker,
new Uri[] {IFRAME_URI, PLAYLIST_URI},
new Format[] {IFRAME_FORMAT, ExoPlayerTestRunner.VIDEO_FORMAT},
new DefaultHlsDataSourceFactory(new FakeDataSource.Factory()),
/* mediaTransferListener= */ null,
new TimestampAdjusterProvider(),
/* muxedCaptionFormats= */ null,
PlayerId.UNSET,
cmcdConfiguration);
}
private static long playlistTimeToPeriodTimeUs(long playlistTimeUs) {
return playlistTimeUs + PLAYLIST_START_PERIOD_OFFSET_US;
}

View File

@ -84,6 +84,7 @@ public final class HlsMediaPeriodTest {
mockPlaylistTracker,
mockDataSourceFactory,
mock(TransferListener.class),
/* cmcdConfiguration= */ null,
mock(DrmSessionManager.class),
new DrmSessionEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId),

View File

@ -39,12 +39,15 @@ import androidx.media3.exoplayer.source.chunk.ContainerMediaChunk;
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.CmcdLog;
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;
@ -66,13 +69,19 @@ public class DefaultSsChunkSource implements SsChunkSource {
SsManifest manifest,
int streamElementIndex,
ExoTrackSelection trackSelection,
@Nullable TransferListener transferListener) {
@Nullable TransferListener transferListener,
@Nullable CmcdConfiguration cmcdConfiguration) {
DataSource dataSource = dataSourceFactory.createDataSource();
if (transferListener != null) {
dataSource.addTransferListener(transferListener);
}
return new DefaultSsChunkSource(
manifestLoaderErrorThrower, manifest, streamElementIndex, trackSelection, dataSource);
manifestLoaderErrorThrower,
manifest,
streamElementIndex,
trackSelection,
dataSource,
cmcdConfiguration);
}
}
@ -80,6 +89,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
private final int streamElementIndex;
private final ChunkExtractor[] chunkExtractors;
private final DataSource dataSource;
@Nullable private final CmcdConfiguration cmcdConfiguration;
private ExoTrackSelection trackSelection;
private SsManifest manifest;
@ -93,18 +103,21 @@ public class DefaultSsChunkSource implements SsChunkSource {
* @param streamElementIndex The index of the stream element in the manifest.
* @param trackSelection The track selection.
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
*/
public DefaultSsChunkSource(
LoaderErrorThrower manifestLoaderErrorThrower,
SsManifest manifest,
int streamElementIndex,
ExoTrackSelection trackSelection,
DataSource dataSource) {
DataSource dataSource,
@Nullable CmcdConfiguration cmcdConfiguration) {
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.manifest = manifest;
this.streamElementIndex = streamElementIndex;
this.trackSelection = trackSelection;
this.dataSource = dataSource;
this.cmcdConfiguration = cmcdConfiguration;
StreamElement streamElement = manifest.streamElements[streamElementIndex];
chunkExtractors = new ChunkExtractor[trackSelection.length()];
@ -267,6 +280,14 @@ public class DefaultSsChunkSource implements SsChunkSource {
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex);
Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex);
@Nullable
CmcdLog cmcdLog =
cmcdConfiguration == null
? null
: CmcdLog.createInstance(
cmcdConfiguration, trackSelection, playbackPositionUs, loadPositionUs);
;
out.chunk =
newMediaChunk(
trackSelection.getSelectedFormat(),
@ -278,7 +299,8 @@ public class DefaultSsChunkSource implements SsChunkSource {
chunkSeekTimeUs,
trackSelection.getSelectionReason(),
trackSelection.getSelectionData(),
chunkExtractor);
chunkExtractor,
cmcdLog);
}
@Override
@ -322,8 +344,12 @@ public class DefaultSsChunkSource implements SsChunkSource {
long chunkSeekTimeUs,
@C.SelectionReason int trackSelectionReason,
@Nullable Object trackSelectionData,
ChunkExtractor chunkExtractor) {
DataSpec dataSpec = new DataSpec(uri);
ChunkExtractor chunkExtractor,
@Nullable CmcdLog cmcdLog) {
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
cmcdLog == null ? ImmutableMap.of() : cmcdLog.getHttpRequestHeaders();
DataSpec dataSpec =
new DataSpec.Builder().setUri(uri).setHttpRequestHeaders(httpRequestHeaders).build();
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
// To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs.
long sampleOffsetUs = chunkStartTimeUs;

View File

@ -21,6 +21,7 @@ import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest;
import androidx.media3.exoplayer.source.chunk.ChunkSource;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
/** A {@link ChunkSource} for SmoothStreaming. */
@ -39,6 +40,7 @@ public interface SsChunkSource extends ChunkSource {
* @param trackSelection The track selection.
* @param transferListener The transfer listener which should be informed of any data transfers.
* May be null if no listener is available.
* @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
* @return The created {@link SsChunkSource}.
*/
SsChunkSource createChunkSource(
@ -46,7 +48,8 @@ public interface SsChunkSource extends ChunkSource {
SsManifest manifest,
int streamElementIndex,
ExoTrackSelection trackSelection,
@Nullable TransferListener transferListener);
@Nullable TransferListener transferListener,
@Nullable CmcdConfiguration cmcdConfiguration);
}
/**

View File

@ -34,6 +34,7 @@ import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.source.chunk.ChunkSampleStream;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import java.io.IOException;
@ -49,6 +50,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Nullable private final TransferListener transferListener;
private final LoaderErrorThrower manifestLoaderErrorThrower;
private final DrmSessionManager drmSessionManager;
@Nullable private final CmcdConfiguration cmcdConfiguration;
private final DrmSessionEventListener.EventDispatcher drmEventDispatcher;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher;
@ -66,6 +68,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
SsChunkSource.Factory chunkSourceFactory,
@Nullable TransferListener transferListener,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
@Nullable CmcdConfiguration cmcdConfiguration,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
@ -76,6 +79,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
this.chunkSourceFactory = chunkSourceFactory;
this.transferListener = transferListener;
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.cmcdConfiguration = cmcdConfiguration;
this.drmSessionManager = drmSessionManager;
this.drmEventDispatcher = drmEventDispatcher;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
@ -237,7 +241,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int streamElementIndex = trackGroups.indexOf(selection.getTrackGroup());
SsChunkSource chunkSource =
chunkSourceFactory.createChunkSource(
manifestLoaderErrorThrower, manifest, streamElementIndex, selection, transferListener);
manifestLoaderErrorThrower,
manifest,
streamElementIndex,
selection,
transferListener,
cmcdConfiguration);
return new ChunkSampleStream<>(
manifest.streamElements[streamElementIndex].type,
null,

View File

@ -56,6 +56,7 @@ import androidx.media3.exoplayer.source.MediaSourceFactory;
import androidx.media3.exoplayer.source.SequenceableLoader;
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
@ -86,6 +87,7 @@ public final class SsMediaSource extends BaseMediaSource
@Nullable private final DataSource.Factory manifestDataSourceFactory;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
@Nullable private CmcdConfiguration.Factory cmcdConfigurationFactory;
private DrmSessionManagerProvider drmSessionManagerProvider;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long livePresentationDelayMs;
@ -198,6 +200,13 @@ public final class SsMediaSource extends BaseMediaSource
return this;
}
@CanIgnoreReturnValue
@Override
public Factory setCmcdConfigurationFactory(CmcdConfiguration.Factory cmcdConfigurationFactory) {
this.cmcdConfigurationFactory = checkNotNull(cmcdConfigurationFactory);
return this;
}
@CanIgnoreReturnValue
@Override
public Factory setDrmSessionManagerProvider(
@ -248,6 +257,11 @@ public final class SsMediaSource extends BaseMediaSource
.setMimeType(MimeTypes.APPLICATION_SS)
.setUri(hasUri ? mediaItem.localConfiguration.uri : Uri.EMPTY)
.build();
@Nullable
CmcdConfiguration cmcdConfiguration =
cmcdConfigurationFactory == null
? null
: cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
return new SsMediaSource(
mediaItem,
manifest,
@ -255,6 +269,7 @@ public final class SsMediaSource extends BaseMediaSource
/* manifestParser= */ null,
chunkSourceFactory,
compositeSequenceableLoaderFactory,
cmcdConfiguration,
drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
livePresentationDelayMs);
@ -278,6 +293,11 @@ public final class SsMediaSource extends BaseMediaSource
if (!streamKeys.isEmpty()) {
manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys);
}
@Nullable
CmcdConfiguration cmcdConfiguration =
cmcdConfigurationFactory == null
? null
: cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
return new SsMediaSource(
mediaItem,
@ -286,6 +306,7 @@ public final class SsMediaSource extends BaseMediaSource
manifestParser,
chunkSourceFactory,
compositeSequenceableLoaderFactory,
cmcdConfiguration,
drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
livePresentationDelayMs);
@ -317,6 +338,7 @@ public final class SsMediaSource extends BaseMediaSource
private final DataSource.Factory manifestDataSourceFactory;
private final SsChunkSource.Factory chunkSourceFactory;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
@Nullable private final CmcdConfiguration cmcdConfiguration;
private final DrmSessionManager drmSessionManager;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final long livePresentationDelayMs;
@ -341,6 +363,7 @@ public final class SsMediaSource extends BaseMediaSource
@Nullable ParsingLoadable.Parser<? extends SsManifest> manifestParser,
SsChunkSource.Factory chunkSourceFactory,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
@Nullable CmcdConfiguration cmcdConfiguration,
DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
long livePresentationDelayMs) {
@ -356,6 +379,7 @@ public final class SsMediaSource extends BaseMediaSource
this.manifestParser = manifestParser;
this.chunkSourceFactory = chunkSourceFactory;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
this.cmcdConfiguration = cmcdConfiguration;
this.drmSessionManager = drmSessionManager;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.livePresentationDelayMs = livePresentationDelayMs;
@ -403,6 +427,7 @@ public final class SsMediaSource extends BaseMediaSource
chunkSourceFactory,
mediaTransferListener,
compositeSequenceableLoaderFactory,
cmcdConfiguration,
drmSessionManager,
drmEventDispatcher,
loadErrorHandlingPolicy,

View File

@ -67,6 +67,7 @@ public class SsMediaPeriodTest {
mock(SsChunkSource.Factory.class),
mock(TransferListener.class),
mock(CompositeSequenceableLoaderFactory.class),
/* cmcdConfiguration= */ null,
mock(DrmSessionManager.class),
new DrmSessionEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId),