diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/UdpDataSourceContractTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/UdpDataSourceContractTest.java new file mode 100644 index 0000000000..01bba22753 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/UdpDataSourceContractTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2020 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.upstream; + +import static java.lang.Math.min; + +import android.net.Uri; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.testutil.DataSourceContractTest; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import org.junit.Before; +import org.junit.runner.RunWith; + +/** {@link DataSource} contract tests for {@link UdpDataSource}. */ +@RunWith(AndroidJUnit4.class) +public class UdpDataSourceContractTest extends DataSourceContractTest { + + private UdpDataSource udpDataSource; + private byte[] data; + + @Before + public void setUp() { + udpDataSource = new UdpDataSource(); + // UDP is unreliable: it may lose, duplicate or re-order packets. We want to transmit more than + // one UDP packets to thoroughly test the UDP data source. We assume that UDP delivery within + // the same host is reliable. + int dataLength = (10 * 1024) + 512; // 10.5 KiB, not a round number by intention + data = TestUtil.buildTestData(dataLength); + PacketTrasmitterTransferListener transferListener = new PacketTrasmitterTransferListener(data); + udpDataSource.addTransferListener(transferListener); + } + + @Override + protected DataSource createDataSource() { + return udpDataSource; + } + + @Override + protected ImmutableList getTestResources() { + return ImmutableList.of( + new TestResource.Builder() + .setName("local-udp-unicast-socket") + .setUri(Uri.parse("udp://localhost:" + findFreeUdpPort())) + .setExpectedBytes(data) + .resolvesToUnknownLength() + .setEndOfInputExpected(false) + .build()); + } + + @Override + protected Uri getNotFoundUri() { + return Uri.parse("udp://notfound.invalid:12345"); + } + + /** + * A {@link TransferListener} that triggers UDP packet transmissions back to the UDP data source. + */ + private static class PacketTrasmitterTransferListener implements TransferListener { + private final byte[] data; + + public PacketTrasmitterTransferListener(byte[] data) { + this.data = data; + } + + @Override + public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) {} + + @Override + public void onTransferStart(DataSource source, DataSpec dataSpec, boolean isNetwork) { + String host = dataSpec.uri.getHost(); + int port = dataSpec.uri.getPort(); + try (DatagramSocket socket = new DatagramSocket()) { + // Split data in packets of up to 1024 bytes. + for (int offset = 0; offset < data.length; offset += 1024) { + int packetLength = min(1024, data.length - offset); + DatagramPacket packet = + new DatagramPacket(data, offset, packetLength, InetAddress.getByName(host), port); + socket.send(packet); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void onBytesTransferred( + DataSource source, DataSpec dataSpec, boolean isNetwork, int bytesTransferred) {} + + @Override + public void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) {} + } + + /** + * Finds a free UDP port in the range of unreserved ports 50000-60000 that can be used from the + * test or throws an {@link IllegalStateException} if no port is available. + * + *

There is no guarantee that the port returned will still be available as another process may + * occupy it in the mean time. + */ + private static int findFreeUdpPort() { + for (int i = 50000; i <= 60000; i++) { + try { + new DatagramSocket(i).close(); + return i; + } catch (SocketException e) { + // Port is occupied, continue to next port. + } + } + throw new IllegalStateException(); + } +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java index 707928c048..7a670f8aba 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java @@ -86,7 +86,11 @@ public abstract class DataSourceContractTest { DataSource dataSource = createDataSource(); try { long length = dataSource.open(new DataSpec(resource.getUri())); - byte[] data = Util.readToEnd(dataSource); + byte[] data = + resource.isEndOfInputExpected() + ? Util.readToEnd(dataSource) + : Util.readExactly(dataSource, resource.getExpectedBytes().length); + assertThat(length).isEqualTo(resource.getExpectedLength()); assertThat(data).isEqualTo(resource.getExpectedBytes()); } finally { @@ -124,13 +128,19 @@ public abstract class DataSourceContractTest { private final Uri uri; private final byte[] expectedBytes; private final boolean resolvesToKnownLength; + private final boolean endOfInputExpected; private TestResource( - @Nullable String name, Uri uri, byte[] expectedBytes, boolean resolvesToKnownLength) { + @Nullable String name, + Uri uri, + byte[] expectedBytes, + boolean resolvesToKnownLength, + boolean endOfInputExpected) { this.name = name; this.uri = uri; this.expectedBytes = expectedBytes; this.resolvesToKnownLength = resolvesToKnownLength; + this.endOfInputExpected = endOfInputExpected; } /** Returns a human-readable name for the resource, for use in test failure messages. */ @@ -159,16 +169,26 @@ public abstract class DataSourceContractTest { return resolvesToKnownLength ? expectedBytes.length : C.LENGTH_UNSET; } + /** + * Returns whether {@link DataSource#read} is expected to return {@link C#RESULT_END_OF_INPUT} + * after all the resource data are read. + */ + public boolean isEndOfInputExpected() { + return endOfInputExpected; + } + /** Builder for {@link TestResource} instances. */ public static final class Builder { private @MonotonicNonNull String name; private @MonotonicNonNull Uri uri; private byte @MonotonicNonNull [] expectedBytes; private boolean resolvesToKnownLength; + private boolean endOfInputExpected; /** Construct a new instance. */ public Builder() { this.resolvesToKnownLength = true; + this.endOfInputExpected = true; } /** @@ -201,9 +221,22 @@ public abstract class DataSourceContractTest { return this; } + /** + * Sets whether {@link DataSource#read} is expected to return {@link C#RESULT_END_OF_INPUT} + * after all the resource data have been read. By default, this is set to {@code true}. + */ + public Builder setEndOfInputExpected(boolean expected) { + this.endOfInputExpected = expected; + return this; + } + public TestResource build() { return new TestResource( - name, checkNotNull(uri), checkNotNull(expectedBytes), resolvesToKnownLength); + name, + checkNotNull(uri), + checkNotNull(expectedBytes), + resolvesToKnownLength, + endOfInputExpected); } } }