Simplify and accurately compute chunk duration

Refactored `CmcdLog` to `CmcdHeadersFactory` for improved representation of its purpose and updated implementations.

#minor-change

PiperOrigin-RevId: 552831995
This commit is contained in:
rohks 2023-08-01 16:48:19 +00:00 committed by Tianyi Feng
parent 6633b05c0a
commit 11648e6c8e
8 changed files with 136 additions and 128 deletions

View File

@ -35,15 +35,15 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
/** /**
* Represents the data for CMCD (Common Media Client Data) in adaptive streaming formats DASH, HLS, * This class serves as a factory for generating Common Media Client Data (CMCD) HTTP request
* and SmoothStreaming. * headers in adaptive streaming formats, DASH, HLS, and SmoothStreaming.
* *
* <p>It holds various attributes related to the playback of media content according to the * <p>It encapsulates the necessary attributes and information relevant to media content playback,
* specifications outlined in the CMCD standard document <a * following the guidelines specified in the CMCD standard document <a
* href="https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf">CTA-5004</a>. * href="https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf">CTA-5004</a>.
*/ */
@UnstableApi @UnstableApi
public final class CmcdLog { public final class CmcdHeadersFactory {
/** Indicates the streaming format used for media content. */ /** Indicates the streaming format used for media content. */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@ -74,34 +74,62 @@ public final class CmcdLog {
/** Represents the Live Streaming stream type. */ /** Represents the Live Streaming stream type. */
public static final String STREAM_TYPE_LIVE = "l"; public static final String STREAM_TYPE_LIVE = "l";
private final CmcdConfiguration cmcdConfiguration;
private final ExoTrackSelection trackSelection;
private final long bufferedDurationUs;
private final @StreamingFormat String streamingFormat;
private final boolean isLive;
private long chunkDurationUs;
/** /**
* Creates a new instance. * Creates an instance.
* *
* @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source. * @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
* @param trackSelection The {@linkplain ExoTrackSelection track selection}. * @param trackSelection The {@linkplain ExoTrackSelection track selection}.
* @param bufferedDurationUs The duration of media currently buffered from the current playback * @param bufferedDurationUs The duration of media currently buffered from the current playback
* position, in microseconds. * position, in microseconds.
* @param chunkDurationUs The duration of current media chunk being requested, in microseconds. If
* the duration is not known, it can be set to {@link C#TIME_UNSET}.
* @param streamingFormat The streaming format of the media content. Must be one of the allowed * @param streamingFormat The streaming format of the media content. Must be one of the allowed
* streaming formats specified by the {@link StreamingFormat} annotation. * streaming formats specified by the {@link StreamingFormat} annotation.
* @param isLive {@code true} if the media content is being streamed live, {@code false} * @param isLive {@code true} if the media content is being streamed live, {@code false}
* otherwise. * otherwise.
* @throws IllegalArgumentException If {@code bufferedDurationUs} is negative.
*/ */
public static CmcdLog createInstance( public CmcdHeadersFactory(
CmcdConfiguration cmcdConfiguration, CmcdConfiguration cmcdConfiguration,
ExoTrackSelection trackSelection, ExoTrackSelection trackSelection,
long bufferedDurationUs, long bufferedDurationUs,
long chunkDurationUs,
@StreamingFormat String streamingFormat, @StreamingFormat String streamingFormat,
boolean isLive) { boolean isLive) {
checkArgument(bufferedDurationUs >= 0);
this.cmcdConfiguration = cmcdConfiguration;
this.trackSelection = trackSelection;
this.bufferedDurationUs = bufferedDurationUs;
this.streamingFormat = streamingFormat;
this.isLive = isLive;
this.chunkDurationUs = C.TIME_UNSET;
}
/**
* Sets the duration of current media chunk being requested, in microseconds. The default value is
* {@link C#TIME_UNSET}.
*
* @throws IllegalArgumentException If {@code chunkDurationUs} is negative.
*/
@CanIgnoreReturnValue
public CmcdHeadersFactory setChunkDurationUs(long chunkDurationUs) {
checkArgument(chunkDurationUs >= 0);
this.chunkDurationUs = chunkDurationUs;
return this;
}
/** Creates and returns a new {@link ImmutableMap} containing the CMCD HTTP request headers. */
public ImmutableMap<@CmcdConfiguration.HeaderKey String, String> createHttpRequestHeaders() {
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> customData = ImmutableMap<@CmcdConfiguration.HeaderKey String, String> customData =
cmcdConfiguration.requestConfig.getCustomData(); cmcdConfiguration.requestConfig.getCustomData();
int bitrateKbps = trackSelection.getSelectedFormat().bitrate / 1000; int bitrateKbps = Util.ceilDivide(trackSelection.getSelectedFormat().bitrate, 1000);
CmcdLog.CmcdObject.Builder cmcdObject = CmcdObject.Builder cmcdObject =
new CmcdLog.CmcdObject.Builder() new CmcdObject.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_OBJECT));
.setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_OBJECT));
if (cmcdConfiguration.isBitrateLoggingAllowed()) { if (cmcdConfiguration.isBitrateLoggingAllowed()) {
cmcdObject.setBitrateKbps(bitrateKbps); cmcdObject.setBitrateKbps(bitrateKbps);
} }
@ -111,26 +139,25 @@ public final class CmcdLog {
for (int i = 0; i < trackGroup.length; i++) { for (int i = 0; i < trackGroup.length; i++) {
topBitrate = max(topBitrate, trackGroup.getFormat(i).bitrate); topBitrate = max(topBitrate, trackGroup.getFormat(i).bitrate);
} }
cmcdObject.setTopBitrateKbps(topBitrate / 1000); cmcdObject.setTopBitrateKbps(Util.ceilDivide(topBitrate, 1000));
} }
if (cmcdConfiguration.isObjectDurationLoggingAllowed() && chunkDurationUs != C.TIME_UNSET) { if (cmcdConfiguration.isObjectDurationLoggingAllowed() && chunkDurationUs != C.TIME_UNSET) {
cmcdObject.setObjectDurationMs(chunkDurationUs / 1000); cmcdObject.setObjectDurationMs(chunkDurationUs / 1000);
} }
CmcdLog.CmcdRequest.Builder cmcdRequest = CmcdRequest.Builder cmcdRequest =
new CmcdLog.CmcdRequest.Builder() new CmcdRequest.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_REQUEST));
.setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_REQUEST));
if (cmcdConfiguration.isBufferLengthLoggingAllowed()) { if (cmcdConfiguration.isBufferLengthLoggingAllowed()) {
cmcdRequest.setBufferLengthMs(bufferedDurationUs / 1000); cmcdRequest.setBufferLengthMs(bufferedDurationUs / 1000);
} }
if (cmcdConfiguration.isMeasuredThroughputLoggingAllowed() if (cmcdConfiguration.isMeasuredThroughputLoggingAllowed()
&& trackSelection.getLatestBitrateEstimate() != Long.MIN_VALUE) { && trackSelection.getLatestBitrateEstimate() != Long.MIN_VALUE) {
cmcdRequest.setMeasuredThroughputInKbps(trackSelection.getLatestBitrateEstimate() / 1000); cmcdRequest.setMeasuredThroughputInKbps(
Util.ceilDivide(trackSelection.getLatestBitrateEstimate(), 1000));
} }
CmcdLog.CmcdSession.Builder cmcdSession = CmcdSession.Builder cmcdSession =
new CmcdLog.CmcdSession.Builder() new CmcdSession.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_SESSION));
.setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_SESSION));
if (cmcdConfiguration.isContentIdLoggingAllowed()) { if (cmcdConfiguration.isContentIdLoggingAllowed()) {
cmcdSession.setContentId(cmcdConfiguration.contentId); cmcdSession.setContentId(cmcdConfiguration.contentId);
} }
@ -144,40 +171,18 @@ public final class CmcdLog {
cmcdSession.setStreamType(isLive ? STREAM_TYPE_LIVE : STREAM_TYPE_VOD); cmcdSession.setStreamType(isLive ? STREAM_TYPE_LIVE : STREAM_TYPE_VOD);
} }
CmcdLog.CmcdStatus.Builder cmcdStatus = CmcdStatus.Builder cmcdStatus =
new CmcdLog.CmcdStatus.Builder() new CmcdStatus.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_STATUS));
.setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_STATUS));
if (cmcdConfiguration.isMaximumRequestThroughputLoggingAllowed()) { if (cmcdConfiguration.isMaximumRequestThroughputLoggingAllowed()) {
cmcdStatus.setMaximumRequestedThroughputKbps( cmcdStatus.setMaximumRequestedThroughputKbps(
cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps)); 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(); ImmutableMap.Builder<String, String> httpRequestHeaders = ImmutableMap.builder();
this.cmcdObject.populateHttpRequestHeaders(httpRequestHeaders); cmcdObject.build().populateHttpRequestHeaders(httpRequestHeaders);
this.cmcdRequest.populateHttpRequestHeaders(httpRequestHeaders); cmcdRequest.build().populateHttpRequestHeaders(httpRequestHeaders);
this.cmcdSession.populateHttpRequestHeaders(httpRequestHeaders); cmcdSession.build().populateHttpRequestHeaders(httpRequestHeaders);
this.cmcdStatus.populateHttpRequestHeaders(httpRequestHeaders); cmcdStatus.build().populateHttpRequestHeaders(httpRequestHeaders);
return httpRequestHeaders.buildOrThrow(); return httpRequestHeaders.buildOrThrow();
} }
@ -492,7 +497,7 @@ public final class CmcdLog {
/** Sets the {@link CmcdSession#customData}. The default value is {@code null}. */ /** Sets the {@link CmcdSession#customData}. The default value is {@code null}. */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public CmcdSession.Builder setCustomData(@Nullable String customData) { public Builder setCustomData(@Nullable String customData) {
this.customData = customData; this.customData = customData;
return this; return this;
} }
@ -635,7 +640,7 @@ public final class CmcdLog {
/** Sets the {@link CmcdStatus#customData}. The default value is {@code null}. */ /** Sets the {@link CmcdStatus#customData}. The default value is {@code null}. */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public CmcdStatus.Builder setCustomData(@Nullable String customData) { public Builder setCustomData(@Nullable String customData) {
this.customData = customData; this.customData = customData;
return this; return this;
} }

View File

@ -28,9 +28,9 @@ import com.google.common.collect.ImmutableMap;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** Tests for {@link CmcdLog}. */ /** Tests for {@link CmcdHeadersFactory}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class CmcdLogTest { public class CmcdHeadersFactoryTest {
@Test @Test
public void createInstance_populatesCmcdHeaders() { public void createInstance_populatesCmcdHeaders() {
@ -62,17 +62,16 @@ public class CmcdLogTest {
when(trackSelection.getTrackGroup()) when(trackSelection.getTrackGroup())
.thenReturn(new TrackGroup(format, new Format.Builder().setPeakBitrate(1_000_000).build())); .thenReturn(new TrackGroup(format, new Format.Builder().setPeakBitrate(1_000_000).build()));
when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L); when(trackSelection.getLatestBitrateEstimate()).thenReturn(500_000L);
CmcdLog cmcdLog =
CmcdLog.createInstance(
cmcdConfiguration,
trackSelection,
/* bufferedDurationUs= */ 1_760_000,
/* chunkDurationUs= */ 3_000_000,
CmcdLog.STREAMING_FORMAT_DASH,
true);
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> requestHeaders = ImmutableMap<@CmcdConfiguration.HeaderKey String, String> requestHeaders =
cmcdLog.getHttpRequestHeaders(); new CmcdHeadersFactory(
cmcdConfiguration,
trackSelection,
/* bufferedDurationUs= */ 1_760_000,
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH,
/* isLive= */ true)
.setChunkDurationUs(3_000_000)
.createHttpRequestHeaders();
assertThat(requestHeaders) assertThat(requestHeaders)
.containsExactly( .containsExactly(

View File

@ -51,7 +51,7 @@ import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
import androidx.media3.exoplayer.source.chunk.SingleSampleMediaChunk; import androidx.media3.exoplayer.source.chunk.SingleSampleMediaChunk;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.CmcdLog; import androidx.media3.exoplayer.upstream.CmcdHeadersFactory;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower; import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.ChunkIndex; import androidx.media3.extractor.ChunkIndex;
@ -371,24 +371,17 @@ public class DefaultDashChunkSource implements DashChunkSource {
playbackPositionUs, bufferedDurationUs, availableLiveDurationUs, queue, chunkIterators); playbackPositionUs, bufferedDurationUs, availableLiveDurationUs, queue, chunkIterators);
int selectedTrackIndex = trackSelection.getSelectedIndex(); int selectedTrackIndex = trackSelection.getSelectedIndex();
long chunkDurationUs = C.TIME_UNSET;
if (selectedTrackIndex < chunkIterators.length && chunkIterators[selectedTrackIndex].next()) {
chunkDurationUs =
chunkIterators[selectedTrackIndex].getChunkEndTimeUs()
- chunkIterators[selectedTrackIndex].getChunkStartTimeUs();
}
@Nullable @Nullable
CmcdLog cmcdLog = CmcdHeadersFactory cmcdHeadersFactory =
cmcdConfiguration == null cmcdConfiguration == null
? null ? null
: CmcdLog.createInstance( : new CmcdHeadersFactory(
cmcdConfiguration, cmcdConfiguration,
trackSelection, trackSelection,
bufferedDurationUs, bufferedDurationUs,
chunkDurationUs, /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH,
CmcdLog.STREAMING_FORMAT_DASH, /* isLive= */ manifest.dynamic);
manifest.dynamic);
RepresentationHolder representationHolder = updateSelectedBaseUrl(selectedTrackIndex); RepresentationHolder representationHolder = updateSelectedBaseUrl(selectedTrackIndex);
if (representationHolder.chunkExtractor != null) { if (representationHolder.chunkExtractor != null) {
Representation selectedRepresentation = representationHolder.representation; Representation selectedRepresentation = representationHolder.representation;
@ -411,7 +404,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
trackSelection.getSelectionData(), trackSelection.getSelectionData(),
pendingInitializationUri, pendingInitializationUri,
pendingIndexUri, pendingIndexUri,
cmcdLog); cmcdHeadersFactory);
return; return;
} }
} }
@ -477,7 +470,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
maxSegmentCount, maxSegmentCount,
seekTimeUs, seekTimeUs,
nowPeriodTimeUs, nowPeriodTimeUs,
cmcdLog); cmcdHeadersFactory);
} }
@Override @Override
@ -652,7 +645,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
@Nullable Object trackSelectionData, @Nullable Object trackSelectionData,
@Nullable RangedUri initializationUri, @Nullable RangedUri initializationUri,
@Nullable RangedUri indexUri, @Nullable RangedUri indexUri,
@Nullable CmcdLog cmcdLog) { @Nullable CmcdHeadersFactory cmcdHeadersFactory) {
Representation representation = representationHolder.representation; Representation representation = representationHolder.representation;
@Nullable RangedUri requestUri; @Nullable RangedUri requestUri;
if (initializationUri != null) { if (initializationUri != null) {
@ -667,7 +660,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
requestUri = indexUri; requestUri = indexUri;
} }
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders = ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
cmcdLog == null ? ImmutableMap.of() : cmcdLog.getHttpRequestHeaders(); cmcdHeadersFactory == null
? ImmutableMap.of()
: cmcdHeadersFactory.createHttpRequestHeaders();
DataSpec dataSpec = DataSpec dataSpec =
DashUtil.buildDataSpec( DashUtil.buildDataSpec(
representation, representation,
@ -695,12 +690,10 @@ public class DefaultDashChunkSource implements DashChunkSource {
int maxSegmentCount, int maxSegmentCount,
long seekTimeUs, long seekTimeUs,
long nowPeriodTimeUs, long nowPeriodTimeUs,
@Nullable CmcdLog cmcdLog) { @Nullable CmcdHeadersFactory cmcdHeadersFactory) {
Representation representation = representationHolder.representation; Representation representation = representationHolder.representation;
long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum);
RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum); RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum);
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
cmcdLog == null ? ImmutableMap.of() : cmcdLog.getHttpRequestHeaders();
if (representationHolder.chunkExtractor == null) { if (representationHolder.chunkExtractor == null) {
long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum); long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum);
int flags = int flags =
@ -708,6 +701,12 @@ public class DefaultDashChunkSource implements DashChunkSource {
firstSegmentNum, nowPeriodTimeUs) firstSegmentNum, nowPeriodTimeUs)
? 0 ? 0
: DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED; : DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED;
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
cmcdHeadersFactory == null
? ImmutableMap.of()
: cmcdHeadersFactory
.setChunkDurationUs(endTimeUs - startTimeUs)
.createHttpRequestHeaders();
DataSpec dataSpec = DataSpec dataSpec =
DashUtil.buildDataSpec( DashUtil.buildDataSpec(
representation, representation,
@ -751,6 +750,12 @@ public class DefaultDashChunkSource implements DashChunkSource {
representationHolder.isSegmentAvailableAtFullNetworkSpeed(segmentNum, nowPeriodTimeUs) representationHolder.isSegmentAvailableAtFullNetworkSpeed(segmentNum, nowPeriodTimeUs)
? 0 ? 0
: DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED; : DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED;
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
cmcdHeadersFactory == null
? ImmutableMap.of()
: cmcdHeadersFactory
.setChunkDurationUs(endTimeUs - startTimeUs)
.createHttpRequestHeaders();
DataSpec dataSpec = DataSpec dataSpec =
DashUtil.buildDataSpec( DashUtil.buildDataSpec(
representation, representation,

View File

@ -48,7 +48,7 @@ import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
import androidx.media3.exoplayer.trackselection.BaseTrackSelection; import androidx.media3.exoplayer.trackselection.BaseTrackSelection;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.CmcdLog; import androidx.media3.exoplayer.upstream.CmcdHeadersFactory;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@ -481,36 +481,30 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
seenExpectedPlaylistError = false; seenExpectedPlaylistError = false;
expectedPlaylistUrl = null; expectedPlaylistUrl = null;
long chunkDurationUs = C.TIME_UNSET;
if (selectedTrackIndex < mediaChunkIterators.length
&& mediaChunkIterators[selectedTrackIndex].next()) {
chunkDurationUs =
mediaChunkIterators[selectedTrackIndex].getChunkEndTimeUs()
- mediaChunkIterators[selectedTrackIndex].getChunkStartTimeUs();
}
@Nullable @Nullable
CmcdLog cmcdLog = CmcdHeadersFactory cmcdHeadersFactory =
cmcdConfiguration == null cmcdConfiguration == null
? null ? null
: CmcdLog.createInstance( : new CmcdHeadersFactory(
cmcdConfiguration, cmcdConfiguration,
trackSelection, trackSelection,
bufferedDurationUs, bufferedDurationUs,
chunkDurationUs, /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_HLS,
CmcdLog.STREAMING_FORMAT_HLS, /* isLive= */ !playlist.hasEndTag);
!playlist.hasEndTag);
// Check if the media segment or its initialization segment are fully encrypted. // Check if the media segment or its initialization segment are fully encrypted.
@Nullable @Nullable
Uri initSegmentKeyUri = Uri initSegmentKeyUri =
getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase.initializationSegment); getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase.initializationSegment);
out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex, cmcdLog); out.chunk =
maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex, cmcdHeadersFactory);
if (out.chunk != null) { if (out.chunk != null) {
return; return;
} }
@Nullable @Nullable
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase); Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase);
out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex, cmcdLog); out.chunk =
maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex, cmcdHeadersFactory);
if (out.chunk != null) { if (out.chunk != null) {
return; return;
} }
@ -546,7 +540,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri), /* initSegmentKey= */ keyCache.get(initSegmentKeyUri),
shouldSpliceIn, shouldSpliceIn,
playerId, playerId,
cmcdLog); cmcdHeadersFactory);
} }
@Nullable @Nullable
@ -854,7 +848,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable @Nullable
private Chunk maybeCreateEncryptionChunkFor( private Chunk maybeCreateEncryptionChunkFor(
@Nullable Uri keyUri, int selectedTrackIndex, @Nullable CmcdLog cmcdLog) { @Nullable Uri keyUri,
int selectedTrackIndex,
@Nullable CmcdHeadersFactory cmcdHeadersFactory) {
if (keyUri == null) { if (keyUri == null) {
return null; return null;
} }
@ -868,7 +864,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return null; return null;
} }
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders = ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
cmcdLog == null ? ImmutableMap.of() : cmcdLog.getHttpRequestHeaders(); cmcdHeadersFactory == null
? ImmutableMap.of()
: cmcdHeadersFactory.createHttpRequestHeaders();
DataSpec dataSpec = DataSpec dataSpec =
new DataSpec.Builder() new DataSpec.Builder()
.setUri(keyUri) .setUri(keyUri)

View File

@ -34,7 +34,7 @@ import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
import androidx.media3.exoplayer.source.chunk.MediaChunk; import androidx.media3.exoplayer.source.chunk.MediaChunk;
import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.CmcdLog; import androidx.media3.exoplayer.upstream.CmcdHeadersFactory;
import androidx.media3.extractor.DefaultExtractorInput; import androidx.media3.extractor.DefaultExtractorInput;
import androidx.media3.extractor.ExtractorInput; import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.metadata.id3.Id3Decoder; import androidx.media3.extractor.metadata.id3.Id3Decoder;
@ -82,6 +82,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null * @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null
* otherwise. * otherwise.
* @param shouldSpliceIn Whether samples for this chunk should be spliced into existing samples. * @param shouldSpliceIn Whether samples for this chunk should be spliced into existing samples.
* @param cmcdHeadersFactory The {@link CmcdHeadersFactory} for generating CMCD request headers.
*/ */
public static HlsMediaChunk createInstance( public static HlsMediaChunk createInstance(
HlsExtractorFactory extractorFactory, HlsExtractorFactory extractorFactory,
@ -102,11 +103,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable byte[] initSegmentKey, @Nullable byte[] initSegmentKey,
boolean shouldSpliceIn, boolean shouldSpliceIn,
PlayerId playerId, PlayerId playerId,
@Nullable CmcdLog cmcdLog) { @Nullable CmcdHeadersFactory cmcdHeadersFactory) {
// Media segment. // Media segment.
HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase; HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase;
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders = ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
cmcdLog == null ? ImmutableMap.of() : cmcdLog.getHttpRequestHeaders(); cmcdHeadersFactory == null
? ImmutableMap.of()
: cmcdHeadersFactory
.setChunkDurationUs(mediaSegment.durationUs)
.createHttpRequestHeaders();
DataSpec dataSpec = DataSpec dataSpec =
new DataSpec.Builder() new DataSpec.Builder()
.setUri(UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url)) .setUri(UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url))

View File

@ -210,7 +210,7 @@ public class HlsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders) assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly( .containsExactly(
"CMCD-Object", "CMCD-Object",
"br=800,tb=800", "br=800,tb=800,d=4000",
"CMCD-Request", "CMCD-Request",
"bl=0", "bl=0",
"CMCD-Session", "CMCD-Session",
@ -256,7 +256,7 @@ public class HlsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders) assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly( .containsExactly(
"CMCD-Object", "CMCD-Object",
"br=800,tb=800", "br=800,tb=800,d=4000",
"CMCD-Request", "CMCD-Request",
"bl=0", "bl=0",
"CMCD-Session", "CMCD-Session",
@ -303,7 +303,7 @@ public class HlsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders) assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly( .containsExactly(
"CMCD-Object", "CMCD-Object",
"br=800,tb=800,key1=value1", "br=800,tb=800,d=4000,key1=value1",
"CMCD-Request", "CMCD-Request",
"bl=0,key2=\"stringValue\"", "bl=0,key2=\"stringValue\"",
"CMCD-Session", "CMCD-Session",

View File

@ -40,7 +40,7 @@ import androidx.media3.exoplayer.source.chunk.MediaChunk;
import androidx.media3.exoplayer.source.chunk.MediaChunkIterator; import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.CmcdLog; import androidx.media3.exoplayer.upstream.CmcdHeadersFactory;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.FallbackSelection; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.FallbackSelection;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower; import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
@ -280,23 +280,17 @@ public class DefaultSsChunkSource implements SsChunkSource {
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex); int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex);
Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex);
long chunkDurationUs = C.TIME_UNSET;
if (trackSelectionIndex < chunkIterators.length && chunkIterators[trackSelectionIndex].next()) {
chunkDurationUs =
chunkIterators[trackSelectionIndex].getChunkEndTimeUs()
- chunkIterators[trackSelectionIndex].getChunkStartTimeUs();
}
@Nullable @Nullable
CmcdLog cmcdLog = CmcdHeadersFactory cmcdHeadersFactory =
cmcdConfiguration == null cmcdConfiguration == null
? null ? null
: CmcdLog.createInstance( : new CmcdHeadersFactory(
cmcdConfiguration, cmcdConfiguration,
trackSelection, trackSelection,
bufferedDurationUs, bufferedDurationUs,
chunkDurationUs, /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_SS,
CmcdLog.STREAMING_FORMAT_SS, /* isLive= */ manifest.isLive)
manifest.isLive); .setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs);
out.chunk = out.chunk =
newMediaChunk( newMediaChunk(
@ -310,7 +304,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
trackSelection.getSelectionReason(), trackSelection.getSelectionReason(),
trackSelection.getSelectionData(), trackSelection.getSelectionData(),
chunkExtractor, chunkExtractor,
cmcdLog); cmcdHeadersFactory);
} }
@Override @Override
@ -355,9 +349,11 @@ public class DefaultSsChunkSource implements SsChunkSource {
@C.SelectionReason int trackSelectionReason, @C.SelectionReason int trackSelectionReason,
@Nullable Object trackSelectionData, @Nullable Object trackSelectionData,
ChunkExtractor chunkExtractor, ChunkExtractor chunkExtractor,
@Nullable CmcdLog cmcdLog) { @Nullable CmcdHeadersFactory cmcdHeadersFactory) {
ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders = ImmutableMap<@CmcdConfiguration.HeaderKey String, String> httpRequestHeaders =
cmcdLog == null ? ImmutableMap.of() : cmcdLog.getHttpRequestHeaders(); cmcdHeadersFactory == null
? ImmutableMap.of()
: cmcdHeadersFactory.createHttpRequestHeaders();
DataSpec dataSpec = DataSpec dataSpec =
new DataSpec.Builder().setUri(uri).setHttpRequestHeaders(httpRequestHeaders).build(); new DataSpec.Builder().setUri(uri).setHttpRequestHeaders(httpRequestHeaders).build();
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.

View File

@ -64,7 +64,7 @@ public class DefaultSsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders) assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly( .containsExactly(
"CMCD-Object", "CMCD-Object",
"br=307,tb=1536,d=1968", "br=308,tb=1536,d=1968",
"CMCD-Request", "CMCD-Request",
"bl=0,mtp=1000", "bl=0,mtp=1000",
"CMCD-Session", "CMCD-Session",
@ -109,7 +109,7 @@ public class DefaultSsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders) assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly( .containsExactly(
"CMCD-Object", "CMCD-Object",
"br=307,tb=1536,d=1968", "br=308,tb=1536,d=1968",
"CMCD-Request", "CMCD-Request",
"bl=0,mtp=1000", "bl=0,mtp=1000",
"CMCD-Session", "CMCD-Session",
@ -155,7 +155,7 @@ public class DefaultSsChunkSourceTest {
assertThat(output.chunk.dataSpec.httpRequestHeaders) assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly( .containsExactly(
"CMCD-Object", "CMCD-Object",
"br=307,tb=1536,d=1968,key1=value1", "br=308,tb=1536,d=1968,key1=value1",
"CMCD-Request", "CMCD-Request",
"bl=0,mtp=1000,key2=\"stringValue\"", "bl=0,mtp=1000,key2=\"stringValue\"",
"CMCD-Session", "CMCD-Session",