Add buffer starvation(bs), deadline(dl), playback rate(pr), startup(su)

Enhanced the Common Media Client Data (CMCD) logging by incorporating additional fields:
* buffer starvation (bs) : CMCD-Status
* deadline (dl) and startup (su) : CMCD-Request
* playback rate (pr) : CMCD-Session

PiperOrigin-RevId: 555553357
This commit is contained in:
rohks 2023-08-10 17:32:30 +00:00 committed by Tianyi Feng
parent c1913e8d89
commit 4282a6ecd7
11 changed files with 434 additions and 36 deletions

View file

@ -52,6 +52,9 @@
* Enhance `ChunkSource.getNextChunk(long, long, List, ChunkHolder)` method * Enhance `ChunkSource.getNextChunk(long, long, List, ChunkHolder)` method
in the `ChunkSource` interface to `ChunkSource.getNextChunk(LoadingInfo, in the `ChunkSource` interface to `ChunkSource.getNextChunk(LoadingInfo,
long, List, ChunkHolder)`. long, List, ChunkHolder)`.
* Add additional fields to Common Media Client Data (CMCD) logging: buffer
starvation (`bs`), deadline (`dl`), playback rate (`pr`) and startup
(`su`) ([#8699](https://github.com/google/ExoPlayer/issues/8699)).
* Transformer: * Transformer:
* Parse EXIF rotation data for image inputs. * Parse EXIF rotation data for image inputs.
* Remove `TransformationRequest.HdrMode` annotation type and its * Remove `TransformationRequest.HdrMode` annotation type and its

View file

@ -120,4 +120,17 @@ public final class LoadingInfo {
public LoadingInfo.Builder buildUpon() { public LoadingInfo.Builder buildUpon() {
return new LoadingInfo.Builder(this); return new LoadingInfo.Builder(this);
} }
/**
* Checks if rebuffering has occurred since {@code realtimeMs}.
*
* @param realtimeMs The time to compare against, as measured by {@link
* SystemClock#elapsedRealtime()}.
* @return Whether rebuffering has occurred since the provided timestamp.
*/
public boolean rebufferedSince(long realtimeMs) {
return lastRebufferRealtimeMs != C.TIME_UNSET
&& realtimeMs != C.TIME_UNSET
&& lastRebufferRealtimeMs >= realtimeMs;
}
} }

View file

@ -67,7 +67,11 @@ public final class CmcdConfiguration {
KEY_TOP_BITRATE, KEY_TOP_BITRATE,
KEY_OBJECT_DURATION, KEY_OBJECT_DURATION,
KEY_MEASURED_THROUGHPUT, KEY_MEASURED_THROUGHPUT,
KEY_OBJECT_TYPE KEY_OBJECT_TYPE,
KEY_BUFFER_STARVATION,
KEY_DEADLINE,
KEY_PLAYBACK_RATE,
KEY_STARTUP
}) })
@Documented @Documented
@Target(TYPE_USE) @Target(TYPE_USE)
@ -92,6 +96,10 @@ public final class CmcdConfiguration {
public static final String KEY_OBJECT_DURATION = "d"; public static final String KEY_OBJECT_DURATION = "d";
public static final String KEY_MEASURED_THROUGHPUT = "mtp"; public static final String KEY_MEASURED_THROUGHPUT = "mtp";
public static final String KEY_OBJECT_TYPE = "ot"; public static final String KEY_OBJECT_TYPE = "ot";
public static final String KEY_BUFFER_STARVATION = "bs";
public static final String KEY_DEADLINE = "dl";
public static final String KEY_PLAYBACK_RATE = "pr";
public static final String KEY_STARTUP = "su";
/** /**
* Factory for {@link CmcdConfiguration} instances. * Factory for {@link CmcdConfiguration} instances.
@ -301,4 +309,36 @@ public final class CmcdConfiguration {
public boolean isObjectTypeLoggingAllowed() { public boolean isObjectTypeLoggingAllowed() {
return requestConfig.isKeyAllowed(KEY_OBJECT_TYPE); return requestConfig.isKeyAllowed(KEY_OBJECT_TYPE);
} }
/**
* Returns whether logging buffer starvation is allowed based on the {@linkplain RequestConfig
* request configuration}.
*/
public boolean isBufferStarvationLoggingAllowed() {
return requestConfig.isKeyAllowed(KEY_BUFFER_STARVATION);
}
/**
* Returns whether logging deadline is allowed based on the {@linkplain RequestConfig request
* configuration}.
*/
public boolean isDeadlineLoggingAllowed() {
return requestConfig.isKeyAllowed(KEY_DEADLINE);
}
/**
* Returns whether logging playback rate is allowed based on the {@linkplain RequestConfig request
* configuration}.
*/
public boolean isPlaybackRateLoggingAllowed() {
return requestConfig.isKeyAllowed(KEY_PLAYBACK_RATE);
}
/**
* Returns whether logging startup is allowed based on the {@linkplain RequestConfig request
* configuration}.
*/
public boolean isStartupLoggingAllowed() {
return requestConfig.isKeyAllowed(KEY_STARTUP);
}
} }

View file

@ -130,8 +130,11 @@ public final class CmcdHeadersFactory {
private final CmcdConfiguration cmcdConfiguration; private final CmcdConfiguration cmcdConfiguration;
private final ExoTrackSelection trackSelection; private final ExoTrackSelection trackSelection;
private final long bufferedDurationUs; private final long bufferedDurationUs;
private final float playbackRate;
private final @StreamingFormat String streamingFormat; private final @StreamingFormat String streamingFormat;
private final boolean isLive; private final boolean isLive;
private final boolean didRebuffer;
private final boolean isBufferEmpty;
private long chunkDurationUs; private long chunkDurationUs;
private @Nullable @ObjectType String objectType; private @Nullable @ObjectType String objectType;
@ -142,24 +145,36 @@ public final class CmcdHeadersFactory {
* @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 playbackRate The playback rate indicating the current speed of playback.
* @param streamingFormat The streaming format of the media content. Must be one of the allowed * @param streamingFormat The streaming format of the media content. Must be one of the allowed
* streaming formats specified by the {@link StreamingFormat} annotation. * streaming formats specified by the {@link StreamingFormat} annotation.
* @param isLive {@code true} if the media content is being streamed live, {@code false} * @param isLive {@code true} if the media content is being streamed live, {@code false}
* otherwise. * otherwise.
* @param didRebuffer {@code true} if a rebuffering event happened between the previous request
* and this one, {@code false} otherwise.
* @param isBufferEmpty {@code true} if the queue of buffered chunks is empty, {@code false}
* otherwise.
* @throws IllegalArgumentException If {@code bufferedDurationUs} is negative. * @throws IllegalArgumentException If {@code bufferedDurationUs} is negative.
*/ */
public CmcdHeadersFactory( public CmcdHeadersFactory(
CmcdConfiguration cmcdConfiguration, CmcdConfiguration cmcdConfiguration,
ExoTrackSelection trackSelection, ExoTrackSelection trackSelection,
long bufferedDurationUs, long bufferedDurationUs,
float playbackRate,
@StreamingFormat String streamingFormat, @StreamingFormat String streamingFormat,
boolean isLive) { boolean isLive,
boolean didRebuffer,
boolean isBufferEmpty) {
checkArgument(bufferedDurationUs >= 0); checkArgument(bufferedDurationUs >= 0);
checkArgument(playbackRate > 0);
this.cmcdConfiguration = cmcdConfiguration; this.cmcdConfiguration = cmcdConfiguration;
this.trackSelection = trackSelection; this.trackSelection = trackSelection;
this.bufferedDurationUs = bufferedDurationUs; this.bufferedDurationUs = bufferedDurationUs;
this.playbackRate = playbackRate;
this.streamingFormat = streamingFormat; this.streamingFormat = streamingFormat;
this.isLive = isLive; this.isLive = isLive;
this.didRebuffer = didRebuffer;
this.isBufferEmpty = isBufferEmpty;
this.chunkDurationUs = C.TIME_UNSET; this.chunkDurationUs = C.TIME_UNSET;
} }
@ -227,6 +242,12 @@ public final class CmcdHeadersFactory {
cmcdRequest.setMeasuredThroughputInKbps( cmcdRequest.setMeasuredThroughputInKbps(
Util.ceilDivide(trackSelection.getLatestBitrateEstimate(), 1000)); Util.ceilDivide(trackSelection.getLatestBitrateEstimate(), 1000));
} }
if (cmcdConfiguration.isDeadlineLoggingAllowed()) {
cmcdRequest.setDeadlineMs(bufferedDurationUs / (long) (playbackRate * 1000));
}
if (cmcdConfiguration.isStartupLoggingAllowed()) {
cmcdRequest.setStartup(didRebuffer || isBufferEmpty);
}
CmcdSession.Builder cmcdSession = CmcdSession.Builder cmcdSession =
new CmcdSession.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_SESSION)); new CmcdSession.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_SESSION));
@ -242,6 +263,9 @@ public final class CmcdHeadersFactory {
if (cmcdConfiguration.isStreamTypeLoggingAllowed()) { if (cmcdConfiguration.isStreamTypeLoggingAllowed()) {
cmcdSession.setStreamType(isLive ? STREAM_TYPE_LIVE : STREAM_TYPE_VOD); cmcdSession.setStreamType(isLive ? STREAM_TYPE_LIVE : STREAM_TYPE_VOD);
} }
if (cmcdConfiguration.isPlaybackRateLoggingAllowed()) {
cmcdSession.setPlaybackRate(playbackRate);
}
CmcdStatus.Builder cmcdStatus = CmcdStatus.Builder cmcdStatus =
new CmcdStatus.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_STATUS)); new CmcdStatus.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_STATUS));
@ -249,6 +273,9 @@ public final class CmcdHeadersFactory {
cmcdStatus.setMaximumRequestedThroughputKbps( cmcdStatus.setMaximumRequestedThroughputKbps(
cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps)); cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps));
} }
if (cmcdConfiguration.isBufferStarvationLoggingAllowed()) {
cmcdStatus.setBufferStarvation(didRebuffer);
}
ImmutableMap.Builder<String, String> httpRequestHeaders = ImmutableMap.builder(); ImmutableMap.Builder<String, String> httpRequestHeaders = ImmutableMap.builder();
cmcdObject.build().populateHttpRequestHeaders(httpRequestHeaders); cmcdObject.build().populateHttpRequestHeaders(httpRequestHeaders);
@ -262,7 +289,10 @@ public final class CmcdHeadersFactory {
return objectType != null && objectType.equals(OBJECT_TYPE_INIT_SEGMENT); return objectType != null && objectType.equals(OBJECT_TYPE_INIT_SEGMENT);
} }
/** Keys whose values vary with the object being requested. Contains CMCD fields: {@code br}. */ /**
* Keys whose values vary with the object being requested. Contains CMCD fields: {@code br},
* {@code tb}, {@code d} and {@code ot}.
*/
private static final class CmcdObject { private static final class CmcdObject {
/** Builder for {@link CmcdObject} instances. */ /** Builder for {@link CmcdObject} instances. */
@ -416,19 +446,25 @@ public final class CmcdHeadersFactory {
} }
} }
/** Keys whose values vary with each request. Contains CMCD fields: {@code bl}. */ /**
* Keys whose values vary with each request. Contains CMCD fields: {@code bl}, {@code mtp}, {@code
* dl} and {@code su}.
*/
private static final class CmcdRequest { private static final class CmcdRequest {
/** Builder for {@link CmcdRequest} instances. */ /** Builder for {@link CmcdRequest} instances. */
public static final class Builder { public static final class Builder {
private long bufferLengthMs; private long bufferLengthMs;
private long measuredThroughputInKbps; private long measuredThroughputInKbps;
private long deadlineMs;
private boolean startup;
@Nullable private String customData; @Nullable private String customData;
/** Creates a new instance with default values. */ /** Creates a new instance with default values. */
public Builder() { public Builder() {
this.bufferLengthMs = C.TIME_UNSET; this.bufferLengthMs = C.TIME_UNSET;
this.measuredThroughputInKbps = Long.MIN_VALUE; this.measuredThroughputInKbps = Long.MIN_VALUE;
this.deadlineMs = C.TIME_UNSET;
} }
/** /**
@ -458,6 +494,27 @@ public final class CmcdHeadersFactory {
return this; return this;
} }
/**
* Sets the {@link CmcdRequest#deadlineMs}. Rounded to nearest 100 ms. The default value is
* {@link C#TIME_UNSET}.
*
* @throws IllegalArgumentException If {@code deadlineMs} is not equal to {@link C#TIME_UNSET}
* and is negative.
*/
@CanIgnoreReturnValue
public Builder setDeadlineMs(long deadlineMs) {
checkArgument(deadlineMs >= 0 || deadlineMs == C.TIME_UNSET);
this.deadlineMs = ((deadlineMs + 50) / 100) * 100;
return this;
}
/** Sets the {@link CmcdRequest#startup}. The default value is {@code false}. */
@CanIgnoreReturnValue
public Builder setStartup(boolean startup) {
this.startup = startup;
return this;
}
/** Sets the {@link CmcdRequest#customData}. The default value is {@code null}. */ /** Sets the {@link CmcdRequest#customData}. The default value is {@code null}. */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setCustomData(@Nullable String customData) { public Builder setCustomData(@Nullable String customData) {
@ -495,6 +552,23 @@ public final class CmcdHeadersFactory {
*/ */
public final long measuredThroughputInKbps; public final long measuredThroughputInKbps;
/**
* Deadline in milliseconds from the request time until the first sample of this Segment/Object
* needs to be available in order to not create a buffer underrun or any other playback
* problems, or {@link C#TIME_UNSET} if unset.
*
* <p>This value MUST be rounded to the nearest 100 ms. For a playback rate of 1, this may be
* equivalent to the players remaining buffer length.
*/
public final long deadlineMs;
/**
* A boolean indicating whether the chunk is needed urgently due to startup, seeking or recovery
* after a buffer-empty event, or {@code false} if unknown. The media SHOULD not be rendering
* when this request is made.
*/
public final boolean startup;
/** /**
* Custom data where the values of the keys vary with each request, or {@code null} if unset. * Custom data where the values of the keys vary with each request, or {@code null} if unset.
* *
@ -506,6 +580,8 @@ public final class CmcdHeadersFactory {
private CmcdRequest(Builder builder) { private CmcdRequest(Builder builder) {
this.bufferLengthMs = builder.bufferLengthMs; this.bufferLengthMs = builder.bufferLengthMs;
this.measuredThroughputInKbps = builder.measuredThroughputInKbps; this.measuredThroughputInKbps = builder.measuredThroughputInKbps;
this.deadlineMs = builder.deadlineMs;
this.startup = builder.startup;
this.customData = builder.customData; this.customData = builder.customData;
} }
@ -527,6 +603,16 @@ public final class CmcdHeadersFactory {
Util.formatInvariant( Util.formatInvariant(
"%s=%d,", CmcdConfiguration.KEY_MEASURED_THROUGHPUT, measuredThroughputInKbps)); "%s=%d,", CmcdConfiguration.KEY_MEASURED_THROUGHPUT, measuredThroughputInKbps));
} }
if (deadlineMs != C.TIME_UNSET) {
headerValue
.append(CmcdConfiguration.KEY_DEADLINE)
.append("=")
.append(deadlineMs)
.append(",");
}
if (startup) {
headerValue.append(CmcdConfiguration.KEY_STARTUP).append(",");
}
if (!TextUtils.isEmpty(customData)) { if (!TextUtils.isEmpty(customData)) {
headerValue.append(Util.formatInvariant("%s,", customData)); headerValue.append(Util.formatInvariant("%s,", customData));
} }
@ -542,7 +628,7 @@ public final class CmcdHeadersFactory {
/** /**
* Keys whose values are expected to be invariant over the life of the session. Contains CMCD * Keys whose values are expected to be invariant over the life of the session. Contains CMCD
* fields: {@code cid} and {@code sid}. * fields: {@code cid}, {@code sid}, {@code sf}, {@code st}, {@code pr} and {@code v}.
*/ */
private static final class CmcdSession { private static final class CmcdSession {
@ -552,6 +638,7 @@ public final class CmcdHeadersFactory {
@Nullable private String sessionId; @Nullable private String sessionId;
@Nullable private @StreamingFormat String streamingFormat; @Nullable private @StreamingFormat String streamingFormat;
@Nullable private @StreamType String streamType; @Nullable private @StreamType String streamType;
private float playbackRate;
@Nullable private String customData; @Nullable private String customData;
/** /**
@ -596,6 +683,13 @@ public final class CmcdHeadersFactory {
return this; return this;
} }
/** Sets the {@link CmcdSession#playbackRate}. The default value is {@link C#RATE_UNSET}. */
@CanIgnoreReturnValue
public Builder setPlaybackRate(float playbackRate) {
this.playbackRate = playbackRate;
return this;
}
/** 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 Builder setCustomData(@Nullable String customData) { public Builder setCustomData(@Nullable String customData) {
@ -633,9 +727,8 @@ public final class CmcdHeadersFactory {
/** /**
* The streaming format that defines the current request, or{@code null} if unset. Must be one * The streaming format that defines the current request, or{@code null} if unset. Must be one
* of the allowed streaming formats specified by the {@link StreamingFormat} annotation. * of the allowed stream formats specified by the {@link StreamingFormat} annotation. If the
* * streaming format being requested is unknown, then this key MUST NOT be used.
* <p>If the streaming format being requested is unknown, then this key MUST NOT be used.
*/ */
@Nullable public final @StreamingFormat String streamingFormat; @Nullable public final @StreamingFormat String streamingFormat;
@ -645,6 +738,11 @@ public final class CmcdHeadersFactory {
*/ */
@Nullable public final @StreamType String streamType; @Nullable public final @StreamType String streamType;
/**
* The playback rate indicating the current rate of playback, or {@link C#RATE_UNSET} if unset.
*/
public final float playbackRate;
/** /**
* Custom data where the values of the keys are expected to be invariant over the life of the * Custom data where the values of the keys are expected to be invariant over the life of the
* session, or {@code null} if unset. * session, or {@code null} if unset.
@ -659,6 +757,7 @@ public final class CmcdHeadersFactory {
this.sessionId = builder.sessionId; this.sessionId = builder.sessionId;
this.streamingFormat = builder.streamingFormat; this.streamingFormat = builder.streamingFormat;
this.streamType = builder.streamType; this.streamType = builder.streamType;
this.playbackRate = builder.playbackRate;
this.customData = builder.customData; this.customData = builder.customData;
} }
@ -688,6 +787,10 @@ public final class CmcdHeadersFactory {
headerValue.append( headerValue.append(
Util.formatInvariant("%s=%s,", CmcdConfiguration.KEY_STREAM_TYPE, streamType)); Util.formatInvariant("%s=%s,", CmcdConfiguration.KEY_STREAM_TYPE, streamType));
} }
if (playbackRate != C.RATE_UNSET && playbackRate != 1.0f) {
headerValue.append(
Util.formatInvariant("%s=%.2f,", CmcdConfiguration.KEY_PLAYBACK_RATE, playbackRate));
}
if (VERSION != 1) { if (VERSION != 1) {
headerValue.append(Util.formatInvariant("%s=%d,", CmcdConfiguration.KEY_VERSION, VERSION)); headerValue.append(Util.formatInvariant("%s=%d,", CmcdConfiguration.KEY_VERSION, VERSION));
} }
@ -705,13 +808,15 @@ public final class CmcdHeadersFactory {
} }
/** /**
* Keys whose values do not vary with every request or object. Contains CMCD fields: {@code rtp}. * Keys whose values do not vary with every request or object. Contains CMCD fields: {@code rtp}
* and {@code bs}.
*/ */
private static final class CmcdStatus { private static final class CmcdStatus {
/** Builder for {@link CmcdStatus} instances. */ /** Builder for {@link CmcdStatus} instances. */
public static final class Builder { public static final class Builder {
private int maximumRequestedThroughputKbps; private int maximumRequestedThroughputKbps;
private boolean bufferStarvation;
@Nullable private String customData; @Nullable private String customData;
/** Creates a new instance with default values. */ /** Creates a new instance with default values. */
@ -740,6 +845,13 @@ public final class CmcdHeadersFactory {
return this; return this;
} }
/** Sets the {@link CmcdStatus#bufferStarvation}. The default value is {@code false}. */
@CanIgnoreReturnValue
public Builder setBufferStarvation(boolean bufferStarvation) {
this.bufferStarvation = bufferStarvation;
return this;
}
/** 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 Builder setCustomData(@Nullable String customData) { public Builder setCustomData(@Nullable String customData) {
@ -759,6 +871,13 @@ public final class CmcdHeadersFactory {
*/ */
public final int maximumRequestedThroughputKbps; public final int maximumRequestedThroughputKbps;
/**
* A boolean indicating whether the buffer was starved at some point between the prior request
* and this chunk request, resulting in the player being in a rebuffering state and the video or
* audio playback being stalled, or {@code false} if unknown.
*/
public final boolean bufferStarvation;
/** /**
* Custom data where the values of the keys do not vary with every request or object, or {@code * Custom data where the values of the keys do not vary with every request or object, or {@code
* null} if unset. * null} if unset.
@ -770,6 +889,7 @@ public final class CmcdHeadersFactory {
private CmcdStatus(Builder builder) { private CmcdStatus(Builder builder) {
this.maximumRequestedThroughputKbps = builder.maximumRequestedThroughputKbps; this.maximumRequestedThroughputKbps = builder.maximumRequestedThroughputKbps;
this.bufferStarvation = builder.bufferStarvation;
this.customData = builder.customData; this.customData = builder.customData;
} }
@ -788,6 +908,9 @@ public final class CmcdHeadersFactory {
"%s=%d,", "%s=%d,",
CmcdConfiguration.KEY_MAXIMUM_REQUESTED_BITRATE, maximumRequestedThroughputKbps)); CmcdConfiguration.KEY_MAXIMUM_REQUESTED_BITRATE, maximumRequestedThroughputKbps));
} }
if (bufferStarvation) {
headerValue.append(CmcdConfiguration.KEY_BUFFER_STARVATION).append(",");
}
if (!TextUtils.isEmpty(customData)) { if (!TextUtils.isEmpty(customData)) {
headerValue.append(Util.formatInvariant("%s,", customData)); headerValue.append(Util.formatInvariant("%s,", customData));
} }

View file

@ -68,8 +68,11 @@ public class CmcdHeadersFactoryTest {
cmcdConfiguration, cmcdConfiguration,
trackSelection, trackSelection,
/* bufferedDurationUs= */ 1_760_000, /* bufferedDurationUs= */ 1_760_000,
/* playbackRate= */ 2.0f,
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH, /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH,
/* isLive= */ true) /* isLive= */ true,
/* didRebuffer= */ true,
/* isBufferEmpty= */ false)
.setChunkDurationUs(3_000_000) .setChunkDurationUs(3_000_000)
.createHttpRequestHeaders(); .createHttpRequestHeaders();
@ -78,10 +81,10 @@ public class CmcdHeadersFactoryTest {
"CMCD-Object", "CMCD-Object",
"br=840,tb=1000,d=3000,key1=value1", "br=840,tb=1000,d=3000,key1=value1",
"CMCD-Request", "CMCD-Request",
"bl=1800,mtp=500,key2=\"stringValue\"", "bl=1800,mtp=500,dl=900,su,key2=\"stringValue\"",
"CMCD-Session", "CMCD-Session",
"cid=\"mediaId\",sid=\"sessionId\",sf=d,st=l", "cid=\"mediaId\",sid=\"sessionId\",sf=d,st=l,pr=2.00",
"CMCD-Status", "CMCD-Status",
"rtp=1700"); "rtp=1700,bs");
} }
} }

View file

@ -162,6 +162,12 @@ public class DefaultDashChunkSource implements DashChunkSource {
@Nullable private IOException fatalError; @Nullable private IOException fatalError;
private boolean missingLastSegment; private boolean missingLastSegment;
/**
* The time at which the last {@link #getNextChunk(LoadingInfo, long, List, ChunkHolder)} method
* was called, as measured by {@link SystemClock#elapsedRealtime}.
*/
private long lastChunkRequestRealtimeMs;
/** /**
* @param chunkExtractorFactory Creates {@link ChunkExtractor} instances to use for extracting * @param chunkExtractorFactory Creates {@link ChunkExtractor} instances to use for extracting
* chunks. * chunks.
@ -215,6 +221,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
this.maxSegmentsPerLoad = maxSegmentsPerLoad; this.maxSegmentsPerLoad = maxSegmentsPerLoad;
this.playerTrackEmsgHandler = playerTrackEmsgHandler; this.playerTrackEmsgHandler = playerTrackEmsgHandler;
this.cmcdConfiguration = cmcdConfiguration; this.cmcdConfiguration = cmcdConfiguration;
this.lastChunkRequestRealtimeMs = C.TIME_UNSET;
long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
@ -371,7 +378,6 @@ public class DefaultDashChunkSource implements DashChunkSource {
long availableLiveDurationUs = getAvailableLiveDurationUs(nowUnixTimeUs, playbackPositionUs); long availableLiveDurationUs = getAvailableLiveDurationUs(nowUnixTimeUs, playbackPositionUs);
trackSelection.updateSelectedTrack( trackSelection.updateSelectedTrack(
playbackPositionUs, bufferedDurationUs, availableLiveDurationUs, queue, chunkIterators); playbackPositionUs, bufferedDurationUs, availableLiveDurationUs, queue, chunkIterators);
int selectedTrackIndex = trackSelection.getSelectedIndex(); int selectedTrackIndex = trackSelection.getSelectedIndex();
@Nullable @Nullable
@ -382,8 +388,13 @@ public class DefaultDashChunkSource implements DashChunkSource {
cmcdConfiguration, cmcdConfiguration,
trackSelection, trackSelection,
bufferedDurationUs, bufferedDurationUs,
/* playbackRate= */ loadingInfo.playbackSpeed,
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH, /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_DASH,
/* isLive= */ manifest.dynamic); /* isLive= */ manifest.dynamic,
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
/* isBufferEmpty= */ queue.isEmpty());
lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();
RepresentationHolder representationHolder = updateSelectedBaseUrl(selectedTrackIndex); RepresentationHolder representationHolder = updateSelectedBaseUrl(selectedTrackIndex);
if (representationHolder.chunkExtractor != null) { if (representationHolder.chunkExtractor != null) {
Representation selectedRepresentation = representationHolder.representation; Representation selectedRepresentation = representationHolder.representation;

View file

@ -310,7 +310,7 @@ public class DefaultDashChunkSourceTest {
ChunkHolder output = new ChunkHolder(); ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk( chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(), new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0, /* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(), /* queue= */ ImmutableList.of(),
output); output);
@ -320,9 +320,63 @@ public class DefaultDashChunkSourceTest {
"CMCD-Object", "CMCD-Object",
"br=700,tb=1300,d=4000,ot=v", "br=700,tb=1300,d=4000,ot=v",
"CMCD-Request", "CMCD-Request",
"bl=0,mtp=1000", "bl=0,mtp=1000,dl=0,su",
"CMCD-Session", "CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=d,st=v"); "cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=d,st=v");
chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(3_000_000).setPlaybackSpeed(1.25f).build(),
/* loadPositionUs= */ 4_000_000,
/* queue= */ ImmutableList.of((MediaChunk) output.chunk),
output);
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=700,tb=1300,d=4000,ot=v",
"CMCD-Request",
"bl=1000,mtp=1000,dl=800",
"CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=d,st=v,pr=1.25");
}
@Test
public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCorrectBufferStarvationKey()
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();
LoadingInfo loadingInfo =
new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build();
chunkSource.getNextChunk(
loadingInfo, /* loadPositionUs= */ 0, /* queue= */ ImmutableList.of(), output);
assertThat(output.chunk.dataSpec.httpRequestHeaders).doesNotContainKey("CMCD-Status");
loadingInfo =
loadingInfo
.buildUpon()
.setPlaybackPositionUs(2_000_000)
.setLastRebufferRealtimeMs(SystemClock.elapsedRealtime())
.build();
ShadowSystemClock.advanceBy(Duration.ofMillis(100));
chunkSource.getNextChunk(
loadingInfo, /* loadPositionUs= */ 4_000_000, /* queue= */ ImmutableList.of(), output);
assertThat(output.chunk.dataSpec.httpRequestHeaders).containsEntry("CMCD-Status", "bs");
loadingInfo = loadingInfo.buildUpon().setPlaybackPositionUs(6_000_000).build();
ShadowSystemClock.advanceBy(Duration.ofMillis(100));
chunkSource.getNextChunk(
loadingInfo, /* loadPositionUs= */ 8_000_000, /* queue= */ ImmutableList.of(), output);
assertThat(output.chunk.dataSpec.httpRequestHeaders).doesNotContainKey("CMCD-Status");
} }
@Test @Test
@ -355,7 +409,7 @@ public class DefaultDashChunkSourceTest {
ChunkHolder output = new ChunkHolder(); ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk( chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(), new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0, /* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(), /* queue= */ ImmutableList.of(),
output); output);
@ -365,7 +419,7 @@ public class DefaultDashChunkSourceTest {
"CMCD-Object", "CMCD-Object",
"br=700,tb=1300,d=4000,ot=v", "br=700,tb=1300,d=4000,ot=v",
"CMCD-Request", "CMCD-Request",
"bl=0,mtp=1000", "bl=0,mtp=1000,dl=0,su",
"CMCD-Session", "CMCD-Session",
"cid=\"mediaIdcontentIdSuffix\",sf=d,st=v", "cid=\"mediaIdcontentIdSuffix\",sf=d,st=v",
"CMCD-Status", "CMCD-Status",
@ -401,7 +455,7 @@ public class DefaultDashChunkSourceTest {
ChunkHolder output = new ChunkHolder(); ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk( chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(), new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0, /* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(), /* queue= */ ImmutableList.of(),
output); output);
@ -411,7 +465,7 @@ public class DefaultDashChunkSourceTest {
"CMCD-Object", "CMCD-Object",
"br=700,tb=1300,d=4000,ot=v,key1=value1", "br=700,tb=1300,d=4000,ot=v,key1=value1",
"CMCD-Request", "CMCD-Request",
"bl=0,mtp=1000,key2=\"stringValue\"", "bl=0,mtp=1000,dl=0,su,key2=\"stringValue\"",
"CMCD-Session", "CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=d,st=v,key3=1", "cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=d,st=v,key3=1",
"CMCD-Status", "CMCD-Status",

View file

@ -153,6 +153,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private long liveEdgeInPeriodTimeUs; private long liveEdgeInPeriodTimeUs;
private boolean seenExpectedPlaylistError; private boolean seenExpectedPlaylistError;
/**
* The time at which the last {@link #getNextChunk(LoadingInfo, long, List, boolean,
* HlsChunkHolder)} method was called, as measured by {@link SystemClock#elapsedRealtime}.
*/
private long lastChunkRequestRealtimeMs;
/** /**
* @param extractorFactory An {@link HlsExtractorFactory} from which to obtain the extractors for * @param extractorFactory An {@link HlsExtractorFactory} from which to obtain the extractors for
* media chunks. * media chunks.
@ -194,6 +200,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.muxedCaptionFormats = muxedCaptionFormats; this.muxedCaptionFormats = muxedCaptionFormats;
this.playerId = playerId; this.playerId = playerId;
this.cmcdConfiguration = cmcdConfiguration; this.cmcdConfiguration = cmcdConfiguration;
this.lastChunkRequestRealtimeMs = C.TIME_UNSET;
keyCache = new FullSegmentEncryptionKeyCache(KEY_CACHE_SIZE); keyCache = new FullSegmentEncryptionKeyCache(KEY_CACHE_SIZE);
scratchSpace = Util.EMPTY_BYTE_ARRAY; scratchSpace = Util.EMPTY_BYTE_ARRAY;
liveEdgeInPeriodTimeUs = C.TIME_UNSET; liveEdgeInPeriodTimeUs = C.TIME_UNSET;
@ -489,12 +496,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
cmcdConfiguration, cmcdConfiguration,
trackSelection, trackSelection,
bufferedDurationUs, bufferedDurationUs,
/* playbackRate= */ loadingInfo.playbackSpeed,
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_HLS, /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_HLS,
/* isLive= */ !playlist.hasEndTag) /* isLive= */ !playlist.hasEndTag,
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
/* isBufferEmpty= */ queue.isEmpty())
.setObjectType( .setObjectType(
getIsMuxedAudioAndVideo() getIsMuxedAudioAndVideo()
? CmcdHeadersFactory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO ? CmcdHeadersFactory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
: CmcdHeadersFactory.getObjectType(trackSelection)); : CmcdHeadersFactory.getObjectType(trackSelection));
lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();
// 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

View file

@ -21,6 +21,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
@ -42,11 +43,13 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.time.Duration;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.robolectric.shadows.ShadowSystemClock;
/** Unit tests for {@link HlsChunkSource}. */ /** Unit tests for {@link HlsChunkSource}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@ -202,7 +205,7 @@ public class HlsChunkSourceTest {
HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder(); HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder();
testChunkSource.getNextChunk( testChunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(), new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0, /* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(), /* queue= */ ImmutableList.of(),
/* allowEndOfStream= */ true, /* allowEndOfStream= */ true,
@ -213,9 +216,76 @@ public class HlsChunkSourceTest {
"CMCD-Object", "CMCD-Object",
"br=800,tb=800,d=4000,ot=v", "br=800,tb=800,d=4000,ot=v",
"CMCD-Request", "CMCD-Request",
"bl=0", "bl=0,dl=0,su",
"CMCD-Session", "CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=h,st=v"); "cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=h,st=v");
testChunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(3_000_000).setPlaybackSpeed(1.25f).build(),
/* loadPositionUs= */ 4_000_000,
/* queue= */ ImmutableList.of((HlsMediaChunk) output.chunk),
/* allowEndOfStream= */ true,
output);
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=800,tb=800,d=4000,ot=v",
"CMCD-Request",
"bl=1000,dl=800",
"CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=h,st=v,pr=1.25");
}
@Test
public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCorrectBufferStarvationKey()
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();
LoadingInfo loadingInfo =
new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build();
testChunkSource.getNextChunk(
loadingInfo,
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
/* allowEndOfStream= */ true,
output);
assertThat(output.chunk.dataSpec.httpRequestHeaders).doesNotContainKey("CMCD-Status");
loadingInfo =
loadingInfo
.buildUpon()
.setPlaybackPositionUs(2_000_000)
.setLastRebufferRealtimeMs(SystemClock.elapsedRealtime())
.build();
ShadowSystemClock.advanceBy(Duration.ofMillis(100));
testChunkSource.getNextChunk(
loadingInfo,
/* loadPositionUs= */ 4_000_000,
/* queue= */ ImmutableList.of(),
/* allowEndOfStream= */ true,
output);
assertThat(output.chunk.dataSpec.httpRequestHeaders).containsEntry("CMCD-Status", "bs");
loadingInfo = loadingInfo.buildUpon().setPlaybackPositionUs(6_000_000).build();
ShadowSystemClock.advanceBy(Duration.ofMillis(100));
testChunkSource.getNextChunk(
loadingInfo,
/* loadPositionUs= */ 8_000_000,
/* queue= */ ImmutableList.of(),
/* allowEndOfStream= */ true,
output);
assertThat(output.chunk.dataSpec.httpRequestHeaders).doesNotContainKey("CMCD-Status");
} }
@Test @Test
@ -248,7 +318,7 @@ public class HlsChunkSourceTest {
HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder(); HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder();
testChunkSource.getNextChunk( testChunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(), new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0, /* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(), /* queue= */ ImmutableList.of(),
/* allowEndOfStream= */ true, /* allowEndOfStream= */ true,
@ -259,7 +329,7 @@ public class HlsChunkSourceTest {
"CMCD-Object", "CMCD-Object",
"br=800,tb=800,d=4000,ot=v", "br=800,tb=800,d=4000,ot=v",
"CMCD-Request", "CMCD-Request",
"bl=0", "bl=0,dl=0,su",
"CMCD-Session", "CMCD-Session",
"cid=\"mediaIdcontentIdSuffix\",sf=h,st=v", "cid=\"mediaIdcontentIdSuffix\",sf=h,st=v",
"CMCD-Status", "CMCD-Status",
@ -295,7 +365,7 @@ public class HlsChunkSourceTest {
HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder(); HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder();
testChunkSource.getNextChunk( testChunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(), new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0, /* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(), /* queue= */ ImmutableList.of(),
/* allowEndOfStream= */ true, /* allowEndOfStream= */ true,
@ -306,7 +376,7 @@ public class HlsChunkSourceTest {
"CMCD-Object", "CMCD-Object",
"br=800,tb=800,d=4000,ot=v,key1=value1", "br=800,tb=800,d=4000,ot=v,key1=value1",
"CMCD-Request", "CMCD-Request",
"bl=0,key2=\"stringValue\"", "bl=0,dl=0,su,key2=\"stringValue\"",
"CMCD-Session", "CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=h,st=v,key3=1", "cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=h,st=v,key3=1",
"CMCD-Status", "CMCD-Status",

View file

@ -18,6 +18,7 @@ package androidx.media3.exoplayer.smoothstreaming;
import static androidx.media3.exoplayer.trackselection.TrackSelectionUtil.createFallbackOptions; import static androidx.media3.exoplayer.trackselection.TrackSelectionUtil.createFallbackOptions;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
@ -98,6 +99,12 @@ public class DefaultSsChunkSource implements SsChunkSource {
@Nullable private IOException fatalError; @Nullable private IOException fatalError;
/**
* The time at which the last {@link #getNextChunk(LoadingInfo, long, List, ChunkHolder)} method
* was called, as measured by {@link SystemClock#elapsedRealtime}.
*/
private long lastChunkRequestRealtimeMs;
/** /**
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
* @param manifest The initial manifest. * @param manifest The initial manifest.
@ -119,6 +126,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
this.trackSelection = trackSelection; this.trackSelection = trackSelection;
this.dataSource = dataSource; this.dataSource = dataSource;
this.cmcdConfiguration = cmcdConfiguration; this.cmcdConfiguration = cmcdConfiguration;
this.lastChunkRequestRealtimeMs = C.TIME_UNSET;
StreamElement streamElement = manifest.streamElements[streamElementIndex]; StreamElement streamElement = manifest.streamElements[streamElementIndex];
chunkExtractors = new ChunkExtractor[trackSelection.length()]; chunkExtractors = new ChunkExtractor[trackSelection.length()];
@ -290,10 +298,14 @@ public class DefaultSsChunkSource implements SsChunkSource {
cmcdConfiguration, cmcdConfiguration,
trackSelection, trackSelection,
bufferedDurationUs, bufferedDurationUs,
/* playbackRate= */ loadingInfo.playbackSpeed,
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_SS, /* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_SS,
/* isLive= */ manifest.isLive) /* isLive= */ manifest.isLive,
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
/* isBufferEmpty= */ queue.isEmpty())
.setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs) .setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs)
.setObjectType(CmcdHeadersFactory.getObjectType(trackSelection)); .setObjectType(CmcdHeadersFactory.getObjectType(trackSelection));
lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();
out.chunk = out.chunk =
newMediaChunk( newMediaChunk(

View file

@ -18,6 +18,7 @@ package androidx.media3.exoplayer.smoothstreaming;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
@ -27,6 +28,7 @@ import androidx.media3.exoplayer.LoadingInfo;
import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest; import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest;
import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifestParser; import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifestParser;
import androidx.media3.exoplayer.source.chunk.ChunkHolder; import androidx.media3.exoplayer.source.chunk.ChunkHolder;
import androidx.media3.exoplayer.source.chunk.MediaChunk;
import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection; import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter; import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter;
@ -38,8 +40,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
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 java.io.IOException; import java.io.IOException;
import java.time.Duration;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowSystemClock;
/** Unit test for {@link DefaultSsChunkSource}. */ /** Unit test for {@link DefaultSsChunkSource}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@ -57,7 +61,7 @@ public class DefaultSsChunkSourceTest {
ChunkHolder output = new ChunkHolder(); ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk( chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(), new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0, /* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(), /* queue= */ ImmutableList.of(),
output); output);
@ -67,9 +71,63 @@ public class DefaultSsChunkSourceTest {
"CMCD-Object", "CMCD-Object",
"br=308,tb=1536,d=1968,ot=v", "br=308,tb=1536,d=1968,ot=v",
"CMCD-Request", "CMCD-Request",
"bl=0,mtp=1000", "bl=0,mtp=1000,dl=0,su",
"CMCD-Session", "CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=s,st=v"); "cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=s,st=v");
chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(3_000_000).setPlaybackSpeed(2.0f).build(),
/* loadPositionUs= */ 4_000_000,
/* queue= */ ImmutableList.of((MediaChunk) output.chunk),
output);
assertThat(output.chunk.dataSpec.httpRequestHeaders)
.containsExactly(
"CMCD-Object",
"br=308,tb=1536,d=898,ot=v",
"CMCD-Request",
"bl=1000,mtp=1000,dl=500",
"CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=s,st=v,pr=2.00");
}
@Test
public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCorrectBufferStarvationKey()
throws Exception {
CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT;
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
CmcdConfiguration cmcdConfiguration =
cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
SsChunkSource chunkSource = createSsChunkSource(/* numberOfTracks= */ 2, cmcdConfiguration);
ChunkHolder output = new ChunkHolder();
LoadingInfo loadingInfo =
new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build();
chunkSource.getNextChunk(
loadingInfo, /* loadPositionUs= */ 0, /* queue= */ ImmutableList.of(), output);
assertThat(output.chunk.dataSpec.httpRequestHeaders).doesNotContainKey("CMCD-Status");
loadingInfo =
loadingInfo
.buildUpon()
.setPlaybackPositionUs(2_000_000)
.setLastRebufferRealtimeMs(SystemClock.elapsedRealtime())
.build();
ShadowSystemClock.advanceBy(Duration.ofMillis(100));
chunkSource.getNextChunk(
loadingInfo, /* loadPositionUs= */ 4_000_000, /* queue= */ ImmutableList.of(), output);
assertThat(output.chunk.dataSpec.httpRequestHeaders).containsEntry("CMCD-Status", "bs");
loadingInfo = loadingInfo.buildUpon().setPlaybackPositionUs(6_000_000).build();
ShadowSystemClock.advanceBy(Duration.ofMillis(100));
chunkSource.getNextChunk(
loadingInfo, /* loadPositionUs= */ 8_000_000, /* queue= */ ImmutableList.of(), output);
assertThat(output.chunk.dataSpec.httpRequestHeaders).doesNotContainKey("CMCD-Status");
} }
@Test @Test
@ -102,7 +160,7 @@ public class DefaultSsChunkSourceTest {
ChunkHolder output = new ChunkHolder(); ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk( chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(), new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0, /* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(), /* queue= */ ImmutableList.of(),
output); output);
@ -112,7 +170,7 @@ public class DefaultSsChunkSourceTest {
"CMCD-Object", "CMCD-Object",
"br=308,tb=1536,d=1968,ot=v", "br=308,tb=1536,d=1968,ot=v",
"CMCD-Request", "CMCD-Request",
"bl=0,mtp=1000", "bl=0,mtp=1000,dl=0,su",
"CMCD-Session", "CMCD-Session",
"cid=\"mediaIdcontentIdSuffix\",sf=s,st=v", "cid=\"mediaIdcontentIdSuffix\",sf=s,st=v",
"CMCD-Status", "CMCD-Status",
@ -148,7 +206,7 @@ public class DefaultSsChunkSourceTest {
ChunkHolder output = new ChunkHolder(); ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk( chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(), new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0, /* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(), /* queue= */ ImmutableList.of(),
output); output);
@ -158,7 +216,7 @@ public class DefaultSsChunkSourceTest {
"CMCD-Object", "CMCD-Object",
"br=308,tb=1536,d=1968,ot=v,key1=value1", "br=308,tb=1536,d=1968,ot=v,key1=value1",
"CMCD-Request", "CMCD-Request",
"bl=0,mtp=1000,key2=\"stringValue\"", "bl=0,mtp=1000,dl=0,su,key2=\"stringValue\"",
"CMCD-Session", "CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=s,st=v,key3=1", "cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=s,st=v,key3=1",
"CMCD-Status", "CMCD-Status",