From b05e8f509081d983cb1177933445336b0b53ffb5 Mon Sep 17 00:00:00 2001 From: claincly Date: Mon, 21 Jun 2021 10:02:44 +0100 Subject: [PATCH] Add RTP streaming test to playback test. The test prepare_withSupportedTrack_playsTrackUntilEnded - sets up the supported AAC track with the RTSP server; - uses RtpPacketTransmitter to send RTP packets from the server to the client; - runs the player until the playback has ended, and - asserts on the data RTSP has received and queued to the SampleQueue. In the test, it was necessary to create a FakeUdpDataSourceRtpDataChannel. The reason we cannot reuse TransferRtpDataChannel is, we rely on BlockingQueue.poll timeout to identify the end of an RTSP stream, but the time out mechanism is unstable in Robolectric. For example, when the timeout is set to 8,000 ms, the actual timeout occasionally happens after 2,000,000 ms (in FakeClock). PiperOrigin-RevId: 380528710 --- .../source/rtsp/RtspMediaPeriod.java | 8 +- .../source/rtsp/RtspMediaSource.java | 4 +- .../source/rtsp/RtpPacketTransmitter.java | 91 ++++++ .../source/rtsp/RtspPlaybackTest.java | 194 +++++++++--- .../exoplayer2/source/rtsp/RtspServer.java | 12 + .../test/assets/playbackdumps/rtsp/aac.dump | 286 ++++++++++++++++++ 6 files changed, 554 insertions(+), 41 deletions(-) create mode 100644 library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtpPacketTransmitter.java create mode 100644 testdata/src/test/assets/playbackdumps/rtsp/aac.dump diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriod.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriod.java index cb288fc427..3247ad72ea 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriod.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriod.java @@ -486,13 +486,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public void onPlaybackStarted( long startPositionUs, ImmutableList trackTimingList) { // Validate that the trackTimingList contains timings for the selected tracks. - ArrayList trackUrisWithTiming = new ArrayList<>(trackTimingList.size()); + ArrayList trackUrisWithTiming = new ArrayList<>(trackTimingList.size()); for (int i = 0; i < trackTimingList.size(); i++) { - trackUrisWithTiming.add(trackTimingList.get(i).uri); + trackUrisWithTiming.add(checkNotNull(trackTimingList.get(i).uri.getPath())); } for (int i = 0; i < selectedLoadInfos.size(); i++) { RtpLoadInfo loadInfo = selectedLoadInfos.get(i); - if (!trackUrisWithTiming.contains(loadInfo.getTrackUri())) { + if (!trackUrisWithTiming.contains(loadInfo.getTrackUri().getPath())) { playbackException = new RtspPlaybackException( "Server did not provide timing for track " + loadInfo.getTrackUri()); @@ -556,8 +556,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; RtspMediaTrack rtspMediaTrack = tracks.get(i); RtspLoaderWrapper loaderWrapper = new RtspLoaderWrapper(rtspMediaTrack, /* trackId= */ i, rtpDataChannelFactory); - loaderWrapper.startLoading(); rtspLoaderWrappers.add(loaderWrapper); + loaderWrapper.startLoading(); } listener.onSourceInfoRefreshed(timing); diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java index f6c9298b07..a17f3d37f1 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java @@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import android.net.Uri; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.MediaItem; @@ -192,7 +193,8 @@ public final class RtspMediaSource extends BaseMediaSource { private boolean timelineIsLive; private boolean timelineIsPlaceholder; - private RtspMediaSource( + @VisibleForTesting + /* package */ RtspMediaSource( MediaItem mediaItem, RtpDataChannel.Factory rtpDataChannelFactory, String userAgent) { this.mediaItem = mediaItem; this.rtpDataChannelFactory = rtpDataChannelFactory; diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtpPacketTransmitter.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtpPacketTransmitter.java new file mode 100644 index 0000000000..dfaaf01691 --- /dev/null +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtpPacketTransmitter.java @@ -0,0 +1,91 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.rtsp; + +import com.google.android.exoplayer2.source.rtsp.RtspMessageChannel.InterleavedBinaryDataListener; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.HandlerWrapper; +import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; + +/** Transmits media RTP packets periodically. */ +/* package */ final class RtpPacketTransmitter { + + private static final byte[] END_OF_STREAM = new byte[0]; + + private final ImmutableList packets; + private final HandlerWrapper transmissionHandler; + private final long transmissionIntervalMs; + + private RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener; + private int packetIndex; + private volatile boolean isTransmitting; + + /** + * Creates a new instance. + * + * @param rtpPacketStreamDump The {@link RtpPacketStreamDump} to provide RTP packets. + * @param clock The {@link Clock} to use. + */ + public RtpPacketTransmitter(RtpPacketStreamDump rtpPacketStreamDump, Clock clock) { + this.packets = ImmutableList.copyOf(rtpPacketStreamDump.packets); + this.transmissionHandler = + clock.createHandler(Util.getCurrentOrMainLooper(), /* callback= */ null); + this.transmissionIntervalMs = rtpPacketStreamDump.transmissionIntervalMs; + } + + /** + * Starts transmitting binary data to the {@link InterleavedBinaryDataListener}. + * + *

Calling this method after starting the transmission has no effect. + */ + public void startTransmitting(InterleavedBinaryDataListener binaryDataListener) { + if (isTransmitting) { + return; + } + + this.binaryDataListener = binaryDataListener; + packetIndex = 0; + isTransmitting = true; + transmissionHandler.post(this::transmitNextPacket); + } + + /** Stops transmitting, if transmitting has started. */ + private void stopTransmitting() { + if (!isTransmitting) { + return; + } + + signalEndOfStream(); + transmissionHandler.removeCallbacksAndMessages(/* token= */ null); + isTransmitting = false; + } + + private void transmitNextPacket() { + if (packetIndex == packets.size()) { + stopTransmitting(); + return; + } + + byte[] data = Util.getBytesFromHexString(packets.get(packetIndex++)); + binaryDataListener.onInterleavedBinaryDataReceived(data); + transmissionHandler.postDelayed(this::transmitNextPacket, transmissionIntervalMs); + } + + private void signalEndOfStream() { + binaryDataListener.onInterleavedBinaryDataReceived(END_OF_STREAM); + } +} diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java index 389a544a1c..926c24ac3c 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java @@ -15,21 +15,38 @@ */ package com.google.android.exoplayer2.source.rtsp; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; import static com.google.common.truth.Truth.assertThat; +import static java.lang.Math.min; +import android.content.Context; import android.net.Uri; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackException; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.Listener; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.robolectric.PlaybackOutput; import com.google.android.exoplayer2.robolectric.RobolectricUtil; import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig; +import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper; +import com.google.android.exoplayer2.testutil.CapturingRenderersFactory; +import com.google.android.exoplayer2.testutil.DumpFileAsserts; import com.google.android.exoplayer2.testutil.FakeClock; +import com.google.android.exoplayer2.upstream.BaseDataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Rule; @@ -48,13 +65,27 @@ public final class RtspPlaybackTest { "v=0\r\n" + "o=- 1606776316530225 1 IN IP4 127.0.0.1\r\n" + "s=Exoplayer test\r\n" - + "t=0 0\r\n" - + "a=range:npt=0-50.46\r\n"; + + "t=0 0\r\n"; + + private final Context applicationContext; + private final CapturingRenderersFactory capturingRenderersFactory; + private final Clock clock; + private final FakeUdpDataSourceRtpDataChannel fakeRtpDataChannel; + private final RtpDataChannel.Factory rtpDataChannelFactory; private RtpPacketStreamDump aacRtpPacketStreamDump; // ExoPlayer does not support extracting MP4A-LATM RTP payload at the moment. private RtpPacketStreamDump mp4aLatmRtpPacketStreamDump; + /** Creates a new instance. */ + public RtspPlaybackTest() { + applicationContext = ApplicationProvider.getApplicationContext(); + capturingRenderersFactory = new CapturingRenderersFactory(applicationContext); + clock = new FakeClock(/* isAutoAdvancing= */ true); + fakeRtpDataChannel = new FakeUdpDataSourceRtpDataChannel(); + rtpDataChannelFactory = trackId -> fakeRtpDataChannel; + } + @Rule public ShadowMediaCodecConfig mediaCodecConfig = ShadowMediaCodecConfig.forAllSupportedMimeTypes(); @@ -67,28 +98,39 @@ public final class RtspPlaybackTest { } @Test - public void prepare_withSupportedTrack_sendsPlayRequest() throws Exception { + public void prepare_withSupportedTrack_playsTrackUntilEnded() throws Exception { ResponseProvider responseProvider = - new ResponseProvider(ImmutableList.of(aacRtpPacketStreamDump, mp4aLatmRtpPacketStreamDump)); - try (RtspServer rtspServer = new RtspServer(responseProvider)) { + new ResponseProvider( + clock, + ImmutableList.of(aacRtpPacketStreamDump, mp4aLatmRtpPacketStreamDump), + fakeRtpDataChannel); - SimpleExoPlayer player = createSimpleExoPlayer(rtspServer.startAndGetPortNumber()); + try (RtspServer rtspServer = new RtspServer(responseProvider)) { + SimpleExoPlayer player = + createSimpleExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); + + PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); player.prepare(); - RobolectricUtil.runMainLooperUntil(responseProvider::hasReceivedPlayRequest); + player.play(); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); player.release(); // Only setup the supported track (aac). - ImmutableList receivedSetupUris = responseProvider.getReceivedSetupUris(); - assertThat(receivedSetupUris).hasSize(1); - assertThat(receivedSetupUris.get(0).toString()).contains(aacRtpPacketStreamDump.trackName); + assertThat(responseProvider.getDumpsForSetUpTracks()).containsExactly(aacRtpPacketStreamDump); + DumpFileAsserts.assertOutput( + applicationContext, playbackOutput, "playbackdumps/rtsp/aac.dump"); } } @Test public void prepare_noSupportedTrack_throwsPreparationError() throws Exception { + try (RtspServer rtspServer = - new RtspServer(new ResponseProvider(ImmutableList.of(mp4aLatmRtpPacketStreamDump)))) { - SimpleExoPlayer player = createSimpleExoPlayer(rtspServer.startAndGetPortNumber()); + new RtspServer( + new ResponseProvider( + clock, ImmutableList.of(mp4aLatmRtpPacketStreamDump), fakeRtpDataChannel))) { + SimpleExoPlayer player = + createSimpleExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); AtomicReference playbackError = new AtomicReference<>(); player.prepare(); @@ -109,18 +151,17 @@ public final class RtspPlaybackTest { } } - private static SimpleExoPlayer createSimpleExoPlayer(int serverRtspPortNumber) { + private SimpleExoPlayer createSimpleExoPlayer( + int serverRtspPortNumber, RtpDataChannel.Factory rtpDataChannelFactory) { SimpleExoPlayer player = - new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext()) - .setClock(new FakeClock(/* isAutoAdvancing= */ true)) + new SimpleExoPlayer.Builder(applicationContext, capturingRenderersFactory) + .setClock(clock) .build(); - player.setMediaSource( - new RtspMediaSource.Factory() - .setForceUseRtpTcp(true) - .setUserAgent("ExoPlayer:PlaybackTest") - .createMediaSource(MediaItem.fromUri(RtspTestUtils.getTestUri(serverRtspPortNumber)))); - + new RtspMediaSource( + MediaItem.fromUri(RtspTestUtils.getTestUri(serverRtspPortNumber)), + rtpDataChannelFactory, + "ExoPlayer:PlaybackTest")); return player; } @@ -128,29 +169,34 @@ public final class RtspPlaybackTest { private static final String SESSION_ID = "00000000"; - private final ArrayList receivedSetupUris; + private final Clock clock; + private final ArrayList dumpsForSetUpTracks; private final ImmutableList rtpPacketStreamDumps; + private final RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener; - private boolean hasReceivedPlayRequest; + private RtpPacketTransmitter packetTransmitter; /** * Creates a new instance. * + * @param clock The {@link Clock} used in the test. * @param rtpPacketStreamDumps A list of {@link RtpPacketStreamDump}. + * @param binaryDataListener A {@link RtspMessageChannel.InterleavedBinaryDataListener} to send + * RTP data. */ - public ResponseProvider(List rtpPacketStreamDumps) { + public ResponseProvider( + Clock clock, + List rtpPacketStreamDumps, + RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener) { + this.clock = clock; this.rtpPacketStreamDumps = ImmutableList.copyOf(rtpPacketStreamDumps); - receivedSetupUris = new ArrayList<>(); + this.binaryDataListener = binaryDataListener; + dumpsForSetUpTracks = new ArrayList<>(); } - /** Returns whether a PLAY request is received. */ - public boolean hasReceivedPlayRequest() { - return hasReceivedPlayRequest; - } - - /** Returns a list of the received SETUP requests' {@link Uri URIs}. */ - public ImmutableList getReceivedSetupUris() { - return ImmutableList.copyOf(receivedSetupUris); + /** Returns a list of the received SETUP requests' corresponding {@link RtpPacketStreamDump}. */ + public ImmutableList getDumpsForSetUpTracks() { + return ImmutableList.copyOf(dumpsForSetUpTracks); } // RtspServer.ResponseProvider implementation. Called on the main thread. @@ -160,7 +206,7 @@ public final class RtspPlaybackTest { return new RtspResponse( /* status= */ 200, new RtspHeaders.Builder() - .add(RtspHeaders.PUBLIC, "OPTIONS, DESCRIBE, SETUP, PLAY") + .add(RtspHeaders.PUBLIC, "OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN") .build()); } @@ -172,14 +218,22 @@ public final class RtspPlaybackTest { @Override public RtspResponse getSetupResponse(Uri requestedUri, RtspHeaders headers) { - receivedSetupUris.add(requestedUri); + for (RtpPacketStreamDump rtpPacketStreamDump : rtpPacketStreamDumps) { + if (requestedUri.toString().contains(rtpPacketStreamDump.trackName)) { + dumpsForSetUpTracks.add(rtpPacketStreamDump); + packetTransmitter = new RtpPacketTransmitter(rtpPacketStreamDump, clock); + } + } + return new RtspResponse( /* status= */ 200, headers.buildUpon().add(RtspHeaders.SESSION, SESSION_ID).build()); } @Override public RtspResponse getPlayResponse() { - hasReceivedPlayRequest = true; + checkStateNotNull(packetTransmitter); + packetTransmitter.startTransmitting(binaryDataListener); + return new RtspResponse( /* status= */ 200, new RtspHeaders.Builder() @@ -187,4 +241,72 @@ public final class RtspPlaybackTest { .build()); } } + + private static final class FakeUdpDataSourceRtpDataChannel extends BaseDataSource + implements RtpDataChannel, RtspMessageChannel.InterleavedBinaryDataListener { + + private static final int LOCAL_PORT = 40000; + + private final ConcurrentLinkedQueue packetQueue; + + public FakeUdpDataSourceRtpDataChannel() { + super(/* isNetwork= */ false); + packetQueue = new ConcurrentLinkedQueue<>(); + } + + @Override + public String getTransport() { + return Util.formatInvariant("RTP/AVP;unicast;client_port=%d-%d", LOCAL_PORT, LOCAL_PORT + 1); + } + + @Override + public int getLocalPort() { + return LOCAL_PORT; + } + + @Override + public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() { + return this; + } + + @Override + public void onInterleavedBinaryDataReceived(byte[] data) { + packetQueue.add(data); + } + + @Override + public long open(DataSpec dataSpec) { + return C.LENGTH_UNSET; + } + + @Nullable + @Override + public Uri getUri() { + return null; + } + + @Override + public void close() {} + + @Override + public int read(byte[] target, int offset, int length) throws IOException { + if (length == 0) { + return 0; + } + + @Nullable byte[] data = packetQueue.poll(); + if (data == null) { + return 0; + } + + if (data.length == 0) { + // Empty data signals the end of a packet stream. + throw new IOException(new SocketTimeoutException()); + } + + int byteToRead = min(length, data.length); + System.arraycopy(data, /* srcPos= */ 0, target, offset, byteToRead); + return byteToRead; + } + } } diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspServer.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspServer.java index 0799b26fa8..fe537fc720 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspServer.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspServer.java @@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_DESCR import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_OPTIONS; import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_PLAY; import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_SETUP; +import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_TEARDOWN; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import android.net.Uri; @@ -57,6 +58,11 @@ public final class RtspServer implements Closeable { default RtspResponse getPlayResponse() { return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED; } + + /** Returns an RTSP TEARDOWN {@link RtspResponse response}. */ + default RtspResponse getTearDownResponse() { + return new RtspResponse(/* status= */ 200, RtspHeaders.EMPTY); + } } private final Thread listenerThread; @@ -74,6 +80,8 @@ public final class RtspServer implements Closeable { * Creates a new instance. * *

The constructor must be called on a {@link Looper} thread. + * + * @param responseProvider A {@link ResponseProvider}. */ public RtspServer(ResponseProvider responseProvider) { listenerThread = @@ -146,6 +154,10 @@ public final class RtspServer implements Closeable { sendResponse(responseProvider.getPlayResponse(), cSeq); break; + case METHOD_TEARDOWN: + sendResponse(responseProvider.getTearDownResponse(), cSeq); + break; + default: sendResponse(RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED, cSeq); } diff --git a/testdata/src/test/assets/playbackdumps/rtsp/aac.dump b/testdata/src/test/assets/playbackdumps/rtsp/aac.dump new file mode 100644 index 0000000000..c50b32cc5f --- /dev/null +++ b/testdata/src/test/assets/playbackdumps/rtsp/aac.dump @@ -0,0 +1,286 @@ +MediaCodecAdapter (exotest.audio.aac): + buffers.length = 284 + buffers[0] = length 36, hash EED36674 + buffers[1] = length 26, hash 256CDD3D + buffers[2] = length 38, hash ED4094BD + buffers[3] = length 40, hash FFD23472 + buffers[4] = length 37, hash A2293009 + buffers[5] = length 38, hash 8C36283D + buffers[6] = length 35, hash F256915 + buffers[7] = length 37, hash 7F80CF43 + buffers[8] = length 38, hash F88BFC0F + buffers[9] = length 26, hash 550C49E4 + buffers[10] = length 41, hash 71EEAAE5 + buffers[11] = length 44, hash 9215D7AA + buffers[12] = length 35, hash 1309D87E + buffers[13] = length 39, hash 6A30D343 + buffers[14] = length 34, hash 5FA53182 + buffers[15] = length 38, hash 58D59B52 + buffers[16] = length 24, hash 849356B7 + buffers[17] = length 30, hash 9FAE444A + buffers[18] = length 42, hash B414B1D9 + buffers[19] = length 51, hash CBD93490 + buffers[20] = length 35, hash C512F8D9 + buffers[21] = length 45, hash CCB07699 + buffers[22] = length 31, hash 34F57BF7 + buffers[23] = length 36, hash 3544F773 + buffers[24] = length 31, hash 8C236A5F + buffers[25] = length 38, hash DBE133FF + buffers[26] = length 41, hash C00E24AB + buffers[27] = length 36, hash 121487C4 + buffers[28] = length 32, hash EF291869 + buffers[29] = length 32, hash A2C4CDAF + buffers[30] = length 39, hash 1156FC90 + buffers[31] = length 45, hash D4D622C2 + buffers[32] = length 40, hash D761872 + buffers[33] = length 42, hash 936490F5 + buffers[34] = length 36, hash 544D69C7 + buffers[35] = length 31, hash 8180FB77 + buffers[36] = length 19, hash E5243B14 + buffers[37] = length 19, hash E5243B14 + buffers[38] = length 45, hash 5C748139 + buffers[39] = length 36, hash 54396D7C + buffers[40] = length 41, hash CDB6B998 + buffers[41] = length 42, hash 2D7FF6FE + buffers[42] = length 32, hash A0CA3AFB + buffers[43] = length 19, hash E5243B14 + buffers[44] = length 36, hash 1297F5F5 + buffers[45] = length 31, hash 4D29514C + buffers[46] = length 29, hash 8694C757 + buffers[47] = length 30, hash E7FD80C4 + buffers[48] = length 42, hash 79A5EBAC + buffers[49] = length 31, hash 6DC3058A + buffers[50] = length 31, hash B60095E + buffers[51] = length 37, hash 280AFFEF + buffers[52] = length 44, hash D80B2555 + buffers[53] = length 37, hash 6A4DD8D8 + buffers[54] = length 37, hash E15C33E2 + buffers[55] = length 37, hash 420253A4 + buffers[56] = length 37, hash B7E14FBA + buffers[57] = length 31, hash C5C24212 + buffers[58] = length 35, hash 6321FED7 + buffers[59] = length 42, hash D68225E6 + buffers[60] = length 38, hash AA1361B9 + buffers[61] = length 36, hash 414DF7A2 + buffers[62] = length 36, hash EAA60525 + buffers[63] = length 37, hash BFA116DE + buffers[64] = length 32, hash C8395C78 + buffers[65] = length 39, hash D05426B3 + buffers[66] = length 41, hash 38D3A843 + buffers[67] = length 32, hash 9DBC7033 + buffers[68] = length 36, hash EDF2FD47 + buffers[69] = length 31, hash 6E14534F + buffers[70] = length 33, hash AB754D5A + buffers[71] = length 31, hash F9FBF5BC + buffers[72] = length 38, hash 22C1B765 + buffers[73] = length 35, hash 136544E2 + buffers[74] = length 19, hash E5243B14 + buffers[75] = length 27, hash FD3567F0 + buffers[76] = length 38, hash 35DC31B + buffers[77] = length 33, hash C1649146 + buffers[78] = length 29, hash 6CE4F357 + buffers[79] = length 40, hash 5A340368 + buffers[80] = length 37, hash F9043F2B + buffers[81] = length 31, hash 24FCED00 + buffers[82] = length 38, hash B0D3353 + buffers[83] = length 33, hash 8F7223DD + buffers[84] = length 32, hash 4F6C454F + buffers[85] = length 32, hash 47FFFEC8 + buffers[86] = length 34, hash 697BFF10 + buffers[87] = length 24, hash FAF4A2E8 + buffers[88] = length 34, hash E62D4517 + buffers[89] = length 42, hash 6F93846F + buffers[90] = length 33, hash 38FC39FE + buffers[91] = length 26, hash 6FB79344 + buffers[92] = length 31, hash 859B7AB6 + buffers[93] = length 36, hash 8FFC1DE6 + buffers[94] = length 37, hash 3B41B03E + buffers[95] = length 34, hash 16D99E00 + buffers[96] = length 35, hash 1CE3D79C + buffers[97] = length 35, hash 552D9CF8 + buffers[98] = length 34, hash F359AF6C + buffers[99] = length 31, hash 62D7F902 + buffers[100] = length 33, hash B3B3AADE + buffers[101] = length 45, hash B3E0AF4B + buffers[102] = length 37, hash 5B1D0555 + buffers[103] = length 29, hash 24D92EFE + buffers[104] = length 38, hash 6BB9B2CD + buffers[105] = length 38, hash 6047E787 + buffers[106] = length 39, hash F9A0C65A + buffers[107] = length 38, hash 123329EC + buffers[108] = length 35, hash FE4CF3F2 + buffers[109] = length 46, hash D048919B + buffers[110] = length 32, hash 8B2A0375 + buffers[111] = length 42, hash 20AEF4D6 + buffers[112] = length 39, hash 9F770F1C + buffers[113] = length 36, hash FF47BAA2 + buffers[114] = length 35, hash 40AD4BD1 + buffers[115] = length 34, hash 7696B100 + buffers[116] = length 34, hash 79F373 + buffers[117] = length 41, hash A7A2EFF0 + buffers[118] = length 31, hash BE1FE09 + buffers[119] = length 42, hash 89732AB1 + buffers[120] = length 26, hash E3CF8F37 + buffers[121] = length 24, hash 2BC4E0B9 + buffers[122] = length 38, hash C93835A6 + buffers[123] = length 26, hash C874B055 + buffers[124] = length 26, hash CE908616 + buffers[125] = length 32, hash 4D8EF77E + buffers[126] = length 26, hash 74CB204C + buffers[127] = length 26, hash 6D1B5106 + buffers[128] = length 46, hash 6C4E71B + buffers[129] = length 37, hash D7594E9B + buffers[130] = length 26, hash D684F069 + buffers[131] = length 31, hash 6C2CA1DC + buffers[132] = length 24, hash 84CC5D6 + buffers[133] = length 45, hash 4D6DC19F + buffers[134] = length 31, hash 7E2D9CD0 + buffers[135] = length 31, hash 4AD359B7 + buffers[136] = length 45, hash 6701BECE + buffers[137] = length 39, hash 250CEF5B + buffers[138] = length 41, hash BFC04AE3 + buffers[139] = length 33, hash 5487E86F + buffers[140] = length 28, hash 2C61A94 + buffers[141] = length 99, hash 79FE0B6D + buffers[142] = length 2411, hash 9C5FA32B + buffers[143] = length 3051, hash D9CD2931 + buffers[144] = length 3149, hash 884C49D + buffers[145] = length 2925, hash F4DBA01 + buffers[146] = length 1309, hash 5D9812FE + buffers[147] = length 1268, hash C54D64EE + buffers[148] = length 1166, hash 39B5F743 + buffers[149] = length 1164, hash 719E89C4 + buffers[150] = length 1084, hash E75D0B22 + buffers[151] = length 974, hash 36EF9FE6 + buffers[152] = length 978, hash 688C6FF + buffers[153] = length 1008, hash 96412F47 + buffers[154] = length 959, hash 164BAD74 + buffers[155] = length 952, hash 25513E7B + buffers[156] = length 831, hash B017BA4A + buffers[157] = length 978, hash 8FDBCA77 + buffers[158] = length 920, hash 65E1A511 + buffers[159] = length 965, hash 836B26F1 + buffers[160] = length 943, hash 59FDEBF + buffers[161] = length 861, hash 35A8D25A + buffers[162] = length 925, hash 7CDC1BA5 + buffers[163] = length 924, hash 74D7E6AB + buffers[164] = length 891, hash D3AE378A + buffers[165] = length 913, hash 464EE82C + buffers[166] = length 919, hash FE7E2D69 + buffers[167] = length 962, hash 714A1826 + buffers[168] = length 900, hash 6C43BAAD + buffers[169] = length 930, hash F5CB43C3 + buffers[170] = length 892, hash 5BB1BF43 + buffers[171] = length 924, hash 17EE5E5 + buffers[172] = length 905, hash 23DE71EA + buffers[173] = length 947, hash 93B88B98 + buffers[174] = length 908, hash 93C11E64 + buffers[175] = length 901, hash 7FB682FD + buffers[176] = length 847, hash FEA531D6 + buffers[177] = length 892, hash F5CC4A2 + buffers[178] = length 889, hash 8C4DBD81 + buffers[179] = length 920, hash E97965CF + buffers[180] = length 914, hash D497BDF7 + buffers[181] = length 916, hash A3FDDDC3 + buffers[182] = length 877, hash 2667D4F0 + buffers[183] = length 1065, hash 38AB7A57 + buffers[184] = length 998, hash 39D90A4C + buffers[185] = length 1004, hash 824B1E18 + buffers[186] = length 910, hash DA53C55A + buffers[187] = length 842, hash E4D5FF52 + buffers[188] = length 902, hash 8191ACBA + buffers[189] = length 851, hash BDC4F7CF + buffers[190] = length 881, hash DABBB1DA + buffers[191] = length 942, hash E4FA1D93 + buffers[192] = length 896, hash 3AD1120 + buffers[193] = length 903, hash 5841ACE + buffers[194] = length 883, hash FCD9B32A + buffers[195] = length 945, hash A5D31FA1 + buffers[196] = length 862, hash C8A923F3 + buffers[197] = length 842, hash CF9B5E68 + buffers[198] = length 862, hash 3366000B + buffers[199] = length 908, hash BF27023C + buffers[200] = length 927, hash F0D9A7AE + buffers[201] = length 785, hash 139BD0E4 + buffers[202] = length 971, hash 9FC0880 + buffers[203] = length 852, hash CC9CF9D9 + buffers[204] = length 949, hash 7956FFF5 + buffers[205] = length 1002, hash 418F6F68 + buffers[206] = length 925, hash 384F4521 + buffers[207] = length 831, hash 3D3AA7F0 + buffers[208] = length 910, hash 2376DCC3 + buffers[209] = length 931, hash DC853371 + buffers[210] = length 861, hash 43BA8398 + buffers[211] = length 897, hash 4297CCC2 + buffers[212] = length 979, hash AAC47F17 + buffers[213] = length 874, hash CC669B20 + buffers[214] = length 955, hash 90CF49B5 + buffers[215] = length 931, hash 7C4771D0 + buffers[216] = length 944, hash E40BA49F + buffers[217] = length 960, hash 74BAEAC6 + buffers[218] = length 919, hash B95AEB7F + buffers[219] = length 956, hash B9DC3369 + buffers[220] = length 936, hash 1749FA4 + buffers[221] = length 870, hash B807545E + buffers[222] = length 857, hash 204AF4FA + buffers[223] = length 971, hash 8A9721F9 + buffers[224] = length 834, hash 33E2475 + buffers[225] = length 927, hash 11590032 + buffers[226] = length 930, hash CCAB31A0 + buffers[227] = length 932, hash 6AB5681 + buffers[228] = length 912, hash F00EA504 + buffers[229] = length 900, hash FA6B36B4 + buffers[230] = length 901, hash 3C03269 + buffers[231] = length 899, hash 5C0A5964 + buffers[232] = length 933, hash 3B6634C1 + buffers[233] = length 889, hash 747CE87 + buffers[234] = length 902, hash 1AEF855C + buffers[235] = length 944, hash 2D9A1961 + buffers[236] = length 944, hash 869E8001 + buffers[237] = length 825, hash 444C183D + buffers[238] = length 888, hash C57DAA6F + buffers[239] = length 906, hash FFEE0A27 + buffers[240] = length 901, hash 247E01D0 + buffers[241] = length 908, hash 3033ECFE + buffers[242] = length 821, hash 185DE244 + buffers[243] = length 903, hash 93796A7C + buffers[244] = length 937, hash 92F26AC + buffers[245] = length 905, hash 777EE22C + buffers[246] = length 919, hash E8CDE87E + buffers[247] = length 903, hash 33B11DBB + buffers[248] = length 901, hash D171136B + buffers[249] = length 930, hash B27209D7 + buffers[250] = length 918, hash A15B0F67 + buffers[251] = length 889, hash 76BF964C + buffers[252] = length 947, hash EE97BD3C + buffers[253] = length 883, hash 8E73F561 + buffers[254] = length 905, hash B8623586 + buffers[255] = length 962, hash C4BF2311 + buffers[256] = length 832, hash C2EA800 + buffers[257] = length 875, hash 66A9A7F6 + buffers[258] = length 916, hash 6D7E660E + buffers[259] = length 963, hash C3C5D78F + buffers[260] = length 785, hash 691D7FA8 + buffers[261] = length 986, hash C0616C8D + buffers[262] = length 941, hash F93D01C6 + buffers[263] = length 910, hash 8FF68C75 + buffers[264] = length 934, hash 3ED7FA43 + buffers[265] = length 917, hash E1E4EF2 + buffers[266] = length 882, hash 4265D6A3 + buffers[267] = length 853, hash F298C9E7 + buffers[268] = length 957, hash EEB18CE0 + buffers[269] = length 965, hash 50782418 + buffers[270] = length 883, hash 4DA28F02 + buffers[271] = length 878, hash CAEF1FF4 + buffers[272] = length 932, hash 402333BB + buffers[273] = length 902, hash 754919C8 + buffers[274] = length 931, hash CF109A99 + buffers[275] = length 907, hash A86AB690 + buffers[276] = length 887, hash ED548C44 + buffers[277] = length 925, hash 8D55A9C3 + buffers[278] = length 935, hash 10309516 + buffers[279] = length 868, hash C745F4A8 + buffers[280] = length 953, hash 19EFA951 + buffers[281] = length 875, hash CB2F1D4F + buffers[282] = length 872, hash 9CE1CA86 + buffers[283] = length 0, hash 1