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
This commit is contained in:
claincly 2021-06-21 10:02:44 +01:00 committed by Oliver Woodman
parent 607fa8bf74
commit b05e8f5090
6 changed files with 554 additions and 41 deletions

View file

@ -486,13 +486,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void onPlaybackStarted(
long startPositionUs, ImmutableList<RtspTrackTiming> trackTimingList) {
// Validate that the trackTimingList contains timings for the selected tracks.
ArrayList<Uri> trackUrisWithTiming = new ArrayList<>(trackTimingList.size());
ArrayList<String> 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);

View file

@ -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;

View file

@ -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<String> 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}.
*
* <p>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);
}
}

View file

@ -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<Uri> 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<Throwable> 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<Uri> receivedSetupUris;
private final Clock clock;
private final ArrayList<RtpPacketStreamDump> dumpsForSetUpTracks;
private final ImmutableList<RtpPacketStreamDump> 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<RtpPacketStreamDump> rtpPacketStreamDumps) {
public ResponseProvider(
Clock clock,
List<RtpPacketStreamDump> 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<Uri> getReceivedSetupUris() {
return ImmutableList.copyOf(receivedSetupUris);
/** Returns a list of the received SETUP requests' corresponding {@link RtpPacketStreamDump}. */
public ImmutableList<RtpPacketStreamDump> 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<byte[]> 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;
}
}
}

View file

@ -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.
*
* <p>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);
}

View file

@ -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