From 4282a6ecd7253e1056a030e4e0b0810aa306cc04 Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 10 Aug 2023 17:32:30 +0000 Subject: [PATCH] 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 --- RELEASENOTES.md | 3 + .../media3/exoplayer/LoadingInfo.java | 13 ++ .../exoplayer/upstream/CmcdConfiguration.java | 42 +++++- .../upstream/CmcdHeadersFactory.java | 141 ++++++++++++++++-- .../upstream/CmcdHeadersFactoryTest.java | 11 +- .../dash/DefaultDashChunkSource.java | 15 +- .../dash/DefaultDashChunkSourceTest.java | 66 +++++++- .../media3/exoplayer/hls/HlsChunkSource.java | 13 +- .../exoplayer/hls/HlsChunkSourceTest.java | 82 +++++++++- .../smoothstreaming/DefaultSsChunkSource.java | 14 +- .../DefaultSsChunkSourceTest.java | 70 ++++++++- 11 files changed, 434 insertions(+), 36 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0feeb56828..258c8d9647 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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 diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadingInfo.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadingInfo.java index 82025e6c67..ebbd8f2b9b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadingInfo.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadingInfo.java @@ -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; + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java index 99796d8ef0..c39999a1a2 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdConfiguration.java @@ -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); + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java index 64f12cebcd..fdab76f0bb 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactory.java @@ -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 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. + * + *

This value MUST be rounded to the nearest 100 ms. For a playback rate of 1, this may be + * equivalent to the player’s 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. - * - *

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)); } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java index 1379f85348..bb37cde1be 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/CmcdHeadersFactoryTest.java @@ -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"); } } diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java index 1859825f87..3df13f588b 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java @@ -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; diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java index edfb19c8d5..f8dfaa685a 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java @@ -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", diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java index a6c111b741..fd8a19b278 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java @@ -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 diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java index 32f7ddbcfc..6d2e78cf78 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java @@ -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", diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java index 850ae1983f..63adf0848d 100644 --- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java +++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSource.java @@ -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( diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java index 574494ed9c..fe862051f3 100644 --- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java +++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultSsChunkSourceTest.java @@ -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",