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
in the `ChunkSource` interface to `ChunkSource.getNextChunk(LoadingInfo,
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:
* Parse EXIF rotation data for image inputs.
* Remove `TransformationRequest.HdrMode` annotation type and its

View File

@ -120,4 +120,17 @@ public final class LoadingInfo {
public LoadingInfo.Builder buildUpon() {
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_OBJECT_DURATION,
KEY_MEASURED_THROUGHPUT,
KEY_OBJECT_TYPE
KEY_OBJECT_TYPE,
KEY_BUFFER_STARVATION,
KEY_DEADLINE,
KEY_PLAYBACK_RATE,
KEY_STARTUP
})
@Documented
@Target(TYPE_USE)
@ -92,6 +96,10 @@ public final class CmcdConfiguration {
public static final String KEY_OBJECT_DURATION = "d";
public static final String KEY_MEASURED_THROUGHPUT = "mtp";
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.
@ -301,4 +309,36 @@ public final class CmcdConfiguration {
public boolean isObjectTypeLoggingAllowed() {
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 ExoTrackSelection trackSelection;
private final long bufferedDurationUs;
private final float playbackRate;
private final @StreamingFormat String streamingFormat;
private final boolean isLive;
private final boolean didRebuffer;
private final boolean isBufferEmpty;
private long chunkDurationUs;
private @Nullable @ObjectType String objectType;
@ -142,24 +145,36 @@ public final class CmcdHeadersFactory {
* @param trackSelection The {@linkplain ExoTrackSelection track selection}.
* @param bufferedDurationUs The duration of media currently buffered from the current playback
* position, in microseconds.
* @param playbackRate The playback rate indicating the current speed of playback.
* @param streamingFormat The streaming format of the media content. Must be one of the allowed
* streaming formats specified by the {@link StreamingFormat} annotation.
* @param isLive {@code true} if the media content is being streamed live, {@code false}
* otherwise.
* @param didRebuffer {@code true} if a rebuffering event happened between the previous request
* and this one, {@code false} otherwise.
* @param isBufferEmpty {@code true} if the queue of buffered chunks is empty, {@code false}
* otherwise.
* @throws IllegalArgumentException If {@code bufferedDurationUs} is negative.
*/
public CmcdHeadersFactory(
CmcdConfiguration cmcdConfiguration,
ExoTrackSelection trackSelection,
long bufferedDurationUs,
float playbackRate,
@StreamingFormat String streamingFormat,
boolean isLive) {
boolean isLive,
boolean didRebuffer,
boolean isBufferEmpty) {
checkArgument(bufferedDurationUs >= 0);
checkArgument(playbackRate > 0);
this.cmcdConfiguration = cmcdConfiguration;
this.trackSelection = trackSelection;
this.bufferedDurationUs = bufferedDurationUs;
this.playbackRate = playbackRate;
this.streamingFormat = streamingFormat;
this.isLive = isLive;
this.didRebuffer = didRebuffer;
this.isBufferEmpty = isBufferEmpty;
this.chunkDurationUs = C.TIME_UNSET;
}
@ -227,6 +242,12 @@ public final class CmcdHeadersFactory {
cmcdRequest.setMeasuredThroughputInKbps(
Util.ceilDivide(trackSelection.getLatestBitrateEstimate(), 1000));
}
if (cmcdConfiguration.isDeadlineLoggingAllowed()) {
cmcdRequest.setDeadlineMs(bufferedDurationUs / (long) (playbackRate * 1000));
}
if (cmcdConfiguration.isStartupLoggingAllowed()) {
cmcdRequest.setStartup(didRebuffer || isBufferEmpty);
}
CmcdSession.Builder cmcdSession =
new CmcdSession.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_SESSION));
@ -242,6 +263,9 @@ public final class CmcdHeadersFactory {
if (cmcdConfiguration.isStreamTypeLoggingAllowed()) {
cmcdSession.setStreamType(isLive ? STREAM_TYPE_LIVE : STREAM_TYPE_VOD);
}
if (cmcdConfiguration.isPlaybackRateLoggingAllowed()) {
cmcdSession.setPlaybackRate(playbackRate);
}
CmcdStatus.Builder cmcdStatus =
new CmcdStatus.Builder().setCustomData(customData.get(CmcdConfiguration.KEY_CMCD_STATUS));
@ -249,6 +273,9 @@ public final class CmcdHeadersFactory {
cmcdStatus.setMaximumRequestedThroughputKbps(
cmcdConfiguration.requestConfig.getRequestedMaximumThroughputKbps(bitrateKbps));
}
if (cmcdConfiguration.isBufferStarvationLoggingAllowed()) {
cmcdStatus.setBufferStarvation(didRebuffer);
}
ImmutableMap.Builder<String, String> httpRequestHeaders = ImmutableMap.builder();
cmcdObject.build().populateHttpRequestHeaders(httpRequestHeaders);
@ -262,7 +289,10 @@ public final class CmcdHeadersFactory {
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 {
/** 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 {
/** Builder for {@link CmcdRequest} instances. */
public static final class Builder {
private long bufferLengthMs;
private long measuredThroughputInKbps;
private long deadlineMs;
private boolean startup;
@Nullable private String customData;
/** Creates a new instance with default values. */
public Builder() {
this.bufferLengthMs = C.TIME_UNSET;
this.measuredThroughputInKbps = Long.MIN_VALUE;
this.deadlineMs = C.TIME_UNSET;
}
/**
@ -458,6 +494,27 @@ public final class CmcdHeadersFactory {
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}. */
@CanIgnoreReturnValue
public Builder setCustomData(@Nullable String customData) {
@ -495,6 +552,23 @@ public final class CmcdHeadersFactory {
*/
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.
*
@ -506,6 +580,8 @@ public final class CmcdHeadersFactory {
private CmcdRequest(Builder builder) {
this.bufferLengthMs = builder.bufferLengthMs;
this.measuredThroughputInKbps = builder.measuredThroughputInKbps;
this.deadlineMs = builder.deadlineMs;
this.startup = builder.startup;
this.customData = builder.customData;
}
@ -527,6 +603,16 @@ public final class CmcdHeadersFactory {
Util.formatInvariant(
"%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)) {
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
* 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 {
@ -552,6 +638,7 @@ public final class CmcdHeadersFactory {
@Nullable private String sessionId;
@Nullable private @StreamingFormat String streamingFormat;
@Nullable private @StreamType String streamType;
private float playbackRate;
@Nullable private String customData;
/**
@ -596,6 +683,13 @@ public final class CmcdHeadersFactory {
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}. */
@CanIgnoreReturnValue
public Builder setCustomData(@Nullable String customData) {
@ -632,10 +726,9 @@ public final class CmcdHeadersFactory {
@Nullable public final String sessionId;
/**
* 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.
*
* <p>If the streaming format being requested is unknown, then this key MUST NOT be used.
* The streaming format that defines the current request, or{@code null} if unset. Must be one
* 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.
*/
@Nullable public final @StreamingFormat String streamingFormat;
@ -645,6 +738,11 @@ public final class CmcdHeadersFactory {
*/
@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
* session, or {@code null} if unset.
@ -659,6 +757,7 @@ public final class CmcdHeadersFactory {
this.sessionId = builder.sessionId;
this.streamingFormat = builder.streamingFormat;
this.streamType = builder.streamType;
this.playbackRate = builder.playbackRate;
this.customData = builder.customData;
}
@ -688,6 +787,10 @@ public final class CmcdHeadersFactory {
headerValue.append(
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) {
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 {
/** Builder for {@link CmcdStatus} instances. */
public static final class Builder {
private int maximumRequestedThroughputKbps;
private boolean bufferStarvation;
@Nullable private String customData;
/** Creates a new instance with default values. */
@ -740,6 +845,13 @@ public final class CmcdHeadersFactory {
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}. */
@CanIgnoreReturnValue
public Builder setCustomData(@Nullable String customData) {
@ -759,6 +871,13 @@ public final class CmcdHeadersFactory {
*/
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
* null} if unset.
@ -770,6 +889,7 @@ public final class CmcdHeadersFactory {
private CmcdStatus(Builder builder) {
this.maximumRequestedThroughputKbps = builder.maximumRequestedThroughputKbps;
this.bufferStarvation = builder.bufferStarvation;
this.customData = builder.customData;
}
@ -788,6 +908,9 @@ public final class CmcdHeadersFactory {
"%s=%d,",
CmcdConfiguration.KEY_MAXIMUM_REQUESTED_BITRATE, maximumRequestedThroughputKbps));
}
if (bufferStarvation) {
headerValue.append(CmcdConfiguration.KEY_BUFFER_STARVATION).append(",");
}
if (!TextUtils.isEmpty(customData)) {
headerValue.append(Util.formatInvariant("%s,", customData));
}

View File

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

View File

@ -162,6 +162,12 @@ public class DefaultDashChunkSource implements DashChunkSource {
@Nullable private IOException fatalError;
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
* chunks.
@ -215,6 +221,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
this.maxSegmentsPerLoad = maxSegmentsPerLoad;
this.playerTrackEmsgHandler = playerTrackEmsgHandler;
this.cmcdConfiguration = cmcdConfiguration;
this.lastChunkRequestRealtimeMs = C.TIME_UNSET;
long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
@ -371,7 +378,6 @@ public class DefaultDashChunkSource implements DashChunkSource {
long availableLiveDurationUs = getAvailableLiveDurationUs(nowUnixTimeUs, playbackPositionUs);
trackSelection.updateSelectedTrack(
playbackPositionUs, bufferedDurationUs, availableLiveDurationUs, queue, chunkIterators);
int selectedTrackIndex = trackSelection.getSelectedIndex();
@Nullable
@ -382,8 +388,13 @@ public class DefaultDashChunkSource implements DashChunkSource {
cmcdConfiguration,
trackSelection,
bufferedDurationUs,
/* playbackRate= */ loadingInfo.playbackSpeed,
/* 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);
if (representationHolder.chunkExtractor != null) {
Representation selectedRepresentation = representationHolder.representation;

View File

@ -310,7 +310,7 @@ public class DefaultDashChunkSourceTest {
ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(),
new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
output);
@ -320,9 +320,63 @@ public class DefaultDashChunkSourceTest {
"CMCD-Object",
"br=700,tb=1300,d=4000,ot=v",
"CMCD-Request",
"bl=0,mtp=1000",
"bl=0,mtp=1000,dl=0,su",
"CMCD-Session",
"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
@ -355,7 +409,7 @@ public class DefaultDashChunkSourceTest {
ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(),
new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
output);
@ -365,7 +419,7 @@ public class DefaultDashChunkSourceTest {
"CMCD-Object",
"br=700,tb=1300,d=4000,ot=v",
"CMCD-Request",
"bl=0,mtp=1000",
"bl=0,mtp=1000,dl=0,su",
"CMCD-Session",
"cid=\"mediaIdcontentIdSuffix\",sf=d,st=v",
"CMCD-Status",
@ -401,7 +455,7 @@ public class DefaultDashChunkSourceTest {
ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(),
new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
output);
@ -411,7 +465,7 @@ public class DefaultDashChunkSourceTest {
"CMCD-Object",
"br=700,tb=1300,d=4000,ot=v,key1=value1",
"CMCD-Request",
"bl=0,mtp=1000,key2=\"stringValue\"",
"bl=0,mtp=1000,dl=0,su,key2=\"stringValue\"",
"CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=d,st=v,key3=1",
"CMCD-Status",

View File

@ -153,6 +153,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private long liveEdgeInPeriodTimeUs;
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
* media chunks.
@ -194,6 +200,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.muxedCaptionFormats = muxedCaptionFormats;
this.playerId = playerId;
this.cmcdConfiguration = cmcdConfiguration;
this.lastChunkRequestRealtimeMs = C.TIME_UNSET;
keyCache = new FullSegmentEncryptionKeyCache(KEY_CACHE_SIZE);
scratchSpace = Util.EMPTY_BYTE_ARRAY;
liveEdgeInPeriodTimeUs = C.TIME_UNSET;
@ -489,12 +496,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
cmcdConfiguration,
trackSelection,
bufferedDurationUs,
/* playbackRate= */ loadingInfo.playbackSpeed,
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_HLS,
/* isLive= */ !playlist.hasEndTag)
/* isLive= */ !playlist.hasEndTag,
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
/* isBufferEmpty= */ queue.isEmpty())
.setObjectType(
getIsMuxedAudioAndVideo()
? CmcdHeadersFactory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO
: CmcdHeadersFactory.getObjectType(trackSelection));
lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();
// Check if the media segment or its initialization segment are fully encrypted.
@Nullable

View File

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

View File

@ -18,6 +18,7 @@ package androidx.media3.exoplayer.smoothstreaming;
import static androidx.media3.exoplayer.trackselection.TrackSelectionUtil.createFallbackOptions;
import android.net.Uri;
import android.os.SystemClock;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
@ -98,6 +99,12 @@ public class DefaultSsChunkSource implements SsChunkSource {
@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 manifest The initial manifest.
@ -119,6 +126,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
this.trackSelection = trackSelection;
this.dataSource = dataSource;
this.cmcdConfiguration = cmcdConfiguration;
this.lastChunkRequestRealtimeMs = C.TIME_UNSET;
StreamElement streamElement = manifest.streamElements[streamElementIndex];
chunkExtractors = new ChunkExtractor[trackSelection.length()];
@ -290,10 +298,14 @@ public class DefaultSsChunkSource implements SsChunkSource {
cmcdConfiguration,
trackSelection,
bufferedDurationUs,
/* playbackRate= */ loadingInfo.playbackSpeed,
/* streamingFormat= */ CmcdHeadersFactory.STREAMING_FORMAT_SS,
/* isLive= */ manifest.isLive)
/* isLive= */ manifest.isLive,
/* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
/* isBufferEmpty= */ queue.isEmpty())
.setChunkDurationUs(chunkEndTimeUs - chunkStartTimeUs)
.setObjectType(CmcdHeadersFactory.getObjectType(trackSelection));
lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();
out.chunk =
newMediaChunk(

View File

@ -18,6 +18,7 @@ package androidx.media3.exoplayer.smoothstreaming;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import android.os.SystemClock;
import androidx.annotation.Nullable;
import androidx.media3.common.Format;
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.SsManifestParser;
import androidx.media3.exoplayer.source.chunk.ChunkHolder;
import androidx.media3.exoplayer.source.chunk.MediaChunk;
import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
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.ImmutableMap;
import java.io.IOException;
import java.time.Duration;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowSystemClock;
/** Unit test for {@link DefaultSsChunkSource}. */
@RunWith(AndroidJUnit4.class)
@ -57,7 +61,7 @@ public class DefaultSsChunkSourceTest {
ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(),
new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
output);
@ -67,9 +71,63 @@ public class DefaultSsChunkSourceTest {
"CMCD-Object",
"br=308,tb=1536,d=1968,ot=v",
"CMCD-Request",
"bl=0,mtp=1000",
"bl=0,mtp=1000,dl=0,su",
"CMCD-Session",
"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
@ -102,7 +160,7 @@ public class DefaultSsChunkSourceTest {
ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(),
new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
output);
@ -112,7 +170,7 @@ public class DefaultSsChunkSourceTest {
"CMCD-Object",
"br=308,tb=1536,d=1968,ot=v",
"CMCD-Request",
"bl=0,mtp=1000",
"bl=0,mtp=1000,dl=0,su",
"CMCD-Session",
"cid=\"mediaIdcontentIdSuffix\",sf=s,st=v",
"CMCD-Status",
@ -148,7 +206,7 @@ public class DefaultSsChunkSourceTest {
ChunkHolder output = new ChunkHolder();
chunkSource.getNextChunk(
new LoadingInfo.Builder().setPlaybackPositionUs(0).build(),
new LoadingInfo.Builder().setPlaybackPositionUs(0).setPlaybackSpeed(1.0f).build(),
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
output);
@ -158,7 +216,7 @@ public class DefaultSsChunkSourceTest {
"CMCD-Object",
"br=308,tb=1536,d=1968,ot=v,key1=value1",
"CMCD-Request",
"bl=0,mtp=1000,key2=\"stringValue\"",
"bl=0,mtp=1000,dl=0,su,key2=\"stringValue\"",
"CMCD-Session",
"cid=\"mediaId\",sid=\"" + cmcdConfiguration.sessionId + "\",sf=s,st=v,key3=1",
"CMCD-Status",