Skip played server side inserted ads in a single period window

This change makes sure played server side ads are skipped in a single period
timeline. It avoids creating an ad-MediaPeriodInfo for played postrolls and
creates a content info instead. It also sets the end position for content infos
that terminate the stream before the stream is actually finished. This prevents
the player from continue playing the remaining media delivered by the
MediaPeriod.

We also make sure that the discontinuity of played ads are not reported because
there is actually no discontinuity.

#minor-release

PiperOrigin-RevId: 428734387
This commit is contained in:
bachinger 2022-02-15 11:18:22 +00:00 committed by Ian Baker
parent 4dae8c75a8
commit d681e264aa
7 changed files with 218 additions and 59 deletions

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
@ -2153,14 +2154,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
// If we advance more than one period at a time, notify listeners after each update.
maybeNotifyPlaybackInfoChanged();
}
MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod();
MediaPeriodHolder newPlayingPeriodHolder = checkNotNull(queue.advancePlayingPeriod());
boolean isCancelledSSAIAdTransition =
playbackInfo.periodId.periodUid.equals(newPlayingPeriodHolder.info.id.periodUid)
&& playbackInfo.periodId.adGroupIndex == C.INDEX_UNSET
&& newPlayingPeriodHolder.info.id.adGroupIndex == C.INDEX_UNSET
&& playbackInfo.periodId.nextAdGroupIndex
!= newPlayingPeriodHolder.info.id.nextAdGroupIndex;
playbackInfo =
handlePositionDiscontinuity(
newPlayingPeriodHolder.info.id,
newPlayingPeriodHolder.info.startPositionUs,
newPlayingPeriodHolder.info.requestedContentPositionUs,
/* discontinuityStartPositionUs= */ newPlayingPeriodHolder.info.startPositionUs,
/* reportDiscontinuity= */ true,
/* reportDiscontinuity= */ !isCancelledSSAIAdTransition,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
resetPendingPauseAtEndOfPeriod();
updatePlaybackPositions();

View File

@ -39,9 +39,10 @@ import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
public final long requestedContentPositionUs;
/**
* The end position to which the media period's content is clipped in order to play a following ad
* group, in microseconds, or {@link C#TIME_UNSET} if there is no following ad group or if this
* media period is an ad. The value {@link C#TIME_END_OF_SOURCE} indicates that a postroll ad
* follows at the end of this content media period.
* group or to terminate a server side ad inserted stream before a played postroll, in
* microseconds, or {@link C#TIME_UNSET} if the content is not clipped or if this media period is
* an ad. The value {@link C#TIME_END_OF_SOURCE} indicates that a postroll ad follows at the end
* of this content media period.
*/
public final long endPositionUs;
/**

View File

@ -21,6 +21,7 @@ import static java.lang.Math.max;
import android.os.Handler;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.C;
import androidx.media3.common.Player.RepeatMode;
import androidx.media3.common.Timeline;
@ -801,8 +802,14 @@ import com.google.common.collect.ImmutableList;
} else {
// Play the next ad group if it's still available.
int adIndexInAdGroup = period.getFirstAdIndexToPlay(currentPeriodId.nextAdGroupIndex);
if (adIndexInAdGroup == period.getAdCountInAdGroup(currentPeriodId.nextAdGroupIndex)) {
// The next ad group has no ads left to play. Play content from the end position instead.
boolean isPlayedServerSideInsertedAd =
period.isServerSideInsertedAdGroup(currentPeriodId.nextAdGroupIndex)
&& period.getAdState(currentPeriodId.nextAdGroupIndex, adIndexInAdGroup)
== AdPlaybackState.AD_STATE_PLAYED;
if (adIndexInAdGroup == period.getAdCountInAdGroup(currentPeriodId.nextAdGroupIndex)
|| isPlayedServerSideInsertedAd) {
// The next ad group has no ads left to play or is a played SSAI ad group. Play content from
// the end position instead.
long startPositionUs =
getMinStartPositionAfterAdGroupUs(
timeline, currentPeriodId.periodUid, currentPeriodId.nextAdGroupIndex);
@ -888,6 +895,20 @@ import com.google.common.collect.ImmutableList;
long windowSequenceNumber) {
timeline.getPeriodByUid(periodUid, period);
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
boolean clipPeriodAtContentDuration = false;
if (nextAdGroupIndex == C.INDEX_UNSET) {
// Clip SSAI streams when at the end of the period.
clipPeriodAtContentDuration =
period.getAdGroupCount() > 0
&& period.isServerSideInsertedAdGroup(period.getRemovedAdGroupCount());
} else if (period.isServerSideInsertedAdGroup(nextAdGroupIndex)
&& period.getAdGroupTimeUs(nextAdGroupIndex) == period.durationUs) {
if (period.hasPlayedAdGroup(nextAdGroupIndex)) {
// Clip period before played SSAI post-rolls.
nextAdGroupIndex = C.INDEX_UNSET;
clipPeriodAtContentDuration = true;
}
}
MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);
boolean isLastInPeriod = isLastInPeriod(id);
boolean isLastInWindow = isLastInWindow(timeline, id);
@ -897,7 +918,7 @@ import com.google.common.collect.ImmutableList;
long endPositionUs =
nextAdGroupIndex != C.INDEX_UNSET
? period.getAdGroupTimeUs(nextAdGroupIndex)
: C.TIME_UNSET;
: clipPeriodAtContentDuration ? period.durationUs : C.TIME_UNSET;
long durationUs =
endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE
? period.durationUs

View File

@ -46,8 +46,11 @@ import static androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE;
import static androidx.media3.common.Player.COMMAND_SET_VOLUME;
import static androidx.media3.common.Player.COMMAND_STOP;
import static androidx.media3.common.Player.STATE_ENDED;
import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState;
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample;
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
import static androidx.media3.test.utils.TestUtil.assertTimelinesSame;
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilPosition;
@ -789,21 +792,8 @@ public final class ExoPlayerTest {
.getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true);
player.release();
// There is still one discontinuity from content to content for the failed ad insertion.
PositionInfo positionInfo =
new PositionInfo(
window.uid,
/* mediaItemIndex= */ 0,
window.mediaItem,
period.uid,
/* periodIndex= */ 0,
/* positionMs= */ 5_000,
/* contentPositionMs= */ 5_000,
/* adGroupIndex= */ C.INDEX_UNSET,
/* adIndexInAdGroup= */ C.INDEX_UNSET);
verify(mockListener)
.onPositionDiscontinuity(
positionInfo, positionInfo, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
// Content to content transition is ignored.
verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
}
@Test
@ -863,24 +853,7 @@ public final class ExoPlayerTest {
.getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true);
player.release();
// There is still one discontinuity from content to content for the failed ad insertion and the
// normal ad transition for the successful ad insertion.
PositionInfo positionInfoFailedAd =
new PositionInfo(
window.uid,
/* mediaItemIndex= */ 0,
window.mediaItem,
period.uid,
/* periodIndex= */ 0,
/* positionMs= */ 5_000,
/* contentPositionMs= */ 5_000,
/* adGroupIndex= */ C.INDEX_UNSET,
/* adIndexInAdGroup= */ C.INDEX_UNSET);
verify(mockListener)
.onPositionDiscontinuity(
positionInfoFailedAd,
positionInfoFailedAd,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
// There content to content discontinuity after the failed ad is suppressed.
PositionInfo positionInfoContentAtSuccessfulAd =
new PositionInfo(
window.uid,
@ -5029,10 +5002,9 @@ public final class ExoPlayerTest {
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
assertThat(reasonArgumentCaptor.getAllValues()).containsExactly(1, 2, 0, 0, 0, 0).inOrder();
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
assertThat(reasons).containsExactly(1, 2, 0, 0, 0, 0).inOrder();
// seek discontinuities
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
@ -5121,10 +5093,9 @@ public final class ExoPlayerTest {
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
assertThat(reasonArgumentCaptor.getAllValues()).containsExactly(1, 2, 0, 0, 0).inOrder();
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
assertThat(reasons).containsExactly(1, 2, 0, 0, 0).inOrder();
// seek
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
@ -5184,10 +5155,9 @@ public final class ExoPlayerTest {
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
assertThat(reasonArgumentCaptor.getAllValues()).containsExactly(1, 0, 0, 0, 0, 0).inOrder();
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
assertThat(reasons).containsExactly(1, 0, 0, 0, 0, 0).inOrder();
// seek discontinuity
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(newPositions.get(0).periodIndex).isEqualTo(0);
@ -5252,10 +5222,9 @@ public final class ExoPlayerTest {
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
assertThat(reasonArgumentCaptor.getAllValues()).containsExactly(1, 2, 0, 0, 0, 0).inOrder();
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
assertThat(reasons).containsExactly(1, 2, 0, 0, 0, 0).inOrder();
// seek discontinuity
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
@ -5334,10 +5303,9 @@ public final class ExoPlayerTest {
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
assertThat(reasonArgumentCaptor.getAllValues()).containsExactly(1).inOrder();
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
assertThat(reasons).containsExactly(1).inOrder();
// seek discontinuity
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
@ -5347,7 +5315,8 @@ public final class ExoPlayerTest {
}
@Test
public void play_playedSSAIPreMidPostRolls_skipsAllAds() throws Exception {
public void play_playedSSAIPreMidPostRollsMultiPeriodWindow_contentPeriodTransitionsOnly()
throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
@ -5371,7 +5340,7 @@ public final class ExoPlayerTest {
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
sourceReference.set(
new ServerSideAdInsertionMediaSource(
new FakeMediaSource(adTimeline),
new FakeMediaSource(adTimeline, ExoPlayerTestRunner.AUDIO_FORMAT),
contentTimeline -> {
sourceReference
.get()
@ -5385,16 +5354,18 @@ public final class ExoPlayerTest {
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
ArgumentCaptor<Integer> playbackStateCaptor = ArgumentCaptor.forClass(Integer.class);
verify(listener, times(3)).onPlaybackStateChanged(playbackStateCaptor.capture());
assertThat(playbackStateCaptor.getAllValues()).containsExactly(2, 3, 4).inOrder();
verify(listener, times(3))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
assertThat(reasonArgumentCaptor.getAllValues()).containsExactly(0, 0, 0).inOrder();
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
assertThat(reasons).containsExactly(0, 0, 0).inOrder();
// Auto discontinuity from the empty ad period to the first content period.
// Auto discontinuity from the empty pre-roll period to the first content period.
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(0).positionMs).isEqualTo(0);
@ -5407,7 +5378,7 @@ public final class ExoPlayerTest {
assertThat(newPositions.get(1).periodIndex).isEqualTo(4);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(1).positionMs).isEqualTo(1250);
// Auto discontinuity from the second content period to the last frame of the last postroll.
// Auto discontinuity from the second content period to the last frame of the last ad period.
assertThat(oldPositions.get(2).periodIndex).isEqualTo(4);
assertThat(oldPositions.get(2).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(2).periodIndex).isEqualTo(7);
@ -5415,6 +5386,86 @@ public final class ExoPlayerTest {
assertThat(newPositions.get(2).positionMs).isEqualTo(2500);
}
@Test
public void play_playedSSAIPreMidPostRollsSinglePeriodWindow_noDiscontinuities()
throws Exception {
AdPlaybackState adPlaybackState =
addAdGroupToAdPlaybackState(
new AdPlaybackState("adsId"),
/* fromPositionUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ C.MICROS_PER_SECOND);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US
+ (3 * C.MICROS_PER_SECOND),
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ C.MICROS_PER_SECOND);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US
+ (5 * C.MICROS_PER_SECOND),
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ C.MICROS_PER_SECOND);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US
+ (9 * C.MICROS_PER_SECOND),
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ C.MICROS_PER_SECOND);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup+ */ 0);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup+ */ 0);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 2, /* adIndexInAdGroup+ */ 0);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 3, /* adIndexInAdGroup+ */ 0);
FakeTimeline adTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
"windowId",
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ DEFAULT_WINDOW_DURATION_US,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
/* adPlaybackStates= */ ImmutableList.of(adPlaybackState),
MediaItem.EMPTY));
Listener listener = mock(Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(listener);
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
sourceReference.set(
new ServerSideAdInsertionMediaSource(
new FakeMediaSource(adTimeline, ExoPlayerTestRunner.AUDIO_FORMAT),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
long finalPositionMs = player.getCurrentPosition();
player.release();
assertThat(finalPositionMs).isEqualTo(6000);
verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt());
ArgumentCaptor<Integer> playbackStateCaptor = ArgumentCaptor.forClass(Integer.class);
verify(listener, times(3)).onPlaybackStateChanged(playbackStateCaptor.capture());
assertThat(playbackStateCaptor.getAllValues()).containsExactly(2, 3, 4).inOrder();
}
@Test
public void becomingNoisyIgnoredIfBecomingNoisyHandlingIsDisabled() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();

View File

@ -388,7 +388,7 @@ public final class MediaPeriodQueueTest {
/* periodUid= */ firstPeriodUid,
/* startPositionUs= */ SECOND_AD_START_TIME_US,
/* requestedContentPositionUs= */ SECOND_AD_START_TIME_US,
/* endPositionUs= */ C.TIME_UNSET,
/* endPositionUs= */ CONTENT_DURATION_US,
/* durationUs= */ CONTENT_DURATION_US,
/* isFollowedByTransitionToSameStream= */ false,
/* isLastInPeriod= */ true,

View File

@ -68,7 +68,7 @@ public final class ServerSideAdInsertionMediaSourceTest {
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
private static final String TEST_ASSET = "asset:///media/mp4/sample.mp4";
private static final String TEST_ASSET_DUMP = "playbackdumps/mp4/sample.mp4.dump";
private static final String TEST_ASSET_DUMP = "playbackdumps/mp4/ssai-sample.mp4.dump";
@Test
public void timeline_containsAdsDefinedInAdPlaybackState() throws Exception {

View File

@ -0,0 +1,79 @@
MediaCodecAdapter (exotest.audio.aac):
buffers.length = 44
buffers[0] = length 23, hash 47DE9131
buffers[1] = length 6, hash 31EC5206
buffers[2] = length 148, hash 894A176B
buffers[3] = length 189, hash CEF235A1
buffers[4] = length 205, hash BBF5F7B0
buffers[5] = length 210, hash F278B193
buffers[6] = length 210, hash 82DA1589
buffers[7] = length 207, hash 5BE231DF
buffers[8] = length 225, hash 18819EE1
buffers[9] = length 215, hash CA7FA67B
buffers[10] = length 211, hash 581A1C18
buffers[11] = length 216, hash ADB88187
buffers[12] = length 229, hash 2E8BA4DC
buffers[13] = length 232, hash 22F0C510
buffers[14] = length 235, hash 867AD0DC
buffers[15] = length 231, hash 84E823A8
buffers[16] = length 226, hash 1BEF3A95
buffers[17] = length 216, hash EAA345AE
buffers[18] = length 229, hash 6957411F
buffers[19] = length 219, hash 41275022
buffers[20] = length 241, hash 6495DF96
buffers[21] = length 228, hash 63D95906
buffers[22] = length 238, hash 34F676F9
buffers[23] = length 234, hash E5CBC045
buffers[24] = length 231, hash 5FC43661
buffers[25] = length 217, hash 682708ED
buffers[26] = length 239, hash D43780FC
buffers[27] = length 243, hash C5E17980
buffers[28] = length 231, hash AC5837BA
buffers[29] = length 230, hash 169EE895
buffers[30] = length 238, hash C48FF3F1
buffers[31] = length 225, hash 531E4599
buffers[32] = length 232, hash CB3C6B8D
buffers[33] = length 243, hash F8C94C7
buffers[34] = length 232, hash A646A7D0
buffers[35] = length 237, hash E8B787A5
buffers[36] = length 228, hash 3FA7A29F
buffers[37] = length 235, hash B9B33B0A
buffers[38] = length 264, hash 71A4869E
buffers[39] = length 257, hash D049B54C
buffers[40] = length 227, hash 66757231
buffers[41] = length 227, hash BD374F1B
buffers[42] = length 235, hash 999477F6
buffers[43] = length 0, hash 1
MediaCodecAdapter (exotest.video.avc):
buffers.length = 31
buffers[0] = length 36692, hash D216076E
buffers[1] = length 5312, hash D45D3CA0
buffers[2] = length 599, hash 1BE7812D
buffers[3] = length 7735, hash 4490F110
buffers[4] = length 987, hash 560B5036
buffers[5] = length 673, hash ED7CD8C7
buffers[6] = length 523, hash 3020DF50
buffers[7] = length 6061, hash 736C72B2
buffers[8] = length 992, hash FE132F23
buffers[9] = length 623, hash 5B2C1816
buffers[10] = length 421, hash 742E69C1
buffers[11] = length 4899, hash F72F86A1
buffers[12] = length 568, hash 519A8E50
buffers[13] = length 620, hash 3990AA39
buffers[14] = length 5450, hash F06EC4AA
buffers[15] = length 1051, hash 92DFA63A
buffers[16] = length 874, hash 69587FB4
buffers[17] = length 781, hash 36BE495B
buffers[18] = length 4725, hash AC0C8CD3
buffers[19] = length 1022, hash 5D8BFF34
buffers[20] = length 790, hash 99413A99
buffers[21] = length 610, hash 5E129290
buffers[22] = length 2751, hash 769974CB
buffers[23] = length 745, hash B78A477A
buffers[24] = length 621, hash CF741E7A
buffers[25] = length 505, hash 1DB4894E
buffers[26] = length 1268, hash C15348DC
buffers[27] = length 880, hash C2DE85D0
buffers[28] = length 530, hash C98BC6A8
buffers[29] = length 568, hash 4FE5C8EA
buffers[30] = length 0, hash 1