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:
parent
a67ce066df
commit
b55ddf12b4
@ -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)).
|
||||
|
@ -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()) {
|
||||
|
@ -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}.
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
Loading…
x
Reference in New Issue
Block a user