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